summaryrefslogtreecommitdiffstats
path: root/core/java/android
diff options
context:
space:
mode:
Diffstat (limited to 'core/java/android')
-rw-r--r--core/java/android/accounts/AccountMonitor.java150
-rw-r--r--core/java/android/accounts/AccountMonitorListener.java29
-rw-r--r--core/java/android/accounts/AccountsServiceConstants.java78
-rw-r--r--core/java/android/accounts/IAccountsService.aidl54
-rwxr-xr-xcore/java/android/accounts/package.html5
-rw-r--r--core/java/android/annotation/SdkConstant.java36
-rw-r--r--core/java/android/annotation/Widget.java37
-rw-r--r--core/java/android/app/Activity.java3419
-rw-r--r--core/java/android/app/ActivityGroup.java111
-rw-r--r--core/java/android/app/ActivityManager.java604
-rw-r--r--core/java/android/app/ActivityManagerNative.java2054
-rw-r--r--core/java/android/app/ActivityThread.java3754
-rw-r--r--core/java/android/app/AlarmManager.java214
-rw-r--r--core/java/android/app/AlertDialog.java598
-rw-r--r--core/java/android/app/AliasActivity.java123
-rw-r--r--core/java/android/app/Application.java74
-rw-r--r--core/java/android/app/ApplicationContext.java2596
-rw-r--r--core/java/android/app/ApplicationLoaders.java72
-rw-r--r--core/java/android/app/ApplicationThreadNative.java652
-rw-r--r--core/java/android/app/DatePickerDialog.java185
-rw-r--r--core/java/android/app/Dialog.java935
-rw-r--r--core/java/android/app/ExpandableListActivity.java323
-rw-r--r--core/java/android/app/IActivityManager.java353
-rw-r--r--core/java/android/app/IActivityPendingResult.aidl27
-rw-r--r--core/java/android/app/IActivityWatcher.aidl55
-rwxr-xr-xcore/java/android/app/IAlarmManager.aidl33
-rw-r--r--core/java/android/app/IApplicationThread.java118
-rw-r--r--core/java/android/app/IInstrumentationWatcher.aidl31
-rwxr-xr-xcore/java/android/app/IIntentReceiver.aidl33
-rw-r--r--core/java/android/app/IIntentSender.aidl27
-rw-r--r--core/java/android/app/INotificationManager.aidl34
-rw-r--r--core/java/android/app/ISearchManager.aidl25
-rw-r--r--core/java/android/app/IServiceConnection.aidl26
-rw-r--r--core/java/android/app/IStatusBar.aidl29
-rwxr-xr-xcore/java/android/app/IThumbnailReceiver.aidl30
-rw-r--r--core/java/android/app/ITransientNotification.aidl25
-rw-r--r--core/java/android/app/IWallpaperService.aidl55
-rw-r--r--core/java/android/app/IWallpaperServiceCallback.aidl31
-rw-r--r--core/java/android/app/Instrumentation.java1601
-rw-r--r--core/java/android/app/KeyguardManager.java155
-rw-r--r--core/java/android/app/LauncherActivity.java226
-rw-r--r--core/java/android/app/ListActivity.java316
-rw-r--r--core/java/android/app/LocalActivityManager.java597
-rw-r--r--core/java/android/app/Notification.aidl19
-rw-r--r--core/java/android/app/Notification.java482
-rw-r--r--core/java/android/app/NotificationManager.java136
-rw-r--r--core/java/android/app/PendingIntent.aidl20
-rw-r--r--core/java/android/app/PendingIntent.java489
-rw-r--r--core/java/android/app/ProgressDialog.java301
-rw-r--r--core/java/android/app/ResultInfo.java86
-rw-r--r--core/java/android/app/SearchDialog.java1606
-rw-r--r--core/java/android/app/SearchManager.java1328
-rw-r--r--core/java/android/app/Service.java378
-rw-r--r--core/java/android/app/StatusBarManager.java139
-rw-r--r--core/java/android/app/TabActivity.java148
-rw-r--r--core/java/android/app/TimePickerDialog.java162
-rw-r--r--core/java/android/app/package.html72
-rw-r--r--core/java/android/bluetooth/AtCommandHandler.java93
-rw-r--r--core/java/android/bluetooth/AtCommandResult.java117
-rw-r--r--core/java/android/bluetooth/AtParser.java370
-rw-r--r--core/java/android/bluetooth/BluetoothAudioGateway.java190
-rw-r--r--core/java/android/bluetooth/BluetoothDevice.java601
-rw-r--r--core/java/android/bluetooth/BluetoothHeadset.java198
-rw-r--r--core/java/android/bluetooth/BluetoothIntent.java118
-rw-r--r--core/java/android/bluetooth/Database.java200
-rw-r--r--core/java/android/bluetooth/DeviceClass.java131
-rw-r--r--core/java/android/bluetooth/HeadsetBase.java310
-rw-r--r--core/java/android/bluetooth/IBluetoothDevice.aidl85
-rw-r--r--core/java/android/bluetooth/IBluetoothDeviceCallback.aidl28
-rw-r--r--core/java/android/bluetooth/IBluetoothHeadset.aidl42
-rw-r--r--core/java/android/bluetooth/IBluetoothHeadsetCallback.aidl25
-rw-r--r--core/java/android/bluetooth/RfcommSocket.java674
-rw-r--r--core/java/android/bluetooth/ScoSocket.java201
-rw-r--r--core/java/android/bluetooth/package.html14
-rw-r--r--core/java/android/content/AbstractTableMerger.java577
-rw-r--r--core/java/android/content/ActivityNotFoundException.java35
-rw-r--r--core/java/android/content/AsyncQueryHandler.java338
-rw-r--r--core/java/android/content/BroadcastReceiver.java399
-rw-r--r--core/java/android/content/ComponentCallbacks.java54
-rw-r--r--core/java/android/content/ComponentName.aidl19
-rw-r--r--core/java/android/content/ComponentName.java276
-rw-r--r--core/java/android/content/ContentInsertHandler.java50
-rw-r--r--core/java/android/content/ContentProvider.java562
-rw-r--r--core/java/android/content/ContentProviderNative.java435
-rw-r--r--core/java/android/content/ContentQueryMap.java172
-rw-r--r--core/java/android/content/ContentResolver.java644
-rw-r--r--core/java/android/content/ContentService.java376
-rw-r--r--core/java/android/content/ContentServiceNative.java209
-rw-r--r--core/java/android/content/ContentUris.java67
-rw-r--r--core/java/android/content/ContentValues.java501
-rw-r--r--core/java/android/content/Context.java1631
-rw-r--r--core/java/android/content/ContextWrapper.java422
-rw-r--r--core/java/android/content/DefaultDataHandler.java257
-rw-r--r--core/java/android/content/DialogInterface.java113
-rw-r--r--core/java/android/content/Formatter.java66
-rw-r--r--core/java/android/content/IContentProvider.java68
-rw-r--r--core/java/android/content/IContentService.java53
-rw-r--r--core/java/android/content/ISyncAdapter.aidl43
-rw-r--r--core/java/android/content/ISyncContext.aidl38
-rw-r--r--core/java/android/content/Intent.aidl20
-rw-r--r--core/java/android/content/Intent.java4391
-rw-r--r--core/java/android/content/IntentFilter.aidl19
-rw-r--r--core/java/android/content/IntentFilter.java1408
-rw-r--r--core/java/android/content/MutableContextWrapper.java38
-rw-r--r--core/java/android/content/ReceiverCallNotAllowedException.java32
-rw-r--r--core/java/android/content/SearchRecentSuggestionsProvider.java385
-rw-r--r--core/java/android/content/ServiceConnection.java53
-rw-r--r--core/java/android/content/SharedPreferences.java282
-rw-r--r--core/java/android/content/SyncAdapter.java70
-rw-r--r--core/java/android/content/SyncContext.java73
-rw-r--r--core/java/android/content/SyncManager.java2170
-rw-r--r--core/java/android/content/SyncProvider.java53
-rw-r--r--core/java/android/content/SyncResult.aidl19
-rw-r--r--core/java/android/content/SyncResult.java178
-rw-r--r--core/java/android/content/SyncStateContentProviderHelper.java234
-rw-r--r--core/java/android/content/SyncStats.aidl19
-rw-r--r--core/java/android/content/SyncStats.java112
-rw-r--r--core/java/android/content/SyncStorageEngine.java758
-rw-r--r--core/java/android/content/SyncUIContext.java37
-rw-r--r--core/java/android/content/SyncableContentProvider.java620
-rw-r--r--core/java/android/content/TempProviderSyncAdapter.java546
-rw-r--r--core/java/android/content/TempProviderSyncResult.java36
-rw-r--r--core/java/android/content/UriMatcher.java262
-rw-r--r--core/java/android/content/package.html649
-rwxr-xr-xcore/java/android/content/pm/ActivityInfo.aidl20
-rw-r--r--core/java/android/content/pm/ActivityInfo.java332
-rwxr-xr-xcore/java/android/content/pm/ApplicationInfo.aidl20
-rw-r--r--core/java/android/content/pm/ApplicationInfo.java303
-rw-r--r--core/java/android/content/pm/ComponentInfo.java138
-rwxr-xr-xcore/java/android/content/pm/IPackageDataObserver.aidl28
-rw-r--r--core/java/android/content/pm/IPackageDeleteObserver.aidl28
-rw-r--r--core/java/android/content/pm/IPackageInstallObserver.aidl27
-rw-r--r--core/java/android/content/pm/IPackageManager.aidl230
-rwxr-xr-xcore/java/android/content/pm/IPackageStatsObserver.aidl30
-rwxr-xr-xcore/java/android/content/pm/InstrumentationInfo.aidl20
-rw-r--r--core/java/android/content/pm/InstrumentationInfo.java96
-rwxr-xr-xcore/java/android/content/pm/PackageInfo.aidl20
-rw-r--r--core/java/android/content/pm/PackageInfo.java170
-rw-r--r--core/java/android/content/pm/PackageItemInfo.java188
-rw-r--r--core/java/android/content/pm/PackageManager.java1453
-rw-r--r--core/java/android/content/pm/PackageParser.java2287
-rwxr-xr-xcore/java/android/content/pm/PackageStats.aidl20
-rwxr-xr-xcore/java/android/content/pm/PackageStats.java63
-rwxr-xr-xcore/java/android/content/pm/PermissionGroupInfo.aidl20
-rw-r--r--core/java/android/content/pm/PermissionGroupInfo.java108
-rwxr-xr-xcore/java/android/content/pm/PermissionInfo.aidl20
-rw-r--r--core/java/android/content/pm/PermissionInfo.java156
-rwxr-xr-xcore/java/android/content/pm/ProviderInfo.aidl20
-rw-r--r--core/java/android/content/pm/ProviderInfo.java129
-rwxr-xr-xcore/java/android/content/pm/ResolveInfo.aidl20
-rw-r--r--core/java/android/content/pm/ResolveInfo.java280
-rwxr-xr-xcore/java/android/content/pm/ServiceInfo.aidl20
-rw-r--r--core/java/android/content/pm/ServiceInfo.java56
-rwxr-xr-xcore/java/android/content/pm/Signature.aidl20
-rw-r--r--core/java/android/content/pm/Signature.java158
-rw-r--r--core/java/android/content/pm/package.html7
-rw-r--r--core/java/android/content/res/AssetFileDescriptor.java81
-rw-r--r--core/java/android/content/res/AssetManager.java697
-rw-r--r--core/java/android/content/res/ColorStateList.java328
-rwxr-xr-xcore/java/android/content/res/Configuration.aidl21
-rw-r--r--core/java/android/content/res/Configuration.java386
-rw-r--r--core/java/android/content/res/PluralRules.java111
-rw-r--r--core/java/android/content/res/Resources.java1659
-rw-r--r--core/java/android/content/res/StringBlock.java330
-rw-r--r--core/java/android/content/res/TypedArray.java660
-rw-r--r--core/java/android/content/res/XmlBlock.java515
-rw-r--r--core/java/android/content/res/XmlResourceParser.java36
-rw-r--r--core/java/android/content/res/package.html8
-rw-r--r--core/java/android/database/AbstractCursor.java617
-rw-r--r--core/java/android/database/AbstractWindowedCursor.java204
-rw-r--r--core/java/android/database/BulkCursorNative.java440
-rw-r--r--core/java/android/database/BulkCursorToCursorAdaptor.java256
-rw-r--r--core/java/android/database/CharArrayBuffer.java33
-rw-r--r--core/java/android/database/ContentObservable.java56
-rw-r--r--core/java/android/database/ContentObserver.java138
-rw-r--r--core/java/android/database/CrossProcessCursor.java42
-rw-r--r--core/java/android/database/Cursor.java587
-rw-r--r--core/java/android/database/CursorIndexOutOfBoundsException.java31
-rw-r--r--core/java/android/database/CursorJoiner.java265
-rw-r--r--core/java/android/database/CursorToBulkCursorAdaptor.java233
-rw-r--r--core/java/android/database/CursorWindow.java478
-rw-r--r--core/java/android/database/CursorWrapper.java305
-rw-r--r--core/java/android/database/DataSetObservable.java47
-rw-r--r--core/java/android/database/DataSetObserver.java41
-rw-r--r--core/java/android/database/DatabaseUtils.java1002
-rw-r--r--core/java/android/database/IBulkCursor.java90
-rwxr-xr-xcore/java/android/database/IContentObserver.aidl31
-rw-r--r--core/java/android/database/MatrixCursor.java267
-rw-r--r--core/java/android/database/MergeCursor.java257
-rw-r--r--core/java/android/database/Observable.java78
-rw-r--r--core/java/android/database/SQLException.java30
-rw-r--r--core/java/android/database/StaleDataException.java34
-rw-r--r--core/java/android/database/package.html14
-rw-r--r--core/java/android/database/sqlite/SQLiteAbortException.java30
-rw-r--r--core/java/android/database/sqlite/SQLiteClosable.java55
-rw-r--r--core/java/android/database/sqlite/SQLiteConstraintException.java28
-rw-r--r--core/java/android/database/sqlite/SQLiteCursor.java450
-rw-r--r--core/java/android/database/sqlite/SQLiteCursorDriver.java58
-rw-r--r--core/java/android/database/sqlite/SQLiteDatabase.java1512
-rw-r--r--core/java/android/database/sqlite/SQLiteDatabaseCorruptException.java28
-rw-r--r--core/java/android/database/sqlite/SQLiteDebug.java107
-rw-r--r--core/java/android/database/sqlite/SQLiteDirectCursorDriver.java97
-rw-r--r--core/java/android/database/sqlite/SQLiteDiskIOException.java29
-rw-r--r--core/java/android/database/sqlite/SQLiteDoneException.java31
-rw-r--r--core/java/android/database/sqlite/SQLiteException.java30
-rw-r--r--core/java/android/database/sqlite/SQLiteFullException.java28
-rw-r--r--core/java/android/database/sqlite/SQLiteMisuseException.java25
-rw-r--r--core/java/android/database/sqlite/SQLiteOpenHelper.java229
-rw-r--r--core/java/android/database/sqlite/SQLiteProgram.java262
-rw-r--r--core/java/android/database/sqlite/SQLiteQuery.java182
-rw-r--r--core/java/android/database/sqlite/SQLiteQueryBuilder.java520
-rw-r--r--core/java/android/database/sqlite/SQLiteStatement.java118
-rw-r--r--core/java/android/database/sqlite/package.html20
-rw-r--r--core/java/android/ddm/DdmHandleAppName.java104
-rw-r--r--core/java/android/ddm/DdmHandleExit.java78
-rw-r--r--core/java/android/ddm/DdmHandleHeap.java194
-rw-r--r--core/java/android/ddm/DdmHandleHello.java138
-rw-r--r--core/java/android/ddm/DdmHandleNativeHeap.java92
-rw-r--r--core/java/android/ddm/DdmHandleThread.java182
-rw-r--r--core/java/android/ddm/DdmRegister.java58
-rw-r--r--core/java/android/ddm/README.txt6
-rwxr-xr-xcore/java/android/ddm/package.html5
-rw-r--r--core/java/android/debug/JNITest.java48
-rwxr-xr-xcore/java/android/debug/package.html5
-rw-r--r--core/java/android/hardware/Camera.java633
-rw-r--r--core/java/android/hardware/ISensorService.aidl30
-rw-r--r--core/java/android/hardware/SensorListener.java46
-rw-r--r--core/java/android/hardware/SensorManager.java619
-rw-r--r--core/java/android/hardware/package.html5
-rw-r--r--core/java/android/net/ConnectivityManager.java243
-rw-r--r--core/java/android/net/Credentials.java48
-rw-r--r--core/java/android/net/DhcpInfo.aidl19
-rw-r--r--core/java/android/net/DhcpInfo.java96
-rw-r--r--core/java/android/net/IConnectivityManager.aidl47
-rw-r--r--core/java/android/net/LocalServerSocket.java117
-rw-r--r--core/java/android/net/LocalSocket.java288
-rw-r--r--core/java/android/net/LocalSocketAddress.java100
-rw-r--r--core/java/android/net/LocalSocketImpl.java490
-rw-r--r--core/java/android/net/MailTo.java172
-rw-r--r--core/java/android/net/MobileDataStateTracker.java479
-rw-r--r--core/java/android/net/NetworkConnectivityListener.java220
-rw-r--r--core/java/android/net/NetworkInfo.aidl19
-rw-r--r--core/java/android/net/NetworkInfo.java305
-rw-r--r--core/java/android/net/NetworkStateTracker.java306
-rw-r--r--core/java/android/net/NetworkUtils.java120
-rw-r--r--core/java/android/net/ParseException.java30
-rw-r--r--core/java/android/net/Proxy.java120
-rw-r--r--core/java/android/net/SSLCertificateSocketFactory.java254
-rw-r--r--core/java/android/net/SntpClient.java201
-rwxr-xr-xcore/java/android/net/Uri.aidl19
-rw-r--r--core/java/android/net/Uri.java2251
-rw-r--r--core/java/android/net/UrlQuerySanitizer.java913
-rw-r--r--core/java/android/net/WebAddress.java134
-rw-r--r--core/java/android/net/http/AndroidHttpClient.java452
-rw-r--r--core/java/android/net/http/AndroidHttpClientConnection.java464
-rw-r--r--core/java/android/net/http/CertificateChainValidator.java444
-rw-r--r--core/java/android/net/http/CertificateValidatorCache.java254
-rw-r--r--core/java/android/net/http/CharArrayBuffers.java89
-rw-r--r--core/java/android/net/http/Connection.java523
-rw-r--r--core/java/android/net/http/ConnectionThread.java137
-rw-r--r--core/java/android/net/http/DomainNameChecker.java277
-rw-r--r--core/java/android/net/http/EventHandler.java147
-rw-r--r--core/java/android/net/http/Headers.java384
-rw-r--r--core/java/android/net/http/HttpAuthHeader.java422
-rw-r--r--core/java/android/net/http/HttpConnection.java96
-rw-r--r--core/java/android/net/http/HttpLog.java44
-rw-r--r--core/java/android/net/http/HttpsConnection.java427
-rw-r--r--core/java/android/net/http/IdleCache.java175
-rw-r--r--core/java/android/net/http/LoggingEventHandler.java90
-rw-r--r--core/java/android/net/http/Request.java456
-rw-r--r--core/java/android/net/http/RequestFeeder.java42
-rw-r--r--core/java/android/net/http/RequestHandle.java402
-rw-r--r--core/java/android/net/http/RequestQueue.java647
-rw-r--r--core/java/android/net/http/SslCertificate.java251
-rw-r--r--core/java/android/net/http/SslError.java144
-rw-r--r--core/java/android/net/http/Timer.java41
-rwxr-xr-xcore/java/android/net/http/package.html2
-rwxr-xr-xcore/java/android/net/package.html5
-rw-r--r--core/java/android/os/AsyncResult.java68
-rw-r--r--core/java/android/os/BadParcelableException.java31
-rw-r--r--core/java/android/os/Base64Utils.java31
-rw-r--r--core/java/android/os/BatteryManager.java44
-rw-r--r--core/java/android/os/Binder.java333
-rw-r--r--core/java/android/os/Broadcaster.java212
-rw-r--r--core/java/android/os/Build.java89
-rw-r--r--core/java/android/os/Bundle.aidl20
-rw-r--r--core/java/android/os/Bundle.java1452
-rw-r--r--core/java/android/os/ConditionVariable.java141
-rw-r--r--core/java/android/os/CountDownTimer.java138
-rw-r--r--core/java/android/os/DeadObjectException.java28
-rw-r--r--core/java/android/os/Debug.java699
-rw-r--r--core/java/android/os/Environment.java119
-rw-r--r--core/java/android/os/Exec.java63
-rw-r--r--core/java/android/os/FileObserver.java146
-rw-r--r--core/java/android/os/FileUtils.java197
-rw-r--r--core/java/android/os/Handler.java548
-rw-r--r--core/java/android/os/HandlerInterface.java27
-rw-r--r--core/java/android/os/HandlerThread.java93
-rw-r--r--core/java/android/os/Hardware.java42
-rw-r--r--core/java/android/os/IBinder.java212
-rw-r--r--core/java/android/os/ICheckinService.aidl43
-rwxr-xr-xcore/java/android/os/IHardwareService.aidl40
-rw-r--r--core/java/android/os/IInterface.java31
-rw-r--r--core/java/android/os/IMessenger.aidl25
-rw-r--r--core/java/android/os/IMountService.aidl51
-rw-r--r--core/java/android/os/INetStatService.aidl30
-rw-r--r--core/java/android/os/IParentalControlCallback.aidl27
-rw-r--r--core/java/android/os/IPermissionController.aidl23
-rw-r--r--core/java/android/os/IPowerManager.aidl31
-rw-r--r--core/java/android/os/IServiceManager.java70
-rw-r--r--core/java/android/os/LocalPowerManager.java42
-rw-r--r--core/java/android/os/Looper.java214
-rw-r--r--core/java/android/os/MailboxNotAvailableException.java37
-rw-r--r--core/java/android/os/MemoryFile.java258
-rw-r--r--core/java/android/os/Message.aidl20
-rw-r--r--core/java/android/os/Message.java405
-rw-r--r--core/java/android/os/MessageQueue.java329
-rw-r--r--core/java/android/os/Messenger.aidl20
-rw-r--r--core/java/android/os/Messenger.java141
-rw-r--r--core/java/android/os/NetStat.java51
-rw-r--r--core/java/android/os/Parcel.java2051
-rw-r--r--core/java/android/os/ParcelFileDescriptor.aidl20
-rw-r--r--core/java/android/os/ParcelFileDescriptor.java250
-rw-r--r--core/java/android/os/ParcelFormatException.java31
-rw-r--r--core/java/android/os/Parcelable.java112
-rw-r--r--core/java/android/os/PatternMatcher.aidl19
-rw-r--r--core/java/android/os/PatternMatcher.java197
-rw-r--r--core/java/android/os/Power.java126
-rw-r--r--core/java/android/os/PowerManager.java392
-rw-r--r--core/java/android/os/Process.java694
-rw-r--r--core/java/android/os/Registrant.java124
-rw-r--r--core/java/android/os/RegistrantList.java128
-rw-r--r--core/java/android/os/RemoteCallbackList.java259
-rw-r--r--core/java/android/os/RemoteException.java27
-rw-r--r--core/java/android/os/RemoteMailException.java31
-rw-r--r--core/java/android/os/ServiceManager.java122
-rw-r--r--core/java/android/os/ServiceManagerNative.java174
-rw-r--r--core/java/android/os/StatFs.java74
-rw-r--r--core/java/android/os/SystemClock.java154
-rw-r--r--core/java/android/os/SystemProperties.java143
-rw-r--r--core/java/android/os/SystemService.java31
-rwxr-xr-xcore/java/android/os/TokenWatcher.java197
-rw-r--r--core/java/android/os/UEventObserver.java191
-rw-r--r--core/java/android/os/Vibrator.java86
-rw-r--r--core/java/android/os/package.html6
-rw-r--r--core/java/android/package.html10
-rw-r--r--core/java/android/pim/ContactsAsyncHelper.java336
-rw-r--r--core/java/android/pim/DateException.java26
-rw-r--r--core/java/android/pim/DateFormat.java493
-rw-r--r--core/java/android/pim/DateUtils.java1408
-rw-r--r--core/java/android/pim/EventRecurrence.java420
-rw-r--r--core/java/android/pim/ICalendar.java643
-rw-r--r--core/java/android/pim/RecurrenceSet.java398
-rw-r--r--core/java/android/pim/Time.java570
-rw-r--r--core/java/android/pim/package.html7
-rw-r--r--core/java/android/preference/CheckBoxPreference.java295
-rw-r--r--core/java/android/preference/DialogPreference.java445
-rw-r--r--core/java/android/preference/EditTextPreference.java228
-rw-r--r--core/java/android/preference/GenericInflater.java520
-rw-r--r--core/java/android/preference/ListPreference.java277
-rw-r--r--core/java/android/preference/OnDependencyChangeListener.java32
-rw-r--r--core/java/android/preference/Preference.java1577
-rw-r--r--core/java/android/preference/PreferenceActivity.java284
-rw-r--r--core/java/android/preference/PreferenceCategory.java58
-rw-r--r--core/java/android/preference/PreferenceGroup.java320
-rw-r--r--core/java/android/preference/PreferenceGroupAdapter.java246
-rw-r--r--core/java/android/preference/PreferenceInflater.java103
-rw-r--r--core/java/android/preference/PreferenceManager.java790
-rw-r--r--core/java/android/preference/PreferenceScreen.java252
-rw-r--r--core/java/android/preference/RingtonePreference.java242
-rw-r--r--core/java/android/preference/SeekBarPreference.java62
-rw-r--r--core/java/android/preference/VolumePreference.java161
-rw-r--r--core/java/android/preference/package.html23
-rw-r--r--core/java/android/provider/BaseColumns.java32
-rw-r--r--core/java/android/provider/Browser.java450
-rw-r--r--core/java/android/provider/Calendar.java1115
-rw-r--r--core/java/android/provider/CallLog.java190
-rw-r--r--core/java/android/provider/Checkin.java290
-rw-r--r--core/java/android/provider/Contacts.java1606
-rw-r--r--core/java/android/provider/Downloads.java604
-rw-r--r--core/java/android/provider/DrmStore.java185
-rw-r--r--core/java/android/provider/Gmail.java2355
-rw-r--r--core/java/android/provider/Im.java1937
-rw-r--r--core/java/android/provider/MediaStore.java1224
-rw-r--r--core/java/android/provider/OpenableColumns.java37
-rw-r--r--core/java/android/provider/SearchRecentSuggestions.java224
-rw-r--r--core/java/android/provider/Settings.java2073
-rw-r--r--core/java/android/provider/SubscribedFeeds.java204
-rw-r--r--core/java/android/provider/Sync.java603
-rw-r--r--core/java/android/provider/SyncConstValue.java71
-rw-r--r--core/java/android/provider/Telephony.java1694
-rw-r--r--core/java/android/provider/package.html11
-rw-r--r--core/java/android/security/Md5MessageDigest.java43
-rw-r--r--core/java/android/security/MessageDigest.java43
-rw-r--r--core/java/android/security/Sha1MessageDigest.java43
-rw-r--r--core/java/android/security/package.html5
-rw-r--r--core/java/android/server/BluetoothDeviceService.java1175
-rw-r--r--core/java/android/server/BluetoothEventLoop.java289
-rw-r--r--core/java/android/server/checkin/CheckinProvider.java388
-rw-r--r--core/java/android/server/checkin/FallbackCheckinService.java45
-rw-r--r--core/java/android/server/checkin/package.html5
-rw-r--r--core/java/android/server/data/BuildData.java89
-rw-r--r--core/java/android/server/data/CrashData.java145
-rw-r--r--core/java/android/server/data/StackTraceElementData.java80
-rw-r--r--core/java/android/server/data/ThrowableData.java138
-rwxr-xr-xcore/java/android/server/data/package.html5
-rwxr-xr-xcore/java/android/server/package.html5
-rw-r--r--core/java/android/server/search/SearchManagerService.java156
-rw-r--r--core/java/android/server/search/SearchableInfo.aidl19
-rw-r--r--core/java/android/server/search/SearchableInfo.java747
-rw-r--r--core/java/android/server/search/package.html5
-rw-r--r--core/java/android/speech/recognition/AbstractEmbeddedGrammarListener.java51
-rw-r--r--core/java/android/speech/recognition/AbstractGrammarListener.java39
-rw-r--r--core/java/android/speech/recognition/AbstractRecognizerListener.java83
-rw-r--r--core/java/android/speech/recognition/AbstractSrecGrammarListener.java59
-rw-r--r--core/java/android/speech/recognition/AudioAlreadyInUseException.java34
-rw-r--r--core/java/android/speech/recognition/AudioDriverErrorException.java33
-rw-r--r--core/java/android/speech/recognition/AudioSource.java45
-rw-r--r--core/java/android/speech/recognition/AudioSourceListener.java44
-rw-r--r--core/java/android/speech/recognition/AudioStream.java35
-rw-r--r--core/java/android/speech/recognition/Codec.java126
-rw-r--r--core/java/android/speech/recognition/DeviceSpeaker.java77
-rw-r--r--core/java/android/speech/recognition/DeviceSpeakerListener.java44
-rw-r--r--core/java/android/speech/recognition/EmbeddedGrammar.java43
-rw-r--r--core/java/android/speech/recognition/EmbeddedGrammarListener.java58
-rw-r--r--core/java/android/speech/recognition/EmbeddedRecognizer.java66
-rw-r--r--core/java/android/speech/recognition/Grammar.java43
-rw-r--r--core/java/android/speech/recognition/GrammarErrorException.java33
-rw-r--r--core/java/android/speech/recognition/GrammarListener.java45
-rw-r--r--core/java/android/speech/recognition/GrammarOverflowException.java33
-rw-r--r--core/java/android/speech/recognition/InvalidURLException.java34
-rw-r--r--core/java/android/speech/recognition/Logger.java127
-rw-r--r--core/java/android/speech/recognition/MediaFileReader.java90
-rw-r--r--core/java/android/speech/recognition/MediaFileReaderListener.java29
-rw-r--r--core/java/android/speech/recognition/MediaFileWriter.java49
-rw-r--r--core/java/android/speech/recognition/MediaFileWriterListener.java40
-rw-r--r--core/java/android/speech/recognition/Microphone.java76
-rw-r--r--core/java/android/speech/recognition/MicrophoneListener.java29
-rw-r--r--core/java/android/speech/recognition/NBestRecognitionResult.java113
-rw-r--r--core/java/android/speech/recognition/ParameterErrorException.java33
-rw-r--r--core/java/android/speech/recognition/ParametersListener.java63
-rw-r--r--core/java/android/speech/recognition/ParseErrorException.java33
-rw-r--r--core/java/android/speech/recognition/RecognitionResult.java27
-rw-r--r--core/java/android/speech/recognition/Recognizer.java102
-rw-r--r--core/java/android/speech/recognition/RecognizerListener.java142
-rw-r--r--core/java/android/speech/recognition/SlotItem.java27
-rw-r--r--core/java/android/speech/recognition/SrecGrammar.java81
-rw-r--r--core/java/android/speech/recognition/SrecGrammarListener.java58
-rw-r--r--core/java/android/speech/recognition/VoicetagItem.java82
-rw-r--r--core/java/android/speech/recognition/VoicetagItemListener.java49
-rw-r--r--core/java/android/speech/recognition/WordItem.java58
-rw-r--r--core/java/android/speech/recognition/impl/AudioStreamImpl.java84
-rw-r--r--core/java/android/speech/recognition/impl/DeviceSpeakerImpl.java164
-rw-r--r--core/java/android/speech/recognition/impl/EmbeddedGrammarImpl.java73
-rw-r--r--core/java/android/speech/recognition/impl/EmbeddedRecognizerImpl.java246
-rw-r--r--core/java/android/speech/recognition/impl/EntryImpl.java147
-rw-r--r--core/java/android/speech/recognition/impl/GrammarImpl.java114
-rw-r--r--core/java/android/speech/recognition/impl/LoggerImpl.java166
-rw-r--r--core/java/android/speech/recognition/impl/MediaFileReaderImpl.java156
-rw-r--r--core/java/android/speech/recognition/impl/MediaFileWriterImpl.java102
-rw-r--r--core/java/android/speech/recognition/impl/MicrophoneImpl.java165
-rw-r--r--core/java/android/speech/recognition/impl/NBestRecognitionResultImpl.java106
-rw-r--r--core/java/android/speech/recognition/impl/SrecGrammarImpl.java120
-rw-r--r--core/java/android/speech/recognition/impl/System.java179
-rw-r--r--core/java/android/speech/recognition/impl/VoicetagItemImpl.java206
-rw-r--r--core/java/android/speech/recognition/impl/WordItemImpl.java157
-rwxr-xr-xcore/java/android/speech/recognition/impl/package.html5
-rw-r--r--core/java/android/speech/recognition/package.html6
-rw-r--r--core/java/android/speech/srec/Srec.java162
-rw-r--r--core/java/android/syncml/package.html6
-rw-r--r--core/java/android/syncml/pim/PropertyNode.java40
-rw-r--r--core/java/android/syncml/pim/VBuilder.java62
-rw-r--r--core/java/android/syncml/pim/VDataBuilder.java142
-rw-r--r--core/java/android/syncml/pim/VNode.java29
-rw-r--r--core/java/android/syncml/pim/VParser.java723
-rw-r--r--core/java/android/syncml/pim/package.html6
-rw-r--r--core/java/android/syncml/pim/vcalendar/CalendarStruct.java56
-rw-r--r--core/java/android/syncml/pim/vcalendar/VCalComposer.java189
-rw-r--r--core/java/android/syncml/pim/vcalendar/VCalException.java41
-rw-r--r--core/java/android/syncml/pim/vcalendar/VCalParser.java116
-rw-r--r--core/java/android/syncml/pim/vcalendar/VCalParser_V10.java1628
-rw-r--r--core/java/android/syncml/pim/vcalendar/VCalParser_V20.java186
-rw-r--r--core/java/android/syncml/pim/vcalendar/package.html6
-rw-r--r--core/java/android/syncml/pim/vcard/ContactStruct.java91
-rw-r--r--core/java/android/syncml/pim/vcard/VCardComposer.java350
-rw-r--r--core/java/android/syncml/pim/vcard/VCardException.java41
-rw-r--r--core/java/android/syncml/pim/vcard/VCardParser.java142
-rw-r--r--core/java/android/syncml/pim/vcard/VCardParser_V21.java970
-rw-r--r--core/java/android/syncml/pim/vcard/VCardParser_V30.java157
-rw-r--r--core/java/android/syncml/pim/vcard/package.html6
-rw-r--r--core/java/android/test/AndroidTestCase.java86
-rw-r--r--core/java/android/test/FlakyTest.java41
-rw-r--r--core/java/android/test/InstrumentationTestCase.java260
-rw-r--r--core/java/android/test/InstrumentationTestSuite.java74
-rw-r--r--core/java/android/test/PerformanceTestCase.java65
-rw-r--r--core/java/android/test/UiThreadTest.java33
-rw-r--r--core/java/android/test/package.html5
-rw-r--r--core/java/android/test/suitebuilder/annotation/Smoke.java34
-rw-r--r--core/java/android/test/suitebuilder/annotation/Suppress.java33
-rw-r--r--core/java/android/text/AlteredCharSequence.java127
-rw-r--r--core/java/android/text/AndroidCharacter.java45
-rw-r--r--core/java/android/text/Annotation.java40
-rw-r--r--core/java/android/text/AutoText.java246
-rw-r--r--core/java/android/text/BoringLayout.java388
-rw-r--r--core/java/android/text/ClipboardManager.java88
-rw-r--r--core/java/android/text/DynamicLayout.java503
-rw-r--r--core/java/android/text/Editable.java142
-rw-r--r--core/java/android/text/GetChars.java33
-rw-r--r--core/java/android/text/GraphicsOperations.java46
-rw-r--r--core/java/android/text/Html.java750
-rw-r--r--core/java/android/text/IClipboard.aidl42
-rw-r--r--core/java/android/text/InputFilter.java92
-rw-r--r--core/java/android/text/Layout.java1745
-rw-r--r--core/java/android/text/LoginFilter.java206
-rw-r--r--core/java/android/text/PackedIntVector.java368
-rw-r--r--core/java/android/text/PackedObjectVector.java188
-rw-r--r--core/java/android/text/Selection.java426
-rw-r--r--core/java/android/text/SpanWatcher.java42
-rw-r--r--core/java/android/text/Spannable.java70
-rw-r--r--core/java/android/text/SpannableString.java56
-rw-r--r--core/java/android/text/SpannableStringBuilder.java1136
-rw-r--r--core/java/android/text/SpannableStringInternal.java372
-rw-r--r--core/java/android/text/Spanned.java160
-rw-r--r--core/java/android/text/SpannedString.java48
-rw-r--r--core/java/android/text/StaticLayout.java1118
-rw-r--r--core/java/android/text/Styled.java298
-rw-r--r--core/java/android/text/TextPaint.java55
-rw-r--r--core/java/android/text/TextUtils.java1570
-rw-r--r--core/java/android/text/TextWatcher.java57
-rw-r--r--core/java/android/text/method/ArrowKeyMovementMethod.java266
-rw-r--r--core/java/android/text/method/BaseKeyListener.java112
-rw-r--r--core/java/android/text/method/CharacterPickerDialog.java134
-rw-r--r--core/java/android/text/method/DateKeyListener.java52
-rw-r--r--core/java/android/text/method/DateTimeKeyListener.java52
-rw-r--r--core/java/android/text/method/DialerKeyListener.java109
-rw-r--r--core/java/android/text/method/DigitsKeyListener.java206
-rw-r--r--core/java/android/text/method/HideReturnsTransformationMethod.java59
-rw-r--r--core/java/android/text/method/KeyListener.java42
-rw-r--r--core/java/android/text/method/LinkMovementMethod.java256
-rw-r--r--core/java/android/text/method/MetaKeyKeyListener.java250
-rw-r--r--core/java/android/text/method/MovementMethod.java43
-rw-r--r--core/java/android/text/method/MultiTapKeyListener.java285
-rw-r--r--core/java/android/text/method/NumberKeyListener.java132
-rw-r--r--core/java/android/text/method/PasswordTransformationMethod.java260
-rw-r--r--core/java/android/text/method/QwertyKeyListener.java444
-rw-r--r--core/java/android/text/method/ReplacementTransformationMethod.java205
-rw-r--r--core/java/android/text/method/ScrollingMovementMethod.java234
-rw-r--r--core/java/android/text/method/SingleLineTransformationMethod.java60
-rw-r--r--core/java/android/text/method/TextKeyListener.java339
-rw-r--r--core/java/android/text/method/TimeKeyListener.java52
-rw-r--r--core/java/android/text/method/Touch.java138
-rw-r--r--core/java/android/text/method/TransformationMethod.java46
-rw-r--r--core/java/android/text/method/package.html21
-rw-r--r--core/java/android/text/package.html13
-rw-r--r--core/java/android/text/style/AbsoluteSizeSpan.java43
-rw-r--r--core/java/android/text/style/AlignmentSpan.java39
-rw-r--r--core/java/android/text/style/BackgroundColorSpan.java37
-rw-r--r--core/java/android/text/style/BulletSpan.java76
-rw-r--r--core/java/android/text/style/CharacterStyle.java87
-rw-r--r--core/java/android/text/style/ClickableSpan.java43
-rw-r--r--core/java/android/text/style/DrawableMarginSpan.java79
-rw-r--r--core/java/android/text/style/DynamicDrawableSpan.java82
-rw-r--r--core/java/android/text/style/ForegroundColorSpan.java38
-rw-r--r--core/java/android/text/style/IconMarginSpan.java73
-rw-r--r--core/java/android/text/style/ImageSpan.java99
-rw-r--r--core/java/android/text/style/LeadingMarginSpan.java59
-rw-r--r--core/java/android/text/style/LineBackgroundSpan.java30
-rw-r--r--core/java/android/text/style/LineHeightSpan.java29
-rw-r--r--core/java/android/text/style/MaskFilterSpan.java39
-rw-r--r--core/java/android/text/style/MetricAffectingSpan.java85
-rw-r--r--core/java/android/text/style/ParagraphStyle.java26
-rw-r--r--core/java/android/text/style/QuoteSpan.java64
-rw-r--r--core/java/android/text/style/RasterizerSpan.java39
-rw-r--r--core/java/android/text/style/RelativeSizeSpan.java43
-rw-r--r--core/java/android/text/style/ReplacementSpan.java43
-rw-r--r--core/java/android/text/style/ScaleXSpan.java43
-rw-r--r--core/java/android/text/style/StrikethroughSpan.java27
-rw-r--r--core/java/android/text/style/StyleSpan.java93
-rw-r--r--core/java/android/text/style/SubscriptSpan.java31
-rw-r--r--core/java/android/text/style/SuperscriptSpan.java31
-rw-r--r--core/java/android/text/style/TabStopSpan.java37
-rw-r--r--core/java/android/text/style/TextAppearanceSpan.java198
-rw-r--r--core/java/android/text/style/TypefaceSpan.java77
-rw-r--r--core/java/android/text/style/URLSpan.java43
-rw-r--r--core/java/android/text/style/UnderlineSpan.java27
-rw-r--r--core/java/android/text/style/UpdateLayout.java24
-rw-r--r--core/java/android/text/style/WrapTogetherSpan.java23
-rw-r--r--core/java/android/text/style/package.html10
-rw-r--r--core/java/android/text/util/Linkify.java533
-rw-r--r--core/java/android/text/util/Regex.java192
-rw-r--r--core/java/android/text/util/Rfc822Token.java172
-rw-r--r--core/java/android/text/util/Rfc822Tokenizer.java292
-rw-r--r--core/java/android/text/util/Rfc822Validator.java128
-rw-r--r--core/java/android/text/util/package.html6
-rw-r--r--core/java/android/util/AndroidException.java34
-rw-r--r--core/java/android/util/AndroidRuntimeException.java34
-rw-r--r--core/java/android/util/AttributeSet.java269
-rw-r--r--core/java/android/util/Config.java51
-rw-r--r--core/java/android/util/DayOfMonthCursor.java173
-rw-r--r--core/java/android/util/DebugUtils.java104
-rw-r--r--core/java/android/util/DisplayMetrics.java85
-rw-r--r--core/java/android/util/EventLog.java288
-rw-r--r--core/java/android/util/EventLogTags.java90
-rw-r--r--core/java/android/util/FloatMath.java74
-rw-r--r--core/java/android/util/Log.java247
-rw-r--r--core/java/android/util/LogPrinter.java47
-rw-r--r--core/java/android/util/MonthDisplayHelper.java213
-rw-r--r--core/java/android/util/PrintWriterPrinter.java40
-rw-r--r--core/java/android/util/Printer.java31
-rw-r--r--core/java/android/util/SparseArray.java340
-rw-r--r--core/java/android/util/SparseBooleanArray.java245
-rw-r--r--core/java/android/util/SparseIntArray.java244
-rw-r--r--core/java/android/util/StateSet.java178
-rw-r--r--core/java/android/util/StringBuilderPrinter.java42
-rw-r--r--core/java/android/util/TimeFormatException.java26
-rw-r--r--core/java/android/util/TimeUtils.java109
-rw-r--r--core/java/android/util/TimingLogger.java144
-rw-r--r--core/java/android/util/TypedValue.java477
-rw-r--r--core/java/android/util/Xml.java185
-rw-r--r--core/java/android/util/XmlPullAttributes.java146
-rw-r--r--core/java/android/util/package.html6
-rw-r--r--core/java/android/view/AbsSavedState.java89
-rw-r--r--core/java/android/view/ContextMenu.java91
-rw-r--r--core/java/android/view/ContextThemeWrapper.java103
-rw-r--r--core/java/android/view/Display.java121
-rw-r--r--core/java/android/view/FocusFinder.java480
-rw-r--r--core/java/android/view/FocusFinderHelper.java59
-rw-r--r--core/java/android/view/GestureDetector.java368
-rw-r--r--core/java/android/view/Gravity.java179
-rw-r--r--core/java/android/view/IApplicationToken.aidl28
-rw-r--r--core/java/android/view/IOnKeyguardExitResult.aidl25
-rw-r--r--core/java/android/view/IRotationWatcher.aidl25
-rw-r--r--core/java/android/view/IWindow.aidl57
-rw-r--r--core/java/android/view/IWindowManager.aidl125
-rw-r--r--core/java/android/view/IWindowSession.aidl73
-rw-r--r--core/java/android/view/InflateException.java40
-rw-r--r--core/java/android/view/KeyCharacterMap.java521
-rw-r--r--core/java/android/view/KeyEvent.aidl20
-rw-r--r--core/java/android/view/KeyEvent.java786
-rw-r--r--core/java/android/view/LayoutInflater.java744
-rw-r--r--core/java/android/view/Menu.java427
-rw-r--r--core/java/android/view/MenuInflater.java325
-rw-r--r--core/java/android/view/MenuItem.java368
-rw-r--r--core/java/android/view/MotionEvent.aidl20
-rw-r--r--core/java/android/view/MotionEvent.java659
-rw-r--r--core/java/android/view/OrientationListener.java126
-rw-r--r--core/java/android/view/RawInputEvent.java169
-rw-r--r--core/java/android/view/SoundEffectConstants.java57
-rw-r--r--core/java/android/view/SubMenu.java103
-rw-r--r--core/java/android/view/Surface.aidl20
-rw-r--r--core/java/android/view/Surface.java298
-rw-r--r--core/java/android/view/SurfaceHolder.java284
-rw-r--r--core/java/android/view/SurfaceSession.java49
-rw-r--r--core/java/android/view/SurfaceView.java593
-rw-r--r--core/java/android/view/TouchDelegate.java151
-rw-r--r--core/java/android/view/VelocityTracker.java215
-rw-r--r--core/java/android/view/View.java7481
-rw-r--r--core/java/android/view/ViewConfiguration.java228
-rw-r--r--core/java/android/view/ViewDebug.java927
-rw-r--r--core/java/android/view/ViewGroup.java3389
-rw-r--r--core/java/android/view/ViewManager.java27
-rw-r--r--core/java/android/view/ViewParent.java185
-rw-r--r--core/java/android/view/ViewRoot.java2212
-rw-r--r--core/java/android/view/ViewStub.java279
-rw-r--r--core/java/android/view/ViewTreeObserver.java376
-rw-r--r--core/java/android/view/VolumePanel.java341
-rw-r--r--core/java/android/view/Window.java962
-rwxr-xr-xcore/java/android/view/WindowManager.aidl21
-rw-r--r--core/java/android/view/WindowManager.java693
-rw-r--r--core/java/android/view/WindowManagerImpl.java339
-rw-r--r--core/java/android/view/WindowManagerPolicy.java717
-rw-r--r--core/java/android/view/animation/AccelerateDecelerateInterpolator.java37
-rw-r--r--core/java/android/view/animation/AccelerateInterpolator.java62
-rw-r--r--core/java/android/view/animation/AlphaAnimation.java81
-rw-r--r--core/java/android/view/animation/Animation.java796
-rw-r--r--core/java/android/view/animation/AnimationSet.java380
-rw-r--r--core/java/android/view/animation/AnimationUtils.java329
-rw-r--r--core/java/android/view/animation/CycleInterpolator.java47
-rw-r--r--core/java/android/view/animation/DecelerateInterpolator.java61
-rw-r--r--core/java/android/view/animation/GridLayoutAnimationController.java424
-rw-r--r--core/java/android/view/animation/Interpolator.java39
-rw-r--r--core/java/android/view/animation/LayoutAnimationController.java573
-rw-r--r--core/java/android/view/animation/LinearInterpolator.java37
-rw-r--r--core/java/android/view/animation/RotateAnimation.java165
-rw-r--r--core/java/android/view/animation/ScaleAnimation.java186
-rw-r--r--core/java/android/view/animation/Transformation.java139
-rw-r--r--core/java/android/view/animation/TranslateAnimation.java171
-rwxr-xr-xcore/java/android/view/animation/package.html244
-rw-r--r--core/java/android/view/package.html6
-rw-r--r--core/java/android/webkit/BrowserFrame.java781
-rw-r--r--core/java/android/webkit/ByteArrayBuilder.java134
-rw-r--r--core/java/android/webkit/CacheLoader.java65
-rw-r--r--core/java/android/webkit/CacheManager.java643
-rw-r--r--core/java/android/webkit/CallbackProxy.java906
-rw-r--r--core/java/android/webkit/ContentLoader.java123
-rw-r--r--core/java/android/webkit/CookieManager.java898
-rw-r--r--core/java/android/webkit/CookieSyncManager.java205
-rw-r--r--core/java/android/webkit/DataLoader.java80
-rw-r--r--core/java/android/webkit/DateSorter.java121
-rw-r--r--core/java/android/webkit/DownloadListener.java33
-rw-r--r--core/java/android/webkit/FileLoader.java131
-rw-r--r--core/java/android/webkit/FrameLoader.java402
-rw-r--r--core/java/android/webkit/HttpAuthHandler.java186
-rw-r--r--core/java/android/webkit/HttpDateTime.java195
-rw-r--r--core/java/android/webkit/JWebCoreJavaBridge.java194
-rw-r--r--core/java/android/webkit/JsPromptResult.java52
-rw-r--r--core/java/android/webkit/JsResult.java82
-rw-r--r--core/java/android/webkit/LoadListener.java1409
-rw-r--r--core/java/android/webkit/MimeTypeMap.java499
-rw-r--r--core/java/android/webkit/Network.java350
-rw-r--r--core/java/android/webkit/PerfChecker.java49
-rw-r--r--core/java/android/webkit/Plugin.java126
-rw-r--r--core/java/android/webkit/PluginList.java83
-rw-r--r--core/java/android/webkit/SslErrorHandler.java255
-rw-r--r--core/java/android/webkit/StreamLoader.java199
-rw-r--r--core/java/android/webkit/TextDialog.java543
-rw-r--r--core/java/android/webkit/URLUtil.java362
-rw-r--r--core/java/android/webkit/UrlInterceptHandler.java34
-rw-r--r--core/java/android/webkit/UrlInterceptRegistry.java106
-rw-r--r--core/java/android/webkit/WebBackForwardList.java188
-rw-r--r--core/java/android/webkit/WebChromeClient.java160
-rw-r--r--core/java/android/webkit/WebHistoryItem.java163
-rw-r--r--core/java/android/webkit/WebIconDatabase.java251
-rw-r--r--core/java/android/webkit/WebSettings.java903
-rw-r--r--core/java/android/webkit/WebSyncManager.java162
-rw-r--r--core/java/android/webkit/WebView.java4609
-rw-r--r--core/java/android/webkit/WebViewClient.java206
-rw-r--r--core/java/android/webkit/WebViewCore.java1553
-rw-r--r--core/java/android/webkit/WebViewDatabase.java962
-rw-r--r--core/java/android/webkit/gears/AndroidGpsLocationProvider.java156
-rw-r--r--core/java/android/webkit/gears/AndroidRadioDataProvider.java244
-rw-r--r--core/java/android/webkit/gears/DesktopAndroid.java113
-rw-r--r--core/java/android/webkit/gears/GearsPluginSettings.java95
-rw-r--r--core/java/android/webkit/gears/HtmlDialogAndroid.java174
-rw-r--r--core/java/android/webkit/gears/HttpRequestAndroid.java730
-rw-r--r--core/java/android/webkit/gears/IGearsDialogService.java107
-rw-r--r--core/java/android/webkit/gears/UrlInterceptHandlerGears.java497
-rw-r--r--core/java/android/webkit/gears/VersionExtractor.java147
-rw-r--r--core/java/android/webkit/gears/ZipInflater.java200
-rw-r--r--core/java/android/webkit/gears/package.html3
-rw-r--r--core/java/android/webkit/package.html7
-rw-r--r--core/java/android/widget/AbsListView.java3196
-rw-r--r--core/java/android/widget/AbsSeekBar.java298
-rw-r--r--core/java/android/widget/AbsSpinner.java490
-rw-r--r--core/java/android/widget/AbsoluteLayout.java216
-rw-r--r--core/java/android/widget/Adapter.java149
-rw-r--r--core/java/android/widget/AdapterView.java1094
-rw-r--r--core/java/android/widget/AnalogClock.java241
-rwxr-xr-xcore/java/android/widget/AppSecurityPermissions.java383
-rw-r--r--core/java/android/widget/ArrayAdapter.java455
-rw-r--r--core/java/android/widget/AutoCompleteTextView.java762
-rw-r--r--core/java/android/widget/BaseAdapter.java76
-rw-r--r--core/java/android/widget/BaseExpandableListAdapter.java106
-rw-r--r--core/java/android/widget/Button.java69
-rw-r--r--core/java/android/widget/CheckBox.java65
-rw-r--r--core/java/android/widget/Checkable.java42
-rw-r--r--core/java/android/widget/CheckedTextView.java198
-rw-r--r--core/java/android/widget/Chronometer.java223
-rw-r--r--core/java/android/widget/CompoundButton.java318
-rw-r--r--core/java/android/widget/CursorAdapter.java382
-rw-r--r--core/java/android/widget/CursorFilter.java71
-rw-r--r--core/java/android/widget/CursorTreeAdapter.java522
-rw-r--r--core/java/android/widget/DatePicker.java330
-rw-r--r--core/java/android/widget/DialerFilter.java431
-rw-r--r--core/java/android/widget/DigitalClock.java135
-rw-r--r--core/java/android/widget/DoubleDigitManager.java105
-rw-r--r--core/java/android/widget/EditText.java102
-rw-r--r--core/java/android/widget/ExpandableListAdapter.java210
-rw-r--r--core/java/android/widget/ExpandableListConnector.java797
-rw-r--r--core/java/android/widget/ExpandableListPosition.java102
-rw-r--r--core/java/android/widget/ExpandableListView.java1057
-rw-r--r--core/java/android/widget/Filter.java281
-rw-r--r--core/java/android/widget/FilterQueryProvider.java42
-rw-r--r--core/java/android/widget/Filterable.java37
-rw-r--r--core/java/android/widget/FrameLayout.java448
-rw-r--r--core/java/android/widget/Gallery.java1338
-rw-r--r--core/java/android/widget/GridView.java1828
-rw-r--r--core/java/android/widget/HeaderViewListAdapter.java241
-rw-r--r--core/java/android/widget/ImageButton.java57
-rw-r--r--core/java/android/widget/ImageSwitcher.java59
-rw-r--r--core/java/android/widget/ImageView.java883
-rw-r--r--core/java/android/widget/LinearLayout.java1315
-rw-r--r--core/java/android/widget/ListAdapter.java44
-rw-r--r--core/java/android/widget/ListView.java3204
-rw-r--r--core/java/android/widget/MediaController.java544
-rw-r--r--core/java/android/widget/MultiAutoCompleteTextView.java282
-rw-r--r--core/java/android/widget/PopupWindow.java803
-rw-r--r--core/java/android/widget/ProgressBar.java820
-rw-r--r--core/java/android/widget/RadioButton.java72
-rw-r--r--core/java/android/widget/RadioGroup.java371
-rw-r--r--core/java/android/widget/RatingBar.java311
-rw-r--r--core/java/android/widget/RelativeLayout.java950
-rw-r--r--core/java/android/widget/RemoteViews.java649
-rw-r--r--core/java/android/widget/ResourceCursorAdapter.java75
-rw-r--r--core/java/android/widget/ResourceCursorTreeAdapter.java109
-rw-r--r--core/java/android/widget/ScrollBarDrawable.java248
-rw-r--r--core/java/android/widget/ScrollView.java1213
-rw-r--r--core/java/android/widget/Scroller.java368
-rw-r--r--core/java/android/widget/SeekBar.java116
-rw-r--r--core/java/android/widget/SimpleAdapter.java366
-rw-r--r--core/java/android/widget/SimpleCursorAdapter.java365
-rw-r--r--core/java/android/widget/SimpleCursorTreeAdapter.java241
-rw-r--r--core/java/android/widget/SimpleExpandableListAdapter.java301
-rw-r--r--core/java/android/widget/Spinner.java364
-rw-r--r--core/java/android/widget/SpinnerAdapter.java43
-rw-r--r--core/java/android/widget/TabHost.java632
-rw-r--r--core/java/android/widget/TabWidget.java289
-rw-r--r--core/java/android/widget/TableLayout.java755
-rw-r--r--core/java/android/widget/TableRow.java531
-rw-r--r--core/java/android/widget/TextSwitcher.java91
-rw-r--r--core/java/android/widget/TextView.java4866
-rw-r--r--core/java/android/widget/TimePicker.java360
-rw-r--r--core/java/android/widget/Toast.java399
-rw-r--r--core/java/android/widget/ToggleButton.java147
-rw-r--r--core/java/android/widget/TwoLineListItem.java90
-rw-r--r--core/java/android/widget/VideoView.java509
-rw-r--r--core/java/android/widget/ViewAnimator.java247
-rw-r--r--core/java/android/widget/ViewFlipper.java99
-rw-r--r--core/java/android/widget/ViewSwitcher.java135
-rw-r--r--core/java/android/widget/WrapperListAdapter.java32
-rw-r--r--core/java/android/widget/ZoomButton.java110
-rw-r--r--core/java/android/widget/ZoomControls.java110
-rw-r--r--core/java/android/widget/package.html32
823 files changed, 248474 insertions, 0 deletions
diff --git a/core/java/android/accounts/AccountMonitor.java b/core/java/android/accounts/AccountMonitor.java
new file mode 100644
index 0000000..9bcc1e7
--- /dev/null
+++ b/core/java/android/accounts/AccountMonitor.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.accounts;
+
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.database.SQLException;
+import android.os.IBinder;
+import android.os.Process;
+import android.os.RemoteException;
+import android.util.Log;
+
+/**
+ * A helper class that calls back on the provided
+ * AccountMonitorListener with the set of current accounts both when
+ * it gets created and whenever the set changes. It does this by
+ * binding to the AccountsService and registering to receive the
+ * intent broadcast when the set of accounts is changed. The
+ * connection to the accounts service is only made when it needs to
+ * fetch the current list of accounts (that is, when the
+ * AccountMonitor is first created, and when the intent is received).
+ */
+public class AccountMonitor extends BroadcastReceiver implements ServiceConnection {
+ private final Context mContext;
+ private final AccountMonitorListener mListener;
+ private boolean mClosed = false;
+
+ // This thread runs in the background and runs the code to update accounts
+ // in the listener.
+ private class AccountUpdater extends Thread {
+ private IBinder mService;
+
+ public AccountUpdater(IBinder service) {
+ mService = service;
+ }
+
+ @Override
+ public void run() {
+ Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+ IAccountsService accountsService = IAccountsService.Stub.asInterface(mService);
+ String[] accounts;
+ try {
+ accounts = accountsService.getAccounts();
+ } catch (RemoteException e) {
+ // if the service was killed then the system will restart it and when it does we
+ // will get another onServiceConnected, at which point we will do a notify.
+ Log.w("AccountMonitor", "Remote exception when getting accounts", e);
+ return;
+ }
+ mContext.unbindService(AccountMonitor.this);
+
+ try {
+ mListener.onAccountsUpdated(accounts);
+ } catch (SQLException e) {
+ // Better luck next time. If the problem was disk-full,
+ // the STORAGE_OK intent will re-trigger the update.
+ Log.e("AccountMonitor", "Can't update accounts", e);
+ }
+ }
+ }
+
+ /**
+ * Initializes the AccountMonitor and initiates a bind to the
+ * AccountsService to get the initial account list. For 1.0,
+ * the "list" is always a single account.
+ *
+ * @param context the context we are running in
+ * @param listener the user to notify when the account set changes
+ */
+ public AccountMonitor(Context context, AccountMonitorListener listener) {
+ if (listener == null) {
+ throw new IllegalArgumentException("listener is null");
+ }
+
+ mContext = context;
+ mListener = listener;
+
+ // Register an intent receiver to monitor account changes
+ IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(AccountsServiceConstants.LOGIN_ACCOUNTS_CHANGED_ACTION);
+ intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_OK); // To recover from disk-full.
+ mContext.registerReceiver(this, intentFilter);
+
+ // Send the listener the initial state now.
+ notifyListener();
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ notifyListener();
+ }
+
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ // Create a background thread to update the accounts.
+ new AccountUpdater(service).start();
+ }
+
+ public void onServiceDisconnected(ComponentName className) {
+ }
+
+ private void notifyListener() {
+ // initiate the bind
+ if (!mContext.bindService(AccountsServiceConstants.SERVICE_INTENT, this,
+ Context.BIND_AUTO_CREATE)) {
+ // This is normal if GLS isn't part of this build.
+ Log.w("AccountMonitor",
+ "Couldn't connect to the accounts service (Missing service?)");
+ }
+ }
+
+ /**
+ * calls close()
+ * @throws Throwable
+ */
+ @Override
+ protected void finalize() throws Throwable {
+ close();
+ super.finalize();
+ }
+
+ /**
+ * Unregisters the account receiver. Consecutive calls to this
+ * method are harmless, but also do nothing. Once this call is
+ * made no more notifications will occur.
+ */
+ public synchronized void close() {
+ if (!mClosed) {
+ mContext.unregisterReceiver(this);
+ mClosed = true;
+ }
+ }
+}
diff --git a/core/java/android/accounts/AccountMonitorListener.java b/core/java/android/accounts/AccountMonitorListener.java
new file mode 100644
index 0000000..d0bd9a9
--- /dev/null
+++ b/core/java/android/accounts/AccountMonitorListener.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.accounts;
+
+/**
+ * An interface that contains the callback used by the AccountMonitor
+ */
+public interface AccountMonitorListener {
+ /**
+ * This invoked when the AccountMonitor starts up and whenever the account
+ * set changes.
+ * @param currentAccounts the current accounts
+ */
+ void onAccountsUpdated(String[] currentAccounts);
+}
diff --git a/core/java/android/accounts/AccountsServiceConstants.java b/core/java/android/accounts/AccountsServiceConstants.java
new file mode 100644
index 0000000..b882e7b
--- /dev/null
+++ b/core/java/android/accounts/AccountsServiceConstants.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.accounts;
+
+import android.content.Intent;
+
+/**
+ * Miscellaneous constants used by the AccountsService and its
+ * clients.
+ */
+// TODO: These constants *could* come directly from the
+// IAccountsService interface, but that's not possible since the
+// aidl compiler doesn't let you define constants (yet.)
+public class AccountsServiceConstants {
+ /** This class is never instantiated. */
+ private AccountsServiceConstants() {
+ }
+
+ /**
+ * Action sent as a broadcast Intent by the AccountsService
+ * when accounts are added to and/or removed from the device's
+ * database, or when the primary account is changed.
+ */
+ public static final String LOGIN_ACCOUNTS_CHANGED_ACTION =
+ "android.accounts.LOGIN_ACCOUNTS_CHANGED";
+
+ /**
+ * Action sent as a broadcast Intent by the AccountsService
+ * when it starts up and no accounts are available (so some should be added).
+ */
+ public static final String LOGIN_ACCOUNTS_MISSING_ACTION =
+ "android.accounts.LOGIN_ACCOUNTS_MISSING";
+
+ /**
+ * Action on the intent used to bind to the IAccountsService interface. This
+ * is used for services that have multiple interfaces (allowing
+ * them to differentiate the interface intended, and return the proper
+ * Binder.)
+ */
+ private static final String ACCOUNTS_SERVICE_ACTION = "android.accounts.IAccountsService";
+
+ /*
+ * The intent uses a component in addition to the action to ensure the actual
+ * accounts service is bound to (a malicious third-party app could
+ * theoretically have a service with the same action).
+ */
+ /** The intent used to bind to the accounts service. */
+ public static final Intent SERVICE_INTENT =
+ new Intent()
+ .setClassName("com.google.android.googleapps",
+ "com.google.android.googleapps.GoogleLoginService")
+ .setAction(ACCOUNTS_SERVICE_ACTION);
+
+ /**
+ * Checks whether the intent is to bind to the accounts service.
+ *
+ * @param bindIntent The Intent used to bind to the service.
+ * @return Whether the intent is to bind to the accounts service.
+ */
+ public static final boolean isForAccountsService(Intent bindIntent) {
+ String otherAction = bindIntent.getAction();
+ return otherAction != null && otherAction.equals(ACCOUNTS_SERVICE_ACTION);
+ }
+}
diff --git a/core/java/android/accounts/IAccountsService.aidl b/core/java/android/accounts/IAccountsService.aidl
new file mode 100644
index 0000000..dda513c
--- /dev/null
+++ b/core/java/android/accounts/IAccountsService.aidl
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.accounts;
+
+/**
+ * Central application service that allows querying the list of accounts.
+ */
+interface IAccountsService {
+ /**
+ * Gets the list of Accounts the user has previously logged
+ * in to. Accounts are of the form "username@domain".
+ * <p>
+ * This method will return an empty array if the device doesn't
+ * know about any accounts (yet).
+ *
+ * @return The accounts. The array will be zero-length if the
+ * AccountsService doesn't know about any accounts yet.
+ */
+ String[] getAccounts();
+
+ /**
+ * This is an interim solution for bypassing a forgotten gesture on the
+ * unlock screen (it is hidden, please make sure it stays this way!). This
+ * will be *removed* when the unlock screen design supports additional
+ * authenticators.
+ * <p>
+ * The user will be presented with username and password fields that are
+ * called as parameters to this method. If true is returned, the user is
+ * able to define a new gesture and get back into the system. If false, the
+ * user can try again.
+ *
+ * @param username The username entered.
+ * @param password The password entered.
+ * @return Whether to allow the user to bypass the lock screen and define a
+ * new gesture.
+ * @hide (The package is already hidden, but just in case someone
+ * unhides that, this should not be revealed.)
+ */
+ boolean shouldUnlock(String username, String password);
+}
diff --git a/core/java/android/accounts/package.html b/core/java/android/accounts/package.html
new file mode 100755
index 0000000..c9f96a6
--- /dev/null
+++ b/core/java/android/accounts/package.html
@@ -0,0 +1,5 @@
+<body>
+
+{@hide}
+
+</body>
diff --git a/core/java/android/annotation/SdkConstant.java b/core/java/android/annotation/SdkConstant.java
new file mode 100644
index 0000000..6ac70f0
--- /dev/null
+++ b/core/java/android/annotation/SdkConstant.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.annotation;
+
+import java.lang.annotation.Target;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Indicates a constant field value should be exported to be used in the SDK tools.
+ * @hide
+ */
+@Target({ ElementType.FIELD })
+@Retention(RetentionPolicy.SOURCE)
+public @interface SdkConstant {
+ public static enum SdkConstantType {
+ ACTIVITY_INTENT_ACTION, BROADCAST_INTENT_ACTION, SERVICE_ACTION, INTENT_CATEGORY;
+ }
+
+ SdkConstantType value();
+}
diff --git a/core/java/android/annotation/Widget.java b/core/java/android/annotation/Widget.java
new file mode 100644
index 0000000..6756cd7
--- /dev/null
+++ b/core/java/android/annotation/Widget.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.annotation;
+
+import java.lang.annotation.Target;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Indicates a class is a widget usable by application developers to create UI.
+ * <p>
+ * This must be used in cases where:
+ * <ul>
+ * <li>The widget is not in the package <code>android.widget</code></li>
+ * <li>The widget extends <code>android.view.ViewGroup</code></li>
+ * </ul>
+ * @hide
+ */
+@Target({ ElementType.TYPE })
+@Retention(RetentionPolicy.SOURCE)
+public @interface Widget {
+}
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
new file mode 100644
index 0000000..fa310a5
--- /dev/null
+++ b/core/java/android/app/Activity.java
@@ -0,0 +1,3419 @@
+/*
+ * 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.app;
+
+import android.content.ComponentCallbacks;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.Handler;
+import android.os.IBinder;
+import android.text.Selection;
+import android.text.SpannableStringBuilder;
+import android.text.method.TextKeyListener;
+import android.util.AttributeSet;
+import android.util.Config;
+import android.util.EventLog;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.ContextMenu;
+import android.view.ContextThemeWrapper;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.View.OnCreateContextMenuListener;
+import android.widget.AdapterView;
+
+import com.android.internal.policy.PolicyManager;
+
+import java.util.ArrayList;
+
+/**
+ * An activity is a single, focused thing that the user can do. Almost all
+ * activities interact with the user, so the Activity class takes care of
+ * creating a window for you in which you can place your UI with
+ * {@link #setContentView}. While activities are often presented to the user
+ * as full-screen windows, they can also be used in other ways: as floating
+ * windows (via a theme with {@link android.R.attr#windowIsFloating} set)
+ * or embedded inside of another activity (using {@link ActivityGroup}).
+ *
+ * There are two methods almost all subclasses of Activity will implement:
+ *
+ * <ul>
+ * <li> {@link #onCreate} is where you initialize your activity. Most
+ * importantly, here you will usually call {@link #setContentView(int)}
+ * with a layout resource defining your UI, and using {@link #findViewById}
+ * to retrieve the widgets in that UI that you need to interact with
+ * programmatically.
+ *
+ * <li> {@link #onPause} is where you deal with the user leaving your
+ * activity. Most importantly, any changes made by the user should at this
+ * point be committed (usually to the
+ * {@link android.content.ContentProvider} holding the data).
+ * </ul>
+ *
+ * <p>To be of use with {@link android.content.Context#startActivity Context.startActivity()}, all
+ * activity classes must have a corresponding
+ * {@link android.R.styleable#AndroidManifestActivity &lt;activity&gt;}
+ * declaration in their package's <code>AndroidManifest.xml</code>.</p>
+ *
+ * <p>The Activity class is an important part of an
+ * <a href="{@docRoot}intro/lifecycle.html">application's overall lifecycle</a>,
+ * and the way activities are launched and put together is a fundamental
+ * part of the platform's
+ * <a href="{@docRoot}intro/appmodel.html">application model</a>.</p>
+ *
+ * <p>Topics covered here:
+ * <ol>
+ * <li><a href="#ActivityLifecycle">Activity Lifecycle</a>
+ * <li><a href="#ConfigurationChanges">Configuration Changes</a>
+ * <li><a href="#StartingActivities">Starting Activities and Getting Results</a>
+ * <li><a href="#SavingPersistentState">Saving Persistent State</a>
+ * <li><a href="#Permissions">Permissions</a>
+ * <li><a href="#ProcessLifecycle">Process Lifecycle</a>
+ * </ol>
+ *
+ * <a name="ActivityLifecycle"></a>
+ * <h3>Activity Lifecycle</h3>
+ *
+ * <p>Activities in the system are managed as an <em>activity stack</em>.
+ * When a new activity is started, it is placed on the top of the stack
+ * and becomes the running activity -- the previous activity always remains
+ * below it in the stack, and will not come to the foreground again until
+ * the new activity exits.</p>
+ *
+ * <p>An activity has essentially four states:</p>
+ * <ul>
+ * <li> If an activity in the foreground of the screen (at the top of
+ * the stack),
+ * it is <em>active</em> or <em>running</em>. </li>
+ * <li>If an activity has lost focus but is still visible (that is, a new non-full-sized
+ * or transparent activity has focus on top of your activity), it
+ * is <em>paused</em>. A paused activity is completely alive (it
+ * maintains all state and member information and remains attached to
+ * the window manager), but can be killed by the system in extreme
+ * low memory situations.
+ * <li>If an activity is completely obscured by another activity,
+ * it is <em>stopped</em>. It still retains all state and member information,
+ * however, it is no longer visible to the user so its window is hidden
+ * and it will often be killed by the system when memory is needed
+ * elsewhere.</li>
+ * <li>If an activity is paused or stopped, the system can drop the activity
+ * from memory by either asking it to finish, or simply killing its
+ * process. When it is displayed again to the user, it must be
+ * completely restarted and restored to its previous state.</li>
+ * </ul>
+ *
+ * <p>The following diagram shows the important state paths of an Activity.
+ * The square rectangles represent callback methods you can implement to
+ * perform operations when the Activity moves between states. The colored
+ * ovals are major states the Activity can be in.</p>
+ *
+ * <p><img src="../../../images/activity_lifecycle.png"
+ * alt="State diagram for an Android Activity Lifecycle." border="0" /></p>
+ *
+ * <p>There are three key loops you may be interested in monitoring within your
+ * activity:
+ *
+ * <ul>
+ * <li>The <b>entire lifetime</b> of an activity happens between the first call
+ * to {@link android.app.Activity#onCreate} through to a single final call
+ * to {@link android.app.Activity#onDestroy}. An activity will do all setup
+ * of "global" state in onCreate(), and release all remaining resources in
+ * onDestroy(). For example, if it has a thread running in the background
+ * to download data from the network, it may create that thread in onCreate()
+ * and then stop the thread in onDestroy().
+ *
+ * <li>The <b>visible lifetime</b> of an activity happens between a call to
+ * {@link android.app.Activity#onStart} until a corresponding call to
+ * {@link android.app.Activity#onStop}. During this time the user can see the
+ * activity on-screen, though it may not be in the foreground and interacting
+ * with the user. Between these two methods you can maintain resources that
+ * are needed to show the activity to the user. For example, you can register
+ * a {@link android.content.BroadcastReceiver} in onStart() to monitor for changes
+ * that impact your UI, and unregister it in onStop() when the user an no
+ * longer see what you are displaying. The onStart() and onStop() methods
+ * can be called multiple times, as the activity becomes visible and hidden
+ * to the user.
+ *
+ * <li>The <b>foreground lifetime</b> of an activity happens between a call to
+ * {@link android.app.Activity#onResume} until a corresponding call to
+ * {@link android.app.Activity#onPause}. During this time the activity is
+ * in front of all other activities and interacting with the user. An activity
+ * can frequently go between the resumed and paused states -- for example when
+ * the device goes to sleep, when an activity result is delivered, when a new
+ * intent is delivered -- so the code in these methods should be fairly
+ * lightweight.
+ * </ul>
+ *
+ * <p>The entire lifecycle of an activity is defined by the following
+ * Activity methods. All of these are hooks that you can override
+ * to do appropriate work when the activity changes state. All
+ * activities will implement {@link android.app.Activity#onCreate}
+ * to do their initial setup; many will also implement
+ * {@link android.app.Activity#onPause} to commit changes to data and
+ * otherwise prepare to stop interacting with the user. You should always
+ * call up to your superclass when implementing these methods.</p>
+ *
+ * </p>
+ * <pre class="prettyprint">
+ * public class Activity extends ApplicationContext {
+ * protected void onCreate(Bundle savedInstanceState);
+ *
+ * protected void onStart();
+ *
+ * protected void onRestart();
+ *
+ * protected void onResume();
+ *
+ * protected void onPause();
+ *
+ * protected void onStop();
+ *
+ * protected void onDestroy();
+ * }
+ * </pre>
+ *
+ * <p>In general the movement through an activity's lifecycle looks like
+ * this:</p>
+ *
+ * <table border="2" width="85%" align="center" frame="hsides" rules="rows">
+ * <colgroup align="left" span="3" />
+ * <colgroup align="left" />
+ * <colgroup align="center" />
+ * <colgroup align="center" />
+ *
+ * <thead>
+ * <tr><th colspan="3">Method</th> <th>Description</th> <th>Killable?</th> <th>Next</th></tr>
+ * </thead>
+ *
+ * <tbody>
+ * <tr><th colspan="3" align="left" border="0">{@link android.app.Activity#onCreate onCreate()}</th>
+ * <td>Called when the activity is first created.
+ * This is where you should do all of your normal static set up:
+ * create views, bind data to lists, etc. This method also
+ * provides you with a Bundle containing the activity's previously
+ * frozen state, if there was one.
+ * <p>Always followed by <code>onStart()</code>.</td>
+ * <td align="center">No</td>
+ * <td align="center"><code>onStart()</code></td>
+ * </tr>
+ *
+ * <tr><td rowspan="5" style="border-left: none; border-right: none;">&nbsp;&nbsp;&nbsp;&nbsp;</td>
+ * <th colspan="2" align="left" border="0">{@link android.app.Activity#onRestart onRestart()}</th>
+ * <td>Called after your activity has been stopped, prior to it being
+ * started again.
+ * <p>Always followed by <code>onStart()</code></td>
+ * <td align="center">No</td>
+ * <td align="center"><code>onStart()</code></td>
+ * </tr>
+ *
+ * <tr><th colspan="2" align="left" border="0">{@link android.app.Activity#onStart onStart()}</th>
+ * <td>Called when the activity is becoming visible to the user.
+ * <p>Followed by <code>onResume()</code> if the activity comes
+ * to the foreground, or <code>onStop()</code> if it becomes hidden.</td>
+ * <td align="center">No</td>
+ * <td align="center"><code>onResume()</code> or <code>onStop()</code></td>
+ * </tr>
+ *
+ * <tr><td rowspan="2" style="border-left: none;">&nbsp;&nbsp;&nbsp;&nbsp;</td>
+ * <th align="left" border="0">{@link android.app.Activity#onResume onResume()}</th>
+ * <td>Called when the activity will start
+ * interacting with the user. At this point your activity is at
+ * the top of the activity stack, with user input going to it.
+ * <p>Always followed by <code>onPause()</code>.</td>
+ * <td align="center">No</td>
+ * <td align="center"><code>onPause()</code></td>
+ * </tr>
+ *
+ * <tr><th align="left" border="0">{@link android.app.Activity#onPause onPause()}</th>
+ * <td>Called when the system is about to start resuming a previous
+ * activity. This is typically used to commit unsaved changes to
+ * persistent data, stop animations and other things that may be consuming
+ * CPU, etc. Implementations of this method must be very quick because
+ * the next activity will not be resumed until this method returns.
+ * <p>Followed by either <code>onResume()</code> if the activity
+ * returns back to the front, or <code>onStop()</code> if it becomes
+ * invisible to the user.</td>
+ * <td align="center"><font color="#800000"><strong>Yes</strong></font></td>
+ * <td align="center"><code>onResume()</code> or<br>
+ * <code>onStop()</code></td>
+ * </tr>
+ *
+ * <tr><th colspan="2" align="left" border="0">{@link android.app.Activity#onStop onStop()}</th>
+ * <td>Called when the activity is no longer visible to the user, because
+ * another activity has been resumed and is covering this one. This
+ * may happen either because a new activity is being started, an existing
+ * one is being brought in front of this one, or this one is being
+ * destroyed.
+ * <p>Followed by either <code>onRestart()</code> if
+ * this activity is coming back to interact with the user, or
+ * <code>onDestroy()</code> if this activity is going away.</td>
+ * <td align="center"><font color="#800000"><strong>Yes</strong></font></td>
+ * <td align="center"><code>onRestart()</code> or<br>
+ * <code>onDestroy()</code></td>
+ * </tr>
+ *
+ * <tr><th colspan="3" align="left" border="0">{@link android.app.Activity#onDestroy onDestroy()}</th>
+ * <td>The final call you receive before your
+ * activity is destroyed. This can happen either because the
+ * activity is finishing (someone called {@link Activity#finish} on
+ * it, or because the system is temporarily destroying this
+ * instance of the activity to save space. You can distinguish
+ * between these two scenarios with the {@link
+ * Activity#isFinishing} method.</td>
+ * <td align="center"><font color="#800000"><strong>Yes</strong></font></td>
+ * <td align="center"><em>nothing</em></td>
+ * </tr>
+ * </tbody>
+ * </table>
+ *
+ * <p>Note the "Killable" column in the above table -- for those methods that
+ * are marked as being killable, after that method returns the process hosting the
+ * activity may killed by the system <em>at any time</em> without another line
+ * of its code being executed. Because of this, you should use the
+ * {@link #onPause} method to write any persistent data (such as user edits)
+ * to storage. In addition, the method
+ * {@link #onSaveInstanceState(Bundle)} is called before placing the activity
+ * in such a background state, allowing you to save away any dynamic instance
+ * state in your activity into the given Bundle, to be later received in
+ * {@link #onCreate} if the activity needs to be re-created.
+ * See the <a href="#ProcessLifecycle">Process Lifecycle</a>
+ * section for more information on how the lifecycle of a process is tied
+ * to the activities it is hosting. Note that it is important to save
+ * persistent data in {@link #onPause} instead of {@link #onSaveInstanceState}
+ * because the later is not part of the lifecycle callbacks, so will not
+ * be called in every situation as described in its documentation.</p>
+ *
+ * <p>For those methods that are not marked as being killable, the activity's
+ * process will not be killed by the system starting from the time the method
+ * is called and continuing after it returns. Thus an activity is in the killable
+ * state, for example, between after <code>onPause()</code> to the start of
+ * <code>onResume()</code>.</p>
+ *
+ * <a name="ConfigurationChanges"></a>
+ * <h3>Configuration Changes</h3>
+ *
+ * <p>If the configuration of the device (as defined by the
+ * {@link Configuration Resources.Configuration} class) changes,
+ * then anything displaying a user interface will need to update to match that
+ * configuration. Because Activity is the primary mechanism for interacting
+ * with the user, it includes special support for handling configuration
+ * changes.</p>
+ *
+ * <p>Unless you specify otherwise, a configuration change (such as a change
+ * in screen orientation, language, input devices, etc) will cause your
+ * current activity to be <em>destroyed</em>, going through the normal activity
+ * lifecycle process of {@link #onPause},
+ * {@link #onStop}, and {@link #onDestroy} as appropriate. If the activity
+ * had been in the foreground or visible to the user, once {@link #onDestroy} is
+ * called in that instance then a new instance of the activity will be
+ * created, with whatever savedInstanceState the previous instance had generated
+ * from {@link #onSaveInstanceState}.</p>
+ *
+ * <p>This is done because any application resource,
+ * including layout files, can change based on any configuration value. Thus
+ * the only safe way to handle a configuration change is to re-retrieve all
+ * resources, including layouts, drawables, and strings. Because activities
+ * must already know how to save their state and re-create themselves from
+ * that state, this is a convenient way to have an activity restart itself
+ * with a new configuration.</p>
+ *
+ * <p>In some special cases, you may want to bypass restarting of your
+ * activity based on one or more types of configuration changes. This is
+ * done with the {@link android.R.attr#configChanges android:configChanges}
+ * attribute in its manifest. For any types of configuration changes you say
+ * that you handle there, you will receive a call to your current activity's
+ * {@link #onConfigurationChanged} method instead of being restarted. If
+ * a configuration change involves any that you do not handle, however, the
+ * activity will still be restarted and {@link #onConfigurationChanged}
+ * will not be called.</p>
+ *
+ * <a name="StartingActivities"></a>
+ * <h3>Starting Activities and Getting Results</h3>
+ *
+ * <p>The {@link android.app.Activity#startActivity}
+ * method is used to start a
+ * new activity, which will be placed at the top of the activity stack. It
+ * takes a single argument, an {@link android.content.Intent Intent},
+ * which describes the activity
+ * to be executed.</p>
+ *
+ * <p>Sometimes you want to get a result back from an activity when it
+ * ends. For example, you may start an activity that lets the user pick
+ * a person in a list of contacts; when it ends, it returns the person
+ * that was selected. To do this, you call the
+ * {@link android.app.Activity#startActivityForResult(Intent, int)}
+ * version with a second integer parameter identifying the call. The result
+ * will come back through your {@link android.app.Activity#onActivityResult}
+ * method.</p>
+ *
+ * <p>When an activity exits, it can call
+ * {@link android.app.Activity#setResult(int)}
+ * to return data back to its parent. It must always supply a result code,
+ * which can be the standard results RESULT_CANCELED, RESULT_OK, or any
+ * custom values starting at RESULT_FIRST_USER. In addition, it can optionally
+ * return back an Intent containing any additional data it wants. All of this
+ * information appears back on the
+ * parent's <code>Activity.onActivityResult()</code>, along with the integer
+ * identifier it originally supplied.</p>
+ *
+ * <p>If a child activity fails for any reason (such as crashing), the parent
+ * activity will receive a result with the code RESULT_CANCELED.</p>
+ *
+ * <pre class="prettyprint">
+ * public class MyActivity extends Activity {
+ * ...
+ *
+ * static final int PICK_CONTACT_REQUEST = 0;
+ *
+ * protected boolean onKeyDown(int keyCode, KeyEvent event) {
+ * if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
+ * // When the user center presses, let them pick a contact.
+ * startActivityForResult(
+ * new Intent(Intent.ACTION_PICK,
+ * new Uri("content://contacts")),
+ * PICK_CONTACT_REQUEST);
+ * return true;
+ * }
+ * return false;
+ * }
+ *
+ * protected void onActivityResult(int requestCode, int resultCode,
+ * Intent data) {
+ * if (requestCode == PICK_CONTACT_REQUEST) {
+ * if (resultCode == RESULT_OK) {
+ * // A contact was picked. Here we will just display it
+ * // to the user.
+ * startActivity(new Intent(Intent.ACTION_VIEW, data));
+ * }
+ * }
+ * }
+ * }
+ * </pre>
+ *
+ * <a name="SavingPersistentState"></a>
+ * <h3>Saving Persistent State</h3>
+ *
+ * <p>There are generally two kinds of persistent state than an activity
+ * will deal with: shared document-like data (typically stored in a SQLite
+ * database using a {@linkplain android.content.ContentProvider content provider})
+ * and internal state such as user preferences.</p>
+ *
+ * <p>For content provider data, we suggest that activities use a
+ * "edit in place" user model. That is, any edits a user makes are effectively
+ * made immediately without requiring an additional confirmation step.
+ * Supporting this model is generally a simple matter of following two rules:</p>
+ *
+ * <ul>
+ * <li> <p>When creating a new document, the backing database entry or file for
+ * it is created immediately. For example, if the user chooses to write
+ * a new e-mail, a new entry for that e-mail is created as soon as they
+ * start entering data, so that if they go to any other activity after
+ * that point this e-mail will now appear in the list of drafts.</p>
+ * <li> <p>When an activity's <code>onPause()</code> method is called, it should
+ * commit to the backing content provider or file any changes the user
+ * has made. This ensures that those changes will be seen by any other
+ * activity that is about to run. You will probably want to commit
+ * your data even more aggressively at key times during your
+ * activity's lifecycle: for example before starting a new
+ * activity, before finishing your own activity, when the user
+ * switches between input fields, etc.</p>
+ * </ul>
+ *
+ * <p>This model is designed to prevent data loss when a user is navigating
+ * between activities, and allows the system to safely kill an activity (because
+ * system resources are needed somewhere else) at any time after it has been
+ * paused. Note this implies
+ * that the user pressing BACK from your activity does <em>not</em>
+ * mean "cancel" -- it means to leave the activity with its current contents
+ * saved away. Cancelling edits in an activity must be provided through
+ * some other mechanism, such as an explicit "revert" or "undo" option.</p>
+ *
+ * <p>See the {@linkplain android.content.ContentProvider content package} for
+ * more information about content providers. These are a key aspect of how
+ * different activities invoke and propagate data between themselves.</p>
+ *
+ * <p>The Activity class also provides an API for managing internal persistent state
+ * associated with an activity. This can be used, for example, to remember
+ * the user's preferred initial display in a calendar (day view or week view)
+ * or the user's default home page in a web browser.</p>
+ *
+ * <p>Activity persistent state is managed
+ * with the method {@link #getPreferences},
+ * allowing you to retrieve and
+ * modify a set of name/value pairs associated with the activity. To use
+ * preferences that are shared across multiple application components
+ * (activities, receivers, services, providers), you can use the underlying
+ * {@link Context#getSharedPreferences Context.getSharedPreferences()} method
+ * to retrieve a preferences
+ * object stored under a specific name.
+ * (Note that it is not possible to share settings data across application
+ * packages -- for that you will need a content provider.)</p>
+ *
+ * <p>Here is an excerpt from a calendar activity that stores the user's
+ * preferred view mode in its persistent settings:</p>
+ *
+ * <pre class="prettyprint">
+ * public class CalendarActivity extends Activity {
+ * ...
+ *
+ * static final int DAY_VIEW_MODE = 0;
+ * static final int WEEK_VIEW_MODE = 1;
+ *
+ * private SharedPreferences mPrefs;
+ * private int mCurViewMode;
+ *
+ * protected void onCreate(Bundle savedInstanceState) {
+ * super.onCreate(savedInstanceState);
+ *
+ * SharedPreferences mPrefs = getSharedPreferences();
+ * mCurViewMode = mPrefs.getInt("view_mode" DAY_VIEW_MODE);
+ * }
+ *
+ * protected void onPause() {
+ * super.onPause();
+ *
+ * SharedPreferences.Editor ed = mPrefs.edit();
+ * ed.putInt("view_mode", mCurViewMode);
+ * ed.commit();
+ * }
+ * }
+ * </pre>
+ *
+ * <a name="Permissions"></a>
+ * <h3>Permissions</h3>
+ *
+ * <p>The ability to start a particular Activity can be enforced when it is
+ * declared in its
+ * manifest's {@link android.R.styleable#AndroidManifestActivity &lt;activity&gt;}
+ * tag. By doing so, other applications will need to declare a corresponding
+ * {@link android.R.styleable#AndroidManifestUsesPermission &lt;uses-permission&gt;}
+ * element in their own manifest to be able to start that activity.
+ *
+ * <p>See the <a href="{@docRoot}devel/security.html">Security Model</a>
+ * document for more information on permissions and security in general.
+ *
+ * <a name="ProcessLifecycle"></a>
+ * <h3>Process Lifecycle</h3>
+ *
+ * <p>The Android system attempts to keep application process around for as
+ * long as possible, but eventually will need to remove old processes when
+ * memory runs low. As described in <a href="#ActivityLifecycle">Activity
+ * Lifecycle</a>, the decision about which process to remove is intimately
+ * tied to the state of the user's interaction with it. In general, there
+ * are four states a process can be in based on the activities running in it,
+ * listed here in order of importance. The system will kill less important
+ * processes (the last ones) before it resorts to killing more important
+ * processes (the first ones).
+ *
+ * <ol>
+ * <li> <p>The <b>foreground activity</b> (the activity at the top of the screen
+ * that the user is currently interacting with) is considered the most important.
+ * Its process will only be killed as a last resort, if it uses more memory
+ * than is available on the device. Generally at this point the device has
+ * reached a memory paging state, so this is required in order to keep the user
+ * interface responsive.
+ * <li> <p>A <b>visible activity</b> (an activity that is visible to the user
+ * but not in the foreground, such as one sitting behind a foreground dialog)
+ * is considered extremely important and will not be killed unless that is
+ * required to keep the foreground activity running.
+ * <li> <p>A <b>background activity</b> (an activity that is not visible to
+ * the user and has been paused) is no longer critical, so the system may
+ * safely kill its process to reclaim memory for other foreground or
+ * visible processes. If its process needs to be killed, when the user navigates
+ * back to the activity (making it visible on the screen again), its
+ * {@link #onCreate} method will be called with the savedInstanceState it had previously
+ * supplied in {@link #onSaveInstanceState} so that it can restart itself in the same
+ * state as the user last left it.
+ * <li> <p>An <b>empty process</b> is one hosting no activities or other
+ * application components (such as {@link Service} or
+ * {@link android.content.BroadcastReceiver} classes). These are killed very
+ * quickly by the system as memory becomes low. For this reason, any
+ * background operation you do outside of an activity must be executed in the
+ * context of an activity BroadcastReceiver or Service to ensure that the system
+ * knows it needs to keep your process around.
+ * </ol>
+ *
+ * <p>Sometimes an Activity may need to do a long-running operation that exists
+ * independently of the activity lifecycle itself. An example may be a camera
+ * application that allows you to upload a picture to a web site. The upload
+ * may take a long time, and the application should allow the user to leave
+ * the application will it is executing. To accomplish this, your Activity
+ * should start a {@link Service} in which the upload takes place. This allows
+ * the system to properly prioritize your process (considering it to be more
+ * important than other non-visible applications) for the duration of the
+ * upload, independent of whether the original activity is paused, stopped,
+ * or finished.
+ */
+public class Activity extends ContextThemeWrapper
+ implements LayoutInflater.Factory,
+ Window.Callback, KeyEvent.Callback,
+ OnCreateContextMenuListener, ComponentCallbacks {
+ private static final String TAG = "Activity";
+
+ /** Standard activity result: operation canceled. */
+ public static final int RESULT_CANCELED = 0;
+ /** Standard activity result: operation succeeded. */
+ public static final int RESULT_OK = -1;
+ /** Start of user-defined activity results. */
+ public static final int RESULT_FIRST_USER = 1;
+
+ private static long sInstanceCount = 0;
+
+ private static final String WINDOW_HIERARCHY_TAG = "android:viewHierarchyState";
+ private static final String SAVED_DIALOG_IDS_KEY = "android:savedDialogIds";
+ private static final String SAVED_DIALOGS_TAG = "android:savedDialogs";
+ private static final String SAVED_DIALOG_KEY_PREFIX = "android:dialog_";
+ private static final String SAVED_SEARCH_DIALOG_KEY = "android:search_dialog";
+
+ private SparseArray<Dialog> mManagedDialogs;
+
+ // set by the thread after the constructor and before onCreate(Bundle savedInstanceState) is called.
+ private Instrumentation mInstrumentation;
+ private IBinder mToken;
+ /*package*/ String mEmbeddedID;
+ private Application mApplication;
+ private Intent mIntent;
+ private ComponentName mComponent;
+ /*package*/ ActivityInfo mActivityInfo;
+ /*package*/ ActivityThread mMainThread;
+ private Object mLastNonConfigurationInstance;
+ Activity mParent;
+ boolean mCalled;
+ private boolean mResumed;
+ private boolean mStopped;
+ boolean mFinished;
+ boolean mStartedActivity;
+ /*package*/ int mConfigChangeFlags;
+ /*package*/ Configuration mCurrentConfig;
+
+ private Window mWindow;
+
+ private WindowManager mWindowManager;
+ /*package*/ View mDecor = null;
+
+ private CharSequence mTitle;
+ private int mTitleColor = 0;
+
+ private static final class ManagedCursor {
+ ManagedCursor(Cursor cursor) {
+ mCursor = cursor;
+ mReleased = false;
+ mUpdated = false;
+ }
+
+ private final Cursor mCursor;
+ private boolean mReleased;
+ private boolean mUpdated;
+ }
+ private final ArrayList<ManagedCursor> mManagedCursors =
+ new ArrayList<ManagedCursor>();
+
+ // protected by synchronized (this)
+ int mResultCode = RESULT_CANCELED;
+ Intent mResultData = null;
+
+ private boolean mTitleReady = false;
+
+ private int mDefaultKeyMode = DEFAULT_KEYS_DISABLE;
+ private SpannableStringBuilder mDefaultKeySsb = null;
+
+ protected static final int[] FOCUSED_STATE_SET = {com.android.internal.R.attr.state_focused};
+
+ private Thread mUiThread;
+ private final Handler mHandler = new Handler();
+
+ public Activity() {
+ ++sInstanceCount;
+ }
+
+
+ @Override
+ protected void finalize() throws Throwable {
+ super.finalize();
+ --sInstanceCount;
+ }
+
+ public static long getInstanceCount() {
+ return sInstanceCount;
+ }
+
+ /** Return the intent that started this activity. */
+ public Intent getIntent() {
+ return mIntent;
+ }
+
+ /**
+ * Change the intent returned by {@link #getIntent}. This holds a
+ * reference to the given intent; it does not copy it. Often used in
+ * conjunction with {@link #onNewIntent}.
+ *
+ * @param newIntent The new Intent object to return from getIntent
+ *
+ * @see #getIntent
+ * @see #onNewIntent
+ */
+ public void setIntent(Intent newIntent) {
+ mIntent = newIntent;
+ }
+
+ /** Return the application that owns this activity. */
+ public final Application getApplication() {
+ return mApplication;
+ }
+
+ /** Is this activity embedded inside of another activity? */
+ public final boolean isChild() {
+ return mParent != null;
+ }
+
+ /** Return the parent activity if this view is an embedded child. */
+ public final Activity getParent() {
+ return mParent;
+ }
+
+ /** Retrieve the window manager for showing custom windows. */
+ public WindowManager getWindowManager() {
+ return mWindowManager;
+ }
+
+ /**
+ * Retrieve the current {@link android.view.Window} for the activity.
+ * This can be used to directly access parts of the Window API that
+ * are not available through Activity/Screen.
+ *
+ * @return Window The current window, or null if the activity is not
+ * visual.
+ */
+ public Window getWindow() {
+ return mWindow;
+ }
+
+ /**
+ * Calls {@link android.view.Window#getCurrentFocus} on the
+ * Window of this Activity to return the currently focused view.
+ *
+ * @return View The current View with focus or null.
+ *
+ * @see #getWindow
+ * @see android.view.Window#getCurrentFocus
+ */
+ public View getCurrentFocus() {
+ return mWindow != null ? mWindow.getCurrentFocus() : null;
+ }
+
+ @Override
+ public int getWallpaperDesiredMinimumWidth() {
+ int width = super.getWallpaperDesiredMinimumWidth();
+ return width <= 0 ? getWindowManager().getDefaultDisplay().getWidth() : width;
+ }
+
+ @Override
+ public int getWallpaperDesiredMinimumHeight() {
+ int height = super.getWallpaperDesiredMinimumHeight();
+ return height <= 0 ? getWindowManager().getDefaultDisplay().getHeight() : height;
+ }
+
+ /**
+ * Called when the activity is starting. This is where most initialization
+ * should go: calling {@link #setContentView(int)} to inflate the
+ * activity's UI, using {@link #findViewById} to programmatically interact
+ * with widgets in the UI, calling
+ * {@link #managedQuery(android.net.Uri , String[], String, String[], String)} to retrieve
+ * cursors for data being displayed, etc.
+ *
+ * <p>You can call {@link #finish} from within this function, in
+ * which case onDestroy() will be immediately called without any of the rest
+ * of the activity lifecycle ({@link #onStart}, {@link #onResume},
+ * {@link #onPause}, etc) executing.
+ *
+ * <p><em>Derived classes must call through to the super class's
+ * implementation of this method. If they do not, an exception will be
+ * thrown.</em></p>
+ *
+ * @param savedInstanceState If the activity is being re-initialized after
+ * previously being shut down then this Bundle contains the data it most
+ * recently supplied in {@link #onSaveInstanceState}. <b><i>Note: Otherwise it is null.</i></b>
+ *
+ * @see #onStart
+ * @see #onSaveInstanceState
+ * @see #onRestoreInstanceState
+ * @see #onPostCreate
+ */
+ protected void onCreate(Bundle savedInstanceState) {
+ mCalled = true;
+ }
+
+ /**
+ * The hook for {@link ActivityThread} to restore the state of this activity.
+ *
+ * Calls {@link #onSaveInstanceState(android.os.Bundle)} and
+ * {@link #restoreManagedDialogs(android.os.Bundle)}.
+ *
+ * @param savedInstanceState contains the saved state
+ */
+ final void performRestoreInstanceState(Bundle savedInstanceState) {
+ onRestoreInstanceState(savedInstanceState);
+ restoreManagedDialogs(savedInstanceState);
+
+ // Also restore the state of a search dialog (if any)
+ // TODO more generic than just this manager
+ SearchManager searchManager =
+ (SearchManager) getSystemService(Context.SEARCH_SERVICE);
+ searchManager.restoreSearchDialog(savedInstanceState, SAVED_SEARCH_DIALOG_KEY);
+ }
+
+ /**
+ * This method is called after {@link #onStart} when the activity is
+ * being re-initialized from a previously saved state, given here in
+ * <var>state</var>. Most implementations will simply use {@link #onCreate}
+ * to restore their state, but it is sometimes convenient to do it here
+ * after all of the initialization has been done or to allow subclasses to
+ * decide whether to use your default implementation. The default
+ * implementation of this method performs a restore of any view state that
+ * had previously been frozen by {@link #onSaveInstanceState}.
+ *
+ * <p>This method is called between {@link #onStart} and
+ * {@link #onPostCreate}.
+ *
+ * @param savedInstanceState the data most recently supplied in {@link #onSaveInstanceState}.
+ *
+ * @see #onCreate
+ * @see #onPostCreate
+ * @see #onResume
+ * @see #onSaveInstanceState
+ */
+ protected void onRestoreInstanceState(Bundle savedInstanceState) {
+ if (mWindow != null) {
+ Bundle windowState = savedInstanceState.getBundle(WINDOW_HIERARCHY_TAG);
+ if (windowState != null) {
+ mWindow.restoreHierarchyState(windowState);
+ }
+ }
+ }
+
+ /**
+ * Restore the state of any saved managed dialogs.
+ *
+ * @param savedInstanceState The bundle to restore from.
+ */
+ private void restoreManagedDialogs(Bundle savedInstanceState) {
+ final Bundle b = savedInstanceState.getBundle(SAVED_DIALOGS_TAG);
+ if (b == null) {
+ return;
+ }
+
+ final int[] ids = b.getIntArray(SAVED_DIALOG_IDS_KEY);
+ final int numDialogs = ids.length;
+ mManagedDialogs = new SparseArray<Dialog>(numDialogs);
+ for (int i = 0; i < numDialogs; i++) {
+ final Integer dialogId = ids[i];
+ Bundle dialogState = b.getBundle(savedDialogKeyFor(dialogId));
+ if (dialogState != null) {
+ final Dialog dialog = onCreateDialog(dialogId);
+ dialog.onRestoreInstanceState(dialogState);
+ mManagedDialogs.put(dialogId, dialog);
+ }
+ }
+ }
+
+ private String savedDialogKeyFor(int key) {
+ return SAVED_DIALOG_KEY_PREFIX + key;
+ }
+
+
+ /**
+ * Called when activity start-up is complete (after {@link #onStart}
+ * and {@link #onRestoreInstanceState} have been called). Applications will
+ * generally not implement this method; it is intended for system
+ * classes to do final initialization after application code has run.
+ *
+ * <p><em>Derived classes must call through to the super class's
+ * implementation of this method. If they do not, an exception will be
+ * thrown.</em></p>
+ *
+ * @param savedInstanceState If the activity is being re-initialized after
+ * previously being shut down then this Bundle contains the data it most
+ * recently supplied in {@link #onSaveInstanceState}. <b><i>Note: Otherwise it is null.</i></b>
+ * @see #onCreate
+ */
+ protected void onPostCreate(Bundle savedInstanceState) {
+ if (!isChild()) {
+ mTitleReady = true;
+ onTitleChanged(getTitle(), getTitleColor());
+ }
+ mCalled = true;
+ }
+
+ /**
+ * Called after {@link #onCreate} or {@link #onStop} when the current
+ * activity is now being displayed to the user. It will
+ * be followed by {@link #onRestart}.
+ *
+ * <p><em>Derived classes must call through to the super class's
+ * implementation of this method. If they do not, an exception will be
+ * thrown.</em></p>
+ *
+ * @see #onCreate
+ * @see #onStop
+ * @see #onResume
+ */
+ protected void onStart() {
+ mCalled = true;
+ }
+
+ /**
+ * Called after {@link #onStart} when the current activity is being
+ * re-displayed to the user (the user has navigated back to it). It will
+ * be followed by {@link #onResume}.
+ *
+ * <p>For activities that are using raw {@link Cursor} objects (instead of
+ * creating them through
+ * {@link #managedQuery(android.net.Uri , String[], String, String[], String)},
+ * this is usually the place
+ * where the cursor should be requeried (because you had deactivated it in
+ * {@link #onStop}.
+ *
+ * <p><em>Derived classes must call through to the super class's
+ * implementation of this method. If they do not, an exception will be
+ * thrown.</em></p>
+ *
+ * @see #onStop
+ * @see #onResume
+ */
+ protected void onRestart() {
+ mCalled = true;
+ }
+
+ /**
+ * Called after {@link #onRestoreInstanceState}, {@link #onRestart}, or
+ * {@link #onPause}, for your activity to start interacting with the user.
+ * This is a good place to begin animations, open exclusive-access devices
+ * (such as the camera), etc.
+ *
+ * <p>Keep in mind that onResume is not the best indicator that your activity
+ * is visible to the user; a system window such as the keyguard may be in
+ * front. Use {@link #onWindowFocusChanged} to know for certain that your
+ * activity is visible to the user (for example, to resume a game).
+ *
+ * <p><em>Derived classes must call through to the super class's
+ * implementation of this method. If they do not, an exception will be
+ * thrown.</em></p>
+ *
+ * @see #onRestoreInstanceState
+ * @see #onRestart
+ * @see #onPostResume
+ * @see #onPause
+ */
+ protected void onResume() {
+ mCalled = true;
+ }
+
+ /**
+ * Called when activity resume is complete (after {@link #onResume} has
+ * been called). Applications will generally not implement this method;
+ * it is intended for system classes to do final setup after application
+ * resume code has run.
+ *
+ * <p><em>Derived classes must call through to the super class's
+ * implementation of this method. If they do not, an exception will be
+ * thrown.</em></p>
+ *
+ * @see #onResume
+ */
+ protected void onPostResume() {
+ final Window win = getWindow();
+ if (win != null) win.makeActive();
+ mCalled = true;
+ }
+
+ /**
+ * This is called for activities that set launchMode to "singleTop" in
+ * their package, or if a client used the {@link Intent#FLAG_ACTIVITY_SINGLE_TOP}
+ * flag when calling {@link #startActivity}. In either case, when the
+ * activity is re-launched while at the top of the activity stack instead
+ * of a new instance of the activity being started, onNewIntent() will be
+ * called on the existing instance with the Intent that was used to
+ * re-launch it.
+ *
+ * <p>An activity will always be paused before receiving a new intent, so
+ * you can count on {@link #onResume} being called after this method.
+ *
+ * <p>Note that {@link #getIntent} still returns the original Intent. You
+ * can use {@link #setIntent} to update it to this new Intent.
+ *
+ * @param intent The new intent that was started for the activity.
+ *
+ * @see #getIntent
+ * @see #setIntent
+ * @see #onResume
+ */
+ protected void onNewIntent(Intent intent) {
+ }
+
+ /**
+ * The hook for {@link ActivityThread} to save the state of this activity.
+ *
+ * Calls {@link #onSaveInstanceState(android.os.Bundle)}
+ * and {@link #saveManagedDialogs(android.os.Bundle)}.
+ *
+ * @param outState The bundle to save the state to.
+ */
+ final void performSaveInstanceState(Bundle outState) {
+ onSaveInstanceState(outState);
+ saveManagedDialogs(outState);
+
+ // Also save the state of a search dialog (if any)
+ // TODO more generic than just this manager
+ SearchManager searchManager =
+ (SearchManager) getSystemService(Context.SEARCH_SERVICE);
+ searchManager.saveSearchDialog(outState, SAVED_SEARCH_DIALOG_KEY);
+ }
+
+ /**
+ * Called to retrieve per-instance state from an activity before being killed
+ * so that the state can be restored in {@link #onCreate} or
+ * {@link #onRestoreInstanceState} (the {@link Bundle} populated by this method
+ * will be passed to both).
+ *
+ * <p>This method is called before an activity may be killed so that when it
+ * comes back some time in the future it can restore its state. For example,
+ * if activity B is launched in front of activity A, and at some point activity
+ * A is killed to reclaim resources, activity A will have a chance to save the
+ * current state of its user interface via this method so that when the user
+ * returns to activity A, the state of the user interface can be restored
+ * via {@link #onCreate} or {@link #onRestoreInstanceState}.
+ *
+ * <p>Do not confuse this method with activity lifecycle callbacks such as
+ * {@link #onPause}, which is always called when an activity is being placed
+ * in the background or on its way to destruction, or {@link #onStop} which
+ * is called before destruction. One example of when {@link #onPause} and
+ * {@link #onStop} is called and not this method is when a user navigates back
+ * from activity B to activity A: there is no need to call {@link #onSaveInstanceState}
+ * on B because that particular instance will never be restored, so the
+ * system avoids calling it. An example when {@link #onPause} is called and
+ * not {@link #onSaveInstanceState} is when activity B is launched in front of activity A:
+ * the system may avoid calling {@link #onSaveInstanceState} on activity A if it isn't
+ * killed during the lifetime of B since the state of the user interface of
+ * A will stay intact.
+ *
+ * <p>The default implementation takes care of most of the UI per-instance
+ * state for you by calling {@link android.view.View#onSaveInstanceState()} on each
+ * view in the hierarchy that has an id, and by saving the id of the currently
+ * focused view (all of which is restored by the default implementation of
+ * {@link #onRestoreInstanceState}). If you override this method to save additional
+ * information not captured by each individual view, you will likely want to
+ * call through to the default implementation, otherwise be prepared to save
+ * all of the state of each view yourself.
+ *
+ * <p>If called, this method will occur before {@link #onStop}. There are
+ * no guarantees about whether it will occur before or after {@link #onPause}.
+ *
+ * @param outState Bundle in which to place your saved state.
+ *
+ * @see #onCreate
+ * @see #onRestoreInstanceState
+ * @see #onPause
+ */
+ protected void onSaveInstanceState(Bundle outState) {
+ outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState());
+ }
+
+ /**
+ * Save the state of any managed dialogs.
+ *
+ * @param outState place to store the saved state.
+ */
+ private void saveManagedDialogs(Bundle outState) {
+ if (mManagedDialogs == null) {
+ return;
+ }
+
+ final int numDialogs = mManagedDialogs.size();
+ if (numDialogs == 0) {
+ return;
+ }
+
+ Bundle dialogState = new Bundle();
+
+ int[] ids = new int[mManagedDialogs.size()];
+
+ // save each dialog's bundle, gather the ids
+ for (int i = 0; i < numDialogs; i++) {
+ final int key = mManagedDialogs.keyAt(i);
+ ids[i] = key;
+ final Dialog dialog = mManagedDialogs.valueAt(i);
+ dialogState.putBundle(savedDialogKeyFor(key), dialog.onSaveInstanceState());
+ }
+
+ dialogState.putIntArray(SAVED_DIALOG_IDS_KEY, ids);
+ outState.putBundle(SAVED_DIALOGS_TAG, dialogState);
+ }
+
+
+ /**
+ * Called as part of the activity lifecycle when an activity is going into
+ * the background, but has not (yet) been killed. The counterpart to
+ * {@link #onResume}.
+ *
+ * <p>When activity B is launched in front of activity A, this callback will
+ * be invoked on A. B will not be created until A's {@link #onPause} returns,
+ * so be sure to not do anything lengthy here.
+ *
+ * <p>This callback is mostly used for saving any persistent state the
+ * activity is editing, to present a "edit in place" model to the user and
+ * making sure nothing is lost if there are not enough resources to start
+ * the new activity without first killing this one. This is also a good
+ * place to do things like stop animations and other things that consume a
+ * noticeable mount of CPU in order to make the switch to the next activity
+ * as fast as possible, or to close resources that are exclusive access
+ * such as the camera.
+ *
+ * <p>In situations where the system needs more memory it may kill paused
+ * processes to reclaim resources. Because of this, you should be sure
+ * that all of your state is saved by the time you return from
+ * this function. In general {@link #onSaveInstanceState} is used to save
+ * per-instance state in the activity and this method is used to store
+ * global persistent data (in content providers, files, etc.)
+ *
+ * <p>After receiving this call you will usually receive a following call
+ * to {@link #onStop} (after the next activity has been resumed and
+ * displayed), however in some cases there will be a direct call back to
+ * {@link #onResume} without going through the stopped state.
+ *
+ * <p><em>Derived classes must call through to the super class's
+ * implementation of this method. If they do not, an exception will be
+ * thrown.</em></p>
+ *
+ * @see #onResume
+ * @see #onSaveInstanceState
+ * @see #onStop
+ */
+ protected void onPause() {
+ mCalled = true;
+ }
+
+ /**
+ * Generate a new thumbnail for this activity. This method is called before
+ * pausing the activity, and should draw into <var>outBitmap</var> the
+ * imagery for the desired thumbnail in the dimensions of that bitmap. It
+ * can use the given <var>canvas</var>, which is configured to draw into the
+ * bitmap, for rendering if desired.
+ *
+ * <p>The default implementation renders the Screen's current view
+ * hierarchy into the canvas to generate a thumbnail.
+ *
+ * <p>If you return false, the bitmap will be filled with a default
+ * thumbnail.
+ *
+ * @param outBitmap The bitmap to contain the thumbnail.
+ * @param canvas Can be used to render into the bitmap.
+ *
+ * @return Return true if you have drawn into the bitmap; otherwise after
+ * you return it will be filled with a default thumbnail.
+ *
+ * @see #onCreateDescription
+ * @see #onSaveInstanceState
+ * @see #onPause
+ */
+ public boolean onCreateThumbnail(Bitmap outBitmap, Canvas canvas) {
+ final View view = mDecor;
+ if (view == null) {
+ return false;
+ }
+
+ final int vw = view.getWidth();
+ final int vh = view.getHeight();
+ final int dw = outBitmap.getWidth();
+ final int dh = outBitmap.getHeight();
+
+ canvas.save();
+ canvas.scale(((float)dw)/vw, ((float)dh)/vh);
+ view.draw(canvas);
+ canvas.restore();
+
+ return true;
+ }
+
+ /**
+ * Generate a new description for this activity. This method is called
+ * before pausing the activity and can, if desired, return some textual
+ * description of its current state to be displayed to the user.
+ *
+ * <p>The default implementation returns null, which will cause you to
+ * inherit the description from the previous activity. If all activities
+ * return null, generally the label of the top activity will be used as the
+ * description.
+ *
+ * @return A description of what the user is doing. It should be short and
+ * sweet (only a few words).
+ *
+ * @see #onCreateThumbnail
+ * @see #onSaveInstanceState
+ * @see #onPause
+ */
+ public CharSequence onCreateDescription() {
+ return null;
+ }
+
+ /**
+ * Called when you are no longer visible to the user. You will next
+ * receive either {@link #onStart}, {@link #onDestroy}, or nothing,
+ * depending on later user activity.
+ *
+ * <p>Note that this method may never be called, in low memory situations
+ * where the system does not have enough memory to keep your activity's
+ * process running after its {@link #onPause} method is called.
+ *
+ * <p><em>Derived classes must call through to the super class's
+ * implementation of this method. If they do not, an exception will be
+ * thrown.</em></p>
+ *
+ * @see #onRestart
+ * @see #onResume
+ * @see #onSaveInstanceState
+ * @see #onDestroy
+ */
+ protected void onStop() {
+ mCalled = true;
+ }
+
+ /**
+ * Perform any final cleanup before an activity is destroyed. This can
+ * happen either because the activity is finishing (someone called
+ * {@link #finish} on it, or because the system is temporarily destroying
+ * this instance of the activity to save space. You can distinguish
+ * between these two scenarios with the {@link #isFinishing} method.
+ *
+ * <p><em>Note: do not count on this method being called as a place for
+ * saving data! For example, if an activity is editing data in a content
+ * provider, those edits should be committed in either {@link #onPause} or
+ * {@link #onSaveInstanceState}, not here.</em> This method is usually implemented to
+ * free resources like threads that are associated with an activity, so
+ * that a destroyed activity does not leave such things around while the
+ * rest of its application is still running. There are situations where
+ * the system will simply kill the activity's hosting process without
+ * calling this method (or any others) in it, so it should not be used to
+ * do things that are intended to remain around after the process goes
+ * away.
+ *
+ * <p><em>Derived classes must call through to the super class's
+ * implementation of this method. If they do not, an exception will be
+ * thrown.</em></p>
+ *
+ * @see #onPause
+ * @see #onStop
+ * @see #finish
+ * @see #isFinishing
+ */
+ protected void onDestroy() {
+ mCalled = true;
+
+ // dismiss any dialogs we are managing.
+ if (mManagedDialogs != null) {
+
+ final int numDialogs = mManagedDialogs.size();
+ for (int i = 0; i < numDialogs; i++) {
+ final Dialog dialog = mManagedDialogs.valueAt(i);
+ if (dialog.isShowing()) {
+ dialog.dismiss();
+ }
+ }
+ }
+
+ // also dismiss search dialog if showing
+ // TODO more generic than just this manager
+ SearchManager searchManager =
+ (SearchManager) getSystemService(Context.SEARCH_SERVICE);
+ searchManager.stopSearch();
+
+ // close any cursors we are managing.
+ int numCursors = mManagedCursors.size();
+ for (int i = 0; i < numCursors; i++) {
+ ManagedCursor c = mManagedCursors.get(i);
+ if (c != null) {
+ c.mCursor.close();
+ }
+ }
+ }
+
+ /**
+ * Called by the system when the device configuration changes while your
+ * activity is running. Note that this will <em>only</em> be called if
+ * you have selected configurations you would like to handle with the
+ * {@link android.R.attr#configChanges} attribute in your manifest. If
+ * any configuration change occurs that is not selected to be reported
+ * by that attribute, then instead of reporting it the system will stop
+ * and restart the activity (to have it launched with the new
+ * configuration).
+ *
+ * <p>At the time that this function has been called, your Resources
+ * object will have been updated to return resource values matching the
+ * new configuration.
+ *
+ * @param newConfig The new device configuration.
+ */
+ public void onConfigurationChanged(Configuration newConfig) {
+ mCalled = true;
+
+ // also update search dialog if showing
+ // TODO more generic than just this manager
+ SearchManager searchManager =
+ (SearchManager) getSystemService(Context.SEARCH_SERVICE);
+ searchManager.onConfigurationChanged(newConfig);
+
+ if (mWindow != null) {
+ // Pass the configuration changed event to the window
+ mWindow.onConfigurationChanged(newConfig);
+ }
+ }
+
+ /**
+ * If this activity is being destroyed because it can not handle a
+ * configuration parameter being changed (and thus its
+ * {@link #onConfigurationChanged(Configuration)} method is
+ * <em>not</em> being called), then you can use this method to discover
+ * the set of changes that have occurred while in the process of being
+ * destroyed. Note that there is no guarantee that these will be
+ * accurate (other changes could have happened at any time), so you should
+ * only use this as an optimization hint.
+ *
+ * @return Returns a bit field of the configuration parameters that are
+ * changing, as defined by the {@link android.content.res.Configuration}
+ * class.
+ */
+ public int getChangingConfigurations() {
+ return mConfigChangeFlags;
+ }
+
+ /**
+ * Retrieve the non-configuration instance data that was previously
+ * returned by {@link #onRetainNonConfigurationInstance()}. This will
+ * be available from the initial {@link #onCreate} and
+ * {@link #onStart} calls to the new instance, allowing you to extract
+ * any useful dynamic state from the previous instance.
+ *
+ * <p>Note that the data you retrieve here should <em>only</em> be used
+ * as an optimization for handling configuration changes. You should always
+ * be able to handle getting a null pointer back, and an activity must
+ * still be able to restore itself to its previous state (through the
+ * normal {@link #onSaveInstanceState(Bundle)} mechanism) even if this
+ * function returns null.
+ *
+ * @return Returns the object previously returned by
+ * {@link #onRetainNonConfigurationInstance()}.
+ */
+ public Object getLastNonConfigurationInstance() {
+ return mLastNonConfigurationInstance;
+ }
+
+ /**
+ * Called by the system, as part of destroying an
+ * activity due to a configuration change, when it is known that a new
+ * instance will immediately be created for the new configuration. You
+ * can return any object you like here, including the activity instance
+ * itself, which can later be retrieved by calling
+ * {@link #getLastNonConfigurationInstance()} in the new activity
+ * instance.
+ *
+ * <p>This function is called purely as an optimization, and you must
+ * not rely on it being called. When it is called, a number of guarantees
+ * will be made to help optimize configuration switching:
+ * <ul>
+ * <li> The function will be called between {@link #onStop} and
+ * {@link #onDestroy}.
+ * <li> A new instance of the activity will <em>always</em> be immediately
+ * created after this one's {@link #onDestroy()} is called.
+ * <li> The object you return here will <em>always</em> be available from
+ * the {@link #getLastNonConfigurationInstance()} method of the following
+ * activity instance as described there.
+ * </ul>
+ *
+ * <p>These guarantees are designed so that an activity can use this API
+ * to propagate extensive state from the old to new activity instance, from
+ * loaded bitmaps, to network connections, to evenly actively running
+ * threads. Note that you should <em>not</em> propagate any data that
+ * may change based on the configuration, including any data loaded from
+ * resources such as strings, layouts, or drawables.
+ *
+ * @return Return any Object holding the desired state to propagate to the
+ * next activity instance.
+ */
+ public Object onRetainNonConfigurationInstance() {
+ return null;
+ }
+
+ public void onLowMemory() {
+ mCalled = true;
+ }
+
+ /**
+ * Wrapper around
+ * {@link ContentResolver#query(android.net.Uri , String[], String, String[], String)}
+ * that gives the resulting {@link Cursor} to call
+ * {@link #startManagingCursor} so that the activity will manage its
+ * lifecycle for you.
+ *
+ * @param uri The URI of the content provider to query.
+ * @param projection List of columns to return.
+ * @param selection SQL WHERE clause.
+ * @param sortOrder SQL ORDER BY clause.
+ *
+ * @return The Cursor that was returned by query().
+ *
+ * @see ContentResolver#query(android.net.Uri , String[], String, String[], String)
+ * @see #managedCommitUpdates
+ * @see #startManagingCursor
+ * @hide
+ */
+ public final Cursor managedQuery(Uri uri,
+ String[] projection,
+ String selection,
+ String sortOrder)
+ {
+ Cursor c = getContentResolver().query(uri, projection, selection, null, sortOrder);
+ if (c != null) {
+ startManagingCursor(c);
+ }
+ return c;
+ }
+
+ /**
+ * Wrapper around
+ * {@link ContentResolver#query(android.net.Uri , String[], String, String[], String)}
+ * that gives the resulting {@link Cursor} to call
+ * {@link #startManagingCursor} so that the activity will manage its
+ * lifecycle for you.
+ *
+ * @param uri The URI of the content provider to query.
+ * @param projection List of columns to return.
+ * @param selection SQL WHERE clause.
+ * @param selectionArgs The arguments to selection, if any ?s are pesent
+ * @param sortOrder SQL ORDER BY clause.
+ *
+ * @return The Cursor that was returned by query().
+ *
+ * @see ContentResolver#query(android.net.Uri , String[], String, String[], String)
+ * @see #managedCommitUpdates
+ * @see #startManagingCursor
+ */
+ public final Cursor managedQuery(Uri uri,
+ String[] projection,
+ String selection,
+ String[] selectionArgs,
+ String sortOrder)
+ {
+ Cursor c = getContentResolver().query(uri, projection, selection, selectionArgs, sortOrder);
+ if (c != null) {
+ startManagingCursor(c);
+ }
+ return c;
+ }
+
+ /**
+ * Wrapper around {@link Cursor#commitUpdates()} that takes care of noting
+ * that the Cursor needs to be requeried. You can call this method in
+ * {@link #onPause} or {@link #onStop} to have the system call
+ * {@link Cursor#requery} for you if the activity is later resumed. This
+ * allows you to avoid determing when to do the requery yourself (which is
+ * required for the Cursor to see any data changes that were committed with
+ * it).
+ *
+ * @param c The Cursor whose changes are to be committed.
+ *
+ * @see #managedQuery(android.net.Uri , String[], String, String[], String)
+ * @see #startManagingCursor
+ * @see Cursor#commitUpdates()
+ * @see Cursor#requery
+ * @hide
+ */
+ @Deprecated
+ public void managedCommitUpdates(Cursor c) {
+ synchronized (mManagedCursors) {
+ final int N = mManagedCursors.size();
+ for (int i=0; i<N; i++) {
+ ManagedCursor mc = mManagedCursors.get(i);
+ if (mc.mCursor == c) {
+ c.commitUpdates();
+ mc.mUpdated = true;
+ return;
+ }
+ }
+ throw new RuntimeException(
+ "Cursor " + c + " is not currently managed");
+ }
+ }
+
+ /**
+ * This method allows the activity to take care of managing the given
+ * {@link Cursor}'s lifecycle for you based on the activity's lifecycle.
+ * That is, when the activity is stopped it will automatically call
+ * {@link Cursor#deactivate} on the given Cursor, and when it is later restarted
+ * it will call {@link Cursor#requery} for you. When the activity is
+ * destroyed, all managed Cursors will be closed automatically.
+ *
+ * @param c The Cursor to be managed.
+ *
+ * @see #managedQuery(android.net.Uri , String[], String, String[], String)
+ * @see #stopManagingCursor
+ */
+ public void startManagingCursor(Cursor c) {
+ synchronized (mManagedCursors) {
+ mManagedCursors.add(new ManagedCursor(c));
+ }
+ }
+
+ /**
+ * Given a Cursor that was previously given to
+ * {@link #startManagingCursor}, stop the activity's management of that
+ * cursor.
+ *
+ * @param c The Cursor that was being managed.
+ *
+ * @see #startManagingCursor
+ */
+ public void stopManagingCursor(Cursor c) {
+ synchronized (mManagedCursors) {
+ final int N = mManagedCursors.size();
+ for (int i=0; i<N; i++) {
+ ManagedCursor mc = mManagedCursors.get(i);
+ if (mc.mCursor == c) {
+ mManagedCursors.remove(i);
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Control whether this activity is required to be persistent. By default
+ * activities are not persistent; setting this to true will prevent the
+ * system from stopping this activity or its process when running low on
+ * resources.
+ *
+ * <p><em>You should avoid using this method</em>, it has severe negative
+ * consequences on how well the system can manage its resources. A better
+ * approach is to implement an application service that you control with
+ * {@link Context#startService} and {@link Context#stopService}.
+ *
+ * @param isPersistent Control whether the current activity must be
+ * persistent, true if so, false for the normal
+ * behavior.
+ */
+ public void setPersistent(boolean isPersistent) {
+ if (mParent == null) {
+ try {
+ ActivityManagerNative.getDefault()
+ .setPersistent(mToken, isPersistent);
+ } catch (RemoteException e) {
+ // Empty
+ }
+ } else {
+ throw new RuntimeException("setPersistent() not yet supported for embedded activities");
+ }
+ }
+
+ /**
+ * Finds a view that was identified by the id attribute from the XML that
+ * was processed in {@link #onCreate}.
+ *
+ * @return The view if found or null otherwise.
+ */
+ public View findViewById(int id) {
+ return getWindow().findViewById(id);
+ }
+
+ /**
+ * Set the activity content from a layout resource. The resource will be
+ * inflated, adding all top-level views to the activity.
+ *
+ * @param layoutResID Resource ID to be inflated.
+ */
+ public void setContentView(int layoutResID) {
+ getWindow().setContentView(layoutResID);
+ }
+
+ /**
+ * Set the activity content to an explicit view. This view is placed
+ * directly into the activity's view hierarchy. It can itself be a complex
+ * view hierarhcy.
+ *
+ * @param view The desired content to display.
+ */
+ public void setContentView(View view) {
+ getWindow().setContentView(view);
+ }
+
+ /**
+ * Set the activity content to an explicit view. This view is placed
+ * directly into the activity's view hierarchy. It can itself be a complex
+ * view hierarhcy.
+ *
+ * @param view The desired content to display.
+ * @param params Layout parameters for the view.
+ */
+ public void setContentView(View view, ViewGroup.LayoutParams params) {
+ getWindow().setContentView(view, params);
+ }
+
+ /**
+ * Add an additional content view to the activity. Added after any existing
+ * ones in the activity -- existing views are NOT removed.
+ *
+ * @param view The desired content to display.
+ * @param params Layout parameters for the view.
+ */
+ public void addContentView(View view, ViewGroup.LayoutParams params) {
+ getWindow().addContentView(view, params);
+ }
+
+ /**
+ * Use with {@link #setDefaultKeyMode} to turn off default handling of
+ * keys.
+ *
+ * @see #setDefaultKeyMode
+ */
+ static public final int DEFAULT_KEYS_DISABLE = 0;
+ /**
+ * Use with {@link #setDefaultKeyMode} to launch the dialer during default
+ * key handling.
+ *
+ * @see #setDefaultKeyMode
+ */
+ static public final int DEFAULT_KEYS_DIALER = 1;
+ /**
+ * Use with {@link #setDefaultKeyMode} to execute a menu shortcut in
+ * default key handling.
+ *
+ * <p>That is, the user does not need to hold down the menu key to execute menu shortcuts.
+ *
+ * @see #setDefaultKeyMode
+ */
+ static public final int DEFAULT_KEYS_SHORTCUT = 2;
+ /**
+ * Use with {@link #setDefaultKeyMode} to specify that unhandled keystrokes
+ * will start an application-defined search. (If the application or activity does not
+ * actually define a search, the the keys will be ignored.)
+ *
+ * <p>See {@link android.app.SearchManager android.app.SearchManager} for more details.
+ *
+ * @see #setDefaultKeyMode
+ */
+ static public final int DEFAULT_KEYS_SEARCH_LOCAL = 3;
+
+ /**
+ * Use with {@link #setDefaultKeyMode} to specify that unhandled keystrokes
+ * will start a global search (typically web search, but some platforms may define alternate
+ * methods for global search)
+ *
+ * <p>See {@link android.app.SearchManager android.app.SearchManager} for more details.
+ *
+ * @see #setDefaultKeyMode
+ */
+ static public final int DEFAULT_KEYS_SEARCH_GLOBAL = 4;
+
+ /**
+ * Select the default key handling for this activity. This controls what
+ * will happen to key events that are not otherwise handled. The default
+ * mode ({@link #DEFAULT_KEYS_DISABLE}) will simply drop them on the
+ * floor. Other modes allow you to launch the dialer
+ * ({@link #DEFAULT_KEYS_DIALER}), execute a shortcut in your options
+ * menu without requiring the menu key be held down
+ * ({@link #DEFAULT_KEYS_SHORTCUT}), or launch a search ({@link #DEFAULT_KEYS_SEARCH_LOCAL}
+ * and {@link #DEFAULT_KEYS_SEARCH_GLOBAL}).
+ *
+ * <p>Note that the mode selected here does not impact the default
+ * handling of system keys, such as the "back" and "menu" keys, and your
+ * activity and its views always get a first chance to receive and handle
+ * all application keys.
+ *
+ * @param mode The desired default key mode constant.
+ *
+ * @see #DEFAULT_KEYS_DISABLE
+ * @see #DEFAULT_KEYS_DIALER
+ * @see #DEFAULT_KEYS_SHORTCUT
+ * @see #DEFAULT_KEYS_SEARCH_LOCAL
+ * @see #DEFAULT_KEYS_SEARCH_GLOBAL
+ * @see #onKeyDown
+ */
+ public final void setDefaultKeyMode(int mode) {
+ mDefaultKeyMode = mode;
+
+ // Some modes use a SpannableStringBuilder to track & dispatch input events
+ // This list must remain in sync with the switch in onKeyDown()
+ switch (mode) {
+ case DEFAULT_KEYS_DISABLE:
+ case DEFAULT_KEYS_SHORTCUT:
+ mDefaultKeySsb = null; // not used in these modes
+ break;
+ case DEFAULT_KEYS_DIALER:
+ case DEFAULT_KEYS_SEARCH_LOCAL:
+ case DEFAULT_KEYS_SEARCH_GLOBAL:
+ mDefaultKeySsb = new SpannableStringBuilder();
+ Selection.setSelection(mDefaultKeySsb,0);
+ break;
+ default:
+ throw new IllegalArgumentException();
+ }
+ }
+
+ /**
+ * Called when a key was pressed down and not handled by any of the views
+ * inside of the activity. So, for example, key presses while the cursor
+ * is inside a TextView will not trigger the event (unless it is a navigation
+ * to another object) because TextView handles its own key presses.
+ *
+ * <p>If the focused view didn't want this event, this method is called.
+ *
+ * <p>The default implementation handles KEYCODE_BACK to stop the activity
+ * and go back, and other default key handling if configured with {@link #setDefaultKeyMode}.
+ *
+ * @return Return <code>true</code> to prevent this event from being propagated
+ * further, or <code>false</code> to indicate that you have not handled
+ * this event and it should continue to be propagated.
+ * @see #onKeyUp
+ * @see android.view.KeyEvent
+ */
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) {
+ finish();
+ return true;
+ }
+
+ if (mDefaultKeyMode == DEFAULT_KEYS_DISABLE) {
+ return false;
+ } else if (mDefaultKeyMode == DEFAULT_KEYS_SHORTCUT) {
+ return getWindow().performPanelShortcut(Window.FEATURE_OPTIONS_PANEL,
+ keyCode, event, Menu.FLAG_ALWAYS_PERFORM_CLOSE);
+ } else {
+ // Common code for DEFAULT_KEYS_DIALER & DEFAULT_KEYS_SEARCH_*
+ boolean clearSpannable = false;
+ boolean handled;
+ if ((event.getRepeatCount() != 0) || event.isSystem()) {
+ clearSpannable = true;
+ handled = false;
+ } else {
+ handled = TextKeyListener.getInstance().onKeyDown(null, mDefaultKeySsb,
+ keyCode, event);
+ if (handled && mDefaultKeySsb.length() > 0) {
+ // something useable has been typed - dispatch it now.
+
+ final String str = mDefaultKeySsb.toString();
+ clearSpannable = true;
+
+ switch (mDefaultKeyMode) {
+ case DEFAULT_KEYS_DIALER:
+ Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + str));
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(intent);
+ break;
+ case DEFAULT_KEYS_SEARCH_LOCAL:
+ startSearch(str, false, null, false);
+ break;
+ case DEFAULT_KEYS_SEARCH_GLOBAL:
+ startSearch(str, false, null, true);
+ break;
+ }
+ }
+ }
+ if (clearSpannable) {
+ mDefaultKeySsb.clear();
+ mDefaultKeySsb.clearSpans();
+ Selection.setSelection(mDefaultKeySsb,0);
+ }
+ return handled;
+ }
+ }
+
+ /**
+ * Called when a key was released and not handled by any of the views
+ * inside of the activity. So, for example, key presses while the cursor
+ * is inside a TextView will not trigger the event (unless it is a navigation
+ * to another object) because TextView handles its own key presses.
+ *
+ * @return Return <code>true</code> to prevent this event from being propagated
+ * further, or <code>false</code> to indicate that you have not handled
+ * this event and it should continue to be propagated.
+ * @see #onKeyDown
+ * @see KeyEvent
+ */
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ return false;
+ }
+
+ /**
+ * Default implementation of {@link KeyEvent.Callback#onKeyMultiple(int, int, KeyEvent)
+ * KeyEvent.Callback.onKeyMultiple()}: always returns false (doesn't handle
+ * the event).
+ */
+ public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
+ return false;
+ }
+
+ /**
+ * Called when a touch screen event was not handled by any of the views
+ * under it. This is most useful to process touch events that happen
+ * outside of your window bounds, where there is no view to receive it.
+ *
+ * @param event The touch screen event being processed.
+ *
+ * @return Return true if you have consumed the event, false if you haven't.
+ * The default implementation always returns false.
+ */
+ public boolean onTouchEvent(MotionEvent event) {
+ return false;
+ }
+
+ /**
+ * Called when the trackball was moved and not handled by any of the
+ * views inside of the activity. So, for example, if the trackball moves
+ * while focus is on a button, you will receive a call here because
+ * buttons do not normally do anything with trackball events. The call
+ * here happens <em>before</em> trackball movements are converted to
+ * DPAD key events, which then get sent back to the view hierarchy, and
+ * will be processed at the point for things like focus navigation.
+ *
+ * @param event The trackball event being processed.
+ *
+ * @return Return true if you have consumed the event, false if you haven't.
+ * The default implementation always returns false.
+ */
+ public boolean onTrackballEvent(MotionEvent event) {
+ return false;
+ }
+
+ public void onWindowAttributesChanged(WindowManager.LayoutParams params) {
+ // Update window manager if: we have a view, that view is
+ // attached to its parent (which will be a RootView), and
+ // this activity is not embedded.
+ if (mParent == null) {
+ View decor = mDecor;
+ if (decor != null && decor.getParent() != null) {
+ getWindowManager().updateViewLayout(decor, params);
+ }
+ }
+ }
+
+ public void onContentChanged() {
+ }
+
+ /**
+ * Called when the current {@link Window} of the activity gains or loses
+ * focus. This is the best indicator of whether this activity is visible
+ * to the user.
+ *
+ * @param hasFocus Whether the window of this activity has focus.
+ */
+ public void onWindowFocusChanged(boolean hasFocus) {
+ }
+
+ /**
+ * Called to process key events. You can override this to intercept all
+ * key events before they are dispatched to the window. Be sure to call
+ * this implementation for key events that should be handled normally.
+ *
+ * @param event The key event.
+ *
+ * @return boolean Return true if this event was consumed.
+ */
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ if (getWindow().superDispatchKeyEvent(event)) {
+ return true;
+ }
+ return event.dispatch(this);
+ }
+
+ /**
+ * Called to process touch screen events. You can override this to
+ * intercept all touch screen events before they are dispatched to the
+ * window. Be sure to call this implementation for touch screen events
+ * that should be handled normally.
+ *
+ * @param ev The touch screen event.
+ *
+ * @return boolean Return true if this event was consumed.
+ */
+ public boolean dispatchTouchEvent(MotionEvent ev) {
+ if (getWindow().superDispatchTouchEvent(ev)) {
+ return true;
+ }
+ return onTouchEvent(ev);
+ }
+
+ /**
+ * Called to process trackball events. You can override this to
+ * intercept all trackball events before they are dispatched to the
+ * window. Be sure to call this implementation for trackball events
+ * that should be handled normally.
+ *
+ * @param ev The trackball event.
+ *
+ * @return boolean Return true if this event was consumed.
+ */
+ public boolean dispatchTrackballEvent(MotionEvent ev) {
+ if (getWindow().superDispatchTrackballEvent(ev)) {
+ return true;
+ }
+ return onTrackballEvent(ev);
+ }
+
+ /**
+ * Default implementation of
+ * {@link android.view.Window.Callback#onCreatePanelView}
+ * for activities. This
+ * simply returns null so that all panel sub-windows will have the default
+ * menu behavior.
+ */
+ public View onCreatePanelView(int featureId) {
+ return null;
+ }
+
+ /**
+ * Default implementation of
+ * {@link android.view.Window.Callback#onCreatePanelMenu}
+ * for activities. This calls through to the new
+ * {@link #onCreateOptionsMenu} method for the
+ * {@link android.view.Window#FEATURE_OPTIONS_PANEL} panel,
+ * so that subclasses of Activity don't need to deal with feature codes.
+ */
+ public boolean onCreatePanelMenu(int featureId, Menu menu) {
+ if (featureId == Window.FEATURE_OPTIONS_PANEL) {
+ return onCreateOptionsMenu(menu);
+ }
+ return false;
+ }
+
+ /**
+ * Default implementation of
+ * {@link android.view.Window.Callback#onPreparePanel}
+ * for activities. This
+ * calls through to the new {@link #onPrepareOptionsMenu} method for the
+ * {@link android.view.Window#FEATURE_OPTIONS_PANEL}
+ * panel, so that subclasses of
+ * Activity don't need to deal with feature codes.
+ */
+ public boolean onPreparePanel(int featureId, View view, Menu menu) {
+ if (featureId == Window.FEATURE_OPTIONS_PANEL && menu != null) {
+ boolean goforit = onPrepareOptionsMenu(menu);
+ return goforit && menu.hasVisibleItems();
+ }
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @return The default implementation returns true.
+ */
+ public boolean onMenuOpened(int featureId, Menu menu) {
+ return true;
+ }
+
+ /**
+ * Default implementation of
+ * {@link android.view.Window.Callback#onMenuItemSelected}
+ * for activities. This calls through to the new
+ * {@link #onOptionsItemSelected} method for the
+ * {@link android.view.Window#FEATURE_OPTIONS_PANEL}
+ * panel, so that subclasses of
+ * Activity don't need to deal with feature codes.
+ */
+ public boolean onMenuItemSelected(int featureId, MenuItem item) {
+ switch (featureId) {
+ case Window.FEATURE_OPTIONS_PANEL:
+ // Put event logging here so it gets called even if subclass
+ // doesn't call through to superclass's implmeentation of each
+ // of these methods below
+ EventLog.writeEvent(50000, 0, item.getTitleCondensed());
+ return onOptionsItemSelected(item);
+
+ case Window.FEATURE_CONTEXT_MENU:
+ EventLog.writeEvent(50000, 1, item.getTitleCondensed());
+ return onContextItemSelected(item);
+
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Default implementation of
+ * {@link android.view.Window.Callback#onPanelClosed(int, Menu)} for
+ * activities. This calls through to {@link #onOptionsMenuClosed(Menu)}
+ * method for the {@link android.view.Window#FEATURE_OPTIONS_PANEL} panel,
+ * so that subclasses of Activity don't need to deal with feature codes.
+ * For context menus ({@link Window#FEATURE_CONTEXT_MENU}), the
+ * {@link #onContextMenuClosed(Menu)} will be called.
+ */
+ public void onPanelClosed(int featureId, Menu menu) {
+ switch (featureId) {
+ case Window.FEATURE_OPTIONS_PANEL:
+ onOptionsMenuClosed(menu);
+ break;
+
+ case Window.FEATURE_CONTEXT_MENU:
+ onContextMenuClosed(menu);
+ break;
+ }
+ }
+
+ /**
+ * Initialize the contents of the Activity's standard options menu. You
+ * should place your menu items in to <var>menu</var>.
+ *
+ * <p>This is only called once, the first time the options menu is
+ * displayed. To update the menu every time it is displayed, see
+ * {@link #onPrepareOptionsMenu}.
+ *
+ * <p>The default implementation populates the menu with standard system
+ * menu items. These are placed in the {@link Menu#CATEGORY_SYSTEM} group so that
+ * they will be correctly ordered with application-defined menu items.
+ * Deriving classes should always call through to the base implementation.
+ *
+ * <p>You can safely hold on to <var>menu</var> (and any items created
+ * from it), making modifications to it as desired, until the next
+ * time onCreateOptionsMenu() is called.
+ *
+ * <p>When you add items to the menu, you can implement the Activity's
+ * {@link #onOptionsItemSelected} method to handle them there.
+ *
+ * @param menu The options menu in which you place your items.
+ *
+ * @return You must return true for the menu to be displayed;
+ * if you return false it will not be shown.
+ *
+ * @see #onPrepareOptionsMenu
+ * @see #onOptionsItemSelected
+ */
+ public boolean onCreateOptionsMenu(Menu menu) {
+ if (mParent != null) {
+ return mParent.onCreateOptionsMenu(menu);
+ }
+ return true;
+ }
+
+ /**
+ * Prepare the Screen's standard options menu to be displayed. This is
+ * called right before the menu is shown, every time it is shown. You can
+ * use this method to efficiently enable/disable items or otherwise
+ * dynamically modify the contents.
+ *
+ * <p>The default implementation updates the system menu items based on the
+ * activity's state. Deriving classes should always call through to the
+ * base class implementation.
+ *
+ * @param menu The options menu as last shown or first initialized by
+ * onCreateOptionsMenu().
+ *
+ * @return You must return true for the menu to be displayed;
+ * if you return false it will not be shown.
+ *
+ * @see #onCreateOptionsMenu
+ */
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ if (mParent != null) {
+ return mParent.onPrepareOptionsMenu(menu);
+ }
+ return true;
+ }
+
+ /**
+ * This hook is called whenever an item in your options menu is selected.
+ * The default implementation simply returns false to have the normal
+ * processing happen (calling the item's Runnable or sending a message to
+ * its Handler as appropriate). You can use this method for any items
+ * for which you would like to do processing without those other
+ * facilities.
+ *
+ * <p>Derived classes should call through to the base class for it to
+ * perform the default menu handling.
+ *
+ * @param item The menu item that was selected.
+ *
+ * @return boolean Return false to allow normal menu processing to
+ * proceed, true to consume it here.
+ *
+ * @see #onCreateOptionsMenu
+ */
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (mParent != null) {
+ return mParent.onOptionsItemSelected(item);
+ }
+ return false;
+ }
+
+ /**
+ * This hook is called whenever the options menu is being closed (either by the user canceling
+ * the menu with the back/menu button, or when an item is selected).
+ *
+ * @param menu The options menu as last shown or first initialized by
+ * onCreateOptionsMenu().
+ */
+ public void onOptionsMenuClosed(Menu menu) {
+ if (mParent != null) {
+ mParent.onOptionsMenuClosed(menu);
+ }
+ }
+
+ /**
+ * Programmatically opens the options menu. If the options menu is already
+ * open, this method does nothing.
+ */
+ public void openOptionsMenu() {
+ mWindow.openPanel(Window.FEATURE_OPTIONS_PANEL, null);
+ }
+
+ /**
+ * Progammatically closes the options menu. If the options menu is already
+ * closed, this method does nothing.
+ */
+ public void closeOptionsMenu() {
+ mWindow.closePanel(Window.FEATURE_OPTIONS_PANEL);
+ }
+
+ /**
+ * Called when a context menu for the {@code view} is about to be shown.
+ * Unlike {@link #onCreateOptionsMenu(Menu)}, this will be called every
+ * time the context menu is about to be shown and should be populated for
+ * the view (or item inside the view for {@link AdapterView} subclasses,
+ * this can be found in the {@code menuInfo})).
+ * <p>
+ * Use {@link #onContextItemSelected(android.view.MenuItem)} to know when an
+ * item has been selected.
+ * <p>
+ * It is not safe to hold onto the context menu after this method returns.
+ * {@inheritDoc}
+ */
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+ }
+
+ /**
+ * Registers a context menu to be shown for the given view (multiple views
+ * can show the context menu). This method will set the
+ * {@link OnCreateContextMenuListener} on the view to this activity, so
+ * {@link #onCreateContextMenu(ContextMenu, View, ContextMenuInfo)} will be
+ * called when it is time to show the context menu.
+ *
+ * @see #unregisterForContextMenu(View)
+ * @param view The view that should show a context menu.
+ */
+ public void registerForContextMenu(View view) {
+ view.setOnCreateContextMenuListener(this);
+ }
+
+ /**
+ * Prevents a context menu to be shown for the given view. This method will remove the
+ * {@link OnCreateContextMenuListener} on the view.
+ *
+ * @see #registerForContextMenu(View)
+ * @param view The view that should stop showing a context menu.
+ */
+ public void unregisterForContextMenu(View view) {
+ view.setOnCreateContextMenuListener(null);
+ }
+
+ /**
+ * Programmatically opens the context menu for a particular {@code view}.
+ * The {@code view} should have been added via
+ * {@link #registerForContextMenu(View)}.
+ *
+ * @param view The view to show the context menu for.
+ */
+ public void openContextMenu(View view) {
+ view.showContextMenu();
+ }
+
+ /**
+ * This hook is called whenever an item in a context menu is selected. The
+ * default implementation simply returns false to have the normal processing
+ * happen (calling the item's Runnable or sending a message to its Handler
+ * as appropriate). You can use this method for any items for which you
+ * would like to do processing without those other facilities.
+ * <p>
+ * Use {@link MenuItem#getMenuInfo()} to get extra information set by the
+ * View that added this menu item.
+ * <p>
+ * Derived classes should call through to the base class for it to perform
+ * the default menu handling.
+ *
+ * @param item The context menu item that was selected.
+ * @return boolean Return false to allow normal context menu processing to
+ * proceed, true to consume it here.
+ */
+ public boolean onContextItemSelected(MenuItem item) {
+ if (mParent != null) {
+ return mParent.onContextItemSelected(item);
+ }
+ return false;
+ }
+
+ /**
+ * This hook is called whenever the context menu is being closed (either by
+ * the user canceling the menu with the back/menu button, or when an item is
+ * selected).
+ *
+ * @param menu The context menu that is being closed.
+ */
+ public void onContextMenuClosed(Menu menu) {
+ if (mParent != null) {
+ mParent.onContextMenuClosed(menu);
+ }
+ }
+
+ /**
+ * Callback for creating dialogs that are managed (saved and restored) for you
+ * by the activity.
+ *
+ * If you use {@link #showDialog(int)}, the activity will call through to
+ * this method the first time, and hang onto it thereafter. Any dialog
+ * that is created by this method will automatically be saved and restored
+ * for you, including whether it is showing.
+ *
+ * If you would like the activity to manage the saving and restoring dialogs
+ * for you, you should override this method and handle any ids that are
+ * passed to {@link #showDialog}.
+ *
+ * If you would like an opportunity to prepare your dialog before it is shown,
+ * override {@link #onPrepareDialog(int, Dialog)}.
+ *
+ * @param id The id of the dialog.
+ * @return The dialog
+ *
+ * @see #onPrepareDialog(int, Dialog)
+ * @see #showDialog(int)
+ * @see #dismissDialog(int)
+ * @see #removeDialog(int)
+ */
+ protected Dialog onCreateDialog(int id) {
+ return null;
+ }
+
+ /**
+ * Provides an opportunity to prepare a managed dialog before it is being
+ * shown.
+ * <p>
+ * Override this if you need to update a managed dialog based on the state
+ * of the application each time it is shown. For example, a time picker
+ * dialog might want to be updated with the current time. You should call
+ * through to the superclass's implementation. The default implementation
+ * will set this Activity as the owner activity on the Dialog.
+ *
+ * @param id The id of the managed dialog.
+ * @param dialog The dialog.
+ * @see #onCreateDialog(int)
+ * @see #showDialog(int)
+ * @see #dismissDialog(int)
+ * @see #removeDialog(int)
+ */
+ protected void onPrepareDialog(int id, Dialog dialog) {
+ dialog.setOwnerActivity(this);
+ }
+
+ /**
+ * Show a dialog managed by this activity. A call to {@link #onCreateDialog(int)}
+ * will be made with the same id the first time this is called for a given
+ * id. From thereafter, the dialog will be automatically saved and restored.
+ *
+ * Each time a dialog is shown, {@link #onPrepareDialog(int, Dialog)} will
+ * be made to provide an opportunity to do any timely preparation.
+ *
+ * @param id The id of the managed dialog.
+ *
+ * @see #onCreateDialog(int)
+ * @see #onPrepareDialog(int, Dialog)
+ * @see #dismissDialog(int)
+ * @see #removeDialog(int)
+ */
+ public final void showDialog(int id) {
+ if (mManagedDialogs == null) {
+ mManagedDialogs = new SparseArray<Dialog>();
+ }
+ Dialog dialog = mManagedDialogs.get(id);
+ if (dialog == null) {
+ dialog = onCreateDialog(id);
+ if (dialog == null) {
+ throw new IllegalArgumentException("Activity#onCreateDialog did "
+ + "not create a dialog for id " + id);
+ }
+ dialog.dispatchOnCreate(null);
+ mManagedDialogs.put(id, dialog);
+ }
+
+ onPrepareDialog(id, dialog);
+ dialog.show();
+ }
+
+ /**
+ * Dismiss a dialog that was previously shown via {@link #showDialog(int)}.
+ *
+ * @param id The id of the managed dialog.
+ *
+ * @throws IllegalArgumentException if the id was not previously shown via
+ * {@link #showDialog(int)}.
+ *
+ * @see #onCreateDialog(int)
+ * @see #onPrepareDialog(int, Dialog)
+ * @see #showDialog(int)
+ * @see #removeDialog(int)
+ */
+ public final void dismissDialog(int id) {
+ if (mManagedDialogs == null) {
+ throw missingDialog(id);
+
+ }
+ final Dialog dialog = mManagedDialogs.get(id);
+ if (dialog == null) {
+ throw missingDialog(id);
+ }
+ dialog.dismiss();
+ }
+
+ /**
+ * Creates an exception to throw if a user passed in a dialog id that is
+ * unexpected.
+ */
+ private IllegalArgumentException missingDialog(int id) {
+ return new IllegalArgumentException("no dialog with id " + id + " was ever "
+ + "shown via Activity#showDialog");
+ }
+
+ /**
+ * Removes any internal references to a dialog managed by this Activity.
+ * If the dialog is showing, it will dismiss it as part of the clean up.
+ *
+ * This can be useful if you know that you will never show a dialog again and
+ * want to avoid the overhead of saving and restoring it in the future.
+ *
+ * @param id The id of the managed dialog.
+ *
+ * @see #onCreateDialog(int)
+ * @see #onPrepareDialog(int, Dialog)
+ * @see #showDialog(int)
+ * @see #dismissDialog(int)
+ */
+ public final void removeDialog(int id) {
+
+ if (mManagedDialogs == null) {
+ return;
+ }
+
+ final Dialog dialog = mManagedDialogs.get(id);
+ if (dialog == null) {
+ return;
+ }
+
+ dialog.dismiss();
+ mManagedDialogs.remove(id);
+ }
+
+ /**
+ * This hook is called when the user signals the desire to start a search.
+ *
+ * <p>You can use this function as a simple way to launch the search UI, in response to a
+ * menu item, search button, or other widgets within your activity. Unless overidden,
+ * calling this function is the same as calling:
+ * <p>The default implementation simply calls
+ * {@link #startSearch startSearch(null, false, null, false)}, launching a local search.
+ *
+ * <p>You can override this function to force global search, e.g. in response to a dedicated
+ * search key, or to block search entirely (by simply returning false).
+ *
+ * @return Returns true if search launched, false if activity blocks it
+ *
+ * @see android.app.SearchManager
+ */
+ public boolean onSearchRequested() {
+ startSearch(null, false, null, false);
+ return true;
+ }
+
+ /**
+ * This hook is called to launch the search UI.
+ *
+ * <p>It is typically called from onSearchRequested(), either directly from
+ * Activity.onSearchRequested() or from an overridden version in any given
+ * Activity. If your goal is simply to activate search, it is preferred to call
+ * onSearchRequested(), which may have been overriden elsewhere in your Activity. If your goal
+ * is to inject specific data such as context data, it is preferred to <i>override</i>
+ * onSearchRequested(), so that any callers to it will benefit from the override.
+ *
+ * @param initialQuery Any non-null non-empty string will be inserted as
+ * pre-entered text in the search query box.
+ * @param selectInitialQuery If true, the intial query will be preselected, which means that
+ * any further typing will replace it. This is useful for cases where an entire pre-formed
+ * query is being inserted. If false, the selection point will be placed at the end of the
+ * inserted query. This is useful when the inserted query is text that the user entered,
+ * and the user would expect to be able to keep typing. <i>This parameter is only meaningful
+ * if initialQuery is a non-empty string.</i>
+ * @param appSearchData An application can insert application-specific
+ * context here, in order to improve quality or specificity of its own
+ * searches. This data will be returned with SEARCH intent(s). Null if
+ * no extra data is required.
+ * @param globalSearch If false, this will only launch the search that has been specifically
+ * defined by the application (which is usually defined as a local search). If no default
+ * search is defined in the current application or activity, no search will be launched.
+ * If true, this will always launch a platform-global (e.g. web-based) search instead.
+ *
+ * @see android.app.SearchManager
+ * @see #onSearchRequested
+ */
+ public void startSearch(String initialQuery, boolean selectInitialQuery,
+ Bundle appSearchData, boolean globalSearch) {
+ // activate the search manager and start it up!
+ SearchManager searchManager = (SearchManager)
+ getSystemService(Context.SEARCH_SERVICE);
+ searchManager.startSearch(initialQuery, selectInitialQuery, getComponentName(),
+ appSearchData, globalSearch);
+ }
+
+ /**
+ * Request that key events come to this activity. Use this if your
+ * activity has no views with focus, but the activity still wants
+ * a chance to process key events.
+ *
+ * @see android.view.Window#takeKeyEvents
+ */
+ public void takeKeyEvents(boolean get) {
+ getWindow().takeKeyEvents(get);
+ }
+
+ /**
+ * Enable extended window features. This is a convenience for calling
+ * {@link android.view.Window#requestFeature getWindow().requestFeature()}.
+ *
+ * @param featureId The desired feature as defined in
+ * {@link android.view.Window}.
+ * @return Returns true if the requested feature is supported and now
+ * enabled.
+ *
+ * @see android.view.Window#requestFeature
+ */
+ public final boolean requestWindowFeature(int featureId) {
+ return getWindow().requestFeature(featureId);
+ }
+
+ /**
+ * Convenience for calling
+ * {@link android.view.Window#setFeatureDrawableResource}.
+ */
+ public final void setFeatureDrawableResource(int featureId, int resId) {
+ getWindow().setFeatureDrawableResource(featureId, resId);
+ }
+
+ /**
+ * Convenience for calling
+ * {@link android.view.Window#setFeatureDrawableUri}.
+ */
+ public final void setFeatureDrawableUri(int featureId, Uri uri) {
+ getWindow().setFeatureDrawableUri(featureId, uri);
+ }
+
+ /**
+ * Convenience for calling
+ * {@link android.view.Window#setFeatureDrawable(int, Drawable)}.
+ */
+ public final void setFeatureDrawable(int featureId, Drawable drawable) {
+ getWindow().setFeatureDrawable(featureId, drawable);
+ }
+
+ /**
+ * Convenience for calling
+ * {@link android.view.Window#setFeatureDrawableAlpha}.
+ */
+ public final void setFeatureDrawableAlpha(int featureId, int alpha) {
+ getWindow().setFeatureDrawableAlpha(featureId, alpha);
+ }
+
+ /**
+ * Convenience for calling
+ * {@link android.view.Window#getLayoutInflater}.
+ */
+ public LayoutInflater getLayoutInflater() {
+ return getWindow().getLayoutInflater();
+ }
+
+ /**
+ * Returns a {@link MenuInflater} with this context.
+ */
+ public MenuInflater getMenuInflater() {
+ return new MenuInflater(this);
+ }
+
+ @Override
+ protected void onApplyThemeResource(Resources.Theme theme,
+ int resid,
+ boolean first)
+ {
+ if (mParent == null) {
+ super.onApplyThemeResource(theme, resid, first);
+ } else {
+ try {
+ theme.setTo(mParent.getTheme());
+ } catch (Exception e) {
+ // Empty
+ }
+ theme.applyStyle(resid, false);
+ }
+ }
+
+ /**
+ * Launch an activity for which you would like a result when it finished.
+ * When this activity exits, your
+ * onActivityResult() method will be called with the given requestCode.
+ * Using a negative requestCode is the same as calling
+ * {@link #startActivity} (the activity is not launched as a sub-activity).
+ *
+ * <p>Note that this method should only be used with Intent protocols
+ * that are defined to return a result. In other protocols (such as
+ * {@link Intent#ACTION_MAIN} or {@link Intent#ACTION_VIEW}), you may
+ * not get the result when you expect. For example, if the activity you
+ * are launching uses the singleTask launch mode, it will not run in your
+ * task and thus you will immediately receive a cancel result.
+ *
+ * <p>As a special case, if you call startActivityForResult() with a requestCode
+ * >= 0 during the initial onCreate(Bundle savedInstanceState)/onResume() of your
+ * activity, then your window will not be displayed until a result is
+ * returned back from the started activity. This is to avoid visible
+ * flickering when redirecting to another activity.
+ *
+ * <p>This method throws {@link android.content.ActivityNotFoundException}
+ * if there was no Activity found to run the given Intent.
+ *
+ * @param intent The intent to start.
+ * @param requestCode If >= 0, this code will be returned in
+ * onActivityResult() when the activity exits.
+ *
+ * @throws android.content.ActivityNotFoundException
+ *
+ * @see #startActivity
+ */
+ public void startActivityForResult(Intent intent, int requestCode) {
+ if (mParent == null) {
+ Instrumentation.ActivityResult ar =
+ mInstrumentation.execStartActivity(
+ this, mMainThread.getApplicationThread(), mToken, this,
+ intent, requestCode);
+ if (ar != null) {
+ mMainThread.sendActivityResult(
+ mToken, mEmbeddedID, requestCode, ar.getResultCode(),
+ ar.getResultData());
+ }
+ if (requestCode >= 0) {
+ // If this start is requesting a result, we can avoid making
+ // the activity visible until the result is received. Setting
+ // this code during onCreate(Bundle savedInstanceState) or onResume() will keep the
+ // activity hidden during this time, to avoid flickering.
+ // This can only be done when a result is requested because
+ // that guarantees we will get information back when the
+ // activity is finished, no matter what happens to it.
+ mStartedActivity = true;
+ }
+ } else {
+ mParent.startActivityFromChild(this, intent, requestCode);
+ }
+ }
+
+ /**
+ * Launch a new activity. You will not receive any information about when
+ * the activity exits. This implementation overrides the base version,
+ * providing information about
+ * the activity performing the launch. Because of this additional
+ * information, the {@link Intent#FLAG_ACTIVITY_NEW_TASK} launch flag is not
+ * required; if not specified, the new activity will be added to the
+ * task of the caller.
+ *
+ * <p>This method throws {@link android.content.ActivityNotFoundException}
+ * if there was no Activity found to run the given Intent.
+ *
+ * @param intent The intent to start.
+ *
+ * @throws android.content.ActivityNotFoundException
+ *
+ * @see #startActivityForResult
+ */
+ @Override
+ public void startActivity(Intent intent) {
+ startActivityForResult(intent, -1);
+ }
+
+ /**
+ * A special variation to launch an activity only if a new activity
+ * instance is needed to handle the given Intent. In other words, this is
+ * just like {@link #startActivityForResult(Intent, int)} except: if you are
+ * using the {@link Intent#FLAG_ACTIVITY_SINGLE_TOP} flag, or
+ * singleTask or singleTop
+ * {@link android.R.styleable#AndroidManifestActivity_launchMode launchMode},
+ * and the activity
+ * that handles <var>intent</var> is the same as your currently running
+ * activity, then a new instance is not needed. In this case, instead of
+ * the normal behavior of calling {@link #onNewIntent} this function will
+ * return and you can handle the Intent yourself.
+ *
+ * <p>This function can only be called from a top-level activity; if it is
+ * called from a child activity, a runtime exception will be thrown.
+ *
+ * @param intent The intent to start.
+ * @param requestCode If >= 0, this code will be returned in
+ * onActivityResult() when the activity exits, as described in
+ * {@link #startActivityForResult}.
+ *
+ * @return If a new activity was launched then true is returned; otherwise
+ * false is returned and you must handle the Intent yourself.
+ *
+ * @see #startActivity
+ * @see #startActivityForResult
+ */
+ public boolean startActivityIfNeeded(Intent intent, int requestCode) {
+ if (mParent == null) {
+ int result = IActivityManager.START_RETURN_INTENT_TO_CALLER;
+ try {
+ result = ActivityManagerNative.getDefault()
+ .startActivity(mMainThread.getApplicationThread(),
+ intent, intent.resolveTypeIfNeeded(
+ getContentResolver()),
+ null, 0,
+ mToken, mEmbeddedID, requestCode, true, false);
+ } catch (RemoteException e) {
+ // Empty
+ }
+
+ Instrumentation.checkStartActivityResult(result, intent);
+
+ if (requestCode >= 0) {
+ // If this start is requesting a result, we can avoid making
+ // the activity visible until the result is received. Setting
+ // this code during onCreate(Bundle savedInstanceState) or onResume() will keep the
+ // activity hidden during this time, to avoid flickering.
+ // This can only be done when a result is requested because
+ // that guarantees we will get information back when the
+ // activity is finished, no matter what happens to it.
+ mStartedActivity = true;
+ }
+ return result != IActivityManager.START_RETURN_INTENT_TO_CALLER;
+ }
+
+ throw new UnsupportedOperationException(
+ "startActivityIfNeeded can only be called from a top-level activity");
+ }
+
+ /**
+ * Special version of starting an activity, for use when you are replacing
+ * other activity components. You can use this to hand the Intent off
+ * to the next Activity that can handle it. You typically call this in
+ * {@link #onCreate} with the Intent returned by {@link #getIntent}.
+ *
+ * @param intent The intent to dispatch to the next activity. For
+ * correct behavior, this must be the same as the Intent that started
+ * your own activity; the only changes you can make are to the extras
+ * inside of it.
+ *
+ * @return Returns a boolean indicating whether there was another Activity
+ * to start: true if there was a next activity to start, false if there
+ * wasn't. In general, if true is returned you will then want to call
+ * finish() on yourself.
+ */
+ public boolean startNextMatchingActivity(Intent intent) {
+ if (mParent == null) {
+ try {
+ return ActivityManagerNative.getDefault()
+ .startNextMatchingActivity(mToken, intent);
+ } catch (RemoteException e) {
+ // Empty
+ }
+ return false;
+ }
+
+ throw new UnsupportedOperationException(
+ "startNextMatchingActivity can only be called from a top-level activity");
+ }
+
+ /**
+ * This is called when a child activity of this one calls its
+ * {@link #startActivity} or {@link #startActivityForResult} method.
+ *
+ * <p>This method throws {@link android.content.ActivityNotFoundException}
+ * if there was no Activity found to run the given Intent.
+ *
+ * @param child The activity making the call.
+ * @param intent The intent to start.
+ * @param requestCode Reply request code. < 0 if reply is not requested.
+ *
+ * @throws android.content.ActivityNotFoundException
+ *
+ * @see #startActivity
+ * @see #startActivityForResult
+ */
+ public void startActivityFromChild(Activity child, Intent intent,
+ int requestCode) {
+ Instrumentation.ActivityResult ar =
+ mInstrumentation.execStartActivity(
+ this, mMainThread.getApplicationThread(), mToken, child,
+ intent, requestCode);
+ if (ar != null) {
+ mMainThread.sendActivityResult(
+ mToken, child.mEmbeddedID, requestCode,
+ ar.getResultCode(), ar.getResultData());
+ }
+ }
+
+ /**
+ * Call this to set the result that your activity will return to its
+ * caller.
+ *
+ * @param resultCode The result code to propagate back to the originating
+ * activity, often RESULT_CANCELED or RESULT_OK
+ *
+ * @see #RESULT_CANCELED
+ * @see #RESULT_OK
+ * @see #RESULT_FIRST_USER
+ * @see #setResult(int, Intent)
+ */
+ public final void setResult(int resultCode) {
+ synchronized (this) {
+ mResultCode = resultCode;
+ mResultData = null;
+ }
+ }
+
+ /**
+ * Call this to set the result that your activity will return to its
+ * caller.
+ *
+ * @param resultCode The result code to propagate back to the originating
+ * activity, often RESULT_CANCELED or RESULT_OK
+ * @param data The data to propagate back to the originating activity.
+ *
+ * @see #RESULT_CANCELED
+ * @see #RESULT_OK
+ * @see #RESULT_FIRST_USER
+ * @see #setResult(int)
+ */
+ public final void setResult(int resultCode, Intent data) {
+ synchronized (this) {
+ mResultCode = resultCode;
+ mResultData = data;
+ }
+ }
+
+ /**
+ * Return the name of the package that invoked this activity. This is who
+ * the data in {@link #setResult setResult()} will be sent to. You can
+ * use this information to validate that the recipient is allowed to
+ * receive the data.
+ *
+ * <p>Note: if the calling activity is not expecting a result (that is it
+ * did not use the {@link #startActivityForResult}
+ * form that includes a request code), then the calling package will be
+ * null.
+ *
+ * @return The package of the activity that will receive your
+ * reply, or null if none.
+ */
+ public String getCallingPackage() {
+ try {
+ return ActivityManagerNative.getDefault().getCallingPackage(mToken);
+ } catch (RemoteException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Return the name of the activity that invoked this activity. This is
+ * who the data in {@link #setResult setResult()} will be sent to. You
+ * can use this information to validate that the recipient is allowed to
+ * receive the data.
+ *
+ * <p>Note: if the calling activity is not expecting a result (that is it
+ * did not use the {@link #startActivityForResult}
+ * form that includes a request code), then the calling package will be
+ * null.
+ *
+ * @return String The full name of the activity that will receive your
+ * reply, or null if none.
+ */
+ public ComponentName getCallingActivity() {
+ try {
+ return ActivityManagerNative.getDefault().getCallingActivity(mToken);
+ } catch (RemoteException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Check to see whether this activity is in the process of finishing,
+ * either because you called {@link #finish} on it or someone else
+ * has requested that it finished. This is often used in
+ * {@link #onPause} to determine whether the activity is simply pausing or
+ * completely finishing.
+ *
+ * @return If the activity is finishing, returns true; else returns false.
+ *
+ * @see #finish
+ */
+ public boolean isFinishing() {
+ return mFinished;
+ }
+
+ /**
+ * Call this when your activity is done and should be closed. The
+ * ActivityResult is propagated back to whoever launched you via
+ * onActivityResult().
+ */
+ public void finish() {
+ if (mParent == null) {
+ int resultCode;
+ Intent resultData;
+ synchronized (this) {
+ resultCode = mResultCode;
+ resultData = mResultData;
+ }
+ if (Config.LOGV) Log.v(TAG, "Finishing self: token=" + mToken);
+ try {
+ if (ActivityManagerNative.getDefault()
+ .finishActivity(mToken, resultCode, resultData)) {
+ mFinished = true;
+ }
+ } catch (RemoteException e) {
+ // Empty
+ }
+ } else {
+ mParent.finishFromChild(this);
+ }
+ }
+
+ /**
+ * This is called when a child activity of this one calls its
+ * {@link #finish} method. The default implementation simply calls
+ * finish() on this activity (the parent), finishing the entire group.
+ *
+ * @param child The activity making the call.
+ *
+ * @see #finish
+ */
+ public void finishFromChild(Activity child) {
+ finish();
+ }
+
+ /**
+ * Force finish another activity that you had previously started with
+ * {@link #startActivityForResult}.
+ *
+ * @param requestCode The request code of the activity that you had
+ * given to startActivityForResult(). If there are multiple
+ * activities started with this request code, they
+ * will all be finished.
+ */
+ public void finishActivity(int requestCode) {
+ if (mParent == null) {
+ try {
+ ActivityManagerNative.getDefault()
+ .finishSubActivity(mToken, mEmbeddedID, requestCode);
+ } catch (RemoteException e) {
+ // Empty
+ }
+ } else {
+ mParent.finishActivityFromChild(this, requestCode);
+ }
+ }
+
+ /**
+ * This is called when a child activity of this one calls its
+ * finishActivity().
+ *
+ * @param child The activity making the call.
+ * @param requestCode Request code that had been used to start the
+ * activity.
+ */
+ public void finishActivityFromChild(Activity child, int requestCode) {
+ try {
+ ActivityManagerNative.getDefault()
+ .finishSubActivity(mToken, child.mEmbeddedID, requestCode);
+ } catch (RemoteException e) {
+ // Empty
+ }
+ }
+
+ /**
+ * Called when an activity you launched exits, giving you the requestCode
+ * you started it with, the resultCode it returned, and any additional
+ * data from it. The <var>resultCode</var> will be
+ * {@link #RESULT_CANCELED} if the activity explicitly returned that,
+ * didn't return any result, or crashed during its operation.
+ *
+ * <p>You will receive this call immediately before onResume() when your
+ * activity is re-starting.
+ *
+ * @param requestCode The integer request code originally supplied to
+ * startActivityForResult(), allowing you to identify who this
+ * result came from.
+ * @param resultCode The integer result code returned by the child activity
+ * through its setResult().
+ * @param data An Intent, which can return result data to the caller
+ * (various data can be attached to Intent "extras").
+ *
+ * @see #startActivityForResult
+ * @see #createPendingResult
+ * @see #setResult(int)
+ */
+ protected void onActivityResult(int requestCode, int resultCode,
+ Intent data) {
+ }
+
+ /**
+ * Create a new PendingIntent object which you can hand to others
+ * for them to use to send result data back to your
+ * {@link #onActivityResult} callback. The created object will be either
+ * one-shot (becoming invalid after a result is sent back) or multiple
+ * (allowing any number of results to be sent through it).
+ *
+ * @param requestCode Private request code for the sender that will be
+ * associated with the result data when it is returned. The sender can not
+ * modify this value, allowing you to identify incoming results.
+ * @param data Default data to supply in the result, which may be modified
+ * by the sender.
+ * @param flags May be {@link PendingIntent#FLAG_ONE_SHOT PendingIntent.FLAG_ONE_SHOT},
+ * {@link PendingIntent#FLAG_NO_CREATE PendingIntent.FLAG_NO_CREATE},
+ * {@link PendingIntent#FLAG_CANCEL_CURRENT PendingIntent.FLAG_CANCEL_CURRENT},
+ * or any of the flags as supported by
+ * {@link Intent#fillIn Intent.fillIn()} to control which unspecified parts
+ * of the intent that can be supplied when the actual send happens.
+ *
+ * @return Returns an existing or new PendingIntent matching the given
+ * parameters. May return null only if
+ * {@link PendingIntent#FLAG_NO_CREATE PendingIntent.FLAG_NO_CREATE} has been
+ * supplied.
+ *
+ * @see PendingIntent
+ */
+ public PendingIntent createPendingResult(int requestCode, Intent data,
+ int flags) {
+ String packageName = getPackageName();
+ try {
+ IIntentSender target =
+ ActivityManagerNative.getDefault().getIntentSender(
+ IActivityManager.INTENT_SENDER_ACTIVITY_RESULT, packageName,
+ mParent == null ? mToken : mParent.mToken,
+ mEmbeddedID, requestCode, data, null, flags);
+ return target != null ? new PendingIntent(target) : null;
+ } catch (RemoteException e) {
+ // Empty
+ }
+ return null;
+ }
+
+ /**
+ * Change the desired orientation of this activity. If the activity
+ * is currently in the foreground or otherwise impacting the screen
+ * orientation, the screen will immediately be changed (possibly causing
+ * the activity to be restarted). Otherwise, this will be used the next
+ * time the activity is visible.
+ *
+ * @param requestedOrientation An orientation constant as used in
+ * {@link ActivityInfo#screenOrientation ActivityInfo.screenOrientation}.
+ */
+ public void setRequestedOrientation(int requestedOrientation) {
+ if (mParent == null) {
+ try {
+ ActivityManagerNative.getDefault().setRequestedOrientation(
+ mToken, requestedOrientation);
+ } catch (RemoteException e) {
+ // Empty
+ }
+ } else {
+ mParent.setRequestedOrientation(requestedOrientation);
+ }
+ }
+
+ /**
+ * Return the current requested orientation of the activity. This will
+ * either be the orientation requested in its component's manifest, or
+ * the last requested orientation given to
+ * {@link #setRequestedOrientation(int)}.
+ *
+ * @return Returns an orientation constant as used in
+ * {@link ActivityInfo#screenOrientation ActivityInfo.screenOrientation}.
+ */
+ public int getRequestedOrientation() {
+ if (mParent == null) {
+ try {
+ return ActivityManagerNative.getDefault()
+ .getRequestedOrientation(mToken);
+ } catch (RemoteException e) {
+ // Empty
+ }
+ } else {
+ return mParent.getRequestedOrientation();
+ }
+ return ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+ }
+
+ /**
+ * Return the identifier of the task this activity is in. This identifier
+ * will remain the same for the lifetime of the activity.
+ *
+ * @return Task identifier, an opaque integer.
+ */
+ public int getTaskId() {
+ try {
+ return ActivityManagerNative.getDefault()
+ .getTaskForActivity(mToken, false);
+ } catch (RemoteException e) {
+ return -1;
+ }
+ }
+
+ /**
+ * Return whether this activity is the root of a task. The root is the
+ * first activity in a task.
+ *
+ * @return True if this is the root activity, else false.
+ */
+ public boolean isTaskRoot() {
+ try {
+ return ActivityManagerNative.getDefault()
+ .getTaskForActivity(mToken, true) >= 0;
+ } catch (RemoteException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Move the task containing this activity to the back of the activity
+ * stack. The activity's order within the task is unchanged.
+ *
+ * @param nonRoot If false then this only works if the activity is the root
+ * of a task; if true it will work for any activity in
+ * a task.
+ *
+ * @return If the task was moved (or it was already at the
+ * back) true is returned, else false.
+ */
+ public boolean moveTaskToBack(boolean nonRoot) {
+ try {
+ return ActivityManagerNative.getDefault().moveActivityTaskToBack(
+ mToken, nonRoot);
+ } catch (RemoteException e) {
+ // Empty
+ }
+ return false;
+ }
+
+ /**
+ * Returns class name for this activity with the package prefix removed.
+ * This is the default name used to read and write settings.
+ *
+ * @return The local class name.
+ */
+ public String getLocalClassName() {
+ final String pkg = getPackageName();
+ final String cls = mComponent.getClassName();
+ int packageLen = pkg.length();
+ if (!cls.startsWith(pkg) || cls.length() <= packageLen
+ || cls.charAt(packageLen) != '.') {
+ return cls;
+ }
+ return cls.substring(packageLen+1);
+ }
+
+ /**
+ * Returns complete component name of this activity.
+ *
+ * @return Returns the complete component name for this activity
+ */
+ public ComponentName getComponentName()
+ {
+ return mComponent;
+ }
+
+ /**
+ * Retrieve a {@link SharedPreferences} object for accessing preferences
+ * that are private to this activity. This simply calls the underlying
+ * {@link #getSharedPreferences(String, int)} method by passing in this activity's
+ * class name as the preferences name.
+ *
+ * @param mode Operating mode. Use {@link #MODE_PRIVATE} for the default
+ * operation, {@link #MODE_WORLD_READABLE} and
+ * {@link #MODE_WORLD_WRITEABLE} to control permissions.
+ *
+ * @return Returns the single SharedPreferences instance that can be used
+ * to retrieve and modify the preference values.
+ */
+ public SharedPreferences getPreferences(int mode) {
+ return getSharedPreferences(getLocalClassName(), mode);
+ }
+
+ @Override
+ public Object getSystemService(String name) {
+ if (getBaseContext() == null) {
+ throw new IllegalStateException(
+ "System services not available to Activities before onCreate()");
+ }
+
+ if (WINDOW_SERVICE.equals(name)) {
+ return mWindowManager;
+ }
+ return super.getSystemService(name);
+ }
+
+ /**
+ * Change the title associated with this activity. If this is a
+ * top-level activity, the title for its window will change. If it
+ * is an embedded activity, the parent can do whatever it wants
+ * with it.
+ */
+ public void setTitle(CharSequence title) {
+ mTitle = title;
+ onTitleChanged(title, mTitleColor);
+
+ if (mParent != null) {
+ mParent.onChildTitleChanged(this, title);
+ }
+ }
+
+ /**
+ * Change the title associated with this activity. If this is a
+ * top-level activity, the title for its window will change. If it
+ * is an embedded activity, the parent can do whatever it wants
+ * with it.
+ */
+ public void setTitle(int titleId) {
+ setTitle(getText(titleId));
+ }
+
+ public void setTitleColor(int textColor) {
+ mTitleColor = textColor;
+ onTitleChanged(mTitle, textColor);
+ }
+
+ public final CharSequence getTitle() {
+ return mTitle;
+ }
+
+ public final int getTitleColor() {
+ return mTitleColor;
+ }
+
+ protected void onTitleChanged(CharSequence title, int color) {
+ if (mTitleReady) {
+ final Window win = getWindow();
+ if (win != null) {
+ win.setTitle(title);
+ if (color != 0) {
+ win.setTitleColor(color);
+ }
+ }
+ }
+ }
+
+ protected void onChildTitleChanged(Activity childActivity, CharSequence title) {
+ }
+
+ /**
+ * Sets the visibility of the progress bar in the title.
+ * <p>
+ * In order for the progress bar to be shown, the feature must be requested
+ * via {@link #requestWindowFeature(int)}.
+ *
+ * @param visible Whether to show the progress bars in the title.
+ */
+ public final void setProgressBarVisibility(boolean visible) {
+ getWindow().setFeatureInt(Window.FEATURE_PROGRESS, visible ? Window.PROGRESS_VISIBILITY_ON :
+ Window.PROGRESS_VISIBILITY_OFF);
+ }
+
+ /**
+ * Sets the visibility of the indeterminate progress bar in the title.
+ * <p>
+ * In order for the progress bar to be shown, the feature must be requested
+ * via {@link #requestWindowFeature(int)}.
+ *
+ * @param visible Whether to show the progress bars in the title.
+ */
+ public final void setProgressBarIndeterminateVisibility(boolean visible) {
+ getWindow().setFeatureInt(Window.FEATURE_INDETERMINATE_PROGRESS,
+ visible ? Window.PROGRESS_VISIBILITY_ON : Window.PROGRESS_VISIBILITY_OFF);
+ }
+
+ /**
+ * Sets whether the horizontal progress bar in the title should be indeterminate (the circular
+ * is always indeterminate).
+ * <p>
+ * In order for the progress bar to be shown, the feature must be requested
+ * via {@link #requestWindowFeature(int)}.
+ *
+ * @param indeterminate Whether the horizontal progress bar should be indeterminate.
+ */
+ public final void setProgressBarIndeterminate(boolean indeterminate) {
+ getWindow().setFeatureInt(Window.FEATURE_PROGRESS,
+ indeterminate ? Window.PROGRESS_INDETERMINATE_ON : Window.PROGRESS_INDETERMINATE_OFF);
+ }
+
+ /**
+ * Sets the progress for the progress bars in the title.
+ * <p>
+ * In order for the progress bar to be shown, the feature must be requested
+ * via {@link #requestWindowFeature(int)}.
+ *
+ * @param progress The progress for the progress bar. Valid ranges are from
+ * 0 to 10000 (both inclusive). If 10000 is given, the progress
+ * bar will be completely filled and will fade out.
+ */
+ public final void setProgress(int progress) {
+ getWindow().setFeatureInt(Window.FEATURE_PROGRESS, progress + Window.PROGRESS_START);
+ }
+
+ /**
+ * Sets the secondary progress for the progress bar in the title. This
+ * progress is drawn between the primary progress (set via
+ * {@link #setProgress(int)} and the background. It can be ideal for media
+ * scenarios such as showing the buffering progress while the default
+ * progress shows the play progress.
+ * <p>
+ * In order for the progress bar to be shown, the feature must be requested
+ * via {@link #requestWindowFeature(int)}.
+ *
+ * @param secondaryProgress The secondary progress for the progress bar. Valid ranges are from
+ * 0 to 10000 (both inclusive).
+ */
+ public final void setSecondaryProgress(int secondaryProgress) {
+ getWindow().setFeatureInt(Window.FEATURE_PROGRESS,
+ secondaryProgress + Window.PROGRESS_SECONDARY_START);
+ }
+
+ /**
+ * Suggests an audio stream whose volume should be changed by the hardware
+ * volume controls.
+ * <p>
+ * The suggested audio stream will be tied to the window of this Activity.
+ * If the Activity is switched, the stream set here is no longer the
+ * suggested stream. The client does not need to save and restore the old
+ * suggested stream value in onPause and onResume.
+ *
+ * @param streamType The type of the audio stream whose volume should be
+ * changed by the hardware volume controls. It is not guaranteed that
+ * the hardware volume controls will always change this stream's
+ * volume (for example, if a call is in progress, its stream's volume
+ * may be changed instead). To reset back to the default, use
+ * {@link AudioManager#USE_DEFAULT_STREAM_TYPE}.
+ */
+ public final void setVolumeControlStream(int streamType) {
+ getWindow().setVolumeControlStream(streamType);
+ }
+
+ /**
+ * Gets the suggested audio stream whose volume should be changed by the
+ * harwdare volume controls.
+ *
+ * @return The suggested audio stream type whose volume should be changed by
+ * the hardware volume controls.
+ * @see #setVolumeControlStream(int)
+ */
+ public final int getVolumeControlStream() {
+ return getWindow().getVolumeControlStream();
+ }
+
+ /**
+ * Runs the specified action on the UI thread. If the current thread is the UI
+ * thread, then the action is executed immediately. If the current thread is
+ * not the UI thread, the action is posted to the event queue of the UI thread.
+ *
+ * @param action the action to run on the UI thread
+ */
+ public final void runOnUiThread(Runnable action) {
+ if (Thread.currentThread() != mUiThread) {
+ mHandler.post(action);
+ } else {
+ action.run();
+ }
+ }
+
+ /**
+ * Stub implementation of {@link android.view.LayoutInflater.Factory#onCreateView} used when
+ * inflating with the LayoutInflater returned by {@link #getSystemService}. This
+ * implementation simply returns null for all view names.
+ *
+ * @see android.view.LayoutInflater#createView
+ * @see android.view.Window#getLayoutInflater
+ */
+ public View onCreateView(String name, Context context, AttributeSet attrs) {
+ return null;
+ }
+
+ // ------------------ Internal API ------------------
+
+ final void setParent(Activity parent) {
+ mParent = parent;
+ }
+
+ final void attach(Context context, ActivityThread aThread, Instrumentation instr, IBinder token,
+ Application application, Intent intent, ActivityInfo info, CharSequence title,
+ Activity parent, String id, Object lastNonConfigurationInstance,
+ Configuration config) {
+ attachBaseContext(context);
+
+ mWindow = PolicyManager.makeNewWindow(this);
+ mWindow.setCallback(this);
+ mUiThread = Thread.currentThread();
+
+ mMainThread = aThread;
+ mInstrumentation = instr;
+ mToken = token;
+ mApplication = application;
+ mIntent = intent;
+ mComponent = intent.getComponent();
+ mActivityInfo = info;
+ mTitle = title;
+ mParent = parent;
+ mEmbeddedID = id;
+ mLastNonConfigurationInstance = lastNonConfigurationInstance;
+
+ mWindow.setWindowManager(null, mToken, mComponent.flattenToString());
+ if (mParent != null) {
+ mWindow.setContainer(mParent.getWindow());
+ }
+ mWindowManager = mWindow.getWindowManager();
+ mCurrentConfig = config;
+ }
+
+ final IBinder getActivityToken() {
+ return mParent != null ? mParent.getActivityToken() : mToken;
+ }
+
+ final void performStart() {
+ mCalled = false;
+ mInstrumentation.callActivityOnStart(this);
+ if (!mCalled) {
+ throw new SuperNotCalledException(
+ "Activity " + mComponent.toShortString() +
+ " did not call through to super.onStart()");
+ }
+ }
+
+ final void performRestart() {
+ final int N = mManagedCursors.size();
+ for (int i=0; i<N; i++) {
+ ManagedCursor mc = mManagedCursors.get(i);
+ if (mc.mReleased || mc.mUpdated) {
+ mc.mCursor.requery();
+ mc.mReleased = false;
+ mc.mUpdated = false;
+ }
+ }
+
+ if (mStopped) {
+ mStopped = false;
+ mCalled = false;
+ mInstrumentation.callActivityOnRestart(this);
+ if (!mCalled) {
+ throw new SuperNotCalledException(
+ "Activity " + mComponent.toShortString() +
+ " did not call through to super.onRestart()");
+ }
+ performStart();
+ }
+ }
+
+ final void performResume() {
+ performRestart();
+
+ mLastNonConfigurationInstance = null;
+
+ // First call onResume() -before- setting mResumed, so we don't
+ // send out any status bar / menu notifications the client makes.
+ mCalled = false;
+ mInstrumentation.callActivityOnResume(this);
+ if (!mCalled) {
+ throw new SuperNotCalledException(
+ "Activity " + mComponent.toShortString() +
+ " did not call through to super.onResume()");
+ }
+
+ // Now really resume, and install the current status bar and menu.
+ mResumed = true;
+ mCalled = false;
+ onPostResume();
+ if (!mCalled) {
+ throw new SuperNotCalledException(
+ "Activity " + mComponent.toShortString() +
+ " did not call through to super.onPostResume()");
+ }
+ }
+
+ final void performStop() {
+ if (!mStopped) {
+ if (mWindow != null) {
+ mWindow.closeAllPanels();
+ }
+
+ mCalled = false;
+ mInstrumentation.callActivityOnStop(this);
+ if (!mCalled) {
+ throw new SuperNotCalledException(
+ "Activity " + mComponent.toShortString() +
+ " did not call through to super.onStop()");
+ }
+
+ final int N = mManagedCursors.size();
+ for (int i=0; i<N; i++) {
+ ManagedCursor mc = mManagedCursors.get(i);
+ if (!mc.mReleased) {
+ mc.mCursor.deactivate();
+ mc.mReleased = true;
+ }
+ }
+
+ mStopped = true;
+ }
+ mResumed = false;
+ }
+
+ final boolean isResumed() {
+ return mResumed;
+ }
+
+ void dispatchActivityResult(String who, int requestCode,
+ int resultCode, Intent data) {
+ if (Config.LOGV) Log.v(
+ TAG, "Dispatching result: who=" + who + ", reqCode=" + requestCode
+ + ", resCode=" + resultCode + ", data=" + data);
+ if (who == null) {
+ onActivityResult(requestCode, resultCode, data);
+ }
+ }
+}
diff --git a/core/java/android/app/ActivityGroup.java b/core/java/android/app/ActivityGroup.java
new file mode 100644
index 0000000..96bb475
--- /dev/null
+++ b/core/java/android/app/ActivityGroup.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.content.Intent;
+import android.os.Bundle;
+
+/**
+ * A screen that contains and runs multiple embedded activities.
+ */
+public class ActivityGroup extends Activity {
+ private static final String STATES_KEY = "android:states";
+
+ /**
+ * This field should be made private, so it is hidden from the SDK.
+ * {@hide}
+ */
+ protected LocalActivityManager mLocalActivityManager;
+
+ public ActivityGroup() {
+ this(true);
+ }
+
+ public ActivityGroup(boolean singleActivityMode) {
+ mLocalActivityManager = new LocalActivityManager(this, singleActivityMode);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Bundle states = savedInstanceState != null
+ ? (Bundle) savedInstanceState.getBundle(STATES_KEY) : null;
+ mLocalActivityManager.dispatchCreate(states);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mLocalActivityManager.dispatchResume();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ Bundle state = mLocalActivityManager.saveInstanceState();
+ if (state != null) {
+ outState.putBundle(STATES_KEY, state);
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mLocalActivityManager.dispatchPause(isFinishing());
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mLocalActivityManager.dispatchStop();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mLocalActivityManager.dispatchDestroy(isFinishing());
+ }
+
+ public Activity getCurrentActivity() {
+ return mLocalActivityManager.getCurrentActivity();
+ }
+
+ public final LocalActivityManager getLocalActivityManager() {
+ return mLocalActivityManager;
+ }
+
+ @Override
+ void dispatchActivityResult(String who, int requestCode, int resultCode,
+ Intent data) {
+ if (who != null) {
+ Activity act = mLocalActivityManager.getActivity(who);
+ /*
+ if (Config.LOGV) Log.v(
+ TAG, "Dispatching result: who=" + who + ", reqCode=" + requestCode
+ + ", resCode=" + resultCode + ", data=" + data
+ + ", rec=" + rec);
+ */
+ if (act != null) {
+ act.onActivityResult(requestCode, resultCode, data);
+ return;
+ }
+ }
+ super.dispatchActivityResult(who, requestCode, resultCode, data);
+ }
+}
+
+
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
new file mode 100644
index 0000000..6eb1102
--- /dev/null
+++ b/core/java/android/app/ActivityManager.java
@@ -0,0 +1,604 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.IPackageDataObserver;
+import android.graphics.Bitmap;
+import android.os.RemoteException;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.Parcelable.Creator;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.List;
+
+/**
+ * Interact with the overall activities running in the system.
+ */
+public class ActivityManager {
+ private static String TAG = "ActivityManager";
+ private static boolean DEBUG = false;
+ private static boolean localLOGV = DEBUG || android.util.Config.LOGV;
+
+ private final Context mContext;
+ private final Handler mHandler;
+
+ /*package*/ ActivityManager(Context context, Handler handler) {
+ mContext = context;
+ mHandler = handler;
+ }
+
+ /**
+ * Information you can retrieve about tasks that the user has most recently
+ * started or visited.
+ */
+ public static class RecentTaskInfo implements Parcelable {
+ /**
+ * If this task is currently running, this is the identifier for it.
+ * If it is not running, this will be -1.
+ */
+ public int id;
+
+ /**
+ * The original Intent used to launch the task. You can use this
+ * Intent to re-launch the task (if it is no longer running) or bring
+ * the current task to the front.
+ */
+ public Intent baseIntent;
+
+ /**
+ * If this task was started from an alias, this is the actual
+ * activity component that was initially started; the component of
+ * the baseIntent in this case is the name of the actual activity
+ * implementation that the alias referred to. Otherwise, this is null.
+ */
+ public ComponentName origActivity;
+
+ public RecentTaskInfo() {
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(id);
+ if (baseIntent != null) {
+ dest.writeInt(1);
+ baseIntent.writeToParcel(dest, 0);
+ } else {
+ dest.writeInt(0);
+ }
+ ComponentName.writeToParcel(origActivity, dest);
+ }
+
+ public void readFromParcel(Parcel source) {
+ id = source.readInt();
+ if (source.readInt() != 0) {
+ baseIntent = Intent.CREATOR.createFromParcel(source);
+ } else {
+ baseIntent = null;
+ }
+ origActivity = ComponentName.readFromParcel(source);
+ }
+
+ public static final Creator<RecentTaskInfo> CREATOR
+ = new Creator<RecentTaskInfo>() {
+ public RecentTaskInfo createFromParcel(Parcel source) {
+ return new RecentTaskInfo(source);
+ }
+ public RecentTaskInfo[] newArray(int size) {
+ return new RecentTaskInfo[size];
+ }
+ };
+
+ private RecentTaskInfo(Parcel source) {
+ readFromParcel(source);
+ }
+ }
+
+ /**
+ * Flag for use with {@link #getRecentTasks}: return all tasks, even those
+ * that have set their
+ * {@link android.content.Intent#FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS} flag.
+ */
+ public static final int RECENT_WITH_EXCLUDED = 0x0001;
+
+ /**
+ * Return a list of the tasks that the user has recently launched, with
+ * the most recent being first and older ones after in order.
+ *
+ * @param maxNum The maximum number of entries to return in the list. The
+ * actual number returned may be smaller, depending on how many tasks the
+ * user has started and the maximum number the system can remember.
+ *
+ * @return Returns a list of RecentTaskInfo records describing each of
+ * the recent tasks.
+ *
+ * @throws SecurityException Throws SecurityException if the caller does
+ * not hold the {@link android.Manifest.permission#GET_TASKS} permission.
+ */
+ public List<RecentTaskInfo> getRecentTasks(int maxNum, int flags)
+ throws SecurityException {
+ try {
+ return ActivityManagerNative.getDefault().getRecentTasks(maxNum,
+ flags);
+ } catch (RemoteException e) {
+ // System dead, we will be dead too soon!
+ return null;
+ }
+ }
+
+ /**
+ * Information you can retrieve about a particular task that is currently
+ * "running" in the system. Note that a running task does not mean the
+ * given task actual has a process it is actively running in; it simply
+ * means that the user has gone to it and never closed it, but currently
+ * the system may have killed its process and is only holding on to its
+ * last state in order to restart it when the user returns.
+ */
+ public static class RunningTaskInfo implements Parcelable {
+ /**
+ * A unique identifier for this task.
+ */
+ public int id;
+
+ /**
+ * The component launched as the first activity in the task. This can
+ * be considered the "application" of this task.
+ */
+ public ComponentName baseActivity;
+
+ /**
+ * The activity component at the top of the history stack of the task.
+ * This is what the user is currently doing.
+ */
+ public ComponentName topActivity;
+
+ /**
+ * Thumbnail representation of the task's current state.
+ */
+ public Bitmap thumbnail;
+
+ /**
+ * Description of the task's current state.
+ */
+ public CharSequence description;
+
+ /**
+ * Number of activities in this task.
+ */
+ public int numActivities;
+
+ /**
+ * Number of activities that are currently running (not stopped
+ * and persisted) in this task.
+ */
+ public int numRunning;
+
+ public RunningTaskInfo() {
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(id);
+ ComponentName.writeToParcel(baseActivity, dest);
+ ComponentName.writeToParcel(topActivity, dest);
+ if (thumbnail != null) {
+ dest.writeInt(1);
+ thumbnail.writeToParcel(dest, 0);
+ } else {
+ dest.writeInt(0);
+ }
+ TextUtils.writeToParcel(description, dest,
+ Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
+ dest.writeInt(numActivities);
+ dest.writeInt(numRunning);
+ }
+
+ public void readFromParcel(Parcel source) {
+ id = source.readInt();
+ baseActivity = ComponentName.readFromParcel(source);
+ topActivity = ComponentName.readFromParcel(source);
+ if (source.readInt() != 0) {
+ thumbnail = Bitmap.CREATOR.createFromParcel(source);
+ } else {
+ thumbnail = null;
+ }
+ description = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
+ numActivities = source.readInt();
+ numRunning = source.readInt();
+ }
+
+ public static final Creator<RunningTaskInfo> CREATOR = new Creator<RunningTaskInfo>() {
+ public RunningTaskInfo createFromParcel(Parcel source) {
+ return new RunningTaskInfo(source);
+ }
+ public RunningTaskInfo[] newArray(int size) {
+ return new RunningTaskInfo[size];
+ }
+ };
+
+ private RunningTaskInfo(Parcel source) {
+ readFromParcel(source);
+ }
+ }
+
+ /**
+ * Return a list of the tasks that are currently running, with
+ * the most recent being first and older ones after in order. Note that
+ * "running" does not mean any of the task's code is currently loaded or
+ * activity -- the task may have been frozen by the system, so that it
+ * can be restarted in its previous state when next brought to the
+ * foreground.
+ *
+ * @param maxNum The maximum number of entries to return in the list. The
+ * actual number returned may be smaller, depending on how many tasks the
+ * user has started.
+ *
+ * @return Returns a list of RunningTaskInfo records describing each of
+ * the running tasks.
+ *
+ * @throws SecurityException Throws SecurityException if the caller does
+ * not hold the {@link android.Manifest.permission#GET_TASKS} permission.
+ */
+ public List<RunningTaskInfo> getRunningTasks(int maxNum)
+ throws SecurityException {
+ try {
+ return (List<RunningTaskInfo>)ActivityManagerNative.getDefault()
+ .getTasks(maxNum, 0, null);
+ } catch (RemoteException e) {
+ // System dead, we will be dead too soon!
+ return null;
+ }
+ }
+
+ /**
+ * Information you can retrieve about a particular Service that is
+ * currently running in the system.
+ */
+ public static class RunningServiceInfo implements Parcelable {
+ /**
+ * The service component.
+ */
+ public ComponentName service;
+
+ /**
+ * If non-zero, this is the process the service is running in.
+ */
+ public int pid;
+
+ /**
+ * The name of the process this service runs in.
+ */
+ public String process;
+
+ /**
+ * Set to true if the service has asked to run as a foreground process.
+ */
+ public boolean foreground;
+
+ /**
+ * The time when the service was first made activity, either by someone
+ * starting or binding to it.
+ */
+ public long activeSince;
+
+ /**
+ * Set to true if this service has been explicitly started.
+ */
+ public boolean started;
+
+ /**
+ * Number of clients connected to the service.
+ */
+ public int clientCount;
+
+ /**
+ * Number of times the service's process has crashed while the service
+ * is running.
+ */
+ public int crashCount;
+
+ /**
+ * The time when there was last activity in the service (either
+ * explicit requests to start it or clients binding to it).
+ */
+ public long lastActivityTime;
+
+ /**
+ * If non-zero, this service is not currently running, but scheduled to
+ * restart at the given time.
+ */
+ public long restarting;
+
+ public RunningServiceInfo() {
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ ComponentName.writeToParcel(service, dest);
+ dest.writeInt(pid);
+ dest.writeString(process);
+ dest.writeInt(foreground ? 1 : 0);
+ dest.writeLong(activeSince);
+ dest.writeInt(started ? 1 : 0);
+ dest.writeInt(clientCount);
+ dest.writeInt(crashCount);
+ dest.writeLong(lastActivityTime);
+ dest.writeLong(restarting);
+ }
+
+ public void readFromParcel(Parcel source) {
+ service = ComponentName.readFromParcel(source);
+ pid = source.readInt();
+ process = source.readString();
+ foreground = source.readInt() != 0;
+ activeSince = source.readLong();
+ started = source.readInt() != 0;
+ clientCount = source.readInt();
+ crashCount = source.readInt();
+ lastActivityTime = source.readLong();
+ restarting = source.readLong();
+ }
+
+ public static final Creator<RunningServiceInfo> CREATOR = new Creator<RunningServiceInfo>() {
+ public RunningServiceInfo createFromParcel(Parcel source) {
+ return new RunningServiceInfo(source);
+ }
+ public RunningServiceInfo[] newArray(int size) {
+ return new RunningServiceInfo[size];
+ }
+ };
+
+ private RunningServiceInfo(Parcel source) {
+ readFromParcel(source);
+ }
+ }
+
+ /**
+ * Return a list of the services that are currently running.
+ *
+ * @param maxNum The maximum number of entries to return in the list. The
+ * actual number returned may be smaller, depending on how many services
+ * are running.
+ *
+ * @return Returns a list of RunningServiceInfo records describing each of
+ * the running tasks.
+ */
+ public List<RunningServiceInfo> getRunningServices(int maxNum)
+ throws SecurityException {
+ try {
+ return (List<RunningServiceInfo>)ActivityManagerNative.getDefault()
+ .getServices(maxNum, 0);
+ } catch (RemoteException e) {
+ // System dead, we will be dead too soon!
+ return null;
+ }
+ }
+
+ /**
+ * Information you can retrieve about the available memory through
+ * {@link ActivityManager#getMemoryInfo}.
+ */
+ public static class MemoryInfo implements Parcelable {
+ /**
+ * The total available memory on the system. This number should not
+ * be considered absolute: due to the nature of the kernel, a significant
+ * portion of this memory is actually in use and needed for the overall
+ * system to run well.
+ */
+ public long availMem;
+
+ /**
+ * The threshold of {@link #availMem} at which we consider memory to be
+ * low and start killing background services and other non-extraneous
+ * processes.
+ */
+ public long threshold;
+
+ /**
+ * Set to true if the system considers itself to currently be in a low
+ * memory situation.
+ */
+ public boolean lowMemory;
+
+ public MemoryInfo() {
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeLong(availMem);
+ dest.writeLong(threshold);
+ dest.writeInt(lowMemory ? 1 : 0);
+ }
+
+ public void readFromParcel(Parcel source) {
+ availMem = source.readLong();
+ threshold = source.readLong();
+ lowMemory = source.readInt() != 0;
+ }
+
+ public static final Creator<MemoryInfo> CREATOR
+ = new Creator<MemoryInfo>() {
+ public MemoryInfo createFromParcel(Parcel source) {
+ return new MemoryInfo(source);
+ }
+ public MemoryInfo[] newArray(int size) {
+ return new MemoryInfo[size];
+ }
+ };
+
+ private MemoryInfo(Parcel source) {
+ readFromParcel(source);
+ }
+ }
+
+ public void getMemoryInfo(MemoryInfo outInfo) {
+ try {
+ ActivityManagerNative.getDefault().getMemoryInfo(outInfo);
+ } catch (RemoteException e) {
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public boolean clearApplicationUserData(String packageName, IPackageDataObserver observer) {
+ try {
+ return ActivityManagerNative.getDefault().clearApplicationUserData(packageName,
+ observer);
+ } catch (RemoteException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Information you can retrieve about any processes that are in an error condition.
+ */
+ public static class ProcessErrorStateInfo implements Parcelable {
+ /**
+ * Condition codes
+ */
+ public static final int NO_ERROR = 0;
+ public static final int CRASHED = 1;
+ public static final int NOT_RESPONDING = 2;
+
+ /**
+ * The condition that the process is in.
+ */
+ public int condition;
+
+ /**
+ * The process name in which the crash or error occurred.
+ */
+ public String processName;
+
+ /**
+ * The pid of this process; 0 if none
+ */
+ public int pid;
+
+ /**
+ * The kernel user-ID that has been assigned to this process;
+ * currently this is not a unique ID (multiple applications can have
+ * the same uid).
+ */
+ public int uid;
+
+ /**
+ * The tag that was provided when the process crashed.
+ */
+ public String tag;
+
+ /**
+ * A short message describing the error condition.
+ */
+ public String shortMsg;
+
+ /**
+ * A long message describing the error condition.
+ */
+ public String longMsg;
+
+ /**
+ * Raw data about the crash (typically a stack trace).
+ */
+ public byte[] crashData;
+
+ public ProcessErrorStateInfo() {
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(condition);
+ dest.writeString(processName);
+ dest.writeInt(pid);
+ dest.writeInt(uid);
+ dest.writeString(tag);
+ dest.writeString(shortMsg);
+ dest.writeString(longMsg);
+ dest.writeInt(crashData == null ? -1 : crashData.length);
+ dest.writeByteArray(crashData);
+ }
+
+ public void readFromParcel(Parcel source) {
+ condition = source.readInt();
+ processName = source.readString();
+ pid = source.readInt();
+ uid = source.readInt();
+ tag = source.readString();
+ shortMsg = source.readString();
+ longMsg = source.readString();
+ int cdLen = source.readInt();
+ if (cdLen == -1) {
+ crashData = null;
+ } else {
+ crashData = new byte[cdLen];
+ source.readByteArray(crashData);
+ }
+ }
+
+ public static final Creator<ProcessErrorStateInfo> CREATOR =
+ new Creator<ProcessErrorStateInfo>() {
+ public ProcessErrorStateInfo createFromParcel(Parcel source) {
+ return new ProcessErrorStateInfo(source);
+ }
+ public ProcessErrorStateInfo[] newArray(int size) {
+ return new ProcessErrorStateInfo[size];
+ }
+ };
+
+ private ProcessErrorStateInfo(Parcel source) {
+ readFromParcel(source);
+ }
+ }
+
+ /**
+ * Returns a list of any processes that are currently in an error condition. The result
+ * will be null if all processes are running properly at this time.
+ *
+ * @return Returns a list of ProcessErrorStateInfo records, or null if there are no
+ * current error conditions (it will not return an empty list). This list ordering is not
+ * specified.
+ */
+ public List<ProcessErrorStateInfo> getProcessesInErrorState() {
+ try {
+ return ActivityManagerNative.getDefault().getProcessesInErrorState();
+ } catch (RemoteException e) {
+ return null;
+ }
+ }
+}
diff --git a/core/java/android/app/ActivityManagerNative.java b/core/java/android/app/ActivityManagerNative.java
new file mode 100644
index 0000000..e6f1b05
--- /dev/null
+++ b/core/java/android/app/ActivityManagerNative.java
@@ -0,0 +1,2054 @@
+/*
+ * 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.app;
+
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.IPackageDataObserver;
+import android.content.res.Configuration;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.ServiceManager;
+import android.text.TextUtils;
+import android.util.Config;
+import android.util.Log;
+
+import java.io.FileNotFoundException;
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/** {@hide} */
+public abstract class ActivityManagerNative extends Binder implements IActivityManager
+{
+ /**
+ * Cast a Binder object into an activity manager interface, generating
+ * a proxy if needed.
+ */
+ static public IActivityManager asInterface(IBinder obj)
+ {
+ if (obj == null) {
+ return null;
+ }
+ IActivityManager in =
+ (IActivityManager)obj.queryLocalInterface(descriptor);
+ if (in != null) {
+ return in;
+ }
+
+ return new ActivityManagerProxy(obj);
+ }
+
+ /**
+ * Retrieve the system's default/global activity manager.
+ */
+ static public IActivityManager getDefault()
+ {
+ if (gDefault != null) {
+ //if (Config.LOGV) Log.v(
+ // "ActivityManager", "returning cur default = " + gDefault);
+ return gDefault;
+ }
+ IBinder b = ServiceManager.getService("activity");
+ if (Config.LOGV) Log.v(
+ "ActivityManager", "default service binder = " + b);
+ gDefault = asInterface(b);
+ if (Config.LOGV) Log.v(
+ "ActivityManager", "default service = " + gDefault);
+ return gDefault;
+ }
+
+ /**
+ * Convenience for sending a sticky broadcast. For internal use only.
+ * If you don't care about permission, use null.
+ */
+ static public void broadcastStickyIntent(Intent intent, String permission)
+ {
+ try {
+ getDefault().broadcastIntent(
+ null, intent, null, null, Activity.RESULT_OK, null, null,
+ null /*permission*/, false, true);
+ } catch (RemoteException ex) {
+ }
+ }
+
+ static public void noteWakeupAlarm(PendingIntent ps) {
+ try {
+ getDefault().noteWakeupAlarm(ps.getTarget());
+ } catch (RemoteException ex) {
+ }
+ }
+
+ public ActivityManagerNative()
+ {
+ attachInterface(this, descriptor);
+ }
+
+ public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
+ throws RemoteException {
+ switch (code) {
+ case START_ACTIVITY_TRANSACTION:
+ {
+ data.enforceInterface(IActivityManager.descriptor);
+ IBinder b = data.readStrongBinder();
+ IApplicationThread app = ApplicationThreadNative.asInterface(b);
+ Intent intent = Intent.CREATOR.createFromParcel(data);
+ String resolvedType = data.readString();
+ Uri[] grantedUriPermissions = data.createTypedArray(Uri.CREATOR);
+ int grantedMode = data.readInt();
+ IBinder resultTo = data.readStrongBinder();
+ String resultWho = data.readString();
+ int requestCode = data.readInt();
+ boolean onlyIfNeeded = data.readInt() != 0;
+ boolean debug = data.readInt() != 0;
+ int result = startActivity(app, intent, resolvedType,
+ grantedUriPermissions, grantedMode, resultTo, resultWho,
+ requestCode, onlyIfNeeded, debug);
+ reply.writeNoException();
+ reply.writeInt(result);
+ return true;
+ }
+
+ case START_NEXT_MATCHING_ACTIVITY_TRANSACTION:
+ {
+ data.enforceInterface(IActivityManager.descriptor);
+ IBinder callingActivity = data.readStrongBinder();
+ Intent intent = Intent.CREATOR.createFromParcel(data);
+ boolean result = startNextMatchingActivity(callingActivity, intent);
+ reply.writeNoException();
+ reply.writeInt(result ? 1 : 0);
+ return true;
+ }
+
+ case FINISH_ACTIVITY_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ IBinder token = data.readStrongBinder();
+ Intent resultData = null;
+ int resultCode = data.readInt();
+ if (data.readInt() != 0) {
+ resultData = Intent.CREATOR.createFromParcel(data);
+ }
+ boolean res = finishActivity(token, resultCode, resultData);
+ reply.writeNoException();
+ reply.writeInt(res ? 1 : 0);
+ return true;
+ }
+
+ case FINISH_SUB_ACTIVITY_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ IBinder token = data.readStrongBinder();
+ String resultWho = data.readString();
+ int requestCode = data.readInt();
+ finishSubActivity(token, resultWho, requestCode);
+ reply.writeNoException();
+ return true;
+ }
+
+ case REGISTER_RECEIVER_TRANSACTION:
+ {
+ data.enforceInterface(IActivityManager.descriptor);
+ IBinder b = data.readStrongBinder();
+ IApplicationThread app =
+ b != null ? ApplicationThreadNative.asInterface(b) : null;
+ b = data.readStrongBinder();
+ IIntentReceiver rec
+ = b != null ? IIntentReceiver.Stub.asInterface(b) : null;
+ IntentFilter filter = IntentFilter.CREATOR.createFromParcel(data);
+ String perm = data.readString();
+ Intent intent = registerReceiver(app, rec, filter, perm);
+ reply.writeNoException();
+ if (intent != null) {
+ reply.writeInt(1);
+ intent.writeToParcel(reply, 0);
+ } else {
+ reply.writeInt(0);
+ }
+ return true;
+ }
+
+ case UNREGISTER_RECEIVER_TRANSACTION:
+ {
+ data.enforceInterface(IActivityManager.descriptor);
+ IBinder b = data.readStrongBinder();
+ if (b == null) {
+ return true;
+ }
+ IIntentReceiver rec = IIntentReceiver.Stub.asInterface(b);
+ unregisterReceiver(rec);
+ reply.writeNoException();
+ return true;
+ }
+
+ case BROADCAST_INTENT_TRANSACTION:
+ {
+ data.enforceInterface(IActivityManager.descriptor);
+ IBinder b = data.readStrongBinder();
+ IApplicationThread app =
+ b != null ? ApplicationThreadNative.asInterface(b) : null;
+ Intent intent = Intent.CREATOR.createFromParcel(data);
+ String resolvedType = data.readString();
+ b = data.readStrongBinder();
+ IIntentReceiver resultTo =
+ b != null ? IIntentReceiver.Stub.asInterface(b) : null;
+ int resultCode = data.readInt();
+ String resultData = data.readString();
+ Bundle resultExtras = data.readBundle();
+ String perm = data.readString();
+ boolean serialized = data.readInt() != 0;
+ boolean sticky = data.readInt() != 0;
+ int res = broadcastIntent(app, intent, resolvedType, resultTo,
+ resultCode, resultData, resultExtras, perm,
+ serialized, sticky);
+ reply.writeNoException();
+ reply.writeInt(res);
+ return true;
+ }
+
+ case UNBROADCAST_INTENT_TRANSACTION:
+ {
+ data.enforceInterface(IActivityManager.descriptor);
+ IBinder b = data.readStrongBinder();
+ IApplicationThread app = b != null ? ApplicationThreadNative.asInterface(b) : null;
+ Intent intent = Intent.CREATOR.createFromParcel(data);
+ unbroadcastIntent(app, intent);
+ reply.writeNoException();
+ return true;
+ }
+
+ case FINISH_RECEIVER_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ IBinder who = data.readStrongBinder();
+ int resultCode = data.readInt();
+ String resultData = data.readString();
+ Bundle resultExtras = data.readBundle();
+ boolean resultAbort = data.readInt() != 0;
+ if (who != null) {
+ finishReceiver(who, resultCode, resultData, resultExtras, resultAbort);
+ }
+ reply.writeNoException();
+ return true;
+ }
+
+ case SET_PERSISTENT_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ IBinder token = data.readStrongBinder();
+ boolean isPersistent = data.readInt() != 0;
+ if (token != null) {
+ setPersistent(token, isPersistent);
+ }
+ reply.writeNoException();
+ return true;
+ }
+
+ case ATTACH_APPLICATION_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ IApplicationThread app = ApplicationThreadNative.asInterface(
+ data.readStrongBinder());
+ if (app != null) {
+ attachApplication(app);
+ }
+ reply.writeNoException();
+ return true;
+ }
+
+ case ACTIVITY_IDLE_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ IBinder token = data.readStrongBinder();
+ if (token != null) {
+ activityIdle(token);
+ }
+ reply.writeNoException();
+ return true;
+ }
+
+ case ACTIVITY_PAUSED_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ IBinder token = data.readStrongBinder();
+ Bundle map = data.readBundle();
+ activityPaused(token, map);
+ reply.writeNoException();
+ return true;
+ }
+
+ case ACTIVITY_STOPPED_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ IBinder token = data.readStrongBinder();
+ Bitmap thumbnail = data.readInt() != 0
+ ? Bitmap.CREATOR.createFromParcel(data) : null;
+ CharSequence description = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(data);
+ activityStopped(token, thumbnail, description);
+ reply.writeNoException();
+ return true;
+ }
+
+ case ACTIVITY_DESTROYED_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ IBinder token = data.readStrongBinder();
+ activityDestroyed(token);
+ reply.writeNoException();
+ return true;
+ }
+
+ case GET_CALLING_PACKAGE_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ IBinder token = data.readStrongBinder();
+ String res = token != null ? getCallingPackage(token) : null;
+ reply.writeNoException();
+ reply.writeString(res);
+ return true;
+ }
+
+ case GET_CALLING_ACTIVITY_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ IBinder token = data.readStrongBinder();
+ ComponentName cn = getCallingActivity(token);
+ reply.writeNoException();
+ ComponentName.writeToParcel(cn, reply);
+ return true;
+ }
+
+ case GET_TASKS_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ int maxNum = data.readInt();
+ int fl = data.readInt();
+ IBinder receiverBinder = data.readStrongBinder();
+ IThumbnailReceiver receiver = receiverBinder != null
+ ? IThumbnailReceiver.Stub.asInterface(receiverBinder)
+ : null;
+ List list = getTasks(maxNum, fl, receiver);
+ reply.writeNoException();
+ int N = list != null ? list.size() : -1;
+ reply.writeInt(N);
+ int i;
+ for (i=0; i<N; i++) {
+ ActivityManager.RunningTaskInfo info =
+ (ActivityManager.RunningTaskInfo)list.get(i);
+ info.writeToParcel(reply, 0);
+ }
+ return true;
+ }
+
+ case GET_RECENT_TASKS_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ int maxNum = data.readInt();
+ int fl = data.readInt();
+ List<ActivityManager.RecentTaskInfo> list = getRecentTasks(maxNum,
+ fl);
+ reply.writeNoException();
+ reply.writeTypedList(list);
+ return true;
+ }
+
+ case GET_SERVICES_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ int maxNum = data.readInt();
+ int fl = data.readInt();
+ List list = getServices(maxNum, fl);
+ reply.writeNoException();
+ int N = list != null ? list.size() : -1;
+ reply.writeInt(N);
+ int i;
+ for (i=0; i<N; i++) {
+ ActivityManager.RunningServiceInfo info =
+ (ActivityManager.RunningServiceInfo)list.get(i);
+ info.writeToParcel(reply, 0);
+ }
+ return true;
+ }
+
+ case GET_PROCESSES_IN_ERROR_STATE_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ List<ActivityManager.ProcessErrorStateInfo> list = getProcessesInErrorState();
+ reply.writeNoException();
+ reply.writeTypedList(list);
+ return true;
+ }
+
+ case MOVE_TASK_TO_FRONT_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ int task = data.readInt();
+ moveTaskToFront(task);
+ reply.writeNoException();
+ return true;
+ }
+
+ case MOVE_TASK_TO_BACK_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ int task = data.readInt();
+ moveTaskToBack(task);
+ reply.writeNoException();
+ return true;
+ }
+
+ case MOVE_ACTIVITY_TASK_TO_BACK_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ IBinder token = data.readStrongBinder();
+ boolean nonRoot = data.readInt() != 0;
+ boolean res = moveActivityTaskToBack(token, nonRoot);
+ reply.writeNoException();
+ reply.writeInt(res ? 1 : 0);
+ return true;
+ }
+
+ case MOVE_TASK_BACKWARDS_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ int task = data.readInt();
+ moveTaskBackwards(task);
+ reply.writeNoException();
+ return true;
+ }
+
+ case GET_TASK_FOR_ACTIVITY_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ IBinder token = data.readStrongBinder();
+ boolean onlyRoot = data.readInt() != 0;
+ int res = token != null
+ ? getTaskForActivity(token, onlyRoot) : -1;
+ reply.writeNoException();
+ reply.writeInt(res);
+ return true;
+ }
+
+ case FINISH_OTHER_INSTANCES_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ IBinder token = data.readStrongBinder();
+ ComponentName className = ComponentName.readFromParcel(data);
+ finishOtherInstances(token, className);
+ reply.writeNoException();
+ return true;
+ }
+
+ case REPORT_THUMBNAIL_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ IBinder token = data.readStrongBinder();
+ Bitmap thumbnail = data.readInt() != 0
+ ? Bitmap.CREATOR.createFromParcel(data) : null;
+ CharSequence description = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(data);
+ reportThumbnail(token, thumbnail, description);
+ reply.writeNoException();
+ return true;
+ }
+
+ case GET_CONTENT_PROVIDER_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ IBinder b = data.readStrongBinder();
+ IApplicationThread app = ApplicationThreadNative.asInterface(b);
+ String name = data.readString();
+ ContentProviderHolder cph = getContentProvider(app, name);
+ reply.writeNoException();
+ if (cph != null) {
+ reply.writeInt(1);
+ cph.writeToParcel(reply, 0);
+ } else {
+ reply.writeInt(0);
+ }
+ return true;
+ }
+
+ case PUBLISH_CONTENT_PROVIDERS_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ IBinder b = data.readStrongBinder();
+ IApplicationThread app = ApplicationThreadNative.asInterface(b);
+ ArrayList<ContentProviderHolder> providers =
+ data.createTypedArrayList(ContentProviderHolder.CREATOR);
+ publishContentProviders(app, providers);
+ reply.writeNoException();
+ return true;
+ }
+
+ case REMOVE_CONTENT_PROVIDER_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ IBinder b = data.readStrongBinder();
+ IApplicationThread app = ApplicationThreadNative.asInterface(b);
+ String name = data.readString();
+ removeContentProvider(app, name);
+ reply.writeNoException();
+ return true;
+ }
+
+ case START_SERVICE_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ IBinder b = data.readStrongBinder();
+ IApplicationThread app = ApplicationThreadNative.asInterface(b);
+ Intent service = Intent.CREATOR.createFromParcel(data);
+ String resolvedType = data.readString();
+ ComponentName cn = startService(app, service, resolvedType);
+ reply.writeNoException();
+ ComponentName.writeToParcel(cn, reply);
+ return true;
+ }
+
+ case STOP_SERVICE_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ IBinder b = data.readStrongBinder();
+ IApplicationThread app = ApplicationThreadNative.asInterface(b);
+ Intent service = Intent.CREATOR.createFromParcel(data);
+ String resolvedType = data.readString();
+ int res = stopService(app, service, resolvedType);
+ reply.writeNoException();
+ reply.writeInt(res);
+ return true;
+ }
+
+ case STOP_SERVICE_TOKEN_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ ComponentName className = ComponentName.readFromParcel(data);
+ IBinder token = data.readStrongBinder();
+ int startId = data.readInt();
+ boolean res = stopServiceToken(className, token, startId);
+ reply.writeNoException();
+ reply.writeInt(res ? 1 : 0);
+ return true;
+ }
+
+ case SET_SERVICE_FOREGROUND_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ ComponentName className = ComponentName.readFromParcel(data);
+ IBinder token = data.readStrongBinder();
+ boolean isForeground = data.readInt() != 0;
+ setServiceForeground(className, token, isForeground);
+ reply.writeNoException();
+ return true;
+ }
+
+ case BIND_SERVICE_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ IBinder b = data.readStrongBinder();
+ IApplicationThread app = ApplicationThreadNative.asInterface(b);
+ IBinder token = data.readStrongBinder();
+ Intent service = Intent.CREATOR.createFromParcel(data);
+ String resolvedType = data.readString();
+ b = data.readStrongBinder();
+ int fl = data.readInt();
+ IServiceConnection conn = IServiceConnection.Stub.asInterface(b);
+ int res = bindService(app, token, service, resolvedType, conn, fl);
+ reply.writeNoException();
+ reply.writeInt(res);
+ return true;
+ }
+
+ case UNBIND_SERVICE_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ IBinder b = data.readStrongBinder();
+ IServiceConnection conn = IServiceConnection.Stub.asInterface(b);
+ boolean res = unbindService(conn);
+ reply.writeNoException();
+ reply.writeInt(res ? 1 : 0);
+ return true;
+ }
+
+ case PUBLISH_SERVICE_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ IBinder token = data.readStrongBinder();
+ Intent intent = Intent.CREATOR.createFromParcel(data);
+ IBinder service = data.readStrongBinder();
+ publishService(token, intent, service);
+ reply.writeNoException();
+ return true;
+ }
+
+ case UNBIND_FINISHED_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ IBinder token = data.readStrongBinder();
+ Intent intent = Intent.CREATOR.createFromParcel(data);
+ boolean doRebind = data.readInt() != 0;
+ unbindFinished(token, intent, doRebind);
+ reply.writeNoException();
+ return true;
+ }
+
+ case SERVICE_DONE_EXECUTING_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ IBinder token = data.readStrongBinder();
+ serviceDoneExecuting(token);
+ reply.writeNoException();
+ return true;
+ }
+
+ case START_INSTRUMENTATION_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ ComponentName className = ComponentName.readFromParcel(data);
+ String profileFile = data.readString();
+ int fl = data.readInt();
+ Bundle arguments = data.readBundle();
+ IBinder b = data.readStrongBinder();
+ IInstrumentationWatcher w = IInstrumentationWatcher.Stub.asInterface(b);
+ boolean res = startInstrumentation(className, profileFile, fl, arguments, w);
+ reply.writeNoException();
+ reply.writeInt(res ? 1 : 0);
+ return true;
+ }
+
+
+ case FINISH_INSTRUMENTATION_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ IBinder b = data.readStrongBinder();
+ IApplicationThread app = ApplicationThreadNative.asInterface(b);
+ int resultCode = data.readInt();
+ Bundle results = data.readBundle();
+ finishInstrumentation(app, resultCode, results);
+ reply.writeNoException();
+ return true;
+ }
+
+ case GET_CONFIGURATION_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ Configuration config = getConfiguration();
+ reply.writeNoException();
+ config.writeToParcel(reply, 0);
+ return true;
+ }
+
+ case UPDATE_CONFIGURATION_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ Configuration config = Configuration.CREATOR.createFromParcel(data);
+ updateConfiguration(config);
+ reply.writeNoException();
+ return true;
+ }
+
+ case SET_REQUESTED_ORIENTATION_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ IBinder token = data.readStrongBinder();
+ int requestedOrientation = data.readInt();
+ setRequestedOrientation(token, requestedOrientation);
+ reply.writeNoException();
+ return true;
+ }
+
+ case GET_REQUESTED_ORIENTATION_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ IBinder token = data.readStrongBinder();
+ int req = getRequestedOrientation(token);
+ reply.writeNoException();
+ reply.writeInt(req);
+ return true;
+ }
+
+ case GET_ACTIVITY_CLASS_FOR_TOKEN_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ IBinder token = data.readStrongBinder();
+ ComponentName cn = getActivityClassForToken(token);
+ reply.writeNoException();
+ ComponentName.writeToParcel(cn, reply);
+ return true;
+ }
+
+ case GET_PACKAGE_FOR_TOKEN_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ IBinder token = data.readStrongBinder();
+ reply.writeNoException();
+ reply.writeString(getPackageForToken(token));
+ return true;
+ }
+
+ case GET_INTENT_SENDER_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ int type = data.readInt();
+ String packageName = data.readString();
+ IBinder token = data.readStrongBinder();
+ String resultWho = data.readString();
+ int requestCode = data.readInt();
+ Intent requestIntent = data.readInt() != 0
+ ? Intent.CREATOR.createFromParcel(data) : null;
+ String requestResolvedType = data.readString();
+ int fl = data.readInt();
+ IIntentSender res = getIntentSender(type, packageName, token,
+ resultWho, requestCode, requestIntent,
+ requestResolvedType, fl);
+ reply.writeNoException();
+ reply.writeStrongBinder(res != null ? res.asBinder() : null);
+ return true;
+ }
+
+ case CANCEL_INTENT_SENDER_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ IIntentSender r = IIntentSender.Stub.asInterface(
+ data.readStrongBinder());
+ cancelIntentSender(r);
+ reply.writeNoException();
+ return true;
+ }
+
+ case GET_PACKAGE_FOR_INTENT_SENDER_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ IIntentSender r = IIntentSender.Stub.asInterface(
+ data.readStrongBinder());
+ String res = getPackageForIntentSender(r);
+ reply.writeNoException();
+ reply.writeString(res);
+ return true;
+ }
+
+ case SET_PROCESS_LIMIT_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ int max = data.readInt();
+ setProcessLimit(max);
+ reply.writeNoException();
+ return true;
+ }
+
+ case GET_PROCESS_LIMIT_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ int limit = getProcessLimit();
+ reply.writeNoException();
+ reply.writeInt(limit);
+ return true;
+ }
+
+ case SET_PROCESS_FOREGROUND_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ IBinder token = data.readStrongBinder();
+ int pid = data.readInt();
+ boolean isForeground = data.readInt() != 0;
+ setProcessForeground(token, pid, isForeground);
+ reply.writeNoException();
+ return true;
+ }
+
+ case CHECK_PERMISSION_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ String perm = data.readString();
+ int pid = data.readInt();
+ int uid = data.readInt();
+ int res = checkPermission(perm, pid, uid);
+ reply.writeNoException();
+ reply.writeInt(res);
+ return true;
+ }
+
+ case CHECK_URI_PERMISSION_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ Uri uri = Uri.CREATOR.createFromParcel(data);
+ int pid = data.readInt();
+ int uid = data.readInt();
+ int mode = data.readInt();
+ int res = checkUriPermission(uri, pid, uid, mode);
+ reply.writeNoException();
+ reply.writeInt(res);
+ return true;
+ }
+
+ case CLEAR_APP_DATA_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ String packageName = data.readString();
+ IPackageDataObserver observer = IPackageDataObserver.Stub.asInterface(
+ data.readStrongBinder());
+ boolean res = clearApplicationUserData(packageName, observer);
+ reply.writeNoException();
+ reply.writeInt(res ? 1 : 0);
+ return true;
+ }
+
+ case GRANT_URI_PERMISSION_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ IBinder b = data.readStrongBinder();
+ IApplicationThread app = ApplicationThreadNative.asInterface(b);
+ String targetPkg = data.readString();
+ Uri uri = Uri.CREATOR.createFromParcel(data);
+ int mode = data.readInt();
+ grantUriPermission(app, targetPkg, uri, mode);
+ reply.writeNoException();
+ return true;
+ }
+
+ case REVOKE_URI_PERMISSION_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ IBinder b = data.readStrongBinder();
+ IApplicationThread app = ApplicationThreadNative.asInterface(b);
+ Uri uri = Uri.CREATOR.createFromParcel(data);
+ int mode = data.readInt();
+ revokeUriPermission(app, uri, mode);
+ reply.writeNoException();
+ return true;
+ }
+
+ case SHOW_WAITING_FOR_DEBUGGER_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ IBinder b = data.readStrongBinder();
+ IApplicationThread app = ApplicationThreadNative.asInterface(b);
+ boolean waiting = data.readInt() != 0;
+ showWaitingForDebugger(app, waiting);
+ reply.writeNoException();
+ return true;
+ }
+
+ case GET_MEMORY_INFO_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ ActivityManager.MemoryInfo mi = new ActivityManager.MemoryInfo();
+ getMemoryInfo(mi);
+ reply.writeNoException();
+ mi.writeToParcel(reply, 0);
+ return true;
+ }
+
+ case UNHANDLED_BACK_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ unhandledBack();
+ reply.writeNoException();
+ return true;
+ }
+
+ case OPEN_CONTENT_URI_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ Uri uri = Uri.parse(data.readString());
+ ParcelFileDescriptor pfd = openContentUri(uri);
+ reply.writeNoException();
+ if (pfd != null) {
+ reply.writeInt(1);
+ pfd.writeToParcel(reply, Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
+ } else {
+ reply.writeInt(0);
+ }
+ return true;
+ }
+
+ case GOING_TO_SLEEP_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ goingToSleep();
+ reply.writeNoException();
+ return true;
+ }
+
+ case WAKING_UP_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ wakingUp();
+ reply.writeNoException();
+ return true;
+ }
+
+ case SET_DEBUG_APP_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ String pn = data.readString();
+ boolean wfd = data.readInt() != 0;
+ boolean per = data.readInt() != 0;
+ setDebugApp(pn, wfd, per);
+ reply.writeNoException();
+ return true;
+ }
+
+ case SET_ALWAYS_FINISH_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ boolean enabled = data.readInt() != 0;
+ setAlwaysFinish(enabled);
+ reply.writeNoException();
+ return true;
+ }
+
+ case SET_ACTIVITY_WATCHER_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ IActivityWatcher watcher = IActivityWatcher.Stub.asInterface(
+ data.readStrongBinder());
+ setActivityWatcher(watcher);
+ return true;
+ }
+
+ case ENTER_SAFE_MODE_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ enterSafeMode();
+ reply.writeNoException();
+ return true;
+ }
+
+ case NOTE_WAKEUP_ALARM_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ IIntentSender is = IIntentSender.Stub.asInterface(
+ data.readStrongBinder());
+ noteWakeupAlarm(is);
+ reply.writeNoException();
+ return true;
+ }
+
+ case KILL_PIDS_FOR_MEMORY_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ int[] pids = data.createIntArray();
+ boolean res = killPidsForMemory(pids);
+ reply.writeNoException();
+ reply.writeInt(res ? 1 : 0);
+ return true;
+ }
+
+ case REPORT_PSS_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ IBinder b = data.readStrongBinder();
+ IApplicationThread app = ApplicationThreadNative.asInterface(b);
+ int pss = data.readInt();
+ reportPss(app, pss);
+ reply.writeNoException();
+ return true;
+ }
+
+ case START_RUNNING_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ String pkg = data.readString();
+ String cls = data.readString();
+ String action = data.readString();
+ String indata = data.readString();
+ startRunning(pkg, cls, action, indata);
+ reply.writeNoException();
+ return true;
+ }
+
+ case SYSTEM_READY_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ systemReady();
+ reply.writeNoException();
+ return true;
+ }
+
+ case HANDLE_APPLICATION_ERROR_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ IBinder app = data.readStrongBinder();
+ int fl = data.readInt();
+ String tag = data.readString();
+ String shortMsg = data.readString();
+ String longMsg = data.readString();
+ byte[] crashData = data.createByteArray();
+ int res = handleApplicationError(app, fl, tag, shortMsg, longMsg,
+ crashData);
+ reply.writeNoException();
+ reply.writeInt(res);
+ return true;
+ }
+
+ case SIGNAL_PERSISTENT_PROCESSES_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ int sig = data.readInt();
+ signalPersistentProcesses(sig);
+ reply.writeNoException();
+ return true;
+ }
+
+ case RESTART_PACKAGE_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ String packageName = data.readString();
+ restartPackage(packageName);
+ reply.writeNoException();
+ return true;
+ }
+
+ }
+
+ return super.onTransact(code, data, reply, flags);
+ }
+
+ public IBinder asBinder()
+ {
+ return this;
+ }
+
+ private static IActivityManager gDefault;
+}
+
+class ActivityManagerProxy implements IActivityManager
+{
+ public ActivityManagerProxy(IBinder remote)
+ {
+ mRemote = remote;
+ }
+
+ public IBinder asBinder()
+ {
+ return mRemote;
+ }
+
+ public int startActivity(IApplicationThread caller, Intent intent,
+ String resolvedType, Uri[] grantedUriPermissions, int grantedMode,
+ IBinder resultTo, String resultWho,
+ int requestCode, boolean onlyIfNeeded,
+ boolean debug) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(caller != null ? caller.asBinder() : null);
+ intent.writeToParcel(data, 0);
+ data.writeString(resolvedType);
+ data.writeTypedArray(grantedUriPermissions, 0);
+ data.writeInt(grantedMode);
+ data.writeStrongBinder(resultTo);
+ data.writeString(resultWho);
+ data.writeInt(requestCode);
+ data.writeInt(onlyIfNeeded ? 1 : 0);
+ data.writeInt(debug ? 1 : 0);
+ mRemote.transact(START_ACTIVITY_TRANSACTION, data, reply, 0);
+ reply.readException();
+ int result = reply.readInt();
+ reply.recycle();
+ data.recycle();
+ return result;
+ }
+ public boolean startNextMatchingActivity(IBinder callingActivity,
+ Intent intent) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(callingActivity);
+ intent.writeToParcel(data, 0);
+ mRemote.transact(START_NEXT_MATCHING_ACTIVITY_TRANSACTION, data, reply, 0);
+ reply.readException();
+ int result = reply.readInt();
+ reply.recycle();
+ data.recycle();
+ return result != 0;
+ }
+ public boolean finishActivity(IBinder token, int resultCode, Intent resultData)
+ throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(token);
+ data.writeInt(resultCode);
+ if (resultData != null) {
+ data.writeInt(1);
+ resultData.writeToParcel(data, 0);
+ } else {
+ data.writeInt(0);
+ }
+ mRemote.transact(FINISH_ACTIVITY_TRANSACTION, data, reply, 0);
+ reply.readException();
+ boolean res = reply.readInt() != 0;
+ data.recycle();
+ reply.recycle();
+ return res;
+ }
+ public void finishSubActivity(IBinder token, String resultWho, int requestCode) throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(token);
+ data.writeString(resultWho);
+ data.writeInt(requestCode);
+ mRemote.transact(FINISH_SUB_ACTIVITY_TRANSACTION, data, reply, 0);
+ reply.readException();
+ data.recycle();
+ reply.recycle();
+ }
+ public Intent registerReceiver(IApplicationThread caller,
+ IIntentReceiver receiver,
+ IntentFilter filter, String perm) throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(caller != null ? caller.asBinder() : null);
+ data.writeStrongBinder(receiver != null ? receiver.asBinder() : null);
+ filter.writeToParcel(data, 0);
+ data.writeString(perm);
+ mRemote.transact(REGISTER_RECEIVER_TRANSACTION, data, reply, 0);
+ reply.readException();
+ Intent intent = null;
+ int haveIntent = reply.readInt();
+ if (haveIntent != 0) {
+ intent = Intent.CREATOR.createFromParcel(reply);
+ }
+ reply.recycle();
+ data.recycle();
+ return intent;
+ }
+ public void unregisterReceiver(IIntentReceiver receiver) throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(receiver.asBinder());
+ mRemote.transact(UNREGISTER_RECEIVER_TRANSACTION, data, reply, 0);
+ reply.readException();
+ data.recycle();
+ reply.recycle();
+ }
+ public int broadcastIntent(IApplicationThread caller,
+ Intent intent, String resolvedType, IIntentReceiver resultTo,
+ int resultCode, String resultData, Bundle map,
+ String requiredPermission, boolean serialized,
+ boolean sticky) throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(caller != null ? caller.asBinder() : null);
+ intent.writeToParcel(data, 0);
+ data.writeString(resolvedType);
+ data.writeStrongBinder(resultTo != null ? resultTo.asBinder() : null);
+ data.writeInt(resultCode);
+ data.writeString(resultData);
+ data.writeBundle(map);
+ data.writeString(requiredPermission);
+ data.writeInt(serialized ? 1 : 0);
+ data.writeInt(sticky ? 1 : 0);
+ mRemote.transact(BROADCAST_INTENT_TRANSACTION, data, reply, 0);
+ reply.readException();
+ int res = reply.readInt();
+ reply.recycle();
+ data.recycle();
+ return res;
+ }
+ public void unbroadcastIntent(IApplicationThread caller, Intent intent) throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(caller != null ? caller.asBinder() : null);
+ intent.writeToParcel(data, 0);
+ mRemote.transact(UNBROADCAST_INTENT_TRANSACTION, data, reply, 0);
+ reply.readException();
+ data.recycle();
+ reply.recycle();
+ }
+ public void finishReceiver(IBinder who, int resultCode, String resultData, Bundle map, boolean abortBroadcast) throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(who);
+ data.writeInt(resultCode);
+ data.writeString(resultData);
+ data.writeBundle(map);
+ data.writeInt(abortBroadcast ? 1 : 0);
+ mRemote.transact(FINISH_RECEIVER_TRANSACTION, data, reply, IBinder.FLAG_ONEWAY);
+ reply.readException();
+ data.recycle();
+ reply.recycle();
+ }
+ public void setPersistent(IBinder token, boolean isPersistent) throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(token);
+ data.writeInt(isPersistent ? 1 : 0);
+ mRemote.transact(SET_PERSISTENT_TRANSACTION, data, reply, 0);
+ reply.readException();
+ data.recycle();
+ reply.recycle();
+ }
+ public void attachApplication(IApplicationThread app) throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(app.asBinder());
+ mRemote.transact(ATTACH_APPLICATION_TRANSACTION, data, reply, 0);
+ reply.readException();
+ data.recycle();
+ reply.recycle();
+ }
+ public void activityIdle(IBinder token) throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(token);
+ mRemote.transact(ACTIVITY_IDLE_TRANSACTION, data, reply, IBinder.FLAG_ONEWAY);
+ reply.readException();
+ data.recycle();
+ reply.recycle();
+ }
+ public void activityPaused(IBinder token, Bundle state) throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(token);
+ data.writeBundle(state);
+ mRemote.transact(ACTIVITY_PAUSED_TRANSACTION, data, reply, 0);
+ reply.readException();
+ data.recycle();
+ reply.recycle();
+ }
+ public void activityStopped(IBinder token,
+ Bitmap thumbnail, CharSequence description) throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(token);
+ if (thumbnail != null) {
+ data.writeInt(1);
+ thumbnail.writeToParcel(data, 0);
+ } else {
+ data.writeInt(0);
+ }
+ TextUtils.writeToParcel(description, data, 0);
+ mRemote.transact(ACTIVITY_STOPPED_TRANSACTION, data, reply, IBinder.FLAG_ONEWAY);
+ reply.readException();
+ data.recycle();
+ reply.recycle();
+ }
+ public void activityDestroyed(IBinder token) throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(token);
+ mRemote.transact(ACTIVITY_DESTROYED_TRANSACTION, data, reply, IBinder.FLAG_ONEWAY);
+ reply.readException();
+ data.recycle();
+ reply.recycle();
+ }
+ public String getCallingPackage(IBinder token) throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(token);
+ mRemote.transact(GET_CALLING_PACKAGE_TRANSACTION, data, reply, 0);
+ reply.readException();
+ String res = reply.readString();
+ data.recycle();
+ reply.recycle();
+ return res;
+ }
+ public ComponentName getCallingActivity(IBinder token)
+ throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(token);
+ mRemote.transact(GET_CALLING_ACTIVITY_TRANSACTION, data, reply, 0);
+ reply.readException();
+ ComponentName res = ComponentName.readFromParcel(reply);
+ data.recycle();
+ reply.recycle();
+ return res;
+ }
+ public List getTasks(int maxNum, int flags,
+ IThumbnailReceiver receiver) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeInt(maxNum);
+ data.writeInt(flags);
+ data.writeStrongBinder(receiver != null ? receiver.asBinder() : null);
+ mRemote.transact(GET_TASKS_TRANSACTION, data, reply, 0);
+ reply.readException();
+ ArrayList list = null;
+ int N = reply.readInt();
+ if (N >= 0) {
+ list = new ArrayList();
+ while (N > 0) {
+ ActivityManager.RunningTaskInfo info =
+ ActivityManager.RunningTaskInfo.CREATOR
+ .createFromParcel(reply);
+ list.add(info);
+ N--;
+ }
+ }
+ data.recycle();
+ reply.recycle();
+ return list;
+ }
+ public List<ActivityManager.RecentTaskInfo> getRecentTasks(int maxNum,
+ int flags) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeInt(maxNum);
+ data.writeInt(flags);
+ mRemote.transact(GET_RECENT_TASKS_TRANSACTION, data, reply, 0);
+ reply.readException();
+ ArrayList<ActivityManager.RecentTaskInfo> list
+ = reply.createTypedArrayList(ActivityManager.RecentTaskInfo.CREATOR);
+ data.recycle();
+ reply.recycle();
+ return list;
+ }
+ public List getServices(int maxNum, int flags) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeInt(maxNum);
+ data.writeInt(flags);
+ mRemote.transact(GET_SERVICES_TRANSACTION, data, reply, 0);
+ reply.readException();
+ ArrayList list = null;
+ int N = reply.readInt();
+ if (N >= 0) {
+ list = new ArrayList();
+ while (N > 0) {
+ ActivityManager.RunningServiceInfo info =
+ ActivityManager.RunningServiceInfo.CREATOR
+ .createFromParcel(reply);
+ list.add(info);
+ N--;
+ }
+ }
+ data.recycle();
+ reply.recycle();
+ return list;
+ }
+ public List<ActivityManager.ProcessErrorStateInfo> getProcessesInErrorState()
+ throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ mRemote.transact(GET_PROCESSES_IN_ERROR_STATE_TRANSACTION, data, reply, 0);
+ reply.readException();
+ ArrayList<ActivityManager.ProcessErrorStateInfo> list
+ = reply.createTypedArrayList(ActivityManager.ProcessErrorStateInfo.CREATOR);
+ data.recycle();
+ reply.recycle();
+ return list;
+ }
+ public void moveTaskToFront(int task) throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeInt(task);
+ mRemote.transact(MOVE_TASK_TO_FRONT_TRANSACTION, data, reply, 0);
+ reply.readException();
+ data.recycle();
+ reply.recycle();
+ }
+ public void moveTaskToBack(int task) throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeInt(task);
+ mRemote.transact(MOVE_TASK_TO_BACK_TRANSACTION, data, reply, 0);
+ reply.readException();
+ data.recycle();
+ reply.recycle();
+ }
+ public boolean moveActivityTaskToBack(IBinder token, boolean nonRoot)
+ throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(token);
+ data.writeInt(nonRoot ? 1 : 0);
+ mRemote.transact(MOVE_ACTIVITY_TASK_TO_BACK_TRANSACTION, data, reply, 0);
+ reply.readException();
+ boolean res = reply.readInt() != 0;
+ data.recycle();
+ reply.recycle();
+ return res;
+ }
+ public void moveTaskBackwards(int task) throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeInt(task);
+ mRemote.transact(MOVE_TASK_BACKWARDS_TRANSACTION, data, reply, 0);
+ reply.readException();
+ data.recycle();
+ reply.recycle();
+ }
+ public int getTaskForActivity(IBinder token, boolean onlyRoot) throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(token);
+ data.writeInt(onlyRoot ? 1 : 0);
+ mRemote.transact(GET_TASK_FOR_ACTIVITY_TRANSACTION, data, reply, 0);
+ reply.readException();
+ int res = reply.readInt();
+ data.recycle();
+ reply.recycle();
+ return res;
+ }
+ public void finishOtherInstances(IBinder token, ComponentName className) throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(token);
+ ComponentName.writeToParcel(className, data);
+ mRemote.transact(FINISH_OTHER_INSTANCES_TRANSACTION, data, reply, 0);
+ reply.readException();
+ data.recycle();
+ reply.recycle();
+ }
+ public void reportThumbnail(IBinder token,
+ Bitmap thumbnail, CharSequence description) throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(token);
+ if (thumbnail != null) {
+ data.writeInt(1);
+ thumbnail.writeToParcel(data, 0);
+ } else {
+ data.writeInt(0);
+ }
+ TextUtils.writeToParcel(description, data, 0);
+ mRemote.transact(REPORT_THUMBNAIL_TRANSACTION, data, reply, IBinder.FLAG_ONEWAY);
+ reply.readException();
+ data.recycle();
+ reply.recycle();
+ }
+ public ContentProviderHolder getContentProvider(IApplicationThread caller,
+ String name) throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(caller != null ? caller.asBinder() : null);
+ data.writeString(name);
+ mRemote.transact(GET_CONTENT_PROVIDER_TRANSACTION, data, reply, 0);
+ reply.readException();
+ int res = reply.readInt();
+ ContentProviderHolder cph = null;
+ if (res != 0) {
+ cph = ContentProviderHolder.CREATOR.createFromParcel(reply);
+ }
+ data.recycle();
+ reply.recycle();
+ return cph;
+ }
+ public void publishContentProviders(IApplicationThread caller,
+ List<ContentProviderHolder> providers) throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(caller != null ? caller.asBinder() : null);
+ data.writeTypedList(providers);
+ mRemote.transact(PUBLISH_CONTENT_PROVIDERS_TRANSACTION, data, reply, 0);
+ reply.readException();
+ data.recycle();
+ reply.recycle();
+ }
+
+ public void removeContentProvider(IApplicationThread caller,
+ String name) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(caller != null ? caller.asBinder() : null);
+ data.writeString(name);
+ mRemote.transact(REMOVE_CONTENT_PROVIDER_TRANSACTION, data, reply, 0);
+ reply.readException();
+ data.recycle();
+ reply.recycle();
+ }
+
+ public ComponentName startService(IApplicationThread caller, Intent service,
+ String resolvedType) throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(caller != null ? caller.asBinder() : null);
+ service.writeToParcel(data, 0);
+ data.writeString(resolvedType);
+ mRemote.transact(START_SERVICE_TRANSACTION, data, reply, 0);
+ reply.readException();
+ ComponentName res = ComponentName.readFromParcel(reply);
+ data.recycle();
+ reply.recycle();
+ return res;
+ }
+ public int stopService(IApplicationThread caller, Intent service,
+ String resolvedType) throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(caller != null ? caller.asBinder() : null);
+ service.writeToParcel(data, 0);
+ data.writeString(resolvedType);
+ mRemote.transact(STOP_SERVICE_TRANSACTION, data, reply, 0);
+ reply.readException();
+ int res = reply.readInt();
+ reply.recycle();
+ data.recycle();
+ return res;
+ }
+ public boolean stopServiceToken(ComponentName className, IBinder token,
+ int startId) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ ComponentName.writeToParcel(className, data);
+ data.writeStrongBinder(token);
+ data.writeInt(startId);
+ mRemote.transact(STOP_SERVICE_TOKEN_TRANSACTION, data, reply, 0);
+ reply.readException();
+ boolean res = reply.readInt() != 0;
+ data.recycle();
+ reply.recycle();
+ return res;
+ }
+ public void setServiceForeground(ComponentName className, IBinder token,
+ boolean isForeground) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ ComponentName.writeToParcel(className, data);
+ data.writeStrongBinder(token);
+ data.writeInt(isForeground ? 1 : 0);
+ mRemote.transact(SET_SERVICE_FOREGROUND_TRANSACTION, data, reply, 0);
+ reply.readException();
+ data.recycle();
+ reply.recycle();
+ }
+ public int bindService(IApplicationThread caller, IBinder token,
+ Intent service, String resolvedType, IServiceConnection connection,
+ int flags) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(caller != null ? caller.asBinder() : null);
+ data.writeStrongBinder(token);
+ service.writeToParcel(data, 0);
+ data.writeString(resolvedType);
+ data.writeStrongBinder(connection.asBinder());
+ data.writeInt(flags);
+ mRemote.transact(BIND_SERVICE_TRANSACTION, data, reply, 0);
+ reply.readException();
+ int res = reply.readInt();
+ data.recycle();
+ reply.recycle();
+ return res;
+ }
+ public boolean unbindService(IServiceConnection connection) throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(connection.asBinder());
+ mRemote.transact(UNBIND_SERVICE_TRANSACTION, data, reply, 0);
+ reply.readException();
+ boolean res = reply.readInt() != 0;
+ data.recycle();
+ reply.recycle();
+ return res;
+ }
+
+ public void publishService(IBinder token,
+ Intent intent, IBinder service) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(token);
+ intent.writeToParcel(data, 0);
+ data.writeStrongBinder(service);
+ mRemote.transact(PUBLISH_SERVICE_TRANSACTION, data, reply, 0);
+ reply.readException();
+ data.recycle();
+ reply.recycle();
+ }
+
+ public void unbindFinished(IBinder token, Intent intent, boolean doRebind)
+ throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(token);
+ intent.writeToParcel(data, 0);
+ data.writeInt(doRebind ? 1 : 0);
+ mRemote.transact(UNBIND_FINISHED_TRANSACTION, data, reply, 0);
+ reply.readException();
+ data.recycle();
+ reply.recycle();
+ }
+
+ public void serviceDoneExecuting(IBinder token) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(token);
+ mRemote.transact(SERVICE_DONE_EXECUTING_TRANSACTION, data, reply, IBinder.FLAG_ONEWAY);
+ reply.readException();
+ data.recycle();
+ reply.recycle();
+ }
+
+ public boolean startInstrumentation(ComponentName className, String profileFile,
+ int flags, Bundle arguments, IInstrumentationWatcher watcher)
+ throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ ComponentName.writeToParcel(className, data);
+ data.writeString(profileFile);
+ data.writeInt(flags);
+ data.writeBundle(arguments);
+ data.writeStrongBinder(watcher != null ? watcher.asBinder() : null);
+ mRemote.transact(START_INSTRUMENTATION_TRANSACTION, data, reply, 0);
+ reply.readException();
+ boolean res = reply.readInt() != 0;
+ reply.recycle();
+ data.recycle();
+ return res;
+ }
+
+ public void finishInstrumentation(IApplicationThread target,
+ int resultCode, Bundle results) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(target != null ? target.asBinder() : null);
+ data.writeInt(resultCode);
+ data.writeBundle(results);
+ mRemote.transact(FINISH_INSTRUMENTATION_TRANSACTION, data, reply, 0);
+ reply.readException();
+ data.recycle();
+ reply.recycle();
+ }
+ public Configuration getConfiguration() throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ mRemote.transact(GET_CONFIGURATION_TRANSACTION, data, reply, 0);
+ reply.readException();
+ Configuration res = Configuration.CREATOR.createFromParcel(reply);
+ reply.recycle();
+ data.recycle();
+ return res;
+ }
+ public void updateConfiguration(Configuration values) throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ values.writeToParcel(data, 0);
+ mRemote.transact(UPDATE_CONFIGURATION_TRANSACTION, data, reply, 0);
+ reply.readException();
+ data.recycle();
+ reply.recycle();
+ }
+ public void setRequestedOrientation(IBinder token, int requestedOrientation)
+ throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(token);
+ data.writeInt(requestedOrientation);
+ mRemote.transact(SET_REQUESTED_ORIENTATION_TRANSACTION, data, reply, 0);
+ reply.readException();
+ data.recycle();
+ reply.recycle();
+ }
+ public int getRequestedOrientation(IBinder token) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(token);
+ mRemote.transact(GET_REQUESTED_ORIENTATION_TRANSACTION, data, reply, 0);
+ reply.readException();
+ int res = reply.readInt();
+ data.recycle();
+ reply.recycle();
+ return res;
+ }
+ public ComponentName getActivityClassForToken(IBinder token)
+ throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(token);
+ mRemote.transact(GET_ACTIVITY_CLASS_FOR_TOKEN_TRANSACTION, data, reply, 0);
+ reply.readException();
+ ComponentName res = ComponentName.readFromParcel(reply);
+ data.recycle();
+ reply.recycle();
+ return res;
+ }
+ public String getPackageForToken(IBinder token) throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(token);
+ mRemote.transact(GET_PACKAGE_FOR_TOKEN_TRANSACTION, data, reply, 0);
+ reply.readException();
+ String res = reply.readString();
+ data.recycle();
+ reply.recycle();
+ return res;
+ }
+ public IIntentSender getIntentSender(int type,
+ String packageName, IBinder token, String resultWho,
+ int requestCode, Intent intent, String resolvedType, int flags)
+ throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeInt(type);
+ data.writeString(packageName);
+ data.writeStrongBinder(token);
+ data.writeString(resultWho);
+ data.writeInt(requestCode);
+ if (intent != null) {
+ data.writeInt(1);
+ intent.writeToParcel(data, 0);
+ } else {
+ data.writeInt(0);
+ }
+ data.writeString(resolvedType);
+ data.writeInt(flags);
+ mRemote.transact(GET_INTENT_SENDER_TRANSACTION, data, reply, 0);
+ reply.readException();
+ IIntentSender res = IIntentSender.Stub.asInterface(
+ reply.readStrongBinder());
+ data.recycle();
+ reply.recycle();
+ return res;
+ }
+ public void cancelIntentSender(IIntentSender sender) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(sender.asBinder());
+ mRemote.transact(CANCEL_INTENT_SENDER_TRANSACTION, data, reply, 0);
+ reply.readException();
+ data.recycle();
+ reply.recycle();
+ }
+ public String getPackageForIntentSender(IIntentSender sender) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(sender.asBinder());
+ mRemote.transact(GET_PACKAGE_FOR_INTENT_SENDER_TRANSACTION, data, reply, 0);
+ reply.readException();
+ String res = reply.readString();
+ data.recycle();
+ reply.recycle();
+ return res;
+ }
+ public void setProcessLimit(int max) throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeInt(max);
+ mRemote.transact(SET_PROCESS_LIMIT_TRANSACTION, data, reply, 0);
+ reply.readException();
+ data.recycle();
+ reply.recycle();
+ }
+ public int getProcessLimit() throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ mRemote.transact(GET_PROCESS_LIMIT_TRANSACTION, data, reply, 0);
+ reply.readException();
+ int res = reply.readInt();
+ data.recycle();
+ reply.recycle();
+ return res;
+ }
+ public void setProcessForeground(IBinder token, int pid,
+ boolean isForeground) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(token);
+ data.writeInt(pid);
+ data.writeInt(isForeground ? 1 : 0);
+ mRemote.transact(SET_PROCESS_FOREGROUND_TRANSACTION, data, reply, 0);
+ reply.readException();
+ data.recycle();
+ reply.recycle();
+ }
+ public int checkPermission(String permission, int pid, int uid)
+ throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeString(permission);
+ data.writeInt(pid);
+ data.writeInt(uid);
+ mRemote.transact(CHECK_PERMISSION_TRANSACTION, data, reply, 0);
+ reply.readException();
+ int res = reply.readInt();
+ data.recycle();
+ reply.recycle();
+ return res;
+ }
+ public boolean clearApplicationUserData(final String packageName,
+ final IPackageDataObserver observer) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeString(packageName);
+ data.writeStrongBinder(observer.asBinder());
+ mRemote.transact(CLEAR_APP_DATA_TRANSACTION, data, reply, 0);
+ reply.readException();
+ boolean res = reply.readInt() != 0;
+ data.recycle();
+ reply.recycle();
+ return res;
+ }
+ public int checkUriPermission(Uri uri, int pid, int uid, int mode)
+ throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ uri.writeToParcel(data, 0);
+ data.writeInt(pid);
+ data.writeInt(uid);
+ data.writeInt(mode);
+ mRemote.transact(CHECK_URI_PERMISSION_TRANSACTION, data, reply, 0);
+ reply.readException();
+ int res = reply.readInt();
+ data.recycle();
+ reply.recycle();
+ return res;
+ }
+ public void grantUriPermission(IApplicationThread caller, String targetPkg,
+ Uri uri, int mode) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(caller.asBinder());
+ data.writeString(targetPkg);
+ uri.writeToParcel(data, 0);
+ data.writeInt(mode);
+ mRemote.transact(GRANT_URI_PERMISSION_TRANSACTION, data, reply, 0);
+ reply.readException();
+ data.recycle();
+ reply.recycle();
+ }
+ public void revokeUriPermission(IApplicationThread caller, Uri uri,
+ int mode) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(caller.asBinder());
+ uri.writeToParcel(data, 0);
+ data.writeInt(mode);
+ mRemote.transact(REVOKE_URI_PERMISSION_TRANSACTION, data, reply, 0);
+ reply.readException();
+ data.recycle();
+ reply.recycle();
+ }
+ public void showWaitingForDebugger(IApplicationThread who, boolean waiting)
+ throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(who.asBinder());
+ data.writeInt(waiting ? 1 : 0);
+ mRemote.transact(SHOW_WAITING_FOR_DEBUGGER_TRANSACTION, data, reply, 0);
+ reply.readException();
+ data.recycle();
+ reply.recycle();
+ }
+ public void getMemoryInfo(ActivityManager.MemoryInfo outInfo) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ mRemote.transact(GET_MEMORY_INFO_TRANSACTION, data, reply, 0);
+ reply.readException();
+ outInfo.readFromParcel(reply);
+ data.recycle();
+ reply.recycle();
+ }
+ public void unhandledBack() throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ mRemote.transact(UNHANDLED_BACK_TRANSACTION, data, reply, 0);
+ reply.readException();
+ data.recycle();
+ reply.recycle();
+ }
+ public ParcelFileDescriptor openContentUri(Uri uri) throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ mRemote.transact(OPEN_CONTENT_URI_TRANSACTION, data, reply, 0);
+ reply.readException();
+ ParcelFileDescriptor pfd = null;
+ if (reply.readInt() != 0) {
+ pfd = ParcelFileDescriptor.CREATOR.createFromParcel(reply);
+ }
+ data.recycle();
+ reply.recycle();
+ return pfd;
+ }
+ public void goingToSleep() throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ mRemote.transact(GOING_TO_SLEEP_TRANSACTION, data, reply, 0);
+ reply.readException();
+ data.recycle();
+ reply.recycle();
+ }
+ public void wakingUp() throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ mRemote.transact(WAKING_UP_TRANSACTION, data, reply, 0);
+ reply.readException();
+ data.recycle();
+ reply.recycle();
+ }
+ public void setDebugApp(
+ String packageName, boolean waitForDebugger, boolean persistent)
+ throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeString(packageName);
+ data.writeInt(waitForDebugger ? 1 : 0);
+ data.writeInt(persistent ? 1 : 0);
+ mRemote.transact(SET_DEBUG_APP_TRANSACTION, data, reply, 0);
+ reply.readException();
+ data.recycle();
+ reply.recycle();
+ }
+ public void setAlwaysFinish(boolean enabled) throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeInt(enabled ? 1 : 0);
+ mRemote.transact(SET_ALWAYS_FINISH_TRANSACTION, data, reply, 0);
+ reply.readException();
+ data.recycle();
+ reply.recycle();
+ }
+ public void setActivityWatcher(IActivityWatcher watcher) throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(watcher != null ? watcher.asBinder() : null);
+ mRemote.transact(SET_ACTIVITY_WATCHER_TRANSACTION, data, reply, 0);
+ reply.readException();
+ data.recycle();
+ reply.recycle();
+ }
+ public void enterSafeMode() throws RemoteException {
+ Parcel data = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ mRemote.transact(ENTER_SAFE_MODE_TRANSACTION, data, null, 0);
+ data.recycle();
+ }
+ public void noteWakeupAlarm(IIntentSender sender) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ data.writeStrongBinder(sender.asBinder());
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ mRemote.transact(NOTE_WAKEUP_ALARM_TRANSACTION, data, null, 0);
+ data.recycle();
+ }
+ public boolean killPidsForMemory(int[] pids) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeIntArray(pids);
+ mRemote.transact(KILL_PIDS_FOR_MEMORY_TRANSACTION, data, reply, 0);
+ boolean res = reply.readInt() != 0;
+ data.recycle();
+ reply.recycle();
+ return res;
+ }
+ public void reportPss(IApplicationThread caller, int pss) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(caller.asBinder());
+ data.writeInt(pss);
+ mRemote.transact(REPORT_PSS_TRANSACTION, data, null, 0);
+ data.recycle();
+ }
+ public void startRunning(String pkg, String cls, String action,
+ String indata) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeString(pkg);
+ data.writeString(cls);
+ data.writeString(action);
+ data.writeString(indata);
+ mRemote.transact(START_RUNNING_TRANSACTION, data, reply, 0);
+ reply.readException();
+ data.recycle();
+ reply.recycle();
+ }
+ public void systemReady() throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ mRemote.transact(SYSTEM_READY_TRANSACTION, data, reply, 0);
+ reply.readException();
+ data.recycle();
+ reply.recycle();
+ }
+ public int handleApplicationError(IBinder app, int flags,
+ String tag, String shortMsg, String longMsg,
+ byte[] crashData) throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeStrongBinder(app);
+ data.writeInt(flags);
+ data.writeString(tag);
+ data.writeString(shortMsg);
+ data.writeString(longMsg);
+ data.writeByteArray(crashData);
+ mRemote.transact(HANDLE_APPLICATION_ERROR_TRANSACTION, data, reply, 0);
+ reply.readException();
+ int res = reply.readInt();
+ reply.recycle();
+ data.recycle();
+ return res;
+ }
+
+ public void signalPersistentProcesses(int sig) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeInt(sig);
+ mRemote.transact(SIGNAL_PERSISTENT_PROCESSES_TRANSACTION, data, reply, 0);
+ reply.readException();
+ data.recycle();
+ reply.recycle();
+ }
+
+ public void restartPackage(String packageName) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeString(packageName);
+ mRemote.transact(RESTART_PACKAGE_TRANSACTION, data, reply, 0);
+ reply.readException();
+ data.recycle();
+ reply.recycle();
+ }
+
+ private IBinder mRemote;
+}
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
new file mode 100644
index 0000000..d03a76f
--- /dev/null
+++ b/core/java/android/app/ActivityThread.java
@@ -0,0 +1,3754 @@
+/*
+ * 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.app;
+
+import android.content.BroadcastReceiver;
+import android.content.ComponentCallbacks;
+import android.content.ComponentName;
+import android.content.ContentProvider;
+import android.content.Context;
+import android.content.IContentProvider;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageManager;
+import android.content.pm.InstrumentationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ProviderInfo;
+import android.content.pm.ServiceInfo;
+import android.content.res.AssetManager;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteDebug;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.net.http.AndroidHttpClient;
+import android.os.Bundle;
+import android.os.Debug;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.MessageQueue;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemClock;
+import android.util.AndroidRuntimeException;
+import android.util.Config;
+import android.util.DisplayMetrics;
+import android.util.EventLog;
+import android.util.Log;
+import android.view.Display;
+import android.view.View;
+import android.view.ViewDebug;
+import android.view.ViewManager;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.WindowManagerImpl;
+
+import com.android.internal.os.BinderInternal;
+import com.android.internal.os.RuntimeInit;
+import com.android.internal.util.ArrayUtils;
+
+import org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileOutputStream;
+import java.io.PrintWriter;
+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.TimeZone;
+import java.util.regex.Pattern;
+
+final class IntentReceiverLeaked extends AndroidRuntimeException {
+ public IntentReceiverLeaked(String msg) {
+ super(msg);
+ }
+}
+
+final class ServiceConnectionLeaked extends AndroidRuntimeException {
+ public ServiceConnectionLeaked(String msg) {
+ super(msg);
+ }
+}
+
+final class SuperNotCalledException extends AndroidRuntimeException {
+ public SuperNotCalledException(String msg) {
+ super(msg);
+ }
+}
+
+/**
+ * This manages the execution of the main thread in an
+ * application process, scheduling and executing activities,
+ * broadcasts, and other operations on it as the activity
+ * manager requests.
+ *
+ * {@hide}
+ */
+public final class ActivityThread {
+ private static final String TAG = "ActivityThread";
+ private static final boolean DEBUG = false;
+ private static final boolean localLOGV = DEBUG ? Config.LOGD : Config.LOGV;
+ private static final boolean DEBUG_BROADCAST = false;
+ private static final long MIN_TIME_BETWEEN_GCS = 5*1000;
+ private static final Pattern PATTERN_SEMICOLON = Pattern.compile(";");
+ private static final int SQLITE_MEM_RELEASED_EVENT_LOG_TAG = 75003;
+ private static final int LOG_ON_PAUSE_CALLED = 30021;
+ private static final int LOG_ON_RESUME_CALLED = 30022;
+
+
+ public static final ActivityThread currentActivityThread() {
+ return (ActivityThread)sThreadLocal.get();
+ }
+
+ public static final String currentPackageName()
+ {
+ ActivityThread am = currentActivityThread();
+ return (am != null && am.mBoundApplication != null)
+ ? am.mBoundApplication.processName : null;
+ }
+
+ public static IPackageManager getPackageManager() {
+ if (sPackageManager != null) {
+ //Log.v("PackageManager", "returning cur default = " + sPackageManager);
+ return sPackageManager;
+ }
+ IBinder b = ServiceManager.getService("package");
+ //Log.v("PackageManager", "default service binder = " + b);
+ sPackageManager = IPackageManager.Stub.asInterface(b);
+ //Log.v("PackageManager", "default service = " + sPackageManager);
+ return sPackageManager;
+ }
+
+ DisplayMetrics getDisplayMetricsLocked() {
+ if (mDisplay == null) {
+ WindowManager wm = WindowManagerImpl.getDefault();
+ mDisplay = wm.getDefaultDisplay();
+ }
+ DisplayMetrics metrics = new DisplayMetrics();
+ mDisplay.getMetrics(metrics);
+ return metrics;
+ }
+
+ Resources getTopLevelResources(String appDir) {
+ synchronized (mPackages) {
+ //Log.w(TAG, "getTopLevelResources: " + appDir);
+ WeakReference<Resources> wr = mActiveResources.get(appDir);
+ Resources r = wr != null ? wr.get() : null;
+ if (r != null && r.getAssets().isUpToDate()) {
+ //Log.w(TAG, "Returning cached resources " + r + " " + appDir);
+ return r;
+ }
+
+ //if (r != null) {
+ // Log.w(TAG, "Throwing away out-of-date resources!!!! "
+ // + r + " " + appDir);
+ //}
+
+ AssetManager assets = new AssetManager();
+ if (assets.addAssetPath(appDir) == 0) {
+ return null;
+ }
+ DisplayMetrics metrics = getDisplayMetricsLocked();
+ r = new Resources(assets, metrics, getConfiguration());
+ //Log.i(TAG, "Created app resources " + r + ": " + r.getConfiguration());
+ // XXX need to remove entries when weak references go away
+ mActiveResources.put(appDir, new WeakReference<Resources>(r));
+ return r;
+ }
+ }
+
+ final Handler getHandler() {
+ return mH;
+ }
+
+ public final static class PackageInfo {
+
+ private final ActivityThread mActivityThread;
+ private final ApplicationInfo mApplicationInfo;
+ private final String mPackageName;
+ private final String mAppDir;
+ private final String mResDir;
+ private final String[] mSharedLibraries;
+ private final String mDataDir;
+ private final File mDataDirFile;
+ private final ClassLoader mBaseClassLoader;
+ private final boolean mSecurityViolation;
+ private final boolean mIncludeCode;
+ private Resources mResources;
+ private ClassLoader mClassLoader;
+ private Application mApplication;
+ private final HashMap<Context, HashMap<BroadcastReceiver, ReceiverDispatcher>> mReceivers
+ = new HashMap<Context, HashMap<BroadcastReceiver, ReceiverDispatcher>>();
+ private final HashMap<Context, HashMap<BroadcastReceiver, ReceiverDispatcher>> mUnregisteredReceivers
+ = new HashMap<Context, HashMap<BroadcastReceiver, ReceiverDispatcher>>();
+ private final HashMap<Context, HashMap<ServiceConnection, ServiceDispatcher>> mServices
+ = new HashMap<Context, HashMap<ServiceConnection, ServiceDispatcher>>();
+ private final HashMap<Context, HashMap<ServiceConnection, ServiceDispatcher>> mUnboundServices
+ = new HashMap<Context, HashMap<ServiceConnection, ServiceDispatcher>>();
+
+ int mClientCount = 0;
+
+ public PackageInfo(ActivityThread activityThread, ApplicationInfo aInfo,
+ ActivityThread mainThread, ClassLoader baseLoader,
+ boolean securityViolation, boolean includeCode) {
+ mActivityThread = activityThread;
+ mApplicationInfo = aInfo;
+ mPackageName = aInfo.packageName;
+ mAppDir = aInfo.sourceDir;
+ mResDir = aInfo.publicSourceDir;
+ mSharedLibraries = aInfo.sharedLibraryFiles;
+ mDataDir = aInfo.dataDir;
+ mDataDirFile = mDataDir != null ? new File(mDataDir) : null;
+ mBaseClassLoader = baseLoader;
+ mSecurityViolation = securityViolation;
+ mIncludeCode = includeCode;
+
+ if (mAppDir == null) {
+ if (mSystemContext == null) {
+ mSystemContext =
+ ApplicationContext.createSystemContext(mainThread);
+ mSystemContext.getResources().updateConfiguration(
+ mainThread.getConfiguration(),
+ mainThread.getDisplayMetricsLocked());
+ //Log.i(TAG, "Created system resources "
+ // + mSystemContext.getResources() + ": "
+ // + mSystemContext.getResources().getConfiguration());
+ }
+ mClassLoader = mSystemContext.getClassLoader();
+ mResources = mSystemContext.getResources();
+ }
+ }
+
+ public PackageInfo(ActivityThread activityThread, String name,
+ Context systemContext) {
+ mActivityThread = activityThread;
+ mApplicationInfo = new ApplicationInfo();
+ mApplicationInfo.packageName = name;
+ mPackageName = name;
+ mAppDir = null;
+ mResDir = null;
+ mSharedLibraries = null;
+ mDataDir = null;
+ mDataDirFile = null;
+ mBaseClassLoader = null;
+ mSecurityViolation = false;
+ mIncludeCode = true;
+ mClassLoader = systemContext.getClassLoader();
+ mResources = systemContext.getResources();
+ }
+
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ public boolean isSecurityViolation() {
+ return mSecurityViolation;
+ }
+
+ /**
+ * Gets the array of shared libraries that are listed as
+ * used by the given package.
+ *
+ * @param packageName the name of the package (note: not its
+ * file name)
+ * @return null-ok; the array of shared libraries, each one
+ * a fully-qualified path
+ */
+ private static String[] getLibrariesFor(String packageName) {
+ ApplicationInfo ai = null;
+ try {
+ ai = getPackageManager().getApplicationInfo(packageName,
+ PackageManager.GET_SHARED_LIBRARY_FILES);
+ } catch (RemoteException e) {
+ throw new AssertionError(e);
+ }
+
+ if (ai == null) {
+ return null;
+ }
+
+ return ai.sharedLibraryFiles;
+ }
+
+ /**
+ * Combines two arrays (of library names) such that they are
+ * concatenated in order but are devoid of duplicates. The
+ * result is a single string with the names of the libraries
+ * separated by colons, or <code>null</code> if both lists
+ * were <code>null</code> or empty.
+ *
+ * @param list1 null-ok; the first list
+ * @param list2 null-ok; the second list
+ * @return null-ok; the combination
+ */
+ private static String combineLibs(String[] list1, String[] list2) {
+ StringBuilder result = new StringBuilder(300);
+ boolean first = true;
+
+ if (list1 != null) {
+ for (String s : list1) {
+ if (first) {
+ first = false;
+ } else {
+ result.append(':');
+ }
+ result.append(s);
+ }
+ }
+
+ // Only need to check for duplicates if list1 was non-empty.
+ boolean dupCheck = !first;
+
+ if (list2 != null) {
+ for (String s : list2) {
+ if (dupCheck && ArrayUtils.contains(list1, s)) {
+ continue;
+ }
+
+ if (first) {
+ first = false;
+ } else {
+ result.append(':');
+ }
+ result.append(s);
+ }
+ }
+
+ return result.toString();
+ }
+
+ public ClassLoader getClassLoader() {
+ synchronized (this) {
+ if (mClassLoader != null) {
+ return mClassLoader;
+ }
+
+ if (mIncludeCode && !mPackageName.equals("android")) {
+ String zip = mAppDir;
+
+ /*
+ * The following is a bit of a hack to inject
+ * instrumentation into the system: If the app
+ * being started matches one of the instrumentation names,
+ * then we combine both the "instrumentation" and
+ * "instrumented" app into the path, along with the
+ * concatenation of both apps' shared library lists.
+ */
+
+ String instrumentationAppDir =
+ mActivityThread.mInstrumentationAppDir;
+ String instrumentationAppPackage =
+ mActivityThread.mInstrumentationAppPackage;
+ String instrumentedAppDir =
+ mActivityThread.mInstrumentedAppDir;
+ String[] instrumentationLibs = null;
+
+ if (mAppDir.equals(instrumentationAppDir)
+ || mAppDir.equals(instrumentedAppDir)) {
+ zip = instrumentationAppDir + ":" + instrumentedAppDir;
+ if (! instrumentedAppDir.equals(instrumentationAppDir)) {
+ instrumentationLibs =
+ getLibrariesFor(instrumentationAppPackage);
+ }
+ }
+
+ if ((mSharedLibraries != null) ||
+ (instrumentationLibs != null)) {
+ zip =
+ combineLibs(mSharedLibraries, instrumentationLibs)
+ + ':' + zip;
+ }
+
+ /*
+ * With all the combination done (if necessary, actually
+ * create the class loader.
+ */
+
+ if (localLOGV) Log.v(TAG, "Class path: " + zip);
+
+ mClassLoader =
+ ApplicationLoaders.getDefault().getClassLoader(
+ zip, mDataDir, mBaseClassLoader);
+ } else {
+ if (mBaseClassLoader == null) {
+ mClassLoader = ClassLoader.getSystemClassLoader();
+ } else {
+ mClassLoader = mBaseClassLoader;
+ }
+ }
+ return mClassLoader;
+ }
+ }
+
+ public String getAppDir() {
+ return mAppDir;
+ }
+
+ public String getResDir() {
+ return mResDir;
+ }
+
+ public String getDataDir() {
+ return mDataDir;
+ }
+
+ public File getDataDirFile() {
+ return mDataDirFile;
+ }
+
+ public AssetManager getAssets(ActivityThread mainThread) {
+ return getResources(mainThread).getAssets();
+ }
+
+ public Resources getResources(ActivityThread mainThread) {
+ if (mResources == null) {
+ mResources = mainThread.getTopLevelResources(mResDir);
+ }
+ return mResources;
+ }
+
+ public Application makeApplication() {
+ if (mApplication != null) {
+ return mApplication;
+ }
+
+ Application app = null;
+
+ String appClass = mApplicationInfo.className;
+ if (appClass == null) {
+ appClass = "android.app.Application";
+ }
+
+ try {
+ java.lang.ClassLoader cl = getClassLoader();
+ ApplicationContext appContext = new ApplicationContext();
+ appContext.init(this, null, mActivityThread);
+ app = mActivityThread.mInstrumentation.newApplication(
+ cl, appClass, appContext);
+ appContext.setOuterContext(app);
+ } catch (Exception e) {
+ if (!mActivityThread.mInstrumentation.onException(app, e)) {
+ throw new RuntimeException(
+ "Unable to instantiate application " + appClass
+ + ": " + e.toString(), e);
+ }
+ }
+ mActivityThread.mAllApplications.add(app);
+ return mApplication = app;
+ }
+
+ public void removeContextRegistrations(Context context,
+ String who, String what) {
+ HashMap<BroadcastReceiver, ReceiverDispatcher> rmap =
+ mReceivers.remove(context);
+ if (rmap != null) {
+ Iterator<ReceiverDispatcher> it = rmap.values().iterator();
+ while (it.hasNext()) {
+ ReceiverDispatcher rd = it.next();
+ IntentReceiverLeaked leak = new IntentReceiverLeaked(
+ what + " " + who + " has leaked IntentReceiver "
+ + rd.getIntentReceiver() + " that was " +
+ "originally registered here. Are you missing a " +
+ "call to unregisterReceiver()?");
+ leak.setStackTrace(rd.getLocation().getStackTrace());
+ Log.e(TAG, leak.getMessage(), leak);
+ try {
+ ActivityManagerNative.getDefault().unregisterReceiver(
+ rd.getIIntentReceiver());
+ } catch (RemoteException e) {
+ // system crashed, nothing we can do
+ }
+ }
+ }
+ mUnregisteredReceivers.remove(context);
+ //Log.i(TAG, "Receiver registrations: " + mReceivers);
+ HashMap<ServiceConnection, ServiceDispatcher> smap =
+ mServices.remove(context);
+ if (smap != null) {
+ Iterator<ServiceDispatcher> it = smap.values().iterator();
+ while (it.hasNext()) {
+ ServiceDispatcher sd = it.next();
+ ServiceConnectionLeaked leak = new ServiceConnectionLeaked(
+ what + " " + who + " has leaked ServiceConnection "
+ + sd.getServiceConnection() + " that was originally bound here");
+ leak.setStackTrace(sd.getLocation().getStackTrace());
+ Log.e(TAG, leak.getMessage(), leak);
+ try {
+ ActivityManagerNative.getDefault().unbindService(
+ sd.getIServiceConnection());
+ } catch (RemoteException e) {
+ // system crashed, nothing we can do
+ }
+ sd.doForget();
+ }
+ }
+ mUnboundServices.remove(context);
+ //Log.i(TAG, "Service registrations: " + mServices);
+ }
+
+ public IIntentReceiver getReceiverDispatcher(BroadcastReceiver r,
+ Context context, Handler handler,
+ Instrumentation instrumentation, boolean registered) {
+ synchronized (mReceivers) {
+ ReceiverDispatcher rd = null;
+ HashMap<BroadcastReceiver, ReceiverDispatcher> map = null;
+ if (registered) {
+ map = mReceivers.get(context);
+ if (map != null) {
+ rd = map.get(r);
+ }
+ }
+ if (rd == null) {
+ rd = new ReceiverDispatcher(r, context, handler,
+ instrumentation, registered);
+ if (registered) {
+ if (map == null) {
+ map = new HashMap<BroadcastReceiver, ReceiverDispatcher>();
+ mReceivers.put(context, map);
+ }
+ map.put(r, rd);
+ }
+ } else {
+ rd.validate(context, handler);
+ }
+ return rd.getIIntentReceiver();
+ }
+ }
+
+ public IIntentReceiver forgetReceiverDispatcher(Context context,
+ BroadcastReceiver r) {
+ synchronized (mReceivers) {
+ HashMap<BroadcastReceiver, ReceiverDispatcher> map = mReceivers.get(context);
+ ReceiverDispatcher rd = null;
+ if (map != null) {
+ rd = map.get(r);
+ if (rd != null) {
+ map.remove(r);
+ if (map.size() == 0) {
+ mReceivers.remove(context);
+ }
+ if (r.getDebugUnregister()) {
+ HashMap<BroadcastReceiver, ReceiverDispatcher> holder
+ = mUnregisteredReceivers.get(context);
+ if (holder == null) {
+ holder = new HashMap<BroadcastReceiver, ReceiverDispatcher>();
+ mUnregisteredReceivers.put(context, holder);
+ }
+ RuntimeException ex = new IllegalArgumentException(
+ "Originally unregistered here:");
+ ex.fillInStackTrace();
+ rd.setUnregisterLocation(ex);
+ holder.put(r, rd);
+ }
+ return rd.getIIntentReceiver();
+ }
+ }
+ HashMap<BroadcastReceiver, ReceiverDispatcher> holder
+ = mUnregisteredReceivers.get(context);
+ if (holder != null) {
+ rd = holder.get(r);
+ if (rd != null) {
+ RuntimeException ex = rd.getUnregisterLocation();
+ throw new IllegalArgumentException(
+ "Unregistering Receiver " + r
+ + " that was already unregistered", ex);
+ }
+ }
+ if (context == null) {
+ throw new IllegalStateException("Unbinding Receiver " + r
+ + " from Context that is no longer in use: " + context);
+ } else {
+ throw new IllegalArgumentException("Receiver not registered: " + r);
+ }
+
+ }
+ }
+
+ static final class ReceiverDispatcher {
+
+ final static class InnerReceiver extends IIntentReceiver.Stub {
+ final WeakReference<ReceiverDispatcher> mDispatcher;
+ final ReceiverDispatcher mStrongRef;
+
+ InnerReceiver(ReceiverDispatcher rd, boolean strong) {
+ mDispatcher = new WeakReference<ReceiverDispatcher>(rd);
+ mStrongRef = strong ? rd : null;
+ }
+ public void performReceive(Intent intent, int resultCode,
+ String data, Bundle extras, boolean ordered) {
+ ReceiverDispatcher rd = mDispatcher.get();
+ if (DEBUG_BROADCAST) {
+ int seq = intent.getIntExtra("seq", -1);
+ Log.i(TAG, "Receiving broadcast " + intent.getAction() + " seq=" + seq
+ + " to " + rd);
+ }
+ if (rd != null) {
+ rd.performReceive(intent, resultCode, data, extras, ordered);
+ }
+ }
+ }
+
+ final IIntentReceiver.Stub mIIntentReceiver;
+ final BroadcastReceiver mReceiver;
+ final Context mContext;
+ final Handler mActivityThread;
+ final Instrumentation mInstrumentation;
+ final boolean mRegistered;
+ final IntentReceiverLeaked mLocation;
+ RuntimeException mUnregisterLocation;
+
+ final class Args implements Runnable {
+ private Intent mCurIntent;
+ private int mCurCode;
+ private String mCurData;
+ private Bundle mCurMap;
+ private boolean mCurOrdered;
+
+ public void run() {
+ BroadcastReceiver receiver = mReceiver;
+ if (DEBUG_BROADCAST) {
+ int seq = mCurIntent.getIntExtra("seq", -1);
+ Log.i(TAG, "Dispathing broadcast " + mCurIntent.getAction() + " seq=" + seq
+ + " to " + mReceiver);
+ }
+ if (receiver == null) {
+ return;
+ }
+
+ IActivityManager mgr = ActivityManagerNative.getDefault();
+ Intent intent = mCurIntent;
+ mCurIntent = null;
+ try {
+ ClassLoader cl = mReceiver.getClass().getClassLoader();
+ intent.setExtrasClassLoader(cl);
+ if (mCurMap != null) {
+ mCurMap.setClassLoader(cl);
+ }
+ receiver.setOrderedHint(true);
+ receiver.setResult(mCurCode, mCurData, mCurMap);
+ receiver.clearAbortBroadcast();
+ receiver.setOrderedHint(mCurOrdered);
+ receiver.onReceive(mContext, intent);
+ } catch (Exception e) {
+ if (mRegistered && mCurOrdered) {
+ try {
+ mgr.finishReceiver(mIIntentReceiver,
+ mCurCode, mCurData, mCurMap, false);
+ } catch (RemoteException ex) {
+ }
+ }
+ if (mInstrumentation == null ||
+ !mInstrumentation.onException(mReceiver, e)) {
+ throw new RuntimeException(
+ "Error receiving broadcast " + intent
+ + " in " + mReceiver, e);
+ }
+ }
+ if (mRegistered && mCurOrdered) {
+ try {
+ mgr.finishReceiver(mIIntentReceiver,
+ receiver.getResultCode(),
+ receiver.getResultData(),
+ receiver.getResultExtras(false),
+ receiver.getAbortBroadcast());
+ } catch (RemoteException ex) {
+ }
+ }
+ }
+ }
+
+ ReceiverDispatcher(BroadcastReceiver receiver, Context context,
+ Handler activityThread, Instrumentation instrumentation,
+ boolean registered) {
+ if (activityThread == null) {
+ throw new NullPointerException("Handler must not be null");
+ }
+
+ mIIntentReceiver = new InnerReceiver(this, !registered);
+ mReceiver = receiver;
+ mContext = context;
+ mActivityThread = activityThread;
+ mInstrumentation = instrumentation;
+ mRegistered = registered;
+ mLocation = new IntentReceiverLeaked(null);
+ mLocation.fillInStackTrace();
+ }
+
+ void validate(Context context, Handler activityThread) {
+ if (mContext != context) {
+ throw new IllegalStateException(
+ "Receiver " + mReceiver +
+ " registered with differing Context (was " +
+ mContext + " now " + context + ")");
+ }
+ if (mActivityThread != activityThread) {
+ throw new IllegalStateException(
+ "Receiver " + mReceiver +
+ " registered with differing handler (was " +
+ mActivityThread + " now " + activityThread + ")");
+ }
+ }
+
+ IntentReceiverLeaked getLocation() {
+ return mLocation;
+ }
+
+ BroadcastReceiver getIntentReceiver() {
+ return mReceiver;
+ }
+
+ IIntentReceiver getIIntentReceiver() {
+ return mIIntentReceiver;
+ }
+
+ void setUnregisterLocation(RuntimeException ex) {
+ mUnregisterLocation = ex;
+ }
+
+ RuntimeException getUnregisterLocation() {
+ return mUnregisterLocation;
+ }
+
+ public void performReceive(Intent intent, int resultCode,
+ String data, Bundle extras, boolean ordered) {
+ if (DEBUG_BROADCAST) {
+ int seq = intent.getIntExtra("seq", -1);
+ Log.i(TAG, "Enqueueing broadcast " + intent.getAction() + " seq=" + seq
+ + " to " + mReceiver);
+ }
+ Args args = new Args();
+ args.mCurIntent = intent;
+ args.mCurCode = resultCode;
+ args.mCurData = data;
+ args.mCurMap = extras;
+ args.mCurOrdered = ordered;
+ if (!mActivityThread.post(args)) {
+ if (mRegistered) {
+ IActivityManager mgr = ActivityManagerNative.getDefault();
+ try {
+ mgr.finishReceiver(mIIntentReceiver, args.mCurCode,
+ args.mCurData, args.mCurMap, false);
+ } catch (RemoteException ex) {
+ }
+ }
+ }
+ }
+
+ }
+
+ public final IServiceConnection getServiceDispatcher(ServiceConnection c,
+ Context context, Handler handler, int flags) {
+ synchronized (mServices) {
+ ServiceDispatcher sd = null;
+ HashMap<ServiceConnection, ServiceDispatcher> map = mServices.get(context);
+ if (map != null) {
+ sd = map.get(c);
+ }
+ if (sd == null) {
+ sd = new ServiceDispatcher(c, context, handler, flags);
+ if (map == null) {
+ map = new HashMap<ServiceConnection, ServiceDispatcher>();
+ mServices.put(context, map);
+ }
+ map.put(c, sd);
+ } else {
+ sd.validate(context, handler);
+ }
+ return sd.getIServiceConnection();
+ }
+ }
+
+ public final IServiceConnection forgetServiceDispatcher(Context context,
+ ServiceConnection c) {
+ synchronized (mServices) {
+ HashMap<ServiceConnection, ServiceDispatcher> map
+ = mServices.get(context);
+ ServiceDispatcher sd = null;
+ if (map != null) {
+ sd = map.get(c);
+ if (sd != null) {
+ map.remove(c);
+ sd.doForget();
+ if (map.size() == 0) {
+ mServices.remove(context);
+ }
+ if ((sd.getFlags()&Context.BIND_DEBUG_UNBIND) != 0) {
+ HashMap<ServiceConnection, ServiceDispatcher> holder
+ = mUnboundServices.get(context);
+ if (holder == null) {
+ holder = new HashMap<ServiceConnection, ServiceDispatcher>();
+ mUnboundServices.put(context, holder);
+ }
+ RuntimeException ex = new IllegalArgumentException(
+ "Originally unbound here:");
+ ex.fillInStackTrace();
+ sd.setUnbindLocation(ex);
+ holder.put(c, sd);
+ }
+ return sd.getIServiceConnection();
+ }
+ }
+ HashMap<ServiceConnection, ServiceDispatcher> holder
+ = mUnboundServices.get(context);
+ if (holder != null) {
+ sd = holder.get(c);
+ if (sd != null) {
+ RuntimeException ex = sd.getUnbindLocation();
+ throw new IllegalArgumentException(
+ "Unbinding Service " + c
+ + " that was already unbound", ex);
+ }
+ }
+ if (context == null) {
+ throw new IllegalStateException("Unbinding Service " + c
+ + " from Context that is no longer in use: " + context);
+ } else {
+ throw new IllegalArgumentException("Service not registered: " + c);
+ }
+ }
+ }
+
+ static final class ServiceDispatcher {
+ private final InnerConnection mIServiceConnection;
+ private final ServiceConnection mConnection;
+ private final Context mContext;
+ private final Handler mActivityThread;
+ private final ServiceConnectionLeaked mLocation;
+ private final int mFlags;
+
+ private RuntimeException mUnbindLocation;
+
+ private boolean mDied;
+
+ private static class ConnectionInfo {
+ IBinder binder;
+ IBinder.DeathRecipient deathMonitor;
+ }
+
+ private static class InnerConnection extends IServiceConnection.Stub {
+ final WeakReference<ServiceDispatcher> mDispatcher;
+
+ InnerConnection(ServiceDispatcher sd) {
+ mDispatcher = new WeakReference<ServiceDispatcher>(sd);
+ }
+
+ public void connected(ComponentName name, IBinder service) throws RemoteException {
+ ServiceDispatcher sd = mDispatcher.get();
+ if (sd != null) {
+ sd.connected(name, service);
+ }
+ }
+ }
+
+ private final HashMap<ComponentName, ConnectionInfo> mActiveConnections
+ = new HashMap<ComponentName, ConnectionInfo>();
+
+ ServiceDispatcher(ServiceConnection conn,
+ Context context, Handler activityThread, int flags) {
+ mIServiceConnection = new InnerConnection(this);
+ mConnection = conn;
+ mContext = context;
+ mActivityThread = activityThread;
+ mLocation = new ServiceConnectionLeaked(null);
+ mLocation.fillInStackTrace();
+ mFlags = flags;
+ }
+
+ void validate(Context context, Handler activityThread) {
+ if (mContext != context) {
+ throw new RuntimeException(
+ "ServiceConnection " + mConnection +
+ " registered with differing Context (was " +
+ mContext + " now " + context + ")");
+ }
+ if (mActivityThread != activityThread) {
+ throw new RuntimeException(
+ "ServiceConnection " + mConnection +
+ " registered with differing handler (was " +
+ mActivityThread + " now " + activityThread + ")");
+ }
+ }
+
+ void doForget() {
+ synchronized(this) {
+ Iterator<ConnectionInfo> it = mActiveConnections.values().iterator();
+ while (it.hasNext()) {
+ ConnectionInfo ci = it.next();
+ ci.binder.unlinkToDeath(ci.deathMonitor, 0);
+ }
+ mActiveConnections.clear();
+ }
+ }
+
+ ServiceConnectionLeaked getLocation() {
+ return mLocation;
+ }
+
+ ServiceConnection getServiceConnection() {
+ return mConnection;
+ }
+
+ IServiceConnection getIServiceConnection() {
+ return mIServiceConnection;
+ }
+
+ int getFlags() {
+ return mFlags;
+ }
+
+ void setUnbindLocation(RuntimeException ex) {
+ mUnbindLocation = ex;
+ }
+
+ RuntimeException getUnbindLocation() {
+ return mUnbindLocation;
+ }
+
+ public void connected(ComponentName name, IBinder service) {
+ if (mActivityThread != null) {
+ mActivityThread.post(new RunConnection(name, service, 0));
+ } else {
+ doConnected(name, service);
+ }
+ }
+
+ public void death(ComponentName name, IBinder service) {
+ ConnectionInfo old;
+
+ synchronized (this) {
+ mDied = true;
+ old = mActiveConnections.remove(name);
+ if (old == null || old.binder != service) {
+ // Death for someone different than who we last
+ // reported... just ignore it.
+ return;
+ }
+ old.binder.unlinkToDeath(old.deathMonitor, 0);
+ }
+
+ if (mActivityThread != null) {
+ mActivityThread.post(new RunConnection(name, service, 1));
+ } else {
+ doDeath(name, service);
+ }
+ }
+
+ public void doConnected(ComponentName name, IBinder service) {
+ ConnectionInfo old;
+ ConnectionInfo info;
+
+ synchronized (this) {
+ old = mActiveConnections.get(name);
+ if (old != null && old.binder == service) {
+ // Huh, already have this one. Oh well!
+ return;
+ }
+
+ if (service != null) {
+ // A new service is being connected... set it all up.
+ mDied = false;
+ info = new ConnectionInfo();
+ info.binder = service;
+ info.deathMonitor = new DeathMonitor(name, service);
+ try {
+ service.linkToDeath(info.deathMonitor, 0);
+ mActiveConnections.put(name, info);
+ } catch (RemoteException e) {
+ // This service was dead before we got it... just
+ // don't do anything with it.
+ mActiveConnections.remove(name);
+ return;
+ }
+
+ } else {
+ // The named service is being disconnected... clean up.
+ mActiveConnections.remove(name);
+ }
+
+ if (old != null) {
+ old.binder.unlinkToDeath(old.deathMonitor, 0);
+ }
+ }
+
+ // If there was an old service, it is not disconnected.
+ if (old != null) {
+ mConnection.onServiceDisconnected(name);
+ }
+ // If there is a new service, it is now connected.
+ if (service != null) {
+ mConnection.onServiceConnected(name, service);
+ }
+ }
+
+ public void doDeath(ComponentName name, IBinder service) {
+ mConnection.onServiceDisconnected(name);
+ }
+
+ private final class RunConnection implements Runnable {
+ RunConnection(ComponentName name, IBinder service, int command) {
+ mName = name;
+ mService = service;
+ mCommand = command;
+ }
+
+ public void run() {
+ if (mCommand == 0) {
+ doConnected(mName, mService);
+ } else if (mCommand == 1) {
+ doDeath(mName, mService);
+ }
+ }
+
+ final ComponentName mName;
+ final IBinder mService;
+ final int mCommand;
+ }
+
+ private final class DeathMonitor implements IBinder.DeathRecipient
+ {
+ DeathMonitor(ComponentName name, IBinder service) {
+ mName = name;
+ mService = service;
+ }
+
+ public void binderDied() {
+ death(mName, mService);
+ }
+
+ final ComponentName mName;
+ final IBinder mService;
+ }
+ }
+ }
+
+ private static ApplicationContext mSystemContext = null;
+
+ private static final class ActivityRecord {
+ IBinder token;
+ Intent intent;
+ Bundle state;
+ Activity activity;
+ Window window;
+ Activity parent;
+ String embeddedID;
+ Object lastNonConfigurationInstance;
+ boolean paused;
+ boolean stopped;
+ boolean hideForNow;
+ Configuration newConfig;
+ ActivityRecord nextIdle;
+
+ ActivityInfo activityInfo;
+ PackageInfo packageInfo;
+
+ List<ResultInfo> pendingResults;
+ List<Intent> pendingIntents;
+
+ boolean startsNotResumed;
+
+ ActivityRecord() {
+ parent = null;
+ embeddedID = null;
+ paused = false;
+ stopped = false;
+ hideForNow = false;
+ nextIdle = null;
+ }
+
+ public String toString() {
+ ComponentName componentName = intent.getComponent();
+ return "ActivityRecord{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " token=" + token + " " + (componentName == null
+ ? "no component name" : componentName.toShortString())
+ + "}";
+ }
+ }
+
+ private final class ProviderRecord implements IBinder.DeathRecipient {
+ final String mName;
+ final IContentProvider mProvider;
+ final ContentProvider mLocalProvider;
+
+ ProviderRecord(String name, IContentProvider provider,
+ ContentProvider localProvider) {
+ mName = name;
+ mProvider = provider;
+ mLocalProvider = localProvider;
+ }
+
+ public void binderDied() {
+ removeDeadProvider(mName, mProvider);
+ }
+ }
+
+ private static final class NewIntentData {
+ List<Intent> intents;
+ IBinder token;
+ public String toString() {
+ return "NewIntentData{intents=" + intents + " token=" + token + "}";
+ }
+ }
+
+ private static final class ReceiverData {
+ Intent intent;
+ ActivityInfo info;
+ int resultCode;
+ String resultData;
+ Bundle resultExtras;
+ boolean sync;
+ boolean resultAbort;
+ public String toString() {
+ return "ReceiverData{intent=" + intent + " packageName=" +
+ info.packageName + " resultCode=" + resultCode
+ + " resultData=" + resultData + " resultExtras=" + resultExtras + "}";
+ }
+ }
+
+ private static final class CreateServiceData {
+ IBinder token;
+ ServiceInfo info;
+ Intent intent;
+ public String toString() {
+ return "CreateServiceData{token=" + token + " className="
+ + info.name + " packageName=" + info.packageName
+ + " intent=" + intent + "}";
+ }
+ }
+
+ private static final class BindServiceData {
+ IBinder token;
+ Intent intent;
+ boolean rebind;
+ public String toString() {
+ return "BindServiceData{token=" + token + " intent=" + intent + "}";
+ }
+ }
+
+ private static final class ServiceArgsData {
+ IBinder token;
+ int startId;
+ Intent args;
+ public String toString() {
+ return "ServiceArgsData{token=" + token + " startId=" + startId
+ + " args=" + args + "}";
+ }
+ }
+
+ private static final class AppBindData {
+ PackageInfo info;
+ String processName;
+ ApplicationInfo appInfo;
+ List<ProviderInfo> providers;
+ ComponentName instrumentationName;
+ String profileFile;
+ Bundle instrumentationArgs;
+ IInstrumentationWatcher instrumentationWatcher;
+ int debugMode;
+ Configuration config;
+ boolean handlingProfiling;
+ public String toString() {
+ return "AppBindData{appInfo=" + appInfo + "}";
+ }
+ }
+
+ private static final class DumpServiceInfo {
+ FileDescriptor fd;
+ IBinder service;
+ String[] args;
+ boolean dumped;
+ }
+
+ private static final class ResultData {
+ IBinder token;
+ List<ResultInfo> results;
+ public String toString() {
+ return "ResultData{token=" + token + " results" + results + "}";
+ }
+ }
+
+ private static final class ContextCleanupInfo {
+ ApplicationContext context;
+ String what;
+ String who;
+ }
+
+ private final class ApplicationThread extends ApplicationThreadNative {
+ private static final String HEAP_COLUMN = "%17s %8s %8s %8s %8s";
+ private static final String ONE_COUNT_COLUMN = "%17s %8d";
+ private static final String TWO_COUNT_COLUMNS = "%17s %8d %17s %8d";
+
+ public final void schedulePauseActivity(IBinder token, boolean finished,
+ int configChanges) {
+ queueOrSendMessage(
+ finished ? H.PAUSE_ACTIVITY_FINISHING : H.PAUSE_ACTIVITY,
+ token, configChanges);
+ }
+
+ public final void scheduleStopActivity(IBinder token, boolean showWindow,
+ int configChanges) {
+ queueOrSendMessage(
+ showWindow ? H.STOP_ACTIVITY_SHOW : H.STOP_ACTIVITY_HIDE,
+ token, 0, configChanges);
+ }
+
+ public final void scheduleWindowVisibility(IBinder token, boolean showWindow) {
+ queueOrSendMessage(
+ showWindow ? H.SHOW_WINDOW : H.HIDE_WINDOW,
+ token);
+ }
+
+ public final void scheduleResumeActivity(IBinder token) {
+ queueOrSendMessage(H.RESUME_ACTIVITY, token);
+ }
+
+ public final void scheduleSendResult(IBinder token, List<ResultInfo> results) {
+ ResultData res = new ResultData();
+ res.token = token;
+ res.results = results;
+ queueOrSendMessage(H.SEND_RESULT, res);
+ }
+
+ // we use token to identify this activity without having to send the
+ // activity itself back to the activity manager. (matters more with ipc)
+ public final void scheduleLaunchActivity(Intent intent, IBinder token,
+ ActivityInfo info, Bundle state, List<ResultInfo> pendingResults,
+ List<Intent> pendingNewIntents, boolean notResumed) {
+ ActivityRecord r = new ActivityRecord();
+
+ r.token = token;
+ r.intent = intent;
+ r.activityInfo = info;
+ r.state = state;
+
+ r.pendingResults = pendingResults;
+ r.pendingIntents = pendingNewIntents;
+
+ r.startsNotResumed = notResumed;
+
+ queueOrSendMessage(H.LAUNCH_ACTIVITY, r);
+ }
+
+ public final void scheduleRelaunchActivity(IBinder token,
+ List<ResultInfo> pendingResults, List<Intent> pendingNewIntents,
+ int configChanges, boolean notResumed) {
+ ActivityRecord r = new ActivityRecord();
+
+ r.token = token;
+ r.pendingResults = pendingResults;
+ r.pendingIntents = pendingNewIntents;
+ r.startsNotResumed = notResumed;
+
+ synchronized (mRelaunchingActivities) {
+ mRelaunchingActivities.add(r);
+ }
+
+ queueOrSendMessage(H.RELAUNCH_ACTIVITY, r, configChanges);
+ }
+
+ public final void scheduleNewIntent(List<Intent> intents, IBinder token) {
+ NewIntentData data = new NewIntentData();
+ data.intents = intents;
+ data.token = token;
+
+ queueOrSendMessage(H.NEW_INTENT, data);
+ }
+
+ public final void scheduleDestroyActivity(IBinder token, boolean finishing,
+ int configChanges) {
+ queueOrSendMessage(H.DESTROY_ACTIVITY, token, finishing ? 1 : 0,
+ configChanges);
+ }
+
+ public final void scheduleReceiver(Intent intent, ActivityInfo info,
+ int resultCode, String data, Bundle extras, boolean sync) {
+ ReceiverData r = new ReceiverData();
+
+ r.intent = intent;
+ r.info = info;
+ r.resultCode = resultCode;
+ r.resultData = data;
+ r.resultExtras = extras;
+ r.sync = sync;
+
+ queueOrSendMessage(H.RECEIVER, r);
+ }
+
+ public final void scheduleCreateService(IBinder token,
+ ServiceInfo info) {
+ CreateServiceData s = new CreateServiceData();
+ s.token = token;
+ s.info = info;
+
+ queueOrSendMessage(H.CREATE_SERVICE, s);
+ }
+
+ public final void scheduleBindService(IBinder token, Intent intent,
+ boolean rebind) {
+ BindServiceData s = new BindServiceData();
+ s.token = token;
+ s.intent = intent;
+ s.rebind = rebind;
+
+ queueOrSendMessage(H.BIND_SERVICE, s);
+ }
+
+ public final void scheduleUnbindService(IBinder token, Intent intent) {
+ BindServiceData s = new BindServiceData();
+ s.token = token;
+ s.intent = intent;
+
+ queueOrSendMessage(H.UNBIND_SERVICE, s);
+ }
+
+ public final void scheduleServiceArgs(IBinder token, int startId,
+ Intent args) {
+ ServiceArgsData s = new ServiceArgsData();
+ s.token = token;
+ s.startId = startId;
+ s.args = args;
+
+ queueOrSendMessage(H.SERVICE_ARGS, s);
+ }
+
+ public final void scheduleStopService(IBinder token) {
+ queueOrSendMessage(H.STOP_SERVICE, token);
+ }
+
+ public final void bindApplication(String processName,
+ ApplicationInfo appInfo, List<ProviderInfo> providers,
+ ComponentName instrumentationName, String profileFile,
+ Bundle instrumentationArgs, IInstrumentationWatcher instrumentationWatcher,
+ int debugMode, Configuration config,
+ Map<String, IBinder> services) {
+ Process.setArgV0(processName);
+
+ if (services != null) {
+ // Setup the service cache in the ServiceManager
+ ServiceManager.initServiceCache(services);
+ }
+
+ AppBindData data = new AppBindData();
+ data.processName = processName;
+ data.appInfo = appInfo;
+ data.providers = providers;
+ data.instrumentationName = instrumentationName;
+ data.profileFile = profileFile;
+ data.instrumentationArgs = instrumentationArgs;
+ data.instrumentationWatcher = instrumentationWatcher;
+ data.debugMode = debugMode;
+ data.config = config;
+ queueOrSendMessage(H.BIND_APPLICATION, data);
+ }
+
+ public final void scheduleExit() {
+ queueOrSendMessage(H.EXIT_APPLICATION, null);
+ }
+
+ public void requestThumbnail(IBinder token) {
+ queueOrSendMessage(H.REQUEST_THUMBNAIL, token);
+ }
+
+ public void scheduleConfigurationChanged(Configuration config) {
+ synchronized (mRelaunchingActivities) {
+ mPendingConfiguration = config;
+ }
+ queueOrSendMessage(H.CONFIGURATION_CHANGED, config);
+ }
+
+ public void updateTimeZone() {
+ TimeZone.setDefault(null);
+ }
+
+ public void processInBackground() {
+ mH.removeMessages(H.GC_WHEN_IDLE);
+ mH.sendMessage(mH.obtainMessage(H.GC_WHEN_IDLE));
+ }
+
+ public void dumpService(FileDescriptor fd, IBinder servicetoken, String[] args) {
+ DumpServiceInfo data = new DumpServiceInfo();
+ data.fd = fd;
+ data.service = servicetoken;
+ data.args = args;
+ data.dumped = false;
+ queueOrSendMessage(H.DUMP_SERVICE, data);
+ synchronized (data) {
+ while (!data.dumped) {
+ try {
+ data.wait();
+ } catch (InterruptedException e) {
+ // no need to do anything here, we will keep waiting until
+ // dumped is set
+ }
+ }
+ }
+ }
+
+ // This function exists to make sure all receiver dispatching is
+ // correctly ordered, since these are one-way calls and the binder driver
+ // applies transaction ordering per object for such calls.
+ public void scheduleRegisteredReceiver(IIntentReceiver receiver, Intent intent,
+ int resultCode, String dataStr, Bundle extras, boolean ordered)
+ throws RemoteException {
+ receiver.performReceive(intent, resultCode, dataStr, extras, ordered);
+ }
+
+ public void scheduleLowMemory() {
+ queueOrSendMessage(H.LOW_MEMORY, null);
+ }
+
+ public void scheduleActivityConfigurationChanged(IBinder token) {
+ queueOrSendMessage(H.ACTIVITY_CONFIGURATION_CHANGED, token);
+ }
+
+ public void requestPss() {
+ try {
+ ActivityManagerNative.getDefault().reportPss(this,
+ (int)Process.getPss(Process.myPid()));
+ } catch (RemoteException e) {
+ }
+ }
+
+ @Override
+ protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ long nativeMax = Debug.getNativeHeapSize() / 1024;
+ long nativeAllocated = Debug.getNativeHeapAllocatedSize() / 1024;
+ long nativeFree = Debug.getNativeHeapFreeSize() / 1024;
+
+ Debug.MemoryInfo memInfo = new Debug.MemoryInfo();
+ Debug.getMemoryInfo(memInfo);
+
+ final int nativeShared = memInfo.nativeSharedDirty;
+ final int dalvikShared = memInfo.dalvikSharedDirty;
+ final int otherShared = memInfo.otherSharedDirty;
+
+ final int nativePrivate = memInfo.nativePrivateDirty;
+ final int dalvikPrivate = memInfo.dalvikPrivateDirty;
+ final int otherPrivate = memInfo.otherPrivateDirty;
+
+ Runtime runtime = Runtime.getRuntime();
+
+ long dalvikMax = runtime.totalMemory() / 1024;
+ long dalvikFree = runtime.freeMemory() / 1024;
+ long dalvikAllocated = dalvikMax - dalvikFree;
+
+ printRow(pw, HEAP_COLUMN, "", "native", "dalvik", "other", "total");
+ printRow(pw, HEAP_COLUMN, "size:", nativeMax, dalvikMax, "N/A", nativeMax + dalvikMax);
+ printRow(pw, HEAP_COLUMN, "allocated:", nativeAllocated, dalvikAllocated, "N/A",
+ nativeAllocated + dalvikAllocated);
+ printRow(pw, HEAP_COLUMN, "free:", nativeFree, dalvikFree, "N/A",
+ nativeFree + dalvikFree);
+
+ printRow(pw, HEAP_COLUMN, "(Pss):", memInfo.nativePss, memInfo.dalvikPss,
+ memInfo.otherPss, memInfo.nativePss + memInfo.dalvikPss + memInfo.otherPss);
+
+ printRow(pw, HEAP_COLUMN, "(shared dirty):", nativeShared, dalvikShared, otherShared,
+ nativeShared + dalvikShared + otherShared);
+ printRow(pw, HEAP_COLUMN, "(priv dirty):", nativePrivate, dalvikPrivate, otherPrivate,
+ nativePrivate + dalvikPrivate + otherPrivate);
+
+ pw.println(" ");
+ pw.println(" Objects");
+ printRow(pw, TWO_COUNT_COLUMNS, "Views:", ViewDebug.getViewInstanceCount(), "ViewRoots:",
+ ViewDebug.getViewRootInstanceCount());
+
+ printRow(pw, TWO_COUNT_COLUMNS, "AppContexts:", ApplicationContext.getInstanceCount(),
+ "Activities:", Activity.getInstanceCount());
+
+ printRow(pw, TWO_COUNT_COLUMNS, "Assets:", AssetManager.getGlobalAssetCount(),
+ "AssetManagers:", AssetManager.getGlobalAssetManagerCount());
+
+ printRow(pw, TWO_COUNT_COLUMNS, "Local Binders:", Debug.getBinderLocalObjectCount(),
+ "Proxy Binders:", Debug.getBinderProxyObjectCount());
+ printRow(pw, ONE_COUNT_COLUMN, "Death Recipients:", Debug.getBinderDeathObjectCount());
+
+ printRow(pw, ONE_COUNT_COLUMN, "OpenSSL Sockets:", OpenSSLSocketImpl.getInstanceCount());
+
+ // SQLite mem info
+ long sqliteAllocated = SQLiteDebug.getHeapAllocatedSize() / 1024;
+ SQLiteDebug.PagerStats stats = new SQLiteDebug.PagerStats();
+ SQLiteDebug.getPagerStats(stats);
+
+ pw.println(" ");
+ pw.println(" SQL");
+ printRow(pw, TWO_COUNT_COLUMNS, "heap:", sqliteAllocated, "dbFiles:",
+ stats.databaseBytes / 1024);
+ printRow(pw, TWO_COUNT_COLUMNS, "numPagers:", stats.numPagers, "inactivePageKB:",
+ (stats.totalBytes - stats.referencedBytes) / 1024);
+ printRow(pw, ONE_COUNT_COLUMN, "activePageKB:", stats.referencedBytes / 1024);
+ }
+
+ private void printRow(PrintWriter pw, String format, Object...objs) {
+ pw.println(String.format(format, objs));
+ }
+ }
+
+ private final class H extends Handler {
+ public static final int LAUNCH_ACTIVITY = 100;
+ public static final int PAUSE_ACTIVITY = 101;
+ public static final int PAUSE_ACTIVITY_FINISHING= 102;
+ public static final int STOP_ACTIVITY_SHOW = 103;
+ public static final int STOP_ACTIVITY_HIDE = 104;
+ public static final int SHOW_WINDOW = 105;
+ public static final int HIDE_WINDOW = 106;
+ public static final int RESUME_ACTIVITY = 107;
+ public static final int SEND_RESULT = 108;
+ public static final int DESTROY_ACTIVITY = 109;
+ public static final int BIND_APPLICATION = 110;
+ public static final int EXIT_APPLICATION = 111;
+ public static final int NEW_INTENT = 112;
+ public static final int RECEIVER = 113;
+ public static final int CREATE_SERVICE = 114;
+ public static final int SERVICE_ARGS = 115;
+ public static final int STOP_SERVICE = 116;
+ public static final int REQUEST_THUMBNAIL = 117;
+ public static final int CONFIGURATION_CHANGED = 118;
+ public static final int CLEAN_UP_CONTEXT = 119;
+ public static final int GC_WHEN_IDLE = 120;
+ public static final int BIND_SERVICE = 121;
+ public static final int UNBIND_SERVICE = 122;
+ public static final int DUMP_SERVICE = 123;
+ public static final int LOW_MEMORY = 124;
+ public static final int ACTIVITY_CONFIGURATION_CHANGED = 125;
+ public static final int RELAUNCH_ACTIVITY = 126;
+ String codeToString(int code) {
+ if (localLOGV) {
+ switch (code) {
+ case LAUNCH_ACTIVITY: return "LAUNCH_ACTIVITY";
+ case PAUSE_ACTIVITY: return "PAUSE_ACTIVITY";
+ case PAUSE_ACTIVITY_FINISHING: return "PAUSE_ACTIVITY_FINISHING";
+ case STOP_ACTIVITY_SHOW: return "STOP_ACTIVITY_SHOW";
+ case STOP_ACTIVITY_HIDE: return "STOP_ACTIVITY_HIDE";
+ case SHOW_WINDOW: return "SHOW_WINDOW";
+ case HIDE_WINDOW: return "HIDE_WINDOW";
+ case RESUME_ACTIVITY: return "RESUME_ACTIVITY";
+ case SEND_RESULT: return "SEND_RESULT";
+ case DESTROY_ACTIVITY: return "DESTROY_ACTIVITY";
+ case BIND_APPLICATION: return "BIND_APPLICATION";
+ case EXIT_APPLICATION: return "EXIT_APPLICATION";
+ case NEW_INTENT: return "NEW_INTENT";
+ case RECEIVER: return "RECEIVER";
+ case CREATE_SERVICE: return "CREATE_SERVICE";
+ case SERVICE_ARGS: return "SERVICE_ARGS";
+ case STOP_SERVICE: return "STOP_SERVICE";
+ case REQUEST_THUMBNAIL: return "REQUEST_THUMBNAIL";
+ case CONFIGURATION_CHANGED: return "CONFIGURATION_CHANGED";
+ case CLEAN_UP_CONTEXT: return "CLEAN_UP_CONTEXT";
+ case GC_WHEN_IDLE: return "GC_WHEN_IDLE";
+ case BIND_SERVICE: return "BIND_SERVICE";
+ case UNBIND_SERVICE: return "UNBIND_SERVICE";
+ case DUMP_SERVICE: return "DUMP_SERVICE";
+ case LOW_MEMORY: return "LOW_MEMORY";
+ case ACTIVITY_CONFIGURATION_CHANGED: return "ACTIVITY_CONFIGURATION_CHANGED";
+ case RELAUNCH_ACTIVITY: return "RELAUNCH_ACTIVITY";
+ }
+ }
+ return "(unknown)";
+ }
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case LAUNCH_ACTIVITY: {
+ ActivityRecord r = (ActivityRecord)msg.obj;
+
+ r.packageInfo = getPackageInfoNoCheck(
+ r.activityInfo.applicationInfo);
+ handleLaunchActivity(r);
+ } break;
+ case RELAUNCH_ACTIVITY: {
+ ActivityRecord r = (ActivityRecord)msg.obj;
+ handleRelaunchActivity(r, msg.arg1);
+ } break;
+ case PAUSE_ACTIVITY:
+ handlePauseActivity((IBinder)msg.obj, false, msg.arg2);
+ break;
+ case PAUSE_ACTIVITY_FINISHING:
+ handlePauseActivity((IBinder)msg.obj, true, msg.arg2);
+ break;
+ case STOP_ACTIVITY_SHOW:
+ handleStopActivity((IBinder)msg.obj, true, msg.arg2);
+ break;
+ case STOP_ACTIVITY_HIDE:
+ handleStopActivity((IBinder)msg.obj, false, msg.arg2);
+ break;
+ case SHOW_WINDOW:
+ handleWindowVisibility((IBinder)msg.obj, true);
+ break;
+ case HIDE_WINDOW:
+ handleWindowVisibility((IBinder)msg.obj, false);
+ break;
+ case RESUME_ACTIVITY:
+ handleResumeActivity((IBinder)msg.obj, true);
+ break;
+ case SEND_RESULT:
+ handleSendResult((ResultData)msg.obj);
+ break;
+ case DESTROY_ACTIVITY:
+ handleDestroyActivity((IBinder)msg.obj, msg.arg1 != 0,
+ msg.arg2, false);
+ break;
+ case BIND_APPLICATION:
+ AppBindData data = (AppBindData)msg.obj;
+ handleBindApplication(data);
+ break;
+ case EXIT_APPLICATION:
+ if (mInitialApplication != null) {
+ mInitialApplication.onTerminate();
+ }
+ Looper.myLooper().quit();
+ break;
+ case NEW_INTENT:
+ handleNewIntent((NewIntentData)msg.obj);
+ break;
+ case RECEIVER:
+ handleReceiver((ReceiverData)msg.obj);
+ break;
+ case CREATE_SERVICE:
+ handleCreateService((CreateServiceData)msg.obj);
+ break;
+ case BIND_SERVICE:
+ handleBindService((BindServiceData)msg.obj);
+ break;
+ case UNBIND_SERVICE:
+ handleUnbindService((BindServiceData)msg.obj);
+ break;
+ case SERVICE_ARGS:
+ handleServiceArgs((ServiceArgsData)msg.obj);
+ break;
+ case STOP_SERVICE:
+ handleStopService((IBinder)msg.obj);
+ break;
+ case REQUEST_THUMBNAIL:
+ handleRequestThumbnail((IBinder)msg.obj);
+ break;
+ case CONFIGURATION_CHANGED:
+ handleConfigurationChanged((Configuration)msg.obj);
+ break;
+ case CLEAN_UP_CONTEXT:
+ ContextCleanupInfo cci = (ContextCleanupInfo)msg.obj;
+ cci.context.performFinalCleanup(cci.who, cci.what);
+ break;
+ case GC_WHEN_IDLE:
+ scheduleGcIdler();
+ break;
+ case DUMP_SERVICE:
+ handleDumpService((DumpServiceInfo)msg.obj);
+ break;
+ case LOW_MEMORY:
+ handleLowMemory();
+ break;
+ case ACTIVITY_CONFIGURATION_CHANGED:
+ handleActivityConfigurationChanged((IBinder)msg.obj);
+ break;
+ }
+ }
+ }
+
+ private final class Idler implements MessageQueue.IdleHandler {
+ public final boolean queueIdle() {
+ ActivityRecord a = mNewActivities;
+ if (a != null) {
+ mNewActivities = null;
+ IActivityManager am = ActivityManagerNative.getDefault();
+ ActivityRecord prev;
+ do {
+ if (localLOGV) Log.v(
+ TAG, "Reporting idle of " + a +
+ " finished=" +
+ (a.activity != null ? a.activity.mFinished : false));
+ if (a.activity != null && !a.activity.mFinished) {
+ try {
+ am.activityIdle(a.token);
+ } catch (RemoteException ex) {
+ }
+ }
+ prev = a;
+ a = a.nextIdle;
+ prev.nextIdle = null;
+ } while (a != null);
+ }
+ return false;
+ }
+ }
+
+ final class GcIdler implements MessageQueue.IdleHandler {
+ public final boolean queueIdle() {
+ doGcIfNeeded();
+ return false;
+ }
+ }
+
+ static IPackageManager sPackageManager;
+
+ final ApplicationThread mAppThread = new ApplicationThread();
+ final Looper mLooper = Looper.myLooper();
+ final H mH = new H();
+ final HashMap<IBinder, ActivityRecord> mActivities
+ = new HashMap<IBinder, ActivityRecord>();
+ // List of new activities (via ActivityRecord.nextIdle) that should
+ // be reported when next we idle.
+ ActivityRecord mNewActivities = null;
+ // Number of activities that are currently visible on-screen.
+ int mNumVisibleActivities = 0;
+ final HashMap<IBinder, Service> mServices
+ = new HashMap<IBinder, Service>();
+ AppBindData mBoundApplication;
+ Configuration mConfiguration;
+ Application mInitialApplication;
+ final ArrayList<Application> mAllApplications
+ = new ArrayList<Application>();
+ static final ThreadLocal sThreadLocal = new ThreadLocal();
+ Instrumentation mInstrumentation;
+ String mInstrumentationAppDir = null;
+ String mInstrumentationAppPackage = null;
+ String mInstrumentedAppDir = null;
+ boolean mSystemThread = false;
+
+ /**
+ * Activities that are enqueued to be relaunched. This list is accessed
+ * by multiple threads, so you must synchronize on it when accessing it.
+ */
+ final ArrayList<ActivityRecord> mRelaunchingActivities
+ = new ArrayList<ActivityRecord>();
+ Configuration mPendingConfiguration = null;
+
+ // These can be accessed by multiple threads; mPackages is the lock.
+ // XXX For now we keep around information about all packages we have
+ // seen, not removing entries from this map.
+ final HashMap<String, WeakReference<PackageInfo>> mPackages
+ = new HashMap<String, WeakReference<PackageInfo>>();
+ final HashMap<String, WeakReference<PackageInfo>> mResourcePackages
+ = new HashMap<String, WeakReference<PackageInfo>>();
+ Display mDisplay = null;
+ HashMap<String, WeakReference<Resources> > mActiveResources
+ = new HashMap<String, WeakReference<Resources> >();
+
+ // The lock of mProviderMap protects the following variables.
+ final HashMap<String, ProviderRecord> mProviderMap
+ = new HashMap<String, ProviderRecord>();
+ final HashMap<IBinder, ProviderRefCount> mProviderRefCountMap
+ = new HashMap<IBinder, ProviderRefCount>();
+ final HashMap<IBinder, ProviderRecord> mLocalProviders
+ = new HashMap<IBinder, ProviderRecord>();
+
+ final GcIdler mGcIdler = new GcIdler();
+ boolean mGcIdlerScheduled = false;
+
+ public final PackageInfo getPackageInfo(String packageName, int flags) {
+ synchronized (mPackages) {
+ WeakReference<PackageInfo> ref;
+ if ((flags&Context.CONTEXT_INCLUDE_CODE) != 0) {
+ ref = mPackages.get(packageName);
+ } else {
+ ref = mResourcePackages.get(packageName);
+ }
+ PackageInfo packageInfo = ref != null ? ref.get() : null;
+ //Log.i(TAG, "getPackageInfo " + packageName + ": " + packageInfo);
+ if (packageInfo != null && (packageInfo.mResources == null
+ || packageInfo.mResources.getAssets().isUpToDate())) {
+ if (packageInfo.isSecurityViolation()
+ && (flags&Context.CONTEXT_IGNORE_SECURITY) == 0) {
+ throw new SecurityException(
+ "Requesting code from " + packageName
+ + " to be run in process "
+ + mBoundApplication.processName
+ + "/" + mBoundApplication.appInfo.uid);
+ }
+ return packageInfo;
+ }
+ }
+
+ ApplicationInfo ai = null;
+ try {
+ ai = getPackageManager().getApplicationInfo(packageName,
+ PackageManager.GET_SHARED_LIBRARY_FILES);
+ } catch (RemoteException e) {
+ }
+
+ if (ai != null) {
+ return getPackageInfo(ai, flags);
+ }
+
+ return null;
+ }
+
+ public final PackageInfo getPackageInfo(ApplicationInfo ai, int flags) {
+ boolean includeCode = (flags&Context.CONTEXT_INCLUDE_CODE) != 0;
+ boolean securityViolation = includeCode && ai.uid != 0
+ && ai.uid != Process.SYSTEM_UID && (mBoundApplication != null
+ ? ai.uid != mBoundApplication.appInfo.uid : true);
+ if ((flags&(Context.CONTEXT_INCLUDE_CODE
+ |Context.CONTEXT_IGNORE_SECURITY))
+ == Context.CONTEXT_INCLUDE_CODE) {
+ if (securityViolation) {
+ String msg = "Requesting code from " + ai.packageName
+ + " (with uid " + ai.uid + ")";
+ if (mBoundApplication != null) {
+ msg = msg + " to be run in process "
+ + mBoundApplication.processName + " (with uid "
+ + mBoundApplication.appInfo.uid + ")";
+ }
+ throw new SecurityException(msg);
+ }
+ }
+ return getPackageInfo(ai, null, securityViolation, includeCode);
+ }
+
+ public final PackageInfo getPackageInfoNoCheck(ApplicationInfo ai) {
+ return getPackageInfo(ai, null, false, true);
+ }
+
+ private final PackageInfo getPackageInfo(ApplicationInfo aInfo,
+ ClassLoader baseLoader, boolean securityViolation, boolean includeCode) {
+ synchronized (mPackages) {
+ WeakReference<PackageInfo> ref;
+ if (includeCode) {
+ ref = mPackages.get(aInfo.packageName);
+ } else {
+ ref = mResourcePackages.get(aInfo.packageName);
+ }
+ PackageInfo packageInfo = ref != null ? ref.get() : null;
+ if (packageInfo == null || (packageInfo.mResources != null
+ && !packageInfo.mResources.getAssets().isUpToDate())) {
+ if (localLOGV) Log.v(TAG, (includeCode ? "Loading code package "
+ : "Loading resource-only package ") + aInfo.packageName
+ + " (in " + (mBoundApplication != null
+ ? mBoundApplication.processName : null)
+ + ")");
+ packageInfo =
+ new PackageInfo(this, aInfo, this, baseLoader,
+ securityViolation, includeCode &&
+ (aInfo.flags&ApplicationInfo.FLAG_HAS_CODE) != 0);
+ if (includeCode) {
+ mPackages.put(aInfo.packageName,
+ new WeakReference<PackageInfo>(packageInfo));
+ } else {
+ mResourcePackages.put(aInfo.packageName,
+ new WeakReference<PackageInfo>(packageInfo));
+ }
+ }
+ return packageInfo;
+ }
+ }
+
+ public final boolean hasPackageInfo(String packageName) {
+ synchronized (mPackages) {
+ WeakReference<PackageInfo> ref;
+ ref = mPackages.get(packageName);
+ if (ref != null && ref.get() != null) {
+ return true;
+ }
+ ref = mResourcePackages.get(packageName);
+ if (ref != null && ref.get() != null) {
+ return true;
+ }
+ return false;
+ }
+ }
+
+ ActivityThread() {
+ }
+
+ public ApplicationThread getApplicationThread()
+ {
+ return mAppThread;
+ }
+
+ public Instrumentation getInstrumentation()
+ {
+ return mInstrumentation;
+ }
+
+ public Configuration getConfiguration() {
+ return mConfiguration;
+ }
+
+ public boolean isProfiling() {
+ return mBoundApplication != null && mBoundApplication.profileFile != null;
+ }
+
+ public String getProfileFilePath() {
+ return mBoundApplication.profileFile;
+ }
+
+ public Looper getLooper() {
+ return mLooper;
+ }
+
+ public Application getApplication() {
+ return mInitialApplication;
+ }
+
+ public ApplicationContext getSystemContext() {
+ synchronized (this) {
+ if (mSystemContext == null) {
+ ApplicationContext context =
+ ApplicationContext.createSystemContext(this);
+ PackageInfo info = new PackageInfo(this, "android", context);
+ context.init(info, null, this);
+ context.getResources().updateConfiguration(
+ getConfiguration(), getDisplayMetricsLocked());
+ mSystemContext = context;
+ //Log.i(TAG, "Created system resources " + context.getResources()
+ // + ": " + context.getResources().getConfiguration());
+ }
+ }
+ return mSystemContext;
+ }
+
+ void scheduleGcIdler() {
+ if (!mGcIdlerScheduled) {
+ mGcIdlerScheduled = true;
+ Looper.myQueue().addIdleHandler(mGcIdler);
+ }
+ mH.removeMessages(H.GC_WHEN_IDLE);
+ }
+
+ void unscheduleGcIdler() {
+ if (mGcIdlerScheduled) {
+ mGcIdlerScheduled = false;
+ Looper.myQueue().removeIdleHandler(mGcIdler);
+ }
+ mH.removeMessages(H.GC_WHEN_IDLE);
+ }
+
+ void doGcIfNeeded() {
+ mGcIdlerScheduled = false;
+ final long now = SystemClock.uptimeMillis();
+ //Log.i(TAG, "**** WE MIGHT WANT TO GC: then=" + Binder.getLastGcTime()
+ // + "m now=" + now);
+ if ((BinderInternal.getLastGcTime()+MIN_TIME_BETWEEN_GCS) < now) {
+ //Log.i(TAG, "**** WE DO, WE DO WANT TO GC!");
+ BinderInternal.forceGc("bg");
+ }
+ }
+
+ public final ActivityInfo resolveActivityInfo(Intent intent) {
+ ActivityInfo aInfo = intent.resolveActivityInfo(
+ mInitialApplication.getPackageManager(), PackageManager.GET_SHARED_LIBRARY_FILES);
+ if (aInfo == null) {
+ // Throw an exception.
+ Instrumentation.checkStartActivityResult(
+ IActivityManager.START_CLASS_NOT_FOUND, intent);
+ }
+ return aInfo;
+ }
+
+ public final Activity startActivityNow(Activity parent, String id,
+ Intent intent, IBinder token, Bundle state) {
+ ActivityInfo aInfo = resolveActivityInfo(intent);
+ return startActivityNow(parent, id, intent, aInfo, token, state);
+ }
+
+ public final Activity startActivityNow(Activity parent, String id,
+ Intent intent, ActivityInfo activityInfo, IBinder token, Bundle state) {
+ ActivityRecord r = new ActivityRecord();
+ r.token = token;
+ r.intent = intent;
+ r.state = state;
+ r.parent = parent;
+ r.embeddedID = id;
+ r.activityInfo = activityInfo;
+ if (localLOGV) {
+ ComponentName compname = intent.getComponent();
+ String name;
+ if (compname != null) {
+ name = compname.toShortString();
+ } else {
+ name = "(Intent " + intent + ").getComponent() returned null";
+ }
+ Log.v(TAG, "Performing launch: action=" + intent.getAction()
+ + ", comp=" + name
+ + ", token=" + token);
+ }
+ return performLaunchActivity(r);
+ }
+
+ public final Activity getActivity(IBinder token) {
+ return mActivities.get(token).activity;
+ }
+
+ public final void sendActivityResult(
+ IBinder token, String id, int requestCode,
+ int resultCode, Intent data) {
+ ArrayList<ResultInfo> list = new ArrayList<ResultInfo>();
+ list.add(new ResultInfo(id, requestCode, resultCode, data));
+ mAppThread.scheduleSendResult(token, list);
+ }
+
+ // if the thread hasn't started yet, we don't have the handler, so just
+ // save the messages until we're ready.
+ private final void queueOrSendMessage(int what, Object obj) {
+ queueOrSendMessage(what, obj, 0, 0);
+ }
+
+ private final void queueOrSendMessage(int what, Object obj, int arg1) {
+ queueOrSendMessage(what, obj, arg1, 0);
+ }
+
+ private final void queueOrSendMessage(int what, Object obj, int arg1, int arg2) {
+ synchronized (this) {
+ if (localLOGV) Log.v(
+ TAG, "SCHEDULE " + what + " " + mH.codeToString(what)
+ + ": " + arg1 + " / " + obj);
+ Message msg = Message.obtain();
+ msg.what = what;
+ msg.obj = obj;
+ msg.arg1 = arg1;
+ msg.arg2 = arg2;
+ mH.sendMessage(msg);
+ }
+ }
+
+ final void scheduleContextCleanup(ApplicationContext context, String who,
+ String what) {
+ ContextCleanupInfo cci = new ContextCleanupInfo();
+ cci.context = context;
+ cci.who = who;
+ cci.what = what;
+ queueOrSendMessage(H.CLEAN_UP_CONTEXT, cci);
+ }
+
+ private final Activity performLaunchActivity(ActivityRecord r) {
+ // System.out.println("##### [" + System.currentTimeMillis() + "] ActivityThread.performLaunchActivity(" + r + ")");
+
+ ActivityInfo aInfo = r.activityInfo;
+ if (r.packageInfo == null) {
+ r.packageInfo = getPackageInfo(aInfo.applicationInfo,
+ Context.CONTEXT_INCLUDE_CODE);
+ }
+
+ ComponentName component = r.intent.getComponent();
+ if (component == null) {
+ component = r.intent.resolveActivity(
+ mInitialApplication.getPackageManager());
+ r.intent.setComponent(component);
+ }
+
+ if (r.activityInfo.targetActivity != null) {
+ component = new ComponentName(r.activityInfo.packageName,
+ r.activityInfo.targetActivity);
+ }
+
+ Activity activity = null;
+ try {
+ java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
+ activity = mInstrumentation.newActivity(
+ cl, component.getClassName(), r.intent);
+ r.intent.setExtrasClassLoader(cl);
+ if (r.state != null) {
+ r.state.setClassLoader(cl);
+ }
+ } catch (Exception e) {
+ if (!mInstrumentation.onException(activity, e)) {
+ throw new RuntimeException(
+ "Unable to instantiate activity " + component
+ + ": " + e.toString(), e);
+ }
+ }
+
+ try {
+ Application app = r.packageInfo.makeApplication();
+
+ if (localLOGV) Log.v(TAG, "Performing launch of " + r);
+ if (localLOGV) Log.v(
+ TAG, r + ": app=" + app
+ + ", appName=" + app.getPackageName()
+ + ", pkg=" + r.packageInfo.getPackageName()
+ + ", comp=" + r.intent.getComponent().toShortString()
+ + ", dir=" + r.packageInfo.getAppDir());
+
+ if (activity != null) {
+ ApplicationContext appContext = new ApplicationContext();
+ appContext.init(r.packageInfo, r.token, this);
+ appContext.setOuterContext(activity);
+ CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
+ Configuration config = new Configuration(mConfiguration);
+ activity.attach(appContext, this, getInstrumentation(), r.token, app,
+ r.intent, r.activityInfo, title, r.parent, r.embeddedID,
+ r.lastNonConfigurationInstance, config);
+
+ r.lastNonConfigurationInstance = null;
+ activity.mStartedActivity = false;
+ int theme = r.activityInfo.getThemeResource();
+ if (theme != 0) {
+ activity.setTheme(theme);
+ }
+
+ activity.mCalled = false;
+ mInstrumentation.callActivityOnCreate(activity, r.state);
+ if (!activity.mCalled) {
+ throw new SuperNotCalledException(
+ "Activity " + r.intent.getComponent().toShortString() +
+ " did not call through to super.onCreate()");
+ }
+ r.activity = activity;
+ r.stopped = true;
+ if (!r.activity.mFinished) {
+ activity.performStart();
+ r.stopped = false;
+ }
+ if (!r.activity.mFinished) {
+ if (r.state != null) {
+ mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state);
+ }
+ }
+ if (!r.activity.mFinished) {
+ activity.mCalled = false;
+ mInstrumentation.callActivityOnPostCreate(activity, r.state);
+ if (!activity.mCalled) {
+ throw new SuperNotCalledException(
+ "Activity " + r.intent.getComponent().toShortString() +
+ " did not call through to super.onPostCreate()");
+ }
+ }
+ r.state = null;
+ }
+ r.paused = true;
+
+ mActivities.put(r.token, r);
+
+ } catch (SuperNotCalledException e) {
+ throw e;
+
+ } catch (Exception e) {
+ if (!mInstrumentation.onException(activity, e)) {
+ throw new RuntimeException(
+ "Unable to start activity " + component
+ + ": " + e.toString(), e);
+ }
+ }
+
+ return activity;
+ }
+
+ private final void handleLaunchActivity(ActivityRecord r) {
+ // If we are getting ready to gc after going to the background, well
+ // we are back active so skip it.
+ unscheduleGcIdler();
+
+ if (localLOGV) Log.v(
+ TAG, "Handling launch of " + r);
+ Activity a = performLaunchActivity(r);
+
+ if (a != null) {
+ handleResumeActivity(r.token, false);
+
+ if (!r.activity.mFinished && r.startsNotResumed) {
+ // The activity manager actually wants this one to start out
+ // paused, because it needs to be visible but isn't in the
+ // foreground. We accomplish this by going through the
+ // normal startup (because activities expect to go through
+ // onResume() the first time they run, before their window
+ // is displayed), and then pausing it. However, in this case
+ // we do -not- need to do the full pause cycle (of freezing
+ // and such) because the activity manager assumes it can just
+ // retain the current state it has.
+ try {
+ r.activity.mCalled = false;
+ mInstrumentation.callActivityOnPause(r.activity);
+ if (!r.activity.mCalled) {
+ throw new SuperNotCalledException(
+ "Activity " + r.intent.getComponent().toShortString() +
+ " did not call through to super.onPause()");
+ }
+
+ } catch (SuperNotCalledException e) {
+ throw e;
+
+ } catch (Exception e) {
+ if (!mInstrumentation.onException(r.activity, e)) {
+ throw new RuntimeException(
+ "Unable to pause activity "
+ + r.intent.getComponent().toShortString()
+ + ": " + e.toString(), e);
+ }
+ }
+ r.paused = true;
+ }
+ } else {
+ // If there was an error, for any reason, tell the activity
+ // manager to stop us.
+ try {
+ ActivityManagerNative.getDefault()
+ .finishActivity(r.token, Activity.RESULT_CANCELED, null);
+ } catch (RemoteException ex) {
+ }
+ }
+ }
+
+ private final void deliverNewIntents(ActivityRecord r,
+ List<Intent> intents) {
+ final int N = intents.size();
+ for (int i=0; i<N; i++) {
+ Intent intent = intents.get(i);
+ intent.setExtrasClassLoader(r.activity.getClassLoader());
+ mInstrumentation.callActivityOnNewIntent(r.activity, intent);
+ }
+ }
+
+ public final void performNewIntents(IBinder token,
+ List<Intent> intents) {
+ ActivityRecord r = mActivities.get(token);
+ if (r != null) {
+ final boolean resumed = !r.paused;
+ if (resumed) {
+ mInstrumentation.callActivityOnPause(r.activity);
+ }
+ deliverNewIntents(r, intents);
+ if (resumed) {
+ mInstrumentation.callActivityOnResume(r.activity);
+ }
+ }
+ }
+
+ private final void handleNewIntent(NewIntentData data) {
+ performNewIntents(data.token, data.intents);
+ }
+
+ private final void handleReceiver(ReceiverData data) {
+ // If we are getting ready to gc after going to the background, well
+ // we are back active so skip it.
+ unscheduleGcIdler();
+
+ String component = data.intent.getComponent().getClassName();
+
+ PackageInfo packageInfo = getPackageInfoNoCheck(
+ data.info.applicationInfo);
+
+ IActivityManager mgr = ActivityManagerNative.getDefault();
+
+ BroadcastReceiver receiver = null;
+ try {
+ java.lang.ClassLoader cl = packageInfo.getClassLoader();
+ data.intent.setExtrasClassLoader(cl);
+ if (data.resultExtras != null) {
+ data.resultExtras.setClassLoader(cl);
+ }
+ receiver = (BroadcastReceiver)cl.loadClass(component).newInstance();
+ } catch (Exception e) {
+ try {
+ mgr.finishReceiver(mAppThread.asBinder(), data.resultCode,
+ data.resultData, data.resultExtras, data.resultAbort);
+ } catch (RemoteException ex) {
+ }
+ throw new RuntimeException(
+ "Unable to instantiate receiver " + component
+ + ": " + e.toString(), e);
+ }
+
+ try {
+ Application app = packageInfo.makeApplication();
+
+ if (localLOGV) Log.v(
+ TAG, "Performing receive of " + data.intent
+ + ": app=" + app
+ + ", appName=" + app.getPackageName()
+ + ", pkg=" + packageInfo.getPackageName()
+ + ", comp=" + data.intent.getComponent().toShortString()
+ + ", dir=" + packageInfo.getAppDir());
+
+ ApplicationContext context = (ApplicationContext)app.getBaseContext();
+ receiver.setOrderedHint(true);
+ receiver.setResult(data.resultCode, data.resultData,
+ data.resultExtras);
+ receiver.setOrderedHint(data.sync);
+ receiver.onReceive(context.getReceiverRestrictedContext(),
+ data.intent);
+ } catch (Exception e) {
+ try {
+ mgr.finishReceiver(mAppThread.asBinder(), data.resultCode,
+ data.resultData, data.resultExtras, data.resultAbort);
+ } catch (RemoteException ex) {
+ }
+ if (!mInstrumentation.onException(receiver, e)) {
+ throw new RuntimeException(
+ "Unable to start receiver " + component
+ + ": " + e.toString(), e);
+ }
+ }
+
+ try {
+ if (data.sync) {
+ mgr.finishReceiver(
+ mAppThread.asBinder(), receiver.getResultCode(),
+ receiver.getResultData(), receiver.getResultExtras(false),
+ receiver.getAbortBroadcast());
+ } else {
+ mgr.finishReceiver(mAppThread.asBinder(), 0, null, null, false);
+ }
+ } catch (RemoteException ex) {
+ }
+ }
+
+ private final void handleCreateService(CreateServiceData data) {
+ // If we are getting ready to gc after going to the background, well
+ // we are back active so skip it.
+ unscheduleGcIdler();
+
+ PackageInfo packageInfo = getPackageInfoNoCheck(
+ data.info.applicationInfo);
+ Service service = null;
+ try {
+ java.lang.ClassLoader cl = packageInfo.getClassLoader();
+ service = (Service) cl.loadClass(data.info.name).newInstance();
+ } catch (Exception e) {
+ if (!mInstrumentation.onException(service, e)) {
+ throw new RuntimeException(
+ "Unable to instantiate service " + data.info.name
+ + ": " + e.toString(), e);
+ }
+ }
+
+ try {
+ if (localLOGV) Log.v(TAG, "Creating service " + data.info.name);
+
+ ApplicationContext context = new ApplicationContext();
+ context.init(packageInfo, null, this);
+
+ Application app = packageInfo.makeApplication();
+ context.setOuterContext(service);
+ service.attach(context, this, data.info.name, data.token, app,
+ ActivityManagerNative.getDefault());
+ service.onCreate();
+ mServices.put(data.token, service);
+ try {
+ ActivityManagerNative.getDefault().serviceDoneExecuting(data.token);
+ } catch (RemoteException e) {
+ // nothing to do.
+ }
+ } catch (Exception e) {
+ if (!mInstrumentation.onException(service, e)) {
+ throw new RuntimeException(
+ "Unable to create service " + data.info.name
+ + ": " + e.toString(), e);
+ }
+ }
+ }
+
+ private final void handleBindService(BindServiceData data) {
+ Service s = mServices.get(data.token);
+ if (s != null) {
+ try {
+ data.intent.setExtrasClassLoader(s.getClassLoader());
+ try {
+ if (!data.rebind) {
+ IBinder binder = s.onBind(data.intent);
+ ActivityManagerNative.getDefault().publishService(
+ data.token, data.intent, binder);
+ } else {
+ s.onRebind(data.intent);
+ ActivityManagerNative.getDefault().serviceDoneExecuting(
+ data.token);
+ }
+ } catch (RemoteException ex) {
+ }
+ } catch (Exception e) {
+ if (!mInstrumentation.onException(s, e)) {
+ throw new RuntimeException(
+ "Unable to bind to service " + s
+ + " with " + data.intent + ": " + e.toString(), e);
+ }
+ }
+ }
+ }
+
+ private final void handleUnbindService(BindServiceData data) {
+ Service s = mServices.get(data.token);
+ if (s != null) {
+ try {
+ data.intent.setExtrasClassLoader(s.getClassLoader());
+ boolean doRebind = s.onUnbind(data.intent);
+ try {
+ if (doRebind) {
+ ActivityManagerNative.getDefault().unbindFinished(
+ data.token, data.intent, doRebind);
+ } else {
+ ActivityManagerNative.getDefault().serviceDoneExecuting(
+ data.token);
+ }
+ } catch (RemoteException ex) {
+ }
+ } catch (Exception e) {
+ if (!mInstrumentation.onException(s, e)) {
+ throw new RuntimeException(
+ "Unable to unbind to service " + s
+ + " with " + data.intent + ": " + e.toString(), e);
+ }
+ }
+ }
+ }
+
+ private void handleDumpService(DumpServiceInfo info) {
+ try {
+ Service s = mServices.get(info.service);
+ if (s != null) {
+ PrintWriter pw = new PrintWriter(new FileOutputStream(info.fd));
+ s.dump(info.fd, pw, info.args);
+ pw.close();
+ }
+ } finally {
+ synchronized (info) {
+ info.dumped = true;
+ info.notifyAll();
+ }
+ }
+ }
+
+ private final void handleServiceArgs(ServiceArgsData data) {
+ Service s = mServices.get(data.token);
+ if (s != null) {
+ try {
+ if (data.args != null) {
+ data.args.setExtrasClassLoader(s.getClassLoader());
+ }
+ s.onStart(data.args, data.startId);
+ try {
+ ActivityManagerNative.getDefault().serviceDoneExecuting(data.token);
+ } catch (RemoteException e) {
+ // nothing to do.
+ }
+ } catch (Exception e) {
+ if (!mInstrumentation.onException(s, e)) {
+ throw new RuntimeException(
+ "Unable to start service " + s
+ + " with " + data.args + ": " + e.toString(), e);
+ }
+ }
+ }
+ }
+
+ private final void handleStopService(IBinder token) {
+ Service s = mServices.remove(token);
+ if (s != null) {
+ try {
+ if (localLOGV) Log.v(TAG, "Destroying service " + s);
+ s.onDestroy();
+ Context context = s.getBaseContext();
+ if (context instanceof ApplicationContext) {
+ final String who = s.getClassName();
+ ((ApplicationContext) context).scheduleFinalCleanup(who, "Service");
+ }
+ try {
+ ActivityManagerNative.getDefault().serviceDoneExecuting(token);
+ } catch (RemoteException e) {
+ // nothing to do.
+ }
+ } catch (Exception e) {
+ if (!mInstrumentation.onException(s, e)) {
+ throw new RuntimeException(
+ "Unable to stop service " + s
+ + ": " + e.toString(), e);
+ }
+ }
+ }
+ //Log.i(TAG, "Running services: " + mServices);
+ }
+
+ public final ActivityRecord performResumeActivity(IBinder token,
+ boolean clearHide) {
+ ActivityRecord r = mActivities.get(token);
+ if (localLOGV) Log.v(TAG, "Performing resume of " + r
+ + " finished=" + r.activity.mFinished);
+ if (r != null && !r.activity.mFinished) {
+ if (clearHide) {
+ r.hideForNow = false;
+ r.activity.mStartedActivity = false;
+ }
+ try {
+ if (r.pendingIntents != null) {
+ deliverNewIntents(r, r.pendingIntents);
+ r.pendingIntents = null;
+ }
+ if (r.pendingResults != null) {
+ deliverResults(r, r.pendingResults);
+ r.pendingResults = null;
+ }
+ r.activity.performResume();
+
+ EventLog.writeEvent(LOG_ON_RESUME_CALLED,
+ r.activity.getComponentName().getClassName());
+
+ r.paused = false;
+ r.stopped = false;
+ if (r.activity.mStartedActivity) {
+ r.hideForNow = true;
+ }
+ r.state = null;
+ } catch (Exception e) {
+ if (!mInstrumentation.onException(r.activity, e)) {
+ throw new RuntimeException(
+ "Unable to resume activity "
+ + r.intent.getComponent().toShortString()
+ + ": " + e.toString(), e);
+ }
+ }
+ }
+ return r;
+ }
+
+ final void handleResumeActivity(IBinder token, boolean clearHide) {
+ // If we are getting ready to gc after going to the background, well
+ // we are back active so skip it.
+ unscheduleGcIdler();
+
+ ActivityRecord r = performResumeActivity(token, clearHide);
+
+ if (r != null) {
+ final Activity a = r.activity;
+
+ if (localLOGV) Log.v(
+ TAG, "Resume " + r + " started activity: " +
+ a.mStartedActivity + ", hideForNow: " + r.hideForNow
+ + ", finished: " + a.mFinished);
+
+ // If the window hasn't yet been added to the window manager,
+ // and this guy didn't finish itself or start another activity,
+ // then go ahead and add the window.
+ if (r.window == null && !a.mFinished && !a.mStartedActivity) {
+ r.window = r.activity.getWindow();
+ View decor = r.window.getDecorView();
+ decor.setVisibility(View.INVISIBLE);
+ ViewManager wm = a.getWindowManager();
+ WindowManager.LayoutParams l = r.window.getAttributes();
+ a.mDecor = decor;
+ l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
+ wm.addView(decor, l);
+
+ // If the window has already been added, but during resume
+ // we started another activity, then don't yet make the
+ // window visisble.
+ } else if (a.mStartedActivity) {
+ if (localLOGV) Log.v(
+ TAG, "Launch " + r + " mStartedActivity set");
+ r.hideForNow = true;
+ }
+
+ // The window is now visible if it has been added, we are not
+ // simply finishing, and we are not starting another activity.
+ if (!r.activity.mFinished && r.activity.mDecor != null
+ && !r.hideForNow) {
+ if (r.newConfig != null) {
+ performConfigurationChanged(r.activity, r.newConfig);
+ r.newConfig = null;
+ }
+ r.activity.mDecor.setVisibility(View.VISIBLE);
+ mNumVisibleActivities++;
+ }
+
+ r.nextIdle = mNewActivities;
+ mNewActivities = r;
+ if (localLOGV) Log.v(
+ TAG, "Scheduling idle handler for " + r);
+ Looper.myQueue().addIdleHandler(new Idler());
+
+ } else {
+ // If an exception was thrown when trying to resume, then
+ // just end this activity.
+ try {
+ ActivityManagerNative.getDefault()
+ .finishActivity(token, Activity.RESULT_CANCELED, null);
+ } catch (RemoteException ex) {
+ }
+ }
+ }
+
+ private int mThumbnailWidth = -1;
+ private int mThumbnailHeight = -1;
+
+ private final Bitmap createThumbnailBitmap(ActivityRecord r) {
+ Bitmap thumbnail = null;
+ try {
+ int w = mThumbnailWidth;
+ int h;
+ if (w < 0) {
+ Resources res = r.activity.getResources();
+ mThumbnailHeight = h =
+ res.getDimensionPixelSize(com.android.internal.R.dimen.thumbnail_height);
+
+ mThumbnailWidth = w =
+ res.getDimensionPixelSize(com.android.internal.R.dimen.thumbnail_width);
+ } else {
+ h = mThumbnailHeight;
+ }
+
+ // XXX Only set hasAlpha if needed?
+ thumbnail = Bitmap.createBitmap(w, h, Bitmap.Config.RGB_565);
+ thumbnail.eraseColor(0);
+ Canvas cv = new Canvas(thumbnail);
+ if (!r.activity.onCreateThumbnail(thumbnail, cv)) {
+ thumbnail = null;
+ }
+ } catch (Exception e) {
+ if (!mInstrumentation.onException(r.activity, e)) {
+ throw new RuntimeException(
+ "Unable to create thumbnail of "
+ + r.intent.getComponent().toShortString()
+ + ": " + e.toString(), e);
+ }
+ thumbnail = null;
+ }
+
+ return thumbnail;
+ }
+
+ private final void handlePauseActivity(IBinder token, boolean finished,
+ int configChanges) {
+ ActivityRecord r = mActivities.get(token);
+ if (r != null) {
+ r.activity.mConfigChangeFlags |= configChanges;
+ Bundle state = performPauseActivity(token, finished, true);
+
+ // Tell the activity manager we have paused.
+ try {
+ ActivityManagerNative.getDefault().activityPaused(token, state);
+ } catch (RemoteException ex) {
+ }
+ }
+ }
+
+ final Bundle performPauseActivity(IBinder token, boolean finished,
+ boolean saveState) {
+ ActivityRecord r = mActivities.get(token);
+ return r != null ? performPauseActivity(r, finished, saveState) : null;
+ }
+
+ final Bundle performPauseActivity(ActivityRecord r, boolean finished,
+ boolean saveState) {
+ if (r.paused) {
+ if (r.activity.mFinished) {
+ // If we are finishing, we won't call onResume() in certain cases.
+ // So here we likewise don't want to call onPause() if the activity
+ // isn't resumed.
+ return null;
+ }
+ RuntimeException e = new RuntimeException(
+ "Performing pause of activity that is not resumed: "
+ + r.intent.getComponent().toShortString());
+ Log.e(TAG, e.getMessage(), e);
+ }
+ Bundle state = null;
+ if (finished) {
+ r.activity.mFinished = true;
+ }
+ try {
+ // Next have the activity save its current state and managed dialogs...
+ if (!r.activity.mFinished && saveState) {
+ state = new Bundle();
+ mInstrumentation.callActivityOnSaveInstanceState(r.activity, state);
+ r.state = state;
+ }
+ // Now we are idle.
+ r.activity.mCalled = false;
+ mInstrumentation.callActivityOnPause(r.activity);
+ EventLog.writeEvent(LOG_ON_PAUSE_CALLED, r.activity.getComponentName().getClassName());
+ if (!r.activity.mCalled) {
+ throw new SuperNotCalledException(
+ "Activity " + r.intent.getComponent().toShortString() +
+ " did not call through to super.onPause()");
+ }
+
+ } catch (SuperNotCalledException e) {
+ throw e;
+
+ } catch (Exception e) {
+ if (!mInstrumentation.onException(r.activity, e)) {
+ throw new RuntimeException(
+ "Unable to pause activity "
+ + r.intent.getComponent().toShortString()
+ + ": " + e.toString(), e);
+ }
+ }
+ r.paused = true;
+ return state;
+ }
+
+ final void performStopActivity(IBinder token) {
+ ActivityRecord r = mActivities.get(token);
+ performStopActivityInner(r, null, false);
+ }
+
+ private static class StopInfo {
+ Bitmap thumbnail;
+ CharSequence description;
+ }
+
+ private final class ProviderRefCount {
+ public int count;
+ ProviderRefCount(int pCount) {
+ count = pCount;
+ }
+ }
+
+ private final void performStopActivityInner(ActivityRecord r,
+ StopInfo info, boolean keepShown) {
+ if (localLOGV) Log.v(TAG, "Performing stop of " + r);
+ if (r != null) {
+ if (!keepShown && r.stopped) {
+ if (r.activity.mFinished) {
+ // If we are finishing, we won't call onResume() in certain
+ // cases. So here we likewise don't want to call onStop()
+ // if the activity isn't resumed.
+ return;
+ }
+ RuntimeException e = new RuntimeException(
+ "Performing stop of activity that is not resumed: "
+ + r.intent.getComponent().toShortString());
+ Log.e(TAG, e.getMessage(), e);
+ }
+
+ if (info != null) {
+ try {
+ // First create a thumbnail for the activity...
+ //info.thumbnail = createThumbnailBitmap(r);
+ info.description = r.activity.onCreateDescription();
+ } catch (Exception e) {
+ if (!mInstrumentation.onException(r.activity, e)) {
+ throw new RuntimeException(
+ "Unable to save state of activity "
+ + r.intent.getComponent().toShortString()
+ + ": " + e.toString(), e);
+ }
+ }
+ }
+
+ if (!keepShown) {
+ try {
+ // Now we are idle.
+ r.activity.performStop();
+ } catch (Exception e) {
+ if (!mInstrumentation.onException(r.activity, e)) {
+ throw new RuntimeException(
+ "Unable to stop activity "
+ + r.intent.getComponent().toShortString()
+ + ": " + e.toString(), e);
+ }
+ }
+ r.stopped = true;
+ }
+
+ r.paused = true;
+ }
+ }
+
+ private final void updateVisibility(ActivityRecord r, boolean show) {
+ View v = r.activity.mDecor;
+ if (v != null) {
+ if (show) {
+ if (v.getVisibility() != View.VISIBLE) {
+ v.setVisibility(View.VISIBLE);
+ mNumVisibleActivities++;
+ }
+ if (r.newConfig != null) {
+ performConfigurationChanged(r.activity, r.newConfig);
+ r.newConfig = null;
+ }
+ } else {
+ if (v.getVisibility() == View.VISIBLE) {
+ v.setVisibility(View.INVISIBLE);
+ mNumVisibleActivities--;
+ }
+ }
+ }
+ }
+
+ private final void handleStopActivity(IBinder token, boolean show, int configChanges) {
+ ActivityRecord r = mActivities.get(token);
+ r.activity.mConfigChangeFlags |= configChanges;
+
+ StopInfo info = new StopInfo();
+ performStopActivityInner(r, info, show);
+
+ if (localLOGV) Log.v(
+ TAG, "Finishing stop of " + r + ": show=" + show
+ + " win=" + r.window);
+
+ updateVisibility(r, show);
+
+ // Tell activity manager we have been stopped.
+ try {
+ ActivityManagerNative.getDefault().activityStopped(
+ r.token, info.thumbnail, info.description);
+ } catch (RemoteException ex) {
+ }
+ }
+
+ final void performRestartActivity(IBinder token) {
+ ActivityRecord r = mActivities.get(token);
+ if (r.stopped) {
+ r.activity.performRestart();
+ r.stopped = false;
+ }
+ }
+
+ private final void handleWindowVisibility(IBinder token, boolean show) {
+ ActivityRecord r = mActivities.get(token);
+ if (!show && !r.stopped) {
+ performStopActivityInner(r, null, show);
+ } else if (show && r.stopped) {
+ // If we are getting ready to gc after going to the background, well
+ // we are back active so skip it.
+ unscheduleGcIdler();
+
+ r.activity.performRestart();
+ r.stopped = false;
+ }
+ if (r.activity.mDecor != null) {
+ if (Config.LOGV) Log.v(
+ TAG, "Handle window " + r + " visibility: " + show);
+ updateVisibility(r, show);
+ }
+ }
+
+ private final void deliverResults(ActivityRecord r, List<ResultInfo> results) {
+ final int N = results.size();
+ for (int i=0; i<N; i++) {
+ ResultInfo ri = results.get(i);
+ try {
+ if (ri.mData != null) {
+ ri.mData.setExtrasClassLoader(r.activity.getClassLoader());
+ }
+ r.activity.dispatchActivityResult(ri.mResultWho,
+ ri.mRequestCode, ri.mResultCode, ri.mData);
+ } catch (Exception e) {
+ if (!mInstrumentation.onException(r.activity, e)) {
+ throw new RuntimeException(
+ "Failure delivering result " + ri + " to activity "
+ + r.intent.getComponent().toShortString()
+ + ": " + e.toString(), e);
+ }
+ }
+ }
+ }
+
+ private final void handleSendResult(ResultData res) {
+ ActivityRecord r = mActivities.get(res.token);
+ if (localLOGV) Log.v(TAG, "Handling send result to " + r);
+ if (r != null) {
+ final boolean resumed = !r.paused;
+ if (!r.activity.mFinished && r.activity.mDecor != null
+ && r.hideForNow && resumed) {
+ // We had hidden the activity because it started another
+ // one... we have gotten a result back and we are not
+ // paused, so make sure our window is visible.
+ updateVisibility(r, true);
+ }
+ if (resumed) {
+ try {
+ // Now we are idle.
+ r.activity.mCalled = false;
+ mInstrumentation.callActivityOnPause(r.activity);
+ if (!r.activity.mCalled) {
+ throw new SuperNotCalledException(
+ "Activity " + r.intent.getComponent().toShortString()
+ + " did not call through to super.onPause()");
+ }
+ } catch (SuperNotCalledException e) {
+ throw e;
+ } catch (Exception e) {
+ if (!mInstrumentation.onException(r.activity, e)) {
+ throw new RuntimeException(
+ "Unable to pause activity "
+ + r.intent.getComponent().toShortString()
+ + ": " + e.toString(), e);
+ }
+ }
+ }
+ deliverResults(r, res.results);
+ if (resumed) {
+ mInstrumentation.callActivityOnResume(r.activity);
+ }
+ }
+ }
+
+ public final ActivityRecord performDestroyActivity(IBinder token, boolean finishing) {
+ return performDestroyActivity(token, finishing, 0, false);
+ }
+
+ private final ActivityRecord performDestroyActivity(IBinder token, boolean finishing,
+ int configChanges, boolean getNonConfigInstance) {
+ ActivityRecord r = mActivities.get(token);
+ if (localLOGV) Log.v(TAG, "Performing finish of " + r);
+ if (r != null) {
+ r.activity.mConfigChangeFlags |= configChanges;
+ if (finishing) {
+ r.activity.mFinished = true;
+ }
+ if (!r.paused) {
+ try {
+ r.activity.mCalled = false;
+ mInstrumentation.callActivityOnPause(r.activity);
+ EventLog.writeEvent(LOG_ON_PAUSE_CALLED,
+ r.activity.getComponentName().getClassName());
+ if (!r.activity.mCalled) {
+ throw new SuperNotCalledException(
+ "Activity " + r.intent.getComponent().toShortString()
+ + " did not call through to super.onPause()");
+ }
+ } catch (SuperNotCalledException e) {
+ throw e;
+ } catch (Exception e) {
+ if (!mInstrumentation.onException(r.activity, e)) {
+ throw new RuntimeException(
+ "Unable to pause activity "
+ + r.intent.getComponent().toShortString()
+ + ": " + e.toString(), e);
+ }
+ }
+ r.paused = true;
+ }
+ if (!r.stopped) {
+ try {
+ r.activity.performStop();
+ } catch (SuperNotCalledException e) {
+ throw e;
+ } catch (Exception e) {
+ if (!mInstrumentation.onException(r.activity, e)) {
+ throw new RuntimeException(
+ "Unable to stop activity "
+ + r.intent.getComponent().toShortString()
+ + ": " + e.toString(), e);
+ }
+ }
+ r.stopped = true;
+ }
+ if (getNonConfigInstance) {
+ try {
+ r.lastNonConfigurationInstance
+ = r.activity.onRetainNonConfigurationInstance();
+ } catch (Exception e) {
+ if (!mInstrumentation.onException(r.activity, e)) {
+ throw new RuntimeException(
+ "Unable to retain activity "
+ + r.intent.getComponent().toShortString()
+ + ": " + e.toString(), e);
+ }
+ }
+
+ }
+ try {
+ r.activity.mCalled = false;
+ r.activity.onDestroy();
+ if (!r.activity.mCalled) {
+ throw new SuperNotCalledException(
+ "Activity " + r.intent.getComponent().toShortString() +
+ " did not call through to super.onDestroy()");
+ }
+ if (r.window != null) {
+ r.window.closeAllPanels();
+ }
+ } catch (SuperNotCalledException e) {
+ throw e;
+ } catch (Exception e) {
+ if (!mInstrumentation.onException(r.activity, e)) {
+ throw new RuntimeException(
+ "Unable to destroy activity "
+ + r.intent.getComponent().toShortString()
+ + ": " + e.toString(), e);
+ }
+ }
+ }
+ mActivities.remove(token);
+
+ return r;
+ }
+
+ private final void handleDestroyActivity(IBinder token, boolean finishing,
+ int configChanges, boolean getNonConfigInstance) {
+ ActivityRecord r = performDestroyActivity(token, finishing,
+ configChanges, getNonConfigInstance);
+ if (r != null) {
+ WindowManager wm = r.activity.getWindowManager();
+ View v = r.activity.mDecor;
+ if (v != null) {
+ if (v.getVisibility() == View.VISIBLE) {
+ mNumVisibleActivities--;
+ }
+ IBinder wtoken = v.getWindowToken();
+ wm.removeViewImmediate(v);
+ if (wtoken != null) {
+ WindowManagerImpl.getDefault().closeAll(wtoken,
+ r.activity.getClass().getName(), "Activity");
+ }
+ r.activity.mDecor = null;
+ }
+ WindowManagerImpl.getDefault().closeAll(token,
+ r.activity.getClass().getName(), "Activity");
+
+ // Mocked out contexts won't be participating in the normal
+ // process lifecycle, but if we're running with a proper
+ // ApplicationContext we need to have it tear down things
+ // cleanly.
+ Context c = r.activity.getBaseContext();
+ if (c instanceof ApplicationContext) {
+ ((ApplicationContext) c).scheduleFinalCleanup(
+ r.activity.getClass().getName(), "Activity");
+ }
+ }
+ if (finishing) {
+ try {
+ ActivityManagerNative.getDefault().activityDestroyed(token);
+ } catch (RemoteException ex) {
+ // If the system process has died, it's game over for everyone.
+ }
+ }
+ }
+
+ private final void handleRelaunchActivity(ActivityRecord tmp, int configChanges) {
+ // If we are getting ready to gc after going to the background, well
+ // we are back active so skip it.
+ unscheduleGcIdler();
+
+ Configuration changedConfig = null;
+
+ // First: make sure we have the most recent configuration and most
+ // recent version of the activity, or skip it if some previous call
+ // had taken a more recent version.
+ synchronized (mRelaunchingActivities) {
+ int N = mRelaunchingActivities.size();
+ IBinder token = tmp.token;
+ tmp = null;
+ for (int i=0; i<N; i++) {
+ ActivityRecord r = mRelaunchingActivities.get(i);
+ if (r.token == token) {
+ tmp = r;
+ mRelaunchingActivities.remove(i);
+ i--;
+ N--;
+ }
+ }
+
+ if (tmp == null) {
+ return;
+ }
+
+ if (mPendingConfiguration != null) {
+ changedConfig = mPendingConfiguration;
+ mPendingConfiguration = null;
+ }
+ }
+
+ // If there was a pending configuration change, execute it first.
+ if (changedConfig != null) {
+ handleConfigurationChanged(changedConfig);
+ }
+
+ ActivityRecord r = mActivities.get(tmp.token);
+ if (localLOGV) Log.v(TAG, "Handling relaunch of " + r);
+ if (r == null) {
+ return;
+ }
+
+ r.activity.mConfigChangeFlags |= configChanges;
+
+ Bundle savedState = null;
+ if (!r.paused) {
+ savedState = performPauseActivity(r.token, false, true);
+ }
+
+ handleDestroyActivity(r.token, false, configChanges, true);
+
+ r.activity = null;
+ r.window = null;
+ r.hideForNow = false;
+ r.nextIdle = null;
+ r.pendingResults = tmp.pendingResults;
+ r.pendingIntents = tmp.pendingIntents;
+ r.startsNotResumed = tmp.startsNotResumed;
+ if (savedState != null) {
+ r.state = savedState;
+ }
+
+ handleLaunchActivity(r);
+ }
+
+ private final void handleRequestThumbnail(IBinder token) {
+ ActivityRecord r = mActivities.get(token);
+ Bitmap thumbnail = createThumbnailBitmap(r);
+ CharSequence description = null;
+ try {
+ description = r.activity.onCreateDescription();
+ } catch (Exception e) {
+ if (!mInstrumentation.onException(r.activity, e)) {
+ throw new RuntimeException(
+ "Unable to create description of activity "
+ + r.intent.getComponent().toShortString()
+ + ": " + e.toString(), e);
+ }
+ }
+ //System.out.println("Reporting top thumbnail " + thumbnail);
+ try {
+ ActivityManagerNative.getDefault().reportThumbnail(
+ token, thumbnail, description);
+ } catch (RemoteException ex) {
+ }
+ }
+
+ ArrayList<ComponentCallbacks> collectComponentCallbacksLocked(
+ boolean allActivities, Configuration newConfig) {
+ ArrayList<ComponentCallbacks> callbacks
+ = new ArrayList<ComponentCallbacks>();
+
+ if (mActivities.size() > 0) {
+ Iterator<ActivityRecord> it = mActivities.values().iterator();
+ while (it.hasNext()) {
+ ActivityRecord ar = it.next();
+ Activity a = ar.activity;
+ if (a != null) {
+ if (!ar.activity.mFinished && (allActivities ||
+ (a != null && !ar.paused))) {
+ // If the activity is currently resumed, its configuration
+ // needs to change right now.
+ callbacks.add(a);
+ } else if (newConfig != null) {
+ // Otherwise, we will tell it about the change
+ // the next time it is resumed or shown. Note that
+ // the activity manager may, before then, decide the
+ // activity needs to be destroyed to handle its new
+ // configuration.
+ ar.newConfig = newConfig;
+ }
+ }
+ }
+ }
+ if (mServices.size() > 0) {
+ Iterator<Service> it = mServices.values().iterator();
+ while (it.hasNext()) {
+ callbacks.add(it.next());
+ }
+ }
+ synchronized (mProviderMap) {
+ if (mLocalProviders.size() > 0) {
+ Iterator<ProviderRecord> it = mLocalProviders.values().iterator();
+ while (it.hasNext()) {
+ callbacks.add(it.next().mLocalProvider);
+ }
+ }
+ }
+ final int N = mAllApplications.size();
+ for (int i=0; i<N; i++) {
+ callbacks.add(mAllApplications.get(i));
+ }
+
+ return callbacks;
+ }
+
+ private final void performConfigurationChanged(
+ ComponentCallbacks cb, Configuration config) {
+ // Only for Activity objects, check that they actually call up to their
+ // superclass implementation. ComponentCallbacks is an interface, so
+ // we check the runtime type and act accordingly.
+ Activity activity = (cb instanceof Activity) ? (Activity) cb : null;
+ if (activity != null) {
+ activity.mCalled = false;
+ }
+
+ boolean shouldChangeConfig = false;
+ if ((activity == null) || (activity.mCurrentConfig == null)) {
+ shouldChangeConfig = true;
+ } else {
+
+ // If the new config is the same as the config this Activity
+ // is already running with then don't bother calling
+ // onConfigurationChanged
+ int diff = activity.mCurrentConfig.diff(config);
+ if (diff != 0) {
+
+ // If this activity doesn't handle any of the config changes
+ // then don't bother calling onConfigurationChanged as we're
+ // going to destroy it.
+ if ((~activity.mActivityInfo.configChanges & diff) == 0) {
+ shouldChangeConfig = true;
+ }
+ }
+ }
+
+ if (shouldChangeConfig) {
+ cb.onConfigurationChanged(config);
+
+ if (activity != null) {
+ if (!activity.mCalled) {
+ throw new SuperNotCalledException(
+ "Activity " + activity.getLocalClassName() +
+ " did not call through to super.onConfigurationChanged()");
+ }
+ activity.mConfigChangeFlags = 0;
+ activity.mCurrentConfig = new Configuration(config);
+ }
+ }
+ }
+
+ final void handleConfigurationChanged(Configuration config) {
+
+ synchronized (mRelaunchingActivities) {
+ if (mPendingConfiguration != null) {
+ config = mPendingConfiguration;
+ mPendingConfiguration = null;
+ }
+ }
+
+ ArrayList<ComponentCallbacks> callbacks
+ = new ArrayList<ComponentCallbacks>();
+
+ synchronized(mPackages) {
+ if (mConfiguration == null) {
+ mConfiguration = new Configuration();
+ }
+ mConfiguration.updateFrom(config);
+
+ // set it for java, this also affects newly created Resources
+ if (config.locale != null) {
+ Locale.setDefault(config.locale);
+ }
+
+ if (mSystemContext != null) {
+ mSystemContext.getResources().updateConfiguration(config, null);
+ //Log.i(TAG, "Updated system resources " + mSystemContext.getResources()
+ // + ": " + mSystemContext.getResources().getConfiguration());
+ }
+
+ ApplicationContext.ApplicationPackageManager.configurationChanged();
+ //Log.i(TAG, "Configuration changed in " + currentPackageName());
+ {
+ Iterator<WeakReference<Resources>> it =
+ mActiveResources.values().iterator();
+ //Iterator<Map.Entry<String, WeakReference<Resources>>> it =
+ // mActiveResources.entrySet().iterator();
+ while (it.hasNext()) {
+ WeakReference<Resources> v = it.next();
+ Resources r = v.get();
+ if (r != null) {
+ r.updateConfiguration(config, null);
+ //Log.i(TAG, "Updated app resources " + v.getKey()
+ // + " " + r + ": " + r.getConfiguration());
+ } else {
+ //Log.i(TAG, "Removing old resources " + v.getKey());
+ it.remove();
+ }
+ }
+ }
+
+ callbacks = collectComponentCallbacksLocked(false, config);
+ }
+
+ final int N = callbacks.size();
+ for (int i=0; i<N; i++) {
+ performConfigurationChanged(callbacks.get(i), config);
+ }
+ }
+
+ final void handleActivityConfigurationChanged(IBinder token) {
+ ActivityRecord r = mActivities.get(token);
+ if (r == null || r.activity == null) {
+ return;
+ }
+
+ performConfigurationChanged(r.activity, mConfiguration);
+ }
+
+ final void handleLowMemory() {
+ ArrayList<ComponentCallbacks> callbacks
+ = new ArrayList<ComponentCallbacks>();
+
+ synchronized(mPackages) {
+ callbacks = collectComponentCallbacksLocked(true, null);
+ }
+
+ final int N = callbacks.size();
+ for (int i=0; i<N; i++) {
+ callbacks.get(i).onLowMemory();
+ }
+
+ // Ask SQLite to free up as much memory as it can, mostly from it's page caches
+ int sqliteReleased = SQLiteDatabase.releaseMemory();
+ EventLog.writeEvent(SQLITE_MEM_RELEASED_EVENT_LOG_TAG, sqliteReleased);
+
+ BinderInternal.forceGc("mem");
+ }
+
+ private final void handleBindApplication(AppBindData data) {
+ mBoundApplication = data;
+ mConfiguration = new Configuration(data.config);
+
+ // We now rely on this being set by zygote.
+ //Process.setGid(data.appInfo.gid);
+ //Process.setUid(data.appInfo.uid);
+
+ // send up app name; do this *before* waiting for debugger
+ android.ddm.DdmHandleAppName.setAppName(data.processName);
+
+ /*
+ * Before spawning a new process, reset the time zone to be the system time zone.
+ * This needs to be done because the system time zone could have changed after the
+ * the spawning of this process. Without doing this this process would have the incorrect
+ * system time zone.
+ */
+ TimeZone.setDefault(null);
+
+ /*
+ * Initialize the default locale in this process for the reasons we set the time zone.
+ */
+ Locale.setDefault(data.config.locale);
+
+ data.info = getPackageInfoNoCheck(data.appInfo);
+
+ if (data.debugMode != IApplicationThread.DEBUG_OFF) {
+ // XXX should have option to change the port.
+ Debug.changeDebugPort(8100);
+ if (data.debugMode == IApplicationThread.DEBUG_WAIT) {
+ Log.w(TAG, "Application " + data.info.getPackageName()
+ + " is waiting for the debugger on port 8100...");
+
+ IActivityManager mgr = ActivityManagerNative.getDefault();
+ try {
+ mgr.showWaitingForDebugger(mAppThread, true);
+ } catch (RemoteException ex) {
+ }
+
+ Debug.waitForDebugger();
+
+ try {
+ mgr.showWaitingForDebugger(mAppThread, false);
+ } catch (RemoteException ex) {
+ }
+
+ } else {
+ Log.w(TAG, "Application " + data.info.getPackageName()
+ + " can be debugged on port 8100...");
+ }
+ }
+
+ if (data.instrumentationName != null) {
+ ApplicationContext appContext = new ApplicationContext();
+ appContext.init(data.info, null, this);
+ InstrumentationInfo ii = null;
+ try {
+ ii = appContext.getPackageManager().
+ getInstrumentationInfo(data.instrumentationName, 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ }
+ if (ii == null) {
+ throw new RuntimeException(
+ "Unable to find instrumentation info for: "
+ + data.instrumentationName);
+ }
+
+ mInstrumentationAppDir = ii.sourceDir;
+ mInstrumentationAppPackage = ii.packageName;
+ mInstrumentedAppDir = data.info.getAppDir();
+
+ ApplicationInfo instrApp = new ApplicationInfo();
+ instrApp.packageName = ii.packageName;
+ instrApp.sourceDir = ii.sourceDir;
+ instrApp.publicSourceDir = ii.publicSourceDir;
+ instrApp.dataDir = ii.dataDir;
+ PackageInfo pi = getPackageInfo(instrApp,
+ appContext.getClassLoader(), false, true);
+ ApplicationContext instrContext = new ApplicationContext();
+ instrContext.init(pi, null, this);
+
+ try {
+ java.lang.ClassLoader cl = instrContext.getClassLoader();
+ mInstrumentation = (Instrumentation)
+ cl.loadClass(data.instrumentationName.getClassName()).newInstance();
+ } catch (Exception e) {
+ throw new RuntimeException(
+ "Unable to instantiate instrumentation "
+ + data.instrumentationName + ": " + e.toString(), e);
+ }
+
+ mInstrumentation.init(this, instrContext, appContext,
+ new ComponentName(ii.packageName, ii.name), data.instrumentationWatcher);
+
+ if (data.profileFile != null && !ii.handleProfiling) {
+ data.handlingProfiling = true;
+ File file = new File(data.profileFile);
+ file.getParentFile().mkdirs();
+ Debug.startMethodTracing(file.toString(), 8 * 1024 * 1024);
+ }
+
+ try {
+ mInstrumentation.onCreate(data.instrumentationArgs);
+ }
+ catch (Exception e) {
+ throw new RuntimeException(
+ "Exception thrown in onCreate() of "
+ + data.instrumentationName + ": " + e.toString(), e);
+ }
+
+ } else {
+ mInstrumentation = new Instrumentation();
+ }
+
+ Application app = data.info.makeApplication();
+ mInitialApplication = app;
+
+ List<ProviderInfo> providers = data.providers;
+ if (providers != null) {
+ installContentProviders(app, providers);
+ }
+
+ try {
+ mInstrumentation.callApplicationOnCreate(app);
+ } catch (Exception e) {
+ if (!mInstrumentation.onException(app, e)) {
+ throw new RuntimeException(
+ "Unable to create application " + app.getClass().getName()
+ + ": " + e.toString(), e);
+ }
+ }
+ }
+
+ /*package*/ final void finishInstrumentation(int resultCode, Bundle results) {
+ IActivityManager am = ActivityManagerNative.getDefault();
+ if (mBoundApplication.profileFile != null && mBoundApplication.handlingProfiling) {
+ Debug.stopMethodTracing();
+ }
+ //Log.i(TAG, "am: " + ActivityManagerNative.getDefault()
+ // + ", app thr: " + mAppThread);
+ try {
+ am.finishInstrumentation(mAppThread, resultCode, results);
+ } catch (RemoteException ex) {
+ }
+ }
+
+ private final void installContentProviders(
+ Context context, List<ProviderInfo> providers) {
+ final ArrayList<IActivityManager.ContentProviderHolder> results =
+ new ArrayList<IActivityManager.ContentProviderHolder>();
+
+ Iterator<ProviderInfo> i = providers.iterator();
+ while (i.hasNext()) {
+ ProviderInfo cpi = i.next();
+ StringBuilder buf = new StringBuilder(128);
+ buf.append("Publishing provider ");
+ buf.append(cpi.authority);
+ buf.append(": ");
+ buf.append(cpi.name);
+ Log.i(TAG, buf.toString());
+ IContentProvider cp = installProvider(context, null, cpi, false);
+ if (cp != null) {
+ IActivityManager.ContentProviderHolder cph =
+ new IActivityManager.ContentProviderHolder(cpi);
+ cph.provider = cp;
+ results.add(cph);
+ // Don't ever unload this provider from the process.
+ synchronized(mProviderMap) {
+ mProviderRefCountMap.put(cp.asBinder(), new ProviderRefCount(10000));
+ }
+ }
+ }
+
+ try {
+ ActivityManagerNative.getDefault().publishContentProviders(
+ getApplicationThread(), results);
+ } catch (RemoteException ex) {
+ }
+ }
+
+ private final IContentProvider getProvider(Context context, String name) {
+ synchronized(mProviderMap) {
+ final ProviderRecord pr = mProviderMap.get(name);
+ if (pr != null) {
+ return pr.mProvider;
+ }
+ }
+
+ IActivityManager.ContentProviderHolder holder = null;
+ try {
+ holder = ActivityManagerNative.getDefault().getContentProvider(
+ getApplicationThread(), name);
+ } catch (RemoteException ex) {
+ }
+ if (holder == null) {
+ Log.e(TAG, "Failed to find provider info for " + name);
+ return null;
+ }
+ if (holder.permissionFailure != null) {
+ throw new SecurityException("Permission " + holder.permissionFailure
+ + " required for provider " + name);
+ }
+
+ IContentProvider prov = installProvider(context, holder.provider,
+ holder.info, true);
+ //Log.i(TAG, "noReleaseNeeded=" + holder.noReleaseNeeded);
+ if (holder.noReleaseNeeded || holder.provider == null) {
+ // We are not going to release the provider if it is an external
+ // provider that doesn't care about being released, or if it is
+ // a local provider running in this process.
+ //Log.i(TAG, "*** NO RELEASE NEEDED");
+ synchronized(mProviderMap) {
+ mProviderRefCountMap.put(prov.asBinder(), new ProviderRefCount(10000));
+ }
+ }
+ return prov;
+ }
+
+ public final IContentProvider acquireProvider(Context c, String name) {
+ IContentProvider provider = getProvider(c, name);
+ if(provider == null)
+ return null;
+ IBinder jBinder = provider.asBinder();
+ synchronized(mProviderMap) {
+ ProviderRefCount prc = mProviderRefCountMap.get(jBinder);
+ if(prc == null) {
+ mProviderRefCountMap.put(jBinder, new ProviderRefCount(1));
+ } else {
+ prc.count++;
+ } //end else
+ } //end synchronized
+ return provider;
+ }
+
+ public final boolean releaseProvider(IContentProvider provider) {
+ if(provider == null) {
+ return false;
+ }
+ IBinder jBinder = provider.asBinder();
+ synchronized(mProviderMap) {
+ ProviderRefCount prc = mProviderRefCountMap.get(jBinder);
+ if(prc == null) {
+ if(localLOGV) Log.v(TAG, "releaseProvider::Weird shouldnt be here");
+ return false;
+ } else {
+ prc.count--;
+ if(prc.count == 0) {
+ mProviderRefCountMap.remove(jBinder);
+ //invoke removeProvider to dereference provider
+ removeProviderLocked(provider);
+ } //end if
+ } //end else
+ } //end synchronized
+ return true;
+ }
+
+ public final void removeProviderLocked(IContentProvider provider) {
+ if (provider == null) {
+ return;
+ }
+ IBinder providerBinder = provider.asBinder();
+ boolean amRemoveFlag = false;
+
+ // remove the provider from mProviderMap
+ Iterator<ProviderRecord> iter = mProviderMap.values().iterator();
+ while (iter.hasNext()) {
+ ProviderRecord pr = iter.next();
+ IBinder myBinder = pr.mProvider.asBinder();
+ if (myBinder == providerBinder) {
+ //find if its published by this process itself
+ if(pr.mLocalProvider != null) {
+ if(localLOGV) Log.i(TAG, "removeProvider::found local provider returning");
+ return;
+ }
+ if(localLOGV) Log.v(TAG, "removeProvider::Not local provider Unlinking " +
+ "death recipient");
+ //content provider is in another process
+ myBinder.unlinkToDeath(pr, 0);
+ iter.remove();
+ //invoke remove only once for the very first name seen
+ if(!amRemoveFlag) {
+ try {
+ if(localLOGV) Log.v(TAG, "removeProvider::Invoking " +
+ "ActivityManagerNative.removeContentProvider("+pr.mName);
+ ActivityManagerNative.getDefault().removeContentProvider(getApplicationThread(), pr.mName);
+ amRemoveFlag = true;
+ } catch (RemoteException e) {
+ //do nothing content provider object is dead any way
+ } //end catch
+ }
+ } //end if myBinder
+ } //end while iter
+ }
+
+ final void removeDeadProvider(String name, IContentProvider provider) {
+ synchronized(mProviderMap) {
+ ProviderRecord pr = mProviderMap.get(name);
+ if (pr.mProvider.asBinder() == provider.asBinder()) {
+ Log.i(TAG, "Removing dead content provider: " + name);
+ mProviderMap.remove(name);
+ }
+ }
+ }
+
+ final void removeDeadProviderLocked(String name, IContentProvider provider) {
+ ProviderRecord pr = mProviderMap.get(name);
+ if (pr.mProvider.asBinder() == provider.asBinder()) {
+ Log.i(TAG, "Removing dead content provider: " + name);
+ mProviderMap.remove(name);
+ }
+ }
+
+ private final IContentProvider installProvider(Context context,
+ IContentProvider provider, ProviderInfo info, boolean noisy) {
+ ContentProvider localProvider = null;
+ if (provider == null) {
+ if (noisy) {
+ Log.d(TAG, "Loading provider " + info.authority + ": "
+ + info.name);
+ }
+ Context c = null;
+ ApplicationInfo ai = info.applicationInfo;
+ if (context.getPackageName().equals(ai.packageName)) {
+ c = context;
+ } else if (mInitialApplication != null &&
+ mInitialApplication.getPackageName().equals(ai.packageName)) {
+ c = mInitialApplication;
+ } else {
+ try {
+ c = context.createPackageContext(ai.packageName,
+ Context.CONTEXT_INCLUDE_CODE);
+ } catch (PackageManager.NameNotFoundException e) {
+ }
+ }
+ if (c == null) {
+ Log.w(TAG, "Unable to get context for package " +
+ ai.packageName +
+ " while loading content provider " +
+ info.name);
+ return null;
+ }
+ try {
+ final java.lang.ClassLoader cl = c.getClassLoader();
+ localProvider = (ContentProvider)cl.
+ loadClass(info.name).newInstance();
+ provider = localProvider.getIContentProvider();
+ if (provider == null) {
+ Log.e(TAG, "Failed to instantiate class " +
+ info.name + " from sourceDir " +
+ info.applicationInfo.sourceDir);
+ return null;
+ }
+ if (Config.LOGV) Log.v(
+ TAG, "Instantiating local provider " + info.name);
+ // XXX Need to create the correct context for this provider.
+ localProvider.attachInfo(c, info);
+ } catch (java.lang.Exception e) {
+ if (!mInstrumentation.onException(null, e)) {
+ throw new RuntimeException(
+ "Unable to get provider " + info.name
+ + ": " + e.toString(), e);
+ }
+ return null;
+ }
+ } else if (localLOGV) {
+ Log.v(TAG, "Installing external provider " + info.authority + ": "
+ + info.name);
+ }
+
+ synchronized (mProviderMap) {
+ // Cache the pointer for the remote provider.
+ String names[] = PATTERN_SEMICOLON.split(info.authority);
+ for (int i=0; i<names.length; i++) {
+ ProviderRecord pr = new ProviderRecord(names[i], provider,
+ localProvider);
+ try {
+ provider.asBinder().linkToDeath(pr, 0);
+ mProviderMap.put(names[i], pr);
+ } catch (RemoteException e) {
+ return null;
+ }
+ }
+ if (localProvider != null) {
+ mLocalProviders.put(provider.asBinder(),
+ new ProviderRecord(null, provider, localProvider));
+ }
+ }
+
+ return provider;
+ }
+
+ private final void attach(boolean system) {
+ sThreadLocal.set(this);
+ mSystemThread = system;
+ AndroidHttpClient.setThreadBlocked(true);
+ if (!system) {
+ android.ddm.DdmHandleAppName.setAppName("<pre-initialized>");
+ RuntimeInit.setApplicationObject(mAppThread.asBinder());
+ IActivityManager mgr = ActivityManagerNative.getDefault();
+ try {
+ mgr.attachApplication(mAppThread);
+ } catch (RemoteException ex) {
+ }
+ } else {
+ // Don't set application object here -- if the system crashes,
+ // we can't display an alert, we just want to die die die.
+ android.ddm.DdmHandleAppName.setAppName("system_process");
+ try {
+ mInstrumentation = new Instrumentation();
+ ApplicationContext context = new ApplicationContext();
+ context.init(getSystemContext().mPackageInfo, null, this);
+ Application app = Instrumentation.newApplication(Application.class, context);
+ mAllApplications.add(app);
+ mInitialApplication = app;
+ app.onCreate();
+ } catch (Exception e) {
+ throw new RuntimeException(
+ "Unable to instantiate Application():" + e.toString(), e);
+ }
+ }
+ }
+
+ private final void detach()
+ {
+ AndroidHttpClient.setThreadBlocked(false);
+ sThreadLocal.set(null);
+ }
+
+ public static final ActivityThread systemMain() {
+ ActivityThread thread = new ActivityThread();
+ thread.attach(true);
+ return thread;
+ }
+
+ public final void installSystemProviders(List providers) {
+ if (providers != null) {
+ installContentProviders(mInitialApplication,
+ (List<ProviderInfo>)providers);
+ }
+ }
+
+ public static final void main(String[] args) {
+ Process.setArgV0("<pre-initialized>");
+
+ Looper.prepareMainLooper();
+
+ ActivityThread thread = new ActivityThread();
+ thread.attach(false);
+
+ Looper.loop();
+
+ if (Process.supportsProcesses()) {
+ throw new RuntimeException("Main thread loop unexpectedly exited");
+ }
+
+ thread.detach();
+ String name;
+ if (thread.mInitialApplication != null) name = thread.mInitialApplication.getPackageName();
+ else name = "<unknown>";
+ Log.i(TAG, "Main thread of " + name + " is now exiting");
+ }
+}
diff --git a/core/java/android/app/AlarmManager.java b/core/java/android/app/AlarmManager.java
new file mode 100644
index 0000000..35c6ac1
--- /dev/null
+++ b/core/java/android/app/AlarmManager.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.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
+ * to schedule your application to be run at some point in the future. When
+ * an alarm goes off, the {@link Intent} that had been registered for it
+ * is broadcast by the system, automatically starting the target application
+ * if it is not already running. Registered alarms are retained while the
+ * device is asleep (and can optionally wake the device up if they go off
+ * during that time), but will be cleared if it is turned off and rebooted.
+ *
+ * <p><b>Note: The Alarm Manager is intended for cases where you want to have
+ * your application code run at a specific time, even if your application is
+ * not currently running. For normal timing operations (ticks, timeouts,
+ * etc) it is easier and much more efficient to use
+ * {@link android.os.Handler}.</b>
+ *
+ * <p>You do not
+ * instantiate this class directly; instead, retrieve it through
+ * {@link android.content.Context#getSystemService
+ * Context.getSystemService(Context.ALARM_SERVICE)}.
+ */
+public class AlarmManager
+{
+ /**
+ * Alarm time in {@link System#currentTimeMillis System.currentTimeMillis()}
+ * (wall clock time in UTC), which will wake up the device when
+ * it goes off.
+ */
+ public static final int RTC_WAKEUP = 0;
+ /**
+ * Alarm time in {@link System#currentTimeMillis System.currentTimeMillis()}
+ * (wall clock time in UTC). This alarm does not wake the
+ * device up; if it goes off while the device is asleep, it will not be
+ * delivered until the next time the device wakes up.
+ */
+ public static final int RTC = 1;
+ /**
+ * Alarm time in {@link android.os.SystemClock#elapsedRealtime
+ * SystemClock.elapsedRealtime()} (time since boot, including sleep),
+ * which will wake up the device when it goes off.
+ */
+ public static final int ELAPSED_REALTIME_WAKEUP = 2;
+ /**
+ * Alarm time in {@link android.os.SystemClock#elapsedRealtime
+ * SystemClock.elapsedRealtime()} (time since boot, including sleep).
+ * This alarm does not wake the device up; if it goes off while the device
+ * is asleep, it will not be delivered until the next time the device
+ * wakes up.
+ */
+ public static final int ELAPSED_REALTIME = 3;
+
+ private static IAlarmManager mService;
+
+ static {
+ mService = IAlarmManager.Stub.asInterface(
+ ServiceManager.getService(Context.ALARM_SERVICE));
+ }
+
+ /**
+ * package private on purpose
+ */
+ AlarmManager() {
+ }
+
+ /**
+ * Schedule an alarm. <b>Note: for timing operations (ticks, timeouts,
+ * etc) it is easier and much more efficient to use
+ * {@link android.os.Handler}.</b> If there is already an alarm scheduled
+ * for the same IntentSender, it will first be canceled.
+ *
+ * <p>If the time occurs in the past, the alarm will be triggered
+ * immediately. If there is already an alarm for this Intent
+ * scheduled (with the equality of two intents being defined by
+ * {@link Intent#filterEquals}), then it will be removed and replaced by
+ * this one.
+ *
+ * <p>
+ * The alarm is an intent broadcast that goes to an intent receiver that
+ * you registered with {@link android.content.Context#registerReceiver}
+ * or through the &lt;receiver&gt; tag in an AndroidManifest.xml file.
+ *
+ * <p>
+ * Alarm intents are delivered with a data extra of type int called
+ * {@link Intent#EXTRA_ALARM_COUNT Intent.EXTRA_ALARM_COUNT} that indicates
+ * how many past alarm events have been accumulated into this intent
+ * broadcast. Recurring alarms that have gone undelivered because the
+ * phone was asleep may have a count greater than one when delivered.
+ *
+ * @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 operation Action to perform when the alarm goes off;
+ * typically comes from {@link PendingIntent#getBroadcast
+ * IntentSender.getBroadcast()}.
+ *
+ * @see android.os.Handler
+ * @see #setRepeating
+ * @see #cancel
+ * @see android.content.Context#sendBroadcast
+ * @see android.content.Context#registerReceiver
+ * @see android.content.Intent#filterEquals
+ * @see #ELAPSED_REALTIME
+ * @see #ELAPSED_REALTIME_WAKEUP
+ * @see #RTC
+ * @see #RTC_WAKEUP
+ */
+ public void set(int type, long triggerAtTime, PendingIntent operation) {
+ try {
+ mService.set(type, triggerAtTime, operation);
+ } catch (RemoteException ex) {
+ }
+ }
+
+ /**
+ * Schedule a repeating alarm. <b>Note: for timing operations (ticks,
+ * timeouts, etc) it is easier and much more efficient to use
+ * {@link android.os.Handler}.</b> If there is already an alarm scheduled
+ * for the same IntentSender, it will first be canceled.
+ *
+ * <p>Like {@link #set}, except you can also
+ * supply a rate at which the alarm will repeat. This alarm continues
+ * repeating until explicitly removed with {@link #cancel}. If the time
+ * occurs in the past, the alarm will be triggered immediately, with an
+ * alarm count depending on how far in the past the trigger time is relative
+ * to the repeat interval.
+ *
+ * <p>If an alarm is delayed (by system sleep, for example, for non
+ * _WAKEUP alarm types), a skipped repeat will be delivered as soon as
+ * possible. After that, future alarms will be delivered according to the
+ * original schedule; they do not drift over time. For example, if you have
+ * set a recurring alarm for the top of every hour but the phone was asleep
+ * from 7:45 until 8:45, an alarm will be sent as soon as the phone awakens,
+ * then the next alarm will be sent at 9:00.
+ *
+ * <p>If your application wants to allow the delivery times to drift in
+ * order to guarantee that at least a certain time interval always elapses
+ * between alarms, then the approach to take is to use one-time alarms,
+ * scheduling the next one yourself when handling each alarm delivery.
+ *
+ * @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 operation Action to perform when the alarm goes off;
+ * typically comes from {@link PendingIntent#getBroadcast
+ * IntentSender.getBroadcast()}.
+ *
+ * @see android.os.Handler
+ * @see #set
+ * @see #cancel
+ * @see android.content.Context#sendBroadcast
+ * @see android.content.Context#registerReceiver
+ * @see android.content.Intent#filterEquals
+ * @see #ELAPSED_REALTIME
+ * @see #ELAPSED_REALTIME_WAKEUP
+ * @see #RTC
+ * @see #RTC_WAKEUP
+ */
+ public void setRepeating(int type, long triggerAtTime, long interval,
+ PendingIntent operation) {
+ try {
+ mService.setRepeating(type, triggerAtTime, interval, operation);
+ } catch (RemoteException ex) {
+ }
+ }
+
+ /**
+ * Remove any alarms with a matching {@link Intent}.
+ * Any alarm, of any type, whose Intent matches this one (as defined by
+ * {@link Intent#filterEquals}), will be canceled.
+ *
+ * @param operation IntentSender which matches a previously added
+ * IntentSender.
+ *
+ * @see #set
+ */
+ public void cancel(PendingIntent operation) {
+ try {
+ mService.remove(operation);
+ } catch (RemoteException ex) {
+ }
+ }
+
+ public void setTimeZone(String timeZone) {
+ try {
+ mService.setTimeZone(timeZone);
+ } catch (RemoteException ex) {
+ }
+ }
+}
diff --git a/core/java/android/app/AlertDialog.java b/core/java/android/app/AlertDialog.java
new file mode 100644
index 0000000..cc80ba4
--- /dev/null
+++ b/core/java/android/app/AlertDialog.java
@@ -0,0 +1,598 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.content.Context;
+import android.content.DialogInterface;
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.Message;
+import android.view.KeyEvent;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+
+import com.android.internal.app.AlertController;
+
+/**
+ * A subclass of Dialog that can display one, two or three buttons. If you only want to
+ * display a String in this dialog box, use the setMessage() method. If you
+ * want to display a more complex view, look up the FrameLayout called "body"
+ * and add your view to it:
+ *
+ * <pre>
+ * FrameLayout fl = (FrameLayout) findViewById(R.id.body);
+ * fl.add(myView, new LayoutParams(FILL_PARENT, WRAP_CONTENT));
+ * </pre>
+ */
+public class AlertDialog extends Dialog implements DialogInterface {
+ private AlertController mAlert;
+
+ protected AlertDialog(Context context) {
+ this(context, com.android.internal.R.style.Theme_Dialog_Alert);
+ }
+
+ protected AlertDialog(Context context, int theme) {
+ super(context, theme);
+ mAlert = new AlertController(context, this, getWindow());
+ }
+
+ protected AlertDialog(Context context, boolean cancelable, OnCancelListener cancelListener) {
+ super(context, com.android.internal.R.style.Theme_Dialog_Alert);
+ setCancelable(cancelable);
+ setOnCancelListener(cancelListener);
+ mAlert = new AlertController(context, this, getWindow());
+ }
+
+ @Override
+ public void setTitle(CharSequence title) {
+ super.setTitle(title);
+ mAlert.setTitle(title);
+ }
+
+ /**
+ * @see Builder#setCustomTitle(View)
+ */
+ public void setCustomTitle(View customTitleView) {
+ mAlert.setCustomTitle(customTitleView);
+ }
+
+ public void setMessage(CharSequence message) {
+ mAlert.setMessage(message);
+ }
+
+ /**
+ * Set the view to display in that dialog.
+ */
+ public void setView(View view) {
+ mAlert.setView(view);
+ }
+
+ public void setButton(CharSequence text, Message msg) {
+ mAlert.setButton(text, msg);
+ }
+
+ public void setButton2(CharSequence text, Message msg) {
+ mAlert.setButton2(text, msg);
+ }
+
+ public void setButton3(CharSequence text, Message msg) {
+ mAlert.setButton3(text, msg);
+ }
+
+ /**
+ * Set a listener to be invoked when button 1 of the dialog is pressed.
+ * @param text The text to display in button 1.
+ * @param listener The {@link DialogInterface.OnClickListener} to use.
+ */
+ public void setButton(CharSequence text, final OnClickListener listener) {
+ mAlert.setButton(text, listener);
+ }
+
+ /**
+ * Set a listener to be invoked when button 2 of the dialog is pressed.
+ * @param text The text to display in button 2.
+ * @param listener The {@link DialogInterface.OnClickListener} to use.
+ */
+ public void setButton2(CharSequence text, final OnClickListener listener) {
+ mAlert.setButton2(text, listener);
+ }
+
+ /**
+ * Set a listener to be invoked when button 3 of the dialog is pressed.
+ * @param text The text to display in button 3.
+ * @param listener The {@link DialogInterface.OnClickListener} to use.
+ */
+ public void setButton3(CharSequence text, final OnClickListener listener) {
+ mAlert.setButton3(text, listener);
+ }
+
+ /**
+ * Set resId to 0 if you don't want an icon.
+ * @param resId the resourceId of the drawable to use as the icon or 0
+ * if you don't want an icon.
+ */
+ public void setIcon(int resId) {
+ mAlert.setIcon(resId);
+ }
+
+ public void setIcon(Drawable icon) {
+ mAlert.setIcon(icon);
+ }
+
+ public void setInverseBackgroundForced(boolean forceInverseBackground) {
+ mAlert.setInverseBackgroundForced(forceInverseBackground);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mAlert.installContent();
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (mAlert.onKeyDown(keyCode, event)) return true;
+ return super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ if (mAlert.onKeyUp(keyCode, event)) return true;
+ return super.onKeyUp(keyCode, event);
+ }
+
+ public static class Builder {
+ private final AlertController.AlertParams P;
+
+ /**
+ * Constructor using a context for this builder and the {@link AlertDialog} it creates.
+ */
+ public Builder(Context context) {
+ P = new AlertController.AlertParams(context);
+ }
+
+ /**
+ * Set the title using the given resource id.
+ */
+ public Builder setTitle(int titleId) {
+ P.mTitle = P.mContext.getText(titleId);
+ return this;
+ }
+
+ /**
+ * Set the title displayed in the {@link Dialog}.
+ */
+ public Builder setTitle(CharSequence title) {
+ P.mTitle = title;
+ return this;
+ }
+
+ /**
+ * Set the title using the custom view {@code customTitleView}. The
+ * methods {@link #setTitle(int)} and {@link #setIcon(int)} should be
+ * sufficient for most titles, but this is provided if the title needs
+ * more customization. Using this will replace the title and icon set
+ * via the other methods.
+ *
+ * @param customTitleView The custom view to use as the title.
+ */
+ public Builder setCustomTitle(View customTitleView) {
+ P.mCustomTitleView = customTitleView;
+ return this;
+ }
+
+ /**
+ * Set the message to display using the given resource id.
+ */
+ public Builder setMessage(int messageId) {
+ P.mMessage = P.mContext.getText(messageId);
+ return this;
+ }
+
+ /**
+ * Set the message to display.
+ */
+ public Builder setMessage(CharSequence message) {
+ P.mMessage = message;
+ return this;
+ }
+
+ /**
+ * Set the resource id of the {@link Drawable} to be used in the title.
+ */
+ public Builder setIcon(int iconId) {
+ P.mIconId = iconId;
+ return this;
+ }
+
+ /**
+ * Set the {@link Drawable} to be used in the title.
+ */
+ public Builder setIcon(Drawable icon) {
+ P.mIcon = icon;
+ return this;
+ }
+
+ /**
+ * Set a listener to be invoked when the positive button of the dialog is pressed.
+ * @param textId The resource id of the text to display in the positive button
+ * @param listener The {@link DialogInterface.OnClickListener} to use.
+ */
+ public Builder setPositiveButton(int textId, final OnClickListener listener) {
+ P.mPositiveButtonText = P.mContext.getText(textId);
+ P.mPositiveButtonListener = listener;
+ return this;
+ }
+
+ /**
+ * Set a listener to be invoked when the positive button of the dialog is pressed.
+ * @param text The text to display in the positive button
+ * @param listener The {@link DialogInterface.OnClickListener} to use.
+ */
+ public Builder setPositiveButton(CharSequence text, final OnClickListener listener) {
+ P.mPositiveButtonText = text;
+ P.mPositiveButtonListener = listener;
+ return this;
+ }
+
+ /**
+ * Set a listener to be invoked when the negative button of the dialog is pressed.
+ * @param textId The resource id of the text to display in the negative button
+ * @param listener The {@link DialogInterface.OnClickListener} to use.
+ */
+ public Builder setNegativeButton(int textId, final OnClickListener listener) {
+ P.mNegativeButtonText = P.mContext.getText(textId);
+ P.mNegativeButtonListener = listener;
+ return this;
+ }
+
+ /**
+ * Set a listener to be invoked when the negative button of the dialog is pressed.
+ * @param text The text to display in the negative button
+ * @param listener The {@link DialogInterface.OnClickListener} to use.
+ */
+ public Builder setNegativeButton(CharSequence text, final OnClickListener listener) {
+ P.mNegativeButtonText = text;
+ P.mNegativeButtonListener = listener;
+ return this;
+ }
+
+ /**
+ * Set a listener to be invoked when the neutral button of the dialog is pressed.
+ * @param textId The resource id of the text to display in the neutral button
+ * @param listener The {@link DialogInterface.OnClickListener} to use.
+ */
+ public Builder setNeutralButton(int textId, final OnClickListener listener) {
+ P.mNeutralButtonText = P.mContext.getText(textId);
+ P.mNeutralButtonListener = listener;
+ return this;
+ }
+
+ /**
+ * Set a listener to be invoked when the neutral button of the dialog is pressed.
+ * @param text The text to display in the neutral button
+ * @param listener The {@link DialogInterface.OnClickListener} to use.
+ */
+ public Builder setNeutralButton(CharSequence text, final OnClickListener listener) {
+ P.mNeutralButtonText = text;
+ P.mNeutralButtonListener = listener;
+ return this;
+ }
+
+ /**
+ * Sets whether the dialog is cancelable or not default is true.
+ */
+ public Builder setCancelable(boolean cancelable) {
+ P.mCancelable = cancelable;
+ return this;
+ }
+
+ /**
+ * Sets the callback that will be called if the dialog is canceled.
+ * @see #setCancelable(boolean)
+ */
+ public Builder setOnCancelListener(OnCancelListener onCancelListener) {
+ P.mOnCancelListener = onCancelListener;
+ return this;
+ }
+
+ /**
+ * Sets the callback that will be called if a key is dispatched to the dialog.
+ */
+ public Builder setOnKeyListener(OnKeyListener onKeyListener) {
+ P.mOnKeyListener = onKeyListener;
+ return this;
+ }
+
+ /**
+ * Set a list of items to be displayed in the dialog as the content, you will be notified of the
+ * selected item via the supplied listener. This should be an array type i.e. R.array.foo
+ */
+ public Builder setItems(int itemsId, final OnClickListener listener) {
+ P.mItems = P.mContext.getResources().getTextArray(itemsId);
+ P.mOnClickListener = listener;
+ return this;
+ }
+
+ /**
+ * Set a list of items to be displayed in the dialog as the content, you will be notified of the
+ * selected item via the supplied listener.
+ */
+ public Builder setItems(CharSequence[] items, final OnClickListener listener) {
+ P.mItems = items;
+ P.mOnClickListener = listener;
+ return this;
+ }
+
+ /**
+ * Set a list of items, which are supplied by the given {@link ListAdapter}, to be
+ * displayed in the dialog as the content, you will be notified of the
+ * selected item via the supplied listener.
+ *
+ * @param adapter The {@link ListAdapter} to supply the list of items
+ * @param listener The listener that will be called when an item is clicked.
+ */
+ public Builder setAdapter(final ListAdapter adapter, final OnClickListener listener) {
+ P.mAdapter = adapter;
+ P.mOnClickListener = listener;
+ return this;
+ }
+
+ /**
+ * Set a list of items, which are supplied by the given {@link Cursor}, to be
+ * displayed in the dialog as the content, you will be notified of the
+ * selected item via the supplied listener.
+ *
+ * @param cursor The {@link Cursor} to supply the list of items
+ * @param listener The listener that will be called when an item is clicked.
+ * @param labelColumn The column name on the cursor containing the string to display
+ * in the label.
+ */
+ public Builder setCursor(final Cursor cursor, final OnClickListener listener,
+ String labelColumn) {
+ P.mCursor = cursor;
+ P.mLabelColumn = labelColumn;
+ P.mOnClickListener = listener;
+ return this;
+ }
+
+ /**
+ * Set a list of items to be displayed in the dialog as the content,
+ * you will be notified of the selected item via the supplied listener.
+ * This should be an array type, e.g. R.array.foo. The list will have
+ * a check mark displayed to the right of the text for each checked
+ * item. Clicking on an item in the list will not dismiss the dialog.
+ * Clicking on a button will dismiss the dialog.
+ *
+ * @param itemsId the resource id of an array i.e. R.array.foo
+ * @param checkedItems specifies which items are checked. It should be null in which case no
+ * items are checked. If non null it must be exactly the same length as the array of
+ * items.
+ * @param listener notified when an item on the list is clicked. The dialog will not be
+ * dismissed when an item is clicked. It will only be dismissed if clicked on a
+ * button, if no buttons are supplied it's up to the user to dismiss the dialog.
+ */
+ public Builder setMultiChoiceItems(int itemsId, boolean[] checkedItems,
+ final OnMultiChoiceClickListener listener) {
+ P.mItems = P.mContext.getResources().getTextArray(itemsId);
+ P.mOnCheckboxClickListener = listener;
+ P.mCheckedItems = checkedItems;
+ P.mIsMultiChoice = true;
+ return this;
+ }
+
+ /**
+ * Set a list of items to be displayed in the dialog as the content,
+ * you will be notified of the selected item via the supplied listener.
+ * The list will have a check mark displayed to the right of the text
+ * for each checked item. Clicking on an item in the list will not
+ * dismiss the dialog. Clicking on a button will dismiss the dialog.
+ *
+ * @param items the text of the items to be displayed in the list.
+ * @param checkedItems specifies which items are checked. It should be null in which case no
+ * items are checked. If non null it must be exactly the same length as the array of
+ * items.
+ * @param listener notified when an item on the list is clicked. The dialog will not be
+ * dismissed when an item is clicked. It will only be dismissed if clicked on a
+ * button, if no buttons are supplied it's up to the user to dismiss the dialog.
+ */
+ public Builder setMultiChoiceItems(CharSequence[] items, boolean[] checkedItems,
+ final OnMultiChoiceClickListener listener) {
+ P.mItems = items;
+ P.mOnCheckboxClickListener = listener;
+ P.mCheckedItems = checkedItems;
+ P.mIsMultiChoice = true;
+ return this;
+ }
+
+ /**
+ * Set a list of items to be displayed in the dialog as the content,
+ * you will be notified of the selected item via the supplied listener.
+ * The list will have a check mark displayed to the right of the text
+ * for each checked item. Clicking on an item in the list will not
+ * dismiss the dialog. Clicking on a button will dismiss the dialog.
+ *
+ * @param cursor the cursor used to provide the items.
+ * @param isCheckedColumn specifies the column name on the cursor to use to determine
+ * whether a checkbox is checked or not. It must return an integer value where 1
+ * means checked and 0 means unchecked.
+ * @param labelColumn The column name on the cursor containing the string to display in the
+ * label.
+ * @param listener notified when an item on the list is clicked. The dialog will not be
+ * dismissed when an item is clicked. It will only be dismissed if clicked on a
+ * button, if no buttons are supplied it's up to the user to dismiss the dialog.
+ */
+ public Builder setMultiChoiceItems(Cursor cursor, String isCheckedColumn, String labelColumn,
+ final OnMultiChoiceClickListener listener) {
+ P.mCursor = cursor;
+ P.mOnCheckboxClickListener = listener;
+ P.mIsCheckedColumn = isCheckedColumn;
+ P.mLabelColumn = labelColumn;
+ P.mIsMultiChoice = true;
+ return this;
+ }
+
+ /**
+ * Set a list of items to be displayed in the dialog as the content, you will be notified of
+ * the selected item via the supplied listener. This should be an array type i.e.
+ * R.array.foo The list will have a check mark displayed to the right of the text for the
+ * checked item. Clicking on an item in the list will not dismiss the dialog. Clicking on a
+ * button will dismiss the dialog.
+ *
+ * @param itemsId the resource id of an array i.e. R.array.foo
+ * @param checkedItem specifies which item is checked. If -1 no items are checked.
+ * @param listener notified when an item on the list is clicked. The dialog will not be
+ * dismissed when an item is clicked. It will only be dismissed if clicked on a
+ * button, if no buttons are supplied it's up to the user to dismiss the dialog.
+ */
+ public Builder setSingleChoiceItems(int itemsId, int checkedItem,
+ final OnClickListener listener) {
+ P.mItems = P.mContext.getResources().getTextArray(itemsId);
+ P.mOnClickListener = listener;
+ P.mCheckedItem = checkedItem;
+ P.mIsSingleChoice = true;
+ return this;
+ }
+
+ /**
+ * Set a list of items to be displayed in the dialog as the content, you will be notified of
+ * the selected item via the supplied listener. The list will have a check mark displayed to
+ * the right of the text for the checked item. Clicking on an item in the list will not
+ * dismiss the dialog. Clicking on a button will dismiss the dialog.
+ *
+ * @param cursor the cursor to retrieve the items from.
+ * @param checkedItem specifies which item is checked. If -1 no items are checked.
+ * @param labelColumn The column name on the cursor containing the string to display in the
+ * label.
+ * @param listener notified when an item on the list is clicked. The dialog will not be
+ * dismissed when an item is clicked. It will only be dismissed if clicked on a
+ * button, if no buttons are supplied it's up to the user to dismiss the dialog.
+ */
+ public Builder setSingleChoiceItems(Cursor cursor, int checkedItem, String labelColumn,
+ final OnClickListener listener) {
+ P.mCursor = cursor;
+ P.mOnClickListener = listener;
+ P.mCheckedItem = checkedItem;
+ P.mLabelColumn = labelColumn;
+ P.mIsSingleChoice = true;
+ return this;
+ }
+
+ /**
+ * Set a list of items to be displayed in the dialog as the content, you will be notified of
+ * the selected item via the supplied listener. The list will have a check mark displayed to
+ * the right of the text for the checked item. Clicking on an item in the list will not
+ * dismiss the dialog. Clicking on a button will dismiss the dialog.
+ *
+ * @param items the items to be displayed.
+ * @param checkedItem specifies which item is checked. If -1 no items are checked.
+ * @param listener notified when an item on the list is clicked. The dialog will not be
+ * dismissed when an item is clicked. It will only be dismissed if clicked on a
+ * button, if no buttons are supplied it's up to the user to dismiss the dialog.
+ */
+ public Builder setSingleChoiceItems(CharSequence[] items, int checkedItem, final OnClickListener listener) {
+ P.mItems = items;
+ P.mOnClickListener = listener;
+ P.mCheckedItem = checkedItem;
+ P.mIsSingleChoice = true;
+ return this;
+ }
+
+ /**
+ * Set a list of items to be displayed in the dialog as the content, you will be notified of
+ * the selected item via the supplied listener. The list will have a check mark displayed to
+ * the right of the text for the checked item. Clicking on an item in the list will not
+ * dismiss the dialog. Clicking on a button will dismiss the dialog.
+ *
+ * @param adapter The {@link ListAdapter} to supply the list of items
+ * @param checkedItem specifies which item is checked. If -1 no items are checked.
+ * @param listener notified when an item on the list is clicked. The dialog will not be
+ * dismissed when an item is clicked. It will only be dismissed if clicked on a
+ * button, if no buttons are supplied it's up to the user to dismiss the dialog.
+ */
+ public Builder setSingleChoiceItems(ListAdapter adapter, int checkedItem, final OnClickListener listener) {
+ P.mAdapter = adapter;
+ P.mOnClickListener = listener;
+ P.mCheckedItem = checkedItem;
+ P.mIsSingleChoice = true;
+ return this;
+ }
+
+ /**
+ * Sets a listener to be invoked when an item in the list is selected.
+ *
+ * @param listener The listener to be invoked.
+ * @see AdapterView#setOnItemSelectedListener(android.widget.AdapterView.OnItemSelectedListener)
+ */
+ public Builder setOnItemSelectedListener(final AdapterView.OnItemSelectedListener listener) {
+ P.mOnItemSelectedListener = listener;
+ return this;
+ }
+
+ /**
+ * Set a custom view to be the contents of the Dialog. If the supplied view is an instance
+ * of a {@link ListView} the light background will be used.
+ */
+ public Builder setView(View view) {
+ P.mView = view;
+ return this;
+ }
+
+ /**
+ * Sets the Dialog to use the inverse background, regardless of what the
+ * contents is.
+ *
+ * @param useInverseBackground Whether to use the inverse background
+ * @return This Builder object to allow for chaining of sets.
+ */
+ public Builder setInverseBackgroundForced(boolean useInverseBackground) {
+ P.mForceInverseBackground = useInverseBackground;
+ return this;
+ }
+
+ /**
+ * Creates a {@link AlertDialog} with the arguments supplied to this builder. It does not
+ * {@link Dialog#show()} the dialog. This allows the user to do any extra processing
+ * before displaying the dialog. Use {@link #show()} if you don't have any other processing
+ * to do and want this to be created and displayed.
+ */
+ public AlertDialog create() {
+ final AlertDialog dialog = new AlertDialog(P.mContext);
+ P.apply(dialog.mAlert);
+ dialog.setCancelable(P.mCancelable);
+ dialog.setOnCancelListener(P.mOnCancelListener);
+ if (P.mOnKeyListener != null) {
+ dialog.setOnKeyListener(P.mOnKeyListener);
+ }
+ return dialog;
+ }
+
+ /**
+ * Creates a {@link AlertDialog} with the arguments supplied to this builder and
+ * {@link Dialog#show()}'s the dialog.
+ */
+ public AlertDialog show() {
+ AlertDialog dialog = create();
+ dialog.show();
+ return dialog;
+ }
+ }
+
+}
diff --git a/core/java/android/app/AliasActivity.java b/core/java/android/app/AliasActivity.java
new file mode 100644
index 0000000..4f91e02
--- /dev/null
+++ b/core/java/android/app/AliasActivity.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.res.XmlResourceParser;
+import android.os.Bundle;
+import android.util.AttributeSet;
+import android.util.Xml;
+import com.android.internal.util.XmlUtils;
+
+import java.io.IOException;
+
+/**
+ * Stub activity that launches another activity (and then finishes itself)
+ * based on information in its component's manifest meta-data. This is a
+ * simple way to implement an alias-like mechanism.
+ *
+ * To use this activity, you should include in the manifest for the associated
+ * component an entry named "android.app.alias". It is a reference to an XML
+ * resource describing an intent that launches the real application.
+ */
+public class AliasActivity extends Activity {
+ /**
+ * This is the name under which you should store in your component the
+ * meta-data information about the alias. It is a reference to an XML
+ * resource describing an intent that launches the real application.
+ * {@hide}
+ */
+ public final String ALIAS_META_DATA = "android.app.alias";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ XmlResourceParser parser = null;
+ try {
+ ActivityInfo ai = getPackageManager().getActivityInfo(
+ getComponentName(), PackageManager.GET_META_DATA);
+ parser = ai.loadXmlMetaData(getPackageManager(),
+ ALIAS_META_DATA);
+ if (parser == null) {
+ throw new RuntimeException("Alias requires a meta-data field "
+ + ALIAS_META_DATA);
+ }
+
+ Intent intent = parseAlias(parser);
+ if (intent == null) {
+ throw new RuntimeException(
+ "No <intent> tag found in alias description");
+ }
+
+ startActivity(intent);
+ finish();
+
+ } catch (PackageManager.NameNotFoundException e) {
+ throw new RuntimeException("Error parsing alias", e);
+ } catch (XmlPullParserException e) {
+ throw new RuntimeException("Error parsing alias", e);
+ } catch (IOException e) {
+ throw new RuntimeException("Error parsing alias", e);
+ } finally {
+ if (parser != null) parser.close();
+ }
+ }
+
+ private Intent parseAlias(XmlPullParser parser)
+ throws XmlPullParserException, IOException {
+ AttributeSet attrs = Xml.asAttributeSet(parser);
+
+ Intent intent = null;
+
+ int type;
+ while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
+ && type != XmlPullParser.START_TAG) {
+ }
+
+ String nodeName = parser.getName();
+ if (!"alias".equals(nodeName)) {
+ throw new RuntimeException(
+ "Alias meta-data must start with <alias> tag; found"
+ + nodeName + " at " + parser.getPositionDescription());
+ }
+
+ int outerDepth = parser.getDepth();
+ while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
+ && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+ if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+ continue;
+ }
+
+ nodeName = parser.getName();
+ if ("intent".equals(nodeName)) {
+ Intent gotIntent = Intent.parseIntent(getResources(), parser, attrs);
+ if (intent == null) intent = gotIntent;
+ } else {
+ XmlUtils.skipCurrentTag(parser);
+ }
+ }
+
+ return intent;
+ }
+
+}
diff --git a/core/java/android/app/Application.java b/core/java/android/app/Application.java
new file mode 100644
index 0000000..45ce860
--- /dev/null
+++ b/core/java/android/app/Application.java
@@ -0,0 +1,74 @@
+/*
+ * 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.app;
+
+import android.content.ComponentCallbacks;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.res.Configuration;
+
+/**
+ * Base class for those who need to maintain global application state. You can
+ * provide your own implementation by specifying its name in your
+ * AndroidManifest.xml's &lt;application&gt; tag, which will cause that class
+ * to be instantiated for you when the process for your application/package is
+ * created.
+ */
+public class Application extends ContextWrapper implements ComponentCallbacks {
+
+ public Application() {
+ super(null);
+ }
+
+ /**
+ * Called when the application is starting, before any other application
+ * objects have been created. Implementations should be as quick as
+ * possible (for example using lazy initialization of state) since the time
+ * spent in this function directly impacts the performance of starting the
+ * first activity, service, or receiver in a process.
+ * If you override this method, be sure to call super.onCreate().
+ */
+ public void onCreate() {
+ }
+
+ /**
+ * Called when the application is stopping. There are no more application
+ * objects running and the process will exit. <em>Note: never depend on
+ * this method being called; in many cases an unneeded application process
+ * will simply be killed by the kernel without executing any application
+ * code.</em>
+ * If you override this method, be sure to call super.onTerminate().
+ */
+ public void onTerminate() {
+ }
+
+ public void onConfigurationChanged(Configuration newConfig) {
+ }
+
+ public void onLowMemory() {
+ }
+
+ // ------------------ Internal API ------------------
+
+ /**
+ * @hide
+ */
+ /* package */ final void attach(Context context) {
+ attachBaseContext(context);
+ }
+
+}
diff --git a/core/java/android/app/ApplicationContext.java b/core/java/android/app/ApplicationContext.java
new file mode 100644
index 0000000..342ffcf
--- /dev/null
+++ b/core/java/android/app/ApplicationContext.java
@@ -0,0 +1,2596 @@
+/*
+ * 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.app;
+
+import com.google.android.collect.Maps;
+import com.android.internal.util.XmlUtils;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.IBluetoothDevice;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.IContentProvider;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ReceiverCallNotAllowedException;
+import android.content.ServiceConnection;
+import android.content.SharedPreferences;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.ComponentInfo;
+import android.content.pm.IPackageDataObserver;
+import android.content.pm.IPackageDeleteObserver;
+import android.content.pm.IPackageStatsObserver;
+import android.content.pm.IPackageInstallObserver;
+import android.content.pm.IPackageManager;
+import android.content.pm.InstrumentationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PermissionGroupInfo;
+import android.content.pm.PermissionInfo;
+import android.content.pm.ProviderInfo;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.content.res.AssetManager;
+import android.content.res.Resources;
+import android.content.res.XmlResourceParser;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteDatabase.CursorFactory;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.hardware.SensorManager;
+import android.location.ILocationManager;
+import android.location.LocationManager;
+import android.media.AudioManager;
+import android.net.ConnectivityManager;
+import android.net.IConnectivityManager;
+import android.net.Uri;
+import android.net.wifi.IWifiManager;
+import android.net.wifi.WifiManager;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.FileUtils;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.IPowerManager;
+import android.os.ParcelFileDescriptor;
+import android.os.PowerManager;
+import android.os.Process;
+import android.os.ServiceManager;
+import android.os.Vibrator;
+import android.os.FileUtils.FileStatus;
+import android.telephony.TelephonyManager;
+import android.text.ClipboardManager;
+import android.util.AndroidRuntimeException;
+import android.util.Log;
+import android.view.ContextThemeWrapper;
+import android.view.LayoutInflater;
+import android.view.WindowManagerImpl;
+
+import com.android.internal.policy.PolicyManager;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+class ReceiverRestrictedContext extends ContextWrapper {
+ ReceiverRestrictedContext(Context base) {
+ super(base);
+ }
+
+ @Override
+ public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
+ return registerReceiver(receiver, filter, null, null);
+ }
+
+ @Override
+ public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter,
+ String broadcastPermission, Handler scheduler) {
+ throw new ReceiverCallNotAllowedException(
+ "IntentReceiver components are not allowed to register to receive intents");
+ //ex.fillInStackTrace();
+ //Log.e("IntentReceiver", ex.getMessage(), ex);
+ //return mContext.registerReceiver(receiver, filter, broadcastPermission,
+ // scheduler);
+ }
+
+ @Override
+ public boolean bindService(Intent service, ServiceConnection conn, int flags) {
+ throw new ReceiverCallNotAllowedException(
+ "IntentReceiver components are not allowed to bind to services");
+ //ex.fillInStackTrace();
+ //Log.e("IntentReceiver", ex.getMessage(), ex);
+ //return mContext.bindService(service, interfaceName, conn, flags);
+ }
+}
+
+/**
+ * Common implementation of Context API, which Activity and other application
+ * classes inherit.
+ */
+@SuppressWarnings({"EmptyCatchBlock"})
+class ApplicationContext extends Context {
+ private final static String TAG = "ApplicationContext";
+ private final static boolean DEBUG_ICONS = false;
+
+ private static final Object sSync = new Object();
+ private static PowerManager sPowerManager;
+ private static ConnectivityManager sConnectivityManager;
+ private static WifiManager sWifiManager;
+ private static LocationManager sLocationManager;
+ private static boolean sIsBluetoothDeviceCached = false;
+ private static BluetoothDevice sBluetoothDevice;
+ private static IWallpaperService sWallpaperService;
+ private static final HashMap<File, SharedPreferencesImpl> sSharedPrefs =
+ new HashMap<File, SharedPreferencesImpl>();
+
+ private AudioManager mAudioManager;
+ /*package*/ ActivityThread.PackageInfo mPackageInfo;
+ private Resources mResources;
+ /*package*/ ActivityThread mMainThread;
+ private Context mOuterContext;
+ private IBinder mActivityToken = null;
+ private ApplicationContentResolver mContentResolver;
+ private int mThemeResource = 0;
+ private Resources.Theme mTheme = null;
+ private PackageManager mPackageManager;
+ private NotificationManager mNotificationManager = null;
+ private ActivityManager mActivityManager = null;
+ private Context mReceiverRestrictedContext = null;
+ private SearchManager mSearchManager = null;
+ private SensorManager mSensorManager = null;
+ private Vibrator mVibrator = null;
+ private LayoutInflater mLayoutInflater = null;
+ private StatusBarManager mStatusBarManager = null;
+ private TelephonyManager mTelephonyManager = null;
+ private ClipboardManager mClipboardManager = null;
+
+ private final Object mSync = new Object();
+
+ private File mDatabasesDir;
+ private File mPreferencesDir;
+ private File mFilesDir;
+
+
+ private File mCacheDir;
+
+ private Drawable mWallpaper;
+ private IWallpaperServiceCallback mWallpaperCallback = null;
+
+ private static long sInstanceCount = 0;
+
+ private static final String[] EMPTY_FILE_LIST = {};
+
+ @Override
+ protected void finalize() throws Throwable {
+ super.finalize();
+ --sInstanceCount;
+ }
+
+ public static long getInstanceCount() {
+ return sInstanceCount;
+ }
+
+ @Override
+ public AssetManager getAssets() {
+ return mResources.getAssets();
+ }
+
+ @Override
+ public Resources getResources() {
+ return mResources;
+ }
+
+ @Override
+ public PackageManager getPackageManager() {
+ if (mPackageManager != null) {
+ return mPackageManager;
+ }
+
+ IPackageManager pm = ActivityThread.getPackageManager();
+ if (pm != null) {
+ // Doesn't matter if we make more than one instance.
+ return (mPackageManager = new ApplicationPackageManager(this, pm));
+ }
+
+ return null;
+ }
+
+ @Override
+ public ContentResolver getContentResolver() {
+ return mContentResolver;
+ }
+
+ @Override
+ public Looper getMainLooper() {
+ return mMainThread.getLooper();
+ }
+
+ @Override
+ public Context getApplicationContext() {
+ return mMainThread.getApplication();
+ }
+
+ @Override
+ public void setTheme(int resid) {
+ mThemeResource = resid;
+ }
+
+ @Override
+ public Resources.Theme getTheme() {
+ if (mTheme == null) {
+ if (mThemeResource == 0) {
+ mThemeResource = com.android.internal.R.style.Theme;
+ }
+ mTheme = mResources.newTheme();
+ mTheme.applyStyle(mThemeResource, true);
+ }
+ return mTheme;
+ }
+
+ @Override
+ public ClassLoader getClassLoader() {
+ return mPackageInfo != null ?
+ mPackageInfo.getClassLoader() : ClassLoader.getSystemClassLoader();
+ }
+
+ @Override
+ public String getPackageName() {
+ if (mPackageInfo != null) {
+ return mPackageInfo.getPackageName();
+ }
+ throw new RuntimeException("Not supported in system context");
+ }
+
+ @Override
+ public String getPackageResourcePath() {
+ if (mPackageInfo != null) {
+ return mPackageInfo.getResDir();
+ }
+ throw new RuntimeException("Not supported in system context");
+ }
+
+ @Override
+ public String getPackageCodePath() {
+ if (mPackageInfo != null) {
+ return mPackageInfo.getAppDir();
+ }
+ throw new RuntimeException("Not supported in system context");
+ }
+
+ @Override
+ public SharedPreferences getSharedPreferences(String name, int mode) {
+ File f;
+ f = makeFilename(getPreferencesDir(), name + ".xml");
+ SharedPreferencesImpl sp;
+ synchronized (sSharedPrefs) {
+ sp = sSharedPrefs.get(f);
+ if (sp != null && !sp.hasFileChanged()) {
+ //Log.i(TAG, "Returning existing prefs " + name + ": " + sp);
+ return sp;
+ }
+ }
+
+ Map map = null;
+ try {
+ FileInputStream str = new FileInputStream(f);
+ map = XmlUtils.readMapXml(str);
+ str.close();
+ } catch (org.xmlpull.v1.XmlPullParserException e) {
+ } catch (java.io.FileNotFoundException e) {
+ } catch (java.io.IOException e) {
+ }
+
+ synchronized (sSharedPrefs) {
+ if (sp != null) {
+ //Log.i(TAG, "Updating existing prefs " + name + " " + sp + ": " + map);
+ sp.replace(map);
+ } else {
+ sp = sSharedPrefs.get(f);
+ if (sp == null) {
+ sp = new SharedPreferencesImpl(f, mode, map);
+ sSharedPrefs.put(f, sp);
+ }
+ }
+ return sp;
+ }
+ }
+
+ private File getPreferencesDir() {
+ synchronized (mSync) {
+ if (mPreferencesDir == null) {
+ mPreferencesDir = new File(getDataDirFile(), "shared_prefs");
+ }
+ return mPreferencesDir;
+ }
+ }
+
+ @Override
+ public FileInputStream openFileInput(String name)
+ throws FileNotFoundException {
+ File f = makeFilename(getFilesDir(), name);
+ return new FileInputStream(f);
+ }
+
+ @Override
+ public FileOutputStream openFileOutput(String name, int mode)
+ throws FileNotFoundException {
+ final boolean append = (mode&MODE_APPEND) != 0;
+ File f = makeFilename(getFilesDir(), name);
+ try {
+ FileOutputStream fos = new FileOutputStream(f, append);
+ setFilePermissionsFromMode(f.toString(), mode, 0);
+ return fos;
+ } catch (FileNotFoundException e) {
+ }
+
+ File parent = f.getParentFile();
+ parent.mkdir();
+ FileUtils.setPermissions(
+ parent.toString(),
+ FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH,
+ -1, -1);
+ FileOutputStream fos = new FileOutputStream(f, append);
+ setFilePermissionsFromMode(f.toString(), mode, 0);
+ return fos;
+ }
+
+ @Override
+ public boolean deleteFile(String name) {
+ File f = makeFilename(getFilesDir(), name);
+ return f.delete();
+ }
+
+ @Override
+ public File getFilesDir() {
+ synchronized (mSync) {
+ if (mFilesDir == null) {
+ mFilesDir = new File(getDataDirFile(), "files");
+ }
+ return mFilesDir;
+ }
+ }
+
+ @Override
+ public File getCacheDir() {
+ synchronized (mSync) {
+ if (mCacheDir == null) {
+ mCacheDir = new File(getDataDirFile(), "cache");
+ }
+ if (!mCacheDir.exists()) {
+ if(!mCacheDir.mkdirs()) {
+ Log.w(TAG, "Unable to create cache directory");
+ return null;
+ }
+ FileUtils.setPermissions(
+ mCacheDir.toString(),
+ FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH,
+ -1, -1);
+ }
+ }
+ return mCacheDir;
+ }
+
+
+ @Override
+ public File getFileStreamPath(String name) {
+ return makeFilename(getFilesDir(), name);
+ }
+
+ @Override
+ public String[] fileList() {
+ final String[] list = getFilesDir().list();
+ return (list != null) ? list : EMPTY_FILE_LIST;
+ }
+
+ @Override
+ public SQLiteDatabase openOrCreateDatabase(String name, int mode, CursorFactory factory) {
+ File dir = getDatabasesDir();
+ if (!dir.isDirectory() && dir.mkdir()) {
+ FileUtils.setPermissions(dir.toString(),
+ FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH,
+ -1, -1);
+ }
+
+ File f = makeFilename(dir, name);
+ SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(f, factory);
+ setFilePermissionsFromMode(f.toString(), mode, 0);
+ return db;
+ }
+
+ @Override
+ public boolean deleteDatabase(String name) {
+ try {
+ File f = makeFilename(getDatabasesDir(), name);
+ return f.delete();
+ } catch (Exception e) {
+ }
+ return false;
+ }
+
+ @Override
+ public File getDatabasePath(String name) {
+ return makeFilename(getDatabasesDir(), name);
+ }
+
+ @Override
+ public String[] databaseList() {
+ final String[] list = getDatabasesDir().list();
+ return (list != null) ? list : EMPTY_FILE_LIST;
+ }
+
+
+ private File getDatabasesDir() {
+ synchronized (mSync) {
+ if (mDatabasesDir == null) {
+ mDatabasesDir = new File(getDataDirFile(), "databases");
+ }
+ if (mDatabasesDir.getPath().equals("databases")) {
+ mDatabasesDir = new File("/data/system");
+ }
+ return mDatabasesDir;
+ }
+ }
+
+ @Override
+ public Drawable getWallpaper() {
+ Drawable dr = peekWallpaper();
+ return dr != null ? dr : getResources().getDrawable(
+ com.android.internal.R.drawable.default_wallpaper);
+ }
+
+ @Override
+ public synchronized Drawable peekWallpaper() {
+ if (mWallpaper != null) {
+ return mWallpaper;
+ }
+ mWallpaperCallback = new WallpaperCallback(this);
+ mWallpaper = getCurrentWallpaperLocked();
+ return mWallpaper;
+ }
+
+ private Drawable getCurrentWallpaperLocked() {
+ try {
+ ParcelFileDescriptor fd = getWallpaperService().getWallpaper(mWallpaperCallback);
+ if (fd != null) {
+ Bitmap bm = BitmapFactory.decodeFileDescriptor(fd.getFileDescriptor());
+ if (bm != null) {
+ return new BitmapDrawable(bm);
+ }
+ }
+ } catch (RemoteException e) {
+ }
+ return null;
+ }
+
+ @Override
+ public int getWallpaperDesiredMinimumWidth() {
+ try {
+ return getWallpaperService().getWidthHint();
+ } catch (RemoteException e) {
+ // Shouldn't happen!
+ return 0;
+ }
+ }
+
+ @Override
+ public int getWallpaperDesiredMinimumHeight() {
+ try {
+ return getWallpaperService().getHeightHint();
+ } catch (RemoteException e) {
+ // Shouldn't happen!
+ return 0;
+ }
+ }
+
+ @Override
+ public void setWallpaper(Bitmap bitmap) throws IOException {
+ try {
+ ParcelFileDescriptor fd = getWallpaperService().setWallpaper();
+ if (fd == null) {
+ return;
+ }
+ FileOutputStream fos = null;
+ try {
+ fos = new ParcelFileDescriptor.AutoCloseOutputStream(fd);
+ bitmap.compress(Bitmap.CompressFormat.PNG, 90, fos);
+ } finally {
+ if (fos != null) {
+ fos.close();
+ }
+ }
+ } catch (RemoteException e) {
+ }
+ }
+
+ @Override
+ public void setWallpaper(InputStream data) throws IOException {
+ try {
+ ParcelFileDescriptor fd = getWallpaperService().setWallpaper();
+ if (fd == null) {
+ return;
+ }
+ FileOutputStream fos = null;
+ try {
+ fos = new ParcelFileDescriptor.AutoCloseOutputStream(fd);
+ setWallpaper(data, fos);
+ } finally {
+ if (fos != null) {
+ fos.close();
+ }
+ }
+ } catch (RemoteException e) {
+ }
+ }
+
+ private void setWallpaper(InputStream data, FileOutputStream fos)
+ throws IOException {
+ byte[] buffer = new byte[32768];
+ int amt;
+ while ((amt=data.read(buffer)) > 0) {
+ fos.write(buffer, 0, amt);
+ }
+ }
+
+ @Override
+ public void clearWallpaper() throws IOException {
+ try {
+ /* Set the wallpaper to the default values */
+ ParcelFileDescriptor fd = getWallpaperService().setWallpaper();
+ if (fd != null) {
+ FileOutputStream fos = null;
+ try {
+ fos = new ParcelFileDescriptor.AutoCloseOutputStream(fd);
+ setWallpaper(getResources().openRawResource(
+ com.android.internal.R.drawable.default_wallpaper),
+ fos);
+ } finally {
+ if (fos != null) {
+ fos.close();
+ }
+ }
+ }
+ } catch (RemoteException e) {
+ }
+ }
+
+ @Override
+ public void startActivity(Intent intent) {
+ if ((intent.getFlags()&Intent.FLAG_ACTIVITY_NEW_TASK) == 0) {
+ throw new AndroidRuntimeException(
+ "Calling startActivity() from outside of an Activity "
+ + " context requires the FLAG_ACTIVITY_NEW_TASK flag."
+ + " Is this really what you want?");
+ }
+ mMainThread.getInstrumentation().execStartActivity(
+ getOuterContext(), mMainThread.getApplicationThread(), null, null, intent, -1);
+ }
+
+ @Override
+ public void sendBroadcast(Intent intent) {
+ String resolvedType = intent.resolveTypeIfNeeded(getContentResolver());
+ try {
+ ActivityManagerNative.getDefault().broadcastIntent(
+ mMainThread.getApplicationThread(), intent, resolvedType, null,
+ Activity.RESULT_OK, null, null, null, false, false);
+ } catch (RemoteException e) {
+ }
+ }
+
+ @Override
+ public void sendBroadcast(Intent intent, String receiverPermission) {
+ String resolvedType = intent.resolveTypeIfNeeded(getContentResolver());
+ try {
+ ActivityManagerNative.getDefault().broadcastIntent(
+ mMainThread.getApplicationThread(), intent, resolvedType, null,
+ Activity.RESULT_OK, null, null, receiverPermission, false, false);
+ } catch (RemoteException e) {
+ }
+ }
+
+ @Override
+ public void sendOrderedBroadcast(Intent intent,
+ String receiverPermission) {
+ String resolvedType = intent.resolveTypeIfNeeded(getContentResolver());
+ try {
+ ActivityManagerNative.getDefault().broadcastIntent(
+ mMainThread.getApplicationThread(), intent, resolvedType, null,
+ Activity.RESULT_OK, null, null, receiverPermission, true, false);
+ } catch (RemoteException e) {
+ }
+ }
+
+ @Override
+ public void sendOrderedBroadcast(Intent intent,
+ String receiverPermission, BroadcastReceiver resultReceiver,
+ Handler scheduler, int initialCode, String initialData,
+ Bundle initialExtras) {
+ IIntentReceiver rd = null;
+ if (resultReceiver != null) {
+ if (mPackageInfo != null) {
+ if (scheduler == null) {
+ scheduler = mMainThread.getHandler();
+ }
+ rd = mPackageInfo.getReceiverDispatcher(
+ resultReceiver, getOuterContext(), scheduler,
+ mMainThread.getInstrumentation(), false);
+ } else {
+ if (scheduler == null) {
+ scheduler = mMainThread.getHandler();
+ }
+ rd = new ActivityThread.PackageInfo.ReceiverDispatcher(
+ resultReceiver, getOuterContext(), scheduler, null, false).getIIntentReceiver();
+ }
+ }
+ String resolvedType = intent.resolveTypeIfNeeded(getContentResolver());
+ try {
+ ActivityManagerNative.getDefault().broadcastIntent(
+ mMainThread.getApplicationThread(), intent, resolvedType, rd,
+ initialCode, initialData, initialExtras, receiverPermission,
+ true, false);
+ } catch (RemoteException e) {
+ }
+ }
+
+ @Override
+ public void sendStickyBroadcast(Intent intent) {
+ String resolvedType = intent.resolveTypeIfNeeded(getContentResolver());
+ try {
+ ActivityManagerNative.getDefault().broadcastIntent(
+ mMainThread.getApplicationThread(), intent, resolvedType, null,
+ Activity.RESULT_OK, null, null, null, false, true);
+ } catch (RemoteException e) {
+ }
+ }
+
+ @Override
+ public void removeStickyBroadcast(Intent intent) {
+ String resolvedType = intent.resolveTypeIfNeeded(getContentResolver());
+ if (resolvedType != null) {
+ intent = new Intent(intent);
+ intent.setDataAndType(intent.getData(), resolvedType);
+ }
+ try {
+ ActivityManagerNative.getDefault().unbroadcastIntent(
+ mMainThread.getApplicationThread(), intent);
+ } catch (RemoteException e) {
+ }
+ }
+
+ @Override
+ public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
+ return registerReceiver(receiver, filter, null, null);
+ }
+
+ @Override
+ public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter,
+ String broadcastPermission, Handler scheduler) {
+ return registerReceiverInternal(receiver, filter, broadcastPermission,
+ scheduler, getOuterContext());
+ }
+
+ private Intent registerReceiverInternal(BroadcastReceiver receiver,
+ IntentFilter filter, String broadcastPermission,
+ Handler scheduler, Context context) {
+ IIntentReceiver rd = null;
+ if (receiver != null) {
+ if (mPackageInfo != null && context != null) {
+ if (scheduler == null) {
+ scheduler = mMainThread.getHandler();
+ }
+ rd = mPackageInfo.getReceiverDispatcher(
+ receiver, context, scheduler,
+ mMainThread.getInstrumentation(), true);
+ } else {
+ if (scheduler == null) {
+ scheduler = mMainThread.getHandler();
+ }
+ rd = new ActivityThread.PackageInfo.ReceiverDispatcher(
+ receiver, context, scheduler, null, false).getIIntentReceiver();
+ }
+ }
+ try {
+ return ActivityManagerNative.getDefault().registerReceiver(
+ mMainThread.getApplicationThread(),
+ rd, filter, broadcastPermission);
+ } catch (RemoteException e) {
+ return null;
+ }
+ }
+
+ @Override
+ public void unregisterReceiver(BroadcastReceiver receiver) {
+ if (mPackageInfo != null) {
+ IIntentReceiver rd = mPackageInfo.forgetReceiverDispatcher(
+ getOuterContext(), receiver);
+ try {
+ ActivityManagerNative.getDefault().unregisterReceiver(rd);
+ } catch (RemoteException e) {
+ }
+ } else {
+ throw new RuntimeException("Not supported in system context");
+ }
+ }
+
+ @Override
+ public ComponentName startService(Intent service) {
+ try {
+ ComponentName cn = ActivityManagerNative.getDefault().startService(
+ mMainThread.getApplicationThread(), service,
+ service.resolveTypeIfNeeded(getContentResolver()));
+ if (cn != null && cn.getPackageName().equals("!")) {
+ throw new SecurityException(
+ "Not allowed to start service " + service
+ + " without permission " + cn.getClassName());
+ }
+ return cn;
+ } catch (RemoteException e) {
+ return null;
+ }
+ }
+
+ @Override
+ public boolean stopService(Intent service) {
+ try {
+ int res = ActivityManagerNative.getDefault().stopService(
+ mMainThread.getApplicationThread(), service,
+ service.resolveTypeIfNeeded(getContentResolver()));
+ if (res < 0) {
+ throw new SecurityException(
+ "Not allowed to stop service " + service);
+ }
+ return res != 0;
+ } catch (RemoteException e) {
+ return false;
+ }
+ }
+
+ @Override
+ public boolean bindService(Intent service, ServiceConnection conn,
+ int flags) {
+ IServiceConnection sd;
+ if (mPackageInfo != null) {
+ sd = mPackageInfo.getServiceDispatcher(conn, getOuterContext(),
+ mMainThread.getHandler(), flags);
+ } else {
+ throw new RuntimeException("Not supported in system context");
+ }
+ try {
+ int res = ActivityManagerNative.getDefault().bindService(
+ mMainThread.getApplicationThread(), getActivityToken(),
+ service, service.resolveTypeIfNeeded(getContentResolver()),
+ sd, flags);
+ if (res < 0) {
+ throw new SecurityException(
+ "Not allowed to bind to service " + service);
+ }
+ return res != 0;
+ } catch (RemoteException e) {
+ return false;
+ }
+ }
+
+ @Override
+ public void unbindService(ServiceConnection conn) {
+ if (mPackageInfo != null) {
+ IServiceConnection sd = mPackageInfo.forgetServiceDispatcher(
+ getOuterContext(), conn);
+ try {
+ ActivityManagerNative.getDefault().unbindService(sd);
+ } catch (RemoteException e) {
+ }
+ } else {
+ throw new RuntimeException("Not supported in system context");
+ }
+ }
+
+ @Override
+ public boolean startInstrumentation(ComponentName className,
+ String profileFile, Bundle arguments) {
+ try {
+ return ActivityManagerNative.getDefault().startInstrumentation(
+ className, profileFile, 0, arguments, null);
+ } catch (RemoteException e) {
+ // System has crashed, nothing we can do.
+ }
+ return false;
+ }
+
+ @Override
+ public Object getSystemService(String name) {
+ if (WINDOW_SERVICE.equals(name)) {
+ return WindowManagerImpl.getDefault();
+ } else if (LAYOUT_INFLATER_SERVICE.equals(name)) {
+ synchronized (mSync) {
+ LayoutInflater inflater = mLayoutInflater;
+ if (inflater != null) {
+ return inflater;
+ }
+ mLayoutInflater = inflater =
+ PolicyManager.makeNewLayoutInflater(getOuterContext());
+ return inflater;
+ }
+ } else if (ACTIVITY_SERVICE.equals(name)) {
+ return getActivityManager();
+ } else if (ALARM_SERVICE.equals(name)) {
+ return new AlarmManager();
+ } else if (POWER_SERVICE.equals(name)) {
+ return getPowerManager();
+ } else if (CONNECTIVITY_SERVICE.equals(name)) {
+ return getConnectivityManager();
+ } else if (WIFI_SERVICE.equals(name)) {
+ return getWifiManager();
+ } else if (NOTIFICATION_SERVICE.equals(name)) {
+ return getNotificationManager();
+ } else if (KEYGUARD_SERVICE.equals(name)) {
+ return new KeyguardManager();
+ } else if (LOCATION_SERVICE.equals(name)) {
+ return getLocationManager();
+ } else if (SEARCH_SERVICE.equals(name)) {
+ return getSearchManager();
+ } else if ( SENSOR_SERVICE.equals(name)) {
+ return getSensorManager();
+ } else if (BLUETOOTH_SERVICE.equals(name)) {
+ return getBluetoothDevice();
+ } else if (VIBRATOR_SERVICE.equals(name)) {
+ return getVibrator();
+ } else if (STATUS_BAR_SERVICE.equals(name)) {
+ synchronized (mSync) {
+ if (mStatusBarManager == null) {
+ mStatusBarManager = new StatusBarManager(getOuterContext());
+ }
+ return mStatusBarManager;
+ }
+ } else if (AUDIO_SERVICE.equals(name)) {
+ return getAudioManager();
+ } else if (TELEPHONY_SERVICE.equals(name)) {
+ return getTelephonyManager();
+ } else if (CLIPBOARD_SERVICE.equals(name)) {
+ return getClipboardManager();
+ }
+
+ return null;
+ }
+
+ private ActivityManager getActivityManager() {
+ synchronized (mSync) {
+ if (mActivityManager == null) {
+ mActivityManager = new ActivityManager(getOuterContext(),
+ mMainThread.getHandler());
+ }
+ }
+ return mActivityManager;
+ }
+
+ private PowerManager getPowerManager() {
+ synchronized (sSync) {
+ if (sPowerManager == null) {
+ IBinder b = ServiceManager.getService(POWER_SERVICE);
+ IPowerManager service = IPowerManager.Stub.asInterface(b);
+ sPowerManager = new PowerManager(service, mMainThread.getHandler());
+ }
+ }
+ return sPowerManager;
+ }
+
+ private ConnectivityManager getConnectivityManager()
+ {
+ synchronized (sSync) {
+ if (sConnectivityManager == null) {
+ IBinder b = ServiceManager.getService(CONNECTIVITY_SERVICE);
+ IConnectivityManager service = IConnectivityManager.Stub.asInterface(b);
+ sConnectivityManager = new ConnectivityManager(service);
+ }
+ }
+ return sConnectivityManager;
+ }
+
+ private WifiManager getWifiManager()
+ {
+ synchronized (sSync) {
+ if (sWifiManager == null) {
+ IBinder b = ServiceManager.getService(WIFI_SERVICE);
+ IWifiManager service = IWifiManager.Stub.asInterface(b);
+ sWifiManager = new WifiManager(service, mMainThread.getHandler());
+ }
+ }
+ return sWifiManager;
+ }
+
+ private NotificationManager getNotificationManager()
+ {
+ synchronized (mSync) {
+ if (mNotificationManager == null) {
+ mNotificationManager = new NotificationManager(
+ new ContextThemeWrapper(getOuterContext(), com.android.internal.R.style.Theme_Dialog),
+ mMainThread.getHandler());
+ }
+ }
+ return mNotificationManager;
+ }
+
+ private TelephonyManager getTelephonyManager() {
+ synchronized (mSync) {
+ if (mTelephonyManager == null) {
+ mTelephonyManager = new TelephonyManager(getOuterContext());
+ }
+ }
+ return mTelephonyManager;
+ }
+
+ private ClipboardManager getClipboardManager() {
+ synchronized (mSync) {
+ if (mClipboardManager == null) {
+ mClipboardManager = new ClipboardManager(getOuterContext(),
+ mMainThread.getHandler());
+ }
+ }
+ return mClipboardManager;
+ }
+
+ private LocationManager getLocationManager() {
+ synchronized (sSync) {
+ if (sLocationManager == null) {
+ IBinder b = ServiceManager.getService(LOCATION_SERVICE);
+ ILocationManager service = ILocationManager.Stub.asInterface(b);
+ sLocationManager = new LocationManager(service);
+ }
+ }
+ return sLocationManager;
+ }
+
+ private SearchManager getSearchManager() {
+ // This is only useable in Activity Contexts
+ if (getActivityToken() == null) {
+ throw new AndroidRuntimeException(
+ "Acquiring SearchManager objects only valid in Activity Contexts.");
+ }
+ synchronized (mSync) {
+ if (mSearchManager == null) {
+ mSearchManager = new SearchManager(getOuterContext(), mMainThread.getHandler());
+ }
+ }
+ return mSearchManager;
+ }
+
+ private BluetoothDevice getBluetoothDevice() {
+ if (sIsBluetoothDeviceCached) {
+ return sBluetoothDevice;
+ }
+ synchronized (sSync) {
+ IBinder b = ServiceManager.getService(BLUETOOTH_SERVICE);
+ if (b == null) {
+ sBluetoothDevice = null;
+ } else {
+ IBluetoothDevice service = IBluetoothDevice.Stub.asInterface(b);
+ sBluetoothDevice = new BluetoothDevice(service);
+ }
+ sIsBluetoothDeviceCached = true;
+ }
+ return sBluetoothDevice;
+ }
+
+ private SensorManager getSensorManager() {
+ synchronized (mSync) {
+ if (mSensorManager == null) {
+ mSensorManager = new SensorManager(mMainThread.getHandler().getLooper());
+ }
+ }
+ return mSensorManager;
+ }
+
+ private Vibrator getVibrator() {
+ synchronized (mSync) {
+ if (mVibrator == null) {
+ mVibrator = new Vibrator();
+ }
+ }
+ return mVibrator;
+ }
+
+ private IWallpaperService getWallpaperService() {
+ synchronized (sSync) {
+ if (sWallpaperService == null) {
+ IBinder b = ServiceManager.getService(WALLPAPER_SERVICE);
+ sWallpaperService = IWallpaperService.Stub.asInterface(b);
+ }
+ }
+ return sWallpaperService;
+ }
+
+ private AudioManager getAudioManager()
+ {
+ if (mAudioManager == null) {
+ mAudioManager = new AudioManager(this);
+ }
+ return mAudioManager;
+ }
+
+ @Override
+ public int checkPermission(String permission, int pid, int uid) {
+ if (permission == null) {
+ throw new IllegalArgumentException("permission is null");
+ }
+
+ if (!Process.supportsProcesses()) {
+ return PackageManager.PERMISSION_GRANTED;
+ }
+ try {
+ return ActivityManagerNative.getDefault().checkPermission(
+ permission, pid, uid);
+ } catch (RemoteException e) {
+ return PackageManager.PERMISSION_DENIED;
+ }
+ }
+
+ @Override
+ public int checkCallingPermission(String permission) {
+ if (permission == null) {
+ throw new IllegalArgumentException("permission is null");
+ }
+
+ if (!Process.supportsProcesses()) {
+ return PackageManager.PERMISSION_GRANTED;
+ }
+ int pid = Binder.getCallingPid();
+ if (pid != Process.myPid()) {
+ return checkPermission(permission, pid,
+ Binder.getCallingUid());
+ }
+ return PackageManager.PERMISSION_DENIED;
+ }
+
+ @Override
+ public int checkCallingOrSelfPermission(String permission) {
+ if (permission == null) {
+ throw new IllegalArgumentException("permission is null");
+ }
+
+ return checkPermission(permission, Binder.getCallingPid(),
+ Binder.getCallingUid());
+ }
+
+ private void enforce(
+ String permission, int resultOfCheck,
+ boolean selfToo, int uid, String message) {
+ if (resultOfCheck != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException(
+ (message != null ? (message + ": ") : "") +
+ (selfToo
+ ? "Neither user " + uid + " nor current process has "
+ : "User " + uid + " does not have ") +
+ permission +
+ ".");
+ }
+ }
+
+ public void enforcePermission(
+ String permission, int pid, int uid, String message) {
+ enforce(permission,
+ checkPermission(permission, pid, uid),
+ false,
+ uid,
+ message);
+ }
+
+ public void enforceCallingPermission(String permission, String message) {
+ enforce(permission,
+ checkCallingPermission(permission),
+ false,
+ Binder.getCallingUid(),
+ message);
+ }
+
+ public void enforceCallingOrSelfPermission(
+ String permission, String message) {
+ enforce(permission,
+ checkCallingOrSelfPermission(permission),
+ true,
+ Binder.getCallingUid(),
+ message);
+ }
+
+ @Override
+ public void grantUriPermission(String toPackage, Uri uri, int modeFlags) {
+ try {
+ ActivityManagerNative.getDefault().grantUriPermission(
+ mMainThread.getApplicationThread(), toPackage, uri,
+ modeFlags);
+ } catch (RemoteException e) {
+ }
+ }
+
+ @Override
+ public void revokeUriPermission(Uri uri, int modeFlags) {
+ try {
+ ActivityManagerNative.getDefault().revokeUriPermission(
+ mMainThread.getApplicationThread(), uri,
+ modeFlags);
+ } catch (RemoteException e) {
+ }
+ }
+
+ @Override
+ public int checkUriPermission(Uri uri, int pid, int uid, int modeFlags) {
+ if (!Process.supportsProcesses()) {
+ return PackageManager.PERMISSION_GRANTED;
+ }
+ try {
+ return ActivityManagerNative.getDefault().checkUriPermission(
+ uri, pid, uid, modeFlags);
+ } catch (RemoteException e) {
+ return PackageManager.PERMISSION_DENIED;
+ }
+ }
+
+ @Override
+ public int checkCallingUriPermission(Uri uri, int modeFlags) {
+ if (!Process.supportsProcesses()) {
+ return PackageManager.PERMISSION_GRANTED;
+ }
+ int pid = Binder.getCallingPid();
+ if (pid != Process.myPid()) {
+ return checkUriPermission(uri, pid,
+ Binder.getCallingUid(), modeFlags);
+ }
+ return PackageManager.PERMISSION_DENIED;
+ }
+
+ @Override
+ public int checkCallingOrSelfUriPermission(Uri uri, int modeFlags) {
+ return checkUriPermission(uri, Binder.getCallingPid(),
+ Binder.getCallingUid(), modeFlags);
+ }
+
+ @Override
+ public int checkUriPermission(Uri uri, String readPermission,
+ String writePermission, int pid, int uid, int modeFlags) {
+ if (false) {
+ Log.i("foo", "checkUriPermission: uri=" + uri + "readPermission="
+ + readPermission + " writePermission=" + writePermission
+ + " pid=" + pid + " uid=" + uid + " mode" + modeFlags);
+ }
+ if ((modeFlags&Intent.FLAG_GRANT_READ_URI_PERMISSION) != 0) {
+ if (readPermission == null
+ || checkPermission(readPermission, pid, uid)
+ == PackageManager.PERMISSION_GRANTED) {
+ return PackageManager.PERMISSION_GRANTED;
+ }
+ }
+ if ((modeFlags&Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0) {
+ if (writePermission == null
+ || checkPermission(writePermission, pid, uid)
+ == PackageManager.PERMISSION_GRANTED) {
+ return PackageManager.PERMISSION_GRANTED;
+ }
+ }
+ return uri != null ? checkUriPermission(uri, pid, uid, modeFlags)
+ : PackageManager.PERMISSION_DENIED;
+ }
+
+ private String uriModeFlagToString(int uriModeFlags) {
+ switch (uriModeFlags) {
+ case Intent.FLAG_GRANT_READ_URI_PERMISSION |
+ Intent.FLAG_GRANT_WRITE_URI_PERMISSION:
+ return "read and write";
+ case Intent.FLAG_GRANT_READ_URI_PERMISSION:
+ return "read";
+ case Intent.FLAG_GRANT_WRITE_URI_PERMISSION:
+ return "write";
+ }
+ throw new IllegalArgumentException(
+ "Unknown permission mode flags: " + uriModeFlags);
+ }
+
+ private void enforceForUri(
+ int modeFlags, int resultOfCheck, boolean selfToo,
+ int uid, Uri uri, String message) {
+ if (resultOfCheck != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException(
+ (message != null ? (message + ": ") : "") +
+ (selfToo
+ ? "Neither user " + uid + " nor current process has "
+ : "User " + uid + " does not have ") +
+ uriModeFlagToString(modeFlags) +
+ " permission on " +
+ uri +
+ ".");
+ }
+ }
+
+ public void enforceUriPermission(
+ Uri uri, int pid, int uid, int modeFlags, String message) {
+ enforceForUri(
+ modeFlags, checkUriPermission(uri, pid, uid, modeFlags),
+ false, uid, uri, message);
+ }
+
+ public void enforceCallingUriPermission(
+ Uri uri, int modeFlags, String message) {
+ enforceForUri(
+ modeFlags, checkCallingUriPermission(uri, modeFlags),
+ false, Binder.getCallingUid(), uri, message);
+ }
+
+ public void enforceCallingOrSelfUriPermission(
+ Uri uri, int modeFlags, String message) {
+ enforceForUri(
+ modeFlags,
+ checkCallingOrSelfUriPermission(uri, modeFlags), true,
+ Binder.getCallingUid(), uri, message);
+ }
+
+ public void enforceUriPermission(
+ Uri uri, String readPermission, String writePermission,
+ int pid, int uid, int modeFlags, String message) {
+ enforceForUri(modeFlags,
+ checkUriPermission(
+ uri, readPermission, writePermission, pid, uid,
+ modeFlags),
+ false,
+ uid,
+ uri,
+ message);
+ }
+
+ @Override
+ public Context createPackageContext(String packageName, int flags)
+ throws PackageManager.NameNotFoundException {
+ if (packageName.equals("system") || packageName.equals("android")) {
+ return new ApplicationContext(mMainThread.getSystemContext());
+ }
+
+ ActivityThread.PackageInfo pi =
+ mMainThread.getPackageInfo(packageName, flags);
+ if (pi != null) {
+ ApplicationContext c = new ApplicationContext();
+ c.init(pi, null, mMainThread);
+ if (c.mResources != null) {
+ return c;
+ }
+ }
+
+ // Should be a better exception.
+ throw new PackageManager.NameNotFoundException(
+ "Application package " + packageName + " not found");
+ }
+
+ private File getDataDirFile() {
+ if (mPackageInfo != null) {
+ return mPackageInfo.getDataDirFile();
+ }
+ throw new RuntimeException("Not supported in system context");
+ }
+
+ @Override
+ public File getDir(String name, int mode) {
+ name = "app_" + name;
+ File file = makeFilename(getDataDirFile(), name);
+ if (!file.exists()) {
+ file.mkdir();
+ setFilePermissionsFromMode(file.toString(), mode,
+ FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH);
+ }
+ return file;
+ }
+
+ static ApplicationContext createSystemContext(ActivityThread mainThread) {
+ ApplicationContext context = new ApplicationContext();
+ context.init(Resources.getSystem(), mainThread);
+ return context;
+ }
+
+ ApplicationContext() {
+ ++sInstanceCount;
+ mOuterContext = this;
+ }
+
+ /**
+ * Create a new ApplicationContext from an existing one. The new one
+ * works and operates the same as the one it is copying.
+ *
+ * @param context Existing application context.
+ */
+ public ApplicationContext(ApplicationContext context) {
+ ++sInstanceCount;
+ mPackageInfo = context.mPackageInfo;
+ mResources = context.mResources;
+ mMainThread = context.mMainThread;
+ mContentResolver = context.mContentResolver;
+ mOuterContext = this;
+ }
+
+ final void init(ActivityThread.PackageInfo packageInfo,
+ IBinder activityToken, ActivityThread mainThread) {
+ mPackageInfo = packageInfo;
+ mResources = mPackageInfo.getResources(mainThread);
+ mMainThread = mainThread;
+ mContentResolver = new ApplicationContentResolver(this, mainThread);
+
+ setActivityToken(activityToken);
+ }
+
+ final void init(Resources resources, ActivityThread mainThread) {
+ mPackageInfo = null;
+ mResources = resources;
+ mMainThread = mainThread;
+ mContentResolver = new ApplicationContentResolver(this, mainThread);
+ }
+
+ final void scheduleFinalCleanup(String who, String what) {
+ mMainThread.scheduleContextCleanup(this, who, what);
+ }
+
+ final void performFinalCleanup(String who, String what) {
+ //Log.i(TAG, "Cleanup up context: " + this);
+ mPackageInfo.removeContextRegistrations(getOuterContext(), who, what);
+ }
+
+ final Context getReceiverRestrictedContext() {
+ if (mReceiverRestrictedContext != null) {
+ return mReceiverRestrictedContext;
+ }
+ return mReceiverRestrictedContext = new ReceiverRestrictedContext(getOuterContext());
+ }
+
+ final void setActivityToken(IBinder token) {
+ mActivityToken = token;
+ }
+
+ final void setOuterContext(Context context) {
+ mOuterContext = context;
+ }
+
+ final Context getOuterContext() {
+ return mOuterContext;
+ }
+
+ final IBinder getActivityToken() {
+ return mActivityToken;
+ }
+
+ private static void setFilePermissionsFromMode(String name, int mode,
+ int extraPermissions) {
+ int perms = FileUtils.S_IRUSR|FileUtils.S_IWUSR
+ |FileUtils.S_IRGRP|FileUtils.S_IWGRP
+ |extraPermissions;
+ if ((mode&MODE_WORLD_READABLE) != 0) {
+ perms |= FileUtils.S_IROTH;
+ }
+ if ((mode&MODE_WORLD_WRITEABLE) != 0) {
+ perms |= FileUtils.S_IWOTH;
+ }
+ if (false) {
+ Log.i(TAG, "File " + name + ": mode=0x" + Integer.toHexString(mode)
+ + ", perms=0x" + Integer.toHexString(perms));
+ }
+ FileUtils.setPermissions(name, perms, -1, -1);
+ }
+
+ private File makeFilename(File base, String name) {
+ if (name.indexOf(File.separatorChar) < 0) {
+ return new File(base, name);
+ }
+ throw new IllegalArgumentException(
+ "File " + name + " contains a path separator");
+ }
+
+ // ----------------------------------------------------------------------
+ // ----------------------------------------------------------------------
+ // ----------------------------------------------------------------------
+
+ private static final class ApplicationContentResolver extends ContentResolver {
+ public ApplicationContentResolver(Context context,
+ ActivityThread mainThread)
+ {
+ super(context);
+ mMainThread = mainThread;
+ }
+
+ @Override
+ protected IContentProvider acquireProvider(Context context, String name)
+ {
+ return mMainThread.acquireProvider(context, name);
+ }
+
+ @Override
+ public boolean releaseProvider(IContentProvider provider)
+ {
+ return mMainThread.releaseProvider(provider);
+ }
+
+ private final ActivityThread mMainThread;
+ }
+
+ // ----------------------------------------------------------------------
+ // ----------------------------------------------------------------------
+ // ----------------------------------------------------------------------
+
+ /*package*/
+ static final class ApplicationPackageManager extends PackageManager {
+ @Override
+ public PackageInfo getPackageInfo(String packageName, int flags)
+ throws NameNotFoundException {
+ try {
+ PackageInfo pi = mPM.getPackageInfo(packageName, flags);
+ if (pi != null) {
+ return pi;
+ }
+ } catch (RemoteException e) {
+ throw new RuntimeException("Package manager has died", e);
+ }
+
+ throw new NameNotFoundException(packageName);
+ }
+
+ @Override
+ public int[] getPackageGids(String packageName)
+ throws NameNotFoundException {
+ try {
+ int[] gids = mPM.getPackageGids(packageName);
+ if (gids == null || gids.length > 0) {
+ return gids;
+ }
+ } catch (RemoteException e) {
+ throw new RuntimeException("Package manager has died", e);
+ }
+
+ throw new NameNotFoundException(packageName);
+ }
+
+ @Override
+ public PermissionInfo getPermissionInfo(String name, int flags)
+ throws NameNotFoundException {
+ try {
+ PermissionInfo pi = mPM.getPermissionInfo(name, flags);
+ if (pi != null) {
+ return pi;
+ }
+ } catch (RemoteException e) {
+ throw new RuntimeException("Package manager has died", e);
+ }
+
+ throw new NameNotFoundException(name);
+ }
+
+ @Override
+ public List<PermissionInfo> queryPermissionsByGroup(String group, int flags)
+ throws NameNotFoundException {
+ try {
+ List<PermissionInfo> pi = mPM.queryPermissionsByGroup(group, flags);
+ if (pi != null) {
+ return pi;
+ }
+ } catch (RemoteException e) {
+ throw new RuntimeException("Package manager has died", e);
+ }
+
+ throw new NameNotFoundException(group);
+ }
+
+ @Override
+ public PermissionGroupInfo getPermissionGroupInfo(String name,
+ int flags) throws NameNotFoundException {
+ try {
+ PermissionGroupInfo pgi = mPM.getPermissionGroupInfo(name, flags);
+ if (pgi != null) {
+ return pgi;
+ }
+ } catch (RemoteException e) {
+ throw new RuntimeException("Package manager has died", e);
+ }
+
+ throw new NameNotFoundException(name);
+ }
+
+ @Override
+ public List<PermissionGroupInfo> getAllPermissionGroups(int flags) {
+ try {
+ return mPM.getAllPermissionGroups(flags);
+ } catch (RemoteException e) {
+ throw new RuntimeException("Package manager has died", e);
+ }
+ }
+
+ @Override
+ public ApplicationInfo getApplicationInfo(String packageName, int flags)
+ throws NameNotFoundException {
+ try {
+ ApplicationInfo ai = mPM.getApplicationInfo(packageName, flags);
+ if (ai != null) {
+ return ai;
+ }
+ } catch (RemoteException e) {
+ throw new RuntimeException("Package manager has died", e);
+ }
+
+ throw new NameNotFoundException(packageName);
+ }
+
+ @Override
+ public ActivityInfo getActivityInfo(ComponentName className, int flags)
+ throws NameNotFoundException {
+ try {
+ ActivityInfo ai = mPM.getActivityInfo(className, flags);
+ if (ai != null) {
+ return ai;
+ }
+ } catch (RemoteException e) {
+ throw new RuntimeException("Package manager has died", e);
+ }
+
+ throw new NameNotFoundException(className.toString());
+ }
+
+ @Override
+ public ActivityInfo getReceiverInfo(ComponentName className, int flags)
+ throws NameNotFoundException {
+ try {
+ ActivityInfo ai = mPM.getReceiverInfo(className, flags);
+ if (ai != null) {
+ return ai;
+ }
+ } catch (RemoteException e) {
+ throw new RuntimeException("Package manager has died", e);
+ }
+
+ throw new NameNotFoundException(className.toString());
+ }
+
+ @Override
+ public ServiceInfo getServiceInfo(ComponentName className, int flags)
+ throws NameNotFoundException {
+ try {
+ ServiceInfo si = mPM.getServiceInfo(className, flags);
+ if (si != null) {
+ return si;
+ }
+ } catch (RemoteException e) {
+ throw new RuntimeException("Package manager has died", e);
+ }
+
+ throw new NameNotFoundException(className.toString());
+ }
+
+ @Override
+ public int checkPermission(String permName, String pkgName) {
+ try {
+ return mPM.checkPermission(permName, pkgName);
+ } catch (RemoteException e) {
+ throw new RuntimeException("Package manager has died", e);
+ }
+ }
+
+ @Override
+ public boolean addPermission(PermissionInfo info) {
+ try {
+ return mPM.addPermission(info);
+ } catch (RemoteException e) {
+ throw new RuntimeException("Package manager has died", e);
+ }
+ }
+
+ @Override
+ public void removePermission(String name) {
+ try {
+ mPM.removePermission(name);
+ } catch (RemoteException e) {
+ throw new RuntimeException("Package manager has died", e);
+ }
+ }
+
+ @Override
+ public int checkSignatures(String pkg1, String pkg2) {
+ try {
+ return mPM.checkSignatures(pkg1, pkg2);
+ } catch (RemoteException e) {
+ throw new RuntimeException("Package manager has died", e);
+ }
+ }
+
+ @Override
+ public String[] getPackagesForUid(int uid) {
+ try {
+ return mPM.getPackagesForUid(uid);
+ } catch (RemoteException e) {
+ throw new RuntimeException("Package manager has died", e);
+ }
+ }
+
+ @Override
+ public String getNameForUid(int uid) {
+ try {
+ return mPM.getNameForUid(uid);
+ } catch (RemoteException e) {
+ throw new RuntimeException("Package manager has died", e);
+ }
+ }
+
+ @Override
+ public List<PackageInfo> getInstalledPackages(int flags) {
+ try {
+ return mPM.getInstalledPackages(flags);
+ } catch (RemoteException e) {
+ throw new RuntimeException("Package manager has died", e);
+ }
+ }
+
+ @Override
+ public List<ApplicationInfo> getInstalledApplications(int flags) {
+ try {
+ return mPM.getInstalledApplications(flags);
+ } catch (RemoteException e) {
+ throw new RuntimeException("Package manager has died", e);
+ }
+ }
+
+ @Override
+ public ResolveInfo resolveActivity(Intent intent, int flags) {
+ try {
+ return mPM.resolveIntent(
+ intent,
+ intent.resolveTypeIfNeeded(mContext.getContentResolver()),
+ flags);
+ } catch (RemoteException e) {
+ throw new RuntimeException("Package manager has died", e);
+ }
+ }
+
+ @Override
+ public List<ResolveInfo> queryIntentActivities(Intent intent,
+ int flags) {
+ try {
+ return mPM.queryIntentActivities(
+ intent,
+ intent.resolveTypeIfNeeded(mContext.getContentResolver()),
+ flags);
+ } catch (RemoteException e) {
+ throw new RuntimeException("Package manager has died", e);
+ }
+ }
+
+ @Override
+ public List<ResolveInfo> queryIntentActivityOptions(
+ ComponentName caller, Intent[] specifics, Intent intent,
+ int flags) {
+ final ContentResolver resolver = mContext.getContentResolver();
+
+ String[] specificTypes = null;
+ if (specifics != null) {
+ final int N = specifics.length;
+ for (int i=0; i<N; i++) {
+ Intent sp = specifics[i];
+ if (sp != null) {
+ String t = sp.resolveTypeIfNeeded(resolver);
+ if (t != null) {
+ if (specificTypes == null) {
+ specificTypes = new String[N];
+ }
+ specificTypes[i] = t;
+ }
+ }
+ }
+ }
+
+ try {
+ return mPM.queryIntentActivityOptions(caller, specifics,
+ specificTypes, intent, intent.resolveTypeIfNeeded(resolver),
+ flags);
+ } catch (RemoteException e) {
+ throw new RuntimeException("Package manager has died", e);
+ }
+ }
+
+ @Override
+ public List<ResolveInfo> queryBroadcastReceivers(Intent intent, int flags) {
+ try {
+ return mPM.queryIntentReceivers(
+ intent,
+ intent.resolveTypeIfNeeded(mContext.getContentResolver()),
+ flags);
+ } catch (RemoteException e) {
+ throw new RuntimeException("Package manager has died", e);
+ }
+ }
+
+ @Override
+ public ResolveInfo resolveService(Intent intent, int flags) {
+ try {
+ return mPM.resolveService(
+ intent,
+ intent.resolveTypeIfNeeded(mContext.getContentResolver()),
+ flags);
+ } catch (RemoteException e) {
+ throw new RuntimeException("Package manager has died", e);
+ }
+ }
+
+ @Override
+ public List<ResolveInfo> queryIntentServices(Intent intent, int flags) {
+ try {
+ return mPM.queryIntentServices(
+ intent,
+ intent.resolveTypeIfNeeded(mContext.getContentResolver()),
+ flags);
+ } catch (RemoteException e) {
+ throw new RuntimeException("Package manager has died", e);
+ }
+ }
+
+ @Override
+ public ProviderInfo resolveContentProvider(String name,
+ int flags) {
+ try {
+ return mPM.resolveContentProvider(name, flags);
+ } catch (RemoteException e) {
+ throw new RuntimeException("Package manager has died", e);
+ }
+ }
+
+ @Override
+ public List<ProviderInfo> queryContentProviders(String processName,
+ int uid, int flags) {
+ try {
+ return mPM.queryContentProviders(processName, uid, flags);
+ } catch (RemoteException e) {
+ throw new RuntimeException("Package manager has died", e);
+ }
+ }
+
+ @Override
+ public InstrumentationInfo getInstrumentationInfo(
+ ComponentName className, int flags)
+ throws NameNotFoundException {
+ try {
+ InstrumentationInfo ii = mPM.getInstrumentationInfo(
+ className, flags);
+ if (ii != null) {
+ return ii;
+ }
+ } catch (RemoteException e) {
+ throw new RuntimeException("Package manager has died", e);
+ }
+
+ throw new NameNotFoundException(className.toString());
+ }
+
+ @Override
+ public List<InstrumentationInfo> queryInstrumentation(
+ String targetPackage, int flags) {
+ try {
+ return mPM.queryInstrumentation(targetPackage, flags);
+ } catch (RemoteException e) {
+ throw new RuntimeException("Package manager has died", e);
+ }
+ }
+
+ @Override public Drawable getDrawable(String packageName, int resid,
+ ApplicationInfo appInfo) {
+ ResourceName name = new ResourceName(packageName, resid);
+ Drawable dr = getCachedIcon(name);
+ if (dr != null) {
+ return dr;
+ }
+ if (appInfo == null) {
+ try {
+ appInfo = getApplicationInfo(packageName, 0);
+ } catch (NameNotFoundException e) {
+ return null;
+ }
+ }
+ try {
+ Resources r = getResourcesForApplication(appInfo);
+ dr = r.getDrawable(resid);
+ if (DEBUG_ICONS) Log.v(TAG, "Getting drawable 0x"
+ + Integer.toHexString(resid) + " from " + r
+ + ": " + dr);
+ putCachedIcon(name, dr);
+ return dr;
+ } catch (NameNotFoundException e) {
+ Log.w("PackageManager", "Failure retrieving resources for"
+ + appInfo.packageName);
+ } catch (RuntimeException e) {
+ // If an exception was thrown, fall through to return
+ // default icon.
+ Log.w("PackageManager", "Failure retrieving icon 0x"
+ + Integer.toHexString(resid) + " in package "
+ + packageName, e);
+ }
+ return null;
+ }
+
+ @Override public Drawable getActivityIcon(ComponentName activityName)
+ throws NameNotFoundException {
+ return getActivityInfo(activityName, 0).loadIcon(this);
+ }
+
+ @Override public Drawable getActivityIcon(Intent intent)
+ throws NameNotFoundException {
+ if (intent.getComponent() != null) {
+ return getActivityIcon(intent.getComponent());
+ }
+
+ ResolveInfo info = resolveActivity(
+ intent, PackageManager.MATCH_DEFAULT_ONLY);
+ if (info != null) {
+ return info.activityInfo.loadIcon(this);
+ }
+
+ throw new NameNotFoundException(intent.toURI());
+ }
+
+ @Override public Drawable getDefaultActivityIcon() {
+ return Resources.getSystem().getDrawable(
+ com.android.internal.R.drawable.sym_def_app_icon);
+ }
+
+ @Override public Drawable getApplicationIcon(ApplicationInfo info) {
+ final int icon = info.icon;
+ if (icon != 0) {
+ ResourceName name = new ResourceName(info, icon);
+ Drawable dr = getCachedIcon(name);
+ if (dr != null) {
+ return dr;
+ }
+ try {
+ Resources r = getResourcesForApplication(info);
+ dr = r.getDrawable(icon);
+ if (DEBUG_ICONS) Log.v(TAG, "Getting drawable 0x"
+ + Integer.toHexString(icon) + " from " + r
+ + ": " + dr);
+ putCachedIcon(name, dr);
+ return dr;
+ } catch (NameNotFoundException e) {
+ Log.w("PackageManager", "Failure retrieving resources for"
+ + info.packageName);
+ } catch (RuntimeException e) {
+ // If an exception was thrown, fall through to return
+ // default icon.
+ Log.w("PackageManager", "Failure retrieving app icon", e);
+ }
+ }
+ return getDefaultActivityIcon();
+ }
+
+ @Override public Drawable getApplicationIcon(String packageName)
+ throws NameNotFoundException {
+ return getApplicationIcon(getApplicationInfo(packageName, 0));
+ }
+
+ @Override public Resources getResourcesForActivity(
+ ComponentName activityName) throws NameNotFoundException {
+ return getResourcesForApplication(
+ getActivityInfo(activityName, 0).applicationInfo);
+ }
+
+ @Override public Resources getResourcesForApplication(
+ ApplicationInfo app) throws NameNotFoundException {
+ if (app.packageName.equals("system")) {
+ return mContext.mMainThread.getSystemContext().getResources();
+ }
+ Resources r = mContext.mMainThread.getTopLevelResources(app.publicSourceDir);
+ if (r != null) {
+ return r;
+ }
+ throw new NameNotFoundException("Unable to open " + app.publicSourceDir);
+ }
+
+ @Override public Resources getResourcesForApplication(
+ String appPackageName) throws NameNotFoundException {
+ return getResourcesForApplication(
+ getApplicationInfo(appPackageName, 0));
+ }
+
+ static void configurationChanged() {
+ synchronized (sSync) {
+ sIconCache.clear();
+ sStringCache.clear();
+ }
+ }
+
+ ApplicationPackageManager(ApplicationContext context,
+ IPackageManager pm) {
+ mContext = context;
+ mPM = pm;
+ }
+
+ private Drawable getCachedIcon(ResourceName name) {
+ synchronized (sSync) {
+ WeakReference<Drawable> wr = sIconCache.get(name);
+ if (DEBUG_ICONS) Log.v(TAG, "Get cached weak drawable ref for "
+ + name + ": " + wr);
+ if (wr != null) { // we have the activity
+ Drawable dr = wr.get();
+ if (dr != null) {
+ if (DEBUG_ICONS) Log.v(TAG, "Get cached drawable for "
+ + name + ": " + dr);
+ return dr;
+ }
+ // our entry has been purged
+ sIconCache.remove(name);
+ }
+ }
+ return null;
+ }
+
+ private void establishPackageRemovedReceiver() {
+ // mContext.registerReceiverInternal() winds up acquiring the
+ // main ActivityManagerService.this lock. If we hold our usual
+ // sSync global lock at the same time, we impose a required ordering
+ // on those two locks, which is not good for deadlock prevention.
+ // Use a dedicated lock around initialization of
+ // sPackageRemovedReceiver to avoid this.
+ synchronized (sPackageRemovedSync) {
+ if (sPackageRemovedReceiver == null) {
+ sPackageRemovedReceiver = new PackageRemovedReceiver();
+ IntentFilter filter = new IntentFilter(
+ Intent.ACTION_PACKAGE_REMOVED);
+ filter.addDataScheme("package");
+ mContext.registerReceiverInternal(sPackageRemovedReceiver,
+ filter, null, null, null);
+ }
+ }
+ }
+
+ private void putCachedIcon(ResourceName name, Drawable dr) {
+ establishPackageRemovedReceiver();
+
+ synchronized (sSync) {
+ sIconCache.put(name, new WeakReference<Drawable>(dr));
+ if (DEBUG_ICONS) Log.v(TAG, "Added cached drawable for "
+ + name + ": " + dr);
+ }
+ }
+
+ private static final class PackageRemovedReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Uri data = intent.getData();
+ String ssp;
+ if (data != null && (ssp=data.getSchemeSpecificPart()) != null) {
+ boolean needCleanup = false;
+ synchronized (sSync) {
+ Iterator<ResourceName> it = sIconCache.keySet().iterator();
+ while (it.hasNext()) {
+ ResourceName nm = it.next();
+ if (nm.packageName.equals(ssp)) {
+ //Log.i(TAG, "Removing cached drawable for " + nm);
+ it.remove();
+ needCleanup = true;
+ }
+ }
+ it = sStringCache.keySet().iterator();
+ while (it.hasNext()) {
+ ResourceName nm = it.next();
+ if (nm.packageName.equals(ssp)) {
+ //Log.i(TAG, "Removing cached string for " + nm);
+ it.remove();
+ needCleanup = true;
+ }
+ }
+ }
+ if (needCleanup || ActivityThread.currentActivityThread().hasPackageInfo(ssp)) {
+ ActivityThread.currentActivityThread().scheduleGcIdler();
+ }
+ }
+ }
+ }
+
+ private static final class ResourceName {
+ final String packageName;
+ final int iconId;
+
+ ResourceName(String _packageName, int _iconId) {
+ packageName = _packageName;
+ iconId = _iconId;
+ }
+
+ ResourceName(ApplicationInfo aInfo, int _iconId) {
+ this(aInfo.packageName, _iconId);
+ }
+
+ ResourceName(ComponentInfo cInfo, int _iconId) {
+ this(cInfo.applicationInfo.packageName, _iconId);
+ }
+
+ ResourceName(ResolveInfo rInfo, int _iconId) {
+ this(rInfo.activityInfo.applicationInfo.packageName, _iconId);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ ResourceName that = (ResourceName) o;
+
+ if (iconId != that.iconId) return false;
+ return !(packageName != null ?
+ !packageName.equals(that.packageName) : that.packageName != null);
+
+ }
+
+ @Override
+ public int hashCode() {
+ int result;
+ result = packageName.hashCode();
+ result = 31 * result + iconId;
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "{ResourceName " + packageName + " / " + iconId + "}";
+ }
+ }
+
+ private CharSequence getCachedString(ResourceName name) {
+ synchronized (sSync) {
+ WeakReference<CharSequence> wr = sStringCache.get(name);
+ if (wr != null) { // we have the activity
+ CharSequence cs = wr.get();
+ if (cs != null) {
+ return cs;
+ }
+ // our entry has been purged
+ sStringCache.remove(name);
+ }
+ }
+ return null;
+ }
+
+ private void putCachedString(ResourceName name, CharSequence cs) {
+ establishPackageRemovedReceiver();
+
+ synchronized (sSync) {
+ sStringCache.put(name, new WeakReference<CharSequence>(cs));
+ }
+ }
+
+ private CharSequence getLabel(ResourceName name, ApplicationInfo app, int id) {
+ CharSequence cs = getCachedString(name);
+ if (cs != null) {
+ return cs;
+ }
+ try {
+ Resources r = getResourcesForApplication(app);
+ cs = r.getText(id);
+ putCachedString(name, cs);
+ } catch (NameNotFoundException e) {
+ Log.w("PackageManager", "Failure retrieving resources for"
+ + app.packageName);
+ } catch (RuntimeException e) {
+ // If an exception was thrown, fall through to return null
+ Log.w("ApplicationInfo", "Failure retrieving activity name", e);
+ }
+ return cs;
+ }
+
+ @Override
+ public CharSequence getText(String packageName, int resid,
+ ApplicationInfo appInfo) {
+ ResourceName name = new ResourceName(packageName, resid);
+ CharSequence text = getCachedString(name);
+ if (text != null) {
+ return text;
+ }
+ if (appInfo == null) {
+ try {
+ appInfo = getApplicationInfo(packageName, 0);
+ } catch (NameNotFoundException e) {
+ return null;
+ }
+ }
+ try {
+ Resources r = getResourcesForApplication(appInfo);
+ text = r.getText(resid);
+ putCachedString(name, text);
+ return text;
+ } catch (NameNotFoundException e) {
+ Log.w("PackageManager", "Failure retrieving resources for"
+ + appInfo.packageName);
+ } catch (RuntimeException e) {
+ // If an exception was thrown, fall through to return
+ // default icon.
+ Log.w("PackageManager", "Failure retrieving text 0x"
+ + Integer.toHexString(resid) + " in package "
+ + packageName, e);
+ }
+ return null;
+ }
+
+ @Override
+ public XmlResourceParser getXml(String packageName, int resid,
+ ApplicationInfo appInfo) {
+ if (appInfo == null) {
+ try {
+ appInfo = getApplicationInfo(packageName, 0);
+ } catch (NameNotFoundException e) {
+ return null;
+ }
+ }
+ try {
+ Resources r = getResourcesForApplication(appInfo);
+ return r.getXml(resid);
+ } catch (RuntimeException e) {
+ // If an exception was thrown, fall through to return
+ // default icon.
+ Log.w("PackageManager", "Failure retrieving xml 0x"
+ + Integer.toHexString(resid) + " in package "
+ + packageName, e);
+ } catch (NameNotFoundException e) {
+ Log.w("PackageManager", "Failure retrieving resources for"
+ + appInfo.packageName);
+ }
+ return null;
+ }
+
+ @Override
+ public CharSequence getApplicationLabel(ApplicationInfo info) {
+ if (info.nonLocalizedLabel != null) {
+ return info.nonLocalizedLabel;
+ }
+ final int id = info.labelRes;
+ if (id != 0) {
+ CharSequence cs = getLabel(new ResourceName(info, id), info, id);
+ if (cs != null) {
+ return cs;
+ }
+ }
+ return info.packageName;
+ }
+
+ @Override
+ public void installPackage(Uri packageURI, IPackageInstallObserver observer, int flags) {
+ try {
+ mPM.installPackage(packageURI, observer, flags);
+ } catch (RemoteException e) {
+ // Should never happen!
+ }
+ }
+
+ @Override
+ public void deletePackage(String packageName, IPackageDeleteObserver observer, int flags) {
+ try {
+ mPM.deletePackage(packageName, observer, flags);
+ } catch (RemoteException e) {
+ // Should never happen!
+ }
+ }
+ @Override
+ public void clearApplicationUserData(String packageName,
+ IPackageDataObserver observer) {
+ try {
+ mPM.clearApplicationUserData(packageName, observer);
+ } catch (RemoteException e) {
+ // Should never happen!
+ }
+ }
+ @Override
+ public void deleteApplicationCacheFiles(String packageName,
+ IPackageDataObserver observer) {
+ try {
+ mPM.deleteApplicationCacheFiles(packageName, observer);
+ } catch (RemoteException e) {
+ // Should never happen!
+ }
+ }
+ @Override
+ public void freeApplicationCache(long idealStorageSize,
+ IPackageDataObserver observer) {
+ try {
+ mPM.freeApplicationCache(idealStorageSize, observer);
+ } catch (RemoteException e) {
+ // Should never happen!
+ }
+ }
+ @Override
+ public void getPackageSizeInfo(String packageName,
+ IPackageStatsObserver observer) {
+ try {
+ mPM.getPackageSizeInfo(packageName, observer);
+ } catch (RemoteException e) {
+ // Should never happen!
+ }
+ }
+ @Override
+ public void addPackageToPreferred(String packageName) {
+ try {
+ mPM.addPackageToPreferred(packageName);
+ } catch (RemoteException e) {
+ // Should never happen!
+ }
+ }
+
+ @Override
+ public void removePackageFromPreferred(String packageName) {
+ try {
+ mPM.removePackageFromPreferred(packageName);
+ } catch (RemoteException e) {
+ // Should never happen!
+ }
+ }
+
+ @Override
+ public List<PackageInfo> getPreferredPackages(int flags) {
+ try {
+ return mPM.getPreferredPackages(flags);
+ } catch (RemoteException e) {
+ // Should never happen!
+ }
+ return new ArrayList<PackageInfo>();
+ }
+
+ @Override
+ public void addPreferredActivity(IntentFilter filter,
+ int match, ComponentName[] set, ComponentName activity) {
+ try {
+ mPM.addPreferredActivity(filter, match, set, activity);
+ } catch (RemoteException e) {
+ // Should never happen!
+ }
+ }
+
+ @Override
+ public void clearPackagePreferredActivities(String packageName) {
+ try {
+ mPM.clearPackagePreferredActivities(packageName);
+ } catch (RemoteException e) {
+ // Should never happen!
+ }
+ }
+
+ @Override
+ public int getPreferredActivities(List<IntentFilter> outFilters,
+ List<ComponentName> outActivities, String packageName) {
+ try {
+ return mPM.getPreferredActivities(outFilters, outActivities, packageName);
+ } catch (RemoteException e) {
+ // Should never happen!
+ }
+ return 0;
+ }
+
+ @Override
+ public void setComponentEnabledSetting(ComponentName componentName,
+ int newState, int flags) {
+ try {
+ mPM.setComponentEnabledSetting(componentName, newState, flags);
+ } catch (RemoteException e) {
+ // Should never happen!
+ }
+ }
+
+ @Override
+ public int getComponentEnabledSetting(ComponentName componentName) {
+ try {
+ return mPM.getComponentEnabledSetting(componentName);
+ } catch (RemoteException e) {
+ // Should never happen!
+ }
+ return PackageManager.COMPONENT_ENABLED_STATE_DEFAULT;
+ }
+
+ @Override
+ public void setApplicationEnabledSetting(String packageName,
+ int newState, int flags) {
+ try {
+ mPM.setApplicationEnabledSetting(packageName, newState, flags);
+ } catch (RemoteException e) {
+ // Should never happen!
+ }
+ }
+
+ @Override
+ public int getApplicationEnabledSetting(String packageName) {
+ try {
+ return mPM.getApplicationEnabledSetting(packageName);
+ } catch (RemoteException e) {
+ // Should never happen!
+ }
+ return PackageManager.COMPONENT_ENABLED_STATE_DEFAULT;
+ }
+
+ private final ApplicationContext mContext;
+ private final IPackageManager mPM;
+
+ private static final Object sSync = new Object();
+ private static final Object sPackageRemovedSync = new Object();
+ private static BroadcastReceiver sPackageRemovedReceiver;
+ private static HashMap<ResourceName, WeakReference<Drawable> > sIconCache
+ = new HashMap<ResourceName, WeakReference<Drawable> >();
+ private static HashMap<ResourceName, WeakReference<CharSequence> > sStringCache
+ = new HashMap<ResourceName, WeakReference<CharSequence> >();
+ }
+
+ // ----------------------------------------------------------------------
+ // ----------------------------------------------------------------------
+ // ----------------------------------------------------------------------
+
+ private static final class SharedPreferencesImpl implements SharedPreferences {
+
+ private final File mFile;
+ private final int mMode;
+ private Map mMap;
+ private final FileStatus mFileStatus = new FileStatus();
+ private long mTimestamp;
+
+ private List<OnSharedPreferenceChangeListener> mListeners;
+
+ SharedPreferencesImpl(
+ File file, int mode, Map initialContents) {
+ mFile = file;
+ mMode = mode;
+ mMap = initialContents != null ? initialContents : new HashMap();
+ if (FileUtils.getFileStatus(file.getPath(), mFileStatus)) {
+ mTimestamp = mFileStatus.mtime;
+ }
+ mListeners = new ArrayList<OnSharedPreferenceChangeListener>();
+ }
+
+ public boolean hasFileChanged() {
+ synchronized (this) {
+ if (!FileUtils.getFileStatus(mFile.getPath(), mFileStatus)) {
+ return true;
+ }
+ return mTimestamp != mFileStatus.mtime;
+ }
+ }
+
+ public void replace(Map newContents) {
+ if (newContents != null) {
+ synchronized (this) {
+ mMap = newContents;
+ }
+ }
+ }
+
+ public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
+ synchronized(this) {
+ if (!mListeners.contains(listener)) {
+ mListeners.add(listener);
+ }
+ }
+ }
+
+ public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
+ synchronized(this) {
+ mListeners.remove(listener);
+ }
+ }
+
+ public Map<String, ?> getAll() {
+ synchronized(this) {
+ //noinspection unchecked
+ return new HashMap(mMap);
+ }
+ }
+
+ public String getString(String key, String defValue) {
+ synchronized (this) {
+ String v = (String)mMap.get(key);
+ return v != null ? v : defValue;
+ }
+ }
+
+ public int getInt(String key, int defValue) {
+ synchronized (this) {
+ Integer v = (Integer)mMap.get(key);
+ return v != null ? v : defValue;
+ }
+ }
+ public long getLong(String key, long defValue) {
+ synchronized (this) {
+ Long v = (Long) mMap.get(key);
+ return v != null ? v : defValue;
+ }
+ }
+ public float getFloat(String key, float defValue) {
+ synchronized (this) {
+ Float v = (Float)mMap.get(key);
+ return v != null ? v : defValue;
+ }
+ }
+ public boolean getBoolean(String key, boolean defValue) {
+ synchronized (this) {
+ Boolean v = (Boolean)mMap.get(key);
+ return v != null ? v : defValue;
+ }
+ }
+
+ public boolean contains(String key) {
+ synchronized (this) {
+ return mMap.containsKey(key);
+ }
+ }
+
+ public final class EditorImpl implements Editor {
+ private final Map<String, Object> mModified = Maps.newHashMap();
+ private boolean mClear = false;
+
+ public Editor putString(String key, String value) {
+ synchronized (this) {
+ mModified.put(key, value);
+ return this;
+ }
+ }
+ public Editor putInt(String key, int value) {
+ synchronized (this) {
+ mModified.put(key, value);
+ return this;
+ }
+ }
+ public Editor putLong(String key, long value) {
+ synchronized (this) {
+ mModified.put(key, value);
+ return this;
+ }
+ }
+ public Editor putFloat(String key, float value) {
+ synchronized (this) {
+ mModified.put(key, value);
+ return this;
+ }
+ }
+ public Editor putBoolean(String key, boolean value) {
+ synchronized (this) {
+ mModified.put(key, value);
+ return this;
+ }
+ }
+
+ public Editor remove(String key) {
+ synchronized (this) {
+ mModified.put(key, this);
+ return this;
+ }
+ }
+
+ public Editor clear() {
+ synchronized (this) {
+ mClear = true;
+ return this;
+ }
+ }
+
+ public boolean commit() {
+ boolean returnValue;
+
+ boolean hasListeners;
+ List<String> keysModified = null;
+ List<OnSharedPreferenceChangeListener> listeners = null;
+
+ synchronized (SharedPreferencesImpl.this) {
+ hasListeners = mListeners.size() > 0;
+ if (hasListeners) {
+ keysModified = new ArrayList<String>();
+ listeners = new ArrayList<OnSharedPreferenceChangeListener>(mListeners);
+ }
+
+ synchronized (this) {
+ if (mClear) {
+ mMap.clear();
+ mClear = false;
+ }
+
+ Iterator<Entry<String, Object>> it = mModified.entrySet().iterator();
+ while (it.hasNext()) {
+ Map.Entry<String, Object> e = it.next();
+ String k = e.getKey();
+ Object v = e.getValue();
+ if (v == this) {
+ mMap.remove(k);
+ } else {
+ mMap.put(k, v);
+ }
+
+ if (hasListeners) {
+ keysModified.add(k);
+ }
+ }
+
+ mModified.clear();
+ }
+
+ returnValue = writeFileLocked();
+ }
+
+ if (hasListeners) {
+ for (int i = keysModified.size() - 1; i >= 0; i--) {
+ final String key = keysModified.get(i);
+ // Call in the order they were registered
+ final int listenersSize = listeners.size();
+ for (int j = 0; j < listenersSize; j++) {
+ listeners.get(j).onSharedPreferenceChanged(SharedPreferencesImpl.this, key);
+ }
+ }
+ }
+
+ return returnValue;
+ }
+ }
+
+ public Editor edit() {
+ return new EditorImpl();
+ }
+
+ private boolean writeFileLocked() {
+ try {
+ FileOutputStream str;
+ try {
+ str = new FileOutputStream(mFile);
+ } catch (Exception e) {
+ File parent = mFile.getParentFile();
+ parent.mkdir();
+ FileUtils.setPermissions(
+ parent.toString(),
+ FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH,
+ -1, -1);
+ str = new FileOutputStream(mFile);
+ }
+ XmlUtils.writeMapXml(mMap, str);
+ str.close();
+ setFilePermissionsFromMode(mFile.toString(), mMode, 0);
+ if (FileUtils.getFileStatus(mFile.getPath(), mFileStatus)) {
+ mTimestamp = mFileStatus.mtime;
+ }
+ } catch (org.xmlpull.v1.XmlPullParserException e) {
+ } catch (java.io.FileNotFoundException e) {
+ } catch (java.io.IOException e) {
+ }
+ return false;
+ }
+ }
+
+ private static class WallpaperCallback extends IWallpaperServiceCallback.Stub {
+ private WeakReference<ApplicationContext> mContext;
+
+ public WallpaperCallback(ApplicationContext context) {
+ mContext = new WeakReference<ApplicationContext>(context);
+ }
+
+ public synchronized void onWallpaperChanged() {
+
+ /* The wallpaper has changed but we shouldn't eagerly load the
+ * wallpaper as that would be inefficient. Reset the cached wallpaper
+ * to null so if the user requests the wallpaper again then we'll
+ * fetch it.
+ */
+ final ApplicationContext applicationContext = mContext.get();
+ if (applicationContext != null) {
+ applicationContext.mWallpaper = null;
+ }
+ }
+ }
+}
diff --git a/core/java/android/app/ApplicationLoaders.java b/core/java/android/app/ApplicationLoaders.java
new file mode 100644
index 0000000..2e301c9
--- /dev/null
+++ b/core/java/android/app/ApplicationLoaders.java
@@ -0,0 +1,72 @@
+/*
+ * 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.app;
+
+import dalvik.system.PathClassLoader;
+
+import java.util.HashMap;
+
+class ApplicationLoaders
+{
+ public static ApplicationLoaders getDefault()
+ {
+ return gApplicationLoaders;
+ }
+
+ public ClassLoader getClassLoader(String zip, String appDataDir,
+ ClassLoader parent)
+ {
+ /*
+ * This is the parent we use if they pass "null" in. In theory
+ * this should be the "system" class loader; in practice we
+ * don't use that and can happily (and more efficiently) use the
+ * bootstrap class loader.
+ */
+ ClassLoader baseParent = ClassLoader.getSystemClassLoader().getParent();
+
+ synchronized (mLoaders) {
+ if (parent == null) {
+ parent = baseParent;
+ }
+
+ /*
+ * If we're one step up from the base class loader, find
+ * something in our cache. Otherwise, we create a whole
+ * new ClassLoader for the zip archive.
+ */
+ if (parent == baseParent) {
+ ClassLoader loader = (ClassLoader)mLoaders.get(zip);
+ if (loader != null) {
+ return loader;
+ }
+
+ PathClassLoader pathClassloader =
+ new PathClassLoader(zip, appDataDir + "/lib", parent);
+
+ mLoaders.put(zip, pathClassloader);
+ return pathClassloader;
+ }
+
+ return new PathClassLoader(zip, parent);
+ }
+ }
+
+ private final HashMap mLoaders = new HashMap();
+
+ private static final ApplicationLoaders gApplicationLoaders
+ = new ApplicationLoaders();
+}
diff --git a/core/java/android/app/ApplicationThreadNative.java b/core/java/android/app/ApplicationThreadNative.java
new file mode 100644
index 0000000..6a70329
--- /dev/null
+++ b/core/java/android/app/ApplicationThreadNative.java
@@ -0,0 +1,652 @@
+/*
+ * 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.app;
+
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.ProviderInfo;
+import android.content.pm.ServiceInfo;
+import android.content.res.Configuration;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.ParcelFileDescriptor;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/** {@hide} */
+public abstract class ApplicationThreadNative extends Binder
+ implements IApplicationThread {
+ /**
+ * Cast a Binder object into an application thread interface, generating
+ * a proxy if needed.
+ */
+ static public IApplicationThread asInterface(IBinder obj) {
+ if (obj == null) {
+ return null;
+ }
+ IApplicationThread in =
+ (IApplicationThread)obj.queryLocalInterface(descriptor);
+ if (in != null) {
+ return in;
+ }
+
+ return new ApplicationThreadProxy(obj);
+ }
+
+ public ApplicationThreadNative() {
+ attachInterface(this, descriptor);
+ }
+
+ @Override
+ public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
+ throws RemoteException {
+ switch (code) {
+ case SCHEDULE_PAUSE_ACTIVITY_TRANSACTION:
+ {
+ data.enforceInterface(IApplicationThread.descriptor);
+ IBinder b = data.readStrongBinder();
+ boolean finished = data.readInt() != 0;
+ int configChanges = data.readInt();
+ schedulePauseActivity(b, finished, configChanges);
+ return true;
+ }
+
+ case SCHEDULE_STOP_ACTIVITY_TRANSACTION:
+ {
+ data.enforceInterface(IApplicationThread.descriptor);
+ IBinder b = data.readStrongBinder();
+ boolean show = data.readInt() != 0;
+ int configChanges = data.readInt();
+ scheduleStopActivity(b, show, configChanges);
+ return true;
+ }
+
+ case SCHEDULE_WINDOW_VISIBILITY_TRANSACTION:
+ {
+ data.enforceInterface(IApplicationThread.descriptor);
+ IBinder b = data.readStrongBinder();
+ boolean show = data.readInt() != 0;
+ scheduleWindowVisibility(b, show);
+ return true;
+ }
+
+ case SCHEDULE_RESUME_ACTIVITY_TRANSACTION:
+ {
+ data.enforceInterface(IApplicationThread.descriptor);
+ IBinder b = data.readStrongBinder();
+ scheduleResumeActivity(b);
+ return true;
+ }
+
+ case SCHEDULE_SEND_RESULT_TRANSACTION:
+ {
+ data.enforceInterface(IApplicationThread.descriptor);
+ IBinder b = data.readStrongBinder();
+ List<ResultInfo> ri = data.createTypedArrayList(ResultInfo.CREATOR);
+ scheduleSendResult(b, ri);
+ return true;
+ }
+
+ case SCHEDULE_LAUNCH_ACTIVITY_TRANSACTION:
+ {
+ data.enforceInterface(IApplicationThread.descriptor);
+ Intent intent = Intent.CREATOR.createFromParcel(data);
+ IBinder b = data.readStrongBinder();
+ ActivityInfo info = ActivityInfo.CREATOR.createFromParcel(data);
+ Bundle state = data.readBundle();
+ List<ResultInfo> ri = data.createTypedArrayList(ResultInfo.CREATOR);
+ List<Intent> pi = data.createTypedArrayList(Intent.CREATOR);
+ boolean notResumed = data.readInt() != 0;
+ scheduleLaunchActivity(intent, b, info, state, ri, pi, notResumed);
+ return true;
+ }
+
+ case SCHEDULE_RELAUNCH_ACTIVITY_TRANSACTION:
+ {
+ data.enforceInterface(IApplicationThread.descriptor);
+ IBinder b = data.readStrongBinder();
+ List<ResultInfo> ri = data.createTypedArrayList(ResultInfo.CREATOR);
+ List<Intent> pi = data.createTypedArrayList(Intent.CREATOR);
+ int configChanges = data.readInt();
+ boolean notResumed = data.readInt() != 0;
+ scheduleRelaunchActivity(b, ri, pi, configChanges, notResumed);
+ return true;
+ }
+
+ case SCHEDULE_NEW_INTENT_TRANSACTION:
+ {
+ data.enforceInterface(IApplicationThread.descriptor);
+ List<Intent> pi = data.createTypedArrayList(Intent.CREATOR);
+ IBinder b = data.readStrongBinder();
+ scheduleNewIntent(pi, b);
+ return true;
+ }
+
+ case SCHEDULE_FINISH_ACTIVITY_TRANSACTION:
+ {
+ data.enforceInterface(IApplicationThread.descriptor);
+ IBinder b = data.readStrongBinder();
+ boolean finishing = data.readInt() != 0;
+ int configChanges = data.readInt();
+ scheduleDestroyActivity(b, finishing, configChanges);
+ return true;
+ }
+
+ case SCHEDULE_RECEIVER_TRANSACTION:
+ {
+ data.enforceInterface(IApplicationThread.descriptor);
+ Intent intent = Intent.CREATOR.createFromParcel(data);
+ ActivityInfo info = ActivityInfo.CREATOR.createFromParcel(data);
+ int resultCode = data.readInt();
+ String resultData = data.readString();
+ Bundle resultExtras = data.readBundle();
+ boolean sync = data.readInt() != 0;
+ scheduleReceiver(intent, info, resultCode, resultData,
+ resultExtras, sync);
+ return true;
+ }
+
+ case SCHEDULE_CREATE_SERVICE_TRANSACTION: {
+ data.enforceInterface(IApplicationThread.descriptor);
+ IBinder token = data.readStrongBinder();
+ ServiceInfo info = ServiceInfo.CREATOR.createFromParcel(data);
+ scheduleCreateService(token, info);
+ return true;
+ }
+
+ case SCHEDULE_BIND_SERVICE_TRANSACTION: {
+ data.enforceInterface(IApplicationThread.descriptor);
+ IBinder token = data.readStrongBinder();
+ Intent intent = Intent.CREATOR.createFromParcel(data);
+ boolean rebind = data.readInt() != 0;
+ scheduleBindService(token, intent, rebind);
+ return true;
+ }
+
+ case SCHEDULE_UNBIND_SERVICE_TRANSACTION: {
+ data.enforceInterface(IApplicationThread.descriptor);
+ IBinder token = data.readStrongBinder();
+ Intent intent = Intent.CREATOR.createFromParcel(data);
+ scheduleUnbindService(token, intent);
+ return true;
+ }
+
+ case SCHEDULE_SERVICE_ARGS_TRANSACTION:
+ {
+ data.enforceInterface(IApplicationThread.descriptor);
+ IBinder token = data.readStrongBinder();
+ int startId = data.readInt();
+ Intent args = Intent.CREATOR.createFromParcel(data);
+ scheduleServiceArgs(token, startId, args);
+ return true;
+ }
+
+ case SCHEDULE_STOP_SERVICE_TRANSACTION:
+ {
+ data.enforceInterface(IApplicationThread.descriptor);
+ IBinder token = data.readStrongBinder();
+ scheduleStopService(token);
+ return true;
+ }
+
+ case BIND_APPLICATION_TRANSACTION:
+ {
+ data.enforceInterface(IApplicationThread.descriptor);
+ String packageName = data.readString();
+ ApplicationInfo info =
+ ApplicationInfo.CREATOR.createFromParcel(data);
+ List<ProviderInfo> providers =
+ data.createTypedArrayList(ProviderInfo.CREATOR);
+ ComponentName testName = (data.readInt() != 0)
+ ? new ComponentName(data) : null;
+ String profileName = data.readString();
+ Bundle testArgs = data.readBundle();
+ IBinder binder = data.readStrongBinder();
+ IInstrumentationWatcher testWatcher = IInstrumentationWatcher.Stub.asInterface(binder);
+ int testMode = data.readInt();
+ Configuration config = Configuration.CREATOR.createFromParcel(data);
+ HashMap<String, IBinder> services = data.readHashMap(null);
+ bindApplication(packageName, info,
+ providers, testName, profileName,
+ testArgs, testWatcher, testMode, config, services);
+ return true;
+ }
+
+ case SCHEDULE_EXIT_TRANSACTION:
+ {
+ data.enforceInterface(IApplicationThread.descriptor);
+ scheduleExit();
+ return true;
+ }
+
+ case REQUEST_THUMBNAIL_TRANSACTION:
+ {
+ data.enforceInterface(IApplicationThread.descriptor);
+ IBinder b = data.readStrongBinder();
+ requestThumbnail(b);
+ return true;
+ }
+
+ case SCHEDULE_CONFIGURATION_CHANGED_TRANSACTION:
+ {
+ data.enforceInterface(IApplicationThread.descriptor);
+ Configuration config = Configuration.CREATOR.createFromParcel(data);
+ scheduleConfigurationChanged(config);
+ return true;
+ }
+
+ case UPDATE_TIME_ZONE_TRANSACTION: {
+ data.enforceInterface(IApplicationThread.descriptor);
+ updateTimeZone();
+ return true;
+ }
+
+ case PROCESS_IN_BACKGROUND_TRANSACTION: {
+ data.enforceInterface(IApplicationThread.descriptor);
+ processInBackground();
+ return true;
+ }
+
+ case DUMP_SERVICE_TRANSACTION: {
+ data.enforceInterface(IApplicationThread.descriptor);
+ ParcelFileDescriptor fd = data.readFileDescriptor();
+ final IBinder service = data.readStrongBinder();
+ final String[] args = data.readStringArray();
+ if (fd != null) {
+ dumpService(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(
+ data.readStrongBinder());
+ Intent intent = Intent.CREATOR.createFromParcel(data);
+ int resultCode = data.readInt();
+ String dataStr = data.readString();
+ Bundle extras = data.readBundle();
+ boolean ordered = data.readInt() != 0;
+ scheduleRegisteredReceiver(receiver, intent,
+ resultCode, dataStr, extras, ordered);
+ return true;
+ }
+
+ case SCHEDULE_LOW_MEMORY_TRANSACTION:
+ {
+ scheduleLowMemory();
+ return true;
+ }
+
+ case SCHEDULE_ACTIVITY_CONFIGURATION_CHANGED_TRANSACTION:
+ {
+ data.enforceInterface(IApplicationThread.descriptor);
+ IBinder b = data.readStrongBinder();
+ scheduleActivityConfigurationChanged(b);
+ return true;
+ }
+
+ case REQUEST_PSS_TRANSACTION:
+ {
+ data.enforceInterface(IApplicationThread.descriptor);
+ requestPss();
+ return true;
+ }
+ }
+
+ return super.onTransact(code, data, reply, flags);
+ }
+
+ public IBinder asBinder()
+ {
+ return this;
+ }
+}
+
+class ApplicationThreadProxy implements IApplicationThread {
+ private final IBinder mRemote;
+
+ public ApplicationThreadProxy(IBinder remote) {
+ mRemote = remote;
+ }
+
+ public final IBinder asBinder() {
+ return mRemote;
+ }
+
+ public final void schedulePauseActivity(IBinder token, boolean finished,
+ int configChanges) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ data.writeInterfaceToken(IApplicationThread.descriptor);
+ data.writeStrongBinder(token);
+ data.writeInt(finished ? 1 : 0);
+ data.writeInt(configChanges);
+ mRemote.transact(SCHEDULE_PAUSE_ACTIVITY_TRANSACTION, data, null,
+ IBinder.FLAG_ONEWAY);
+ data.recycle();
+ }
+
+ public final void scheduleStopActivity(IBinder token, boolean showWindow,
+ int configChanges) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ data.writeInterfaceToken(IApplicationThread.descriptor);
+ data.writeStrongBinder(token);
+ data.writeInt(showWindow ? 1 : 0);
+ data.writeInt(configChanges);
+ mRemote.transact(SCHEDULE_STOP_ACTIVITY_TRANSACTION, data, null,
+ IBinder.FLAG_ONEWAY);
+ data.recycle();
+ }
+
+ public final void scheduleWindowVisibility(IBinder token,
+ boolean showWindow) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ data.writeInterfaceToken(IApplicationThread.descriptor);
+ data.writeStrongBinder(token);
+ data.writeInt(showWindow ? 1 : 0);
+ mRemote.transact(SCHEDULE_WINDOW_VISIBILITY_TRANSACTION, data, null,
+ IBinder.FLAG_ONEWAY);
+ data.recycle();
+ }
+
+ public final void scheduleResumeActivity(IBinder token)
+ throws RemoteException {
+ Parcel data = Parcel.obtain();
+ data.writeInterfaceToken(IApplicationThread.descriptor);
+ data.writeStrongBinder(token);
+ mRemote.transact(SCHEDULE_RESUME_ACTIVITY_TRANSACTION, data, null,
+ IBinder.FLAG_ONEWAY);
+ data.recycle();
+ }
+
+ public final void scheduleSendResult(IBinder token, List<ResultInfo> results)
+ throws RemoteException {
+ Parcel data = Parcel.obtain();
+ data.writeInterfaceToken(IApplicationThread.descriptor);
+ data.writeStrongBinder(token);
+ data.writeTypedList(results);
+ mRemote.transact(SCHEDULE_SEND_RESULT_TRANSACTION, data, null,
+ IBinder.FLAG_ONEWAY);
+ data.recycle();
+ }
+
+ public final void scheduleLaunchActivity(Intent intent, IBinder token,
+ ActivityInfo info, Bundle state, List<ResultInfo> pendingResults,
+ List<Intent> pendingNewIntents, boolean notResumed)
+ throws RemoteException {
+ Parcel data = Parcel.obtain();
+ data.writeInterfaceToken(IApplicationThread.descriptor);
+ intent.writeToParcel(data, 0);
+ data.writeStrongBinder(token);
+ info.writeToParcel(data, 0);
+ data.writeBundle(state);
+ data.writeTypedList(pendingResults);
+ data.writeTypedList(pendingNewIntents);
+ data.writeInt(notResumed ? 1 : 0);
+ mRemote.transact(SCHEDULE_LAUNCH_ACTIVITY_TRANSACTION, data, null,
+ IBinder.FLAG_ONEWAY);
+ data.recycle();
+ }
+
+ public final void scheduleRelaunchActivity(IBinder token,
+ List<ResultInfo> pendingResults, List<Intent> pendingNewIntents,
+ int configChanges, boolean notResumed) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ data.writeInterfaceToken(IApplicationThread.descriptor);
+ data.writeStrongBinder(token);
+ data.writeTypedList(pendingResults);
+ data.writeTypedList(pendingNewIntents);
+ data.writeInt(configChanges);
+ data.writeInt(notResumed ? 1 : 0);
+ mRemote.transact(SCHEDULE_RELAUNCH_ACTIVITY_TRANSACTION, data, null,
+ IBinder.FLAG_ONEWAY);
+ data.recycle();
+ }
+
+ public void scheduleNewIntent(List<Intent> intents, IBinder token)
+ throws RemoteException {
+ Parcel data = Parcel.obtain();
+ data.writeInterfaceToken(IApplicationThread.descriptor);
+ data.writeTypedList(intents);
+ data.writeStrongBinder(token);
+ mRemote.transact(SCHEDULE_NEW_INTENT_TRANSACTION, data, null,
+ IBinder.FLAG_ONEWAY);
+ data.recycle();
+ }
+
+ public final void scheduleDestroyActivity(IBinder token, boolean finishing,
+ int configChanges) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ data.writeInterfaceToken(IApplicationThread.descriptor);
+ data.writeStrongBinder(token);
+ data.writeInt(finishing ? 1 : 0);
+ data.writeInt(configChanges);
+ mRemote.transact(SCHEDULE_FINISH_ACTIVITY_TRANSACTION, data, null,
+ IBinder.FLAG_ONEWAY);
+ data.recycle();
+ }
+
+ public final void scheduleReceiver(Intent intent, ActivityInfo info,
+ int resultCode, String resultData,
+ Bundle map, boolean sync) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ data.writeInterfaceToken(IApplicationThread.descriptor);
+ intent.writeToParcel(data, 0);
+ info.writeToParcel(data, 0);
+ data.writeInt(resultCode);
+ data.writeString(resultData);
+ data.writeBundle(map);
+ data.writeInt(sync ? 1 : 0);
+ mRemote.transact(SCHEDULE_RECEIVER_TRANSACTION, data, null,
+ IBinder.FLAG_ONEWAY);
+ data.recycle();
+ }
+
+ public final void scheduleCreateService(IBinder token, ServiceInfo info)
+ throws RemoteException {
+ Parcel data = Parcel.obtain();
+ data.writeInterfaceToken(IApplicationThread.descriptor);
+ data.writeStrongBinder(token);
+ info.writeToParcel(data, 0);
+ mRemote.transact(SCHEDULE_CREATE_SERVICE_TRANSACTION, data, null,
+ IBinder.FLAG_ONEWAY);
+ data.recycle();
+ }
+
+ public final void scheduleBindService(IBinder token, Intent intent, boolean rebind)
+ throws RemoteException {
+ Parcel data = Parcel.obtain();
+ data.writeInterfaceToken(IApplicationThread.descriptor);
+ data.writeStrongBinder(token);
+ intent.writeToParcel(data, 0);
+ data.writeInt(rebind ? 1 : 0);
+ mRemote.transact(SCHEDULE_BIND_SERVICE_TRANSACTION, data, null,
+ IBinder.FLAG_ONEWAY);
+ data.recycle();
+ }
+
+ public final void scheduleUnbindService(IBinder token, Intent intent)
+ throws RemoteException {
+ Parcel data = Parcel.obtain();
+ data.writeInterfaceToken(IApplicationThread.descriptor);
+ data.writeStrongBinder(token);
+ intent.writeToParcel(data, 0);
+ mRemote.transact(SCHEDULE_UNBIND_SERVICE_TRANSACTION, data, null,
+ IBinder.FLAG_ONEWAY);
+ data.recycle();
+ }
+
+ public final void scheduleServiceArgs(IBinder token, int startId,
+ Intent args) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ data.writeInterfaceToken(IApplicationThread.descriptor);
+ data.writeStrongBinder(token);
+ data.writeInt(startId);
+ args.writeToParcel(data, 0);
+ mRemote.transact(SCHEDULE_SERVICE_ARGS_TRANSACTION, data, null,
+ IBinder.FLAG_ONEWAY);
+ data.recycle();
+ }
+
+ public final void scheduleStopService(IBinder token)
+ throws RemoteException {
+ Parcel data = Parcel.obtain();
+ data.writeInterfaceToken(IApplicationThread.descriptor);
+ data.writeStrongBinder(token);
+ mRemote.transact(SCHEDULE_STOP_SERVICE_TRANSACTION, data, null,
+ IBinder.FLAG_ONEWAY);
+ data.recycle();
+ }
+
+ public final void bindApplication(String packageName, ApplicationInfo info,
+ List<ProviderInfo> providers, ComponentName testName,
+ String profileName, Bundle testArgs, IInstrumentationWatcher testWatcher, int debugMode,
+ Configuration config, Map<String, IBinder> services) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ data.writeInterfaceToken(IApplicationThread.descriptor);
+ data.writeString(packageName);
+ info.writeToParcel(data, 0);
+ data.writeTypedList(providers);
+ if (testName == null) {
+ data.writeInt(0);
+ } else {
+ data.writeInt(1);
+ testName.writeToParcel(data, 0);
+ }
+ data.writeString(profileName);
+ data.writeBundle(testArgs);
+ data.writeStrongInterface(testWatcher);
+ data.writeInt(debugMode);
+ config.writeToParcel(data, 0);
+ data.writeMap(services);
+ mRemote.transact(BIND_APPLICATION_TRANSACTION, data, null,
+ IBinder.FLAG_ONEWAY);
+ data.recycle();
+ }
+
+ public final void scheduleExit() throws RemoteException {
+ Parcel data = Parcel.obtain();
+ data.writeInterfaceToken(IApplicationThread.descriptor);
+ mRemote.transact(SCHEDULE_EXIT_TRANSACTION, data, null,
+ IBinder.FLAG_ONEWAY);
+ data.recycle();
+ }
+
+ public final void requestThumbnail(IBinder token)
+ throws RemoteException {
+ Parcel data = Parcel.obtain();
+ data.writeInterfaceToken(IApplicationThread.descriptor);
+ data.writeStrongBinder(token);
+ mRemote.transact(REQUEST_THUMBNAIL_TRANSACTION, data, null,
+ IBinder.FLAG_ONEWAY);
+ data.recycle();
+ }
+
+ public final void scheduleConfigurationChanged(Configuration config)
+ throws RemoteException {
+ Parcel data = Parcel.obtain();
+ data.writeInterfaceToken(IApplicationThread.descriptor);
+ config.writeToParcel(data, 0);
+ mRemote.transact(SCHEDULE_CONFIGURATION_CHANGED_TRANSACTION, data, null,
+ IBinder.FLAG_ONEWAY);
+ data.recycle();
+ }
+
+ public void updateTimeZone() throws RemoteException {
+ Parcel data = Parcel.obtain();
+ data.writeInterfaceToken(IApplicationThread.descriptor);
+ mRemote.transact(UPDATE_TIME_ZONE_TRANSACTION, data, null,
+ IBinder.FLAG_ONEWAY);
+ data.recycle();
+ }
+
+ public void processInBackground() throws RemoteException {
+ Parcel data = Parcel.obtain();
+ data.writeInterfaceToken(IApplicationThread.descriptor);
+ mRemote.transact(PROCESS_IN_BACKGROUND_TRANSACTION, data, null,
+ IBinder.FLAG_ONEWAY);
+ data.recycle();
+ }
+
+ public void dumpService(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_SERVICE_TRANSACTION, data, null, 0);
+ data.recycle();
+ }
+
+ public void scheduleRegisteredReceiver(IIntentReceiver receiver, Intent intent,
+ int resultCode, String dataStr, Bundle extras, boolean ordered)
+ throws RemoteException {
+ Parcel data = Parcel.obtain();
+ data.writeInterfaceToken(IApplicationThread.descriptor);
+ data.writeStrongBinder(receiver.asBinder());
+ intent.writeToParcel(data, 0);
+ data.writeInt(resultCode);
+ data.writeString(dataStr);
+ data.writeBundle(extras);
+ data.writeInt(ordered ? 1 : 0);
+ mRemote.transact(SCHEDULE_REGISTERED_RECEIVER_TRANSACTION, data, null,
+ IBinder.FLAG_ONEWAY);
+ data.recycle();
+ }
+
+ public final void scheduleLowMemory() throws RemoteException {
+ Parcel data = Parcel.obtain();
+ data.writeInterfaceToken(IApplicationThread.descriptor);
+ mRemote.transact(SCHEDULE_LOW_MEMORY_TRANSACTION, data, null,
+ IBinder.FLAG_ONEWAY);
+ data.recycle();
+ }
+
+ public final void scheduleActivityConfigurationChanged(
+ IBinder token) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ data.writeInterfaceToken(IApplicationThread.descriptor);
+ data.writeStrongBinder(token);
+ mRemote.transact(SCHEDULE_ACTIVITY_CONFIGURATION_CHANGED_TRANSACTION, data, null,
+ IBinder.FLAG_ONEWAY);
+ data.recycle();
+ }
+
+ public final void requestPss() throws RemoteException {
+ Parcel data = Parcel.obtain();
+ data.writeInterfaceToken(IApplicationThread.descriptor);
+ mRemote.transact(REQUEST_PSS_TRANSACTION, data, null,
+ IBinder.FLAG_ONEWAY);
+ data.recycle();
+ }
+
+}
+
diff --git a/core/java/android/app/DatePickerDialog.java b/core/java/android/app/DatePickerDialog.java
new file mode 100644
index 0000000..7450559
--- /dev/null
+++ b/core/java/android/app/DatePickerDialog.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.os.Bundle;
+import android.pim.DateFormat;
+import android.text.TextUtils.TruncateAt;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.DatePicker;
+import android.widget.TextView;
+import android.widget.DatePicker.OnDateChangedListener;
+
+import com.android.internal.R;
+
+import java.text.DateFormatSymbols;
+import java.util.Calendar;
+
+/**
+ * A simple dialog containing an {@link android.widget.DatePicker}.
+ */
+public class DatePickerDialog extends AlertDialog implements OnClickListener,
+ OnDateChangedListener {
+
+ private static final String YEAR = "year";
+ private static final String MONTH = "month";
+ private static final String DAY = "day";
+
+ private final DatePicker mDatePicker;
+ private final OnDateSetListener mCallBack;
+ private final Calendar mCalendar;
+ private final java.text.DateFormat mDateFormat;
+ private final String[] mWeekDays;
+
+ private int mInitialYear;
+ private int mInitialMonth;
+ private int mInitialDay;
+
+ /**
+ * The callback used to indicate the user is done filling in the date.
+ */
+ public interface OnDateSetListener {
+
+ /**
+ * @param view The view associated with this listener.
+ * @param year The year that was set.
+ * @param monthOfYear The month that was set (0-11) for compatibility
+ * with {@link java.util.Calendar}.
+ * @param dayOfMonth The day of the month that was set.
+ */
+ void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth);
+ }
+
+ /**
+ * @param context The context the dialog is to run in.
+ * @param callBack How the parent is notified that the date is set.
+ * @param year The initial year of the dialog.
+ * @param monthOfYear The initial month of the dialog.
+ * @param dayOfMonth The initial day of the dialog.
+ */
+ public DatePickerDialog(Context context,
+ OnDateSetListener callBack,
+ int year,
+ int monthOfYear,
+ int dayOfMonth) {
+ this(context, com.android.internal.R.style.Theme_Dialog_Alert,
+ callBack, year, monthOfYear, dayOfMonth);
+ }
+
+ /**
+ * @param context The context the dialog is to run in.
+ * @param theme the theme to apply to this dialog
+ * @param callBack How the parent is notified that the date is set.
+ * @param year The initial year of the dialog.
+ * @param monthOfYear The initial month of the dialog.
+ * @param dayOfMonth The initial day of the dialog.
+ */
+ public DatePickerDialog(Context context,
+ int theme,
+ OnDateSetListener callBack,
+ int year,
+ int monthOfYear,
+ int dayOfMonth) {
+ super(context, theme);
+
+ mCallBack = callBack;
+ mInitialYear = year;
+ mInitialMonth = monthOfYear;
+ mInitialDay = dayOfMonth;
+ DateFormatSymbols symbols = new DateFormatSymbols();
+ mWeekDays = symbols.getShortWeekdays();
+
+ mDateFormat = DateFormat.getLongDateFormat(context);
+ mCalendar = Calendar.getInstance();
+ updateTitle(mInitialYear, mInitialMonth, mInitialDay);
+
+ setButton(context.getText(R.string.date_time_set), this);
+ setButton2(context.getText(R.string.cancel), (OnClickListener) null);
+ setIcon(R.drawable.ic_dialog_time);
+
+ LayoutInflater inflater =
+ (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View view = inflater.inflate(R.layout.date_picker_dialog, null);
+ setView(view);
+ mDatePicker = (DatePicker) view.findViewById(R.id.datePicker);
+ mDatePicker.init(mInitialYear, mInitialMonth, mInitialDay, this);
+ }
+
+ @Override
+ public void show() {
+ super.show();
+
+ /* Sometimes the full month is displayed causing the title
+ * to be very long, in those cases ensure it doesn't wrap to
+ * 2 lines (as that looks jumpy) and ensure we ellipsize the end.
+ */
+ TextView title = (TextView) findViewById(R.id.alertTitle);
+ title.setSingleLine();
+ title.setEllipsize(TruncateAt.END);
+ }
+
+ public void onClick(DialogInterface dialog, int which) {
+ if (mCallBack != null) {
+ mDatePicker.clearFocus();
+ mCallBack.onDateSet(mDatePicker, mDatePicker.getYear(),
+ mDatePicker.getMonth(), mDatePicker.getDayOfMonth());
+ }
+ }
+
+ public void onDateChanged(DatePicker view, int year,
+ int month, int day) {
+ updateTitle(year, month, day);
+ }
+
+ public void updateDate(int year, int monthOfYear, int dayOfMonth) {
+ mInitialYear = year;
+ mInitialMonth = monthOfYear;
+ mInitialDay = dayOfMonth;
+ mDatePicker.updateDate(year, monthOfYear, dayOfMonth);
+ }
+
+ private void updateTitle(int year, int month, int day) {
+ mCalendar.set(Calendar.YEAR, year);
+ mCalendar.set(Calendar.MONTH, month);
+ mCalendar.set(Calendar.DAY_OF_MONTH, day);
+ String weekday = mWeekDays[mCalendar.get(Calendar.DAY_OF_WEEK)];
+ setTitle(weekday + ", " + mDateFormat.format(mCalendar.getTime()));
+ }
+
+ @Override
+ public Bundle onSaveInstanceState() {
+ Bundle state = super.onSaveInstanceState();
+ state.putInt(YEAR, mDatePicker.getYear());
+ state.putInt(MONTH, mDatePicker.getMonth());
+ state.putInt(DAY, mDatePicker.getDayOfMonth());
+ return state;
+ }
+
+ @Override
+ public void onRestoreInstanceState(Bundle savedInstanceState) {
+ super.onRestoreInstanceState(savedInstanceState);
+ int year = savedInstanceState.getInt(YEAR);
+ int month = savedInstanceState.getInt(MONTH);
+ int day = savedInstanceState.getInt(DAY);
+ mDatePicker.init(year, month, day, this);
+ updateTitle(year, month, day);
+ }
+}
diff --git a/core/java/android/app/Dialog.java b/core/java/android/app/Dialog.java
new file mode 100644
index 0000000..f1d2e65
--- /dev/null
+++ b/core/java/android/app/Dialog.java
@@ -0,0 +1,935 @@
+/*
+ * 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.app;
+
+import android.content.Context;
+import android.content.DialogInterface;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Bundle;
+import android.util.Config;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.ContextThemeWrapper;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.LayoutInflater;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.View.OnCreateContextMenuListener;
+
+import com.android.internal.policy.PolicyManager;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * Base class for Dialogs.
+ *
+ * Note: Activities provide a facility to manage the creation, saving and
+ * restoring of dialogs. See {@link Activity#onCreateDialog(int)},
+ * {@link Activity#onPrepareDialog(int, Dialog)},
+ * {@link Activity#showDialog(int)}, and {@link Activity#dismissDialog(int)}. If
+ * these methods are used, {@link #getOwnerActivity()} will return the Activity
+ * that managed this dialog.
+ *
+ */
+public class Dialog implements DialogInterface, Window.Callback,
+ KeyEvent.Callback, OnCreateContextMenuListener {
+ private static final String LOG_TAG = "Dialog";
+
+ private Activity mOwnerActivity;
+
+ final Context mContext;
+ final WindowManager mWindowManager;
+ Window mWindow;
+ View mDecor;
+ /**
+ * This field should be made private, so it is hidden from the SDK.
+ * {@hide}
+ */
+ protected boolean mCancelable = true;
+ private Message mCancelMessage;
+ private Message mDismissMessage;
+
+ /**
+ * Whether to cancel the dialog when a touch is received outside of the
+ * window's bounds.
+ */
+ private boolean mCanceledOnTouchOutside = false;
+
+ private OnKeyListener mOnKeyListener;
+
+ private boolean mCreated = false;
+ private boolean mShowing = false;
+
+ private final Thread mUiThread;
+ private final Handler mHandler = new Handler();
+
+ private final Runnable mDismissAction = new Runnable() {
+ public void run() {
+ dismissDialog();
+ }
+ };
+
+ /**
+ * Create a Dialog window that uses the default dialog frame style.
+ *
+ * @param context The Context the Dialog is to run it. In particular, it
+ * uses the window manager and theme in this context to
+ * present its UI.
+ */
+ public Dialog(Context context) {
+ this(context, 0);
+ }
+
+ /**
+ * Create a Dialog window that uses a custom dialog style.
+ *
+ * @param context The Context in which the Dialog should run. In particular, it
+ * uses the window manager and theme from this context to
+ * present its UI.
+ * @param theme A style resource describing the theme to use for the
+ * window. See <a href="{@docRoot}reference/available-resources.html#stylesandthemes">Style
+ * and Theme Resources</a> for more information about defining and using
+ * styles. This theme is applied on top of the current theme in
+ * <var>context</var>. If 0, the default dialog theme will be used.
+ */
+ public Dialog(Context context, int theme) {
+ mContext = new ContextThemeWrapper(
+ context, theme == 0 ? com.android.internal.R.style.Theme_Dialog : theme);
+ mWindowManager = (WindowManager)context.getSystemService("window");
+ Window w = PolicyManager.makeNewWindow(mContext);
+ mWindow = w;
+ w.setCallback(this);
+ w.setWindowManager(mWindowManager, null, null);
+ w.setGravity(Gravity.CENTER);
+ mUiThread = Thread.currentThread();
+ mDismissCancelHandler = new DismissCancelHandler(this);
+ }
+
+ /**
+ * @deprecated
+ * @hide
+ */
+ @Deprecated
+ protected Dialog(Context context, boolean cancelable,
+ Message cancelCallback) {
+ this(context);
+ mCancelable = cancelable;
+ mCancelMessage = cancelCallback;
+ }
+
+ protected Dialog(Context context, boolean cancelable,
+ OnCancelListener cancelListener) {
+ this(context);
+ mCancelable = cancelable;
+ setOnCancelListener(cancelListener);
+ }
+
+ /**
+ * Retrieve the Context this Dialog is running in.
+ *
+ * @return Context The Context that was supplied to the constructor.
+ */
+ public final Context getContext() {
+ return mContext;
+ }
+
+ /**
+ * Sets the Activity that owns this dialog. An example use: This Dialog will
+ * use the suggested volume control stream of the Activity.
+ *
+ * @param activity The Activity that owns this dialog.
+ */
+ public final void setOwnerActivity(Activity activity) {
+ mOwnerActivity = activity;
+
+ getWindow().setVolumeControlStream(mOwnerActivity.getVolumeControlStream());
+ }
+
+ /**
+ * Returns the Activity that owns this Dialog. For example, if
+ * {@link Activity#showDialog(int)} is used to show this Dialog, that
+ * Activity will be the owner (by default). Depending on how this dialog was
+ * created, this may return null.
+ *
+ * @return The Activity that owns this Dialog.
+ */
+ public final Activity getOwnerActivity() {
+ return mOwnerActivity;
+ }
+
+ /**
+ * @return Whether the dialog is currently showing.
+ */
+ public boolean isShowing() {
+ return mShowing;
+ }
+
+ /**
+ * Start the dialog and display it on screen. The window is placed in the
+ * application layer and opaque. Note that you should not override this
+ * method to do initialization when the dialog is shown, instead implement
+ * that in {@link #onStart}.
+ */
+ public void show() {
+ if (mShowing) {
+ if (Config.LOGV) Log.v(LOG_TAG,
+ "[Dialog] start: already showing, ignore");
+ if (mDecor != null) mDecor.setVisibility(View.VISIBLE);
+ return;
+ }
+
+ if (!mCreated) {
+ dispatchOnCreate(null);
+ }
+
+ onStart();
+ mDecor = mWindow.getDecorView();
+ mWindowManager.addView(mDecor, mWindow.getAttributes());
+ mShowing = true;
+ }
+
+ /**
+ * Hide the dialog, but do not dismiss it.
+ */
+ public void hide() {
+ if (mDecor != null) mDecor.setVisibility(View.GONE);
+ }
+
+ /**
+ * Dismiss this dialog, removing it from the screen. This method can be
+ * invoked safely from any thread. Note that you should not override this
+ * method to do cleanup when the dialog is dismissed, instead implement
+ * that in {@link #onStop}.
+ */
+ public void dismiss() {
+ if (Thread.currentThread() != mUiThread) {
+ mHandler.post(mDismissAction);
+ } else {
+ mDismissAction.run();
+ }
+ }
+
+ private void dismissDialog() {
+ if (mDecor == null) {
+ if (Config.LOGV) Log.v(LOG_TAG,
+ "[Dialog] dismiss: already dismissed, ignore");
+ return;
+ }
+ if (!mShowing) {
+ if (Config.LOGV) Log.v(LOG_TAG,
+ "[Dialog] dismiss: not showing, ignore");
+ return;
+ }
+
+ mWindowManager.removeView(mDecor);
+ mDecor = null;
+ mWindow.closeAllPanels();
+ onStop();
+ mShowing = false;
+
+ sendDismissMessage();
+ }
+
+ private void sendDismissMessage() {
+ if (mDismissMessage != null) {
+ // Obtain a new message so this dialog can be re-used
+ Message.obtain(mDismissMessage).sendToTarget();
+ }
+ }
+
+ // internal method to make sure mcreated is set properly without requiring
+ // users to call through to super in onCreate
+ void dispatchOnCreate(Bundle savedInstanceState) {
+ onCreate(savedInstanceState);
+ mCreated = true;
+ }
+
+ /**
+ * Similar to {@link Activity#onCreate}, you should initialized your dialog
+ * in this method, including calling {@link #setContentView}.
+ * @param savedInstanceState If this dialog is being reinitalized after a
+ * the hosting activity was previously shut down, holds the result from
+ * the most recent call to {@link #onSaveInstanceState}, or null if this
+ * is the first time.
+ */
+ protected void onCreate(Bundle savedInstanceState) {
+ }
+
+ /**
+ * Called when the dialog is starting.
+ */
+ protected void onStart() {
+ }
+
+ /**
+ * Called to tell you that you're stopping.
+ */
+ protected void onStop() {
+ }
+
+ private static final String DIALOG_SHOWING_TAG = "android:dialogShowing";
+ private static final String DIALOG_HIERARCHY_TAG = "android:dialogHierarchy";
+
+ /**
+ * Saves the state of the dialog into a bundle.
+ *
+ * The default implementation saves the state of its view hierarchy, so you'll
+ * likely want to call through to super if you override this to save additional
+ * state.
+ * @return A bundle with the state of the dialog.
+ */
+ public Bundle onSaveInstanceState() {
+ Bundle bundle = new Bundle();
+ bundle.putBoolean(DIALOG_SHOWING_TAG, mShowing);
+ if (mCreated) {
+ bundle.putBundle(DIALOG_HIERARCHY_TAG, mWindow.saveHierarchyState());
+ }
+ return bundle;
+ }
+
+ /**
+ * Restore the state of the dialog from a previously saved bundle.
+ *
+ * The default implementation restores the state of the dialog's view
+ * hierarchy that was saved in the default implementation of {@link #onSaveInstanceState()},
+ * so be sure to call through to super when overriding unless you want to
+ * do all restoring of state yourself.
+ * @param savedInstanceState The state of the dialog previously saved by
+ * {@link #onSaveInstanceState()}.
+ */
+ public void onRestoreInstanceState(Bundle savedInstanceState) {
+ final Bundle dialogHierarchyState = savedInstanceState.getBundle(DIALOG_HIERARCHY_TAG);
+ if (dialogHierarchyState == null) {
+ // dialog has never been shown, or onCreated, nothing to restore.
+ return;
+ }
+ dispatchOnCreate(savedInstanceState);
+ mWindow.restoreHierarchyState(dialogHierarchyState);
+ if (savedInstanceState.getBoolean(DIALOG_SHOWING_TAG)) {
+ show();
+ }
+ }
+
+ /**
+ * Retrieve the current Window for the activity. This can be used to
+ * directly access parts of the Window API that are not available
+ * through Activity/Screen.
+ *
+ * @return Window The current window, or null if the activity is not
+ * visual.
+ */
+ public Window getWindow() {
+ return mWindow;
+ }
+
+ /**
+ * Call {@link android.view.Window#getCurrentFocus} on the
+ * Window if this Activity to return the currently focused view.
+ *
+ * @return View The current View with focus or null.
+ *
+ * @see #getWindow
+ * @see android.view.Window#getCurrentFocus
+ */
+ public View getCurrentFocus() {
+ return mWindow != null ? mWindow.getCurrentFocus() : null;
+ }
+
+ /**
+ * Finds a view that was identified by the id attribute from the XML that
+ * was processed in {@link #onStart}.
+ *
+ * @param id the identifier of the view to find
+ * @return The view if found or null otherwise.
+ */
+ public View findViewById(int id) {
+ return mWindow.findViewById(id);
+ }
+
+ /**
+ * Set the screen content from a layout resource. The resource will be
+ * inflated, adding all top-level views to the screen.
+ *
+ * @param layoutResID Resource ID to be inflated.
+ */
+ public void setContentView(int layoutResID) {
+ mWindow.setContentView(layoutResID);
+ }
+
+ /**
+ * Set the screen content to an explicit view. This view is placed
+ * directly into the screen's view hierarchy. It can itself be a complex
+ * view hierarhcy.
+ *
+ * @param view The desired content to display.
+ */
+ public void setContentView(View view) {
+ mWindow.setContentView(view);
+ }
+
+ /**
+ * Set the screen content to an explicit view. This view is placed
+ * directly into the screen's view hierarchy. It can itself be a complex
+ * view hierarhcy.
+ *
+ * @param view The desired content to display.
+ * @param params Layout parameters for the view.
+ */
+ public void setContentView(View view, ViewGroup.LayoutParams params) {
+ mWindow.setContentView(view, params);
+ }
+
+ /**
+ * Add an additional content view to the screen. Added after any existing
+ * ones in the screen -- existing views are NOT removed.
+ *
+ * @param view The desired content to display.
+ * @param params Layout parameters for the view.
+ */
+ public void addContentView(View view, ViewGroup.LayoutParams params) {
+ mWindow.addContentView(view, params);
+ }
+
+ /**
+ * Set the title text for this dialog's window.
+ *
+ * @param title The new text to display in the title.
+ */
+ public void setTitle(CharSequence title) {
+ mWindow.setTitle(title);
+ mWindow.getAttributes().setTitle(title);
+ }
+
+ /**
+ * Set the title text for this dialog's window. The text is retrieved
+ * from the resources with the supplied identifier.
+ *
+ * @param titleId the title's text resource identifier
+ */
+ public void setTitle(int titleId) {
+ setTitle(mContext.getText(titleId));
+ }
+
+ /**
+ * A key was pressed down.
+ *
+ * <p>If the focused view didn't want this event, this method is called.
+ *
+ * <p>The default implementation handles KEYCODE_BACK to close the
+ * dialog.
+ *
+ * @see #onKeyUp
+ * @see android.view.KeyEvent
+ */
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ if (mCancelable) {
+ cancel();
+ }
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * A key was released.
+ *
+ * @see #onKeyDown
+ * @see KeyEvent
+ */
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ return false;
+ }
+
+ /**
+ * Default implementation of {@link KeyEvent.Callback#onKeyMultiple(int, int, KeyEvent)
+ * KeyEvent.Callback.onKeyMultiple()}: always returns false (doesn't handle
+ * the event).
+ */
+ public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
+ return false;
+ }
+
+ /**
+ * Called when a touch screen event was not handled by any of the views
+ * under it. This is most useful to process touch events that happen outside
+ * of your window bounds, where there is no view to receive it.
+ *
+ * @param event The touch screen event being processed.
+ * @return Return true if you have consumed the event, false if you haven't.
+ * The default implementation will cancel the dialog when a touch
+ * happens outside of the window bounds.
+ */
+ public boolean onTouchEvent(MotionEvent event) {
+ if (mCancelable && mCanceledOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN
+ && isOutOfBounds(event)) {
+ cancel();
+ return true;
+ }
+
+ return false;
+ }
+
+ private boolean isOutOfBounds(MotionEvent event) {
+ final int x = (int) event.getX();
+ final int y = (int) event.getY();
+ final int slop = ViewConfiguration.getWindowTouchSlop();
+ final View decorView = getWindow().getDecorView();
+ return (x < -slop) || (y < -slop)
+ || (x > (decorView.getWidth()+slop))
+ || (y > (decorView.getHeight()+slop));
+ }
+
+ /**
+ * Called when the trackball was moved and not handled by any of the
+ * views inside of the activity. So, for example, if the trackball moves
+ * while focus is on a button, you will receive a call here because
+ * buttons do not normally do anything with trackball events. The call
+ * here happens <em>before</em> trackball movements are converted to
+ * DPAD key events, which then get sent back to the view hierarchy, and
+ * will be processed at the point for things like focus navigation.
+ *
+ * @param event The trackball event being processed.
+ *
+ * @return Return true if you have consumed the event, false if you haven't.
+ * The default implementation always returns false.
+ */
+ public boolean onTrackballEvent(MotionEvent event) {
+ return false;
+ }
+
+ public void onWindowAttributesChanged(WindowManager.LayoutParams params) {
+ if (mDecor != null) {
+ mWindowManager.updateViewLayout(mDecor, params);
+ }
+ }
+
+ public void onContentChanged() {
+ }
+
+ public void onWindowFocusChanged(boolean hasFocus) {
+ }
+
+ /**
+ * Called to process key events. You can override this to intercept all
+ * key events before they are dispatched to the window. Be sure to call
+ * this implementation for key events that should be handled normally.
+ *
+ * @param event The key event.
+ *
+ * @return boolean Return true if this event was consumed.
+ */
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ if ((mOnKeyListener != null) && (mOnKeyListener.onKey(this, event.getKeyCode(), event))) {
+ return true;
+ }
+ if (mWindow.superDispatchKeyEvent(event)) {
+ return true;
+ }
+ return event.dispatch(this);
+ }
+
+ /**
+ * Called to process touch screen events. You can override this to
+ * intercept all touch screen events before they are dispatched to the
+ * window. Be sure to call this implementation for touch screen events
+ * that should be handled normally.
+ *
+ * @param ev The touch screen event.
+ *
+ * @return boolean Return true if this event was consumed.
+ */
+ public boolean dispatchTouchEvent(MotionEvent ev) {
+ if (mWindow.superDispatchTouchEvent(ev)) {
+ return true;
+ }
+ return onTouchEvent(ev);
+ }
+
+ /**
+ * Called to process trackball events. You can override this to
+ * intercept all trackball events before they are dispatched to the
+ * window. Be sure to call this implementation for trackball events
+ * that should be handled normally.
+ *
+ * @param ev The trackball event.
+ *
+ * @return boolean Return true if this event was consumed.
+ */
+ public boolean dispatchTrackballEvent(MotionEvent ev) {
+ if (mWindow.superDispatchTrackballEvent(ev)) {
+ return true;
+ }
+ return onTrackballEvent(ev);
+ }
+
+ /**
+ * @see Activity#onCreatePanelView(int)
+ */
+ public View onCreatePanelView(int featureId) {
+ return null;
+ }
+
+ /**
+ * @see Activity#onCreatePanelMenu(int, Menu)
+ */
+ public boolean onCreatePanelMenu(int featureId, Menu menu) {
+ if (featureId == Window.FEATURE_OPTIONS_PANEL) {
+ return onCreateOptionsMenu(menu);
+ }
+
+ return false;
+ }
+
+ /**
+ * @see Activity#onPreparePanel(int, View, Menu)
+ */
+ public boolean onPreparePanel(int featureId, View view, Menu menu) {
+ if (featureId == Window.FEATURE_OPTIONS_PANEL && menu != null) {
+ boolean goforit = onPrepareOptionsMenu(menu);
+ return goforit && menu.hasVisibleItems();
+ }
+ return true;
+ }
+
+ /**
+ * @see Activity#onMenuOpened(int, Menu)
+ */
+ public boolean onMenuOpened(int featureId, Menu menu) {
+ return true;
+ }
+
+ /**
+ * @see Activity#onMenuItemSelected(int, MenuItem)
+ */
+ public boolean onMenuItemSelected(int featureId, MenuItem item) {
+ return false;
+ }
+
+ /**
+ * @see Activity#onPanelClosed(int, Menu)
+ */
+ public void onPanelClosed(int featureId, Menu menu) {
+ }
+
+ /**
+ * It is usually safe to proxy this call to the owner activity's
+ * {@link Activity#onCreateOptionsMenu(Menu)} if the client desires the same
+ * menu for this Dialog.
+ *
+ * @see Activity#onCreateOptionsMenu(Menu)
+ * @see #getOwnerActivity()
+ */
+ public boolean onCreateOptionsMenu(Menu menu) {
+ return true;
+ }
+
+ /**
+ * It is usually safe to proxy this call to the owner activity's
+ * {@link Activity#onPrepareOptionsMenu(Menu)} if the client desires the
+ * same menu for this Dialog.
+ *
+ * @see Activity#onPrepareOptionsMenu(Menu)
+ * @see #getOwnerActivity()
+ */
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ return true;
+ }
+
+ /**
+ * @see Activity#onOptionsItemSelected(MenuItem)
+ */
+ public boolean onOptionsItemSelected(MenuItem item) {
+ return false;
+ }
+
+ /**
+ * @see Activity#onOptionsMenuClosed(Menu)
+ */
+ public void onOptionsMenuClosed(Menu menu) {
+ }
+
+ /**
+ * @see Activity#openOptionsMenu()
+ */
+ public void openOptionsMenu() {
+ mWindow.openPanel(Window.FEATURE_OPTIONS_PANEL, null);
+ }
+
+ /**
+ * @see Activity#closeOptionsMenu()
+ */
+ public void closeOptionsMenu() {
+ mWindow.closePanel(Window.FEATURE_OPTIONS_PANEL);
+ }
+
+ /**
+ * @see Activity#onCreateContextMenu(ContextMenu, View, ContextMenuInfo)
+ */
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+ }
+
+ /**
+ * @see Activity#registerForContextMenu(View)
+ */
+ public void registerForContextMenu(View view) {
+ view.setOnCreateContextMenuListener(this);
+ }
+
+ /**
+ * @see Activity#unregisterForContextMenu(View)
+ */
+ public void unregisterForContextMenu(View view) {
+ view.setOnCreateContextMenuListener(null);
+ }
+
+ /**
+ * @see Activity#openContextMenu(View)
+ */
+ public void openContextMenu(View view) {
+ view.showContextMenu();
+ }
+
+ /**
+ * @see Activity#onContextItemSelected(MenuItem)
+ */
+ public boolean onContextItemSelected(MenuItem item) {
+ return false;
+ }
+
+ /**
+ * @see Activity#onContextMenuClosed(Menu)
+ */
+ public void onContextMenuClosed(Menu menu) {
+ }
+
+ /**
+ * This hook is called when the user signals the desire to start a search.
+ */
+ public boolean onSearchRequested() {
+ // not during dialogs, no.
+ return false;
+ }
+
+
+ /**
+ * Request that key events come to this dialog. Use this if your
+ * dialog has no views with focus, but the dialog still wants
+ * a chance to process key events.
+ *
+ * @param get true if the dialog should receive key events, false otherwise
+ * @see android.view.Window#takeKeyEvents
+ */
+ public void takeKeyEvents(boolean get) {
+ mWindow.takeKeyEvents(get);
+ }
+
+ /**
+ * Enable extended window features. This is a convenience for calling
+ * {@link android.view.Window#requestFeature getWindow().requestFeature()}.
+ *
+ * @param featureId The desired feature as defined in
+ * {@link android.view.Window}.
+ * @return Returns true if the requested feature is supported and now
+ * enabled.
+ *
+ * @see android.view.Window#requestFeature
+ */
+ public final boolean requestWindowFeature(int featureId) {
+ return getWindow().requestFeature(featureId);
+ }
+
+ /**
+ * Convenience for calling
+ * {@link android.view.Window#setFeatureDrawableResource}.
+ */
+ public final void setFeatureDrawableResource(int featureId, int resId) {
+ getWindow().setFeatureDrawableResource(featureId, resId);
+ }
+
+ /**
+ * Convenience for calling
+ * {@link android.view.Window#setFeatureDrawableUri}.
+ */
+ public final void setFeatureDrawableUri(int featureId, Uri uri) {
+ getWindow().setFeatureDrawableUri(featureId, uri);
+ }
+
+ /**
+ * Convenience for calling
+ * {@link android.view.Window#setFeatureDrawable(int, Drawable)}.
+ */
+ public final void setFeatureDrawable(int featureId, Drawable drawable) {
+ getWindow().setFeatureDrawable(featureId, drawable);
+ }
+
+ /**
+ * Convenience for calling
+ * {@link android.view.Window#setFeatureDrawableAlpha}.
+ */
+ public final void setFeatureDrawableAlpha(int featureId, int alpha) {
+ getWindow().setFeatureDrawableAlpha(featureId, alpha);
+ }
+
+ public LayoutInflater getLayoutInflater() {
+ return getWindow().getLayoutInflater();
+ }
+
+ /**
+ * Sets whether this dialog is cancelable with the
+ * {@link KeyEvent#KEYCODE_BACK BACK} key.
+ */
+ public void setCancelable(boolean flag) {
+ mCancelable = flag;
+ }
+
+ /**
+ * Sets whether this dialog is canceled when touched outside the window's
+ * bounds. If setting to true, the dialog is set to be cancelable if not
+ * already set.
+ *
+ * @param cancel Whether the dialog should be canceled when touched outside
+ * the window.
+ */
+ public void setCanceledOnTouchOutside(boolean cancel) {
+ if (cancel && !mCancelable) {
+ mCancelable = true;
+ }
+
+ mCanceledOnTouchOutside = cancel;
+ }
+
+ /**
+ * Cancel the dialog. This is essentially the same as calling {@link #dismiss()}, but it will
+ * also call your {@link DialogInterface.OnCancelListener} (if registered).
+ */
+ public void cancel() {
+ if (mCancelMessage != null) {
+
+ // Obtain a new message so this dialog can be re-used
+ Message.obtain(mCancelMessage).sendToTarget();
+ }
+ dismiss();
+ }
+
+ /**
+ * Set a listener to be invoked when the dialog is canceled.
+ * <p>
+ * This will only be invoked when the dialog is canceled, if the creator
+ * needs to know when it is dismissed in general, use
+ * {@link #setOnDismissListener}.
+ *
+ * @param listener The {@link DialogInterface.OnCancelListener} to use.
+ */
+ public void setOnCancelListener(final OnCancelListener listener) {
+ if (listener != null) {
+ mCancelMessage = mDismissCancelHandler.obtainMessage(CANCEL, listener);
+ } else {
+ mCancelMessage = null;
+ }
+ }
+
+ /**
+ * Set a message to be sent when the dialog is canceled.
+ * @param msg The msg to send when the dialog is canceled.
+ * @see #setOnCancelListener(android.content.DialogInterface.OnCancelListener)
+ */
+ public void setCancelMessage(final Message msg) {
+ mCancelMessage = msg;
+ }
+
+ /**
+ * Set a listener to be invoked when the dialog is dismissed.
+ * @param listener The {@link DialogInterface.OnDismissListener} to use.
+ */
+ public void setOnDismissListener(final OnDismissListener listener) {
+ if (listener != null) {
+ mDismissMessage = mDismissCancelHandler.obtainMessage(DISMISS, listener);
+ } else {
+ mDismissMessage = null;
+ }
+ }
+
+ /**
+ * Set a message to be sent when the dialog is dismissed.
+ * @param msg The msg to send when the dialog is dismissed.
+ */
+ public void setDismissMessage(final Message msg) {
+ mDismissMessage = msg;
+ }
+
+ /**
+ * By default, this will use the owner Activity's suggested stream type.
+ *
+ * @see Activity#setVolumeControlStream(int)
+ * @see #setOwnerActivity(Activity)
+ */
+ public final void setVolumeControlStream(int streamType) {
+ getWindow().setVolumeControlStream(streamType);
+ }
+
+ /**
+ * @see Activity#getVolumeControlStream()
+ */
+ public final int getVolumeControlStream() {
+ return getWindow().getVolumeControlStream();
+ }
+
+ /**
+ * Sets the callback that will be called if a key is dispatched to the dialog.
+ */
+ public void setOnKeyListener(final OnKeyListener onKeyListener) {
+ mOnKeyListener = onKeyListener;
+ }
+
+ private static final int DISMISS = 0x43;
+ private static final int CANCEL = 0x44;
+
+ private Handler mDismissCancelHandler;
+
+ private static final class DismissCancelHandler extends Handler {
+ private WeakReference<DialogInterface> mDialog;
+
+ public DismissCancelHandler(Dialog dialog) {
+ mDialog = new WeakReference<DialogInterface>(dialog);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case DISMISS:
+ ((OnDismissListener) msg.obj).onDismiss(mDialog.get());
+ break;
+ case CANCEL:
+ ((OnCancelListener) msg.obj).onCancel(mDialog.get());
+ break;
+ }
+ }
+ }
+}
diff --git a/core/java/android/app/ExpandableListActivity.java b/core/java/android/app/ExpandableListActivity.java
new file mode 100644
index 0000000..75dfcae
--- /dev/null
+++ b/core/java/android/app/ExpandableListActivity.java
@@ -0,0 +1,323 @@
+/*
+ * 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.app;
+
+import android.database.Cursor;
+import android.os.Bundle;
+import java.util.List;
+import android.view.ContextMenu;
+import android.view.View;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.View.OnCreateContextMenuListener;
+import android.widget.ExpandableListAdapter;
+import android.widget.ExpandableListView;
+import android.widget.SimpleCursorTreeAdapter;
+import android.widget.SimpleExpandableListAdapter;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+
+import java.util.Map;
+
+/**
+ * An activity that displays an expandable list of items by binding to a data
+ * source implementing the ExpandableListAdapter, and exposes event handlers
+ * when the user selects an item.
+ * <p>
+ * ExpandableListActivity hosts a
+ * {@link android.widget.ExpandableListView ExpandableListView} object that can
+ * be bound to different data sources that provide a two-levels of data (the
+ * top-level is group, and below each group are children). Binding, screen
+ * layout, and row layout are discussed in the following sections.
+ * <p>
+ * <strong>Screen Layout</strong>
+ * </p>
+ * <p>
+ * ExpandableListActivity has a default layout that consists of a single,
+ * full-screen, centered expandable list. However, if you desire, you can
+ * customize the screen layout by setting your own view layout with
+ * setContentView() in onCreate(). To do this, your own view MUST contain an
+ * ExpandableListView object with the id "@android:id/list" (or
+ * {@link android.R.id#list} if it's in code)
+ * <p>
+ * Optionally, your custom view can contain another view object of any type to
+ * display when the list view is empty. This "empty list" notifier must have an
+ * id "android:empty". Note that when an empty view is present, the expandable
+ * list view will be hidden when there is no data to display.
+ * <p>
+ * The following code demonstrates an (ugly) custom screen layout. It has a list
+ * with a green background, and an alternate red "no data" message.
+ * </p>
+ *
+ * <pre>
+ * &lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
+ * &lt;LinearLayout
+ * android:orientation=&quot;vertical&quot;
+ * android:layout_width=&quot;fill_parent&quot;
+ * android:layout_height=&quot;fill_parent&quot;
+ * android:paddingLeft=&quot;8&quot;
+ * android:paddingRight=&quot;8&quot;&gt;
+ *
+ * &lt;ExpandableListView id=&quot;android:list&quot;
+ * android:layout_width=&quot;fill_parent&quot;
+ * android:layout_height=&quot;fill_parent&quot;
+ * android:background=&quot;#00FF00&quot;
+ * android:layout_weight=&quot;1&quot;
+ * android:drawSelectorOnTop=&quot;false&quot;/&gt;
+ *
+ * &lt;TextView id=&quot;android:empty&quot;
+ * android:layout_width=&quot;fill_parent&quot;
+ * android:layout_height=&quot;fill_parent&quot;
+ * android:background=&quot;#FF0000&quot;
+ * android:text=&quot;No data&quot;/&gt;
+ * &lt;/LinearLayout&gt;
+ * </pre>
+ *
+ * <p>
+ * <strong>Row Layout</strong>
+ * </p>
+ * The {@link ExpandableListAdapter} set in the {@link ExpandableListActivity}
+ * via {@link #setListAdapter(ExpandableListAdapter)} provides the {@link View}s
+ * for each row. This adapter has separate methods for providing the group
+ * {@link View}s and child {@link View}s. There are a couple provided
+ * {@link ExpandableListAdapter}s that simplify use of adapters:
+ * {@link SimpleCursorTreeAdapter} and {@link SimpleExpandableListAdapter}.
+ * <p>
+ * With these, you can specify the layout of individual rows for groups and
+ * children in the list. These constructor takes a few parameters that specify
+ * layout resources for groups and children. It also has additional parameters
+ * that let you specify which data field to associate with which object in the
+ * row layout resource. The {@link SimpleCursorTreeAdapter} fetches data from
+ * {@link Cursor}s and the {@link SimpleExpandableListAdapter} fetches data
+ * from {@link List}s of {@link Map}s.
+ * </p>
+ * <p>
+ * Android provides some standard row layout resources. These are in the
+ * {@link android.R.layout} class, and have names such as simple_list_item_1,
+ * simple_list_item_2, and two_line_list_item. The following layout XML is the
+ * source for the resource two_line_list_item, which displays two data
+ * fields,one above the other, for each list row.
+ * </p>
+ *
+ * <pre>
+ * &lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
+ * &lt;LinearLayout
+ * android:layout_width=&quot;fill_parent&quot;
+ * android:layout_height=&quot;wrap_content&quot;
+ * android:orientation=&quot;vertical&quot;&gt;
+ *
+ * &lt;TextView id=&quot;text1&quot;
+ * android:textSize=&quot;16&quot;
+ * android:textStyle=&quot;bold&quot;
+ * android:layout_width=&quot;fill_parent&quot;
+ * android:layout_height=&quot;wrap_content&quot;/&gt;
+ *
+ * &lt;TextView id=&quot;text2&quot;
+ * android:textSize=&quot;16&quot;
+ * android:layout_width=&quot;fill_parent&quot;
+ * android:layout_height=&quot;wrap_content&quot;/&gt;
+ * &lt;/LinearLayout&gt;
+ * </pre>
+ *
+ * <p>
+ * You must identify the data bound to each TextView object in this layout. The
+ * syntax for this is discussed in the next section.
+ * </p>
+ * <p>
+ * <strong>Binding to Data</strong>
+ * </p>
+ * <p>
+ * You bind the ExpandableListActivity's ExpandableListView object to data using
+ * a class that implements the
+ * {@link android.widget.ExpandableListAdapter ExpandableListAdapter} interface.
+ * Android provides two standard list adapters:
+ * {@link android.widget.SimpleExpandableListAdapter SimpleExpandableListAdapter}
+ * for static data (Maps), and
+ * {@link android.widget.SimpleCursorTreeAdapter SimpleCursorTreeAdapter} for
+ * Cursor query results.
+ * </p>
+ *
+ * @see #setListAdapter
+ * @see android.widget.ExpandableListView
+ */
+public class ExpandableListActivity extends Activity implements
+ OnCreateContextMenuListener,
+ ExpandableListView.OnChildClickListener, ExpandableListView.OnGroupCollapseListener,
+ ExpandableListView.OnGroupExpandListener {
+ ExpandableListAdapter mAdapter;
+ ExpandableListView mList;
+ boolean mFinishedStart = false;
+
+ /**
+ * Override this to populate the context menu when an item is long pressed. menuInfo
+ * will contain a {@link AdapterContextMenuInfo} whose position is a packed position
+ * that should be used with {@link ExpandableListView#getPackedPositionType(long)} and
+ * the other similar methods.
+ * <p>
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+ }
+
+ /**
+ * Override this for receiving callbacks when a child has been clicked.
+ * <p>
+ * {@inheritDoc}
+ */
+ public boolean onChildClick(ExpandableListView parent, View v, int groupPosition,
+ int childPosition, long id) {
+ return false;
+ }
+
+ /**
+ * Override this for receiving callbacks when a group has been collapsed.
+ */
+ public void onGroupCollapse(int groupPosition) {
+ }
+
+ /**
+ * Override this for receiving callbacks when a group has been expanded.
+ */
+ public void onGroupExpand(int groupPosition) {
+ }
+
+ /**
+ * Ensures the expandable list view has been created before Activity restores all
+ * of the view states.
+ *
+ *@see Activity#onRestoreInstanceState(Bundle)
+ */
+ @Override
+ protected void onRestoreInstanceState(Bundle state) {
+ ensureList();
+ super.onRestoreInstanceState(state);
+ }
+
+ /**
+ * Updates the screen state (current list and other views) when the
+ * content changes.
+ *
+ * @see Activity#onContentChanged()
+ */
+ @Override
+ public void onContentChanged() {
+ super.onContentChanged();
+ View emptyView = findViewById(com.android.internal.R.id.empty);
+ mList = (ExpandableListView)findViewById(com.android.internal.R.id.list);
+ if (mList == null) {
+ throw new RuntimeException(
+ "Your content must have a ExpandableListView whose id attribute is " +
+ "'android.R.id.list'");
+ }
+ if (emptyView != null) {
+ mList.setEmptyView(emptyView);
+ }
+ mList.setOnChildClickListener(this);
+ mList.setOnGroupExpandListener(this);
+ mList.setOnGroupCollapseListener(this);
+
+ if (mFinishedStart) {
+ setListAdapter(mAdapter);
+ }
+ mFinishedStart = true;
+ }
+
+ /**
+ * Provide the adapter for the expandable list.
+ */
+ public void setListAdapter(ExpandableListAdapter adapter) {
+ synchronized (this) {
+ ensureList();
+ mAdapter = adapter;
+ mList.setAdapter(adapter);
+ }
+ }
+
+ /**
+ * Get the activity's expandable list view widget. This can be used to get the selection,
+ * set the selection, and many other useful functions.
+ *
+ * @see ExpandableListView
+ */
+ public ExpandableListView getExpandableListView() {
+ ensureList();
+ return mList;
+ }
+
+ /**
+ * Get the ExpandableListAdapter associated with this activity's
+ * ExpandableListView.
+ */
+ public ExpandableListAdapter getExpandableListAdapter() {
+ return mAdapter;
+ }
+
+ private void ensureList() {
+ if (mList != null) {
+ return;
+ }
+ setContentView(com.android.internal.R.layout.expandable_list_content);
+ }
+
+ /**
+ * Gets the ID of the currently selected group or child.
+ *
+ * @return The ID of the currently selected group or child.
+ */
+ public long getSelectedId() {
+ return mList.getSelectedId();
+ }
+
+ /**
+ * Gets the position (in packed position representation) of the currently
+ * selected group or child. Use
+ * {@link ExpandableListView#getPackedPositionType},
+ * {@link ExpandableListView#getPackedPositionGroup}, and
+ * {@link ExpandableListView#getPackedPositionChild} to unpack the returned
+ * packed position.
+ *
+ * @return A packed position representation containing the currently
+ * selected group or child's position and type.
+ */
+ public long getSelectedPosition() {
+ return mList.getSelectedPosition();
+ }
+
+ /**
+ * Sets the selection to the specified child. If the child is in a collapsed
+ * group, the group will only be expanded and child subsequently selected if
+ * shouldExpandGroup is set to true, otherwise the method will return false.
+ *
+ * @param groupPosition The position of the group that contains the child.
+ * @param childPosition The position of the child within the group.
+ * @param shouldExpandGroup Whether the child's group should be expanded if
+ * it is collapsed.
+ * @return Whether the selection was successfully set on the child.
+ */
+ public boolean setSelectedChild(int groupPosition, int childPosition, boolean shouldExpandGroup) {
+ return mList.setSelectedChild(groupPosition, childPosition, shouldExpandGroup);
+ }
+
+ /**
+ * Sets the selection to the specified group.
+ * @param groupPosition The position of the group that should be selected.
+ */
+ public void setSelectedGroup(int groupPosition) {
+ mList.setSelectedGroup(groupPosition);
+ }
+
+}
+
diff --git a/core/java/android/app/IActivityManager.java b/core/java/android/app/IActivityManager.java
new file mode 100644
index 0000000..2de21ed
--- /dev/null
+++ b/core/java/android/app/IActivityManager.java
@@ -0,0 +1,353 @@
+/*
+ * 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.app;
+
+import android.app.ActivityManager.MemoryInfo;
+import android.content.ComponentName;
+import android.content.ContentProviderNative;
+import android.content.IContentProvider;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.IPackageDataObserver;
+import android.content.pm.ProviderInfo;
+import android.content.res.Configuration;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.os.IBinder;
+import android.os.IInterface;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.ParcelFileDescriptor;
+import android.text.TextUtils;
+import android.os.Bundle;
+
+import java.util.List;
+
+/**
+ * System private API for talking with the activity manager service. This
+ * provides calls from the application back to the activity manager.
+ *
+ * {@hide}
+ */
+public interface IActivityManager extends IInterface {
+ public static final int START_DELIVERED_TO_TOP = 3;
+ public static final int START_TASK_TO_FRONT = 2;
+ public static final int START_RETURN_INTENT_TO_CALLER = 1;
+ public static final int START_SUCCESS = 0;
+ public static final int START_INTENT_NOT_RESOLVED = -1;
+ public static final int START_CLASS_NOT_FOUND = -2;
+ public static final int START_FORWARD_AND_REQUEST_CONFLICT = -3;
+ public static final int START_PERMISSION_DENIED = -4;
+ public int startActivity(IApplicationThread caller,
+ Intent intent, String resolvedType, Uri[] grantedUriPermissions,
+ int grantedMode, IBinder resultTo, String resultWho, int requestCode,
+ boolean onlyIfNeeded, boolean debug) throws RemoteException;
+ public boolean startNextMatchingActivity(IBinder callingActivity,
+ Intent intent) throws RemoteException;
+ public boolean finishActivity(IBinder token, int code, Intent data)
+ throws RemoteException;
+ public void finishSubActivity(IBinder token, String resultWho, int requestCode) throws RemoteException;
+ public Intent registerReceiver(IApplicationThread caller,
+ IIntentReceiver receiver, IntentFilter filter,
+ String requiredPermission) throws RemoteException;
+ public void unregisterReceiver(IIntentReceiver receiver) throws RemoteException;
+ public static final int BROADCAST_SUCCESS = 0;
+ public static final int BROADCAST_STICKY_CANT_HAVE_PERMISSION = -1;
+ public int broadcastIntent(IApplicationThread caller, Intent intent,
+ String resolvedType, IIntentReceiver resultTo, int resultCode,
+ String resultData, Bundle map, String requiredPermission,
+ boolean serialized, boolean sticky) throws RemoteException;
+ public void unbroadcastIntent(IApplicationThread caller, Intent intent) throws RemoteException;
+ /* oneway */
+ public void finishReceiver(IBinder who, int resultCode, String resultData, Bundle map, boolean abortBroadcast) throws RemoteException;
+ public void setPersistent(IBinder token, boolean isPersistent) throws RemoteException;
+ public void attachApplication(IApplicationThread app) throws RemoteException;
+ /* oneway */
+ public void activityIdle(IBinder token) throws RemoteException;
+ public void activityPaused(IBinder token, Bundle state) throws RemoteException;
+ /* oneway */
+ public void activityStopped(IBinder token,
+ Bitmap thumbnail, CharSequence description) throws RemoteException;
+ /* oneway */
+ public void activityDestroyed(IBinder token) throws RemoteException;
+ public String getCallingPackage(IBinder token) throws RemoteException;
+ public ComponentName getCallingActivity(IBinder token) throws RemoteException;
+ public List getTasks(int maxNum, int flags,
+ IThumbnailReceiver receiver) throws RemoteException;
+ public List<ActivityManager.RecentTaskInfo> getRecentTasks(int maxNum,
+ int flags) throws RemoteException;
+ public List getServices(int maxNum, int flags) throws RemoteException;
+ public List<ActivityManager.ProcessErrorStateInfo> getProcessesInErrorState()
+ throws RemoteException;
+ public void moveTaskToFront(int task) throws RemoteException;
+ public void moveTaskToBack(int task) throws RemoteException;
+ public boolean moveActivityTaskToBack(IBinder token, boolean nonRoot) throws RemoteException;
+ public void moveTaskBackwards(int task) throws RemoteException;
+ public int getTaskForActivity(IBinder token, boolean onlyRoot) throws RemoteException;
+ public void finishOtherInstances(IBinder token, ComponentName className) throws RemoteException;
+ /* oneway */
+ public void reportThumbnail(IBinder token,
+ Bitmap thumbnail, CharSequence description) throws RemoteException;
+ public ContentProviderHolder getContentProvider(IApplicationThread caller,
+ String name) throws RemoteException;
+ public void removeContentProvider(IApplicationThread caller,
+ String name) throws RemoteException;
+ public void publishContentProviders(IApplicationThread caller,
+ List<ContentProviderHolder> providers) throws RemoteException;
+ public ComponentName startService(IApplicationThread caller, Intent service,
+ String resolvedType) throws RemoteException;
+ public int stopService(IApplicationThread caller, Intent service,
+ String resolvedType) throws RemoteException;
+ public boolean stopServiceToken(ComponentName className, IBinder token,
+ int startId) throws RemoteException;
+ public void setServiceForeground(ComponentName className, IBinder token,
+ boolean isForeground) throws RemoteException;
+ public int bindService(IApplicationThread caller, IBinder token,
+ Intent service, String resolvedType,
+ IServiceConnection connection, int flags) throws RemoteException;
+ public boolean unbindService(IServiceConnection connection) throws RemoteException;
+ public void publishService(IBinder token,
+ Intent intent, IBinder service) throws RemoteException;
+ public void unbindFinished(IBinder token, Intent service,
+ boolean doRebind) throws RemoteException;
+ /* oneway */
+ public void serviceDoneExecuting(IBinder token) throws RemoteException;
+
+ public boolean startInstrumentation(ComponentName className, String profileFile,
+ int flags, Bundle arguments, IInstrumentationWatcher watcher)
+ throws RemoteException;
+ public void finishInstrumentation(IApplicationThread target,
+ int resultCode, Bundle results) throws RemoteException;
+
+ public Configuration getConfiguration() throws RemoteException;
+ public void updateConfiguration(Configuration values) throws RemoteException;
+ public void setRequestedOrientation(IBinder token,
+ int requestedOrientation) throws RemoteException;
+ public int getRequestedOrientation(IBinder token) throws RemoteException;
+
+ public ComponentName getActivityClassForToken(IBinder token) throws RemoteException;
+ public String getPackageForToken(IBinder token) throws RemoteException;
+
+ public static final int INTENT_SENDER_BROADCAST = 1;
+ public static final int INTENT_SENDER_ACTIVITY = 2;
+ public static final int INTENT_SENDER_ACTIVITY_RESULT = 3;
+ public static final int INTENT_SENDER_SERVICE = 4;
+ public IIntentSender getIntentSender(int type,
+ String packageName, IBinder token, String resultWho,
+ int requestCode, Intent intent, String resolvedType, int flags) throws RemoteException;
+ public void cancelIntentSender(IIntentSender sender) throws RemoteException;
+ public boolean clearApplicationUserData(final String packageName,
+ final IPackageDataObserver observer) throws RemoteException;
+ public String getPackageForIntentSender(IIntentSender sender) throws RemoteException;
+
+ public void setProcessLimit(int max) throws RemoteException;
+ public int getProcessLimit() throws RemoteException;
+
+ public void setProcessForeground(IBinder token, int pid, boolean isForeground) throws RemoteException;
+
+ public int checkPermission(String permission, int pid, int uid)
+ throws RemoteException;
+
+ public int checkUriPermission(Uri uri, int pid, int uid, int mode)
+ throws RemoteException;
+ public void grantUriPermission(IApplicationThread caller, String targetPkg,
+ Uri uri, int mode) throws RemoteException;
+ public void revokeUriPermission(IApplicationThread caller, Uri uri,
+ int mode) throws RemoteException;
+
+ public void showWaitingForDebugger(IApplicationThread who, boolean waiting)
+ throws RemoteException;
+
+ public void getMemoryInfo(ActivityManager.MemoryInfo outInfo) throws RemoteException;
+
+ public void restartPackage(final String packageName) throws RemoteException;
+
+ // Note: probably don't want to allow applications access to these.
+ public void goingToSleep() throws RemoteException;
+ public void wakingUp() throws RemoteException;
+
+ public void unhandledBack() throws RemoteException;
+ public ParcelFileDescriptor openContentUri(Uri uri) throws RemoteException;
+ public void setDebugApp(
+ String packageName, boolean waitForDebugger, boolean persistent)
+ throws RemoteException;
+ public void setAlwaysFinish(boolean enabled) throws RemoteException;
+ public void setActivityWatcher(IActivityWatcher watcher)
+ throws RemoteException;
+
+ public void enterSafeMode() throws RemoteException;
+
+ public void noteWakeupAlarm(IIntentSender sender) throws RemoteException;
+
+ public boolean killPidsForMemory(int[] pids) throws RemoteException;
+
+ public void reportPss(IApplicationThread caller, int pss) throws RemoteException;
+
+ // Special low-level communication with activity manager.
+ public void startRunning(String pkg, String cls, String action,
+ String data) throws RemoteException;
+ public void systemReady() throws RemoteException;
+ // Returns 1 if the user wants to debug.
+ public int handleApplicationError(IBinder app,
+ int flags, /* 1 == can debug */
+ String tag, String shortMsg, String longMsg,
+ byte[] crashData) throws RemoteException;
+
+ /*
+ * This will deliver the specified signal to all the persistent processes. Currently only
+ * SIGUSR1 is delivered. All others are ignored.
+ */
+ public void signalPersistentProcesses(int signal) throws RemoteException;
+
+ /** Information you can retrieve about a particular application. */
+ public static class ContentProviderHolder implements Parcelable {
+ public final ProviderInfo info;
+ public final String permissionFailure;
+ public IContentProvider provider;
+ public boolean noReleaseNeeded;
+
+ public ContentProviderHolder(ProviderInfo _info) {
+ info = _info;
+ permissionFailure = null;
+ }
+
+ public ContentProviderHolder(ProviderInfo _info,
+ String _permissionFailure) {
+ info = _info;
+ permissionFailure = _permissionFailure;
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ info.writeToParcel(dest, 0);
+ dest.writeString(permissionFailure);
+ if (provider != null) {
+ dest.writeStrongBinder(provider.asBinder());
+ } else {
+ dest.writeStrongBinder(null);
+ }
+ dest.writeInt(noReleaseNeeded ? 1:0);
+ }
+
+ public static final Parcelable.Creator<ContentProviderHolder> CREATOR
+ = new Parcelable.Creator<ContentProviderHolder>() {
+ public ContentProviderHolder createFromParcel(Parcel source) {
+ return new ContentProviderHolder(source);
+ }
+
+ public ContentProviderHolder[] newArray(int size) {
+ return new ContentProviderHolder[size];
+ }
+ };
+
+ private ContentProviderHolder(Parcel source) {
+ info = ProviderInfo.CREATOR.createFromParcel(source);
+ permissionFailure = source.readString();
+ provider = ContentProviderNative.asInterface(
+ source.readStrongBinder());
+ noReleaseNeeded = source.readInt() != 0;
+ }
+ };
+
+ String descriptor = "android.app.IActivityManager";
+
+ // Please keep these transaction codes the same -- they are also
+ // sent by C++ code.
+ int START_RUNNING_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION;
+ int HANDLE_APPLICATION_ERROR_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+1;
+ int START_ACTIVITY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+2;
+ int UNHANDLED_BACK_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+3;
+ int OPEN_CONTENT_URI_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+4;
+
+ // Remaining non-native transaction codes.
+ int FINISH_ACTIVITY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+10;
+ int REGISTER_RECEIVER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+11;
+ int UNREGISTER_RECEIVER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+12;
+ int BROADCAST_INTENT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+13;
+ int UNBROADCAST_INTENT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+14;
+ int FINISH_RECEIVER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+15;
+ int ATTACH_APPLICATION_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+16;
+ int ACTIVITY_IDLE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+17;
+ int ACTIVITY_PAUSED_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+18;
+ int ACTIVITY_STOPPED_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+19;
+ int GET_CALLING_PACKAGE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+20;
+ int GET_CALLING_ACTIVITY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+21;
+ int GET_TASKS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+22;
+ int MOVE_TASK_TO_FRONT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+23;
+ int MOVE_TASK_TO_BACK_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+24;
+ int MOVE_TASK_BACKWARDS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+25;
+ int GET_TASK_FOR_ACTIVITY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+26;
+ int REPORT_THUMBNAIL_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+27;
+ int GET_CONTENT_PROVIDER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+28;
+ int PUBLISH_CONTENT_PROVIDERS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+29;
+ int SET_PERSISTENT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+30;
+ int FINISH_SUB_ACTIVITY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+31;
+ int SYSTEM_READY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+32;
+ int START_SERVICE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+33;
+ int STOP_SERVICE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+34;
+ int BIND_SERVICE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+35;
+ int UNBIND_SERVICE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+36;
+ int PUBLISH_SERVICE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+37;
+ int FINISH_OTHER_INSTANCES_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+38;
+ int GOING_TO_SLEEP_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+39;
+ int WAKING_UP_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+40;
+ int SET_DEBUG_APP_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+41;
+ int SET_ALWAYS_FINISH_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+42;
+ int START_INSTRUMENTATION_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+43;
+ int FINISH_INSTRUMENTATION_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+44;
+ int GET_CONFIGURATION_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+45;
+ int UPDATE_CONFIGURATION_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+46;
+ int STOP_SERVICE_TOKEN_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+47;
+ int GET_ACTIVITY_CLASS_FOR_TOKEN_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+48;
+ int GET_PACKAGE_FOR_TOKEN_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+49;
+ int SET_PROCESS_LIMIT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+50;
+ int GET_PROCESS_LIMIT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+51;
+ int CHECK_PERMISSION_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+52;
+ int CHECK_URI_PERMISSION_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+53;
+ int GRANT_URI_PERMISSION_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+54;
+ int REVOKE_URI_PERMISSION_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+55;
+ int SET_ACTIVITY_WATCHER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+56;
+ int SHOW_WAITING_FOR_DEBUGGER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+57;
+ int SIGNAL_PERSISTENT_PROCESSES_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+58;
+ int GET_RECENT_TASKS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+59;
+ int SERVICE_DONE_EXECUTING_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+60;
+ int ACTIVITY_DESTROYED_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+61;
+ int GET_INTENT_SENDER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+62;
+ int CANCEL_INTENT_SENDER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+63;
+ int GET_PACKAGE_FOR_INTENT_SENDER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+64;
+ int ENTER_SAFE_MODE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+65;
+ int START_NEXT_MATCHING_ACTIVITY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+66;
+ int NOTE_WAKEUP_ALARM_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+67;
+ int REMOVE_CONTENT_PROVIDER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+68;
+ int SET_REQUESTED_ORIENTATION_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+69;
+ int GET_REQUESTED_ORIENTATION_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+70;
+ int UNBIND_FINISHED_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+71;
+ int SET_PROCESS_FOREGROUND_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+72;
+ int SET_SERVICE_FOREGROUND_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+73;
+ int MOVE_ACTIVITY_TASK_TO_BACK_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+74;
+ int GET_MEMORY_INFO_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+75;
+ int GET_PROCESSES_IN_ERROR_STATE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+76;
+ int CLEAR_APP_DATA_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+77;
+ int RESTART_PACKAGE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+78;
+ int KILL_PIDS_FOR_MEMORY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+79;
+ int GET_SERVICES_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+80;
+ int REPORT_PSS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+81;
+}
diff --git a/core/java/android/app/IActivityPendingResult.aidl b/core/java/android/app/IActivityPendingResult.aidl
new file mode 100644
index 0000000..e8eebf1
--- /dev/null
+++ b/core/java/android/app/IActivityPendingResult.aidl
@@ -0,0 +1,27 @@
+/* //device/java/android/android/app/IActivityPendingResult.aidl
+**
+** Copyright 2007, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.app;
+
+import android.os.Bundle;
+
+/** @hide */
+interface IActivityPendingResult
+{
+ boolean sendResult(int code, String data, in Bundle ex);
+}
+
diff --git a/core/java/android/app/IActivityWatcher.aidl b/core/java/android/app/IActivityWatcher.aidl
new file mode 100644
index 0000000..f13a385
--- /dev/null
+++ b/core/java/android/app/IActivityWatcher.aidl
@@ -0,0 +1,55 @@
+/* //device/java/android/android/app/IInstrumentationWatcher.aidl
+**
+** Copyright 2007, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.app;
+
+import android.content.Intent;
+
+/**
+ * Testing interface to monitor what is happening in the activity manager
+ * while tests are running. Not for normal application development.
+ * {@hide}
+ */
+interface IActivityWatcher
+{
+ /**
+ * The system is trying to start an activity. Return true to allow
+ * it to be started as normal, or false to cancel/reject this activity.
+ */
+ boolean activityStarting(in Intent intent, String pkg);
+
+ /**
+ * The system is trying to return to an activity. Return true to allow
+ * it to be resumed as normal, or false to cancel/reject this activity.
+ */
+ boolean activityResuming(String pkg);
+
+ /**
+ * An application process has crashed (in Java). Return true for the
+ * normal error recovery (app crash dialog) to occur, false to kill
+ * it immediately.
+ */
+ boolean appCrashed(String processName, int pid, String shortMsg,
+ String longMsg, in byte[] crashData);
+
+ /**
+ * An application process is not responding. Return 0 to show the "app
+ * not responding" dialog, 1 to continue waiting, or -1 to kill it
+ * immediately.
+ */
+ int appNotResponding(String processName, int pid, String processStats);
+}
diff --git a/core/java/android/app/IAlarmManager.aidl b/core/java/android/app/IAlarmManager.aidl
new file mode 100755
index 0000000..c7f20b9
--- /dev/null
+++ b/core/java/android/app/IAlarmManager.aidl
@@ -0,0 +1,33 @@
+/* //device/java/android/android/app/IAlarmManager.aidl
+**
+** Copyright 2006, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+package android.app;
+
+import android.app.PendingIntent;
+
+/**
+ * System private API for talking with the alarm manager service.
+ *
+ * {@hide}
+ */
+interface IAlarmManager {
+ void set(int type, long triggerAtTime, in PendingIntent operation);
+ void setRepeating(int type, long triggerAtTime, long interval, in PendingIntent operation);
+ void setTimeZone(String zone);
+ void remove(in PendingIntent operation);
+}
+
+
diff --git a/core/java/android/app/IApplicationThread.java b/core/java/android/app/IApplicationThread.java
new file mode 100644
index 0000000..ecd993a
--- /dev/null
+++ b/core/java/android/app/IApplicationThread.java
@@ -0,0 +1,118 @@
+/*
+ * 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.app;
+
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.ProviderInfo;
+import android.content.pm.ServiceInfo;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.IBinder;
+import android.os.IInterface;
+
+import java.io.FileDescriptor;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * System private API for communicating with the application. This is given to
+ * the activity manager by an application when it starts up, for the activity
+ * manager to tell the application about things it needs to do.
+ *
+ * {@hide}
+ */
+public interface IApplicationThread extends IInterface {
+ void schedulePauseActivity(IBinder token, boolean finished,
+ int configChanges) throws RemoteException;
+ void scheduleStopActivity(IBinder token, boolean showWindow,
+ int configChanges) throws RemoteException;
+ void scheduleWindowVisibility(IBinder token, boolean showWindow) throws RemoteException;
+ void scheduleResumeActivity(IBinder token) throws RemoteException;
+ void scheduleSendResult(IBinder token, List<ResultInfo> results) throws RemoteException;
+ void scheduleLaunchActivity(Intent intent, IBinder token,
+ ActivityInfo info, Bundle state, List<ResultInfo> pendingResults,
+ List<Intent> pendingNewIntents, boolean notResumed)
+ throws RemoteException;
+ void scheduleRelaunchActivity(IBinder token, List<ResultInfo> pendingResults,
+ List<Intent> pendingNewIntents, int configChanges,
+ boolean notResumed) throws RemoteException;
+ void scheduleNewIntent(List<Intent> intent, IBinder token) throws RemoteException;
+ void scheduleDestroyActivity(IBinder token, boolean finished,
+ int configChanges) throws RemoteException;
+ void scheduleReceiver(Intent intent, ActivityInfo info, int resultCode,
+ String data, Bundle extras, boolean sync) throws RemoteException;
+ void scheduleCreateService(IBinder token, ServiceInfo info) throws RemoteException;
+ void scheduleBindService(IBinder token,
+ Intent intent, boolean rebind) throws RemoteException;
+ void scheduleUnbindService(IBinder token,
+ Intent intent) throws RemoteException;
+ void scheduleServiceArgs(IBinder token, int startId, Intent args) throws RemoteException;
+ void scheduleStopService(IBinder token) throws RemoteException;
+ static final int DEBUG_OFF = 0;
+ static final int DEBUG_ON = 1;
+ static final int DEBUG_WAIT = 2;
+ void bindApplication(String packageName, ApplicationInfo info, List<ProviderInfo> providers,
+ ComponentName testName, String profileName, Bundle testArguments,
+ IInstrumentationWatcher testWatcher, int debugMode, Configuration config, Map<String,
+ IBinder> services) throws RemoteException;
+ void scheduleExit() throws RemoteException;
+ void requestThumbnail(IBinder token) throws RemoteException;
+ void scheduleConfigurationChanged(Configuration config) throws RemoteException;
+ void updateTimeZone() throws RemoteException;
+ void processInBackground() throws RemoteException;
+ void dumpService(FileDescriptor fd, IBinder servicetoken, String[] args)
+ throws RemoteException;
+ void scheduleRegisteredReceiver(IIntentReceiver receiver, Intent intent,
+ int resultCode, String data, Bundle extras, boolean ordered)
+ throws RemoteException;
+ void scheduleLowMemory() throws RemoteException;
+ void scheduleActivityConfigurationChanged(IBinder token) throws RemoteException;
+ void requestPss() throws RemoteException;
+
+ String descriptor = "android.app.IApplicationThread";
+
+ int SCHEDULE_PAUSE_ACTIVITY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION;
+ int SCHEDULE_STOP_ACTIVITY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+2;
+ int SCHEDULE_WINDOW_VISIBILITY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+3;
+ int SCHEDULE_RESUME_ACTIVITY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+4;
+ int SCHEDULE_SEND_RESULT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+5;
+ int SCHEDULE_LAUNCH_ACTIVITY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+6;
+ int SCHEDULE_NEW_INTENT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+7;
+ int SCHEDULE_FINISH_ACTIVITY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+8;
+ int SCHEDULE_RECEIVER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+9;
+ int SCHEDULE_CREATE_SERVICE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+10;
+ int SCHEDULE_STOP_SERVICE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+11;
+ int BIND_APPLICATION_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+12;
+ int SCHEDULE_EXIT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+13;
+ int REQUEST_THUMBNAIL_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+14;
+ int SCHEDULE_CONFIGURATION_CHANGED_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+15;
+ int SCHEDULE_SERVICE_ARGS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+16;
+ int UPDATE_TIME_ZONE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+17;
+ int PROCESS_IN_BACKGROUND_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+18;
+ int SCHEDULE_BIND_SERVICE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+19;
+ int SCHEDULE_UNBIND_SERVICE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+20;
+ int DUMP_SERVICE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+21;
+ int SCHEDULE_REGISTERED_RECEIVER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+22;
+ int SCHEDULE_LOW_MEMORY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+23;
+ int SCHEDULE_ACTIVITY_CONFIGURATION_CHANGED_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+24;
+ int SCHEDULE_RELAUNCH_ACTIVITY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+25;
+ int REQUEST_PSS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+26;
+}
diff --git a/core/java/android/app/IInstrumentationWatcher.aidl b/core/java/android/app/IInstrumentationWatcher.aidl
new file mode 100644
index 0000000..405a3d8
--- /dev/null
+++ b/core/java/android/app/IInstrumentationWatcher.aidl
@@ -0,0 +1,31 @@
+/* //device/java/android/android/app/IInstrumentationWatcher.aidl
+**
+** Copyright 2007, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.app;
+
+import android.content.ComponentName;
+import android.os.Bundle;
+
+/** @hide */
+oneway interface IInstrumentationWatcher
+{
+ void instrumentationStatus(in ComponentName name, int resultCode,
+ in Bundle results);
+ void instrumentationFinished(in ComponentName name, int resultCode,
+ in Bundle results);
+}
+
diff --git a/core/java/android/app/IIntentReceiver.aidl b/core/java/android/app/IIntentReceiver.aidl
new file mode 100755
index 0000000..5f5d0eb
--- /dev/null
+++ b/core/java/android/app/IIntentReceiver.aidl
@@ -0,0 +1,33 @@
+/*
+**
+** Copyright 2006, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+package android.app;
+
+import android.content.Intent;
+import android.os.Bundle;
+
+/**
+ * System private API for dispatching intent broadcasts. This is given to the
+ * activity manager as part of registering for an intent broadcasts, and is
+ * called when it receives intents.
+ *
+ * {@hide}
+ */
+oneway interface IIntentReceiver {
+ void performReceive(in Intent intent, int resultCode,
+ String data, in Bundle extras, boolean ordered);
+}
+
diff --git a/core/java/android/app/IIntentSender.aidl b/core/java/android/app/IIntentSender.aidl
new file mode 100644
index 0000000..53e135a
--- /dev/null
+++ b/core/java/android/app/IIntentSender.aidl
@@ -0,0 +1,27 @@
+/* //device/java/android/android/app/IActivityPendingResult.aidl
+**
+** Copyright 2007, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.app;
+
+import android.app.IIntentReceiver;
+import android.content.Intent;
+
+/** @hide */
+interface IIntentSender {
+ int send(int code, in Intent intent, String resolvedType,
+ IIntentReceiver finishedReceiver);
+}
diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl
new file mode 100644
index 0000000..c1035b6
--- /dev/null
+++ b/core/java/android/app/INotificationManager.aidl
@@ -0,0 +1,34 @@
+/* //device/java/android/android/app/INotificationManager.aidl
+**
+** Copyright 2007, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.app;
+
+import android.app.ITransientNotification;
+import android.app.Notification;
+import android.content.Intent;
+
+/** {@hide} */
+interface INotificationManager
+{
+ void enqueueNotification(String pkg, int id, in Notification notification, inout int[] idReceived);
+ void cancelNotification(String pkg, int id);
+ void cancelAllNotifications(String pkg);
+
+ void enqueueToast(String pkg, ITransientNotification callback, int duration);
+ void cancelToast(String pkg, ITransientNotification callback);
+}
+
diff --git a/core/java/android/app/ISearchManager.aidl b/core/java/android/app/ISearchManager.aidl
new file mode 100644
index 0000000..6c3617a
--- /dev/null
+++ b/core/java/android/app/ISearchManager.aidl
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2007, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.content.ComponentName;
+import android.server.search.SearchableInfo;
+
+/** @hide */
+interface ISearchManager {
+ SearchableInfo getSearchableInfo(in ComponentName launchActivity, boolean globalSearch);
+}
diff --git a/core/java/android/app/IServiceConnection.aidl b/core/java/android/app/IServiceConnection.aidl
new file mode 100644
index 0000000..6804071
--- /dev/null
+++ b/core/java/android/app/IServiceConnection.aidl
@@ -0,0 +1,26 @@
+/* //device/java/android/android/app/IServiceConnection.aidl
+**
+** Copyright 2007, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.app;
+
+import android.content.ComponentName;
+
+/** @hide */
+oneway interface IServiceConnection {
+ void connected(in ComponentName name, IBinder service);
+}
+
diff --git a/core/java/android/app/IStatusBar.aidl b/core/java/android/app/IStatusBar.aidl
new file mode 100644
index 0000000..c64fa50
--- /dev/null
+++ b/core/java/android/app/IStatusBar.aidl
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2007, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+/** @hide */
+interface IStatusBar
+{
+ void activate();
+ void deactivate();
+ void toggle();
+ void disable(int what, IBinder token, String pkg);
+ IBinder addIcon(String slot, String iconPackage, int iconId, int iconLevel);
+ void updateIcon(IBinder key, String slot, String iconPackage, int iconId, int iconLevel);
+ void removeIcon(IBinder key);
+}
diff --git a/core/java/android/app/IThumbnailReceiver.aidl b/core/java/android/app/IThumbnailReceiver.aidl
new file mode 100755
index 0000000..7943f2c
--- /dev/null
+++ b/core/java/android/app/IThumbnailReceiver.aidl
@@ -0,0 +1,30 @@
+/* //device/java/android/android/app/IThumbnailReceiver.aidl
+**
+** Copyright 2006, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+package android.app;
+
+import android.graphics.Bitmap;
+
+/**
+ * System private API for receiving updated thumbnails from a checkpoint.
+ *
+ * {@hide}
+ */
+oneway interface IThumbnailReceiver {
+ void newThumbnail(int id, in Bitmap thumbnail, CharSequence description);
+ void finished();
+}
+
diff --git a/core/java/android/app/ITransientNotification.aidl b/core/java/android/app/ITransientNotification.aidl
new file mode 100644
index 0000000..35b53a4
--- /dev/null
+++ b/core/java/android/app/ITransientNotification.aidl
@@ -0,0 +1,25 @@
+/* //device/java/android/android/app/ITransientNotification.aidl
+**
+** Copyright 2007, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.app;
+
+/** @hide */
+oneway interface ITransientNotification {
+ void show();
+ void hide();
+}
+
diff --git a/core/java/android/app/IWallpaperService.aidl b/core/java/android/app/IWallpaperService.aidl
new file mode 100644
index 0000000..a332b1a
--- /dev/null
+++ b/core/java/android/app/IWallpaperService.aidl
@@ -0,0 +1,55 @@
+/**
+ * Copyright (c) 2008, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.os.ParcelFileDescriptor;
+import android.app.IWallpaperServiceCallback;
+
+/** @hide */
+interface IWallpaperService {
+
+ /**
+ * Set the wallpaper.
+ */
+ ParcelFileDescriptor setWallpaper();
+
+ /**
+ * Get the wallpaper.
+ */
+ ParcelFileDescriptor getWallpaper(IWallpaperServiceCallback cb);
+
+ /**
+ * Clear the wallpaper.
+ */
+ void clearWallpaper();
+
+ /**
+ * Sets the dimension hint for the wallpaper. These hints indicate the desired
+ * minimum width and height for the wallpaper.
+ */
+ void setDimensionHints(in int width, in int height);
+
+ /**
+ * Returns the desired minimum width for the wallpaper.
+ */
+ int getWidthHint();
+
+ /**
+ * Returns the desired minimum height for the wallpaper.
+ */
+ int getHeightHint();
+}
diff --git a/core/java/android/app/IWallpaperServiceCallback.aidl b/core/java/android/app/IWallpaperServiceCallback.aidl
new file mode 100644
index 0000000..6086f40
--- /dev/null
+++ b/core/java/android/app/IWallpaperServiceCallback.aidl
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+/**
+ * Callback interface used by IWallpaperService to send asynchronous
+ * notifications back to its clients. Note that this is a
+ * one-way interface so the server does not block waiting for the client.
+ *
+ * @hide
+ */
+oneway interface IWallpaperServiceCallback {
+ /**
+ * Called when the wallpaper has changed
+ */
+ void onWallpaperChanged();
+}
diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java
new file mode 100644
index 0000000..f80c947
--- /dev/null
+++ b/core/java/android/app/Instrumentation.java
@@ -0,0 +1,1601 @@
+/*
+ * 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.app;
+
+import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.Debug;
+import android.os.IBinder;
+import android.os.MessageQueue;
+import android.os.Process;
+import android.os.SystemClock;
+import android.os.ServiceManager;
+import android.util.AndroidRuntimeException;
+import android.util.Config;
+import android.util.Log;
+import android.view.IWindowManager;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+import android.view.Window;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * Base class for implementing application instrumentation code. When running
+ * with instrumentation turned on, this class will be instantiated for you
+ * before any of the application code, allowing you to monitor all of the
+ * interaction the system has with the application. An Instrumentation
+ * implementation is described to the system through an AndroidManifest.xml's
+ * &lt;instrumentation&gt; tag.
+ */
+public class Instrumentation {
+ /**
+ * If included in the status or final bundle sent to an IInstrumentationWatcher, this key
+ * identifies the class that is writing the report. This can be used to provide more structured
+ * logging or reporting capabilities in the IInstrumentationWatcher.
+ */
+ public static final String REPORT_KEY_IDENTIFIER = "id";
+ /**
+ * If included in the status or final bundle sent to an IInstrumentationWatcher, this key
+ * identifies a string which can simply be printed to the output stream. Using these streams
+ * provides a "pretty printer" version of the status & final packets. Any bundles including
+ * this key should also include the complete set of raw key/value pairs, so that the
+ * instrumentation can also be launched, and results collected, by an automated system.
+ */
+ public static final String REPORT_KEY_STREAMRESULT = "stream";
+
+ private static final String TAG = "Instrumentation";
+
+ private final Object mSync = new Object();
+ private ActivityThread mThread = null;
+ private MessageQueue mMessageQueue = null;
+ private Context mInstrContext;
+ private Context mAppContext;
+ private ComponentName mComponent;
+ private Thread mRunner;
+ private List<ActivityWaiter> mWaitingActivities;
+ private List<ActivityMonitor> mActivityMonitors;
+ private IInstrumentationWatcher mWatcher;
+ private long mPreCpuTime;
+ private long mStart;
+ private boolean mAutomaticPerformanceSnapshots = false;
+ private Bundle mPrePerfMetrics = new Bundle();
+ private Bundle mPerfMetrics = new Bundle();
+
+ public Instrumentation() {
+ }
+
+ /**
+ * Called when the instrumentation is starting, before any application code
+ * has been loaded. Usually this will be implemented to simply call
+ * {@link #start} to begin the instrumentation thread, which will then
+ * continue execution in {@link #onStart}.
+ *
+ * <p>If you do not need your own thread -- that is you are writing your
+ * instrumentation to be completely asynchronous (returning to the event
+ * loop so that the application can run), you can simply begin your
+ * instrumentation here, for example call {@link Context#startActivity} to
+ * begin the appropriate first activity of the application.
+ *
+ * @param arguments Any additional arguments that were supplied when the
+ * instrumentation was started.
+ */
+ public void onCreate(Bundle arguments) {
+ }
+
+ /**
+ * Create and start a new thread in which to run instrumentation. This new
+ * thread will call to {@link #onStart} where you can implement the
+ * instrumentation.
+ */
+ public void start() {
+ if (mRunner != null) {
+ throw new RuntimeException("Instrumentation already started");
+ }
+ mRunner = new InstrumentationThread("Instr: " + getClass().getName());
+ mRunner.start();
+ }
+
+ /**
+ * Method where the instrumentation thread enters execution. This allows
+ * you to run your instrumentation code in a separate thread than the
+ * application, so that it can perform blocking operation such as
+ * {@link #sendKeySync} or {@link #startActivitySync}.
+ *
+ * <p>You will typically want to call finish() when this function is done,
+ * to end your instrumentation.
+ */
+ public void onStart() {
+ }
+
+ /**
+ * This is called whenever the system captures an unhandled exception that
+ * was thrown by the application. The default implementation simply
+ * returns false, allowing normal system handling of the exception to take
+ * place.
+ *
+ * @param obj The client object that generated the exception. May be an
+ * Application, Activity, BroadcastReceiver, Service, or null.
+ * @param e The exception that was thrown.
+ *
+ * @return To allow normal system exception process to occur, return false.
+ * If true is returned, the system will proceed as if the exception
+ * didn't happen.
+ */
+ public boolean onException(Object obj, Throwable e) {
+ return false;
+ }
+
+ /**
+ * Provide a status report about the application.
+ *
+ * @param resultCode Current success/failure of instrumentation.
+ * @param results Any results to send back to the code that started the instrumentation.
+ */
+ public void sendStatus(int resultCode, Bundle results) {
+ if (mWatcher != null) {
+ try {
+ mWatcher.instrumentationStatus(mComponent, resultCode, results);
+ }
+ catch (RemoteException e) {
+ mWatcher = null;
+ }
+ }
+ }
+
+ /**
+ * Terminate instrumentation of the application. This will cause the
+ * application process to exit, removing this instrumentation from the next
+ * time the application is started.
+ *
+ * @param resultCode Overall success/failure of instrumentation.
+ * @param results Any results to send back to the code that started the
+ * instrumentation.
+ */
+ public void finish(int resultCode, Bundle results) {
+ if (mAutomaticPerformanceSnapshots) {
+ endPerformanceSnapshot();
+ }
+ if (mPerfMetrics != null) {
+ results.putAll(mPerfMetrics);
+ }
+ mThread.finishInstrumentation(resultCode, results);
+ }
+
+ public void setAutomaticPerformanceSnapshots() {
+ mAutomaticPerformanceSnapshots = true;
+ }
+
+ public void startPerformanceSnapshot() {
+ mStart = 0;
+ if (!isProfiling()) {
+ // Add initial binder counts
+ Bundle binderCounts = getBinderCounts();
+ for (String key: binderCounts.keySet()) {
+ addPerfMetricLong("pre_" + key, binderCounts.getLong(key));
+ }
+
+ // Force a GC and zero out the performance counters. Do this
+ // before reading initial CPU/wall-clock times so we don't include
+ // the cost of this setup in our final metrics.
+ startAllocCounting();
+
+ // Record CPU time up to this point, and start timing. Note: this
+ // must happen at the end of this method, otherwise the timing will
+ // include noise.
+ mStart = SystemClock.uptimeMillis();
+ mPreCpuTime = Process.getElapsedCpuTime();
+ }
+ }
+
+ public void endPerformanceSnapshot() {
+ if (!isProfiling()) {
+ // Stop the timing. This must be done first before any other counting is stopped.
+ long cpuTime = Process.getElapsedCpuTime();
+ long duration = SystemClock.uptimeMillis();
+
+ stopAllocCounting();
+
+ long nativeMax = Debug.getNativeHeapSize() / 1024;
+ long nativeAllocated = Debug.getNativeHeapAllocatedSize() / 1024;
+ long nativeFree = Debug.getNativeHeapFreeSize() / 1024;
+
+ Debug.MemoryInfo memInfo = new Debug.MemoryInfo();
+ Debug.getMemoryInfo(memInfo);
+
+ Runtime runtime = Runtime.getRuntime();
+
+ long dalvikMax = runtime.totalMemory() / 1024;
+ long dalvikFree = runtime.freeMemory() / 1024;
+ long dalvikAllocated = dalvikMax - dalvikFree;
+
+ // Add final binder counts
+ Bundle binderCounts = getBinderCounts();
+ for (String key: binderCounts.keySet()) {
+ addPerfMetricLong(key, binderCounts.getLong(key));
+ }
+
+ // Add alloc counts
+ Bundle allocCounts = getAllocCounts();
+ for (String key: allocCounts.keySet()) {
+ addPerfMetricLong(key, allocCounts.getLong(key));
+ }
+
+ addPerfMetricLong("execution_time", duration - mStart);
+ addPerfMetricLong("pre_cpu_time", mPreCpuTime);
+ addPerfMetricLong("cpu_time", cpuTime - mPreCpuTime);
+
+ addPerfMetricLong("native_size", nativeMax);
+ addPerfMetricLong("native_allocated", nativeAllocated);
+ addPerfMetricLong("native_free", nativeFree);
+ addPerfMetricInt("native_pss", memInfo.nativePss);
+ addPerfMetricInt("native_private_dirty", memInfo.nativePrivateDirty);
+ addPerfMetricInt("native_shared_dirty", memInfo.nativeSharedDirty);
+
+ addPerfMetricLong("java_size", dalvikMax);
+ addPerfMetricLong("java_allocated", dalvikAllocated);
+ addPerfMetricLong("java_free", dalvikFree);
+ addPerfMetricInt("java_pss", memInfo.dalvikPss);
+ addPerfMetricInt("java_private_dirty", memInfo.dalvikPrivateDirty);
+ addPerfMetricInt("java_shared_dirty", memInfo.dalvikSharedDirty);
+
+ addPerfMetricInt("other_pss", memInfo.otherPss);
+ addPerfMetricInt("other_private_dirty", memInfo.otherPrivateDirty);
+ addPerfMetricInt("other_shared_dirty", memInfo.otherSharedDirty);
+
+ }
+ }
+
+ private void addPerfMetricLong(String key, long value) {
+ mPerfMetrics.putLong("performance." + key, value);
+ }
+
+ private void addPerfMetricInt(String key, int value) {
+ mPerfMetrics.putInt("performance." + key, value);
+ }
+
+ /**
+ * Called when the instrumented application is stopping, after all of the
+ * normal application cleanup has occurred.
+ */
+ public void onDestroy() {
+ }
+
+ /**
+ * Return the Context of this instrumentation's package. Note that this is
+ * often different than the Context of the application being
+ * instrumentated, since the instrumentation code often lives is a
+ * different package than that of the application it is running against.
+ * See {@link #getTargetContext} to retrieve a Context for the target
+ * application.
+ *
+ * @return The instrumentation's package context.
+ *
+ * @see #getTargetContext
+ */
+ public Context getContext() {
+ return mInstrContext;
+ }
+
+ /**
+ * Returns complete component name of this instrumentation.
+ *
+ * @return Returns the complete component name for this instrumentation.
+ */
+ public ComponentName getComponentName() {
+ return mComponent;
+ }
+
+ /**
+ * Return a Context for the target application being instrumented. Note
+ * that this is often different than the Context of the instrumentation
+ * code, since the instrumentation code often lives is a different package
+ * than that of the application it is running against. See
+ * {@link #getContext} to retrieve a Context for the instrumentation code.
+ *
+ * @return A Context in the target application.
+ *
+ * @see #getContext
+ */
+ public Context getTargetContext() {
+ return mAppContext;
+ }
+
+ /**
+ * Check whether this instrumentation was started with profiling enabled.
+ *
+ * @return Returns true if profiling was enabled when starting, else false.
+ */
+ public boolean isProfiling() {
+ return mThread.isProfiling();
+ }
+
+ /**
+ * This method will start profiling if isProfiling() returns true. You should
+ * only call this method if you set the handleProfiling attribute in the
+ * manifest file for this Instrumentation to true.
+ */
+ public void startProfiling() {
+ if (mThread.isProfiling()) {
+ File file = new File(mThread.getProfileFilePath());
+ file.getParentFile().mkdirs();
+ Debug.startMethodTracing(file.toString(), 8 * 1024 * 1024);
+ }
+ }
+
+ /**
+ * Stops profiling if isProfiling() returns true.
+ */
+ public void stopProfiling() {
+ if (mThread.isProfiling()) {
+ Debug.stopMethodTracing();
+ }
+ }
+
+ /**
+ * Force the global system in or out of touch mode. This can be used if
+ * your instrumentation relies on the UI being in one more or the other
+ * when it starts.
+ *
+ * @param inTouch Set to true to be in touch mode, false to be in
+ * focus mode.
+ */
+ public void setInTouchMode(boolean inTouch) {
+ try {
+ IWindowManager.Stub.asInterface(
+ ServiceManager.getService("window")).setInTouchMode(inTouch);
+ } catch (RemoteException e) {
+ // Shouldn't happen!
+ }
+ }
+
+ /**
+ * Schedule a callback for when the application's main thread goes idle
+ * (has no more events to process).
+ *
+ * @param recipient Called the next time the thread's message queue is
+ * idle.
+ */
+ public void waitForIdle(Runnable recipient) {
+ mMessageQueue.addIdleHandler(new Idler(recipient));
+ mThread.getHandler().post(new EmptyRunnable());
+ }
+
+ /**
+ * Synchronously wait for the application to be idle. Can not be called
+ * from the main application thread -- use {@link #start} to execute
+ * instrumentation in its own thread.
+ */
+ public void waitForIdleSync() {
+ validateNotAppThread();
+ Idler idler = new Idler(null);
+ mMessageQueue.addIdleHandler(idler);
+ mThread.getHandler().post(new EmptyRunnable());
+ idler.waitForIdle();
+ }
+
+ /**
+ * Execute a call on the application's main thread, blocking until it is
+ * complete. Useful for doing things that are not thread-safe, such as
+ * looking at or modifying the view hierarchy.
+ *
+ * @param runner The code to run on the main thread.
+ */
+ public void runOnMainSync(Runnable runner) {
+ validateNotAppThread();
+ SyncRunnable sr = new SyncRunnable(runner);
+ mThread.getHandler().post(sr);
+ sr.waitForComplete();
+ }
+
+ /**
+ * Start a new activity and wait for it to begin running before returning.
+ * In addition to being synchronous, this method as some semantic
+ * differences from the standard {@link Context#startActivity} call: the
+ * activity component is resolved before talking with the activity manager
+ * (its class name is specified in the Intent that this method ultimately
+ * starts), and it does not allow you to start activities that run in a
+ * different process. In addition, if the given Intent resolves to
+ * multiple activities, instead of displaying a dialog for the user to
+ * select an activity, an exception will be thrown.
+ *
+ * <p>The function returns as soon as the activity goes idle following the
+ * call to its {@link Activity#onCreate}. Generally this means it has gone
+ * through the full initialization including {@link Activity#onResume} and
+ * drawn and displayed its initial window.
+ *
+ * @param intent Description of the activity to start.
+ *
+ * @see Context#startActivity
+ */
+ public Activity startActivitySync(Intent intent) {
+ validateNotAppThread();
+
+ synchronized (mSync) {
+ intent = new Intent(intent);
+
+ ActivityInfo ai = intent.resolveActivityInfo(
+ getTargetContext().getPackageManager(), 0);
+ if (ai == null) {
+ throw new RuntimeException("Unable to resolve activity for: " + intent);
+ }
+ if (!ai.applicationInfo.processName.equals(
+ getTargetContext().getPackageName())) {
+ // todo: if this intent is ambiguous, look here to see if
+ // there is a single match that is in our package.
+ throw new RuntimeException("Intent resolved to different package "
+ + ai.applicationInfo.packageName + ": "
+ + intent);
+ }
+
+ intent.setComponent(new ComponentName(
+ ai.applicationInfo.packageName, ai.name));
+ final ActivityWaiter aw = new ActivityWaiter(intent);
+
+ if (mWaitingActivities == null) {
+ mWaitingActivities = new ArrayList();
+ }
+ mWaitingActivities.add(aw);
+
+ getTargetContext().startActivity(intent);
+
+ do {
+ try {
+ mSync.wait();
+ } catch (InterruptedException e) {
+ }
+ } while (mWaitingActivities.contains(aw));
+
+ return aw.activity;
+ }
+ }
+
+ /**
+ * Information about a particular kind of Intent that is being monitored.
+ * An instance of this class is added to the
+ * current instrumentation through {@link #addMonitor}; after being added,
+ * when a new activity is being started the monitor will be checked and, if
+ * matching, its hit count updated and (optionally) the call stopped and a
+ * canned result returned.
+ *
+ * <p>An ActivityMonitor can also be used to look for the creation of an
+ * activity, through the {@link #waitForActivity} method. This will return
+ * after a matching activity has been created with that activity object.
+ */
+ public static class ActivityMonitor {
+ private final IntentFilter mWhich;
+ private final String mClass;
+ private final ActivityResult mResult;
+ private final boolean mBlock;
+
+
+ // This is protected by 'Instrumentation.this.mSync'.
+ /*package*/ int mHits = 0;
+
+ // This is protected by 'this'.
+ /*package*/ Activity mLastActivity = null;
+
+ /**
+ * Create a new ActivityMonitor that looks for a particular kind of
+ * intent to be started.
+ *
+ * @param which The set of intents this monitor is responsible for.
+ * @param result A canned result to return if the monitor is hit; can
+ * be null.
+ * @param block Controls whether the monitor should block the activity
+ * start (returning its canned result) or let the call
+ * proceed.
+ *
+ * @see Instrumentation#addMonitor
+ */
+ public ActivityMonitor(
+ IntentFilter which, ActivityResult result, boolean block) {
+ mWhich = which;
+ mClass = null;
+ mResult = result;
+ mBlock = block;
+ }
+
+ /**
+ * Create a new ActivityMonitor that looks for a specific activity
+ * class to be started.
+ *
+ * @param cls The activity class this monitor is responsible for.
+ * @param result A canned result to return if the monitor is hit; can
+ * be null.
+ * @param block Controls whether the monitor should block the activity
+ * start (returning its canned result) or let the call
+ * proceed.
+ *
+ * @see Instrumentation#addMonitor
+ */
+ public ActivityMonitor(
+ String cls, ActivityResult result, boolean block) {
+ mWhich = null;
+ mClass = cls;
+ mResult = result;
+ mBlock = block;
+ }
+
+ /**
+ * Retrieve the filter associated with this ActivityMonitor.
+ */
+ public final IntentFilter getFilter() {
+ return mWhich;
+ }
+
+ /**
+ * Retrieve the result associated with this ActivityMonitor, or null if
+ * none.
+ */
+ public final ActivityResult getResult() {
+ return mResult;
+ }
+
+ /**
+ * Check whether this monitor blocks activity starts (not allowing the
+ * actual activity to run) or allows them to execute normally.
+ */
+ public final boolean isBlocking() {
+ return mBlock;
+ }
+
+ /**
+ * Retrieve the number of times the monitor has been hit so far.
+ */
+ public final int getHits() {
+ return mHits;
+ }
+
+ /**
+ * Retrieve the most recent activity class that was seen by this
+ * monitor.
+ */
+ public final Activity getLastActivity() {
+ return mLastActivity;
+ }
+
+ /**
+ * Block until an Activity is created that matches this monitor,
+ * returning the resulting activity.
+ *
+ * @return Activity
+ */
+ public final Activity waitForActivity() {
+ synchronized (this) {
+ while (mLastActivity == null) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ }
+ }
+ Activity res = mLastActivity;
+ mLastActivity = null;
+ return res;
+ }
+ }
+
+ /**
+ * Block until an Activity is created that matches this monitor,
+ * returning the resulting activity or till the timeOut period expires.
+ * If the timeOut expires before the activity is started, return null.
+ *
+ * @param timeOut Time to wait before the activity is created.
+ *
+ * @return Activity
+ */
+ public final Activity waitForActivityWithTimeout(long timeOut) {
+ synchronized (this) {
+ try {
+ wait(timeOut);
+ } catch (InterruptedException e) {
+ }
+ if (mLastActivity == null) {
+ return null;
+ } else {
+ Activity res = mLastActivity;
+ mLastActivity = null;
+ return res;
+ }
+ }
+ }
+
+ final boolean match(Context who,
+ Activity activity,
+ Intent intent) {
+ synchronized (this) {
+ if (mWhich != null
+ && mWhich.match(who.getContentResolver(), intent,
+ true, "Instrumentation") < 0) {
+ return false;
+ }
+ if (mClass != null) {
+ String cls = null;
+ if (activity != null) {
+ cls = activity.getClass().getName();
+ } else if (intent.getComponent() != null) {
+ cls = intent.getComponent().getClassName();
+ }
+ if (cls == null || !mClass.equals(cls)) {
+ return false;
+ }
+ }
+ if (activity != null) {
+ mLastActivity = activity;
+ notifyAll();
+ }
+ return true;
+ }
+ }
+ }
+
+ /**
+ * Add a new {@link ActivityMonitor} that will be checked whenever an
+ * activity is started. The monitor is added
+ * after any existing ones; the monitor will be hit only if none of the
+ * existing monitors can themselves handle the Intent.
+ *
+ * @param monitor The new ActivityMonitor to see.
+ *
+ * @see #addMonitor(IntentFilter, ActivityResult, boolean)
+ * @see #checkMonitorHit
+ */
+ public void addMonitor(ActivityMonitor monitor) {
+ synchronized (mSync) {
+ if (mActivityMonitors == null) {
+ mActivityMonitors = new ArrayList();
+ }
+ mActivityMonitors.add(monitor);
+ }
+ }
+
+ /**
+ * A convenience wrapper for {@link #addMonitor(ActivityMonitor)} that
+ * creates an intent filter matching {@link ActivityMonitor} for you and
+ * returns it.
+ *
+ * @param filter The set of intents this monitor is responsible for.
+ * @param result A canned result to return if the monitor is hit; can
+ * be null.
+ * @param block Controls whether the monitor should block the activity
+ * start (returning its canned result) or let the call
+ * proceed.
+ *
+ * @return The newly created and added activity monitor.
+ *
+ * @see #addMonitor(ActivityMonitor)
+ * @see #checkMonitorHit
+ */
+ public ActivityMonitor addMonitor(
+ IntentFilter filter, ActivityResult result, boolean block) {
+ ActivityMonitor am = new ActivityMonitor(filter, result, block);
+ addMonitor(am);
+ return am;
+ }
+
+ /**
+ * A convenience wrapper for {@link #addMonitor(ActivityMonitor)} that
+ * creates a class matching {@link ActivityMonitor} for you and returns it.
+ *
+ * @param cls The activity class this monitor is responsible for.
+ * @param result A canned result to return if the monitor is hit; can
+ * be null.
+ * @param block Controls whether the monitor should block the activity
+ * start (returning its canned result) or let the call
+ * proceed.
+ *
+ * @return The newly created and added activity monitor.
+ *
+ * @see #addMonitor(ActivityMonitor)
+ * @see #checkMonitorHit
+ */
+ public ActivityMonitor addMonitor(
+ String cls, ActivityResult result, boolean block) {
+ ActivityMonitor am = new ActivityMonitor(cls, result, block);
+ addMonitor(am);
+ return am;
+ }
+
+ /**
+ * Test whether an existing {@link ActivityMonitor} has been hit. If the
+ * monitor has been hit at least <var>minHits</var> times, then it will be
+ * removed from the activity monitor list and true returned. Otherwise it
+ * is left as-is and false is returned.
+ *
+ * @param monitor The ActivityMonitor to check.
+ * @param minHits The minimum number of hits required.
+ *
+ * @return True if the hit count has been reached, else false.
+ *
+ * @see #addMonitor
+ */
+ public boolean checkMonitorHit(ActivityMonitor monitor, int minHits) {
+ waitForIdleSync();
+ synchronized (mSync) {
+ if (monitor.getHits() < minHits) {
+ return false;
+ }
+ mActivityMonitors.remove(monitor);
+ }
+ return true;
+ }
+
+ /**
+ * Wait for an existing {@link ActivityMonitor} to be hit. Once the
+ * monitor has been hit, it is removed from the activity monitor list and
+ * the first created Activity object that matched it is returned.
+ *
+ * @param monitor The ActivityMonitor to wait for.
+ *
+ * @return The Activity object that matched the monitor.
+ */
+ public Activity waitForMonitor(ActivityMonitor monitor) {
+ Activity activity = monitor.waitForActivity();
+ synchronized (mSync) {
+ mActivityMonitors.remove(monitor);
+ }
+ return activity;
+ }
+
+ /**
+ * Wait for an existing {@link ActivityMonitor} to be hit till the timeout
+ * expires. Once the monitor has been hit, it is removed from the activity
+ * monitor list and the first created Activity object that matched it is
+ * returned. If the timeout expires, a null object is returned.
+ *
+ * @param monitor The ActivityMonitor to wait for.
+ * @param timeOut The timeout value in secs.
+ *
+ * @return The Activity object that matched the monitor.
+ */
+ public Activity waitForMonitorWithTimeout(ActivityMonitor monitor, long timeOut) {
+ Activity activity = monitor.waitForActivityWithTimeout(timeOut);
+ synchronized (mSync) {
+ mActivityMonitors.remove(monitor);
+ }
+ return activity;
+ }
+
+ /**
+ * Remove an {@link ActivityMonitor} that was previously added with
+ * {@link #addMonitor}.
+ *
+ * @param monitor The monitor to remove.
+ *
+ * @see #addMonitor
+ */
+ public void removeMonitor(ActivityMonitor monitor) {
+ synchronized (mSync) {
+ mActivityMonitors.remove(monitor);
+ }
+ }
+
+ /**
+ * Execute a particular menu item.
+ *
+ * @param targetActivity The activity in question.
+ * @param id The identifier associated with the menu item.
+ * @param flag Additional flags, if any.
+ * @return Whether the invocation was successful (for example, it could be
+ * false if item is disabled).
+ */
+ public boolean invokeMenuActionSync(Activity targetActivity,
+ int id, int flag) {
+ class MenuRunnable implements Runnable {
+ private final Activity activity;
+ private final int identifier;
+ private final int flags;
+ boolean returnValue;
+
+ public MenuRunnable(Activity _activity, int _identifier,
+ int _flags) {
+ activity = _activity;
+ identifier = _identifier;
+ flags = _flags;
+ }
+
+ public void run() {
+ Window win = activity.getWindow();
+
+ returnValue = win.performPanelIdentifierAction(
+ Window.FEATURE_OPTIONS_PANEL,
+ identifier,
+ flags);
+ }
+
+ }
+ MenuRunnable mr = new MenuRunnable(targetActivity, id, flag);
+ runOnMainSync(mr);
+ return mr.returnValue;
+ }
+
+ /**
+ * Show the context menu for the currently focused view and executes a
+ * particular context menu item.
+ *
+ * @param targetActivity The activity in question.
+ * @param id The identifier associated with the context menu item.
+ * @param flag Additional flags, if any.
+ * @return Whether the invocation was successful (for example, it could be
+ * false if item is disabled).
+ */
+ public boolean invokeContextMenuAction(Activity targetActivity, int id, int flag) {
+ validateNotAppThread();
+
+ // Bring up context menu for current focus.
+ // It'd be nice to do this through code, but currently ListView depends on
+ // long press to set metadata for its selected child
+
+ final KeyEvent downEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_CENTER);
+ sendKeySync(downEvent);
+
+ // Need to wait for long press
+ waitForIdleSync();
+ try {
+ Thread.sleep(ViewConfiguration.getLongPressTimeout());
+ } catch (InterruptedException e) {
+ Log.e(TAG, "Could not sleep for long press timeout", e);
+ return false;
+ }
+
+ final KeyEvent upEvent = new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DPAD_CENTER);
+ sendKeySync(upEvent);
+
+ // Wait for context menu to appear
+ waitForIdleSync();
+
+ class ContextMenuRunnable implements Runnable {
+ private final Activity activity;
+ private final int identifier;
+ private final int flags;
+ boolean returnValue;
+
+ public ContextMenuRunnable(Activity _activity, int _identifier,
+ int _flags) {
+ activity = _activity;
+ identifier = _identifier;
+ flags = _flags;
+ }
+
+ public void run() {
+ Window win = activity.getWindow();
+ returnValue = win.performContextMenuIdentifierAction(
+ identifier,
+ flags);
+ }
+
+ }
+
+ ContextMenuRunnable cmr = new ContextMenuRunnable(targetActivity, id, flag);
+ runOnMainSync(cmr);
+ return cmr.returnValue;
+ }
+
+ /**
+ * Sends the key events corresponding to the text to the app being
+ * instrumented.
+ *
+ * @param text The text to be sent.
+ */
+ public void sendStringSync(String text) {
+ if (text == null) {
+ return;
+ }
+ KeyCharacterMap keyCharacterMap =
+ KeyCharacterMap.load(KeyCharacterMap.BUILT_IN_KEYBOARD);
+
+ KeyEvent[] events = keyCharacterMap.getEvents(text.toCharArray());
+
+ if (events != null) {
+ for (int i = 0; i < events.length; i++) {
+ sendKeySync(events[i]);
+ }
+ }
+ }
+
+ /**
+ * 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
+ * from its event processing, though it may <em>not</em> have completely
+ * finished reacting from the event -- for example, if it needs to update
+ * its display as a result, it may still be in the process of doing that.
+ *
+ * @param event The event to send to the current focus.
+ */
+ public void sendKeySync(KeyEvent event) {
+ validateNotAppThread();
+ try {
+ (IWindowManager.Stub.asInterface(ServiceManager.getService("window")))
+ .injectKeyEvent(event, true);
+ } catch (RemoteException e) {
+ }
+ }
+
+ /**
+ * Sends an up and down key event sync to the currently focused window.
+ *
+ * @param key The integer keycode for the event.
+ */
+ public void sendKeyDownUpSync(int key) {
+ sendKeySync(new KeyEvent(KeyEvent.ACTION_DOWN, key));
+ sendKeySync(new KeyEvent(KeyEvent.ACTION_UP, key));
+ }
+
+ /**
+ * Higher-level method for sending both the down and up key events for a
+ * particular character key code. Equivalent to creating both KeyEvent
+ * objects by hand and calling {@link #sendKeySync}. The event appears
+ * as if it came from keyboard 0, the built in one.
+ *
+ * @param keyCode The key code of the character to send.
+ */
+ public void sendCharacterSync(int keyCode) {
+ sendKeySync(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
+ sendKeySync(new KeyEvent(KeyEvent.ACTION_UP, keyCode));
+ }
+
+ /**
+ * Dispatch a pointer event. Finished at some point after the recipient has
+ * returned from its event processing, though it may <em>not</em> have
+ * completely finished reacting from the event -- for example, if it needs
+ * to update its display as a result, it may still be in the process of
+ * doing that.
+ *
+ * @param event A motion event describing the pointer action. (As noted in
+ * {@link MotionEvent#obtain(long, long, int, float, float, int)}, be sure to use
+ * {@link SystemClock#uptimeMillis()} as the timebase.
+ */
+ public void sendPointerSync(MotionEvent event) {
+ validateNotAppThread();
+ try {
+ (IWindowManager.Stub.asInterface(ServiceManager.getService("window")))
+ .injectPointerEvent(event, true);
+ } catch (RemoteException e) {
+ }
+ }
+
+ /**
+ * Dispatch a trackball event. Finished at some point after the recipient has
+ * returned from its event processing, though it may <em>not</em> have
+ * completely finished reacting from the event -- for example, if it needs
+ * to update its display as a result, it may still be in the process of
+ * doing that.
+ *
+ * @param event A motion event describing the trackball action. (As noted in
+ * {@link MotionEvent#obtain(long, long, int, float, float, int)}, be sure to use
+ * {@link SystemClock#uptimeMillis()} as the timebase.
+ */
+ public void sendTrackballEventSync(MotionEvent event) {
+ validateNotAppThread();
+ try {
+ (IWindowManager.Stub.asInterface(ServiceManager.getService("window")))
+ .injectTrackballEvent(event, true);
+ } catch (RemoteException e) {
+ }
+ }
+
+ /**
+ * Perform instantiation of the process's {@link Application} object. The
+ * default implementation provides the normal system behavior.
+ *
+ * @param cl The ClassLoader with which to instantiate the object.
+ * @param className The name of the class implementing the Application
+ * object.
+ * @param context The context to initialize the application with
+ *
+ * @return The newly instantiated Application object.
+ */
+ public Application newApplication(ClassLoader cl, String className, Context context)
+ throws InstantiationException, IllegalAccessException,
+ ClassNotFoundException {
+ return newApplication(cl.loadClass(className), context);
+ }
+
+ /**
+ * Perform instantiation of the process's {@link Application} object. The
+ * default implementation provides the normal system behavior.
+ *
+ * @param clazz The class used to create an Application object from.
+ * @param context The context to initialize the application with
+ *
+ * @return The newly instantiated Application object.
+ */
+ static public Application newApplication(Class<?> clazz, Context context)
+ throws InstantiationException, IllegalAccessException,
+ ClassNotFoundException {
+ Application app = (Application)clazz.newInstance();
+ app.attach(context);
+ return app;
+ }
+
+ /**
+ * Perform calling of the application's {@link Application#onCreate}
+ * method. The default implementation simply calls through to that method.
+ *
+ * @param app The application being created.
+ */
+ public void callApplicationOnCreate(Application app) {
+ app.onCreate();
+ }
+
+ /**
+ * Perform instantiation of an {@link Activity} object. This method is intended for use with
+ * unit tests, such as android.test.ActivityUnitTestCase. The activity will be useable
+ * locally but will be missing some of the linkages necessary for use within the sytem.
+ *
+ * @param clazz The Class of the desired Activity
+ * @param context The base context for the activity to use
+ * @param token The token for this activity to communicate with
+ * @param application The application object (if any)
+ * @param intent The intent that started this Activity
+ * @param info ActivityInfo from the manifest
+ * @param title The title, typically retrieved from the ActivityInfo record
+ * @param parent The parent Activity (if any)
+ * @param id The embedded Id (if any)
+ * @param lastNonConfigurationInstance Arbitrary object that will be
+ * available via {@link Activity#getLastNonConfigurationInstance()
+ * Activity.getLastNonConfigurationInstance()}.
+ * @return Returns the instantiated activity
+ * @throws InstantiationException
+ * @throws IllegalAccessException
+ */
+ public Activity newActivity(Class<?> clazz, Context context,
+ IBinder token, Application application, Intent intent, ActivityInfo info,
+ CharSequence title, Activity parent, String id,
+ Object lastNonConfigurationInstance) throws InstantiationException,
+ IllegalAccessException {
+ Activity activity = (Activity)clazz.newInstance();
+ ActivityThread aThread = null;
+ activity.attach(context, aThread, this, token, application, intent, info, title,
+ parent, id, lastNonConfigurationInstance, new Configuration());
+ return activity;
+ }
+
+ /**
+ * Perform instantiation of the process's {@link Activity} object. The
+ * default implementation provides the normal system behavior.
+ *
+ * @param cl The ClassLoader with which to instantiate the object.
+ * @param className The name of the class implementing the Activity
+ * object.
+ * @param intent The Intent object that specified the activity class being
+ * instantiated.
+ *
+ * @return The newly instantiated Activity object.
+ */
+ public Activity newActivity(ClassLoader cl, String className,
+ Intent intent)
+ throws InstantiationException, IllegalAccessException,
+ ClassNotFoundException {
+ return (Activity)cl.loadClass(className).newInstance();
+ }
+
+ /**
+ * Perform calling of an activity's {@link Activity#onCreate}
+ * method. The default implementation simply calls through to that method.
+ *
+ * @param activity The activity being created.
+ * @param icicle The previously frozen state (or null) to pass through to
+ * onCreate().
+ */
+ public void callActivityOnCreate(Activity activity, Bundle icicle) {
+ if (mWaitingActivities != null) {
+ synchronized (mSync) {
+ final int N = mWaitingActivities.size();
+ for (int i=0; i<N; i++) {
+ final ActivityWaiter aw = mWaitingActivities.get(i);
+ final Intent intent = aw.intent;
+ if (intent.filterEquals(activity.getIntent())) {
+ aw.activity = activity;
+ mMessageQueue.addIdleHandler(new ActivityGoing(aw));
+ }
+ }
+ }
+ }
+
+ activity.onCreate(icicle);
+
+ if (mActivityMonitors != null) {
+ synchronized (mSync) {
+ final int N = mActivityMonitors.size();
+ for (int i=0; i<N; i++) {
+ final ActivityMonitor am = mActivityMonitors.get(i);
+ am.match(activity, activity, activity.getIntent());
+ }
+ }
+ }
+ }
+
+ public void callActivityOnDestroy(Activity activity) {
+ if (mWaitingActivities != null) {
+ synchronized (mSync) {
+ final int N = mWaitingActivities.size();
+ for (int i=0; i<N; i++) {
+ final ActivityWaiter aw = mWaitingActivities.get(i);
+ final Intent intent = aw.intent;
+ if (intent.filterEquals(activity.getIntent())) {
+ aw.activity = activity;
+ mMessageQueue.addIdleHandler(new ActivityGoing(aw));
+ }
+ }
+ }
+ }
+
+ activity.onDestroy();
+
+ if (mActivityMonitors != null) {
+ synchronized (mSync) {
+ final int N = mActivityMonitors.size();
+ for (int i=0; i<N; i++) {
+ final ActivityMonitor am = mActivityMonitors.get(i);
+ am.match(activity, activity, activity.getIntent());
+ }
+ }
+ }
+ }
+
+ /**
+ * Perform calling of an activity's {@link Activity#onRestoreInstanceState}
+ * method. The default implementation simply calls through to that method.
+ *
+ * @param activity The activity being restored.
+ * @param savedInstanceState The previously saved state being restored.
+ */
+ public void callActivityOnRestoreInstanceState(Activity activity, Bundle savedInstanceState) {
+ activity.performRestoreInstanceState(savedInstanceState);
+ }
+
+ /**
+ * Perform calling of an activity's {@link Activity#onPostCreate} method.
+ * The default implementation simply calls through to that method.
+ *
+ * @param activity The activity being created.
+ * @param icicle The previously frozen state (or null) to pass through to
+ * onPostCreate().
+ */
+ public void callActivityOnPostCreate(Activity activity, Bundle icicle) {
+ activity.onPostCreate(icicle);
+ }
+
+ /**
+ * Perform calling of an activity's {@link Activity#onNewIntent}
+ * method. The default implementation simply calls through to that method.
+ *
+ * @param activity The activity receiving a new Intent.
+ * @param intent The new intent being received.
+ */
+ public void callActivityOnNewIntent(Activity activity, Intent intent) {
+ activity.onNewIntent(intent);
+ }
+
+ /**
+ * Perform calling of an activity's {@link Activity#onStart}
+ * method. The default implementation simply calls through to that method.
+ *
+ * @param activity The activity being started.
+ */
+ public void callActivityOnStart(Activity activity) {
+ activity.onStart();
+ }
+
+ /**
+ * Perform calling of an activity's {@link Activity#onRestart}
+ * method. The default implementation simply calls through to that method.
+ *
+ * @param activity The activity being restarted.
+ */
+ public void callActivityOnRestart(Activity activity) {
+ activity.onRestart();
+ }
+
+ /**
+ * Perform calling of an activity's {@link Activity#onResume} method. The
+ * default implementation simply calls through to that method.
+ *
+ * @param activity The activity being resumed.
+ */
+ public void callActivityOnResume(Activity activity) {
+ activity.onResume();
+
+ if (mActivityMonitors != null) {
+ synchronized (mSync) {
+ final int N = mActivityMonitors.size();
+ for (int i=0; i<N; i++) {
+ final ActivityMonitor am = mActivityMonitors.get(i);
+ am.match(activity, activity, activity.getIntent());
+ }
+ }
+ }
+ }
+
+ /**
+ * Perform calling of an activity's {@link Activity#onStop}
+ * method. The default implementation simply calls through to that method.
+ *
+ * @param activity The activity being stopped.
+ */
+ public void callActivityOnStop(Activity activity) {
+ activity.onStop();
+ }
+
+ /**
+ * Perform calling of an activity's {@link Activity#onPause} method. The
+ * default implementation simply calls through to that method.
+ *
+ * @param activity The activity being saved.
+ * @param outState The bundle to pass to the call.
+ */
+ public void callActivityOnSaveInstanceState(Activity activity, Bundle outState) {
+ activity.performSaveInstanceState(outState);
+ }
+
+ /**
+ * Perform calling of an activity's {@link Activity#onPause} method. The
+ * default implementation simply calls through to that method.
+ *
+ * @param activity The activity being paused.
+ */
+ public void callActivityOnPause(Activity activity) {
+ activity.onPause();
+ }
+
+ /*
+ * Starts allocation counting. This triggers a gc and resets the counts.
+ */
+ public void startAllocCounting() {
+ // Before we start trigger a GC and reset the debug counts. Run the
+ // finalizers and another GC before starting and stopping the alloc
+ // counts. This will free up any objects that were just sitting around
+ // waiting for their finalizers to be run.
+ Runtime.getRuntime().gc();
+ Runtime.getRuntime().runFinalization();
+ Runtime.getRuntime().gc();
+
+ Debug.resetAllCounts();
+
+ // start the counts
+ Debug.startAllocCounting();
+ }
+
+ /*
+ * Stops allocation counting.
+ */
+ public void stopAllocCounting() {
+ Runtime.getRuntime().gc();
+ Runtime.getRuntime().runFinalization();
+ Runtime.getRuntime().gc();
+ Debug.stopAllocCounting();
+ }
+
+ /**
+ * If Results already contains Key, it appends Value to the key's ArrayList
+ * associated with the key. If the key doesn't already exist in results, it
+ * adds the key/value pair to results.
+ */
+ private void addValue(String key, int value, Bundle results) {
+ if (results.containsKey(key)) {
+ List<Integer> list = results.getIntegerArrayList(key);
+ if (list != null) {
+ list.add(value);
+ }
+ } else {
+ ArrayList<Integer> list = new ArrayList<Integer>();
+ list.add(value);
+ results.putIntegerArrayList(key, list);
+ }
+ }
+
+ /**
+ * Returns a bundle with the current results from the allocation counting.
+ */
+ public Bundle getAllocCounts() {
+ Bundle results = new Bundle();
+ results.putLong("global_alloc_count", Debug.getGlobalAllocCount());
+ results.putLong("global_alloc_size", Debug.getGlobalAllocSize());
+ results.putLong("global_freed_count", Debug.getGlobalFreedCount());
+ results.putLong("global_freed_size", Debug.getGlobalFreedSize());
+ results.putLong("gc_invocation_count", Debug.getGlobalGcInvocationCount());
+ return results;
+ }
+
+ /**
+ * Returns a bundle with the counts for various binder counts for this process. Currently the only two that are
+ * reported are the number of send and the number of received transactions.
+ */
+ public Bundle getBinderCounts() {
+ Bundle results = new Bundle();
+ results.putLong("sent_transactions", Debug.getBinderSentTransactions());
+ results.putLong("received_transactions", Debug.getBinderReceivedTransactions());
+ return results;
+ }
+
+ /**
+ * Description of a Activity execution result to return to the original
+ * activity.
+ */
+ public static final class ActivityResult {
+ /**
+ * Create a new activity result. See {@link Activity#setResult} for
+ * more information.
+ *
+ * @param resultCode The result code to propagate back to the
+ * originating activity, often RESULT_CANCELED or RESULT_OK
+ * @param resultData The data to propagate back to the originating
+ * activity.
+ */
+ public ActivityResult(int resultCode, Intent resultData) {
+ mResultCode = resultCode;
+ mResultData = resultData;
+ }
+
+ /**
+ * Retrieve the result code contained in this result.
+ */
+ public int getResultCode() {
+ return mResultCode;
+ }
+
+ /**
+ * Retrieve the data contained in this result.
+ */
+ public Intent getResultData() {
+ return mResultData;
+ }
+
+ private final int mResultCode;
+ private final Intent mResultData;
+ }
+
+ /**
+ * Execute a startActivity call made by the application. The default
+ * implementation takes care of updating any active {@link ActivityMonitor}
+ * objects and dispatches this call to the system activity manager; you can
+ * override this to watch for the application to start an activity, and
+ * modify what happens when it does.
+ *
+ * <p>This method returns an {@link ActivityResult} object, which you can
+ * use when intercepting application calls to avoid performing the start
+ * activity action but still return the result the application is
+ * expecting. To do this, override this method to catch the call to start
+ * activity so that it returns a new ActivityResult containing the results
+ * you would like the application to see, and don't call up to the super
+ * class. Note that an application is only expecting a result if
+ * <var>requestCode</var> is &gt;= 0.
+ *
+ * <p>This method throws {@link android.content.ActivityNotFoundException}
+ * if there was no Activity found to run the given Intent.
+ *
+ * @param who The Context from which the activity is being started.
+ * @param whoThread The main thread of the Context from which the activity
+ * is being started.
+ * @param token Internal token identifying to the system who is starting
+ * the activity; may be null.
+ * @param target Which activity is perform the start (and thus receiving
+ * any result); may be null if this call is not being made
+ * from an activity.
+ * @param intent The actual Intent to start.
+ * @param requestCode Identifier for this request's result; less than zero
+ * if the caller is not expecting a result.
+ *
+ * @return To force the return of a particular result, return an
+ * ActivityResult object containing the desired data; otherwise
+ * return null. The default implementation always returns null.
+ *
+ * @throws android.content.ActivityNotFoundException
+ *
+ * @see Activity#startActivity(Intent)
+ * @see Activity#startActivityForResult(Intent, int)
+ * @see Activity#startActivityFromChild
+ *
+ * {@hide}
+ */
+ public ActivityResult execStartActivity(
+ Context who, IApplicationThread whoThread, IBinder token, Activity target,
+ Intent intent, int requestCode) {
+ if (mActivityMonitors != null) {
+ synchronized (mSync) {
+ final int N = mActivityMonitors.size();
+ for (int i=0; i<N; i++) {
+ final ActivityMonitor am = mActivityMonitors.get(i);
+ if (am.match(who, null, intent)) {
+ am.mHits++;
+ if (am.isBlocking()) {
+ return requestCode >= 0 ? am.getResult() : null;
+ }
+ break;
+ }
+ }
+ }
+ }
+ try {
+ int result = ActivityManagerNative.getDefault()
+ .startActivity(whoThread, intent,
+ intent.resolveTypeIfNeeded(who.getContentResolver()),
+ null, 0, token, target != null ? target.mEmbeddedID : null,
+ requestCode, false, false);
+ checkStartActivityResult(result, intent);
+ } catch (RemoteException e) {
+ }
+ return null;
+ }
+
+ /*package*/ final void init(ActivityThread thread,
+ Context instrContext, Context appContext, ComponentName component,
+ IInstrumentationWatcher watcher) {
+ mThread = thread;
+ mMessageQueue = mThread.getLooper().myQueue();
+ mInstrContext = instrContext;
+ mAppContext = appContext;
+ mComponent = component;
+ mWatcher = watcher;
+ }
+
+ /*package*/ static void checkStartActivityResult(int res, Intent intent) {
+ if (res >= IActivityManager.START_SUCCESS) {
+ return;
+ }
+
+ switch (res) {
+ case IActivityManager.START_INTENT_NOT_RESOLVED:
+ case IActivityManager.START_CLASS_NOT_FOUND:
+ if (intent.getComponent() != null)
+ throw new ActivityNotFoundException(
+ "Unable to find explicit activity class "
+ + intent.getComponent().toShortString()
+ + "; have you declared this activity in your AndroidManifest.xml?");
+ throw new ActivityNotFoundException(
+ "No Activity found to handle " + intent);
+ case IActivityManager.START_PERMISSION_DENIED:
+ throw new SecurityException("Not allowed to start activity "
+ + intent);
+ case IActivityManager.START_FORWARD_AND_REQUEST_CONFLICT:
+ throw new AndroidRuntimeException(
+ "FORWARD_RESULT_FLAG used while also requesting a result");
+ default:
+ throw new AndroidRuntimeException("Unknown error code "
+ + res + " when starting " + intent);
+ }
+ }
+
+ private final void validateNotAppThread() {
+ if (ActivityThread.currentActivityThread() != null) {
+ throw new RuntimeException(
+ "This method can not be called from the main application thread");
+ }
+ }
+
+ private final class InstrumentationThread extends Thread {
+ public InstrumentationThread(String name) {
+ super(name);
+ }
+ public void run() {
+ IActivityManager am = ActivityManagerNative.getDefault();
+ try {
+ Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_DISPLAY);
+ } catch (RuntimeException e) {
+ Log.w(TAG, "Exception setting priority of instrumentation thread "
+ + Process.myTid(), e);
+ }
+ if (mAutomaticPerformanceSnapshots) {
+ startPerformanceSnapshot();
+ }
+ onStart();
+ }
+ }
+
+ private static final class EmptyRunnable implements Runnable {
+ public void run() {
+ }
+ }
+
+ private static final class SyncRunnable implements Runnable {
+ private final Runnable mTarget;
+ private boolean mComplete;
+
+ public SyncRunnable(Runnable target) {
+ mTarget = target;
+ }
+
+ public void run() {
+ mTarget.run();
+ synchronized (this) {
+ mComplete = true;
+ notifyAll();
+ }
+ }
+
+ public void waitForComplete() {
+ synchronized (this) {
+ while (!mComplete) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ }
+ }
+ }
+ }
+ }
+
+ private static final class ActivityWaiter {
+ public final Intent intent;
+ public Activity activity;
+
+ public ActivityWaiter(Intent _intent) {
+ intent = _intent;
+ }
+ }
+
+ private final class ActivityGoing implements MessageQueue.IdleHandler {
+ private final ActivityWaiter mWaiter;
+
+ public ActivityGoing(ActivityWaiter waiter) {
+ mWaiter = waiter;
+ }
+
+ public final boolean queueIdle() {
+ synchronized (mSync) {
+ mWaitingActivities.remove(mWaiter);
+ mSync.notifyAll();
+ }
+ return false;
+ }
+ }
+
+ private static final class Idler implements MessageQueue.IdleHandler {
+ private final Runnable mCallback;
+ private boolean mIdle;
+
+ public Idler(Runnable callback) {
+ mCallback = callback;
+ mIdle = false;
+ }
+
+ public final boolean queueIdle() {
+ if (mCallback != null) {
+ mCallback.run();
+ }
+ synchronized (this) {
+ mIdle = true;
+ notifyAll();
+ }
+ return false;
+ }
+
+ public void waitForIdle() {
+ synchronized (this) {
+ while (!mIdle) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/core/java/android/app/KeyguardManager.java b/core/java/android/app/KeyguardManager.java
new file mode 100644
index 0000000..0c07553
--- /dev/null
+++ b/core/java/android/app/KeyguardManager.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.content.Context;
+import android.os.Binder;
+import android.os.RemoteException;
+import android.os.IBinder;
+import android.os.ServiceManager;
+import android.view.IWindowManager;
+import android.view.IOnKeyguardExitResult;
+
+/**
+ * Class that can be used to lock and unlock the keyboard. Get an instance of this
+ * class by calling {@link android.content.Context#getSystemService(java.lang.String)}
+ * with argument {@link android.content.Context#KEYGUARD_SERVICE}. The
+ * Actual class to control the keyboard locking is
+ * {@link android.app.KeyguardManager.KeyguardLock}.
+ */
+public class KeyguardManager {
+ private IWindowManager mWM;
+
+ /**
+ * Handle returned by {@link KeyguardManager#newKeyguardLock} that allows
+ * you to disable / reenable the keyguard.
+ */
+ public class KeyguardLock {
+ private IBinder mToken = new Binder();
+ private String mTag;
+
+ KeyguardLock(String tag) {
+ mTag = tag;
+ }
+
+ /**
+ * Disable the keyguard from showing. If the keyguard is currently
+ * showing, hide it. The keyguard will be prevented from showing again
+ * until {@link #reenableKeyguard()} is called.
+ *
+ * A good place to call this is from {@link android.app.Activity#onResume()}
+ *
+ * @see #reenableKeyguard()
+ */
+ public void disableKeyguard() {
+ try {
+ mWM.disableKeyguard(mToken, mTag);
+ } catch (RemoteException ex) {
+ }
+ }
+
+ /**
+ * Reenable the keyguard. The keyguard will reappear if the previous
+ * call to {@link #disableKeyguard()} caused it it to be hidden.
+ *
+ * A good place to call this is from {@link android.app.Activity#onPause()}
+ *
+ * @see #disableKeyguard()
+ */
+ public void reenableKeyguard() {
+ try {
+ mWM.reenableKeyguard(mToken);
+ } catch (RemoteException ex) {
+ }
+ }
+ }
+
+ /**
+ * Callback passed to {@link KeyguardManager#exitKeyguardSecurely} to notify
+ * caller of result.
+ */
+ public interface OnKeyguardExitResult {
+
+ /**
+ * @param success True if the user was able to authenticate, false if
+ * not.
+ */
+ void onKeyguardExitResult(boolean success);
+ }
+
+
+ KeyguardManager() {
+ mWM = IWindowManager.Stub.asInterface(ServiceManager.getService(Context.WINDOW_SERVICE));
+ }
+
+ /**
+ * Enables you to lock or unlock the keyboard. Get an instance of this class by
+ * calling {@link android.content.Context#getSystemService(java.lang.String) Context.getSystemService()}.
+ * This class is wrapped by {@link android.app.KeyguardManager KeyguardManager}.
+ * @param tag A tag that informally identifies who you are (for debugging who
+ * is disabling he keyguard).
+ *
+ * @return A {@link KeyguardLock} handle to use to disable and reenable the
+ * keyguard.
+ */
+ public KeyguardLock newKeyguardLock(String tag) {
+ return new KeyguardLock(tag);
+ }
+
+ /**
+ * If keyguard screen is showing or in restricted key input mode (i.e. in
+ * keyguard password emergency screen). When in such mode, certain keys,
+ * such as the Home key and the right soft keys, don't work.
+ *
+ * @return true if in keyguard restricted input mode.
+ *
+ * @see android.view.WindowManagerPolicy#inKeyguardRestrictedKeyInputMode
+ */
+ public boolean inKeyguardRestrictedInputMode() {
+ try {
+ return mWM.inKeyguardRestrictedInputMode();
+ } catch (RemoteException ex) {
+ return false;
+ }
+ }
+
+ /**
+ * Exit the keyguard securely. The use case for this api is that, after
+ * disabling the keyguard, your app, which was granted permission to
+ * disable the keyguard and show a limited amount of information deemed
+ * safe without the user getting past the keyguard, needs to navigate to
+ * something that is not safe to view without getting past the keyguard.
+ *
+ * This will, if the keyguard is secure, bring up the unlock screen of
+ * the keyguard.
+ *
+ * @param callback Let's you know whether the operation was succesful and
+ * it is safe to launch anything that would normally be considered safe
+ * once the user has gotten past the keyguard.
+ */
+ public void exitKeyguardSecurely(final OnKeyguardExitResult callback) {
+ try {
+ mWM.exitKeyguardSecurely(new IOnKeyguardExitResult.Stub() {
+ public void onKeyguardExitResult(boolean success) throws RemoteException {
+ callback.onKeyguardExitResult(success);
+ }
+ });
+ } catch (RemoteException e) {
+
+ }
+ }
+}
diff --git a/core/java/android/app/LauncherActivity.java b/core/java/android/app/LauncherActivity.java
new file mode 100644
index 0000000..8f0a4f5
--- /dev/null
+++ b/core/java/android/app/LauncherActivity.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.Bundle;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.Filter;
+import android.widget.Filterable;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+
+/**
+ * Displays a list of all activities which can be performed
+ * for a given intent. Launches when clicked.
+ *
+ */
+public abstract class LauncherActivity extends ListActivity {
+
+ /**
+ * Adapter which shows the set of activities that can be performed for a given intent.
+ */
+ private class ActivityAdapter extends BaseAdapter implements Filterable {
+ private final Object lock = new Object();
+ private ArrayList<ResolveInfo> mOriginalValues;
+
+ protected final Context mContext;
+ protected final Intent mIntent;
+ protected final LayoutInflater mInflater;
+
+ protected List<ResolveInfo> mActivitiesList;
+
+ private Filter mFilter;
+
+ public ActivityAdapter(Context context, Intent intent) {
+ mContext = context;
+ mIntent = new Intent(intent);
+ mIntent.setComponent(null);
+ mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ PackageManager pm = context.getPackageManager();
+ mActivitiesList = pm.queryIntentActivities(intent, 0);
+ if (mActivitiesList != null) {
+ Collections.sort(mActivitiesList, new ResolveInfo.DisplayNameComparator(pm));
+ }
+ }
+
+ public Intent intentForPosition(int position) {
+ if (mActivitiesList == null) {
+ return null;
+ }
+
+ Intent intent = new Intent(mIntent);
+ ActivityInfo ai = mActivitiesList.get(position).activityInfo;
+ intent.setClassName(ai.applicationInfo.packageName, ai.name);
+ return intent;
+ }
+
+ public int getCount() {
+ return mActivitiesList != null ? mActivitiesList.size() : 0;
+ }
+
+ public Object getItem(int position) {
+ return position;
+ }
+
+ public long getItemId(int position) {
+ return position;
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View view;
+ if (convertView == null) {
+ view = mInflater.inflate(
+ com.android.internal.R.layout.simple_list_item_1, parent, false);
+ } else {
+ view = convertView;
+ }
+ bindView(view, mActivitiesList.get(position));
+ return view;
+ }
+
+ private char getCandidateLetter(ResolveInfo info) {
+ PackageManager pm = mContext.getPackageManager();
+ CharSequence label = info.loadLabel(pm);
+
+ if (label == null) {
+ label = info.activityInfo.name;
+ }
+
+ return Character.toLowerCase(label.charAt(0));
+ }
+
+ private void bindView(View view, ResolveInfo info) {
+ TextView text = (TextView) view.findViewById(com.android.internal.R.id.text1);
+
+ PackageManager pm = mContext.getPackageManager();
+ CharSequence label = info.loadLabel(pm);
+ text.setText(label != null ? label : info.activityInfo.name);
+ }
+
+ public Filter getFilter() {
+ if (mFilter == null) {
+ mFilter = new ArrayFilter();
+ }
+ return mFilter;
+ }
+
+ /**
+ * <p>An array filters constrains the content of the array adapter with a prefix. Each item that
+ * does not start with the supplied prefix is removed from the list.</p>
+ */
+ private class ArrayFilter extends Filter {
+ @Override
+ protected FilterResults performFiltering(CharSequence prefix) {
+ FilterResults results = new FilterResults();
+
+ if (mOriginalValues == null) {
+ synchronized (lock) {
+ mOriginalValues = new ArrayList<ResolveInfo>(mActivitiesList);
+ }
+ }
+
+ if (prefix == null || prefix.length() == 0) {
+ synchronized (lock) {
+ ArrayList<ResolveInfo> list = new ArrayList<ResolveInfo>(mOriginalValues);
+ results.values = list;
+ results.count = list.size();
+ }
+ } else {
+ final PackageManager pm = mContext.getPackageManager();
+ final String prefixString = prefix.toString().toLowerCase();
+
+ ArrayList<ResolveInfo> values = mOriginalValues;
+ int count = values.size();
+
+ ArrayList<ResolveInfo> newValues = new ArrayList<ResolveInfo>(count);
+
+ for (int i = 0; i < count; i++) {
+ ResolveInfo value = values.get(i);
+
+ final CharSequence label = value.loadLabel(pm);
+ final CharSequence name = label != null ? label : value.activityInfo.name;
+
+ String[] words = name.toString().toLowerCase().split(" ");
+ int wordCount = words.length;
+
+ for (int k = 0; k < wordCount; k++) {
+ final String word = words[k];
+
+ if (word.startsWith(prefixString)) {
+ newValues.add(value);
+ break;
+ }
+ }
+ }
+
+ results.values = newValues;
+ results.count = newValues.size();
+ }
+
+ return results;
+ }
+
+ @Override
+ protected void publishResults(CharSequence constraint, FilterResults results) {
+ //noinspection unchecked
+ mActivitiesList = (List<ResolveInfo>) results.values;
+ if (results.count > 0) {
+ notifyDataSetChanged();
+ } else {
+ notifyDataSetInvalidated();
+ }
+ }
+ }
+ }
+
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mAdapter = new ActivityAdapter(this, getTargetIntent());
+
+ setListAdapter(mAdapter);
+ getListView().setTextFilterEnabled(true);
+ }
+
+
+ @Override
+ protected void onListItemClick(ListView l, View v, int position, long id) {
+ Intent intent = ((ActivityAdapter)mAdapter).intentForPosition(position);
+
+ startActivity(intent);
+ }
+
+ protected abstract Intent getTargetIntent();
+
+}
diff --git a/core/java/android/app/ListActivity.java b/core/java/android/app/ListActivity.java
new file mode 100644
index 0000000..2818937
--- /dev/null
+++ b/core/java/android/app/ListActivity.java
@@ -0,0 +1,316 @@
+/*
+ * 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.app;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.view.KeyEvent;
+import android.view.View;
+import android.widget.Adapter;
+import android.widget.AdapterView;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+
+/**
+ * An activity that displays a list of items by binding to a data source such as
+ * an array or Cursor, and exposes event handlers when the user selects an item.
+ * <p>
+ * ListActivity hosts a {@link android.widget.ListView ListView} object that can
+ * be bound to different data sources, typically either an array or a Cursor
+ * holding query results. Binding, screen layout, and row layout are discussed
+ * in the following sections.
+ * <p>
+ * <strong>Screen Layout</strong>
+ * </p>
+ * <p>
+ * ListActivity has a default layout that consists of a single, full-screen list
+ * in the center of the screen. However, if you desire, you can customize the
+ * screen layout by setting your own view layout with setContentView() in
+ * onCreate(). To do this, your own view MUST contain a ListView object with the
+ * id "@android:id/list" (or {@link android.R.id#list} if it's in code)
+ * <p>
+ * Optionally, your custom view can contain another view object of any type to
+ * display when the list view is empty. This "empty list" notifier must have an
+ * id "android:empty". Note that when an empty view is present, the list view
+ * will be hidden when there is no data to display.
+ * <p>
+ * The following code demonstrates an (ugly) custom screen layout. It has a list
+ * with a green background, and an alternate red "no data" message.
+ * </p>
+ *
+ * <pre>
+ * &lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
+ * &lt;LinearLayout
+ * android:orientation=&quot;vertical&quot;
+ * android:layout_width=&quot;fill_parent&quot;
+ * android:layout_height=&quot;fill_parent&quot;
+ * android:paddingLeft=&quot;8&quot;
+ * android:paddingRight=&quot;8&quot;&gt;
+ *
+ * &lt;ListView id=&quot;android:list&quot;
+ * android:layout_width=&quot;fill_parent&quot;
+ * android:layout_height=&quot;fill_parent&quot;
+ * android:background=&quot;#00FF00&quot;
+ * android:layout_weight=&quot;1&quot;
+ * android:drawSelectorOnTop=&quot;false&quot;/&gt;
+ *
+ * &lt;TextView id=&quot;android:empty&quot;
+ * android:layout_width=&quot;fill_parent&quot;
+ * android:layout_height=&quot;fill_parent&quot;
+ * android:background=&quot;#FF0000&quot;
+ * android:text=&quot;No data&quot;/&gt;
+ * &lt;/LinearLayout&gt;
+ * </pre>
+ *
+ * <p>
+ * <strong>Row Layout</strong>
+ * </p>
+ * <p>
+ * You can specify the layout of individual rows in the list. You do this by
+ * specifying a layout resource in the ListAdapter object hosted by the activity
+ * (the ListAdapter binds the ListView to the data; more on this later).
+ * <p>
+ * A ListAdapter constructor takes a parameter that specifies a layout resource
+ * for each row. It also has two additional parameters that let you specify
+ * which data field to associate with which object in the row layout resource.
+ * These two parameters are typically parallel arrays.
+ * </p>
+ * <p>
+ * Android provides some standard row layout resources. These are in the
+ * {@link android.R.layout} class, and have names such as simple_list_item_1,
+ * simple_list_item_2, and two_line_list_item. The following layout XML is the
+ * source for the resource two_line_list_item, which displays two data
+ * fields,one above the other, for each list row.
+ * </p>
+ *
+ * <pre>
+ * &lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
+ * &lt;LinearLayout
+ * android:layout_width=&quot;fill_parent&quot;
+ * android:layout_height=&quot;wrap_content&quot;
+ * android:orientation=&quot;vertical&quot;&gt;
+ *
+ * &lt;TextView id=&quot;text1&quot;
+ * android:textSize=&quot;16&quot;
+ * android:textStyle=&quot;bold&quot;
+ * android:layout_width=&quot;fill_parent&quot;
+ * android:layout_height=&quot;wrap_content&quot;/&gt;
+ *
+ * &lt;TextView id=&quot;text2&quot;
+ * android:textSize=&quot;16&quot;
+ * android:layout_width=&quot;fill_parent&quot;
+ * android:layout_height=&quot;wrap_content&quot;/&gt;
+ * &lt;/LinearLayout&gt;
+ * </pre>
+ *
+ * <p>
+ * You must identify the data bound to each TextView object in this layout. The
+ * syntax for this is discussed in the next section.
+ * </p>
+ * <p>
+ * <strong>Binding to Data</strong>
+ * </p>
+ * <p>
+ * You bind the ListActivity's ListView object to data using a class that
+ * implements the {@link android.widget.ListAdapter ListAdapter} interface.
+ * Android provides two standard list adapters:
+ * {@link android.widget.SimpleAdapter SimpleAdapter} for static data (Maps),
+ * and {@link android.widget.SimpleCursorAdapter SimpleCursorAdapter} for Cursor
+ * query results.
+ * </p>
+ * <p>
+ * The following code from a custom ListActivity demonstrates querying the
+ * Contacts provider for all contacts, then binding the Name and Company fields
+ * to a two line row layout in the activity's ListView.
+ * </p>
+ *
+ * <pre>
+ * public class MyListAdapter extends ListActivity {
+ *
+ * &#064;Override
+ * protected void onCreate(Bundle icicle){
+ * super.onCreate(icicle);
+ *
+ * // We'll define a custom screen layout here (the one shown above), but
+ * // typically, you could just use the standard ListActivity layout.
+ * setContentView(R.layout.custom_list_activity_view);
+ *
+ * // Query for all people contacts using the {@link android.provider.Contacts.People} convenience class.
+ * // Put a managed wrapper around the retrieved cursor so we don't have to worry about
+ * // requerying or closing it as the activity changes state.
+ * mCursor = People.query(this.getContentResolver(), null);
+ * startManagingCursor(mCursor);
+ *
+ * // Now create a new list adapter bound to the cursor.
+ * // SimpleListAdapter is designed for binding to a Cursor.
+ * ListAdapter adapter = new SimpleCursorAdapter(
+ * this, // Context.
+ * android.R.layout.two_line_list_item, // Specify the row template to use (here, two columns bound to the two retrieved cursor
+ * rows).
+ * mCursor, // Pass in the cursor to bind to.
+ * new String[] {People.NAME, People.COMPANY}, // Array of cursor columns to bind to.
+ * new int[]); // Parallel array of which template objects to bind to those columns.
+ *
+ * // Bind to our new adapter.
+ * setListAdapter(adapter);
+ * }
+ * }
+ * </pre>
+ *
+ * @see #setListAdapter
+ * @see android.widget.ListView
+ */
+public class ListActivity extends Activity {
+ /**
+ * This field should be made private, so it is hidden from the SDK.
+ * {@hide}
+ */
+ protected ListAdapter mAdapter;
+ /**
+ * This field should be made private, so it is hidden from the SDK.
+ * {@hide}
+ */
+ protected ListView mList;
+
+ private Handler mHandler = new Handler();
+ private boolean mFinishedStart = false;
+
+ private Runnable mRequestFocus = new Runnable() {
+ public void run() {
+ mList.focusableViewAvailable(mList);
+ }
+ };
+
+ /**
+ * This method will be called when an item in the list is selected.
+ * Subclasses should override. Subclasses can call
+ * getListView().getItemAtPosition(position) if they need to access the
+ * data associated with the selected item.
+ *
+ * @param l The ListView where the click happened
+ * @param v The view that was clicked within the ListView
+ * @param position The position of the view in the list
+ * @param id The row id of the item that was clicked
+ */
+ protected void onListItemClick(ListView l, View v, int position, long id) {
+ }
+
+ /**
+ * Ensures the list view has been created before Activity restores all
+ * of the view states.
+ *
+ *@see Activity#onRestoreInstanceState(Bundle)
+ */
+ @Override
+ protected void onRestoreInstanceState(Bundle state) {
+ ensureList();
+ super.onRestoreInstanceState(state);
+ }
+
+ /**
+ * Updates the screen state (current list and other views) when the
+ * content changes.
+ *
+ * @see Activity#onContentChanged()
+ */
+ @Override
+ public void onContentChanged() {
+ super.onContentChanged();
+ View emptyView = findViewById(com.android.internal.R.id.empty);
+ mList = (ListView)findViewById(com.android.internal.R.id.list);
+ if (mList == null) {
+ throw new RuntimeException(
+ "Your content must have a ListView whose id attribute is " +
+ "'android.R.id.list'");
+ }
+ if (emptyView != null) {
+ mList.setEmptyView(emptyView);
+ }
+ mList.setOnItemClickListener(mOnClickListener);
+ if (mFinishedStart) {
+ setListAdapter(mAdapter);
+ }
+ mHandler.post(mRequestFocus);
+ mFinishedStart = true;
+ }
+
+ /**
+ * Provide the cursor for the list view.
+ */
+ public void setListAdapter(ListAdapter adapter) {
+ synchronized (this) {
+ ensureList();
+ mAdapter = adapter;
+ mList.setAdapter(adapter);
+ }
+ }
+
+ /**
+ * Set the currently selected list item to the specified
+ * position with the adapter's data
+ *
+ * @param position
+ */
+ public void setSelection(int position) {
+ mList.setSelection(position);
+ }
+
+ /**
+ * Get the position of the currently selected list item.
+ */
+ public int getSelectedItemPosition() {
+ return mList.getSelectedItemPosition();
+ }
+
+ /**
+ * Get the cursor row ID of the currently selected list item.
+ */
+ public long getSelectedItemId() {
+ return mList.getSelectedItemId();
+ }
+
+ /**
+ * Get the activity's list view widget.
+ */
+ public ListView getListView() {
+ ensureList();
+ return mList;
+ }
+
+ /**
+ * Get the ListAdapter associated with this activity's ListView.
+ */
+ public ListAdapter getListAdapter() {
+ return mAdapter;
+ }
+
+ private void ensureList() {
+ if (mList != null) {
+ return;
+ }
+ setContentView(com.android.internal.R.layout.list_content);
+
+ }
+
+ private AdapterView.OnItemClickListener mOnClickListener = new AdapterView.OnItemClickListener() {
+ public void onItemClick(AdapterView parent, View v, int position, long id)
+ {
+ onListItemClick((ListView)parent, v, position, id);
+ }
+ };
+}
+
diff --git a/core/java/android/app/LocalActivityManager.java b/core/java/android/app/LocalActivityManager.java
new file mode 100644
index 0000000..12e70e3
--- /dev/null
+++ b/core/java/android/app/LocalActivityManager.java
@@ -0,0 +1,597 @@
+/*
+ * 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.app;
+
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.os.Binder;
+import android.os.Bundle;
+import android.util.Config;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.Window;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+/**
+ * Helper class for managing multiple running embedded activities in the same
+ * process. This class is not normally used directly, but rather created for
+ * you as part of the {@link android.app.ActivityGroup} implementation.
+ *
+ * @see ActivityGroup
+ */
+public class LocalActivityManager {
+ private static final String TAG = "LocalActivityManager";
+ private static final boolean localLOGV = false || Config.LOGV;
+
+ // Internal token for an Activity being managed by LocalActivityManager.
+ private static class LocalActivityRecord extends Binder {
+ LocalActivityRecord(String _id, Intent _intent) {
+ id = _id;
+ intent = _intent;
+ }
+
+ final String id; // Unique name of this record.
+ Intent intent; // Which activity to run here.
+ ActivityInfo activityInfo; // Package manager info about activity.
+ Activity activity; // Currently instantiated activity.
+ Window window; // Activity's top-level window.
+ Bundle instanceState; // Last retrieved freeze state.
+ int curState = RESTORED; // Current state the activity is in.
+ }
+
+ static final int RESTORED = 0; // State restored, but no startActivity().
+ static final int INITIALIZING = 1; // Ready to launch (after startActivity()).
+ static final int CREATED = 2; // Created, not started or resumed.
+ static final int STARTED = 3; // Created and started, not resumed.
+ static final int RESUMED = 4; // Created started and resumed.
+ static final int DESTROYED = 5; // No longer with us.
+
+ /** Thread our activities are running in. */
+ private final ActivityThread mActivityThread;
+ /** The containing activity that owns the activities we create. */
+ private final Activity mParent;
+
+ /** The activity that is currently resumed. */
+ private LocalActivityRecord mResumed;
+ /** id -> record of all known activities. */
+ private final Map<String, LocalActivityRecord> mActivities
+ = new HashMap<String, LocalActivityRecord>();
+ /** array of all known activities for easy iterating. */
+ private final ArrayList<LocalActivityRecord> mActivityArray
+ = new ArrayList<LocalActivityRecord>();
+
+ /** True if only one activity can be resumed at a time */
+ private boolean mSingleMode;
+
+ /** Set to true once we find out the container is finishing. */
+ private boolean mFinishing;
+
+ /** Current state the owner (ActivityGroup) is in */
+ private int mCurState = INITIALIZING;
+
+ /** String ids of running activities starting with least recently used. */
+ // TODO: put back in stopping of activities.
+ //private List<LocalActivityRecord> mLRU = new ArrayList();
+
+ /**
+ * Create a new LocalActivityManager for holding activities running within
+ * the given <var>parent</var>.
+ *
+ * @param parent the host of the embedded activities
+ * @param singleMode True if the LocalActivityManger should keep a maximum
+ * of one activity resumed
+ */
+ public LocalActivityManager(Activity parent, boolean singleMode) {
+ mActivityThread = ActivityThread.currentActivityThread();
+ mParent = parent;
+ mSingleMode = singleMode;
+ }
+
+ private void moveToState(LocalActivityRecord r, int desiredState) {
+ if (r.curState == RESTORED || r.curState == DESTROYED) {
+ // startActivity() has not yet been called, so nothing to do.
+ return;
+ }
+
+ if (r.curState == INITIALIZING) {
+ // We need to have always created the activity.
+ if (localLOGV) Log.v(TAG, r.id + ": starting " + r.intent);
+ if (r.activityInfo == null) {
+ r.activityInfo = mActivityThread.resolveActivityInfo(r.intent);
+ }
+ r.activity = mActivityThread.startActivityNow(
+ mParent, r.id, r.intent, r.activityInfo, r, r.instanceState);
+ if (r.activity == null) {
+ return;
+ }
+ r.window = r.activity.getWindow();
+ r.instanceState = null;
+ r.curState = STARTED;
+
+ if (desiredState == RESUMED) {
+ if (localLOGV) Log.v(TAG, r.id + ": resuming");
+ mActivityThread.performResumeActivity(r, true);
+ r.curState = RESUMED;
+ }
+
+ // Don't do anything more here. There is an important case:
+ // if this is being done as part of onCreate() of the group, then
+ // the launching of the activity gets its state a little ahead
+ // of our own (it is now STARTED, while we are only CREATED).
+ // If we just leave things as-is, we'll deal with it as the
+ // group's state catches up.
+ return;
+ }
+
+ switch (r.curState) {
+ case CREATED:
+ if (desiredState == STARTED) {
+ if (localLOGV) Log.v(TAG, r.id + ": restarting");
+ mActivityThread.performRestartActivity(r);
+ r.curState = STARTED;
+ }
+ if (desiredState == RESUMED) {
+ if (localLOGV) Log.v(TAG, r.id + ": restarting and resuming");
+ mActivityThread.performRestartActivity(r);
+ mActivityThread.performResumeActivity(r, true);
+ r.curState = RESUMED;
+ }
+ return;
+
+ case STARTED:
+ if (desiredState == RESUMED) {
+ // Need to resume it...
+ if (localLOGV) Log.v(TAG, r.id + ": resuming");
+ mActivityThread.performResumeActivity(r, true);
+ r.instanceState = null;
+ r.curState = RESUMED;
+ }
+ if (desiredState == CREATED) {
+ if (localLOGV) Log.v(TAG, r.id + ": stopping");
+ mActivityThread.performStopActivity(r);
+ r.curState = CREATED;
+ }
+ return;
+
+ case RESUMED:
+ if (desiredState == STARTED) {
+ if (localLOGV) Log.v(TAG, r.id + ": pausing");
+ performPause(r, mFinishing);
+ r.curState = STARTED;
+ }
+ if (desiredState == CREATED) {
+ if (localLOGV) Log.v(TAG, r.id + ": pausing");
+ performPause(r, mFinishing);
+ if (localLOGV) Log.v(TAG, r.id + ": stopping");
+ mActivityThread.performStopActivity(r);
+ r.curState = CREATED;
+ }
+ return;
+ }
+ }
+
+ private void performPause(LocalActivityRecord r, boolean finishing) {
+ boolean needState = r.instanceState == null;
+ Bundle instanceState = mActivityThread.performPauseActivity(r,
+ finishing, needState);
+ if (needState) {
+ r.instanceState = instanceState;
+ }
+ }
+
+ /**
+ * Start a new activity running in the group. Every activity you start
+ * must have a unique string ID associated with it -- this is used to keep
+ * track of the activity, so that if you later call startActivity() again
+ * on it the same activity object will be retained.
+ *
+ * <p>When there had previously been an activity started under this id,
+ * it may either be destroyed and a new one started, or the current
+ * one re-used, based on these conditions, in order:</p>
+ *
+ * <ul>
+ * <li> If the Intent maps to a different activity component than is
+ * currently running, the current activity is finished and a new one
+ * started.
+ * <li> If the current activity uses a non-multiple launch mode (such
+ * as singleTop), or the Intent has the
+ * {@link Intent#FLAG_ACTIVITY_SINGLE_TOP} flag set, then the current
+ * activity will remain running and its
+ * {@link Activity#onNewIntent(Intent) Activity.onNewIntent()} method
+ * called.
+ * <li> If the new Intent is the same (excluding extras) as the previous
+ * one, and the new Intent does not have the
+ * {@link Intent#FLAG_ACTIVITY_CLEAR_TOP} set, then the current activity
+ * will remain running as-is.
+ * <li> Otherwise, the current activity will be finished and a new
+ * one started.
+ * </ul>
+ *
+ * <p>If the given Intent can not be resolved to an available Activity,
+ * this method throws {@link android.content.ActivityNotFoundException}.
+ *
+ * <p>Warning: There is an issue where, if the Intent does not
+ * include an explicit component, we can restore the state for a different
+ * activity class than was previously running when the state was saved (if
+ * the set of available activities changes between those points).
+ *
+ * @param id Unique identifier of the activity to be started
+ * @param intent The Intent describing the activity to be started
+ *
+ * @return Returns the window of the activity. The caller needs to take
+ * care of adding this window to a view hierarchy, and likewise dealing
+ * with removing the old window if the activity has changed.
+ *
+ * @throws android.content.ActivityNotFoundException
+ */
+ public Window startActivity(String id, Intent intent) {
+ if (mCurState == INITIALIZING) {
+ throw new IllegalStateException(
+ "Activities can't be added until the containing group has been created.");
+ }
+
+ boolean adding = false;
+ boolean sameIntent = false;
+
+ ActivityInfo aInfo = null;
+
+ // Already have information about the new activity id?
+ LocalActivityRecord r = mActivities.get(id);
+ if (r == null) {
+ // Need to create it...
+ r = new LocalActivityRecord(id, intent);
+ adding = true;
+ } else if (r.intent != null) {
+ sameIntent = r.intent.filterEquals(intent);
+ if (sameIntent) {
+ // We are starting the same activity.
+ aInfo = r.activityInfo;
+ }
+ }
+ if (aInfo == null) {
+ aInfo = mActivityThread.resolveActivityInfo(intent);
+ }
+
+ // Pause the currently running activity if there is one and only a single
+ // activity is allowed to be running at a time.
+ if (mSingleMode) {
+ LocalActivityRecord old = mResumed;
+
+ // If there was a previous activity, and it is not the current
+ // activity, we need to stop it.
+ if (old != null && old != r && mCurState == RESUMED) {
+ moveToState(old, STARTED);
+ }
+ }
+
+ if (adding) {
+ // It's a brand new world.
+ mActivities.put(id, r);
+ mActivityArray.add(r);
+
+ } else if (r.activityInfo != null) {
+ // If the new activity is the same as the current one, then
+ // we may be able to reuse it.
+ if (aInfo == r.activityInfo ||
+ (aInfo.name.equals(r.activityInfo.name) &&
+ aInfo.packageName.equals(r.activityInfo.packageName))) {
+ if (aInfo.launchMode != ActivityInfo.LAUNCH_MULTIPLE ||
+ (intent.getFlags()&Intent.FLAG_ACTIVITY_SINGLE_TOP) != 0) {
+ // The activity wants onNewIntent() called.
+ ArrayList<Intent> intents = new ArrayList<Intent>(1);
+ intents.add(intent);
+ if (localLOGV) Log.v(TAG, r.id + ": new intent");
+ mActivityThread.performNewIntents(r, intents);
+ r.intent = intent;
+ moveToState(r, mCurState);
+ if (mSingleMode) {
+ mResumed = r;
+ }
+ return r.window;
+ }
+ if (sameIntent &&
+ (intent.getFlags()&Intent.FLAG_ACTIVITY_CLEAR_TOP) == 0) {
+ // We are showing the same thing, so this activity is
+ // just resumed and stays as-is.
+ r.intent = intent;
+ moveToState(r, mCurState);
+ if (mSingleMode) {
+ mResumed = r;
+ }
+ return r.window;
+ }
+ }
+
+ // The new activity is different than the current one, or it
+ // is a multiple launch activity, so we need to destroy what
+ // is currently there.
+ performDestroy(r, true);
+ }
+
+ r.intent = intent;
+ r.curState = INITIALIZING;
+ r.activityInfo = aInfo;
+
+ moveToState(r, mCurState);
+
+ // When in single mode keep track of the current activity
+ if (mSingleMode) {
+ mResumed = r;
+ }
+ return r.window;
+ }
+
+ private Window performDestroy(LocalActivityRecord r, boolean finish) {
+ Window win = null;
+ win = r.window;
+ if (r.curState == RESUMED && !finish) {
+ performPause(r, finish);
+ }
+ if (localLOGV) Log.v(TAG, r.id + ": destroying");
+ mActivityThread.performDestroyActivity(r, finish);
+ r.activity = null;
+ r.window = null;
+ if (finish) {
+ r.instanceState = null;
+ }
+ r.curState = DESTROYED;
+ return win;
+ }
+
+ /**
+ * Destroy the activity associated with a particular id. This activity
+ * will go through the normal lifecycle events and fine onDestroy(), and
+ * then the id removed from the group.
+ *
+ * @param id Unique identifier of the activity to be destroyed
+ * @param finish If true, this activity will be finished, so its id and
+ * all state are removed from the group.
+ *
+ * @return Returns the window that was used to display the activity, or
+ * null if there was none.
+ */
+ public Window destroyActivity(String id, boolean finish) {
+ LocalActivityRecord r = mActivities.get(id);
+ Window win = null;
+ if (r != null) {
+ win = performDestroy(r, finish);
+ if (finish) {
+ mActivities.remove(r);
+ }
+ }
+ return win;
+ }
+
+ /**
+ * Retrieve the Activity that is currently running.
+ *
+ * @return the currently running (resumed) Activity, or null if there is
+ * not one
+ *
+ * @see #startActivity
+ * @see #getCurrentId
+ */
+ public Activity getCurrentActivity() {
+ return mResumed != null ? mResumed.activity : null;
+ }
+
+ /**
+ * Retrieve the ID of the activity that is currently running.
+ *
+ * @return the ID of the currently running (resumed) Activity, or null if
+ * there is not one
+ *
+ * @see #startActivity
+ * @see #getCurrentActivity
+ */
+ public String getCurrentId() {
+ return mResumed != null ? mResumed.id : null;
+ }
+
+ /**
+ * Return the Activity object associated with a string ID.
+ *
+ * @see #startActivity
+ *
+ * @return the associated Activity object, or null if the id is unknown or
+ * its activity is not currently instantiated
+ */
+ public Activity getActivity(String id) {
+ LocalActivityRecord r = mActivities.get(id);
+ return r != null ? r.activity : null;
+ }
+
+ /**
+ * Restore a state that was previously returned by {@link #saveInstanceState}. This
+ * adds to the activity group information about all activity IDs that had
+ * previously been saved, even if they have not been started yet, so if the
+ * user later navigates to them the correct state will be restored.
+ *
+ * <p>Note: This does <b>not</b> change the current running activity, or
+ * start whatever activity was previously running when the state was saved.
+ * That is up to the client to do, in whatever way it thinks is best.
+ *
+ * @param state a previously saved state; does nothing if this is null
+ *
+ * @see #saveInstanceState
+ */
+ public void dispatchCreate(Bundle state) {
+ if (state != null) {
+ final Iterator<String> i = state.keySet().iterator();
+ while (i.hasNext()) {
+ try {
+ final String id = i.next();
+ final Bundle astate = state.getBundle(id);
+ LocalActivityRecord r = mActivities.get(id);
+ if (r != null) {
+ r.instanceState = astate;
+ } else {
+ r = new LocalActivityRecord(id, null);
+ r.instanceState = astate;
+ mActivities.put(id, r);
+ mActivityArray.add(r);
+ }
+ } catch (Exception e) {
+ // Recover from -all- app errors.
+ Log.e(TAG,
+ "Exception thrown when restoring LocalActivityManager state",
+ e);
+ }
+ }
+ }
+
+ mCurState = CREATED;
+ }
+
+ /**
+ * Retrieve the state of all activities known by the group. For
+ * activities that have previously run and are now stopped or finished, the
+ * last saved state is used. For the current running activity, its
+ * {@link Activity#onSaveInstanceState} is called to retrieve its current state.
+ *
+ * @return a Bundle holding the newly created state of all known activities
+ *
+ * @see #dispatchCreate
+ */
+ public Bundle saveInstanceState() {
+ Bundle state = null;
+
+ // FIXME: child activities will freeze as part of onPaused. Do we
+ // need to do this here?
+ final int N = mActivityArray.size();
+ for (int i=0; i<N; i++) {
+ final LocalActivityRecord r = mActivityArray.get(i);
+ if (state == null) {
+ state = new Bundle();
+ }
+ if ((r.instanceState != null || r.curState == RESUMED)
+ && r.activity != null) {
+ // We need to save the state now, if we don't currently
+ // already have it or the activity is currently resumed.
+ final Bundle childState = new Bundle();
+ r.activity.onSaveInstanceState(childState);
+ r.instanceState = childState;
+ }
+ if (r.instanceState != null) {
+ state.putBundle(r.id, r.instanceState);
+ }
+ }
+
+ return state;
+ }
+
+ /**
+ * Called by the container activity in its {@link Activity#onResume} so
+ * that LocalActivityManager can perform the corresponding action on the
+ * activities it holds.
+ *
+ * @see Activity#onResume
+ */
+ public void dispatchResume() {
+ mCurState = RESUMED;
+ if (mSingleMode) {
+ if (mResumed != null) {
+ moveToState(mResumed, RESUMED);
+ }
+ } else {
+ final int N = mActivityArray.size();
+ for (int i=0; i<N; i++) {
+ moveToState(mActivityArray.get(i), RESUMED);
+ }
+ }
+ }
+
+ /**
+ * Called by the container activity in its {@link Activity#onPause} so
+ * that LocalActivityManager can perform the corresponding action on the
+ * activities it holds.
+ *
+ * @param finishing set to true if the parent activity has been finished;
+ * this can be determined by calling
+ * Activity.isFinishing()
+ *
+ * @see Activity#onPause
+ * @see Activity#isFinishing
+ */
+ public void dispatchPause(boolean finishing) {
+ if (finishing) {
+ mFinishing = true;
+ }
+ mCurState = STARTED;
+ if (mSingleMode) {
+ if (mResumed != null) {
+ moveToState(mResumed, STARTED);
+ }
+ } else {
+ final int N = mActivityArray.size();
+ for (int i=0; i<N; i++) {
+ LocalActivityRecord r = mActivityArray.get(i);
+ if (r.curState == RESUMED) {
+ moveToState(r, STARTED);
+ }
+ }
+ }
+ }
+
+ /**
+ * Called by the container activity in its {@link Activity#onStop} so
+ * that LocalActivityManager can perform the corresponding action on the
+ * activities it holds.
+ *
+ * @see Activity#onStop
+ */
+ public void dispatchStop() {
+ mCurState = CREATED;
+ final int N = mActivityArray.size();
+ for (int i=0; i<N; i++) {
+ LocalActivityRecord r = mActivityArray.get(i);
+ moveToState(r, CREATED);
+ }
+ }
+
+ /**
+ * Remove all activities from this LocalActivityManager, performing an
+ * {@link Activity#onDestroy} on any that are currently instantiated.
+ */
+ public void removeAllActivities() {
+ dispatchDestroy(true);
+ }
+
+ /**
+ * Called by the container activity in its {@link Activity#onDestroy} so
+ * that LocalActivityManager can perform the corresponding action on the
+ * activities it holds.
+ *
+ * @see Activity#onDestroy
+ */
+ public void dispatchDestroy(boolean finishing) {
+ final int N = mActivityArray.size();
+ for (int i=0; i<N; i++) {
+ LocalActivityRecord r = mActivityArray.get(i);
+ if (localLOGV) Log.v(TAG, r.id + ": destroying");
+ mActivityThread.performDestroyActivity(r, finishing);
+ }
+ mActivities.clear();
+ mActivityArray.clear();
+ }
+}
diff --git a/core/java/android/app/Notification.aidl b/core/java/android/app/Notification.aidl
new file mode 100644
index 0000000..9d8129c
--- /dev/null
+++ b/core/java/android/app/Notification.aidl
@@ -0,0 +1,19 @@
+/**
+ * Copyright (c) 2007, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+parcelable Notification;
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
new file mode 100644
index 0000000..cc56385
--- /dev/null
+++ b/core/java/android/app/Notification.java
@@ -0,0 +1,482 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import java.util.Date;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.pim.DateFormat;
+import android.pim.DateUtils;
+import android.text.TextUtils;
+import android.widget.RemoteViews;
+
+/**
+ * A class that represents how a persistent notification is to be presented to
+ * the user using the {@link android.app.NotificationManager}.
+ *
+ */
+public class Notification implements Parcelable
+{
+ /**
+ * Use all default values (where applicable).
+ */
+ public static final int DEFAULT_ALL = ~0;
+
+ /**
+ * Use the default notification sound. This will ignore any given
+ * {@link #sound}.
+ *
+ * @see #defaults
+ */
+ public static final int DEFAULT_SOUND = 1;
+
+ /**
+ * Use the default notification vibrate. This will ignore any given
+ * {@link #vibrate}.
+ *
+ * @see #defaults
+ */
+ public static final int DEFAULT_VIBRATE = 2;
+
+ /**
+ * Use the default notification lights. This will ignore the
+ * {@link #FLAG_SHOW_LIGHTS} bit, and {@link #ledARGB}, {@link #ledOffMS}, or
+ * {@link #ledOnMS}.
+ *
+ * @see #defaults
+ */
+ public static final int DEFAULT_LIGHTS = 4;
+
+ /**
+ * The timestamp for the notification. The icons and expanded views
+ * are sorted by this key.
+ */
+ public long when;
+
+ /**
+ * The resource id of a drawable to use as the icon in the status bar.
+ */
+ public int icon;
+
+ /**
+ * The number of events that this notification represents. For example, if this is the
+ * new mail notification, this would be the number of unread messages. This number is
+ * be superimposed over the icon in the status bar. If the number is 0 or negative, it
+ * is not shown in the status bar.
+ */
+ public int number;
+
+ /**
+ * The intent to execute when the expanded status entry is clicked. If
+ * this is an activity, it must include the
+ * {@link android.content.Intent#FLAG_ACTIVITY_NEW_TASK} flag, which requires
+ * that you take care of task management as described in the
+ * <a href="{@docRoot}intro/appmodel.html">application model</a> document.
+ */
+ public PendingIntent contentIntent;
+
+ /**
+ * The intent to execute when the status entry is deleted by the user
+ * with the "Clear All Notifications" button. This probably shouldn't
+ * be launching an activity since several of those will be sent at the
+ * same time.
+ */
+ public PendingIntent deleteIntent;
+
+ /**
+ * Text to scroll across the screen when this item is added to
+ * the status bar.
+ */
+ public CharSequence tickerText;
+
+ /**
+ * The view that shows when this notification is shown in the expanded status bar.
+ */
+ public RemoteViews contentView;
+
+ /**
+ * If the icon in the status bar is to have more than one level, you can set this. Otherwise,
+ * leave it at its default value of 0.
+ *
+ * @see android.widget.ImageView#setImageLevel
+ * @see android.graphics.drawable#setLevel
+ */
+ public int iconLevel;
+
+ /**
+ * The sound to play.
+ *
+ * <p>
+ * To play the default notification sound, see {@link #defaults}.
+ * </p>
+ */
+ public Uri sound;
+
+ /**
+ * Use this constant as the value for audioStreamType to request that
+ * the default stream type for notifications be used. Currently the
+ * default stream type is STREAM_RING.
+ */
+ public static final int STREAM_DEFAULT = -1;
+
+ /**
+ * The audio stream type to use when playing the sound.
+ * Should be one of the STREAM_ constants from
+ * {@link android.media.AudioManager}.
+ */
+ public int audioStreamType = STREAM_DEFAULT;
+
+
+ /**
+ * The pattern with which to vibrate. This pattern will repeat if {@link
+ * #FLAG_INSISTENT} bit is set in the {@link #flags} field.
+ *
+ * <p>
+ * To vibrate the default pattern, see {@link #defaults}.
+ * </p>
+ *
+ * @see android.os.Vibrator#vibrate(long[],int)
+ */
+ public long[] vibrate;
+
+ /**
+ * The color of the led. The hardware will do its best approximation.
+ *
+ * @see #FLAG_SHOW_LIGHTS
+ * @see #flags
+ */
+ public int ledARGB;
+
+ /**
+ * The number of milliseconds for the LED to be on while it's flashing.
+ * The hardware will do its best approximation.
+ *
+ * @see #FLAG_SHOW_LIGHTS
+ * @see #flags
+ */
+ public int ledOnMS;
+
+ /**
+ * The number of milliseconds for the LED to be off while it's flashing.
+ * The hardware will do its best approximation.
+ *
+ * @see #FLAG_SHOW_LIGHTS
+ * @see #flags
+ */
+ public int ledOffMS;
+
+ /**
+ * Specifies which values should be taken from the defaults.
+ * <p>
+ * To set, OR the desired from {@link #DEFAULT_SOUND},
+ * {@link #DEFAULT_VIBRATE}, {@link #DEFAULT_LIGHTS}. For all default
+ * values, use {@link #DEFAULT_ALL}.
+ * </p>
+ */
+ public int defaults;
+
+
+ /**
+ * Bit to be bitwise-ored into the {@link #flags} field that should be
+ * set if you want the LED on for this notification.
+ * <ul>
+ * <li>To turn the LED off, pass 0 in the alpha channel for colorARGB
+ * or 0 for both ledOnMS and ledOffMS.</li>
+ * <li>To turn the LED on, pass 1 for ledOnMS and 0 for ledOffMS.</li>
+ * <li>To flash the LED, pass the number of milliseconds that it should
+ * be on and off to ledOnMS and ledOffMS.</li>
+ * </ul>
+ * <p>
+ * Since hardware varies, you are not guaranteed that any of the values
+ * you pass are honored exactly. Use the system defaults (TODO) if possible
+ * because they will be set to values that work on any given hardware.
+ * <p>
+ * The alpha channel must be set for forward compatibility.
+ *
+ */
+ public static final int FLAG_SHOW_LIGHTS = 0x00000001;
+
+ /**
+ * Bit to be bitwise-ored into the {@link #flags} field that should be
+ * set if this notification is in reference to something that is ongoing,
+ * like a phone call. It should not be set if this notification is in
+ * reference to something that happened at a particular point in time,
+ * like a missed phone call.
+ */
+ public static final int FLAG_ONGOING_EVENT = 0x00000002;
+
+ /**
+ * Bit to be bitwise-ored into the {@link #flags} field that if set,
+ * the audio and vibration will be repeated until the notification is
+ * cancelled.
+ *
+ * <p>
+ * NOTE: This notion will change when we have decided exactly
+ * what the UI will be.
+ * </p>
+ */
+ public static final int FLAG_INSISTENT = 0x00000004;
+
+ /**
+ * Bit to be bitwise-ored into the {@link #flags} field that should be
+ * set if you want the sound and/or vibration play each time the
+ * notification is sent, even if it has not been canceled before that.
+ */
+ public static final int FLAG_ONLY_ALERT_ONCE = 0x00000008;
+
+ /**
+ * Bit to be bitwise-ored into the {@link #flags} field that should be
+ * set if the notification should be canceled when it is clicked by the
+ * user.
+ */
+ public static final int FLAG_AUTO_CANCEL = 0x00000010;
+
+ /**
+ * Bit to be bitwise-ored into the {@link #flags} field that should be
+ * set if the notification should not be canceled when the user clicks
+ * the Clear all button.
+ */
+ public static final int FLAG_NO_CLEAR = 0x00000020;
+
+ public int flags;
+
+ /**
+ * Constructs a Notification object with everything set to 0.
+ */
+ public Notification()
+ {
+ this.when = System.currentTimeMillis();
+ }
+
+ /**
+ * @deprecated use {@link #Notification(int,CharSequence,long)} and {@link #setLatestEventInfo}.
+ * @hide
+ */
+ public Notification(Context context, int icon, CharSequence tickerText, long when,
+ CharSequence contentTitle, CharSequence contentText, Intent contentIntent)
+ {
+ this.when = when;
+ this.icon = icon;
+ this.tickerText = tickerText;
+ setLatestEventInfo(context, contentTitle, contentText,
+ PendingIntent.getActivity(context, 0, contentIntent, 0));
+ }
+
+ /**
+ * Constructs a Notification object with the information needed to
+ * have a status bar icon without the standard expanded view.
+ *
+ * @param icon The resource id of the icon to put in the status bar.
+ * @param tickerText The text that flows by in the status bar when the notification first
+ * activates.
+ * @param when The time to show in the time field. In the System.currentTimeMillis
+ * timebase.
+ */
+ public Notification(int icon, CharSequence tickerText, long when)
+ {
+ this.icon = icon;
+ this.tickerText = tickerText;
+ this.when = when;
+ }
+
+ /**
+ * Unflatten the notification from a parcel.
+ */
+ public Notification(Parcel parcel)
+ {
+ int version = parcel.readInt();
+
+ when = parcel.readLong();
+ icon = parcel.readInt();
+ number = parcel.readInt();
+ if (parcel.readInt() != 0) {
+ contentIntent = PendingIntent.CREATOR.createFromParcel(parcel);
+ }
+ if (parcel.readInt() != 0) {
+ deleteIntent = PendingIntent.CREATOR.createFromParcel(parcel);
+ }
+ if (parcel.readInt() != 0) {
+ tickerText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel);
+ }
+ if (parcel.readInt() != 0) {
+ contentView = RemoteViews.CREATOR.createFromParcel(parcel);
+ }
+ defaults = parcel.readInt();
+ flags = parcel.readInt();
+ if (parcel.readInt() != 0) {
+ sound = Uri.CREATOR.createFromParcel(parcel);
+ }
+
+ audioStreamType = parcel.readInt();
+ vibrate = parcel.createLongArray();
+ ledARGB = parcel.readInt();
+ ledOnMS = parcel.readInt();
+ ledOffMS = parcel.readInt();
+ iconLevel = parcel.readInt();
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Flatten this notification from a parcel.
+ */
+ public void writeToParcel(Parcel parcel, int flags)
+ {
+ parcel.writeInt(1);
+
+ parcel.writeLong(when);
+ parcel.writeInt(icon);
+ parcel.writeInt(number);
+ if (contentIntent != null) {
+ parcel.writeInt(1);
+ contentIntent.writeToParcel(parcel, 0);
+ } else {
+ parcel.writeInt(0);
+ }
+ if (deleteIntent != null) {
+ parcel.writeInt(1);
+ deleteIntent.writeToParcel(parcel, 0);
+ } else {
+ parcel.writeInt(0);
+ }
+ if (tickerText != null) {
+ parcel.writeInt(1);
+ TextUtils.writeToParcel(tickerText, parcel, flags);
+ } else {
+ parcel.writeInt(0);
+ }
+ if (contentView != null) {
+ parcel.writeInt(1);
+ contentView.writeToParcel(parcel, 0);
+ } else {
+ parcel.writeInt(0);
+ }
+
+ parcel.writeInt(defaults);
+ parcel.writeInt(this.flags);
+
+ if (sound != null) {
+ parcel.writeInt(1);
+ sound.writeToParcel(parcel, 0);
+ } else {
+ parcel.writeInt(0);
+ }
+ parcel.writeInt(audioStreamType);
+ parcel.writeLongArray(vibrate);
+ parcel.writeInt(ledARGB);
+ parcel.writeInt(ledOnMS);
+ parcel.writeInt(ledOffMS);
+ parcel.writeInt(iconLevel);
+ }
+
+ /**
+ * Parcelable.Creator that instantiates Notification objects
+ */
+ public static final Parcelable.Creator<Notification> CREATOR
+ = new Parcelable.Creator<Notification>()
+ {
+ public Notification createFromParcel(Parcel parcel)
+ {
+ return new Notification(parcel);
+ }
+
+ public Notification[] newArray(int size)
+ {
+ return new Notification[size];
+ }
+ };
+
+ /**
+ * Sets the {@link #contentView} field to be a view with the standard "Latest Event"
+ * layout.
+ *
+ * <p>Uses the {@link #icon} and {@link #when} fields to set the icon and time fields
+ * in the view.</p>
+ * @param context The context for your application / activity.
+ * @param contentTitle The title that goes in the expanded entry.
+ * @param contentText The text that goes in the expanded entry.
+ * @param contentIntent The intent to launch when the user clicks the expanded notification.
+ * If this is an activity, it must include the
+ * {@link android.content.Intent#FLAG_ACTIVITY_NEW_TASK} flag, which requires
+ * that you take care of task management as described in the
+ * <a href="{@docRoot}intro/appmodel.html">application model</a> document.
+ */
+ public void setLatestEventInfo(Context context,
+ CharSequence contentTitle, CharSequence contentText, PendingIntent contentIntent) {
+ RemoteViews contentView = new RemoteViews(context.getPackageName(),
+ com.android.internal.R.layout.status_bar_latest_event_content);
+ if (this.icon != 0) {
+ contentView.setImageViewResource(com.android.internal.R.id.icon, this.icon);
+ }
+ if (contentTitle != null) {
+ contentView.setTextViewText(com.android.internal.R.id.title, contentTitle);
+ }
+ if (contentText != null) {
+ contentView.setTextViewText(com.android.internal.R.id.text, contentText);
+ }
+ if (this.when != 0) {
+ Date date = new Date(when);
+ CharSequence str =
+ DateUtils.isToday(when) ? DateFormat.getTimeFormat(context).format(date)
+ : DateFormat.getDateFormat(context).format(date);
+ contentView.setTextViewText(com.android.internal.R.id.time, str);
+ }
+
+ this.contentView = contentView;
+ this.contentIntent = contentIntent;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("Notification(vibrate=");
+ if (this.vibrate != null) {
+ int N = this.vibrate.length-1;
+ sb.append("[");
+ for (int i=0; i<N; i++) {
+ sb.append(this.vibrate[i]);
+ sb.append(',');
+ }
+ sb.append(this.vibrate[N]);
+ sb.append("]");
+ } else if ((this.defaults & DEFAULT_VIBRATE) != 0) {
+ sb.append("default");
+ } else {
+ sb.append("null");
+ }
+ sb.append(",sound=");
+ if (this.sound != null) {
+ sb.append(this.sound.toString());
+ } else if ((this.defaults & DEFAULT_SOUND) != 0) {
+ sb.append("default");
+ } else {
+ sb.append("null");
+ }
+ sb.append(",defaults=0x");
+ sb.append(Integer.toHexString(this.defaults));
+ sb.append(")");
+ return sb.toString();
+ }
+}
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java
new file mode 100644
index 0000000..afb3827
--- /dev/null
+++ b/core/java/android/app/NotificationManager.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.content.Context;
+import android.os.RemoteException;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.ServiceManager;
+import android.util.Log;
+
+/**
+ * Class to notify the user of events that happen. This is how you tell
+ * the user that something has happened in the background. {@more}
+ *
+ * Notifications can take different forms:
+ * <ul>
+ * <li>A persistent icon that goes in the status bar and is accessible
+ * through the launcher, (when the user selects it, a designated Intent
+ * can be launched),</li>
+ * <li>Turning on or flashing LEDs on the device, or</li>
+ * <li>Alerting the user by flashing the backlight, playing a sound,
+ * or vibrating.</li>
+ * </ul>
+ *
+ * <p>
+ * Each of the notify methods takes an int id parameter. This id identifies
+ * this notification from your app to the system, so that id should be unique
+ * within your app. If you call one of the notify methods with an id that is
+ * currently active and a new set of notification parameters, it will be
+ * updated. For example, if you pass a new status bar icon, the old icon in
+ * the status bar will be replaced with the new one. This is also the same
+ * id you pass to the {@link #cancel} method to clear this notification.
+ *
+ * <p>
+ * You do not instantiate this class directly; instead, retrieve it through
+ * {@link android.content.Context#getSystemService}.
+ *
+ * @see android.app.Notification
+ * @see android.content.Context#getSystemService
+ */
+public class NotificationManager
+{
+ private static String TAG = "NotificationManager";
+ private static boolean DEBUG = false;
+ private static boolean localLOGV = DEBUG || android.util.Config.LOGV;
+
+ private static INotificationManager sService;
+
+ static private INotificationManager getService()
+ {
+ if (sService != null) {
+ return sService;
+ }
+ IBinder b = ServiceManager.getService("notification");
+ sService = INotificationManager.Stub.asInterface(b);
+ return sService;
+ }
+
+ /*package*/ NotificationManager(Context context, Handler handler)
+ {
+ mContext = context;
+ }
+
+ /**
+ * Persistent notification on the status bar,
+ *
+ * @param id An identifier for this notification unique within your
+ * application.
+ * @param notification A {@link Notification} object describing how to
+ * notify the user, other than the view you're providing. If you
+ * pass null, there will be no persistent notification and no
+ * flashing, vibration, etc.
+ */
+ public void notify(int id, Notification notification)
+ {
+ int[] idOut = new int[1];
+ INotificationManager service = getService();
+ String pkg = mContext.getPackageName();
+ if (localLOGV) Log.v(TAG, pkg + ": notify(" + id + ", " + notification + ")");
+ try {
+ service.enqueueNotification(pkg, id, notification, idOut);
+ if (id != idOut[0]) {
+ Log.w(TAG, "notify: id corrupted: sent " + id + ", got back " + idOut[0]);
+ }
+ } catch (RemoteException e) {
+ }
+ }
+
+ /**
+ * Cancel a previously shown notification. If it's transient, the view
+ * will be hidden. If it's persistent, it will be removed from the status
+ * bar.
+ */
+ public void cancel(int id)
+ {
+ INotificationManager service = getService();
+ String pkg = mContext.getPackageName();
+ if (localLOGV) Log.v(TAG, pkg + ": cancel(" + id + ")");
+ try {
+ service.cancelNotification(pkg, id);
+ } catch (RemoteException e) {
+ }
+ }
+
+ /**
+ * Cancel all previously shown notifications. See {@link #cancel} for the
+ * detailed behavior.
+ */
+ public void cancelAll()
+ {
+ INotificationManager service = getService();
+ String pkg = mContext.getPackageName();
+ if (localLOGV) Log.v(TAG, pkg + ": cancelAll()");
+ try {
+ service.cancelAllNotifications(pkg);
+ } catch (RemoteException e) {
+ }
+ }
+
+ private Context mContext;
+}
diff --git a/core/java/android/app/PendingIntent.aidl b/core/java/android/app/PendingIntent.aidl
new file mode 100644
index 0000000..f0d530c
--- /dev/null
+++ b/core/java/android/app/PendingIntent.aidl
@@ -0,0 +1,20 @@
+/* //device/java/android/android/content/Intent.aidl
+**
+** Copyright 2007, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.app;
+
+parcelable PendingIntent;
diff --git a/core/java/android/app/PendingIntent.java b/core/java/android/app/PendingIntent.java
new file mode 100644
index 0000000..ba84903
--- /dev/null
+++ b/core/java/android/app/PendingIntent.java
@@ -0,0 +1,489 @@
+/*
+ * 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.app;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AndroidException;
+
+/**
+ * A description of an Intent and target action to perform with it. Instances
+ * of this class are created with {@link #getActivity},
+ * {@link #getBroadcast}, {@link #getService}; the returned object can be
+ * handed to other applications so that they can perform the action you
+ * described on your behalf at a later time.
+ *
+ * <p>By giving a PendingIntent to another application,
+ * you are granting it the right to perform the operation you have specified
+ * as if the other application was yourself (with the same permissions and
+ * identity). As such, you should be careful about how you build the PendingIntent:
+ * often, for example, the base Intent you supply will have the component
+ * name explicitly set to one of your own components, to ensure it is ultimately
+ * sent there and nowhere else.
+ *
+ * <p>A PendingIntent itself is simply a reference to a token maintained by
+ * the system describing the original data used to retrieve it. This means
+ * that, even if its owning application's process is killed, the
+ * PendingIntent itself will remain usable from other processes that
+ * have been given it. If the creating application later re-retrieves the
+ * same kind of PendingIntent (same operation, same Intent action, data,
+ * categories, and components, and same flags), it will receive a PendingIntent
+ * representing the same token if that is still valid, and can thus call
+ * {@link #cancel} to remove it.
+ */
+public final class PendingIntent implements Parcelable {
+ private final IIntentSender mTarget;
+
+ /**
+ * Flag for use with {@link #getActivity}, {@link #getBroadcast}, and
+ * {@link #getService}: this
+ * PendingIntent can only be used once. If set, after
+ * {@link #send()} is called on it, it will be automatically
+ * canceled for you and any future attempt to send through it will fail.
+ */
+ public static final int FLAG_ONE_SHOT = 1<<30;
+ /**
+ * Flag for use with {@link #getActivity}, {@link #getBroadcast}, and
+ * {@link #getService}: if the described PendingIntent does not already
+ * exist, then simply return null instead of creating it.
+ */
+ public static final int FLAG_NO_CREATE = 1<<29;
+ /**
+ * Flag for use with {@link #getActivity}, {@link #getBroadcast}, and
+ * {@link #getService}: if the described PendingIntent already exists,
+ * the current one is canceled before generating a new one. You can use
+ * this to retrieve a new PendingIntent when you are only changing the
+ * extra data in the Intent.
+ */
+ public static final int FLAG_CANCEL_CURRENT = 1<<28;
+
+ /**
+ * Exception thrown when trying to send through a PendingIntent that
+ * has been canceled or is otherwise no longer able to execute the request.
+ */
+ public static class CanceledException extends AndroidException {
+ public CanceledException() {
+ }
+
+ public CanceledException(String name) {
+ super(name);
+ }
+
+ public CanceledException(Exception cause) {
+ super(cause);
+ }
+ };
+
+ /**
+ * Callback interface for discovering when a send operation has
+ * completed. Primarily for use with a PendingIntent that is
+ * performing a broadcast, this provides the same information as
+ * calling {@link Context#sendOrderedBroadcast(Intent, String,
+ * android.content.BroadcastReceiver, Handler, int, String, Bundle)
+ * Context.sendBroadcast()} with a final BroadcastReceiver.
+ */
+ public interface OnFinished {
+ /**
+ * Called when a send operation as completed.
+ *
+ * @param pendingIntent The PendingIntent this operation was sent through.
+ * @param intent The original Intent that was sent.
+ * @param resultCode The final result code determined by the send.
+ * @param resultData The final data collected by a broadcast.
+ * @param resultExtras The final extras collected by a broadcast.
+ */
+ void onSendFinished(PendingIntent pendingIntent, Intent intent,
+ int resultCode, String resultData, Bundle resultExtras);
+ }
+
+ private static class FinishedDispatcher extends IIntentReceiver.Stub
+ implements Runnable {
+ private final PendingIntent mPendingIntent;
+ private final OnFinished mWho;
+ private final Handler mHandler;
+ private Intent mIntent;
+ private int mResultCode;
+ private String mResultData;
+ private Bundle mResultExtras;
+ FinishedDispatcher(PendingIntent pi, OnFinished who, Handler handler) {
+ mPendingIntent = pi;
+ mWho = who;
+ mHandler = handler;
+ }
+ public void performReceive(Intent intent, int resultCode,
+ String data, Bundle extras, boolean serialized) {
+ mIntent = intent;
+ mResultCode = resultCode;
+ mResultData = data;
+ mResultExtras = extras;
+ if (mHandler == null) {
+ run();
+ } else {
+ mHandler.post(this);
+ }
+ }
+ public void run() {
+ mWho.onSendFinished(mPendingIntent, mIntent, mResultCode,
+ mResultData, mResultExtras);
+ }
+ }
+
+ /**
+ * Retrieve a PendingIntent that will start a new activity, like calling
+ * {@link Context#startActivity(Intent) Context.startActivity(Intent)}.
+ * Note that the activity will be started outside of the context of an
+ * existing activity, so you must use the {@link Intent#FLAG_ACTIVITY_NEW_TASK
+ * Intent.FLAG_ACTIVITY_NEW_TASK} launch flag in the Intent.
+ *
+ * @param context The Context in which this PendingIntent should start
+ * the activity.
+ * @param requestCode Private request code for the sender (currently
+ * not used).
+ * @param intent Intent of the activity to be launched.
+ * @param flags May be {@link #FLAG_ONE_SHOT}, {@link #FLAG_NO_CREATE},
+ * {@link #FLAG_CANCEL_CURRENT}, or any of the flags as supported by
+ * {@link Intent#fillIn Intent.fillIn()} to control which unspecified parts
+ * of the intent that can be supplied when the actual send happens.
+ *
+ * @return Returns an existing or new PendingIntent matching the given
+ * parameters. May return null only if {@link #FLAG_NO_CREATE} has been
+ * supplied.
+ */
+ public static PendingIntent getActivity(Context context, int requestCode,
+ Intent intent, int flags) {
+ String packageName = context.getPackageName();
+ String resolvedType = intent != null ? intent.resolveTypeIfNeeded(
+ context.getContentResolver()) : null;
+ try {
+ IIntentSender target =
+ ActivityManagerNative.getDefault().getIntentSender(
+ IActivityManager.INTENT_SENDER_ACTIVITY, packageName,
+ null, null, requestCode, intent, resolvedType, flags);
+ return target != null ? new PendingIntent(target) : null;
+ } catch (RemoteException e) {
+ }
+ return null;
+ }
+
+ /**
+ * Retrieve a PendingIntent that will perform a broadcast, like calling
+ * {@link Context#sendBroadcast(Intent) Context.sendBroadcast()}.
+ *
+ * @param context The Context in which this PendingIntent should perform
+ * the broadcast.
+ * @param requestCode Private request code for the sender (currently
+ * not used).
+ * @param intent The Intent to be broadcast.
+ * @param flags May be {@link #FLAG_ONE_SHOT}, {@link #FLAG_NO_CREATE},
+ * {@link #FLAG_CANCEL_CURRENT}, or any of the flags as supported by
+ * {@link Intent#fillIn Intent.fillIn()} to control which unspecified parts
+ * of the intent that can be supplied when the actual send happens.
+ *
+ * @return Returns an existing or new PendingIntent matching the given
+ * parameters. May return null only if {@link #FLAG_NO_CREATE} has been
+ * supplied.
+ */
+ public static PendingIntent getBroadcast(Context context, int requestCode,
+ Intent intent, int flags) {
+ String packageName = context.getPackageName();
+ String resolvedType = intent != null ? intent.resolveTypeIfNeeded(
+ context.getContentResolver()) : null;
+ try {
+ IIntentSender target =
+ ActivityManagerNative.getDefault().getIntentSender(
+ IActivityManager.INTENT_SENDER_BROADCAST, packageName,
+ null, null, requestCode, intent, resolvedType, flags);
+ return target != null ? new PendingIntent(target) : null;
+ } catch (RemoteException e) {
+ }
+ return null;
+ }
+
+ /**
+ * Retrieve a PendingIntent that will start a service, like calling
+ * {@link Context#startService Context.startService()}. The start
+ * arguments given to the service will come from the extras of the Intent.
+ *
+ * @param context The Context in which this PendingIntent should start
+ * the service.
+ * @param requestCode Private request code for the sender (currently
+ * not used).
+ * @param intent An Intent describing the service to be started.
+ * @param flags May be {@link #FLAG_ONE_SHOT}, {@link #FLAG_NO_CREATE},
+ * {@link #FLAG_CANCEL_CURRENT}, or any of the flags as supported by
+ * {@link Intent#fillIn Intent.fillIn()} to control which unspecified parts
+ * of the intent that can be supplied when the actual send happens.
+ *
+ * @return Returns an existing or new PendingIntent matching the given
+ * parameters. May return null only if {@link #FLAG_NO_CREATE} has been
+ * supplied.
+ */
+ public static PendingIntent getService(Context context, int requestCode,
+ Intent intent, int flags) {
+ String packageName = context.getPackageName();
+ String resolvedType = intent != null ? intent.resolveTypeIfNeeded(
+ context.getContentResolver()) : null;
+ try {
+ IIntentSender target =
+ ActivityManagerNative.getDefault().getIntentSender(
+ IActivityManager.INTENT_SENDER_SERVICE, packageName,
+ null, null, requestCode, intent, resolvedType, flags);
+ return target != null ? new PendingIntent(target) : null;
+ } catch (RemoteException e) {
+ }
+ return null;
+ }
+
+ /**
+ * Cancel a currently active PendingIntent. Only the original application
+ * owning an PendingIntent can cancel it.
+ */
+ public void cancel() {
+ try {
+ ActivityManagerNative.getDefault().cancelIntentSender(mTarget);
+ } catch (RemoteException e) {
+ }
+ }
+
+ /**
+ * Perform the operation associated with this PendingIntent.
+ *
+ * @see #send(Context, int, Intent, android.app.PendingIntent.OnFinished, Handler)
+ *
+ * @throws CanceledException Throws CanceledException if the PendingIntent
+ * is no longer allowing more intents to be sent through it.
+ */
+ public void send() throws CanceledException {
+ send(null, 0, null, null, null);
+ }
+
+ /**
+ * Perform the operation associated with this PendingIntent.
+ *
+ * @param code Result code to supply back to the PendingIntent's target.
+ *
+ * @see #send(Context, int, Intent, android.app.PendingIntent.OnFinished, Handler)
+ *
+ * @throws CanceledException Throws CanceledException if the PendingIntent
+ * is no longer allowing more intents to be sent through it.
+ */
+ public void send(int code) throws CanceledException {
+ send(null, code, null, null, null);
+ }
+
+ /**
+ * Perform the operation associated with this PendingIntent, allowing the
+ * caller to specify information about the Intent to use.
+ *
+ * @param context The Context of the caller.
+ * @param code Result code to supply back to the PendingIntent's target.
+ * @param intent Additional Intent data. See {@link Intent#fillIn
+ * Intent.fillIn()} for information on how this is applied to the
+ * original Intent.
+ *
+ * @see #send(Context, int, Intent, android.app.PendingIntent.OnFinished, Handler)
+ *
+ * @throws CanceledException Throws CanceledException if the PendingIntent
+ * is no longer allowing more intents to be sent through it.
+ */
+ public void send(Context context, int code, Intent intent)
+ throws CanceledException {
+ send(context, code, intent, null, null);
+ }
+
+ /**
+ * Perform the operation associated with this PendingIntent, allowing the
+ * caller to be notified when the send has completed.
+ *
+ * @param code Result code to supply back to the PendingIntent's target.
+ * @param onFinished The object to call back on when the send has
+ * completed, or null for no callback.
+ * @param handler Handler identifying the thread on which the callback
+ * should happen. If null, the callback will happen from the thread
+ * pool of the process.
+ *
+ * @see #send(Context, int, Intent, android.app.PendingIntent.OnFinished, Handler)
+ *
+ * @throws CanceledException Throws CanceledException if the PendingIntent
+ * is no longer allowing more intents to be sent through it.
+ */
+ public void send(int code, OnFinished onFinished, Handler handler)
+ throws CanceledException {
+ send(null, code, null, onFinished, handler);
+ }
+
+ /**
+ * Perform the operation associated with this PendingIntent, allowing the
+ * caller to specify information about the Intent to use and be notified
+ * when the send has completed.
+ *
+ * <p>For the intent parameter, a PendingIntent
+ * often has restrictions on which fields can be supplied here, based on
+ * how the PendingIntent was retrieved in {@link #getActivity},
+ * {@link #getBroadcast}, or {@link #getService}.
+ *
+ * @param context The Context of the caller. This may be null if
+ * <var>intent</var> is also null.
+ * @param code Result code to supply back to the PendingIntent's target.
+ * @param intent Additional Intent data. See {@link Intent#fillIn
+ * Intent.fillIn()} for information on how this is applied to the
+ * original Intent. Use null to not modify the original Intent.
+ * @param onFinished The object to call back on when the send has
+ * completed, or null for no callback.
+ * @param handler Handler identifying the thread on which the callback
+ * should happen. If null, the callback will happen from the thread
+ * pool of the process.
+ *
+ * @see #send()
+ * @see #send(int)
+ * @see #send(Context, int, Intent)
+ * @see #send(int, android.app.PendingIntent.OnFinished, Handler)
+ *
+ * @throws CanceledException Throws CanceledException if the PendingIntent
+ * is no longer allowing more intents to be sent through it.
+ */
+ public void send(Context context, int code, Intent intent,
+ OnFinished onFinished, Handler handler) throws CanceledException {
+ try {
+ String resolvedType = intent != null ?
+ intent.resolveTypeIfNeeded(context.getContentResolver())
+ : null;
+ int res = mTarget.send(code, intent, resolvedType,
+ onFinished != null
+ ? new FinishedDispatcher(this, onFinished, handler)
+ : null);
+ if (res < 0) {
+ throw new CanceledException();
+ }
+ } catch (RemoteException e) {
+ throw new CanceledException(e);
+ }
+ }
+
+ /**
+ * Return the package name of the application that created this
+ * PendingIntent, that is the identity under which you will actually be
+ * sending the Intent. The returned string is supplied by the system, so
+ * that an application can not spoof its package.
+ *
+ * @return The package name of the PendingIntent, or null if there is
+ * none associated with it.
+ */
+ public String getTargetPackage() {
+ try {
+ return ActivityManagerNative.getDefault()
+ .getPackageForIntentSender(mTarget);
+ } catch (RemoteException e) {
+ // Should never happen.
+ return null;
+ }
+ }
+
+ /**
+ * Comparison operator on two PendingIntent objects, such that true
+ * is returned then they both represent the same operation from the
+ * same package. This allows you to use {@link #getActivity},
+ * {@link #getBroadcast}, or {@link #getService} multiple times (even
+ * across a process being killed), resulting in different PendingIntent
+ * objects but whose equals() method identifies them as being the same
+ * operation.
+ */
+ @Override
+ public boolean equals(Object otherObj) {
+ if (otherObj == null) {
+ return false;
+ }
+ try {
+ return mTarget.asBinder().equals(((PendingIntent)otherObj)
+ .mTarget.asBinder());
+ } catch (ClassCastException e) {
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return mTarget.asBinder().hashCode();
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeStrongBinder(mTarget.asBinder());
+ }
+
+ public static final Parcelable.Creator<PendingIntent> CREATOR
+ = new Parcelable.Creator<PendingIntent>() {
+ public PendingIntent createFromParcel(Parcel in) {
+ IBinder target = in.readStrongBinder();
+ return target != null ? new PendingIntent(target) : null;
+ }
+
+ public PendingIntent[] newArray(int size) {
+ return new PendingIntent[size];
+ }
+ };
+
+ /**
+ * Convenience function for writing either a PendingIntent or null pointer to
+ * a Parcel. You must use this with {@link #readPendingIntentOrNullFromParcel}
+ * for later reading it.
+ *
+ * @param sender The PendingIntent to write, or null.
+ * @param out Where to write the PendingIntent.
+ */
+ public static void writePendingIntentOrNullToParcel(PendingIntent sender,
+ Parcel out) {
+ out.writeStrongBinder(sender != null ? sender.mTarget.asBinder()
+ : null);
+ }
+
+ /**
+ * Convenience function for reading either a Messenger or null pointer from
+ * a Parcel. You must have previously written the Messenger with
+ * {@link #writePendingIntentOrNullToParcel}.
+ *
+ * @param in The Parcel containing the written Messenger.
+ *
+ * @return Returns the Messenger read from the Parcel, or null if null had
+ * been written.
+ */
+ public static PendingIntent readPendingIntentOrNullFromParcel(Parcel in) {
+ IBinder b = in.readStrongBinder();
+ return b != null ? new PendingIntent(b) : null;
+ }
+
+ /*package*/ PendingIntent(IIntentSender target) {
+ mTarget = target;
+ }
+
+ /*package*/ PendingIntent(IBinder target) {
+ mTarget = IIntentSender.Stub.asInterface(target);
+ }
+
+ /*package*/ IIntentSender getTarget() {
+ return mTarget;
+ }
+}
diff --git a/core/java/android/app/ProgressDialog.java b/core/java/android/app/ProgressDialog.java
new file mode 100644
index 0000000..8b60cfa
--- /dev/null
+++ b/core/java/android/app/ProgressDialog.java
@@ -0,0 +1,301 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import com.android.internal.R;
+
+import java.text.NumberFormat;
+
+/**
+ * <p>A dialog showing a progress indicator and an optional text message or view.
+ * Only a text message or a view can be used at the same time.</p>
+ * <p>The dialog can be made cancelable on back key press.</p>
+ * <p>The progress range is 0..10000.</p>
+ */
+public class ProgressDialog extends AlertDialog {
+
+ /** Creates a ProgressDialog with a ciruclar, spinning progress
+ * bar. This is the default.
+ */
+ public static final int STYLE_SPINNER = 0;
+
+ /** Creates a ProgressDialog with a horizontal progress bar.
+ */
+ public static final int STYLE_HORIZONTAL = 1;
+
+ private ProgressBar mProgress;
+ private TextView mMessageView;
+
+ private int mProgressStyle = STYLE_SPINNER;
+ private TextView mProgressNumber;
+ private TextView mProgressPercent;
+ private NumberFormat mProgressPercentFormat;
+
+ private int mMax;
+ private int mProgressVal;
+ private int mSecondaryProgressVal;
+ private int mIncrementBy;
+ private int mIncrementSecondaryBy;
+ private Drawable mProgressDrawable;
+ private Drawable mIndeterminateDrawable;
+ private CharSequence mMessage;
+ private boolean mIndeterminate;
+
+ private boolean mHasStarted;
+ private Handler mViewUpdateHandler;
+
+ public ProgressDialog(Context context) {
+ this(context, com.android.internal.R.style.Theme_Dialog_Alert);
+ }
+
+ public ProgressDialog(Context context, int theme) {
+ super(context, theme);
+ }
+
+ public static ProgressDialog show(Context context, CharSequence title,
+ CharSequence message) {
+ return show(context, title, message, false);
+ }
+
+ public static ProgressDialog show(Context context, CharSequence title,
+ CharSequence message, boolean indeterminate) {
+ return show(context, title, message, indeterminate, false, null);
+ }
+
+ public static ProgressDialog show(Context context, CharSequence title,
+ CharSequence message, boolean indeterminate, boolean cancelable) {
+ return show(context, title, message, indeterminate, cancelable, null);
+ }
+
+ public static ProgressDialog show(Context context, CharSequence title,
+ CharSequence message, boolean indeterminate,
+ boolean cancelable, OnCancelListener cancelListener) {
+ ProgressDialog dialog = new ProgressDialog(context);
+ dialog.setTitle(title);
+ dialog.setMessage(message);
+ dialog.setIndeterminate(indeterminate);
+ dialog.setCancelable(cancelable);
+ dialog.setOnCancelListener(cancelListener);
+ dialog.show();
+ return dialog;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ LayoutInflater inflater = LayoutInflater.from(mContext);
+ if (mProgressStyle == STYLE_HORIZONTAL) {
+
+ /* Use a separate handler to update the text views as they
+ * must be updated on the same thread that created them.
+ */
+ mViewUpdateHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ super.handleMessage(msg);
+
+ /* Update the number and percent */
+ int progress = mProgress.getProgress();
+ int max = mProgress.getMax();
+ double percent = (double) progress / (double) max;
+ mProgressNumber.setText(progress + "/" + max);
+ mProgressPercent.setText(mProgressPercentFormat.format(percent));
+ }
+ };
+ View view = inflater.inflate(R.layout.alert_dialog_progress, null);
+ mProgress = (ProgressBar) view.findViewById(R.id.progress);
+ mProgressNumber = (TextView) view.findViewById(R.id.progress_number);
+ mProgressPercent = (TextView) view.findViewById(R.id.progress_percent);
+ mProgressPercentFormat = NumberFormat.getPercentInstance();
+ mProgressPercentFormat.setMaximumFractionDigits(0);
+ setView(view);
+ } else {
+ View view = inflater.inflate(R.layout.progress_dialog, null);
+ mProgress = (ProgressBar) view.findViewById(R.id.progress);
+ mMessageView = (TextView) view.findViewById(R.id.message);
+ setView(view);
+ }
+ if (mMax > 0) {
+ setMax(mMax);
+ }
+ if (mProgressVal > 0) {
+ setProgress(mProgressVal);
+ }
+ if (mSecondaryProgressVal > 0) {
+ setSecondaryProgress(mSecondaryProgressVal);
+ }
+ if (mIncrementBy > 0) {
+ incrementProgressBy(mIncrementBy);
+ }
+ if (mIncrementSecondaryBy > 0) {
+ incrementSecondaryProgressBy(mIncrementSecondaryBy);
+ }
+ if (mProgressDrawable != null) {
+ setProgressDrawable(mProgressDrawable);
+ }
+ if (mIndeterminateDrawable != null) {
+ setIndeterminateDrawable(mIndeterminateDrawable);
+ }
+ if (mMessage != null) {
+ setMessage(mMessage);
+ }
+ setIndeterminate(mIndeterminate);
+ onProgressChanged();
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ mHasStarted = true;
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mHasStarted = false;
+ }
+
+ public void setProgress(int value) {
+ if (mHasStarted) {
+ mProgress.setProgress(value);
+ onProgressChanged();
+ } else {
+ mProgressVal = value;
+ }
+ }
+
+ public void setSecondaryProgress(int secondaryProgress) {
+ if (mProgress != null) {
+ mProgress.setSecondaryProgress(secondaryProgress);
+ onProgressChanged();
+ } else {
+ mSecondaryProgressVal = secondaryProgress;
+ }
+ }
+
+ public int getProgress() {
+ if (mProgress != null) {
+ return mProgress.getProgress();
+ }
+ return mProgressVal;
+ }
+
+ public int getSecondaryProgress() {
+ if (mProgress != null) {
+ return mProgress.getSecondaryProgress();
+ }
+ return mSecondaryProgressVal;
+ }
+
+ public int getMax() {
+ if (mProgress != null) {
+ return mProgress.getMax();
+ }
+ return mMax;
+ }
+
+ public void setMax(int max) {
+ if (mProgress != null) {
+ mProgress.setMax(max);
+ onProgressChanged();
+ } else {
+ mMax = max;
+ }
+ }
+
+ public void incrementProgressBy(int diff) {
+ if (mProgress != null) {
+ mProgress.incrementProgressBy(diff);
+ onProgressChanged();
+ } else {
+ mIncrementBy += diff;
+ }
+ }
+
+ public void incrementSecondaryProgressBy(int diff) {
+ if (mProgress != null) {
+ mProgress.incrementSecondaryProgressBy(diff);
+ onProgressChanged();
+ } else {
+ mIncrementSecondaryBy += diff;
+ }
+ }
+
+ public void setProgressDrawable(Drawable d) {
+ if (mProgress != null) {
+ mProgress.setProgressDrawable(d);
+ } else {
+ mProgressDrawable = d;
+ }
+ }
+
+ public void setIndeterminateDrawable(Drawable d) {
+ if (mProgress != null) {
+ mProgress.setIndeterminateDrawable(d);
+ } else {
+ mIndeterminateDrawable = d;
+ }
+ }
+
+ public void setIndeterminate(boolean indeterminate) {
+ if (mHasStarted && (isIndeterminate() != indeterminate)) {
+ mProgress.setIndeterminate(indeterminate);
+ } else {
+ mIndeterminate = indeterminate;
+ }
+ }
+
+ public boolean isIndeterminate() {
+ if (mProgress != null) {
+ return mProgress.isIndeterminate();
+ }
+ return mIndeterminate;
+ }
+
+ @Override
+ public void setMessage(CharSequence message) {
+ if (mProgress != null) {
+ if (mProgressStyle == STYLE_HORIZONTAL) {
+ super.setMessage(message);
+ } else {
+ mMessageView.setText(message);
+ }
+ } else {
+ mMessage = message;
+ }
+ }
+
+ public void setProgressStyle(int style) {
+ mProgressStyle = style;
+ }
+
+ private void onProgressChanged() {
+ if (mProgressStyle == STYLE_HORIZONTAL) {
+ mViewUpdateHandler.sendEmptyMessage(0);
+ }
+ }
+}
diff --git a/core/java/android/app/ResultInfo.java b/core/java/android/app/ResultInfo.java
new file mode 100644
index 0000000..48a0fc2
--- /dev/null
+++ b/core/java/android/app/ResultInfo.java
@@ -0,0 +1,86 @@
+/*
+ * 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.app;
+
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.Bundle;
+
+import java.util.Map;
+
+/**
+ * {@hide}
+ */
+public class ResultInfo implements Parcelable {
+ public final String mResultWho;
+ public final int mRequestCode;
+ public final int mResultCode;
+ public final Intent mData;
+
+ public ResultInfo(String resultWho, int requestCode, int resultCode,
+ Intent data) {
+ mResultWho = resultWho;
+ mRequestCode = requestCode;
+ mResultCode = resultCode;
+ mData = data;
+ }
+
+ public String toString() {
+ return "ResultInfo{who=" + mResultWho + ", request=" + mRequestCode
+ + ", result=" + mResultCode + ", data=" + mData + "}";
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeString(mResultWho);
+ out.writeInt(mRequestCode);
+ out.writeInt(mResultCode);
+ if (mData != null) {
+ out.writeInt(1);
+ mData.writeToParcel(out, 0);
+ } else {
+ out.writeInt(0);
+ }
+ }
+
+ public static final Parcelable.Creator<ResultInfo> CREATOR
+ = new Parcelable.Creator<ResultInfo>() {
+ public ResultInfo createFromParcel(Parcel in) {
+ return new ResultInfo(in);
+ }
+
+ public ResultInfo[] newArray(int size) {
+ return new ResultInfo[size];
+ }
+ };
+
+ public ResultInfo(Parcel in) {
+ mResultWho = in.readString();
+ mRequestCode = in.readInt();
+ mResultCode = in.readInt();
+ if (in.readInt() != 0) {
+ mData = Intent.CREATOR.createFromParcel(in);
+ } else {
+ mData = null;
+ }
+ }
+}
diff --git a/core/java/android/app/SearchDialog.java b/core/java/android/app/SearchDialog.java
new file mode 100644
index 0000000..5f3f9ef
--- /dev/null
+++ b/core/java/android/app/SearchDialog.java
@@ -0,0 +1,1606 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.content.res.Resources.NotFoundException;
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemClock;
+import android.server.search.SearchableInfo;
+import android.text.Editable;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.View.OnFocusChangeListener;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.widget.AdapterView;
+import android.widget.Button;
+import android.widget.CursorAdapter;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.SimpleCursorAdapter;
+import android.widget.TextView;
+import android.widget.WrapperListAdapter;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.AdapterView.OnItemSelectedListener;
+
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * In-application-process implementation of Search Bar. This is still controlled by the
+ * SearchManager, but it runs in the current activity's process to keep things lighter weight.
+ *
+ * @hide
+ */
+public class SearchDialog extends Dialog {
+
+ // Debugging support
+ final static String LOG_TAG = "SearchDialog";
+ private static final int DBG_LOG_TIMING = 0;
+ final static int DBG_JAM_THREADING = 0;
+
+ // interaction with runtime
+ IntentFilter mCloseDialogsFilter;
+ IntentFilter mPackageFilter;
+ private final Handler mHandler = new Handler(); // why isn't Dialog.mHandler shared?
+
+ private static final String INSTANCE_KEY_COMPONENT = "comp";
+ private static final String INSTANCE_KEY_APPDATA = "data";
+ private static final String INSTANCE_KEY_GLOBALSEARCH = "glob";
+ private static final String INSTANCE_KEY_DISPLAY_QUERY = "dQry";
+ private static final String INSTANCE_KEY_DISPLAY_SEL_START = "sel1";
+ private static final String INSTANCE_KEY_DISPLAY_SEL_END = "sel2";
+ private static final String INSTANCE_KEY_USER_QUERY = "uQry";
+ private static final String INSTANCE_KEY_SUGGESTION_QUERY = "sQry";
+ private static final String INSTANCE_KEY_SELECTED_ELEMENT = "slEl";
+ private static final int INSTANCE_SELECTED_BUTTON = -2;
+ private static final int INSTANCE_SELECTED_QUERY = -1;
+
+ // views & widgets
+ private View mSearchBarLayout;
+ private TextView mBadgeLabel;
+ private LinearLayout mSearchEditLayout;
+ private EditText mSearchTextField;
+ private Button mGoButton;
+ private ListView mSuggestionsList;
+
+ private ViewTreeObserver mViewTreeObserver = null;
+
+ // interaction with searchable application
+ private ComponentName mLaunchComponent;
+ private Bundle mAppSearchData;
+ private boolean mGlobalSearchMode;
+ private Context mActivityContext;
+
+ // interaction with the search manager service
+ private SearchableInfo mSearchable;
+
+ // support for suggestions
+ private SuggestionsRunner mSuggestionsRunner;
+ private String mUserQuery = null;
+ private int mUserQuerySelStart;
+ private int mUserQuerySelEnd;
+ private boolean mNonUserQuery = false;
+ private boolean mLeaveJammedQueryOnRefocus = false;
+ private String mPreviousSuggestionQuery = null;
+ private Context mProviderContext;
+ private Animation mSuggestionsEntry;
+ private Animation mSuggestionsExit;
+ private boolean mSkipNextAnimate;
+ private int mPresetSelection = -1;
+ private String mSuggestionAction = null;
+ private Uri mSuggestionData = null;
+ private String mSuggestionQuery = null;
+
+ /**
+ * Constructor - fires it up and makes it look like the search UI.
+ *
+ * @param context Application Context we can use for system acess
+ */
+ public SearchDialog(Context context) {
+ super(context, com.android.internal.R.style.Theme_Translucent);
+ }
+
+ /**
+ * We create the search dialog just once, and it stays around (hidden)
+ * until activated by the user.
+ */
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Window theWindow = getWindow();
+ theWindow.requestFeature(Window.FEATURE_NO_TITLE);
+ theWindow.setFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND,
+ WindowManager.LayoutParams.FLAG_DIM_BEHIND);
+ theWindow.setGravity(Gravity.TOP|Gravity.FILL_HORIZONTAL);
+
+ setContentView(com.android.internal.R.layout.search_bar);
+
+ // Note: theWindow.setBackgroundDrawable(null) does not work here - you get blackness
+ theWindow.setBackgroundDrawableResource(android.R.color.transparent);
+
+ theWindow.setLayout(ViewGroup.LayoutParams.FILL_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+ WindowManager.LayoutParams lp = theWindow.getAttributes();
+ lp.dimAmount = 0.5f;
+ lp.setTitle("Search Dialog");
+ theWindow.setAttributes(lp);
+
+ // get the view elements for local access
+ mSearchBarLayout = findViewById(com.android.internal.R.id.search_bar);
+ mBadgeLabel = (TextView) findViewById(com.android.internal.R.id.search_badge);
+ mSearchEditLayout = (LinearLayout)findViewById(com.android.internal.R.id.search_edit_frame);
+ mSearchTextField = (EditText) findViewById(com.android.internal.R.id.search_src_text);
+ mGoButton = (Button) findViewById(com.android.internal.R.id.search_go_btn);
+ mSuggestionsList = (ListView) findViewById(com.android.internal.R.id.search_suggest_list);
+
+ // attach listeners
+ mSearchTextField.addTextChangedListener(mTextWatcher);
+ mSearchTextField.setOnKeyListener(mTextKeyListener);
+ mGoButton.setOnClickListener(mGoButtonClickListener);
+ mGoButton.setOnKeyListener(mGoButtonKeyListener);
+ mSuggestionsList.setOnItemClickListener(mSuggestionsListItemClickListener);
+ mSuggestionsList.setOnKeyListener(mSuggestionsKeyListener);
+ mSuggestionsList.setOnFocusChangeListener(mSuggestFocusListener);
+ mSuggestionsList.setOnItemSelectedListener(mSuggestSelectedListener);
+
+ // pre-hide all the extraneous elements
+ mBadgeLabel.setVisibility(View.GONE);
+ mSuggestionsList.setVisibility(View.GONE);
+
+ // Additional adjustments to make Dialog work for Search
+
+ // Touching outside of the search dialog will dismiss it
+ setCanceledOnTouchOutside(true);
+
+ // Preload animations
+ mSuggestionsEntry = AnimationUtils.loadAnimation(getContext(),
+ com.android.internal.R.anim.grow_fade_in);
+ mSuggestionsExit = AnimationUtils.loadAnimation(getContext(),
+ com.android.internal.R.anim.fade_out);
+
+ // Set up broadcast filters
+ mCloseDialogsFilter = new
+ IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
+ mPackageFilter = new IntentFilter();
+ mPackageFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
+ mPackageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+ mPackageFilter.addAction(Intent.ACTION_PACKAGE_CHANGED);
+ mPackageFilter.addDataScheme("package");
+ }
+
+ /**
+ * Set up the search dialog
+ *
+ * @param Returns true if search dialog launched, false if not
+ */
+ public boolean show(String initialQuery, boolean selectInitialQuery,
+ ComponentName componentName, Bundle appSearchData, boolean globalSearch) {
+ if (isShowing()) {
+ // race condition - already showing but not handling events yet.
+ // in this case, just discard the "show" request
+ return true;
+ }
+
+ // Get searchable info from search manager and use to set up other elements of UI
+ // Do this first so we can get out quickly if there's nothing to search
+ ISearchManager sms;
+ sms = ISearchManager.Stub.asInterface(ServiceManager.getService(Context.SEARCH_SERVICE));
+ try {
+ mSearchable = sms.getSearchableInfo(componentName, globalSearch);
+ } catch (RemoteException e) {
+ mSearchable = null;
+ }
+ if (mSearchable == null) {
+ // unfortunately, we can't log here. it would be logspam every time the user
+ // clicks the "search" key on a non-search app
+ return false;
+ }
+
+ // OK, we're going to show ourselves
+ if (mSuggestionsList != null) {
+ mSuggestionsList.setVisibility(View.GONE); // prevent any flicker if was visible
+ }
+ super.show();
+
+ setupSearchableInfo();
+
+ // start the suggestions thread (which will mainly idle)
+ mSuggestionsRunner = new SuggestionsRunner();
+ new Thread(mSuggestionsRunner, "SearchSuggestions").start();
+
+ mLaunchComponent = componentName;
+ mAppSearchData = appSearchData;
+ mGlobalSearchMode = globalSearch;
+
+ // receive broadcasts
+ getContext().registerReceiver(mBroadcastReceiver, mCloseDialogsFilter);
+ getContext().registerReceiver(mBroadcastReceiver, mPackageFilter);
+
+ mViewTreeObserver = mSearchBarLayout.getViewTreeObserver();
+ mViewTreeObserver.addOnGlobalLayoutListener(mGlobalLayoutListener);
+
+ // finally, load the user's initial text (which may trigger suggestions)
+ mNonUserQuery = false;
+ if (initialQuery == null) {
+ initialQuery = ""; // This forces the preload to happen, triggering suggestions
+ }
+ mSearchTextField.setText(initialQuery);
+ if (selectInitialQuery) {
+ mSearchTextField.selectAll();
+ } else {
+ mSearchTextField.setSelection(initialQuery.length());
+ }
+ return true;
+ }
+
+ /**
+ * The default show() for this Dialog is not supported.
+ */
+ @Override
+ public void show() {
+ return;
+ }
+
+ /**
+ * Dismiss the search dialog.
+ *
+ * This function is designed to be idempotent so it can be safely called at any time
+ * (even if already closed) and more likely to really dump any memory. No leaks!
+ */
+ @Override
+ public void dismiss() {
+ if (isShowing()) {
+ super.dismiss();
+ }
+ setOnCancelListener(null);
+ setOnDismissListener(null);
+
+ // stop receiving broadcasts (throws exception if none registered)
+ try {
+ getContext().unregisterReceiver(mBroadcastReceiver);
+ } catch (RuntimeException e) {
+ // This is OK - it just means we didn't have any registered
+ }
+
+ // ignore layout notifications
+ try {
+ if (mViewTreeObserver != null) {
+ mViewTreeObserver.removeGlobalOnLayoutListener(mGlobalLayoutListener);
+ }
+ } catch (RuntimeException e) {
+ // This is OK - none registered or observer "dead"
+ }
+ mViewTreeObserver = null;
+
+ // dump extra memory we're hanging on to
+ if (mSuggestionsRunner != null) {
+ mSuggestionsRunner.cancelSuggestions();
+ mSuggestionsRunner = null;
+ }
+ mLaunchComponent = null;
+ mAppSearchData = null;
+ mSearchable = null;
+ mSuggestionAction = null;
+ mSuggestionData = null;
+ mSuggestionQuery = null;
+ mActivityContext = null;
+ mProviderContext = null;
+ mPreviousSuggestionQuery = null;
+ mUserQuery = null;
+ }
+
+ /**
+ * Save the minimal set of data necessary to recreate the search
+ *
+ * @return A bundle with the state of the dialog.
+ */
+ @Override
+ public Bundle onSaveInstanceState() {
+ Bundle bundle = new Bundle();
+
+ // setup info so I can recreate this particular search
+ bundle.putParcelable(INSTANCE_KEY_COMPONENT, mLaunchComponent);
+ bundle.putBundle(INSTANCE_KEY_APPDATA, mAppSearchData);
+ bundle.putBoolean(INSTANCE_KEY_GLOBALSEARCH, mGlobalSearchMode);
+
+ // UI state
+ bundle.putString(INSTANCE_KEY_DISPLAY_QUERY, mSearchTextField.getText().toString());
+ bundle.putInt(INSTANCE_KEY_DISPLAY_SEL_START, mSearchTextField.getSelectionStart());
+ bundle.putInt(INSTANCE_KEY_DISPLAY_SEL_END, mSearchTextField.getSelectionEnd());
+ bundle.putString(INSTANCE_KEY_USER_QUERY, mUserQuery);
+ bundle.putString(INSTANCE_KEY_SUGGESTION_QUERY, mPreviousSuggestionQuery);
+
+ int selectedElement = INSTANCE_SELECTED_QUERY;
+ if (mGoButton.isFocused()) {
+ selectedElement = INSTANCE_SELECTED_BUTTON;
+ } else if ((mSuggestionsList.getVisibility() == View.VISIBLE) &&
+ mSuggestionsList.isFocused()) {
+ selectedElement = mSuggestionsList.getSelectedItemPosition(); // 0..n
+ }
+ bundle.putInt(INSTANCE_KEY_SELECTED_ELEMENT, selectedElement);
+
+ return bundle;
+ }
+
+ /**
+ * Restore the state of the dialog from a previously saved bundle.
+ *
+ * @param savedInstanceState The state of the dialog previously saved by
+ * {@link #onSaveInstanceState()}.
+ */
+ @Override
+ public void onRestoreInstanceState(Bundle savedInstanceState) {
+ // Get the launch info
+ ComponentName launchComponent = savedInstanceState.getParcelable(INSTANCE_KEY_COMPONENT);
+ Bundle appSearchData = savedInstanceState.getBundle(INSTANCE_KEY_APPDATA);
+ boolean globalSearch = savedInstanceState.getBoolean(INSTANCE_KEY_GLOBALSEARCH);
+
+ // get the UI state
+ String displayQuery = savedInstanceState.getString(INSTANCE_KEY_DISPLAY_QUERY);
+ int querySelStart = savedInstanceState.getInt(INSTANCE_KEY_DISPLAY_SEL_START, -1);
+ int querySelEnd = savedInstanceState.getInt(INSTANCE_KEY_DISPLAY_SEL_END, -1);
+ String userQuery = savedInstanceState.getString(INSTANCE_KEY_USER_QUERY);
+ int selectedElement = savedInstanceState.getInt(INSTANCE_KEY_SELECTED_ELEMENT);
+ String suggestionQuery = savedInstanceState.getString(INSTANCE_KEY_SUGGESTION_QUERY);
+
+ // show the dialog. skip any show/hide animation, we want to go fast.
+ // send the text that actually generates the suggestions here; we'll replace the display
+ // text as necessary in a moment.
+ if (!show(suggestionQuery, false, launchComponent, appSearchData, globalSearch)) {
+ // for some reason, we couldn't re-instantiate
+ return;
+ }
+ mSkipNextAnimate = true;
+
+ mNonUserQuery = true;
+ mSearchTextField.setText(displayQuery);
+ mNonUserQuery = false;
+
+ // clean up the selection state
+ switch (selectedElement) {
+ case INSTANCE_SELECTED_BUTTON:
+ mGoButton.setEnabled(true);
+ mGoButton.setFocusable(true);
+ mGoButton.requestFocus();
+ break;
+ case INSTANCE_SELECTED_QUERY:
+ if (querySelStart >= 0 && querySelEnd >= 0) {
+ mSearchTextField.requestFocus();
+ mSearchTextField.setSelection(querySelStart, querySelEnd);
+ }
+ break;
+ default:
+ // defer selecting a list element until suggestion list appears
+ mPresetSelection = selectedElement;
+ break;
+ }
+ }
+
+ /**
+ * Hook for updating layout on a rotation
+ *
+ */
+ public void onConfigurationChanged(Configuration newConfig) {
+ if (isShowing()) {
+ // Redraw (resources may have changed)
+ updateSearchButton();
+ updateSearchBadge();
+ updateQueryHint();
+ }
+ }
+
+ /**
+ * Use SearchableInfo record (from search manager service) to preconfigure the UI in various
+ * ways.
+ */
+ private void setupSearchableInfo() {
+ if (mSearchable != null) {
+ mActivityContext = mSearchable.getActivityContext(getContext());
+ mProviderContext = mSearchable.getProviderContext(getContext(), mActivityContext);
+
+ updateSearchButton();
+ updateSearchBadge();
+ updateQueryHint();
+ }
+ }
+
+ /**
+ * The list of installed packages has just changed. This means that our current context
+ * may no longer be valid. This would only happen if a package is installed/removed exactly
+ * when the search bar is open. So for now we're just going to close the search
+ * bar.
+ *
+ * Anything fancier would require some checks to see if the user's context was still valid.
+ * Which would be messier.
+ */
+ public void onPackageListChange() {
+ cancel();
+ }
+
+ /**
+ * Update the text in the search button
+ */
+ private void updateSearchButton() {
+ int textId = mSearchable.getSearchButtonText();
+ if (textId == 0) {
+ textId = com.android.internal.R.string.search_go;
+ }
+ String goText = mActivityContext.getResources().getString(textId);
+ mGoButton.setText(goText);
+ }
+
+ /**
+ * Setup the search "Badge" if request by mode flags.
+ */
+ private void updateSearchBadge() {
+ // assume both hidden
+ int visibility = View.GONE;
+ Drawable icon = null;
+ String text = null;
+
+ // optionally show one or the other.
+ if (mSearchable.mBadgeIcon) {
+ icon = mActivityContext.getResources().getDrawable(mSearchable.getIconId());
+ visibility = View.VISIBLE;
+ } else if (mSearchable.mBadgeLabel) {
+ text = mActivityContext.getResources().getText(mSearchable.getLabelId()).toString();
+ visibility = View.VISIBLE;
+ }
+
+ mBadgeLabel.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null);
+ mBadgeLabel.setText(text);
+ mBadgeLabel.setVisibility(visibility);
+ }
+
+ /**
+ * Update the hint in the query text field.
+ */
+ private void updateQueryHint() {
+ if (isShowing()) {
+ String hint = null;
+ if (mSearchable != null) {
+ int hintId = mSearchable.getHintId();
+ if (hintId != 0) {
+ hint = mActivityContext.getString(hintId);
+ }
+ }
+ mSearchTextField.setHint(hint);
+ }
+ }
+
+ /**
+ * Listeners of various types
+ */
+
+ /**
+ * Dialog's OnKeyListener implements various search-specific functionality
+ *
+ * @param keyCode This is the keycode of the typed key, and is the same value as
+ * found in the KeyEvent parameter.
+ * @param event The complete event record for the typed key
+ *
+ * @return Return true if the event was handled here, or false if not.
+ */
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_BACK:
+ cancel();
+ return true;
+ case KeyEvent.KEYCODE_SEARCH:
+ if (TextUtils.getTrimmedLength(mSearchTextField.getText()) != 0) {
+ launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null);
+ } else {
+ cancel();
+ }
+ return true;
+ default:
+ SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
+ if ((actionKey != null) && (actionKey.mQueryActionMsg != null)) {
+ launchQuerySearch(keyCode, actionKey.mQueryActionMsg);
+ return true;
+ }
+ break;
+ }
+ return false;
+ }
+
+ /**
+ * Callback to watch the textedit field for empty/non-empty
+ */
+ private TextWatcher mTextWatcher = new TextWatcher() {
+
+ public void beforeTextChanged(CharSequence s, int start, int
+ before, int after) { }
+
+ public void onTextChanged(CharSequence s, int start,
+ int before, int after) {
+ if (DBG_LOG_TIMING == 1) {
+ dbgLogTiming("onTextChanged()");
+ }
+ updateWidgetState();
+ // Only do suggestions if actually typed by user
+ if (!mNonUserQuery) {
+ updateSuggestions();
+ mUserQuery = mSearchTextField.getText().toString();
+ mUserQuerySelStart = mSearchTextField.getSelectionStart();
+ mUserQuerySelEnd = mSearchTextField.getSelectionEnd();
+ }
+ }
+
+ public void afterTextChanged(Editable s) { }
+ };
+
+ /**
+ * Enable/Disable the cancel button based on edit text state (any text?)
+ */
+ private void updateWidgetState() {
+ // enable the button if we have one or more non-space characters
+ boolean enabled =
+ TextUtils.getTrimmedLength(mSearchTextField.getText()) != 0;
+
+ mGoButton.setEnabled(enabled);
+ mGoButton.setFocusable(enabled);
+ }
+
+ /**
+ * In response to a change in the query text, update the suggestions
+ */
+ private void updateSuggestions() {
+ final String queryText = mSearchTextField.getText().toString();
+ mPreviousSuggestionQuery = queryText;
+ if (DBG_LOG_TIMING == 1) {
+ dbgLogTiming("updateSuggestions()");
+ }
+
+ mSuggestionsRunner.requestSuggestions(mSearchable, queryText);
+
+ // For debugging purposes, put in a lot of strings (really fast typist)
+ if (DBG_JAM_THREADING > 0) {
+ for (int ii = 1; ii < DBG_JAM_THREADING; ++ii) {
+ final String jamQuery = queryText + ii;
+ mSuggestionsRunner.requestSuggestions(mSearchable, jamQuery);
+ }
+ // one final (correct) string for cleanup
+ mSuggestionsRunner.requestSuggestions(mSearchable, queryText);
+ }
+ }
+
+ /**
+ * This class defines a queued message structure for processing user keystrokes, and a
+ * thread that allows the suggestions to be gathered out-of-band, and allows us to skip
+ * over multiple keystrokes if the typist is faster than the content provider.
+ */
+ private class SuggestionsRunner implements Runnable {
+
+ private class Request {
+ final SearchableInfo mSearchableInfo; // query will set these
+ final String mQueryText;
+ final boolean cancelRequest; // cancellation will set this
+
+ // simple constructors
+ Request(final SearchableInfo searchable, final String queryText) {
+ mSearchableInfo = searchable;
+ mQueryText = queryText;
+ cancelRequest = false;
+ }
+
+ Request() {
+ mSearchableInfo = null;
+ mQueryText = null;
+ cancelRequest = true;
+ }
+ }
+
+ private final LinkedBlockingQueue<Request> mSuggestionsQueue =
+ new LinkedBlockingQueue<Request>();
+
+ /**
+ * Queue up a suggestions request (non-blocking - can safely call from UI thread)
+ */
+ public void requestSuggestions(final SearchableInfo searchable, final String queryText) {
+ Request request = new Request(searchable, queryText);
+ try {
+ mSuggestionsQueue.put(request);
+ } catch (InterruptedException e) {
+ // discard the request.
+ }
+ }
+
+ /**
+ * Cancel blocking suggestions, discard any results, and shut down the thread.
+ * (non-blocking - can safely call from UI thread)
+ */
+ private void cancelSuggestions() {
+ Request request = new Request();
+ try {
+ mSuggestionsQueue.put(request);
+ } catch (InterruptedException e) {
+ // discard the request.
+ // TODO can we do better here?
+ }
+ }
+
+ /**
+ * This runnable implements the logic for decoupling keystrokes from suggestions.
+ * The logic isn't quite obvious here, so I'll try to describe it.
+ *
+ * Normally we simply sleep waiting for a keystroke. When a keystroke arrives,
+ * we immediately dispatch a request to gather suggestions.
+ *
+ * But this can take a while, so by the time it comes back, more keystrokes may have
+ * arrived. If anything happened while we were gathering the suggestion, we discard its
+ * results, and then use the most recent keystroke to start the next suggestions request.
+ *
+ * Any request containing cancelRequest == true will cause the thread to immediately
+ * terminate.
+ */
+ public void run() {
+ // outer blocking loop simply waits for a suggestion
+ while (true) {
+ try {
+ Request request = mSuggestionsQueue.take();
+ if (request.cancelRequest) {
+ return;
+ }
+
+ // since we were idle, what we're really interested is the final element
+ // in the queue. So keep pulling until we get the last element.
+ // TODO Could we just do some sort of takeHead() here?
+ while (! mSuggestionsQueue.isEmpty()) {
+ request = mSuggestionsQueue.take();
+ if (request.cancelRequest) {
+ return;
+ }
+ }
+ final Request useRequest = request;
+
+ // now process the final element (unless it's a cancel - that can be discarded)
+
+ if (useRequest.mSearchableInfo != null) {
+
+ // go get the cursor. this is what takes time.
+ final Cursor c = getSuggestions(useRequest.mSearchableInfo,
+ useRequest.mQueryText);
+
+ // We now have a suggestions result. But, if any new requests have arrived,
+ // we're going to discard them - we don't want to waste time displaying
+ // out-of-date results, we just want to get going on the next set.
+ // Note, null cursor is a valid result (no suggestions). This logic also
+ // supports the need to discard the results *and* stop the thread if a kill
+ // request arrives during a query.
+ if (mSuggestionsQueue.size() > 0) {
+ if (c != null) {
+ c.close();
+ }
+ } else {
+ mHandler.post(new Runnable() {
+ public void run() {
+ updateSuggestionsWithCursor(c, useRequest.mSearchableInfo);
+ }
+ });
+ }
+ }
+ } catch (InterruptedException e) {
+ // loop back for more
+ }
+ // At this point the queue may contain zero-to-many new requests; We simply
+ // loop back to handle them (or, block until new requests arrive)
+ }
+ }
+ }
+
+ /**
+ * Back in the UI thread, handle incoming cursors
+ */
+ private final static String[] ONE_LINE_FROM = {SearchManager.SUGGEST_COLUMN_TEXT_1 };
+ private final static String[] ONE_LINE_ICONS_FROM = {SearchManager.SUGGEST_COLUMN_TEXT_1,
+ SearchManager.SUGGEST_COLUMN_ICON_1,
+ SearchManager.SUGGEST_COLUMN_ICON_2};
+ private final static String[] TWO_LINE_FROM = {SearchManager.SUGGEST_COLUMN_TEXT_1,
+ SearchManager.SUGGEST_COLUMN_TEXT_2 };
+ private final static String[] TWO_LINE_ICONS_FROM = {SearchManager.SUGGEST_COLUMN_TEXT_1,
+ SearchManager.SUGGEST_COLUMN_TEXT_2,
+ SearchManager.SUGGEST_COLUMN_ICON_1,
+ SearchManager.SUGGEST_COLUMN_ICON_2 };
+
+ private final static int[] ONE_LINE_TO = {com.android.internal.R.id.text1};
+ private final static int[] ONE_LINE_ICONS_TO = {com.android.internal.R.id.text1,
+ com.android.internal.R.id.icon1,
+ com.android.internal.R.id.icon2};
+ private final static int[] TWO_LINE_TO = {com.android.internal.R.id.text1,
+ com.android.internal.R.id.text2};
+ private final static int[] TWO_LINE_ICONS_TO = {com.android.internal.R.id.text1,
+ com.android.internal.R.id.text2,
+ com.android.internal.R.id.icon1,
+ com.android.internal.R.id.icon2};
+
+ /**
+ * A new cursor (with suggestions) is ready for use. Update the UI.
+ */
+ void updateSuggestionsWithCursor(Cursor c, final SearchableInfo searchable) {
+ ListAdapter adapter = null;
+
+ // first, check for various conditions that disqualify this cursor
+ if ((c == null) || (c.getCount() == 0)) {
+ // no cursor, or cursor with no data
+ } else if ((searchable != mSearchable) || !isShowing()) {
+ // race condition (suggestions arrived after conditions changed)
+ } else {
+ // check cursor before trying to create list views from it
+ int colId = c.getColumnIndex("_id");
+ int col1 = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1);
+ int col2 = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2);
+ int colIc1 = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1);
+ int colIc2 = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_2);
+
+ boolean minimal = (colId >= 0) && (col1 >= 0);
+ boolean hasIcons = (colIc1 >= 0) && (colIc2 >= 0);
+ boolean has2Lines = col2 >= 0;
+
+ if (minimal) {
+ int layout;
+ String[] from;
+ int[] to;
+
+ if (hasIcons) {
+ if (has2Lines) {
+ layout = com.android.internal.R.layout.search_dropdown_item_icons_2line;
+ from = TWO_LINE_ICONS_FROM;
+ to = TWO_LINE_ICONS_TO;
+ } else {
+ layout = com.android.internal.R.layout.search_dropdown_item_icons_1line;
+ from = ONE_LINE_ICONS_FROM;
+ to = ONE_LINE_ICONS_TO;
+ }
+ } else {
+ if (has2Lines) {
+ layout = com.android.internal.R.layout.search_dropdown_item_2line;
+ from = TWO_LINE_FROM;
+ to = TWO_LINE_TO;
+ } else {
+ layout = com.android.internal.R.layout.search_dropdown_item_1line;
+ from = ONE_LINE_FROM;
+ to = ONE_LINE_TO;
+ }
+ }
+ try {
+ if (DBG_LOG_TIMING == 1) {
+ dbgLogTiming("updateSuggestions(3)");
+ }
+ adapter = new SuggestionsCursorAdapter(getContext(), layout, c, from, to,
+ mProviderContext);
+ if (DBG_LOG_TIMING == 1) {
+ dbgLogTiming("updateSuggestions(4)");
+ }
+ } catch (RuntimeException e) {
+ Log.e(LOG_TAG, "Exception while creating SuggestionsCursorAdapter", e);
+ }
+ }
+
+ // Provide some help for developers instead of just silently discarding
+ if ((colIc1 >= 0) != (colIc2 >= 0)) {
+ Log.w(LOG_TAG, "Suggestion icon column(s) discarded, must be 0 or 2 columns.");
+ } else if (adapter == null) {
+ Log.w(LOG_TAG, "Suggestions cursor discarded due to missing required columns.");
+ }
+ }
+
+ // if we have a cursor but we're not using it (e.g. disqualified), close it now
+ if ((c != null) && (adapter == null)) {
+ c.close();
+ c = null;
+ }
+
+ // we only made an adapter if there were 1+ suggestions. Now, based on the existence
+ // of the adapter, we'll also show/hide the list.
+ discardListCursor(mSuggestionsList);
+ if (adapter == null) {
+ showSuggestions(false, !mSkipNextAnimate);
+ } else {
+ layoutSuggestionsList();
+ showSuggestions(true, !mSkipNextAnimate);
+ }
+ mSkipNextAnimate = false;
+ if (DBG_LOG_TIMING == 1) {
+ dbgLogTiming("updateSuggestions(5)");
+ }
+ mSuggestionsList.setAdapter(adapter);
+ // now that we have an adapter, we can actually adjust the selection & scroll positions
+ if (mPresetSelection >= 0) {
+ boolean bTouchMode = mSuggestionsList.isInTouchMode();
+ mSuggestionsList.setSelection(mPresetSelection);
+ mPresetSelection = -1;
+ }
+ if (DBG_LOG_TIMING == 1) {
+ dbgLogTiming("updateSuggestions(6)");
+ }
+ }
+
+ /**
+ * Utility for showing & hiding the suggestions list. This is also responsible for triggering
+ * animation, if any, at the right time.
+ *
+ * @param visible If true, show the suggestions, if false, hide them.
+ * @param animate If true, use animation. If false, "just do it."
+ */
+ private void showSuggestions(boolean visible, boolean animate) {
+ if (visible) {
+ if (animate && (mSuggestionsList.getVisibility() != View.VISIBLE)) {
+ mSuggestionsList.startAnimation(mSuggestionsEntry);
+ }
+ mSuggestionsList.setVisibility(View.VISIBLE);
+ } else {
+ if (animate && (mSuggestionsList.getVisibility() != View.GONE)) {
+ mSuggestionsList.startAnimation(mSuggestionsExit);
+ }
+ mSuggestionsList.setVisibility(View.GONE);
+ }
+ }
+
+ /**
+ * This helper class supports the suggestions list by allowing 3rd party (e.g. app) resources
+ * to be used in suggestions
+ */
+ private static class SuggestionsCursorAdapter extends SimpleCursorAdapter {
+
+ private Resources mProviderResources;
+
+ public SuggestionsCursorAdapter(Context context, int layout, Cursor c,
+ String[] from, int[] to, Context providerContext) {
+ super(context, layout, c, from, to);
+ mProviderResources = providerContext.getResources();
+ }
+
+ /**
+ * Overriding this allows us to affect the way that an icon is loaded. Specifically,
+ * we can be more controlling about the resource path (and allow icons to come from other
+ * packages).
+ *
+ * @param v ImageView to receive an image
+ * @param value the value retrieved from the cursor
+ */
+ @Override
+ public void setViewImage(ImageView v, String value) {
+ int resID;
+ Drawable img = null;
+
+ try {
+ resID = Integer.parseInt(value);
+ if (resID != 0) {
+ img = mProviderResources.getDrawable(resID);
+ }
+ } catch (NumberFormatException nfe) {
+ // img = null;
+ } catch (NotFoundException e2) {
+ // img = null;
+ }
+
+ // finally, set the image to whatever we've gotten
+ v.setImageDrawable(img);
+ }
+
+ /**
+ * This method is overridden purely to provide a bit of protection against
+ * flaky content providers.
+ */
+ @Override
+ /**
+ * @see android.widget.ListAdapter#getView(int, View, ViewGroup)
+ */
+ public View getView(int position, View convertView, ViewGroup parent) {
+ try {
+ return super.getView(position, convertView, parent);
+ } catch (RuntimeException e) {
+ Log.w(LOG_TAG, "Search Suggestions cursor returned exception " + e.toString());
+ // what can I return here?
+ View v = newView(mContext, mCursor, parent);
+ if (v != null) {
+ TextView tv = (TextView) v.findViewById(com.android.internal.R.id.text1);
+ tv.setText(e.toString());
+ }
+ return v;
+ }
+ }
+ }
+
+ /**
+ * Cleanly close the cursor being used by a ListView. Do this before replacing the adapter
+ * or before closing the ListView.
+ */
+ private void discardListCursor(ListView list) {
+ CursorAdapter ca = getSuggestionsAdapter(list);
+ if (ca != null) {
+ Cursor c = ca.getCursor();
+ if (c != null) {
+ ca.changeCursor(null);
+ }
+ }
+ }
+
+ /**
+ * Safely retrieve the suggestions cursor adapter from the ListView
+ *
+ * @param adapterView The ListView containing our adapter
+ * @result The CursorAdapter that we installed, or null if not set
+ */
+ private static CursorAdapter getSuggestionsAdapter(AdapterView<?> adapterView) {
+ CursorAdapter result = null;
+ if (adapterView != null) {
+ Object ad = adapterView.getAdapter();
+ if (ad instanceof CursorAdapter) {
+ result = (CursorAdapter) ad;
+ } else if (ad instanceof WrapperListAdapter) {
+ result = (CursorAdapter) ((WrapperListAdapter)ad).getWrappedAdapter();
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Get the query cursor for the search suggestions.
+ *
+ * @param query The search text entered (so far)
+ * @return Returns a cursor with suggestions, or null if no suggestions
+ */
+ private Cursor getSuggestions(final SearchableInfo searchable, final String query) {
+ Cursor cursor = null;
+ if (searchable.getSuggestAuthority() != null) {
+ try {
+ StringBuilder uriStr = new StringBuilder("content://");
+ uriStr.append(searchable.getSuggestAuthority());
+
+ // if content path provided, insert it now
+ final String contentPath = searchable.getSuggestPath();
+ if (contentPath != null) {
+ uriStr.append('/');
+ uriStr.append(contentPath);
+ }
+
+ // append standard suggestion query path
+ uriStr.append('/' + SearchManager.SUGGEST_URI_PATH_QUERY);
+
+ // inject query, either as selection args or inline
+ String[] selArgs = null;
+ if (searchable.getSuggestSelection() != null) { // if selection provided, use it
+ selArgs = new String[] {query};
+ } else {
+ uriStr.append('/'); // no sel, use REST pattern
+ uriStr.append(Uri.encode(query));
+ }
+
+ // finally, make the query
+ if (DBG_LOG_TIMING == 1) {
+ dbgLogTiming("getSuggestions(1)");
+ }
+ cursor = getContext().getContentResolver().query(
+ Uri.parse(uriStr.toString()), null,
+ searchable.getSuggestSelection(), selArgs,
+ null);
+ if (DBG_LOG_TIMING == 1) {
+ dbgLogTiming("getSuggestions(2)");
+ }
+ } catch (RuntimeException e) {
+ Log.w(LOG_TAG, "Search Suggestions query returned exception " + e.toString());
+ cursor = null;
+ }
+ }
+
+ return cursor;
+ }
+
+ /**
+ * React to typing in the GO button by refocusing to EditText. Continue typing the query.
+ */
+ View.OnKeyListener mGoButtonKeyListener = new View.OnKeyListener() {
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ // also guard against possible race conditions (late arrival after dismiss)
+ if (mSearchable != null) {
+ return refocusingKeyListener(v, keyCode, event);
+ }
+ return false;
+ }
+ };
+
+ /**
+ * React to a click in the GO button by launching a search.
+ */
+ View.OnClickListener mGoButtonClickListener = new View.OnClickListener() {
+ public void onClick(View v) {
+ // also guard against possible race conditions (late arrival after dismiss)
+ if (mSearchable != null) {
+ launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null);
+ }
+ }
+ };
+
+ /**
+ * React to the user typing "enter" or other hardwired keys while typing in the search box.
+ * This handles these special keys while the edit box has focus.
+ */
+ View.OnKeyListener mTextKeyListener = new View.OnKeyListener() {
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ // also guard against possible race conditions (late arrival after dismiss)
+ if (mSearchable != null &&
+ TextUtils.getTrimmedLength(mSearchTextField.getText()) > 0) {
+ if (DBG_LOG_TIMING == 1) {
+ dbgLogTiming("doTextKey()");
+ }
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_ENTER:
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ if (event.getAction() == KeyEvent.ACTION_UP) {
+ v.cancelLongPress();
+ launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null);
+ return true;
+ }
+ break;
+ default:
+ if (event.getAction() == KeyEvent.ACTION_DOWN) {
+ SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
+ if ((actionKey != null) && (actionKey.mQueryActionMsg != null)) {
+ launchQuerySearch(keyCode, actionKey.mQueryActionMsg);
+ return true;
+ }
+ }
+ break;
+ }
+ }
+ return false;
+ }
+ };
+
+ /**
+ * React to the user typing while the suggestions are focused. First, check for action
+ * keys. If not handled, try refocusing regular characters into the EditText. In this case,
+ * replace the query text (start typing fresh text).
+ */
+ View.OnKeyListener mSuggestionsKeyListener = new View.OnKeyListener() {
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ boolean handled = false;
+ // also guard against possible race conditions (late arrival after dismiss)
+ if (mSearchable != null) {
+ handled = doSuggestionsKey(v, keyCode, event);
+ if (!handled) {
+ handled = refocusingKeyListener(v, keyCode, event);
+ }
+ }
+ return handled;
+ }
+ };
+
+ /**
+ * Per UI design, we're going to "steer" any typed keystrokes back into the EditText
+ * box, even if the user has navigated the focus to the dropdown or to the GO button.
+ *
+ * @param v The view into which the keystroke was typed
+ * @param keyCode keyCode of entered key
+ * @param event Full KeyEvent record of entered key
+ */
+ private boolean refocusingKeyListener(View v, int keyCode, KeyEvent event) {
+ boolean handled = false;
+
+ if (!event.isSystem() &&
+ (keyCode != KeyEvent.KEYCODE_DPAD_UP) &&
+ (keyCode != KeyEvent.KEYCODE_DPAD_DOWN) &&
+ (keyCode != KeyEvent.KEYCODE_DPAD_LEFT) &&
+ (keyCode != KeyEvent.KEYCODE_DPAD_RIGHT) &&
+ (keyCode != KeyEvent.KEYCODE_DPAD_CENTER)) {
+ // restore focus and give key to EditText ...
+ // but don't replace the user's query
+ mLeaveJammedQueryOnRefocus = true;
+ if (mSearchTextField.requestFocus()) {
+ handled = mSearchTextField.dispatchKeyEvent(event);
+ }
+ mLeaveJammedQueryOnRefocus = false;
+ }
+ return handled;
+ }
+
+ /**
+ * Update query text based on transitions in and out of suggestions list.
+ */
+ OnFocusChangeListener mSuggestFocusListener = new OnFocusChangeListener() {
+ public void onFocusChange(View v, boolean hasFocus) {
+ // also guard against possible race conditions (late arrival after dismiss)
+ if (mSearchable == null) {
+ return;
+ }
+ // Update query text based on navigation in to/out of the suggestions list
+ if (hasFocus) {
+ // Entering the list view - record selection point from user's query
+ mUserQuery = mSearchTextField.getText().toString();
+ mUserQuerySelStart = mSearchTextField.getSelectionStart();
+ mUserQuerySelEnd = mSearchTextField.getSelectionEnd();
+ // then update the query to match the entered selection
+ jamSuggestionQuery(true, mSuggestionsList,
+ mSuggestionsList.getSelectedItemPosition());
+ } else {
+ // Exiting the list view
+
+ if (mSuggestionsList.getSelectedItemPosition() < 0) {
+ // Direct exit - Leave new suggestion in place (do nothing)
+ } else {
+ // Navigation exit - restore user's query text
+ if (!mLeaveJammedQueryOnRefocus) {
+ jamSuggestionQuery(false, null, -1);
+ }
+ }
+ }
+
+ }
+ };
+
+ /**
+ * Update query text based on movement of selection in/out of suggestion list
+ */
+ OnItemSelectedListener mSuggestSelectedListener = new OnItemSelectedListener() {
+ public void onItemSelected(AdapterView parent, View view, int position, long id) {
+ // Update query text while user navigates through suggestions list
+ // also guard against possible race conditions (late arrival after dismiss)
+ if (mSearchable != null && position >= 0 && mSuggestionsList.isFocused()) {
+ jamSuggestionQuery(true, parent, position);
+ }
+ }
+
+ // No action needed on this callback
+ public void onNothingSelected(AdapterView parent) { }
+ };
+
+ /**
+ * This is the listener for the ACTION_CLOSE_SYSTEM_DIALOGS intent. It's an indication that
+ * we should close ourselves immediately, in order to allow a higher-priority UI to take over
+ * (e.g. phone call received).
+ */
+ private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(action)) {
+ cancel();
+ } else if (Intent.ACTION_PACKAGE_ADDED.equals(action)
+ || Intent.ACTION_PACKAGE_REMOVED.equals(action)
+ || Intent.ACTION_PACKAGE_CHANGED.equals(action)) {
+ onPackageListChange();
+ }
+ }
+ };
+
+ /**
+ * UI-thread handling of dialog dismiss. Called by mBroadcastReceiver.onReceive().
+ *
+ * TODO: This is a really heavyweight solution for something that should be so simple.
+ * For example, we already have a handler, in our superclass, why aren't we sharing that?
+ * I think we need to investigate simplifying this entire methodology, or perhaps boosting
+ * it up into the Dialog class.
+ */
+ private static final int MESSAGE_DISMISS = 0;
+ private Handler mDismissHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.what == MESSAGE_DISMISS) {
+ dismiss();
+ }
+ }
+ };
+
+ /**
+ * Listener for layout changes in the main layout. I use this to dynamically clean up
+ * the layout of the dropdown and make it "pixel perfect."
+ */
+ private ViewTreeObserver.OnGlobalLayoutListener mGlobalLayoutListener
+ = new ViewTreeObserver.OnGlobalLayoutListener() {
+
+ // It's very important that layoutSuggestionsList() does not reset
+ // the values more than once, or this becomes an infinite loop.
+ public void onGlobalLayout() {
+ layoutSuggestionsList();
+ }
+ };
+
+ /**
+ * Various ways to launch searches
+ */
+
+ /**
+ * React to the user clicking the "GO" button. Hide the UI and launch a search.
+ *
+ * @param actionKey Pass a keycode if the launch was triggered by an action key. Pass
+ * KeyEvent.KEYCODE_UNKNOWN for no actionKey code.
+ * @param actionMsg Pass the suggestion-provided message if the launch was triggered by an
+ * action key. Pass null for no actionKey message.
+ */
+ private void launchQuerySearch(int actionKey, final String actionMsg) {
+ final String query = mSearchTextField.getText().toString();
+ final Bundle appData = mAppSearchData;
+ final SearchableInfo si = mSearchable; // cache briefly (dismiss() nulls it)
+ dismiss();
+ sendLaunchIntent(Intent.ACTION_SEARCH, null, query, appData, actionKey, actionMsg, si);
+ }
+
+ /**
+ * React to the user typing an action key while in the suggestions list
+ */
+ private boolean doSuggestionsKey(View v, int keyCode, KeyEvent event) {
+ if (event.getAction() == KeyEvent.ACTION_DOWN) {
+ if (DBG_LOG_TIMING == 1) {
+ dbgLogTiming("doSuggestionsKey()");
+ }
+
+ // First, check for enter or search (both of which we'll treat as a "click")
+ if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH) {
+ AdapterView<?> av = (AdapterView<?>) v;
+ int position = av.getSelectedItemPosition();
+ return launchSuggestion(av, position);
+ }
+
+ // Next, check for left/right moves while we'll manually grab and shift focus
+ if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
+ // give focus to text editor
+ // but don't restore the user's original query
+ mLeaveJammedQueryOnRefocus = true;
+ if (mSearchTextField.requestFocus()) {
+ mLeaveJammedQueryOnRefocus = false;
+ if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
+ mSearchTextField.setSelection(0);
+ } else {
+ mSearchTextField.setSelection(mSearchTextField.length());
+ }
+ return true;
+ }
+ mLeaveJammedQueryOnRefocus = false;
+ }
+
+ // Next, check for an "action key"
+ SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
+ if ((actionKey != null) &&
+ ((actionKey.mSuggestActionMsg != null) ||
+ (actionKey.mSuggestActionMsgColumn != null))) {
+ // launch suggestion using action key column
+ ListView lv = (ListView) v;
+ int position = lv.getSelectedItemPosition();
+ if (position >= 0) {
+ CursorAdapter ca = getSuggestionsAdapter(lv);
+ Cursor c = ca.getCursor();
+ if (c.moveToPosition(position)) {
+ final String actionMsg = getActionKeyMessage(c, actionKey);
+ if (actionMsg != null && (actionMsg.length() > 0)) {
+ // shut down search bar and launch the activity
+ // cache everything we need because dismiss releases mems
+ setupSuggestionIntent(c, mSearchable);
+ final String query = mSearchTextField.getText().toString();
+ final Bundle appData = mAppSearchData;
+ SearchableInfo si = mSearchable;
+ String suggestionAction = mSuggestionAction;
+ Uri suggestionData = mSuggestionData;
+ String suggestionQuery = mSuggestionQuery;
+ dismiss();
+ sendLaunchIntent(suggestionAction, suggestionData,
+ suggestionQuery, appData,
+ keyCode, actionMsg, si);
+ return true;
+ }
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Set or reset the user query to follow the selections in the suggestions
+ *
+ * @param jamQuery True means to set the query, false means to reset it to the user's choice
+ */
+ private void jamSuggestionQuery(boolean jamQuery, AdapterView<?> parent, int position) {
+ mNonUserQuery = true; // disables any suggestions processing
+ if (jamQuery) {
+ CursorAdapter ca = getSuggestionsAdapter(parent);
+ Cursor c = ca.getCursor();
+ if (c.moveToPosition(position)) {
+ setupSuggestionIntent(c, mSearchable);
+ String jamText = null;
+
+ // Simple heuristic for selecting text with which to rewrite the query.
+ if (mSuggestionQuery != null) {
+ jamText = mSuggestionQuery;
+ } else if (mSearchable.mQueryRewriteFromData && (mSuggestionData != null)) {
+ jamText = mSuggestionData.toString();
+ } else if (mSearchable.mQueryRewriteFromText) {
+ try {
+ int column = c.getColumnIndexOrThrow(SearchManager.SUGGEST_COLUMN_TEXT_1);
+ jamText = c.getString(column);
+ } catch (RuntimeException e) {
+ // no work here, jamText is null
+ }
+ }
+ if (jamText != null) {
+ mSearchTextField.setText(jamText);
+ mSearchTextField.selectAll();
+ }
+ }
+ } else {
+ // reset user query
+ mSearchTextField.setText(mUserQuery);
+ try {
+ mSearchTextField.setSelection(mUserQuerySelStart, mUserQuerySelEnd);
+ } catch (IndexOutOfBoundsException e) {
+ // In case of error, just select all
+ Log.e(LOG_TAG, "Caught IndexOutOfBoundsException while setting selection. " +
+ "start=" + mUserQuerySelStart + " end=" + mUserQuerySelEnd +
+ " text=\"" + mUserQuery + "\"");
+ mSearchTextField.selectAll();
+ }
+ }
+ mNonUserQuery = false;
+ }
+
+ /**
+ * Assemble a search intent and send it.
+ *
+ * @param action The intent to send, typically Intent.ACTION_SEARCH
+ * @param data The data for the intent
+ * @param query The user text entered (so far)
+ * @param appData The app data bundle (if supplied)
+ * @param actionKey If the intent was triggered by an action key, e.g. KEYCODE_CALL, it will
+ * be sent here. Pass KeyEvent.KEYCODE_UNKNOWN for no actionKey code.
+ * @param actionMsg If the intent was triggered by an action key, e.g. KEYCODE_CALL, the
+ * corresponding tag message will be sent here. Pass null for no actionKey message.
+ * @param si Reference to the current SearchableInfo. Passed here so it can be used even after
+ * we've called dismiss(), which attempts to null mSearchable.
+ */
+ private void sendLaunchIntent(final String action, final Uri data, final String query,
+ final Bundle appData, int actionKey, final String actionMsg, final SearchableInfo si) {
+ Intent launcher = new Intent(action);
+
+ if (query != null) {
+ launcher.putExtra(SearchManager.QUERY, query);
+ }
+
+ if (data != null) {
+ launcher.setData(data);
+ }
+
+ if (appData != null) {
+ launcher.putExtra(SearchManager.APP_DATA, appData);
+ }
+
+ // add launch info (action key, etc.)
+ if (actionKey != KeyEvent.KEYCODE_UNKNOWN) {
+ launcher.putExtra(SearchManager.ACTION_KEY, actionKey);
+ launcher.putExtra(SearchManager.ACTION_MSG, actionMsg);
+ }
+
+ // attempt to enforce security requirement (no 3rd-party intents)
+ launcher.setComponent(si.mSearchActivity);
+
+ getContext().startActivity(launcher);
+ }
+
+ /**
+ * Handler for clicks in the suggestions list
+ */
+ private OnItemClickListener mSuggestionsListItemClickListener = new OnItemClickListener() {
+ public void onItemClick(AdapterView parent, View v, int position, long id) {
+ // this guard protects against possible race conditions (late arrival of click)
+ if (mSearchable != null) {
+ launchSuggestion(parent, position);
+ }
+ }
+ };
+
+ /**
+ * Shared code for launching a query from a suggestion.
+ *
+ * @param av The AdapterView (really a ListView) containing the suggestions
+ * @param position The suggestion we'll be launching from
+ *
+ * @return Returns true if a successful launch, false if could not (e.g. bad position)
+ */
+ private boolean launchSuggestion(AdapterView<?> av, int position) {
+ CursorAdapter ca = getSuggestionsAdapter(av);
+ Cursor c = ca.getCursor();
+ if ((c != null) && c.moveToPosition(position)) {
+ setupSuggestionIntent(c, mSearchable);
+
+ final Bundle appData = mAppSearchData;
+ SearchableInfo si = mSearchable;
+ String suggestionAction = mSuggestionAction;
+ Uri suggestionData = mSuggestionData;
+ String suggestionQuery = mSuggestionQuery;
+ dismiss();
+ sendLaunchIntent(suggestionAction, suggestionData, suggestionQuery, appData,
+ KeyEvent.KEYCODE_UNKNOWN, null, si);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Manually adjust suggestions list into its perfectly-tweaked position.
+ *
+ * NOTE: This MUST not adjust the parameters if they are already set correctly,
+ * or you create an infinite loop via the ViewTreeObserver.OnGlobalLayoutListener callback.
+ */
+ private void layoutSuggestionsList() {
+ final int FUDGE_SUGG_X = 1;
+ final int FUDGE_SUGG_WIDTH = 2;
+
+ int[] itemLoc = new int[2];
+ mSearchTextField.getLocationOnScreen(itemLoc);
+ int x,width;
+ x = itemLoc[0] + FUDGE_SUGG_X;
+ width = mSearchTextField.getMeasuredWidth() + FUDGE_SUGG_WIDTH;
+
+ // now set params and relayout
+ ViewGroup.MarginLayoutParams lp;
+ lp = (ViewGroup.MarginLayoutParams) mSuggestionsList.getLayoutParams();
+ boolean changing = (lp.width != width) || (lp.leftMargin != x);
+ if (changing) {
+ lp.leftMargin = x;
+ lp.width = width;
+ mSuggestionsList.setLayoutParams(lp);
+ }
+ }
+
+ /**
+ * When a particular suggestion has been selected, perform the various lookups required
+ * to use the suggestion. This includes checking the cursor for suggestion-specific data,
+ * and/or falling back to the XML for defaults; It also creates REST style Uri data when
+ * the suggestion includes a data id.
+ *
+ * NOTE: Return values are in member variables mSuggestionAction & mSuggestionData.
+ *
+ * @param c The suggestions cursor, moved to the row of the user's selection
+ * @param si The searchable activity's info record
+ */
+ void setupSuggestionIntent(Cursor c, SearchableInfo si) {
+ try {
+ // use specific action if supplied, or default action if supplied, or fixed default
+ mSuggestionAction = null;
+ int mColumn = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_ACTION);
+ if (mColumn >= 0) {
+ final String action = c.getString(mColumn);
+ if (action != null) {
+ mSuggestionAction = action;
+ }
+ }
+ if (mSuggestionAction == null) {
+ mSuggestionAction = si.getSuggestIntentAction();
+ }
+ if (mSuggestionAction == null) {
+ mSuggestionAction = Intent.ACTION_SEARCH;
+ }
+
+ // use specific data if supplied, or default data if supplied
+ String data = null;
+ mColumn = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_DATA);
+ if (mColumn >= 0) {
+ final String rowData = c.getString(mColumn);
+ if (rowData != null) {
+ data = rowData;
+ }
+ }
+ if (data == null) {
+ data = si.getSuggestIntentData();
+ }
+
+ // then, if an ID was provided, append it.
+ if (data != null) {
+ mColumn = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
+ if (mColumn >= 0) {
+ final String id = c.getString(mColumn);
+ if (id != null) {
+ data = data + "/" + Uri.encode(id);
+ }
+ }
+ }
+ mSuggestionData = (data == null) ? null : Uri.parse(data);
+
+ mSuggestionQuery = null;
+ mColumn = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_QUERY);
+ if (mColumn >= 0) {
+ final String query = c.getString(mColumn);
+ if (query != null) {
+ mSuggestionQuery = query;
+ }
+ }
+ } catch (RuntimeException e ) {
+ int rowNum;
+ try { // be really paranoid now
+ rowNum = c.getPosition();
+ } catch (RuntimeException e2 ) {
+ rowNum = -1;
+ }
+ Log.w(LOG_TAG, "Search Suggestions cursor at row " + rowNum +
+ " returned exception" + e.toString());
+ }
+ }
+
+ /**
+ * For a given suggestion and a given cursor row, get the action message. If not provided
+ * by the specific row/column, also check for a single definition (for the action key).
+ *
+ * @param c The cursor providing suggestions
+ * @param actionKey The actionkey record being examined
+ *
+ * @return Returns a string, or null if no action key message for this suggestion
+ */
+ private String getActionKeyMessage(Cursor c, final SearchableInfo.ActionKeyInfo actionKey) {
+ String result = null;
+ // check first in the cursor data, for a suggestion-specific message
+ final String column = actionKey.mSuggestActionMsgColumn;
+ if (column != null) {
+ try {
+ int colId = c.getColumnIndexOrThrow(column);
+ result = c.getString(colId);
+ } catch (RuntimeException e) {
+ // OK - result is already null
+ }
+ }
+ // If the cursor didn't give us a message, see if there's a single message defined
+ // for the actionkey (for all suggestions)
+ if (result == null) {
+ result = actionKey.mSuggestActionMsg;
+ }
+ return result;
+ }
+
+ /**
+ * Debugging Support
+ */
+
+ /**
+ * For debugging only, sample the millisecond clock and log it.
+ * Uses AtomicLong so we can use in multiple threads
+ */
+ private AtomicLong mLastLogTime = new AtomicLong(SystemClock.uptimeMillis());
+ private void dbgLogTiming(final String caller) {
+ long millis = SystemClock.uptimeMillis();
+ long oldTime = mLastLogTime.getAndSet(millis);
+ long delta = millis - oldTime;
+ final String report = millis + " (+" + delta + ") ticks for Search keystroke in " + caller;
+ Log.d(LOG_TAG,report);
+ }
+}
diff --git a/core/java/android/app/SearchManager.java b/core/java/android/app/SearchManager.java
new file mode 100644
index 0000000..01babc4
--- /dev/null
+++ b/core/java/android/app/SearchManager.java
@@ -0,0 +1,1328 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.ServiceManager;
+import android.view.KeyEvent;
+
+/**
+ * This class provides access to the system search services.
+ *
+ * <p>In practice, you won't interact with this class directly, as search
+ * services are provided through methods in {@link android.app.Activity Activity}
+ * methods and the the {@link android.content.Intent#ACTION_SEARCH ACTION_SEARCH}
+ * {@link android.content.Intent Intent}. This class does provide a basic
+ * overview of search services and how to integrate them with your activities.
+ * If you do require direct access to the Search Manager, do not instantiate
+ * this class directly; instead, retrieve it through
+ * {@link android.content.Context#getSystemService
+ * context.getSystemService(Context.SEARCH_SERVICE)}.
+ *
+ * <p>Topics covered here:
+ * <ol>
+ * <li><a href="#DeveloperGuide">Developer Guide</a>
+ * <li><a href="#HowSearchIsInvoked">How Search Is Invoked</a>
+ * <li><a href="#QuerySearchApplications">Query-Search Applications</a>
+ * <li><a href="#FilterSearchApplications">Filter-Search Applications</a>
+ * <li><a href="#Suggestions">Search Suggestions</a>
+ * <li><a href="#ActionKeys">Action Keys</a>
+ * <li><a href="#SearchabilityMetadata">Searchability Metadata</a>
+ * <li><a href="#PassingSearchContext">Passing Search Context</a>
+ * </ol>
+ *
+ * <a name="DeveloperGuide"></a>
+ * <h3>Developer Guide</h3>
+ *
+ * <p>The ability to search for user, system, or network based data is considered to be
+ * a core user-level feature of the android platform. At any time, the user should be
+ * able to use a familiar command, button, or keystroke to invoke search, and the user
+ * should be able to search any data which is available to them. The goal is to make search
+ * appear to the user as a seamless, system-wide feature.
+ *
+ * <p>In terms of implementation, there are three broad classes of Applications:
+ * <ol>
+ * <li>Applications that are not inherently searchable</li>
+ * <li>Query-Search Applications</li>
+ * <li>Filter-Search Applications</li>
+ * </ol>
+ * <p>These categories, as well as related topics, are discussed in
+ * the sections below.
+ *
+ * <p>Even if your application is not <i>searchable</i>, it can still support the invocation of
+ * search. Please review the section <a href="#HowSearchIsInvoked">How Search Is Invoked</a>
+ * for more information on how to support this.
+ *
+ * <p>Many applications are <i>searchable</i>. These are
+ * the applications which can convert a query string into a list of results.
+ * Within this subset, applications can be grouped loosely into two families:
+ * <ul><li><i>Query Search</i> applications perform batch-mode searches - each query string is
+ * converted to a list of results.</li>
+ * <li><i>Filter Search</i> applications provide live filter-as-you-type searches.</li></ul>
+ * <p>Generally speaking, you would use query search for network-based data, and filter
+ * search for local data, but this is not a hard requirement and applications
+ * are free to use the model that fits them best (or invent a new model).
+ * <p>It should be clear that the search implementation decouples "search
+ * invocation" from "searchable". This satisfies the goal of making search appear
+ * to be "universal". The user should be able to launch any search from
+ * almost any context.
+ *
+ * <a name="HowSearchIsInvoked"></a>
+ * <h3>How Search Is Invoked</h3>
+ *
+ * <p>Unless impossible or inapplicable, all applications should support
+ * invoking the search UI. This means that when the user invokes the search command,
+ * a search UI will be presented to them. The search command is currently defined as a menu
+ * item called "Search" (with an alphabetic shortcut key of "S"), or on some devices, a dedicated
+ * search button key.
+ * <p>If your application is not inherently searchable, you can also allow the search UI
+ * to be invoked in a "web search" mode. If the user enters a search term and clicks the
+ * "Search" button, this will bring the browser to the front and will launch a web-based
+ * search. The user will be able to click the "Back" button and return to your application.
+ * <p>In general this is implemented by your activity, or the {@link android.app.Activity Activity}
+ * base class, which captures the search command and invokes the Search Manager to
+ * display and operate the search UI. You can also cause the search UI to be presented in response
+ * to user keystrokes in your activity (for example, to instantly start filter searching while
+ * viewing a list and typing any key).
+ * <p>The search UI is presented as a floating
+ * window and does not cause any change in the activity stack. If the user
+ * cancels search, the previous activity re-emerges. If the user launches a
+ * search, this will be done by sending a search {@link android.content.Intent Intent} (see below),
+ * and the normal intent-handling sequence will take place (your activity will pause,
+ * etc.)
+ * <p><b>What you need to do:</b> First, you should consider the way in which you want to
+ * handle invoking search. There are four broad (and partially overlapping) categories for
+ * you to choose from.
+ * <ul><li>You can capture the search command yourself, by including a <i>search</i>
+ * button or menu item - and invoking the search UI directly.</li>
+ * <li>You can provide a <i>type-to-search</i> feature, in which search is invoked automatically
+ * when the user enters any characters.</li>
+ * <li>Even if your application is not inherently searchable, you can allow web search,
+ * via the search key (or even via a search menu item).
+ * <li>You can disable search entirely. This should only be used in very rare circumstances,
+ * as search is a system-wide feature and users will expect it to be available in all contexts.</li>
+ * </ul>
+ *
+ * <p><b>How to define a search menu.</b> The system provides the following resources which may
+ * be useful when adding a search item to your menu:
+ * <ul><li>android.R.drawable.ic_search_category_default is an icon you can use in your menu.</li>
+ * <li>{@link #MENU_KEY SearchManager.MENU_KEY} is the recommended alphabetic shortcut.</li>
+ * </ul>
+ *
+ * <p><b>How to invoke search directly.</b> In order to invoke search directly, from a button
+ * or menu item, you can launch a generic search by calling
+ * {@link android.app.Activity#onSearchRequested onSearchRequested} as shown:
+ * <pre class="prettyprint">
+ * onSearchRequested();</pre>
+ *
+ * <p><b>How to implement type-to-search.</b> While setting up your activity, call
+ * {@link android.app.Activity#setDefaultKeyMode setDefaultKeyMode}:
+ * <pre class="prettyprint">
+ * setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL); // search within your activity
+ * setDefaultKeyMode(DEFAULT_KEYS_SEARCH_GLOBAL); // search using platform global search</pre>
+ *
+ * <p><b>How to enable web-based search.</b> In addition to searching within your activity or
+ * application, you can also use the Search Manager to invoke a platform-global search, typically
+ * a web search. There are two ways to do this:
+ * <ul><li>You can simply define "search" within your application or activity to mean global search.
+ * This is described in more detail in the
+ * <a href="#SearchabilityMetadata">Searchability Metadata</a> section. Briefly, you will
+ * add a single meta-data entry to your manifest, declaring that the default search
+ * for your application is "*". This indicates to the system that no application-specific
+ * search activity is provided, and that it should launch web-based search instead.</li>
+ * <li>You can specify this at invocation time via default keys (see above), overriding
+ * {@link android.app.Activity#onSearchRequested}, or via a direct call to
+ * {@link android.app.Activity#startSearch}. This is most useful if you wish to provide local
+ * searchability <i>and</i> access to global search.</li></ul>
+ *
+ * <p><b>How to disable search from your activity.</b> search is a system-wide feature and users
+ * will expect it to be available in all contexts. If your UI design absolutely precludes
+ * launching search, override {@link android.app.Activity#onSearchRequested onSearchRequested}
+ * as shown:
+ * <pre class="prettyprint">
+ * &#64;Override
+ * public boolean onSearchRequested() {
+ * return false;
+ * }</pre>
+ *
+ * <p><b>Managing focus and knowing if Search is active.</b> The search UI is not a separate
+ * activity, and when the UI is invoked or dismissed, your activity will not typically be paused,
+ * resumed, or otherwise notified by the methods defined in
+ * <a href="android.app.Activity#ActivityLifecycle">Activity Lifecycle</a>. The search UI is
+ * handled in the same way as other system UI elements which may appear from time to time, such as
+ * notifications, screen locks, or other system alerts:
+ * <p>When the search UI appears, your activity will lose input focus.
+ * <p>When the search activity is dismissed, there are three possible outcomes:
+ * <ul><li>If the user simply canceled the search UI, your activity will regain input focus and
+ * proceed as before. See {@link #setOnDismissListener} and {@link #setOnCancelListener} if you
+ * required direct notification of search dialog dismissals.</li>
+ * <li>If the user launched a search, and this required switching to another activity to receive
+ * and process the search {@link android.content.Intent Intent}, your activity will receive the
+ * normal sequence of activity pause or stop notifications.</li>
+ * <li>If the user launched a search, and the current activity is the recipient of the search
+ * {@link android.content.Intent Intent}, you will receive notification via the
+ * {@link android.app.Activity#onNewIntent onNewIntent()} method.</li></ul>
+ * <p>This list is provided in order to clarify the ways in which your activities will interact with
+ * the search UI. More details on searchable activities and search intents are provided in the
+ * sections below.
+ *
+ * <a name="QuerySearchApplications"></a>
+ * <h3>Query-Search Applications</h3>
+ *
+ * <p>Query-search applications are those that take a single query (e.g. a search
+ * string) and present a set of results that may fit. Primary examples include
+ * web queries, map lookups, or email searches (with the common thread being
+ * network query dispatch). It may also be the case that certain local searches
+ * are treated this way. It's up to the application to decide.
+ *
+ * <p><b>What you need to do:</b> The following steps are necessary in order to
+ * implement query search.
+ * <ul>
+ * <li>Implement search invocation as described above. (Strictly speaking,
+ * these are decoupled, but it would make little sense to be "searchable" but not
+ * "search-invoking".)</li>
+ * <li>Your application should have an activity that takes a search string and
+ * converts it to a list of results. This could be your primary display activity
+ * or it could be a dedicated search results activity. This is your <i>searchable</i>
+ * activity and every query-search application must have one.</li>
+ * <li>In the searchable activity, in onCreate(), you must receive and handle the
+ * {@link android.content.Intent#ACTION_SEARCH ACTION_SEARCH}
+ * {@link android.content.Intent Intent}. The text to search (query string) for is provided by
+ * calling
+ * {@link #QUERY getStringExtra(SearchManager.QUERY)}.</li>
+ * <li>To identify and support your searchable activity, you'll need to
+ * provide an XML file providing searchability configuration parameters, a reference to that
+ * in your searchable activity's <a href="../../../devel/bblocks-manifest.html">manifest</a>
+ * entry, and an intent-filter declaring that you can
+ * receive ACTION_SEARCH intents. This is described in more detail in the
+ * <a href="#SearchabilityMetadata">Searchability Metadata</a> section.</li>
+ * <li>Your <a href="../../../devel/bblocks-manifest.html">manifest</a> also needs a metadata entry
+ * providing a global reference to the searchable activity. This is the "glue" directing the search
+ * UI, when invoked from any of your <i>other</i> activities, to use your application as the
+ * default search context. This is also described in more detail in the
+ * <a href="#SearchabilityMetadata">Searchability Metadata</a> section.</li>
+ * <li>Finally, you may want to define your search results activity as with the
+ * {@link android.R.attr#launchMode singleTop} launchMode flag. This allows the system
+ * to launch searches from/to the same activity without creating a pile of them on the
+ * activity stack. If you do this, be sure to also override
+ * {@link android.app.Activity#onNewIntent onNewIntent} to handle the
+ * updated intents (with new queries) as they arrive.</li>
+ * </ul>
+ *
+ * <p>Code snippet showing handling of intents in your search activity:
+ * <pre class="prettyprint">
+ * &#64;Override
+ * protected void onCreate(Bundle icicle) {
+ * super.onCreate(icicle);
+ *
+ * final Intent queryIntent = getIntent();
+ * final String queryAction = queryIntent.getAction();
+ * if (Intent.ACTION_SEARCH.equals(queryAction)) {
+ * doSearchWithIntent(queryIntent);
+ * }
+ * }
+ *
+ * private void doSearchWithIntent(final Intent queryIntent) {
+ * final String queryString = queryIntent.getStringExtra(SearchManager.QUERY);
+ * doSearchWithQuery(queryString);
+ * }</pre>
+ *
+ * <a name="FilterSearchApplications"></a>
+ * <h3>Filter-Search Applications</h3>
+ *
+ * <p>Filter-search applications are those that use live text entry (e.g. keystrokes)) to
+ * display and continuously update a list of results. Primary examples include applications
+ * that use locally-stored data.
+ *
+ * <p>Filter search is not directly supported by the Search Manager. Most filter search
+ * implementations will use variants of {@link android.widget.Filterable}, such as a
+ * {@link android.widget.ListView} bound to a {@link android.widget.SimpleCursorAdapter}. However,
+ * you may find it useful to mix them together, by declaring your filtered view searchable. With
+ * this configuration, you can still present the standard search dialog in all activities
+ * within your application, but transition to a filtered search when you enter the activity
+ * and display the results.
+ *
+ * <a name="Suggestions"></a>
+ * <h3>Search Suggestions</h3>
+ *
+ * <p>A powerful feature of the Search Manager is the ability of any application to easily provide
+ * live "suggestions" in order to prompt the user. Each application implements suggestions in a
+ * different, unique, and appropriate way. Suggestions be drawn from many sources, including but
+ * not limited to:
+ * <ul>
+ * <li>Actual searchable results (e.g. names in the address book)</li>
+ * <li>Recently entered queries</li>
+ * <li>Recently viewed data or results</li>
+ * <li>Contextually appropriate queries or results</li>
+ * <li>Summaries of possible results</li>
+ * </ul>
+ *
+ * <p>Another feature of suggestions is that they can expose queries or results before the user
+ * ever visits the application. This reduces the amount of context switching required, and helps
+ * the user access their data quickly and with less context shifting. In order to provide this
+ * capability, suggestions are accessed via a
+ * {@link android.content.ContentProvider Content Provider}.
+ *
+ * <p>The primary form of suggestions is known as <i>queried suggestions</i> and is based on query
+ * text that the user has already typed. This would generally be based on partial matches in
+ * the available data. In certain situations - for example, when no query text has been typed yet -
+ * an application may also opt to provide <i>zero-query suggestions</i>.
+ * These would typically be drawn from the same data source, but because no partial query text is
+ * available, they should be weighted based on other factors - for example, most recent queries
+ * or most recent results.
+ *
+ * <p><b>Overview of how suggestions are provided.</b> When the search manager identifies a
+ * particular activity as searchable, it will check for certain metadata which indicates that
+ * there is also a source of suggestions. If suggestions are provided, the following steps are
+ * taken.
+ * <ul><li>Using formatting information found in the metadata, the user's query text (whatever
+ * has been typed so far) will be formatted into a query and sent to the suggestions
+ * {@link android.content.ContentProvider Content Provider}.</li>
+ * <li>The suggestions {@link android.content.ContentProvider Content Provider} will create a
+ * {@link android.database.Cursor Cursor} which can iterate over the possible suggestions.</li>
+ * <li>The search manager will populate a list using display data found in each row of the cursor,
+ * and display these suggestions to the user.</li>
+ * <li>If the user types another key, or changes the query in any way, the above steps are repeated
+ * and the suggestions list is updated or repopulated.</li>
+ * <li>If the user clicks or touches the "GO" button, the suggestions are ignored and the search is
+ * launched using the normal {@link android.content.Intent#ACTION_SEARCH ACTION_SEARCH} type of
+ * {@link android.content.Intent Intent}.</li>
+ * <li>If the user uses the directional controls to navigate the focus into the suggestions list,
+ * the query text will be updated while the user navigates from suggestion to suggestion. The user
+ * can then click or touch the updated query and edit it further. If the user navigates back to
+ * the edit field, the original typed query is restored.</li>
+ * <li>If the user clicks or touches a particular suggestion, then a combination of data from the
+ * cursor and
+ * values found in the metadata are used to synthesize an Intent and send it to the application.
+ * Depending on the design of the activity and the way it implements search, this might be a
+ * {@link android.content.Intent#ACTION_SEARCH ACTION_SEARCH} (in order to launch a query), or it
+ * might be a {@link android.content.Intent#ACTION_VIEW ACTION_VIEW}, in order to proceed directly
+ * to display of specific data.</li>
+ * </ul>
+ *
+ * <p><b>Simple Recent-Query-Based Suggestions.</b> The Android framework provides a simple Search
+ * Suggestions provider, which simply records and replays recent queries. For many applications,
+ * this will be sufficient. The basic steps you will need to
+ * do, in order to use the built-in recent queries suggestions provider, are as follows:
+ * <ul>
+ * <li>Implement and test query search, as described in the previous sections.</li>
+ * <li>Create a Provider within your application by extending
+ * {@link android.content.SearchRecentSuggestionsProvider}.</li>
+ * <li>Create a manifest entry describing your provider.</li>
+ * <li>Update your searchable activity's XML configuration file with information about your
+ * provider.</li>
+ * <li>In your searchable activities, capture any user-generated queries and record them
+ * for future searches by calling {@link android.provider.SearchRecentSuggestions#saveRecentQuery}.
+ * </li>
+ * </ul>
+ * <p>For complete implementation details, please refer to
+ * {@link android.content.SearchRecentSuggestionsProvider}. The rest of the information in this
+ * section should not be necessary, as it refers to custom suggestions providers.
+ *
+ * <p><b>Creating a Customized Suggestions Provider:</b> In order to create more sophisticated
+ * suggestion providers, you'll need to take the following steps:
+ * <ul>
+ * <li>Implement and test query search, as described in the previous sections.</li>
+ * <li>Decide how you wish to <i>receive</i> suggestions. Just like queries that the user enters,
+ * suggestions will be delivered to your searchable activity as
+ * {@link android.content.Intent Intent} messages; Unlike simple queries, you have quite a bit of
+ * flexibility in forming those intents. A query search application will probably
+ * wish to continue receiving the {@link android.content.Intent#ACTION_SEARCH ACTION_SEARCH}
+ * {@link android.content.Intent Intent}, which will launch a query search using query text as
+ * provided by the suggestion. A filter search application will probably wish to
+ * receive the {@link android.content.Intent#ACTION_VIEW ACTION_VIEW}
+ * {@link android.content.Intent Intent}, which will take the user directly to a selected entry.
+ * Other interesting suggestions, including hybrids, are possible, and the suggestion provider
+ * can easily mix-and-match results to provide a richer set of suggestions for the user. Finally,
+ * you'll need to update your searchable activity (or other activities) to receive the intents
+ * as you've defined them.</li>
+ * <li>Implement a Content Provider that provides suggestions. If you already have one, and it
+ * has access to your suggestions data. If not, you'll have to create one.
+ * You'll also provide information about your Content Provider in your
+ * package's <a href="../../../devel/bblocks-manifest.html">manifest</a>.</li>
+ * <li>Update your searchable activity's XML configuration file. There are two categories of
+ * information used for suggestions:
+ * <ul><li>The first is (required) data that the search manager will
+ * use to format the queries which are sent to the Content Provider.</li>
+ * <li>The second is (optional) parameters to configure structure
+ * if intents generated by suggestions.</li></li>
+ * </ul>
+ * </ul>
+ *
+ * <p><b>Configuring your Content Provider to Receive Suggestion Queries.</b> The basic job of
+ * a search suggestions {@link android.content.ContentProvider Content Provider} is to provide
+ * "live" (while-you-type) conversion of the user's query text into a set of zero or more
+ * suggestions. Each application is free to define the conversion, and as described above there are
+ * many possible solutions. This section simply defines how to communicate with the suggestion
+ * provider.
+ *
+ * <p>The Search Manager must first determine if your package provides suggestions. This is done
+ * by examination of your searchable meta-data XML file. The android:searchSuggestAuthority
+ * attribute, if provided, is the signal to obtain & display suggestions.
+ *
+ * <p>Every query includes a Uri, and the Search Manager will format the Uri as shown:
+ * <p><pre class="prettyprint">
+ * content:// your.suggest.authority / your.suggest.path / SearchManager.SUGGEST_URI_PATH_QUERY</pre>
+ *
+ * <p>Your Content Provider can receive the query text in one of two ways.
+ * <ul>
+ * <li><b>Query provided as a selection argument.</b> If you define the attribute value
+ * android:searchSuggestSelection and include a string, this string will be passed as the
+ * <i>selection</i> parameter to your Content Provider's query function. You must define a single
+ * selection argument, using the '?' character. The user's query text will be passed to you
+ * as the first element of the selection arguments array.</li>
+ * <li><b>Query provided with Data Uri.</b> If you <i>do not</i> define the attribute value
+ * android:searchSuggestSelection, then the Search Manager will append another "/" followed by
+ * the user's query to the query Uri. The query will be encoding using Uri encoding rules - don't
+ * forget to decode it. (See {@link android.net.Uri#getPathSegments} and
+ * {@link android.net.Uri#getLastPathSegment} for helpful utilities you can use here.)</li>
+ * </ul>
+ *
+ * <p><b>Handling empty queries.</b> Your application should handle the "empty query"
+ * (no user text entered) case properly, and generate useful suggestions in this case. There are a
+ * number of ways to do this; Two are outlined here:
+ * <ul><li>For a simple filter search of local data, you could simply present the entire dataset,
+ * unfiltered. (example: People)</li>
+ * <li>For a query search, you could simply present the most recent queries. This allows the user
+ * to quickly repeat a recent search.</li></ul>
+ *
+ * <p><b>The Format of Individual Suggestions.</b> Your suggestions are communicated back to the
+ * Search Manager by way of a {@link android.database.Cursor Cursor}. The Search Manager will
+ * usually pass a null Projection, which means that your provider can simply return all appropriate
+ * columns for each suggestion. The columns currently defined are:
+ *
+ * <table border="2" width="85%" align="center" frame="hsides" rules="rows">
+ *
+ * <thead>
+ * <tr><th>Column Name</th> <th>Description</th> <th>Required?</th></tr>
+ * </thead>
+ *
+ * <tbody>
+ * <tr><th>{@link #SUGGEST_COLUMN_FORMAT}</th>
+ * <td><i>Unused - can be null.</i></td>
+ * <td align="center">No</td>
+ * </tr>
+ *
+ * <tr><th>{@link #SUGGEST_COLUMN_TEXT_1}</th>
+ * <td>This is the line of text that will be presented to the user as the suggestion.</td>
+ * <td align="center">Yes</td>
+ * </tr>
+ *
+ * <tr><th>{@link #SUGGEST_COLUMN_TEXT_2}</th>
+ * <td>If your cursor includes this column, then all suggestions will be provided in a
+ * two-line format. The data in this column will be displayed as a second, smaller
+ * line of text below the primary suggestion, or it can be null or empty to indicate no
+ * text in this row's suggestion.</td>
+ * <td align="center">No</td>
+ * </tr>
+ *
+ * <tr><th>{@link #SUGGEST_COLUMN_ICON_1}</th>
+ * <td>If your cursor includes this column, then all suggestions will be provided in an
+ * icons+text format. This value should be a reference (resource ID) of the icon to
+ * draw on the left side, or it can be null or zero to indicate no icon in this row.
+ * You must provide both cursor columns, or neither.
+ * </td>
+ * <td align="center">No, but required if you also have {@link #SUGGEST_COLUMN_ICON_2}</td>
+ * </tr>
+ *
+ * <tr><th>{@link #SUGGEST_COLUMN_ICON_2}</th>
+ * <td>If your cursor includes this column, then all suggestions will be provided in an
+ * icons+text format. This value should be a reference (resource ID) of the icon to
+ * draw on the right side, or it can be null or zero to indicate no icon in this row.
+ * You must provide both cursor columns, or neither.
+ * </td>
+ * <td align="center">No, but required if you also have {@link #SUGGEST_COLUMN_ICON_1}</td>
+ * </tr>
+ *
+ * <tr><th>{@link #SUGGEST_COLUMN_INTENT_ACTION}</th>
+ * <td>If this column exists <i>and</i> this element exists at the given row, this is the
+ * action that will be used when forming the suggestion's intent. If the element is
+ * not provided, the action will be taken from the android:searchSuggestIntentAction
+ * field in your XML metadata. <i>At least one of these must be present for the
+ * suggestion to generate an intent.</i> Note: If your action is the same for all
+ * suggestions, it is more efficient to specify it using XML metadata and omit it from
+ * the cursor.</td>
+ * <td align="center">No</td>
+ * </tr>
+ *
+ * <tr><th>{@link #SUGGEST_COLUMN_INTENT_DATA}</th>
+ * <td>If this column exists <i>and</i> this element exists at the given row, this is the
+ * data that will be used when forming the suggestion's intent. If the element is not
+ * provided, the data will be taken from the android:searchSuggestIntentData field in
+ * your XML metadata. If neither source is provided, the Intent's data field will be
+ * null. Note: If your data is the same for all suggestions, or can be described
+ * using a constant part and a specific ID, it is more efficient to specify it using
+ * XML metadata and omit it from the cursor.</td>
+ * <td align="center">No</td>
+ * </tr>
+ *
+ * <tr><th>{@link #SUGGEST_COLUMN_INTENT_DATA_ID}</th>
+ * <td>If this column exists <i>and</i> this element exists at the given row, then "/" and
+ * this value will be appended to the data field in the Intent. This should only be
+ * used if the data field has already been set to an appropriate base string.</td>
+ * <td align="center">No</td>
+ * </tr>
+ *
+ * <tr><th>{@link #SUGGEST_COLUMN_QUERY}</th>
+ * <td>If this column exists <i>and</i> this element exists at the given row, this is the
+ * data that will be used when forming the suggestion's query.</td>
+ * <td align="center">Required if suggestion's action is
+ * {@link android.content.Intent#ACTION_SEARCH ACTION_SEARCH}, optional otherwise.</td>
+ * </tr>
+ *
+ * <tr><th><i>Other Columns</i></th>
+ * <td>Finally, if you have defined any <a href="#ActionKeys">Action Keys</a> and you wish
+ * for them to have suggestion-specific definitions, you'll need to define one
+ * additional column per action key. The action key will only trigger if the
+ * currently-selection suggestion has a non-empty string in the corresponding column.
+ * See the section on <a href="#ActionKeys">Action Keys</a> for additional details and
+ * implementation steps.</td>
+ * <td align="center">No</td>
+ * </tr>
+ *
+ * </tbody>
+ * </table>
+ *
+ * <p>Clearly there are quite a few permutations of your suggestion data, but in the next section
+ * we'll look at a few simple combinations that you'll select from.
+ *
+ * <p><b>The Format Of Intents Sent By Search Suggestions.</b> Although there are many ways to
+ * configure these intents, this document will provide specific information on just a few of them.
+ * <ul><li><b>Launch a query.</b> In this model, each suggestion represents a query that your
+ * searchable activity can perform, and the {@link android.content.Intent Intent} will be formatted
+ * exactly like those sent when the user enters query text and clicks the "GO" button:
+ * <ul>
+ * <li><b>Action:</b> {@link android.content.Intent#ACTION_SEARCH ACTION_SEARCH} provided
+ * using your XML metadata (android:searchSuggestIntentAction).</li>
+ * <li><b>Data:</b> empty (not used).</li>
+ * <li><b>Query:</b> query text supplied by the cursor.</li>
+ * </ul>
+ * </li>
+ * <li><b>Go directly to a result, using a complete Data Uri.</b> In this model, the user will be
+ * taken directly to a specific result.
+ * <ul>
+ * <li><b>Action:</b> {@link android.content.Intent#ACTION_VIEW ACTION_VIEW}</li>
+ * <li><b>Data:</b> a complete Uri, supplied by the cursor, that identifies the desired data.</li>
+ * <li><b>Query:</b> query text supplied with the suggestion (probably ignored)</li>
+ * </ul>
+ * </li>
+ * <li><b>Go directly to a result, using a synthesized Data Uri.</b> This has the same result
+ * as the previous suggestion, but provides the Data Uri in a different way.
+ * <ul>
+ * <li><b>Action:</b> {@link android.content.Intent#ACTION_VIEW ACTION_VIEW}</li>
+ * <li><b>Data:</b> The search manager will assemble a Data Uri using the following elements:
+ * a Uri fragment provided in your XML metadata (android:searchSuggestIntentData), followed by
+ * a single "/", followed by the value found in the {@link #SUGGEST_COLUMN_INTENT_DATA_ID}
+ * entry in your cursor.</li>
+ * <li><b>Query:</b> query text supplied with the suggestion (probably ignored)</li>
+ * </ul>
+ * </li>
+ * </ul>
+ * <p>This list is not meant to be exhaustive. Applications should feel free to define other types
+ * of suggestions. For example, you could reduce long lists of results to summaries, and use one
+ * of the above intents (or one of your own) with specially formatted Data Uri's to display more
+ * detailed results. Or you could display textual shortcuts as suggestions, but launch a display
+ * in a more data-appropriate format such as media artwork.
+ *
+ * <p><b>Suggestion Rewriting.</b> If the user navigates through the suggestions list, the UI
+ * may temporarily rewrite the user's query with a query that matches the currently selected
+ * suggestion. This enables the user to see what query is being suggested, and also allows the user
+ * to click or touch in the entry EditText element and make further edits to the query before
+ * dispatching it. In order to perform this correctly, the Search UI needs to know exactly what
+ * text to rewrite the query with.
+ *
+ * <p>For each suggestion, the following logic is used to select a new query string:
+ * <ul><li>If the suggestion provides an explicit value in the {@link #SUGGEST_COLUMN_QUERY}
+ * column, this value will be used.</li>
+ * <li>If the metadata includes the queryRewriteFromData flag, and the suggestion provides an
+ * explicit value for the intent Data field, this Uri will be used. Note that this should only be
+ * used with Uri's that are intended to be user-visible, such as HTTP. Internal Uri schemes should
+ * not be used in this way.</li>
+ * <li>If the metadata includes the queryRewriteFromText flag, the text in
+ * {@link #SUGGEST_COLUMN_TEXT_1} will be used. This should be used for suggestions in which no
+ * query text is provided and the SUGGEST_COLUMN_INTENT_DATA values are not suitable for user
+ * inspection and editing.</li></ul>
+ *
+ * <a name="ActionKeys"></a>
+ * <h3>Action Keys</h3>
+ *
+ * <p>Searchable activities may also wish to provide shortcuts based on the various action keys
+ * available on the device. The most basic example of this is the contacts app, which enables the
+ * green "dial" key for quick access during searching. Not all action keys are available on
+ * every device, and not all are allowed to be overriden in this way. (For example, the "Home"
+ * key must always return to the home screen, with no exceptions.)
+ *
+ * <p>In order to define action keys for your searchable application, you must do two things.
+ *
+ * <ul>
+ * <li>You'll add one or more <i>actionkey</i> elements to your searchable metadata configuration
+ * file. Each element defines one of the keycodes you are interested in,
+ * defines the conditions under which they are sent, and provides details
+ * on how to communicate the action key event back to your searchable activity.</li>
+ * <li>In your intent receiver, if you wish, you can check for action keys by checking the
+ * extras field of the {@link android.content.Intent Intent}.</li>
+ * </ul>
+ *
+ * <p><b>Updating metadata.</b> For each keycode of interest, you must add an &lt;actionkey&gt;
+ * element. Within this element you must define two or three attributes. The first attribute,
+ * &lt;android:keycode&gt;, is required; It is the key code of the action key event, as defined in
+ * {@link android.view.KeyEvent}. The remaining two attributes define the value of the actionkey's
+ * <i>message</i>, which will be passed to your searchable activity in the
+ * {@link android.content.Intent Intent} (see below for more details). Although each of these
+ * attributes is optional, you must define one or both for the action key to have any effect.
+ * &lt;android:queryActionMsg&gt; provides the message that will be sent if the action key is
+ * pressed while the user is simply entering query text. &lt;android:suggestActionMsgColumn&gt;
+ * is used when action keys are tied to specific suggestions. This attribute provides the name
+ * of a <i>column</i> in your suggestion cursor; The individual suggestion, in that column,
+ * provides the message. (If the cell is empty or null, that suggestion will not work with that
+ * action key.)
+ * <p>See the <a href="#SearchabilityMetadata">Searchability Metadata</a> section for more details
+ * and examples.
+ *
+ * <p><b>Receiving Action Keys</b> Intents launched by action keys will be specially marked
+ * using a combination of values. This enables your searchable application to examine the intent,
+ * if necessary, and perform special processing. For example, clicking a suggested contact might
+ * simply display them; Selecting a suggested contact and clicking the dial button might
+ * immediately call them.
+ *
+ * <p>When a search {@link android.content.Intent Intent} is launched by an action key, two values
+ * will be added to the extras field.
+ * <ul>
+ * <li>To examine the key code, use {@link android.content.Intent#getIntExtra
+ * getIntExtra(SearchManager.ACTION_KEY)}.</li>
+ * <li>To examine the message string, use {@link android.content.Intent#getStringExtra
+ * getStringExtra(SearchManager.ACTION_MSG)}</li>
+ * </ul>
+ *
+ * <a name="SearchabilityMetadata"></a>
+ * <h3>Searchability Metadata</h3>
+ *
+ * <p>Every activity that is searchable must provide a small amount of additional information
+ * in order to properly configure the search system. This controls the way that your search
+ * is presented to the user, and controls for the various modalities described previously.
+ *
+ * <p>If your application is not searchable,
+ * then you do not need to provide any search metadata, and you can skip the rest of this section.
+ * When this search metadata cannot be found, the search manager will assume that the activity
+ * does not implement search. (Note: to implement web-based search, you will need to add
+ * the android.app.default_searchable metadata to your manifest, as shown below.)
+ *
+ * <p>Values you supply in metadata apply only to each local searchable activity. Each
+ * searchable activity can define a completely unique search experience relevant to its own
+ * capabilities and user experience requirements, and a single application can even define multiple
+ * searchable activities.
+ *
+ * <p><b>Metadata for searchable activity.</b> As with your search implementations described
+ * above, you must first identify which of your activities is searchable. In the
+ * <a href="../../../devel/bblocks-manifest.html">manifest</a> entry for this activity, you must
+ * provide two elements:
+ * <ul><li>An intent-filter specifying that you can receive and process the
+ * {@link android.content.Intent#ACTION_SEARCH ACTION_SEARCH} {@link android.content.Intent Intent}.
+ * </li>
+ * <li>A reference to a small XML file (typically called "searchable.xml") which contains the
+ * remaining configuration information for how your application implements search.</li></ul>
+ *
+ * <p>Here is a snippet showing the necessary elements in the
+ * <a href="../../../devel/bblocks-manifest.html">manifest</a> entry for your searchable activity.
+ * <pre class="prettyprint">
+ * &lt;!-- Search Activity - searchable --&gt;
+ * &lt;activity android:name="MySearchActivity"
+ * android:label="Search"
+ * android:launchMode="singleTop"&gt;
+ * &lt;intent-filter&gt;
+ * &lt;action android:name="android.intent.action.SEARCH" /&gt;
+ * &lt;category android:name="android.intent.category.DEFAULT" /&gt;
+ * &lt;/intent-filter&gt;
+ * &lt;meta-data android:name="android.app.searchable"
+ * android:resource="@xml/searchable" /&gt;
+ * &lt;/activity&gt;</pre>
+ *
+ * <p>Next, you must provide the rest of the searchability configuration in
+ * the small XML file, stored in the ../xml/ folder in your build. The XML file is a
+ * simple enumeration of the search configuration parameters for searching within this activity,
+ * application, or package. Here is a sample XML file (named searchable.xml, for use with
+ * the above manifest) for a query-search activity.
+ *
+ * <pre class="prettyprint">
+ * &lt;searchable xmlns:android="http://schemas.android.com/apk/res/android"
+ * android:label="@string/search_label"
+ * android:hint="@string/search_hint" &gt;
+ * &lt;/searchable&gt;</pre>
+ *
+ * <p>Note that all user-visible strings <i>must</i> be provided in the form of "@string"
+ * references. Hard-coded strings, which cannot be localized, will not work properly in search
+ * metadata.
+ *
+ * <p>Attributes you can set in search metadata:
+ * <table border="2" width="85%" align="center" frame="hsides" rules="rows">
+ *
+ * <thead>
+ * <tr><th>Attribute</th> <th>Description</th> <th>Required?</th></tr>
+ * </thead>
+ *
+ * <tbody>
+ * <tr><th>android:label</th>
+ * <td>This is the name for your application that will be presented to the user in a
+ * list of search targets, or in the search box as a label.</td>
+ * <td align="center">Yes</td>
+ * </tr>
+ *
+ * <tr><th>android:icon</th>
+ * <td>If provided, this icon will be used <i>in place</i> of the label string. This
+ * is provided in order to present logos or other non-textual banners.</td>
+ * <td align="center">No</td>
+ * </tr>
+ *
+ * <tr><th>android:hint</th>
+ * <td>This is the text to display in the search text field when no user text has been
+ * entered.</td>
+ * <td align="center">No</td>
+ * </tr>
+ *
+ * <tr><th>android:searchButtonText</th>
+ * <td>If provided, this text will replace the default text in the "Search" button.</td>
+ * <td align="center">No</td>
+ * </tr>
+ *
+ * <tr><th>android:searchMode</th>
+ * <td>If provided and non-zero, sets additional modes for control of the search
+ * presentation. The following mode bits are defined:
+ * <table border="2" align="center" frame="hsides" rules="rows">
+ * <tbody>
+ * <tr><th>showSearchLabelAsBadge</th>
+ * <td>If set, this flag enables the display of the search target (label)
+ * within the search bar. If this flag and showSearchIconAsBadge
+ * (see below) are both not set, no badge will be shown.</td>
+ * </tr>
+ * <tr><th>showSearchIconAsBadge</th>
+ * <td>If set, this flag enables the display of the search target (icon) within
+ * the search bar. If this flag and showSearchLabelAsBadge
+ * (see above) are both not set, no badge will be shown. If both flags
+ * are set, showSearchIconAsBadge has precedence and the icon will be
+ * shown.</td>
+ * </tr>
+ * <tr><th>queryRewriteFromData</th>
+ * <td>If set, this flag causes the suggestion column SUGGEST_COLUMN_INTENT_DATA
+ * to be considered as the text for suggestion query rewriting. This should
+ * only be used when the values in SUGGEST_COLUMN_INTENT_DATA are suitable
+ * for user inspection and editing - typically, HTTP/HTTPS Uri's.</td>
+ * </tr>
+ * <tr><th>queryRewriteFromText</th>
+ * <td>If set, this flag causes the suggestion column SUGGEST_COLUMN_TEXT_1 to
+ * be considered as the text for suggestion query rewriting. This should
+ * be used for suggestions in which no query text is provided and the
+ * SUGGEST_COLUMN_INTENT_DATA values are not suitable for user inspection
+ * and editing.</td>
+ * </tr>
+ * </tbody>
+ * </table></td>
+ * <td align="center">No</td>
+ * </tr>
+ *
+ * </tbody>
+ * </table>
+ *
+ * <p><b>Styleable Resources in your Metadata.</b> It's possible to provide alternate strings
+ * for your searchable application, in order to provide localization and/or to better visual
+ * presentation on different device configurations. Each searchable activity has a single XML
+ * metadata file, but any resource references can be replaced at runtime based on device
+ * configuration, language setting, and other system inputs.
+ *
+ * <p>A concrete example is the "hint" text you supply using the android:searchHint attribute.
+ * In portrait mode you'll have less screen space and may need to provide a shorter string, but
+ * in landscape mode you can provide a longer, more descriptive hint. To do this, you'll need to
+ * define two or more strings.xml files, in the following directories:
+ * <ul><li>.../res/values-land/strings.xml</li>
+ * <li>.../res/values-port/strings.xml</li>
+ * <li>.../res/values/strings.xml</li></ul>
+ *
+ * <p>For more complete documentation on this capability, see
+ * <a href="../../../devel/resources-i18n.html#AlternateResources">Resources and
+ * Internationalization: Supporting Alternate Resources for Alternate Languages and Configurations
+ * </a>.
+ *
+ * <p><b>Metadata for non-searchable activities.</b> Activities which are part of a searchable
+ * application, but don't implement search itself, require a bit of "glue" in order to cause
+ * them to invoke search using your searchable activity as their primary context. If this is not
+ * provided, then searches from these activities will use the system default search context.
+ *
+ * <p>The simplest way to specify this is to add a <i>search reference</i> element to the
+ * application entry in the <a href="../../../devel/bblocks-manifest.html">manifest</a> file.
+ * The value of this reference can be either of:
+ * <ul><li>The name of your searchable activity.
+ * It is typically prefixed by '.' to indicate that it's in the same package.</li>
+ * <li>A "*" indicates that the system may select a default searchable activity, in which
+ * case it will typically select web-based search.</li>
+ * </ul>
+ *
+ * <p>Here is a snippet showing the necessary addition to the manifest entry for your
+ * non-searchable activities.
+ * <pre class="prettyprint">
+ * &lt;application&gt;
+ * &lt;meta-data android:name="android.app.default_searchable"
+ * android:value=".MySearchActivity" /&gt;
+ *
+ * &lt;!-- followed by activities, providers, etc... --&gt;
+ * &lt;/application&gt;</pre>
+ *
+ * <p>You can also specify android.app.default_searchable on a per-activity basis, by including
+ * the meta-data element (as shown above) in one or more activity sections. If found, these will
+ * override the reference in the application section. The only reason to configure your application
+ * this way would be if you wish to partition it into separate sections with different search
+ * behaviors; Otherwise this configuration is not recommended.
+ *
+ * <p><b>Additional Metadata for search suggestions.</b> If you have defined a content provider
+ * to generate search suggestions, you'll need to publish it to the system, and you'll need to
+ * provide a bit of additional XML metadata in order to configure communications with it.
+ *
+ * <p>First, in your <a href="../../../devel/bblocks-manifest.html">manifest</a>, you'll add the
+ * following lines.
+ * <pre class="prettyprint">
+ * &lt;!-- Content provider for search suggestions --&gt;
+ * &lt;provider android:name="YourSuggestionProviderClass"
+ * android:authorities="your.suggestion.authority" /&gt;</pre>
+ *
+ * <p>Next, you'll add a few lines to your XML metadata file, as shown:
+ * <pre class="prettyprint">
+ * &lt;!-- Required attribute for any suggestions provider --&gt;
+ * android:searchSuggestAuthority="your.suggestion.authority"
+ *
+ * &lt;!-- Optional attribute for configuring queries --&gt;
+ * android:searchSuggestSelection="field =?"
+ *
+ * &lt;!-- Optional attributes for configuring intent construction --&gt;
+ * android:searchSuggestIntentAction="intent action string"
+ * android:searchSuggestIntentData="intent data Uri" /&gt;</pre>
+ *
+ * <p>Elements of search metadata that support suggestions:
+ * <table border="2" width="85%" align="center" frame="hsides" rules="rows">
+ *
+ * <thead>
+ * <tr><th>Attribute</th> <th>Description</th> <th>Required?</th></tr>
+ * </thead>
+ *
+ * <tbody>
+ * <tr><th>android:searchSuggestAuthority</th>
+ * <td>This value must match the authority string provided in the <i>provider</i> section
+ * of your <a href="../../../devel/bblocks-manifest.html">manifest</a>.</td>
+ * <td align="center">Yes</td>
+ * </tr>
+ *
+ * <tr><th>android:searchSuggestPath</th>
+ * <td>If provided, this will be inserted in the suggestions query Uri, after the authority
+ * you have provide but before the standard suggestions path. This is only required if
+ * you have a single content provider issuing different types of suggestions (e.g. for
+ * different data types) and you need a way to disambiguate the suggestions queries
+ * when they are received.</td>
+ * <td align="center">No</td>
+ * </tr>
+ *
+ * <tr><th>android:searchSuggestSelection</th>
+ * <td>If provided, this value will be passed into your query function as the
+ * <i>selection</i> parameter. Typically this will be a WHERE clause for your database,
+ * and will contain a single question mark, which represents the actual query string
+ * that has been typed by the user. However, you can also use any non-null value
+ * to simply trigger the delivery of the query text (via selection arguments), and then
+ * use the query text in any way appropriate for your provider (ignoring the actual
+ * text of the selection parameter.)</td>
+ * <td align="center">No</td>
+ * </tr>
+ *
+ * <tr><th>android:searchSuggestIntentAction</th>
+ * <td>If provided, and not overridden by the selected suggestion, this value will be
+ * placed in the action field of the {@link android.content.Intent Intent} when the
+ * user clicks a suggestion.</td>
+ * <td align="center">No</td>
+ *
+ * <tr><th>android:searchSuggestIntentData</th>
+ * <td>If provided, and not overridden by the selected suggestion, this value will be
+ * placed in the data field of the {@link android.content.Intent Intent} when the user
+ * clicks a suggestion.</td>
+ * <td align="center">No</td>
+ * </tr>
+ *
+ * </tbody>
+ * </table>
+ *
+ * <p><b>Additional Metadata for search action keys.</b> For each action key that you would like to
+ * define, you'll need to add an additional element defining that key, and using the attributes
+ * discussed in <a href="#ActionKeys">Action Keys</a>. A simple example is shown here:
+ *
+ * <pre class="prettyprint">&lt;actionkey
+ * android:keycode="KEYCODE_CALL"
+ * android:queryActionMsg="call"
+ * android:suggestActionMsg="call"
+ * android:suggestActionMsgColumn="call_column" /&gt;</pre>
+ *
+ * <p>Elements of search metadata that support search action keys. Note that although each of the
+ * action message elements are marked as <i>optional</i>, at least one must be present for the
+ * action key to have any effect.
+ *
+ * <table border="2" width="85%" align="center" frame="hsides" rules="rows">
+ *
+ * <thead>
+ * <tr><th>Attribute</th> <th>Description</th> <th>Required?</th></tr>
+ * </thead>
+ *
+ * <tbody>
+ * <tr><th>android:keycode</th>
+ * <td>This attribute denotes the action key you wish to respond to. Note that not
+ * all action keys are actually supported using this mechanism, as many of them are
+ * used for typing, navigation, or system functions. This will be added to the
+ * {@link android.content.Intent#ACTION_SEARCH ACTION_SEARCH} intent that is passed to
+ * your searchable activity. To examine the key code, use
+ * {@link android.content.Intent#getIntExtra getIntExtra(SearchManager.ACTION_KEY)}.
+ * <p>Note, in addition to the keycode, you must also provide one or more of the action
+ * specifier attributes.</td>
+ * <td align="center">Yes</td>
+ * </tr>
+ *
+ * <tr><th>android:queryActionMsg</th>
+ * <td>If you wish to handle an action key during normal search query entry, you
+ * must define an action string here. This will be added to the
+ * {@link android.content.Intent#ACTION_SEARCH ACTION_SEARCH} intent that is passed to your
+ * searchable activity. To examine the string, use
+ * {@link android.content.Intent#getStringExtra
+ * getStringExtra(SearchManager.ACTION_MSG)}.</td>
+ * <td align="center">No</td>
+ * </tr>
+ *
+ * <tr><th>android:suggestActionMsg</th>
+ * <td>If you wish to handle an action key while a suggestion is being displayed <i>and
+ * selected</i>, there are two ways to handle this. If <i>all</i> of your suggestions
+ * can handle the action key, you can simply define the action message using this
+ * attribute. This will be added to the
+ * {@link android.content.Intent#ACTION_SEARCH ACTION_SEARCH} intent that is passed to
+ * your searchable activity. To examine the string, use
+ * {@link android.content.Intent#getStringExtra
+ * getStringExtra(SearchManager.ACTION_MSG)}.</td>
+ * <td align="center">No</td>
+ * </tr>
+ *
+ * <tr><th>android:suggestActionMsgColumn</th>
+ * <td>If you wish to handle an action key while a suggestion is being displayed <i>and
+ * selected</i>, but you do not wish to enable this action key for every suggestion,
+ * then you can use this attribute to control it on a suggestion-by-suggestion basis.
+ * First, you must define a column (and name it here) where your suggestions will
+ * include the action string. Then, in your content provider, you must provide this
+ * column, and when desired, provide data in this column.
+ * The search manager will look at your suggestion cursor, using the string
+ * provided here in order to select a column, and will use that to select a string from
+ * the cursor. That string will be added to the
+ * {@link android.content.Intent#ACTION_SEARCH ACTION_SEARCH} intent that is passed to
+ * your searchable activity. To examine the string, use
+ * {@link android.content.Intent#getStringExtra
+ * getStringExtra(SearchManager.ACTION_MSG)}. <i>If the data does not exist for the
+ * selection suggestion, the action key will be ignored.</i></td>
+ * <td align="center">No</td>
+ * </tr>
+ *
+ * </tbody>
+ * </table>
+ *
+ * <a name="PassingSearchContext"></a>
+ * <h3>Passing Search Context</h3>
+ *
+ * <p>In order to improve search experience, an application may wish to specify
+ * additional data along with the search, such as local history or context. For
+ * example, a maps search would be improved by including the current location.
+ * In order to simplify the structure of your activities, this can be done using
+ * the search manager.
+ *
+ * <p>Any data can be provided at the time the search is launched, as long as it
+ * can be stored in a {@link android.os.Bundle Bundle} object.
+ *
+ * <p>To pass application data into the Search Manager, you'll need to override
+ * {@link android.app.Activity#onSearchRequested onSearchRequested} as follows:
+ *
+ * <pre class="prettyprint">
+ * &#64;Override
+ * public boolean onSearchRequested() {
+ * Bundle appData = new Bundle();
+ * appData.put...();
+ * appData.put...();
+ * startSearch(null, false, appData);
+ * return true;
+ * }</pre>
+ *
+ * <p>To receive application data from the Search Manager, you'll extract it from
+ * the {@link android.content.Intent#ACTION_SEARCH ACTION_SEARCH}
+ * {@link android.content.Intent Intent} as follows:
+ *
+ * <pre class="prettyprint">
+ * final Bundle appData = queryIntent.getBundleExtra(SearchManager.APP_DATA);
+ * if (appData != null) {
+ * appData.get...();
+ * appData.get...();
+ * }</pre>
+ */
+public class SearchManager
+ implements DialogInterface.OnDismissListener, DialogInterface.OnCancelListener
+{
+ /**
+ * This is a shortcut definition for the default menu key to use for invoking search.
+ *
+ * See Menu.Item.setAlphabeticShortcut() for more information.
+ */
+ public final static char MENU_KEY = 's';
+
+ /**
+ * This is a shortcut definition for the default menu key to use for invoking search.
+ *
+ * See Menu.Item.setAlphabeticShortcut() for more information.
+ */
+ public final static int MENU_KEYCODE = KeyEvent.KEYCODE_S;
+
+ /**
+ * Intent extra data key: Use this key with
+ * {@link android.content.Intent#getStringExtra
+ * content.Intent.getStringExtra()}
+ * to obtain the query string from Intent.ACTION_SEARCH.
+ */
+ public final static String QUERY = "query";
+
+ /**
+ * Intent extra data key: Use this key with Intent.ACTION_SEARCH and
+ * {@link android.content.Intent#getBundleExtra
+ * content.Intent.getBundleExtra()}
+ * to obtain any additional app-specific data that was inserted by the
+ * activity that launched the search.
+ */
+ public final static String APP_DATA = "app_data";
+
+ /**
+ * Intent extra data key: Use this key with Intent.ACTION_SEARCH and
+ * {@link android.content.Intent#getIntExtra content.Intent.getIntExtra()}
+ * to obtain the keycode that the user used to trigger this query. It will be zero if the
+ * user simply pressed the "GO" button on the search UI. This is primarily used in conjunction
+ * with the keycode attribute in the actionkey element of your searchable.xml configuration
+ * file.
+ */
+ public final static String ACTION_KEY = "action_key";
+
+ /**
+ * Intent extra data key: Use this key with Intent.ACTION_SEARCH and
+ * {@link android.content.Intent#getStringExtra content.Intent.getStringExtra()}
+ * to obtain the action message that was defined for a particular search action key and/or
+ * suggestion. It will be null if the search was launched by typing "enter", touched the the
+ * "GO" button, or other means not involving any action key.
+ */
+ public final static String ACTION_MSG = "action_msg";
+
+ /**
+ * Uri path for queried suggestions data. This is the path that the search manager
+ * will use when querying your content provider for suggestions data based on user input
+ * (e.g. looking for partial matches).
+ * Typically you'll use this with a URI matcher.
+ */
+ public final static String SUGGEST_URI_PATH_QUERY = "search_suggest_query";
+
+ /**
+ * MIME type for suggestions data. You'll use this in your suggestions content provider
+ * in the getType() function.
+ */
+ public final static String SUGGEST_MIME_TYPE =
+ "vnd.android.cursor.dir/vnd.android.search.suggest";
+
+ /**
+ * Column name for suggestions cursor. <i>Unused - can be null or column can be omitted.</i>
+ */
+ public final static String SUGGEST_COLUMN_FORMAT = "suggest_format";
+ /**
+ * Column name for suggestions cursor. <i>Required.</i> This is the primary line of text that
+ * will be presented to the user as the suggestion.
+ */
+ public final static String SUGGEST_COLUMN_TEXT_1 = "suggest_text_1";
+ /**
+ * Column name for suggestions cursor. <i>Optional.</i> If your cursor includes this column,
+ * then all suggestions will be provided in a two-line format. The second line of text is in
+ * a much smaller appearance.
+ */
+ public final static String SUGGEST_COLUMN_TEXT_2 = "suggest_text_2";
+ /**
+ * Column name for suggestions cursor. <i>Optional.</i> If your cursor includes this column,
+ * then all suggestions will be provided in format that includes space for two small icons,
+ * one at the left and one at the right of each suggestion. The data in the column must
+ * be a a resource ID for the icon you wish to have displayed. If you include this column,
+ * you must also include {@link #SUGGEST_COLUMN_ICON_2}.
+ */
+ public final static String SUGGEST_COLUMN_ICON_1 = "suggest_icon_1";
+ /**
+ * Column name for suggestions cursor. <i>Optional.</i> If your cursor includes this column,
+ * then all suggestions will be provided in format that includes space for two small icons,
+ * one at the left and one at the right of each suggestion. The data in the column must
+ * be a a resource ID for the icon you wish to have displayed. If you include this column,
+ * you must also include {@link #SUGGEST_COLUMN_ICON_1}.
+ */
+ public final static String SUGGEST_COLUMN_ICON_2 = "suggest_icon_2";
+ /**
+ * Column name for suggestions cursor. <i>Optional.</i> If this column exists <i>and</i>
+ * this element exists at the given row, this is the action that will be used when
+ * forming the suggestion's intent. If the element is not provided, the action will be taken
+ * from the android:searchSuggestIntentAction field in your XML metadata. <i>At least one of
+ * these must be present for the suggestion to generate an intent.</i> Note: If your action is
+ * the same for all suggestions, it is more efficient to specify it using XML metadata and omit
+ * it from the cursor.
+ */
+ public final static String SUGGEST_COLUMN_INTENT_ACTION = "suggest_intent_action";
+ /**
+ * Column name for suggestions cursor. <i>Optional.</i> If this column exists <i>and</i>
+ * this element exists at the given row, this is the data that will be used when
+ * forming the suggestion's intent. If the element is not provided, the data will be taken
+ * from the android:searchSuggestIntentData field in your XML metadata. If neither source
+ * is provided, the Intent's data field will be null. Note: If your data is
+ * the same for all suggestions, or can be described using a constant part and a specific ID,
+ * it is more efficient to specify it using XML metadata and omit it from the cursor.
+ */
+ public final static String SUGGEST_COLUMN_INTENT_DATA = "suggest_intent_data";
+ /**
+ * Column name for suggestions cursor. <i>Optional.</i> If this column exists <i>and</i>
+ * this element exists at the given row, then "/" and this value will be appended to the data
+ * field in the Intent. This should only be used if the data field has already been set to an
+ * appropriate base string.
+ */
+ public final static String SUGGEST_COLUMN_INTENT_DATA_ID = "suggest_intent_data_id";
+ /**
+ * Column name for suggestions cursor. <i>Required if action is
+ * {@link android.content.Intent#ACTION_SEARCH ACTION_SEARCH}, optional otherwise.</i> If this
+ * column exists <i>and</i> this element exists at the given row, this is the data that will be
+ * used when forming the suggestion's query.
+ */
+ public final static String SUGGEST_COLUMN_QUERY = "suggest_intent_query";
+
+
+ private final Context mContext;
+ private final Handler mHandler;
+
+ private SearchDialog mSearchDialog;
+
+ private OnDismissListener mDismissListener = null;
+ private OnCancelListener mCancelListener = null;
+
+ /*package*/ SearchManager(Context context, Handler handler) {
+ mContext = context;
+ mHandler = handler;
+ }
+ private static ISearchManager mService;
+
+ static {
+ mService = ISearchManager.Stub.asInterface(
+ ServiceManager.getService(Context.SEARCH_SERVICE));
+ }
+
+ /**
+ * Launch search UI.
+ *
+ * <p>The search manager will open a search widget in an overlapping
+ * window, and the underlying activity may be obscured. The search
+ * entry state will remain in effect until one of the following events:
+ * <ul>
+ * <li>The user completes the search. In most cases this will launch
+ * a search intent.</li>
+ * <li>The user uses the back, home, or other keys to exit the search.</li>
+ * <li>The application calls the {@link #stopSearch}
+ * method, which will hide the search window and return focus to the
+ * activity from which it was launched.</li>
+ *
+ * <p>Most applications will <i>not</i> use this interface to invoke search.
+ * The primary method for invoking search is to call
+ * {@link android.app.Activity#onSearchRequested Activity.onSearchRequested()} or
+ * {@link android.app.Activity#startSearch Activity.startSearch()}.
+ *
+ * @param initialQuery A search string can be pre-entered here, but this
+ * is typically null or empty.
+ * @param selectInitialQuery If true, the intial query will be preselected, which means that
+ * any further typing will replace it. This is useful for cases where an entire pre-formed
+ * query is being inserted. If false, the selection point will be placed at the end of the
+ * inserted query. This is useful when the inserted query is text that the user entered,
+ * and the user would expect to be able to keep typing. <i>This parameter is only meaningful
+ * if initialQuery is a non-empty string.</i>
+ * @param launchActivity The ComponentName of the activity that has launched this search.
+ * @param appSearchData An application can insert application-specific
+ * context here, in order to improve quality or specificity of its own
+ * searches. This data will be returned with SEARCH intent(s). Null if
+ * no extra data is required.
+ * @param globalSearch If false, this will only launch the search that has been specifically
+ * defined by the application (which is usually defined as a local search). If no default
+ * search is defined in the current application or activity, no search will be launched.
+ * If true, this will always launch a platform-global (e.g. web-based) search instead.
+ *
+ * @see android.app.Activity#onSearchRequested
+ * @see #stopSearch
+ */
+ public void startSearch(String initialQuery,
+ boolean selectInitialQuery,
+ ComponentName launchActivity,
+ Bundle appSearchData,
+ boolean globalSearch) {
+
+ if (mSearchDialog == null) {
+ mSearchDialog = new SearchDialog(mContext);
+ }
+
+ // activate the search manager and start it up!
+ mSearchDialog.show(initialQuery, selectInitialQuery, launchActivity, appSearchData,
+ globalSearch);
+
+ mSearchDialog.setOnCancelListener(this);
+ mSearchDialog.setOnDismissListener(this);
+ }
+
+ /**
+ * Terminate search UI.
+ *
+ * <p>Typically the user will terminate the search UI by launching a
+ * search or by canceling. This function allows the underlying application
+ * or activity to cancel the search prematurely (for any reason).
+ *
+ * <p>This function can be safely called at any time (even if no search is active.)
+ *
+ * @see #startSearch
+ */
+ public void stopSearch() {
+ if (mSearchDialog != null) {
+ mSearchDialog.cancel();
+ }
+ }
+
+ /**
+ * Determine if the Search UI is currently displayed.
+ *
+ * This is provided primarily for application test purposes.
+ *
+ * @return Returns true if the search UI is currently displayed.
+ *
+ * @hide
+ */
+ public boolean isVisible() {
+ if (mSearchDialog != null) {
+ return mSearchDialog.isShowing();
+ }
+ return false;
+ }
+
+ /**
+ * See {@link #setOnDismissListener} for configuring your activity to monitor search UI state.
+ */
+ public interface OnDismissListener {
+ /**
+ * This method will be called when the search UI is dismissed. To make use if it, you must
+ * implement this method in your activity, and call {@link #setOnDismissListener} to
+ * register it.
+ */
+ public void onDismiss();
+ }
+
+ /**
+ * See {@link #setOnCancelListener} for configuring your activity to monitor search UI state.
+ */
+ public interface OnCancelListener {
+ /**
+ * This method will be called when the search UI is canceled. To make use if it, you must
+ * implement this method in your activity, and call {@link #setOnCancelListener} to
+ * register it.
+ */
+ public void onCancel();
+ }
+
+ /**
+ * Set or clear the callback that will be invoked whenever the search UI is dismissed.
+ *
+ * @param listener The {@link OnDismissListener} to use, or null.
+ */
+ public void setOnDismissListener(final OnDismissListener listener) {
+ mDismissListener = listener;
+ }
+
+ /**
+ * The callback from the search dialog when dismissed
+ * @hide
+ */
+ public void onDismiss(DialogInterface dialog) {
+ if (dialog == mSearchDialog) {
+ if (mDismissListener != null) {
+ mDismissListener.onDismiss();
+ }
+ }
+ }
+
+ /**
+ * Set or clear the callback that will be invoked whenever the search UI is canceled.
+ *
+ * @param listener The {@link OnCancelListener} to use, or null.
+ */
+ public void setOnCancelListener(final OnCancelListener listener) {
+ mCancelListener = listener;
+ }
+
+
+ /**
+ * The callback from the search dialog when canceled
+ * @hide
+ */
+ public void onCancel(DialogInterface dialog) {
+ if (dialog == mSearchDialog) {
+ if (mCancelListener != null) {
+ mCancelListener.onCancel();
+ }
+ }
+ }
+
+ /**
+ * Save instance state so we can recreate after a rotation.
+ *
+ * @hide
+ */
+ void saveSearchDialog(Bundle outState, String key) {
+ if (mSearchDialog != null && mSearchDialog.isShowing()) {
+ Bundle searchDialogState = mSearchDialog.onSaveInstanceState();
+ outState.putBundle(key, searchDialogState);
+ }
+ }
+
+ /**
+ * Restore instance state after a rotation.
+ *
+ * @hide
+ */
+ void restoreSearchDialog(Bundle inState, String key) {
+ Bundle searchDialogState = inState.getBundle(key);
+ if (searchDialogState != null) {
+ if (mSearchDialog == null) {
+ mSearchDialog = new SearchDialog(mContext);
+ }
+ mSearchDialog.onRestoreInstanceState(searchDialogState);
+ }
+ }
+
+ /**
+ * Hook for updating layout on a rotation
+ *
+ * @hide
+ */
+ void onConfigurationChanged(Configuration newConfig) {
+ if (mSearchDialog != null && mSearchDialog.isShowing()) {
+ mSearchDialog.onConfigurationChanged(newConfig);
+ }
+ }
+
+}
diff --git a/core/java/android/app/Service.java b/core/java/android/app/Service.java
new file mode 100644
index 0000000..28b0615
--- /dev/null
+++ b/core/java/android/app/Service.java
@@ -0,0 +1,378 @@
+/*
+ * 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.app;
+
+import android.content.ComponentCallbacks;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.ContextWrapper;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.os.RemoteException;
+import android.os.IBinder;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+/**
+ * A Service is an application component that runs in the background, not
+ * interacting with the user, for an indefinite period of time. Each service
+ * class must have a corresponding
+ * {@link android.R.styleable#AndroidManifestService &lt;service&gt;}
+ * declaration in its package's <code>AndroidManifest.xml</code>. Services
+ * can be started with
+ * {@link android.content.Context#startService Context.startService()} and
+ * {@link android.content.Context#bindService Context.bindService()}.
+ *
+ * <p>Note that services, like other application objects, run in the main
+ * thread of their hosting process. This means that, if your service is going
+ * to do any CPU intensive (such as MP3 playback) or blocking (such as
+ * networking) operations, it should spawn its own thread in which to do that
+ * work. More information on this can be found in the
+ * <a href="{@docRoot}intro/appmodel.html#Threads">Threading section of the
+ * Application Model overview</a>.</p>
+ *
+ * <p>The Service class is an important part of an
+ * <a href="{@docRoot}intro/lifecycle.html">application's overall lifecycle</a>.</p>
+ *
+ * <p>Topics covered here:
+ * <ol>
+ * <li><a href="#ServiceLifecycle">Service Lifecycle</a>
+ * <li><a href="#Permissions">Permissions</a>
+ * <li><a href="#ProcessLifecycle">Process Lifecycle</a>
+ * </ol>
+ *
+ * <a name="ServiceLifecycle"></a>
+ * <h3>Service Lifecycle</h3>
+ *
+ * <p>There are two reasons that a service can be run by the system. If someone
+ * calls {@link android.content.Context#startService Context.startService()} then the system will
+ * retrieve the service (creating it and calling its {@link #onCreate} method
+ * if needed) and then call its {@link #onStart} method with the
+ * arguments supplied by the client. The service will at this point continue
+ * running until {@link android.content.Context#stopService Context.stopService()} or
+ * {@link #stopSelf()} is called. Note that multiple calls to
+ * Context.startService() do not nest (though they do result in multiple corresponding
+ * calls to onStart()), so no matter how many times it is started a service
+ * will be stopped once Context.stopService() or stopSelf() is called.
+ *
+ * <p>Clients can also use {@link android.content.Context#bindService Context.bindService()} to
+ * obtain a persistent connection to a service. This likewise creates the
+ * service if it is not already running (calling {@link #onCreate} while
+ * doing so), but does not call onStart(). The client will receive the
+ * {@link android.os.IBinder} object that the service returns from its
+ * {@link #onBind} method, allowing the client to then make calls back
+ * to the service. The service will remain running as long as the connection
+ * is established (whether or not the client retains a reference on the
+ * service's IBinder). Usually the IBinder returned is for a complex
+ * interface that has been <a href="{@docRoot}reference/aidl.html">written
+ * in aidl</a>.
+ *
+ * <p>A service can be both started and have connections bound to it. In such
+ * a case, the system will keep the service running as long as either it is
+ * started <em>or</em> there are one or more connections to it with the
+ * {@link android.content.Context#BIND_AUTO_CREATE Context.BIND_AUTO_CREATE}
+ * flag. Once neither
+ * of these situations hold, the service's {@link #onDestroy} method is called
+ * and the service is effectively terminated. All cleanup (stopping threads,
+ * unregistering receivers) should be complete upon returning from onDestroy().
+ *
+ * <a name="Permissions"></a>
+ * <h3>Permissions</h3>
+ *
+ * <p>Global access to a service can be enforced when it is declared in its
+ * manifest's {@link android.R.styleable#AndroidManifestService &lt;service&gt;}
+ * tag. By doing so, other applications will need to declare a corresponding
+ * {@link android.R.styleable#AndroidManifestUsesPermission &lt;uses-permission&gt;}
+ * element in their own manifest to be able to start, stop, or bind to
+ * the service.
+ *
+ * <p>In addition, a service can protect individual IPC calls into it with
+ * permissions, by calling the
+ * {@link #checkCallingPermission}
+ * method before executing the implementation of that call.
+ *
+ * <p>See the <a href="{@docRoot}devel/security.html">Security Model</a>
+ * document for more information on permissions and security in general.
+ *
+ * <a name="ProcessLifecycle"></a>
+ * <h3>Process Lifecycle</h3>
+ *
+ * <p>The Android system will attempt to keep the process hosting a service
+ * around as long as the service has been started or has clients bound to it.
+ * When running low on memory and needing to kill existing processes, the
+ * priority of a process hosting the service will be the higher of the
+ * following possibilities:
+ *
+ * <ul>
+ * <li><p>If the service is currently executing code in its
+ * {@link #onCreate onCreate()}, {@link #onStart onStart()},
+ * or {@link #onDestroy onDestroy()} methods, then the hosting process will
+ * be a foreground process to ensure this code can execute without
+ * being killed.
+ * <li><p>If the service has been started, then its hosting process is considered
+ * to be less important than any processes that are currently visible to the
+ * user on-screen, but more important than any process not visible. Because
+ * only a few processes are generally visible to the user, this means that
+ * the service should not be killed except in extreme low memory conditions.
+ * <li><p>If there are clients bound to the service, then the service's hosting
+ * process is never less important than the most important client. That is,
+ * if one of its clients is visible to the user, then the service itself is
+ * considered to be visible.
+ * </ul>
+ *
+ * <p>Note this means that most of the time your service is running, it may
+ * be killed by the system if it is under heavy memory pressure. If this
+ * happens, the system will later try to restart the service. An important
+ * consequence of this is that if you implement {@link #onStart onStart()}
+ * to schedule work to be done asynchronously or in another thread, then you
+ * may want to write information about that work into persistent storage
+ * during the onStart() call so that it does not get lost if the service later
+ * gets killed.
+ *
+ * <p>Other application components running in the same process as the service
+ * (such as an {@link android.app.Activity}) can, of course, increase the
+ * importance of the overall
+ * process beyond just the importance of the service itself.
+ */
+public abstract class Service extends ContextWrapper implements ComponentCallbacks {
+ private static final String TAG = "Service";
+
+ public Service() {
+ super(null);
+ }
+
+ /** Return the application that owns this service. */
+ public final Application getApplication() {
+ return mApplication;
+ }
+
+ /**
+ * Called by the system when the service is first created. Do not call this method directly.
+ * If you override this method, be sure to call super.onCreate().
+ */
+ public void onCreate() {
+ }
+
+ /**
+ * Called by the system every time a client explicitly starts the service by calling
+ * {@link android.content.Context#startService}, providing the arguments it supplied and a
+ * unique integer token representing the start request. Do not call this method directly.
+ * If you override this method, be sure to call super.onStart().
+ *
+ * @param intent The Intent supplied to {@link android.content.Context#startService},
+ * as given.
+ * @param startId A unique integer representing this specific request to
+ * start. Use with {@link #stopSelfResult(int)}.
+ *
+ * @see #stopSelfResult(int)
+ */
+ public void onStart(Intent intent, int startId) {
+ }
+
+ /**
+ * Called by the system to notify a Service that it is no longer used and is being removed. The
+ * service should clean up an resources it holds (threads, registered
+ * receivers, etc) at this point. Upon return, there will be no more calls
+ * in to this Service object and it is effectively dead. Do not call this method directly.
+ * If you override this method, be sure to call super.onDestroy().
+ */
+ public void onDestroy() {
+ }
+
+ public void onConfigurationChanged(Configuration newConfig) {
+ }
+
+ public void onLowMemory() {
+ }
+
+ /**
+ * Return the communication channel to the service. May return null if
+ * clients can not bind to the service. The returned
+ * {@link android.os.IBinder} is usually for a complex interface
+ * that has been <a href="{@docRoot}reference/aidl.html">described using
+ * aidl</a>.
+ *
+ * <p><em>Note that unlike other application components, calls on to the
+ * IBinder interface returned here may not happen on the main thread
+ * of the process</em>. More information about this can be found
+ * in the <a href="{@docRoot}intro/appmodel.html#Threads">Threading section
+ * of the Application Model overview</a>.</p>
+ *
+ * @param intent The Intent that was used to bind to this service,
+ * as given to {@link android.content.Context#bindService
+ * Context.bindService}. Note that any extras that were included with
+ * the Intent at that point will <em>not</em> be seen here.
+ *
+ * @return Return an IBinder through which clients can call on to the
+ * service.
+ */
+ public abstract IBinder onBind(Intent intent);
+
+ /**
+ * Called when all clients have disconnected from a particular interface
+ * published by the service. The default implementation does nothing and
+ * returns false.
+ *
+ * @param intent The Intent that was used to bind to this service,
+ * as given to {@link android.content.Context#bindService
+ * Context.bindService}. Note that any extras that were included with
+ * the Intent at that point will <em>not</em> be seen here.
+ *
+ * @return Return true if you would like to have the service's
+ * {@link #onRebind} method later called when new clients bind to it.
+ */
+ public boolean onUnbind(Intent intent) {
+ return false;
+ }
+
+ /**
+ * Called when new clients have connected to the service, after it had
+ * previously been notified that all had disconnected in its
+ * {@link #onUnbind}. This will only be called if the implementation
+ * of {@link #onUnbind} was overridden to return true.
+ *
+ * @param intent The Intent that was used to bind to this service,
+ * as given to {@link android.content.Context#bindService
+ * Context.bindService}. Note that any extras that were included with
+ * the Intent at that point will <em>not</em> be seen here.
+ */
+ public void onRebind(Intent intent) {
+ }
+
+ /**
+ * Stop the service, if it was previously started. This is the same as
+ * calling {@link android.content.Context#stopService} for this particular service.
+ *
+ * @see #stopSelfResult(int)
+ */
+ public final void stopSelf() {
+ stopSelf(-1);
+ }
+
+ /**
+ * Old version of {@link #stopSelfResult} that doesn't return a result.
+ *
+ * @see #stopSelfResult
+ */
+ public final void stopSelf(int startId) {
+ if (mActivityManager == null) {
+ return;
+ }
+ try {
+ mActivityManager.stopServiceToken(
+ new ComponentName(this, mClassName), mToken, startId);
+ } catch (RemoteException ex) {
+ }
+ }
+
+ /**
+ * Stop the service, if the most recent time it was started was
+ * <var>startId</var>. This is the same as calling {@link
+ * android.content.Context#stopService} for this particular service but allows you to
+ * safely avoid stopping if there is a start request from a client that you
+ * haven't yet see in {@link #onStart}.
+ *
+ * @param startId The most recent start identifier received in {@link
+ * #onStart}.
+ * @return Returns true if the startId matches the last start request
+ * and the service will be stopped, else false.
+ *
+ * @see #stopSelf()
+ */
+ public final boolean stopSelfResult(int startId) {
+ if (mActivityManager == null) {
+ return false;
+ }
+ try {
+ return mActivityManager.stopServiceToken(
+ new ComponentName(this, mClassName), mToken, startId);
+ } catch (RemoteException ex) {
+ }
+ return false;
+ }
+
+ /**
+ * Control whether this service is considered to be a foreground service.
+ * By default services are background, meaning that if the system needs to
+ * kill them to reclaim more memory (such as to display a large page in a
+ * web browser), they can be killed without too much harm. You can set this
+ * flag if killing your service would be disruptive to the user: such as
+ * if your service is performing background music playback, so the user
+ * would notice if their music stopped playing.
+ *
+ * @param isForeground Determines whether this service is considered to
+ * be foreground (true) or background (false).
+ */
+ public final void setForeground(boolean isForeground) {
+ if (mActivityManager == null) {
+ return;
+ }
+ try {
+ mActivityManager.setServiceForeground(
+ new ComponentName(this, mClassName), mToken, isForeground);
+ } catch (RemoteException ex) {
+ }
+ }
+
+ /**
+ * Print the Service's state into the given stream.
+ *
+ * @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.
+ */
+ protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+ writer.println("nothing to dump");
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ super.finalize();
+ //Log.i("Service", "Finalizing Service: " + this);
+ }
+
+ // ------------------ Internal API ------------------
+
+ /**
+ * @hide
+ */
+ public final void attach(
+ Context context,
+ ActivityThread thread, String className, IBinder token,
+ Application application, Object activityManager) {
+ attachBaseContext(context);
+ mThread = thread; // NOTE: unused - remove?
+ mClassName = className;
+ mToken = token;
+ mApplication = application;
+ mActivityManager = (IActivityManager)activityManager;
+ }
+
+ final String getClassName() {
+ return mClassName;
+ }
+
+ // set by the thread after the constructor and before onCreate(Bundle icicle) is called.
+ private ActivityThread mThread = null;
+ private String mClassName = null;
+ private IBinder mToken = null;
+ private Application mApplication = null;
+ private IActivityManager mActivityManager = null;
+}
+
diff --git a/core/java/android/app/StatusBarManager.java b/core/java/android/app/StatusBarManager.java
new file mode 100644
index 0000000..51d7393
--- /dev/null
+++ b/core/java/android/app/StatusBarManager.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package android.app;
+
+import android.content.Context;
+import android.os.Binder;
+import android.os.RemoteException;
+import android.os.IBinder;
+import android.os.ServiceManager;
+
+/**
+ * Allows an app to control the status bar.
+ *
+ * @hide
+ */
+public class StatusBarManager {
+ /**
+ * Flag for {@link #disable} to make the status bar not expandable. Unless you also
+ * set {@link #DISABLE_NOTIFICATIONS}, new notifications will continue to show.
+ */
+ public static final int DISABLE_EXPAND = 0x00000001;
+
+ /**
+ * Flag for {@link #disable} to hide notification icons and ticker text.
+ */
+ public static final int DISABLE_NOTIFICATION_ICONS = 0x00000002;
+
+ /**
+ * Flag for {@link #disable} to disable incoming notification alerts. This will not block
+ * icons, but it will block sound, vibrating and other visual or aural notifications.
+ */
+ public static final int DISABLE_NOTIFICATION_ALERTS = 0x00000004;
+
+ /**
+ * Re-enable all of the status bar features that you've disabled.
+ */
+ public static final int DISABLE_NONE = 0x00000000;
+
+ private Context mContext;
+ private IStatusBar mService;
+ private IBinder mToken = new Binder();
+
+ StatusBarManager(Context context) {
+ mContext = context;
+ mService = IStatusBar.Stub.asInterface(
+ ServiceManager.getService(Context.STATUS_BAR_SERVICE));
+ }
+
+ /**
+ * Disable some features in the status bar. Pass the bitwise-or of the DISABLE_* flags.
+ * To re-enable everything, pass {@link #DISABLE_NONE}.
+ */
+ public void disable(int what) {
+ try {
+ mService.disable(what, mToken, mContext.getPackageName());
+ } catch (RemoteException ex) {
+ // system process is dead anyway.
+ throw new RuntimeException(ex);
+ }
+ }
+
+ /**
+ * Expand the status bar.
+ */
+ public void expand() {
+ try {
+ mService.activate();
+ } catch (RemoteException ex) {
+ // system process is dead anyway.
+ throw new RuntimeException(ex);
+ }
+ }
+
+ /**
+ * Collapse the status bar.
+ */
+ public void collapse() {
+ try {
+ mService.deactivate();
+ } catch (RemoteException ex) {
+ // system process is dead anyway.
+ throw new RuntimeException(ex);
+ }
+ }
+
+ /**
+ * Toggle the status bar.
+ */
+ public void toggle() {
+ try {
+ mService.toggle();
+ } catch (RemoteException ex) {
+ // system process is dead anyway.
+ throw new RuntimeException(ex);
+ }
+ }
+
+ public IBinder addIcon(String slot, int iconId, int iconLevel) {
+ try {
+ return mService.addIcon(slot, mContext.getPackageName(), iconId, iconLevel);
+ } catch (RemoteException ex) {
+ // system process is dead anyway.
+ throw new RuntimeException(ex);
+ }
+ }
+
+ public void updateIcon(IBinder key, String slot, int iconId, int iconLevel) {
+ try {
+ mService.updateIcon(key, slot, mContext.getPackageName(), iconId, iconLevel);
+ } catch (RemoteException ex) {
+ // system process is dead anyway.
+ throw new RuntimeException(ex);
+ }
+ }
+
+ public void removeIcon(IBinder key) {
+ try {
+ mService.removeIcon(key);
+ } catch (RemoteException ex) {
+ // system process is dead anyway.
+ throw new RuntimeException(ex);
+ }
+ }
+}
diff --git a/core/java/android/app/TabActivity.java b/core/java/android/app/TabActivity.java
new file mode 100644
index 0000000..033fa0c
--- /dev/null
+++ b/core/java/android/app/TabActivity.java
@@ -0,0 +1,148 @@
+/*
+ * 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.app;
+
+import android.os.Bundle;
+import android.view.View;
+import android.widget.TabHost;
+import android.widget.TabWidget;
+import android.widget.TextView;
+
+/**
+ * An activity that contains and runs multiple embedded activities or views.
+ */
+public class TabActivity extends ActivityGroup {
+ private TabHost mTabHost;
+ private String mDefaultTab = null;
+ private int mDefaultTabIndex = -1;
+
+ public TabActivity() {
+ }
+
+ /**
+ * Sets the default tab that is the first tab highlighted.
+ *
+ * @param tag the name of the default tab
+ */
+ public void setDefaultTab(String tag) {
+ mDefaultTab = tag;
+ mDefaultTabIndex = -1;
+ }
+
+ /**
+ * Sets the default tab that is the first tab highlighted.
+ *
+ * @param index the index of the default tab
+ */
+ public void setDefaultTab(int index) {
+ mDefaultTab = null;
+ mDefaultTabIndex = index;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Bundle state) {
+ super.onRestoreInstanceState(state);
+ ensureTabHost();
+ String cur = state.getString("currentTab");
+ if (cur != null) {
+ mTabHost.setCurrentTabByTag(cur);
+ }
+ if (mTabHost.getCurrentTab() < 0) {
+ if (mDefaultTab != null) {
+ mTabHost.setCurrentTabByTag(mDefaultTab);
+ } else if (mDefaultTabIndex >= 0) {
+ mTabHost.setCurrentTab(mDefaultTabIndex);
+ }
+ }
+ }
+
+ @Override
+ protected void onPostCreate(Bundle icicle) {
+ super.onPostCreate(icicle);
+
+ ensureTabHost();
+
+ if (mTabHost.getCurrentTab() == -1) {
+ mTabHost.setCurrentTab(0);
+ }
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ String currentTabTag = mTabHost.getCurrentTabTag();
+ if (currentTabTag != null) {
+ outState.putString("currentTab", currentTabTag);
+ }
+ }
+
+ /**
+ * Updates the screen state (current list and other views) when the
+ * content changes.
+ *
+ *@see Activity#onContentChanged()
+ */
+ @Override
+ public void onContentChanged() {
+ super.onContentChanged();
+ mTabHost = (TabHost) findViewById(com.android.internal.R.id.tabhost);
+
+ if (mTabHost == null) {
+ throw new RuntimeException(
+ "Your content must have a TabHost whose id attribute is " +
+ "'android.R.id.tabhost'");
+ }
+ mTabHost.setup(getLocalActivityManager());
+ }
+
+ private void ensureTabHost() {
+ if (mTabHost == null) {
+ this.setContentView(com.android.internal.R.layout.tab_content);
+ }
+ }
+
+ @Override
+ protected void
+ onChildTitleChanged(Activity childActivity, CharSequence title) {
+ // Dorky implementation until we can have multiple activities running.
+ if (getLocalActivityManager().getCurrentActivity() == childActivity) {
+ View tabView = mTabHost.getCurrentTabView();
+ if (tabView != null && tabView instanceof TextView) {
+ ((TextView) tabView).setText(title);
+ }
+ }
+ }
+
+ /**
+ * Returns the {@link TabHost} the activity is using to host its tabs.
+ *
+ * @return the {@link TabHost} the activity is using to host its tabs.
+ */
+ public TabHost getTabHost() {
+ ensureTabHost();
+ return mTabHost;
+ }
+
+ /**
+ * Returns the {@link TabWidget} the activity is using to draw the actual tabs.
+ *
+ * @return the {@link TabWidget} the activity is using to draw the actual tabs.
+ */
+ public TabWidget getTabWidget() {
+ return mTabHost.getTabWidget();
+ }
+}
diff --git a/core/java/android/app/TimePickerDialog.java b/core/java/android/app/TimePickerDialog.java
new file mode 100644
index 0000000..107532e
--- /dev/null
+++ b/core/java/android/app/TimePickerDialog.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.os.Bundle;
+import android.pim.DateFormat;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.TimePicker;
+import android.widget.TimePicker.OnTimeChangedListener;
+
+import com.android.internal.R;
+
+import java.util.Calendar;
+
+/**
+ * A dialog that prompts the user for the time of day using a {@link TimePicker}.
+ */
+public class TimePickerDialog extends AlertDialog implements OnClickListener,
+ OnTimeChangedListener {
+
+ /**
+ * The callback interface used to indicate the user is done filling in
+ * the time (they clicked on the 'Set' button).
+ */
+ public interface OnTimeSetListener {
+
+ /**
+ * @param view The view associated with this listener.
+ * @param hourOfDay The hour that was set.
+ * @param minute The minute that was set.
+ */
+ void onTimeSet(TimePicker view, int hourOfDay, int minute);
+ }
+
+ private static final String HOUR = "hour";
+ private static final String MINUTE = "minute";
+ private static final String IS_24_HOUR = "is24hour";
+
+ private final TimePicker mTimePicker;
+ private final OnTimeSetListener mCallback;
+ private final Calendar mCalendar;
+ private final java.text.DateFormat mDateFormat;
+
+ int mInitialHourOfDay;
+ int mInitialMinute;
+ boolean mIs24HourView;
+
+ /**
+ * @param context Parent.
+ * @param callBack How parent is notified.
+ * @param hourOfDay The initial hour.
+ * @param minute The initial minute.
+ * @param is24HourView Whether this is a 24 hour view, or AM/PM.
+ */
+ public TimePickerDialog(Context context,
+ OnTimeSetListener callBack,
+ int hourOfDay, int minute, boolean is24HourView) {
+ this(context, com.android.internal.R.style.Theme_Dialog_Alert,
+ callBack, hourOfDay, minute, is24HourView);
+ }
+
+ /**
+ * @param context Parent.
+ * @param theme the theme to apply to this dialog
+ * @param callBack How parent is notified.
+ * @param hourOfDay The initial hour.
+ * @param minute The initial minute.
+ * @param is24HourView Whether this is a 24 hour view, or AM/PM.
+ */
+ public TimePickerDialog(Context context,
+ int theme,
+ OnTimeSetListener callBack,
+ int hourOfDay, int minute, boolean is24HourView) {
+ super(context, theme);
+ mCallback = callBack;
+ mInitialHourOfDay = hourOfDay;
+ mInitialMinute = minute;
+ mIs24HourView = is24HourView;
+
+ mDateFormat = DateFormat.getTimeFormat(context);
+ mCalendar = Calendar.getInstance();
+ updateTitle(mInitialHourOfDay, mInitialMinute);
+
+ setButton(context.getText(R.string.date_time_set), this);
+ setButton2(context.getText(R.string.cancel), (OnClickListener) null);
+ setIcon(R.drawable.ic_dialog_time);
+
+ LayoutInflater inflater =
+ (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View view = inflater.inflate(R.layout.time_picker_dialog, null);
+ setView(view);
+ mTimePicker = (TimePicker) view.findViewById(R.id.timePicker);
+
+ // initialize state
+ mTimePicker.setCurrentHour(mInitialHourOfDay);
+ mTimePicker.setCurrentMinute(mInitialMinute);
+ mTimePicker.setIs24HourView(mIs24HourView);
+ mTimePicker.setOnTimeChangedListener(this);
+ }
+
+ public void onClick(DialogInterface dialog, int which) {
+ if (mCallback != null) {
+ mTimePicker.clearFocus();
+ mCallback.onTimeSet(mTimePicker, mTimePicker.getCurrentHour(),
+ mTimePicker.getCurrentMinute());
+ }
+ }
+
+ public void onTimeChanged(TimePicker view, int hourOfDay, int minute) {
+ updateTitle(hourOfDay, minute);
+ }
+
+ public void updateTime(int hourOfDay, int minutOfHour) {
+ mTimePicker.setCurrentHour(hourOfDay);
+ mTimePicker.setCurrentMinute(minutOfHour);
+ }
+
+ private void updateTitle(int hour, int minute) {
+ mCalendar.set(Calendar.HOUR_OF_DAY, hour);
+ mCalendar.set(Calendar.MINUTE, minute);
+ setTitle(mDateFormat.format(mCalendar.getTime()));
+ }
+
+ @Override
+ public Bundle onSaveInstanceState() {
+ Bundle state = super.onSaveInstanceState();
+ state.putInt(HOUR, mTimePicker.getCurrentHour());
+ state.putInt(MINUTE, mTimePicker.getCurrentMinute());
+ state.putBoolean(IS_24_HOUR, mTimePicker.is24HourView());
+ return state;
+ }
+
+ @Override
+ public void onRestoreInstanceState(Bundle savedInstanceState) {
+ super.onRestoreInstanceState(savedInstanceState);
+ int hour = savedInstanceState.getInt(HOUR);
+ int minute = savedInstanceState.getInt(MINUTE);
+ mTimePicker.setCurrentHour(hour);
+ mTimePicker.setCurrentMinute(minute);
+ mTimePicker.setIs24HourView(savedInstanceState.getBoolean(IS_24_HOUR));
+ mTimePicker.setOnTimeChangedListener(this);
+ updateTitle(hour, minute);
+ }
+}
diff --git a/core/java/android/app/package.html b/core/java/android/app/package.html
new file mode 100644
index 0000000..048ee93
--- /dev/null
+++ b/core/java/android/app/package.html
@@ -0,0 +1,72 @@
+<html>
+<head>
+<script type="text/javascript" src="http://www.corp.google.com/style/prettify.js"></script>
+<script src="http://www.corp.google.com/eng/techpubs/include/navbar.js" type="text/javascript"></script>
+</head>
+
+<body>
+
+<p>High-level classes encapsulating the overall Android application model.
+The central class is {@link android.app.Activity}, with other top-level
+application components being defined by {@link android.app.Service} and,
+from the {@link android.content} package, {@link android.content.BroadcastReceiver}
+and {@link android.content.ContentProvider}. It also includes application
+tools, such as dialogs and notifications.</p>
+
+<p>This package builds on top of the lower-level Android packages
+{@link android.widget}, {@link android.view}, {@link android.content},
+{@link android.text}, {@link android.graphics}, {@link android.os}, and
+{@link android.util}.</p>
+
+<p>An {@link android.app.Activity Activity} is a specific operation the
+user can perform, generally corresponding
+to one screen in the user interface.
+It is the basic building block of an Android application.
+Examples of activities are "view the
+list of people," "view the details of a person," "edit information about
+a person," "view an image," etc. Switching from one activity to another
+generally implies adding a new entry on the navigation history; that is,
+going "back" means moving to the previous activity you were doing.</p>
+
+<p>A set of related activities can be grouped together as a "task". Until
+a new task is explicitly specified, all activites you start are considered
+to be part of the current task. While the only way to navigate between
+individual activities is by going "back" in the history stack, the group
+of activities in a task can be moved in relation to other tasks: for example
+to the front or the back of the history stack. This mechanism can be used
+to present to the user a list of things they have been doing, moving
+between them without disrupting previous work.
+</p>
+
+<p>A complete "application" is a set of activities that allow the user to do a
+cohesive group of operations -- such as working with contacts, working with a
+calendar, messaging, etc. Though there can be a custom application object
+associated with a set of activities, in many cases this is not needed --
+each activity provides a particular path into one of the various kinds of
+functionality inside of the application, serving as its on self-contained
+"mini application".
+</p>
+
+<p>This approach allows an application to be broken into pieces, which
+can be reused and replaced in a variety of ways. Consider, for example,
+a "camera application." There are a number of things this application
+must do, each of which is provided by a separate activity: take a picture
+(creating a new image), browse through the existing images, display a
+specific image, etc. If the "contacts application" then wants to let the
+user associate an image with a person, it can simply launch the existing
+"take a picture" or "select an image" activity that is part of the camera
+application and attach the picture it gets back.
+</p>
+
+<p>Note that there is no hard relationship between tasks the user sees and
+applications the developer writes. A task can be composed of activities from
+multiple applications (such as the contact application using an activity in
+the camera application to get a picture for a person), and multiple active
+tasks may be running for the same application (such as editing e-mail messages
+to two different people). The way tasks are organized is purely a UI policy
+decided by the system; for example, typically a new task is started when the
+user goes to the application launcher and selects an application.
+</p>
+
+</body>
+</html>
diff --git a/core/java/android/bluetooth/AtCommandHandler.java b/core/java/android/bluetooth/AtCommandHandler.java
new file mode 100644
index 0000000..8de2133
--- /dev/null
+++ b/core/java/android/bluetooth/AtCommandHandler.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+import android.bluetooth.AtCommandResult;
+
+/**
+ * Handler Interface for {@link AtParser}.<p>
+ * @hide
+ */
+public abstract class AtCommandHandler {
+
+ /**
+ * Handle Basic commands "ATA".<p>
+ * These are single letter commands such as ATA and ATD. Anything following
+ * the single letter command ('A' and 'D' respectively) will be passed as
+ * 'arg'.<p>
+ * For example, "ATDT1234" would result in the call
+ * handleBasicCommand("T1234").<p>
+ * @param arg Everything following the basic command character.
+ * @return The result of this command.
+ */
+ public AtCommandResult handleBasicCommand(String arg) {
+ return new AtCommandResult(AtCommandResult.ERROR);
+ }
+
+ /**
+ * Handle Actions command "AT+FOO".<p>
+ * Action commands are part of the Extended command syntax, and are
+ * typically used to signal an action on "FOO".<p>
+ * @return The result of this command.
+ */
+ public AtCommandResult handleActionCommand() {
+ return new AtCommandResult(AtCommandResult.ERROR);
+ }
+
+ /**
+ * Handle Read command "AT+FOO?".<p>
+ * Read commands are part of the Extended command syntax, and are
+ * typically used to read the value of "FOO".<p>
+ * @return The result of this command.
+ */
+ public AtCommandResult handleReadCommand() {
+ return new AtCommandResult(AtCommandResult.ERROR);
+ }
+
+ /**
+ * Handle Set command "AT+FOO=...".<p>
+ * Set commands are part of the Extended command syntax, and are
+ * typically used to set the value of "FOO". Multiple arguments can be
+ * sent.<p>
+ * AT+FOO=[<arg1>[,<arg2>[,...]]]<p>
+ * Each argument will be either numeric (Integer) or String.
+ * handleSetCommand is passed a generic Object[] array in which each
+ * element will be an Integer (if it can be parsed with parseInt()) or
+ * String.<p>
+ * Missing arguments ",," are set to empty Strings.<p>
+ * @param args Array of String and/or Integer's. There will always be at
+ * least one element in this array.
+ * @return The result of this command.
+ */
+ // Typically used to set this paramter
+ public AtCommandResult handleSetCommand(Object[] args) {
+ return new AtCommandResult(AtCommandResult.ERROR);
+ }
+
+ /**
+ * Handle Test command "AT+FOO=?".<p>
+ * Test commands are part of the Extended command syntax, and are typically
+ * used to request an indication of the range of legal values that "FOO"
+ * can take.<p>
+ * By defualt we return an OK result, to indicate that this command is at
+ * least recognized.<p>
+ * @return The result of this command.
+ */
+ public AtCommandResult handleTestCommand() {
+ return new AtCommandResult(AtCommandResult.OK);
+ }
+}
diff --git a/core/java/android/bluetooth/AtCommandResult.java b/core/java/android/bluetooth/AtCommandResult.java
new file mode 100644
index 0000000..638be2d
--- /dev/null
+++ b/core/java/android/bluetooth/AtCommandResult.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+import java.util.*;
+
+/**
+ * The result of execution of an single AT command.<p>
+ *
+ *
+ * This class can represent the final response to an AT command line, and also
+ * intermediate responses to a single command within a chained AT command
+ * line.<p>
+ *
+ * The actual responses that are intended to be send in reply to the AT command
+ * line are stored in a string array. The final response is stored as an
+ * int enum, converted to a string when toString() is called. Only a single
+ * final response is sent from multiple commands chained into a single command
+ * line.<p>
+ * @hide
+ */
+public class AtCommandResult {
+ // Result code enumerations
+ public static final int OK = 0;
+ public static final int ERROR = 1;
+ public static final int UNSOLICITED = 2;
+
+ private static final String OK_STRING = "OK";
+ private static final String ERROR_STRING = "ERROR";
+
+ private int mResultCode; // Result code
+ private StringBuilder mResponse; // Response with CRLF line breaks
+
+ /**
+ * Construct a new AtCommandResult with given result code, and an empty
+ * response array.
+ * @param resultCode One of OK, ERROR or UNSOLICITED.
+ */
+ public AtCommandResult(int resultCode) {
+ mResultCode = resultCode;
+ mResponse = new StringBuilder();
+ }
+
+ /**
+ * Construct a new AtCommandResult with result code OK, and the specified
+ * single line response.
+ * @param response The single line response.
+ */
+ public AtCommandResult(String response) {
+ this(OK);
+ addResponse(response);
+ }
+
+ public int getResultCode() {
+ return mResultCode;
+ }
+
+ /**
+ * Add another line to the response.
+ */
+ public void addResponse(String response) {
+ appendWithCrlf(mResponse, response);
+ }
+
+ /**
+ * Add the given result into this AtCommandResult object.<p>
+ * Used to combine results from multiple commands in a single command line
+ * (command chaining).
+ * @param result The AtCommandResult to add to this result.
+ */
+ public void addResult(AtCommandResult result) {
+ if (result != null) {
+ appendWithCrlf(mResponse, result.mResponse.toString());
+ mResultCode = result.mResultCode;
+ }
+ }
+
+ /**
+ * Generate the string response ready to send
+ */
+ public String toString() {
+ StringBuilder result = new StringBuilder(mResponse.toString());
+ switch (mResultCode) {
+ case OK:
+ appendWithCrlf(result, OK_STRING);
+ break;
+ case ERROR:
+ appendWithCrlf(result, ERROR_STRING);
+ break;
+ }
+ return result.toString();
+ }
+
+ /** Append a string to a string builder, joining with a double
+ * CRLF. Used to create multi-line AT command replies
+ */
+ public static void appendWithCrlf(StringBuilder str1, String str2) {
+ if (str1.length() > 0 && str2.length() > 0) {
+ str1.append("\r\n\r\n");
+ }
+ str1.append(str2);
+ }
+};
diff --git a/core/java/android/bluetooth/AtParser.java b/core/java/android/bluetooth/AtParser.java
new file mode 100644
index 0000000..1ea3150
--- /dev/null
+++ b/core/java/android/bluetooth/AtParser.java
@@ -0,0 +1,370 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+import android.bluetooth.AtCommandHandler;
+import android.bluetooth.AtCommandResult;
+
+import java.util.*;
+
+/**
+ * An AT (Hayes command) Parser based on (a subset of) the ITU-T V.250 standard.
+ * <p>
+ *
+ * Conforment with the subset of V.250 required for implementation of the
+ * Bluetooth Headset and Handsfree Profiles, as per Bluetooth SIP
+ * specifications. Also implements some V.250 features not required by
+ * Bluetooth - such as chained commands.<p>
+ *
+ * Command handlers are registered with an AtParser object. These handlers are
+ * invoked when command lines are processed by AtParser's process() method.<p>
+ *
+ * The AtParser object accepts a new command line to parse via its process()
+ * method. It breaks each command line into one or more commands. Each command
+ * is parsed for name, type, and (optional) arguments, and an appropriate
+ * external handler method is called through the AtCommandHandler interface.
+ *
+ * The command types are<ul>
+ * <li>Basic Command. For example "ATDT1234567890". Basic command names are a
+ * single character (e.g. "D"), and everything following this character is
+ * passed to the handler as a string argument (e.g. "T1234567890").
+ * <li>Action Command. For example "AT+CIMI". The command name is "CIMI", and
+ * there are no arguments for action commands.
+ * <li>Read Command. For example "AT+VGM?". The command name is "VGM", and there
+ * are no arguments for get commands.
+ * <li>Set Command. For example "AT+VGM=14". The command name is "VGM", and
+ * there is a single integer argument in this case. In the general case then
+ * can be zero or more arguments (comma deliminated) each of integer or string
+ * form.
+ * <li>Test Command. For example "AT+VGM=?. No arguments.
+ * </ul>
+ *
+ * In V.250 the last four command types are known as Extended Commands, and
+ * they are used heavily in Bluetooth.<p>
+ *
+ * Basic commands cannot be chained in this implementation. For Bluetooth
+ * headset/handsfree use this is acceptable, because they only use the basic
+ * commands ATA and ATD, which are not allowed to be chained. For general V.250
+ * use we would need to improve this class to allow Basic command chaining -
+ * however its tricky to get right becuase there is no deliminator for Basic
+ * command chaining.<p>
+ *
+ * Extended commands can be chained. For example:<p>
+ * AT+VGM?;+VGM=14;+CIMI<p>
+ * This is equivalent to:<p>
+ * AT+VGM?
+ * AT+VGM=14
+ * AT+CIMI
+ * Except that only one final result code is return (although several
+ * intermediate responses may be returned), and as soon as one command in the
+ * chain fails the rest are abandonded.<p>
+ *
+ * Handlers are registered by there command name via register(Char c, ...) or
+ * register(String s, ...). Handlers for Basic command should be registered by
+ * the basic command character, and handlers for Extended commands should be
+ * registered by String.<p>
+ *
+ * Refer to:<ul>
+ * <li>ITU-T Recommendation V.250
+ * <li>ETSI TS 127.007 (AT Comannd set for User Equipment, 3GPP TS 27.007)
+ * <li>Bluetooth Headset Profile Spec (K6)
+ * <li>Bluetooth Handsfree Profile Spec (HFP 1.5)
+ * </ul>
+ * @hide
+ */
+public class AtParser {
+
+ // Extended command type enumeration, only used internally
+ private static final int TYPE_ACTION = 0; // AT+FOO
+ private static final int TYPE_READ = 1; // AT+FOO?
+ private static final int TYPE_SET = 2; // AT+FOO=
+ private static final int TYPE_TEST = 3; // AT+FOO=?
+
+ private HashMap<String, AtCommandHandler> mExtHandlers;
+ private HashMap<Character, AtCommandHandler> mBasicHandlers;
+
+ private String mLastInput; // for "A/" (repeat last command) support
+
+ /**
+ * Create a new AtParser.<p>
+ * No handlers are registered.
+ */
+ public AtParser() {
+ mBasicHandlers = new HashMap<Character, AtCommandHandler>();
+ mExtHandlers = new HashMap<String, AtCommandHandler>();
+ mLastInput = "";
+ }
+
+ /**
+ * Register a Basic command handler.<p>
+ * Basic command handlers are later called via their
+ * <code>handleBasicCommand(String args)</code> method.
+ * @param command Command name - a single character
+ * @param handler Handler to register
+ */
+ public void register(Character command, AtCommandHandler handler) {
+ mBasicHandlers.put(command, handler);
+ }
+
+ /**
+ * Register an Extended command handler.<p>
+ * Extended command handlers are later called via:<ul>
+ * <li><code>handleActionCommand()</code>
+ * <li><code>handleGetCommand()</code>
+ * <li><code>handleSetCommand()</code>
+ * <li><code>handleTestCommand()</code>
+ * </ul>
+ * Only one method will be called for each command processed.
+ * @param command Command name - can be multiple characters
+ * @param handler Handler to register
+ */
+ public void register(String command, AtCommandHandler handler) {
+ mExtHandlers.put(command, handler);
+ }
+
+
+ /**
+ * Strip input of whitespace and force Uppercase - except sections inside
+ * quotes. Also fixes unmatched quotes (by appending a quote). Double
+ * quotes " are the only quotes allowed by V.250
+ */
+ static private String clean(String input) {
+ StringBuilder out = new StringBuilder(input.length());
+
+ for (int i = 0; i < input.length(); i++) {
+ char c = input.charAt(i);
+ if (c == '"') {
+ int j = input.indexOf('"', i + 1 ); // search for closing "
+ if (j == -1) { // unmatched ", insert one.
+ out.append(input.substring(i, input.length()));
+ out.append('"');
+ break;
+ }
+ out.append(input.substring(i, j + 1));
+ i = j;
+ } else if (c != ' ') {
+ out.append(Character.toUpperCase(c));
+ }
+ }
+
+ return out.toString();
+ }
+
+ static private boolean isAtoZ(char c) {
+ return (c >= 'A' && c <= 'Z');
+ }
+
+ /**
+ * Find a character ch, ignoring quoted sections.
+ * Return input.length() if not found.
+ */
+ static private int findChar(char ch, String input, int fromIndex) {
+ for (int i = fromIndex; i < input.length(); i++) {
+ char c = input.charAt(i);
+ if (c == '"') {
+ i = input.indexOf('"', i + 1);
+ if (i == -1) {
+ return input.length();
+ }
+ } else if (c == ch) {
+ return i;
+ }
+ }
+ return input.length();
+ }
+
+ /**
+ * Break an argument string into individual arguments (comma deliminated).
+ * Integer arguments are turned into Integer objects. Otherwise a String
+ * object is used.
+ */
+ static private Object[] generateArgs(String input) {
+ int i = 0;
+ int j;
+ ArrayList<Object> out = new ArrayList<Object>();
+ while (i <= input.length()) {
+ j = findChar(',', input, i);
+
+ String arg = input.substring(i, j);
+ try {
+ out.add(new Integer(arg));
+ } catch (NumberFormatException e) {
+ out.add(arg);
+ }
+
+ i = j + 1; // move past comma
+ }
+ return out.toArray();
+ }
+
+ /**
+ * Return the index of the end of character after the last characeter in
+ * the extended command name. Uses the V.250 spec for allowed command
+ * names.
+ */
+ static private int findEndExtendedName(String input, int index) {
+ for (int i = index; i < input.length(); i++) {
+ char c = input.charAt(i);
+
+ // V.250 defines the following chars as legal extended command
+ // names
+ if (isAtoZ(c)) continue;
+ if (c >= '0' && c <= '9') continue;
+ switch (c) {
+ case '!':
+ case '%':
+ case '-':
+ case '.':
+ case '/':
+ case ':':
+ case '_':
+ continue;
+ default:
+ return i;
+ }
+ }
+ return input.length();
+ }
+
+ /**
+ * Processes an incoming AT command line.<p>
+ * This method will invoke zero or one command handler methods for each
+ * command in the command line.<p>
+ * @param raw_input The AT input, without EOL deliminator (e.g. <CR>).
+ * @return Result object for this command line. This can be
+ * converted to a String[] response with toStrings().
+ */
+ public AtCommandResult process(String raw_input) {
+ String input = clean(raw_input);
+
+ // Handle "A/" (repeat previous line)
+ if (input.regionMatches(0, "A/", 0, 2)) {
+ input = new String(mLastInput);
+ } else {
+ mLastInput = new String(input);
+ }
+
+ // Handle empty line - no response necessary
+ if (input.equals("")) {
+ // Return []
+ return new AtCommandResult(AtCommandResult.UNSOLICITED);
+ }
+
+ // Anything else deserves an error
+ if (!input.regionMatches(0, "AT", 0, 2)) {
+ // Return ["ERROR"]
+ return new AtCommandResult(AtCommandResult.ERROR);
+ }
+
+ // Ok we have a command that starts with AT. Process it
+ int index = 2;
+ AtCommandResult result =
+ new AtCommandResult(AtCommandResult.UNSOLICITED);
+ while (index < input.length()) {
+ char c = input.charAt(index);
+
+ if (isAtoZ(c)) {
+ // Option 1: Basic Command
+ // Pass the rest of the line as is to the handler. Do not
+ // look for any more commands on this line.
+ String args = input.substring(index + 1);
+ if (mBasicHandlers.containsKey((Character)c)) {
+ result.addResult(mBasicHandlers.get(
+ (Character)c).handleBasicCommand(args));
+ return result;
+ } else {
+ // no handler
+ result.addResult(
+ new AtCommandResult(AtCommandResult.ERROR));
+ return result;
+ }
+ // control never reaches here
+ }
+
+ if (c == '+') {
+ // Option 2: Extended Command
+ // Search for first non-name character. Shortcircuit if we dont
+ // handle this command name.
+ int i = findEndExtendedName(input, index + 1);
+ String commandName = input.substring(index, i);
+ if (!mExtHandlers.containsKey(commandName)) {
+ // no handler
+ result.addResult(
+ new AtCommandResult(AtCommandResult.ERROR));
+ return result;
+ }
+ AtCommandHandler handler = mExtHandlers.get(commandName);
+
+ // Search for end of this command - this is usually the end of
+ // line
+ int endIndex = findChar(';', input, index);
+
+ // Determine what type of command this is.
+ // Default to TYPE_ACTION if we can't find anything else
+ // obvious.
+ int type;
+
+ if (i >= endIndex) {
+ type = TYPE_ACTION;
+ } else if (input.charAt(i) == '?') {
+ type = TYPE_READ;
+ } else if (input.charAt(i) == '=') {
+ if (i + 1 < endIndex) {
+ if (input.charAt(i + 1) == '?') {
+ type = TYPE_TEST;
+ } else {
+ type = TYPE_SET;
+ }
+ } else {
+ type = TYPE_SET;
+ }
+ } else {
+ type = TYPE_ACTION;
+ }
+
+ // Call this command. Short-circuit as soon as a command fails
+ switch (type) {
+ case TYPE_ACTION:
+ result.addResult(handler.handleActionCommand());
+ break;
+ case TYPE_READ:
+ result.addResult(handler.handleReadCommand());
+ break;
+ case TYPE_TEST:
+ result.addResult(handler.handleTestCommand());
+ break;
+ case TYPE_SET:
+ Object[] args =
+ generateArgs(input.substring(i + 1, endIndex));
+ result.addResult(handler.handleSetCommand(args));
+ break;
+ }
+ if (result.getResultCode() != AtCommandResult.OK) {
+ return result; // short-circuit
+ }
+
+ index = endIndex;
+ } else {
+ // Can't tell if this is a basic or extended command.
+ // Push forwards and hope we hit something.
+ index++;
+ }
+ }
+ // Finished processing (and all results were ok)
+ return result;
+ }
+}
diff --git a/core/java/android/bluetooth/BluetoothAudioGateway.java b/core/java/android/bluetooth/BluetoothAudioGateway.java
new file mode 100644
index 0000000..f3afd2a
--- /dev/null
+++ b/core/java/android/bluetooth/BluetoothAudioGateway.java
@@ -0,0 +1,190 @@
+package android.bluetooth;
+
+import java.lang.Thread;
+
+import android.os.Message;
+import android.os.Handler;
+import android.util.Log;
+
+/**
+ * Listen's for incoming RFCOMM connection for the headset / handsfree service.
+ *
+ * This class is planned for deletion, in favor of a generic Rfcomm class.
+ *
+ * @hide
+ */
+public class BluetoothAudioGateway {
+ private static final String TAG = "BT Audio Gateway";
+ private static final boolean DBG = false;
+
+ private int mNativeData;
+ static { classInitNative(); }
+
+ private BluetoothDevice mBluetooth;
+
+ /* in */
+ private int mHandsfreeAgRfcommChannel = -1;
+ private int mHeadsetAgRfcommChannel = -1;
+
+ /* out */
+ private String mConnectingHeadsetAddress;
+ private int mConnectingHeadsetRfcommChannel; /* -1 when not connected */
+ private int mConnectingHeadsetSocketFd;
+ private String mConnectingHandsfreeAddress;
+ private int mConnectingHandsfreeRfcommChannel; /* -1 when not connected */
+ private int mConnectingHandsfreeSocketFd;
+ private int mTimeoutRemainingMs; /* in/out */
+
+ public static final int DEFAULT_HF_AG_CHANNEL = 10;
+ public static final int DEFAULT_HS_AG_CHANNEL = 11;
+
+ public BluetoothAudioGateway(BluetoothDevice bluetooth) {
+ this(bluetooth, DEFAULT_HF_AG_CHANNEL, DEFAULT_HS_AG_CHANNEL);
+ }
+
+ public BluetoothAudioGateway(BluetoothDevice bluetooth,
+ int handsfreeAgRfcommChannel,
+ int headsetAgRfcommChannel) {
+ mBluetooth = bluetooth;
+ mHandsfreeAgRfcommChannel = handsfreeAgRfcommChannel;
+ mHeadsetAgRfcommChannel = headsetAgRfcommChannel;
+ initializeNativeDataNative();
+ }
+
+ private Thread mConnectThead;
+ private volatile boolean mInterrupted;
+ private static final int SELECT_WAIT_TIMEOUT = 1000;
+
+ private Handler mCallback;
+
+ public class IncomingConnectionInfo {
+ IncomingConnectionInfo(BluetoothDevice bluetooth, String address, int socketFd,
+ int rfcommChan) {
+ mBluetooth = bluetooth;
+ mAddress = address;
+ mSocketFd = socketFd;
+ mRfcommChan = rfcommChan;
+ }
+
+ public BluetoothDevice mBluetooth;
+ public String mAddress;
+ public int mSocketFd;
+ public int mRfcommChan;
+ }
+
+ public static final int MSG_INCOMING_HEADSET_CONNECTION = 100;
+ public static final int MSG_INCOMING_HANDSFREE_CONNECTION = 101;
+
+ public synchronized boolean start(Handler callback) {
+
+ if (mConnectThead == null) {
+ mCallback = callback;
+ mConnectThead = new Thread(TAG) {
+ public void run() {
+ if (DBG) log("Connect Thread starting");
+ while (!mInterrupted) {
+ //Log.i(TAG, "waiting for connect");
+ mConnectingHeadsetRfcommChannel = -1;
+ mConnectingHandsfreeRfcommChannel = -1;
+ if (waitForHandsfreeConnectNative(SELECT_WAIT_TIMEOUT) == false) {
+ if (mTimeoutRemainingMs > 0) {
+ try {
+ Log.i(TAG, "select thread timed out, but " +
+ mTimeoutRemainingMs + "ms of waiting remain.");
+ Thread.sleep(mTimeoutRemainingMs);
+ } catch (InterruptedException e) {
+ Log.i(TAG, "select thread was interrupted (2), exiting");
+ mInterrupted = true;
+ }
+ }
+ }
+ else {
+ Log.i(TAG, "connect notification!");
+ /* A device connected (most likely just one, but
+ it is possible for two separate devices, one
+ a headset and one a handsfree, to connect
+ simultaneously.
+ */
+ if (mConnectingHeadsetRfcommChannel >= 0) {
+ Log.i(TAG, "Incoming connection from headset " +
+ mConnectingHeadsetAddress + " on channel " +
+ mConnectingHeadsetRfcommChannel);
+ Message msg = Message.obtain(mCallback);
+ msg.what = MSG_INCOMING_HEADSET_CONNECTION;
+ msg.obj =
+ new IncomingConnectionInfo(
+ mBluetooth,
+ mConnectingHeadsetAddress,
+ mConnectingHeadsetSocketFd,
+ mConnectingHeadsetRfcommChannel);
+ msg.sendToTarget();
+ }
+ if (mConnectingHandsfreeRfcommChannel >= 0) {
+ Log.i(TAG, "Incoming connection from handsfree " +
+ mConnectingHandsfreeAddress + " on channel " +
+ mConnectingHandsfreeRfcommChannel);
+ Message msg = Message.obtain();
+ msg.setTarget(mCallback);
+ msg.what = MSG_INCOMING_HANDSFREE_CONNECTION;
+ msg.obj =
+ new IncomingConnectionInfo(
+ mBluetooth,
+ mConnectingHandsfreeAddress,
+ mConnectingHandsfreeSocketFd,
+ mConnectingHandsfreeRfcommChannel);
+ msg.sendToTarget();
+ }
+ }
+ }
+ if (DBG) log("Connect Thread finished");
+ }
+ };
+
+ if (setUpListeningSocketsNative() == false) {
+ Log.e(TAG, "Could not set up listening socket, exiting");
+ return false;
+ }
+
+ mInterrupted = false;
+ mConnectThead.start();
+ }
+
+ return true;
+ }
+
+ public synchronized void stop() {
+ if (mConnectThead != null) {
+ if (DBG) log("stopping Connect Thread");
+ mInterrupted = true;
+ try {
+ mConnectThead.interrupt();
+ if (DBG) log("waiting for thread to terminate");
+ mConnectThead.join();
+ mConnectThead = null;
+ mCallback = null;
+ tearDownListeningSocketsNative();
+ } catch (InterruptedException e) {
+ Log.w(TAG, "Interrupted waiting for Connect Thread to join");
+ }
+ }
+ }
+
+ protected void finalize() throws Throwable {
+ try {
+ cleanupNativeDataNative();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ private static native void classInitNative();
+ private native void initializeNativeDataNative();
+ private native void cleanupNativeDataNative();
+ private native boolean waitForHandsfreeConnectNative(int timeoutMs);
+ private native boolean setUpListeningSocketsNative();
+ private native void tearDownListeningSocketsNative();
+
+ private static void log(String msg) {
+ Log.d(TAG, msg);
+ }
+}
diff --git a/core/java/android/bluetooth/BluetoothDevice.java b/core/java/android/bluetooth/BluetoothDevice.java
new file mode 100644
index 0000000..0b24db6
--- /dev/null
+++ b/core/java/android/bluetooth/BluetoothDevice.java
@@ -0,0 +1,601 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.io.UnsupportedEncodingException;
+
+/**
+ * The Android Bluetooth API is not finalized, and *will* change. Use at your
+ * own risk.
+ *
+ * Manages the local Bluetooth device. Scan for devices, create bondings,
+ * power up and down the adapter.
+ *
+ * @hide
+ */
+public class BluetoothDevice {
+ public static final int MODE_UNKNOWN = -1;
+ public static final int MODE_OFF = 0;
+ public static final int MODE_CONNECTABLE = 1;
+ public static final int MODE_DISCOVERABLE = 2;
+
+ public static final int RESULT_FAILURE = -1;
+ public static final int RESULT_SUCCESS = 0;
+
+ private static final String TAG = "BluetoothDevice";
+
+ private final IBluetoothDevice mService;
+ /**
+ * @hide - hide this because it takes a parameter of type
+ * IBluetoothDevice, which is a System private class.
+ * Also note that Context.getSystemService is a factory that
+ * returns a BlueToothDevice. That is the right way to get
+ * a BluetoothDevice.
+ */
+ public BluetoothDevice(IBluetoothDevice service) {
+ mService = service;
+ }
+
+ /**
+ * Get the current status of Bluetooth hardware.
+ *
+ * @return true if Bluetooth enabled, false otherwise.
+ */
+ public boolean isEnabled() {
+ try {
+ return mService.isEnabled();
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ return false;
+ }
+
+ /**
+ * Enable the Bluetooth device.
+ * Turn on the underlying hardware.
+ * This is an asynchronous call, BluetoothIntent.ENABLED_ACTION will be
+ * sent if and when the device is successfully enabled.
+ * @return false if we cannot enable the Bluetooth device. True does not
+ * imply the device was enabled, it only implies that so far there were no
+ * problems.
+ */
+ public boolean enable() {
+ return enable(null);
+ }
+
+ /**
+ * Enable the Bluetooth device.
+ * Turns on the underlying hardware.
+ * This is an asynchronous call. onEnableResult() of your callback will be
+ * called when the call is complete, with either RESULT_SUCCESS or
+ * RESULT_FAILURE.
+ *
+ * Your callback will be called from a binder thread, not the main thread.
+ *
+ * In addition to the callback, BluetoothIntent.ENABLED_ACTION will be
+ * broadcast if the device is successfully enabled.
+ *
+ * @param callback Your callback, null is ok.
+ * @return true if your callback was successfully registered, or false if
+ * there was an error, implying your callback will never be called.
+ */
+ public boolean enable(IBluetoothDeviceCallback callback) {
+ try {
+ return mService.enable(callback);
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ return false;
+ }
+
+ /**
+ * Disable the Bluetooth device.
+ * This turns off the underlying hardware.
+ *
+ * @return true if successful, false otherwise.
+ */
+ public boolean disable() {
+ try {
+ return mService.disable();
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ return false;
+ }
+
+ public String getAddress() {
+ try {
+ return mService.getAddress();
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ return null;
+ }
+
+ /**
+ * Get the friendly Bluetooth name of this device.
+ *
+ * This name is visible to remote Bluetooth devices. Currently it is only
+ * possible to retrieve the Bluetooth name when Bluetooth is enabled.
+ *
+ * @return the Bluetooth name, or null if there was a problem.
+ */
+ public String getName() {
+ try {
+ return mService.getName();
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ return null;
+ }
+
+ /**
+ * Set the friendly Bluetooth name of this device.
+ *
+ * This name is visible to remote Bluetooth devices. The Bluetooth Service
+ * is responsible for persisting this name.
+ *
+ * @param name the name to set
+ * @return true, if the name was successfully set. False otherwise.
+ */
+ public boolean setName(String name) {
+ try {
+ return mService.setName(name);
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ return false;
+ }
+
+ public String getMajorClass() {
+ try {
+ return mService.getMajorClass();
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ return null;
+ }
+ public String getMinorClass() {
+ try {
+ return mService.getMinorClass();
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ return null;
+ }
+ public String getVersion() {
+ try {
+ return mService.getVersion();
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ return null;
+ }
+ public String getRevision() {
+ try {
+ return mService.getRevision();
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ return null;
+ }
+ public String getManufacturer() {
+ try {
+ return mService.getManufacturer();
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ return null;
+ }
+ public String getCompany() {
+ try {
+ return mService.getCompany();
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ return null;
+ }
+
+ public int getMode() {
+ try {
+ return mService.getMode();
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ return MODE_UNKNOWN;
+ }
+ public void setMode(int mode) {
+ try {
+ mService.setMode(mode);
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ }
+
+ public int getDiscoverableTimeout() {
+ try {
+ return mService.getDiscoverableTimeout();
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ return -1;
+ }
+ public void setDiscoverableTimeout(int timeout) {
+ try {
+ mService.setDiscoverableTimeout(timeout);
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ }
+
+ public boolean startDiscovery() {
+ return startDiscovery(true);
+ }
+ public boolean startDiscovery(boolean resolveNames) {
+ try {
+ return mService.startDiscovery(resolveNames);
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ return false;
+ }
+
+ public void cancelDiscovery() {
+ try {
+ mService.cancelDiscovery();
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ }
+
+ public boolean isDiscovering() {
+ try {
+ return mService.isDiscovering();
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ return false;
+ }
+
+ public boolean startPeriodicDiscovery() {
+ try {
+ return mService.startPeriodicDiscovery();
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ return false;
+ }
+ public boolean stopPeriodicDiscovery() {
+ try {
+ return mService.stopPeriodicDiscovery();
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ return false;
+ }
+ public boolean isPeriodicDiscovery() {
+ try {
+ return mService.isPeriodicDiscovery();
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ return false;
+ }
+
+ public String[] listRemoteDevices() {
+ try {
+ return mService.listRemoteDevices();
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ return null;
+ }
+
+ /**
+ * List remote devices that have a low level (ACL) connection.
+ *
+ * RFCOMM, SDP and L2CAP are all built on ACL connections. Devices can have
+ * an ACL connection even when not paired - this is common for SDP queries
+ * or for in-progress pairing requests.
+ *
+ * In most cases you probably want to test if a higher level protocol is
+ * connected, rather than testing ACL connections.
+ *
+ * @return bluetooth hardware addresses of remote devices with a current
+ * ACL connection. Array size is 0 if no devices have a
+ * connection. Null on error.
+ */
+ public String[] listAclConnections() {
+ try {
+ return mService.listAclConnections();
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ return null;
+ }
+
+ /**
+ * Check if a specified remote device has a low level (ACL) connection.
+ *
+ * RFCOMM, SDP and L2CAP are all built on ACL connections. Devices can have
+ * an ACL connection even when not paired - this is common for SDP queries
+ * or for in-progress pairing requests.
+ *
+ * In most cases you probably want to test if a higher level protocol is
+ * connected, rather than testing ACL connections.
+ *
+ * @param address the Bluetooth hardware address you want to check.
+ * @return true if there is an ACL connection, false otherwise and on
+ * error.
+ */
+ public boolean isAclConnected(String address) {
+ try {
+ return mService.isAclConnected(address);
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ return false;
+ }
+
+ /**
+ * Perform a low level (ACL) disconnection of a remote device.
+ *
+ * This forcably disconnects the ACL layer connection to a remote device,
+ * which will cause all RFCOMM, SDP and L2CAP connections to this remote
+ * device to close.
+ *
+ * @param address the Bluetooth hardware address you want to disconnect.
+ * @return true if the device was disconnected, false otherwise and on
+ * error.
+ */
+ public boolean disconnectRemoteDeviceAcl(String address) {
+ try {
+ return mService.disconnectRemoteDeviceAcl(address);
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ return false;
+ }
+
+ /**
+ * Create a bonding with a remote bluetooth device.
+ *
+ * This is an asynchronous call. BluetoothIntent.BONDING_CREATED_ACTION
+ * will be broadcast if and when the remote device is successfully bonded.
+ *
+ * @param address the remote device Bluetooth address.
+ * @return false if we cannot create a bonding to that device, true if
+ * there were no problems beginning the bonding process.
+ */
+ public boolean createBonding(String address) {
+ return createBonding(address, null);
+ }
+
+ /**
+ * Create a bonding with a remote bluetooth device.
+ *
+ * This is an asynchronous call. onCreateBondingResult() of your callback
+ * will be called when the call is complete, with either RESULT_SUCCESS or
+ * RESULT_FAILURE.
+ *
+ * In addition to the callback, BluetoothIntent.BONDING_CREATED_ACTION will
+ * be broadcast if the remote device is successfully bonded.
+ *
+ * @param address The remote device Bluetooth address.
+ * @param callback Your callback, null is ok.
+ * @return true if your callback was successfully registered, or false if
+ * there was an error, implying your callback will never be called.
+ */
+ public boolean createBonding(String address, IBluetoothDeviceCallback callback) {
+ try {
+ return mService.createBonding(address, callback);
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ return false;
+ }
+
+ public boolean cancelBondingProcess(String address) {
+ try {
+ return mService.cancelBondingProcess(address);
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ return false;
+ }
+
+ /**
+ * List remote devices that are bonded (paired) to the local device.
+ *
+ * Bonding (pairing) is the process by which the user enters a pin code for
+ * the device, which generates a shared link key, allowing for
+ * authentication and encryption of future connections. In Android we
+ * require bonding before RFCOMM or SCO connections can be made to a remote
+ * device.
+ *
+ * This function lists which remote devices we have a link key for. It does
+ * not cause any RF transmission, and does not check if the remote device
+ * still has it's link key with us. If the other side no longer has its
+ * link key then the RFCOMM or SCO connection attempt will result in an
+ * error.
+ *
+ * This function does not check if the remote device is in range.
+ *
+ * @return bluetooth hardware addresses of remote devices that are
+ * bonded. Array size is 0 if no devices are bonded. Null on error.
+ */
+ public String[] listBondings() {
+ try {
+ return mService.listBondings();
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ return null;
+ }
+
+ /**
+ * Check if a remote device is bonded (paired) to the local device.
+ *
+ * Bonding (pairing) is the process by which the user enters a pin code for
+ * the device, which generates a shared link key, allowing for
+ * authentication and encryption of future connections. In Android we
+ * require bonding before RFCOMM or SCO connections can be made to a remote
+ * device.
+ *
+ * This function checks if we have a link key with the remote device. It
+ * does not cause any RF transmission, and does not check if the remote
+ * device still has it's link key with us. If the other side no longer has
+ * a link key then the RFCOMM or SCO connection attempt will result in an
+ * error.
+ *
+ * This function does not check if the remote device is in range.
+ *
+ * @param address Bluetooth hardware address of the remote device to check.
+ * @return true if bonded, false otherwise and on error.
+ */
+ public boolean hasBonding(String address) {
+ try {
+ return mService.hasBonding(address);
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ return false;
+ }
+
+ public boolean removeBonding(String address) {
+ try {
+ return mService.removeBonding(address);
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ return false;
+ }
+
+ public String getRemoteName(String address) {
+ try {
+ return mService.getRemoteName(address);
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ return null;
+ }
+
+ public String getRemoteAlias(String address) {
+ try {
+ return mService.getRemoteAlias(address);
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ return null;
+ }
+ public boolean setRemoteAlias(String address, String alias) {
+ try {
+ return mService.setRemoteAlias(address, alias);
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ return false;
+ }
+ public boolean clearRemoteAlias(String address) {
+ try {
+ return mService.clearRemoteAlias(address);
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ return false;
+ }
+ public String getRemoteVersion(String address) {
+ try {
+ return mService.getRemoteVersion(address);
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ return null;
+ }
+ public String getRemoteRevision(String address) {
+ try {
+ return mService.getRemoteRevision(address);
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ return null;
+ }
+ public String getRemoteManufacturer(String address) {
+ try {
+ return mService.getRemoteManufacturer(address);
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ return null;
+ }
+ public String getRemoteCompany(String address) {
+ try {
+ return mService.getRemoteCompany(address);
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ return null;
+ }
+ public String getRemoteMajorClass(String address) {
+ try {
+ return mService.getRemoteMajorClass(address);
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ return null;
+ }
+ public String getRemoteMinorClass(String address) {
+ try {
+ return mService.getRemoteMinorClass(address);
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ return null;
+ }
+ public String[] getRemoteServiceClasses(String address) {
+ try {
+ return mService.getRemoteServiceClasses(address);
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ return null;
+ }
+
+ /**
+ * Returns the RFCOMM channel associated with the 16-byte UUID on
+ * the remote Bluetooth address.
+ *
+ * Performs a SDP ServiceSearchAttributeRequest transaction. The provided
+ * uuid is verified in the returned record. If there was a problem, or the
+ * specified uuid does not exist, -1 is returned.
+ */
+ public boolean getRemoteServiceChannel(String address, short uuid16,
+ IBluetoothDeviceCallback callback) {
+ try {
+ return mService.getRemoteServiceChannel(address, uuid16, callback);
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ return false;
+ }
+
+ public int getRemoteClass(String address) {
+ try {
+ return mService.getRemoteClass(address);
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ return DeviceClass.CLASS_UNKNOWN;
+ }
+ public byte[] getRemoteFeatures(String address) {
+ try {
+ return mService.getRemoteFeatures(address);
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ return null;
+ }
+ public String lastSeen(String address) {
+ try {
+ return mService.lastSeen(address);
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ return null;
+ }
+ public String lastUsed(String address) {
+ try {
+ return mService.lastUsed(address);
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ return null;
+ }
+
+ public boolean setPin(String address, byte[] pin) {
+ try {
+ return mService.setPin(address, pin);
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ return false;
+ }
+ public boolean cancelPin(String address) {
+ try {
+ return mService.cancelPin(address);
+ } catch (RemoteException e) {Log.e(TAG, "", e);}
+ return false;
+ }
+
+ /**
+ * Check that a pin is valid and convert to byte array.
+ *
+ * Bluetooth pin's are 1 to 16 bytes of UTF8 characters.
+ * @param pin pin as java String
+ * @return the pin code as a UTF8 byte array, or null if it is an invalid
+ * Bluetooth pin.
+ */
+ public static byte[] convertPinToBytes(String pin) {
+ if (pin == null) {
+ return null;
+ }
+ byte[] pinBytes;
+ try {
+ pinBytes = pin.getBytes("UTF8");
+ } catch (UnsupportedEncodingException uee) {
+ Log.e(TAG, "UTF8 not supported?!?"); // this should not happen
+ return null;
+ }
+ if (pinBytes.length <= 0 || pinBytes.length > 16) {
+ return null;
+ }
+ return pinBytes;
+ }
+
+
+ /* Sanity check a bluetooth address, such as "00:43:A8:23:10:F0" */
+ private static final int ADDRESS_LENGTH = 17;
+ public static boolean checkBluetoothAddress(String address) {
+ if (address == null || address.length() != ADDRESS_LENGTH) {
+ return false;
+ }
+ for (int i = 0; i < ADDRESS_LENGTH; i++) {
+ char c = address.charAt(i);
+ switch (i % 3) {
+ case 0:
+ case 1:
+ if (Character.digit(c, 16) != -1) {
+ break; // hex character, OK
+ }
+ return false;
+ case 2:
+ if (c == ':') {
+ break; // OK
+ }
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/core/java/android/bluetooth/BluetoothHeadset.java b/core/java/android/bluetooth/BluetoothHeadset.java
new file mode 100644
index 0000000..90db39b
--- /dev/null
+++ b/core/java/android/bluetooth/BluetoothHeadset.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.RemoteException;
+import android.os.IBinder;
+import android.util.Log;
+
+/**
+ * The Android Bluetooth API is not finalized, and *will* change. Use at your
+ * own risk.
+ *
+ * Public API for controlling the Bluetooth Headset Service.
+ *
+ * BluetoothHeadset is a proxy object for controlling the Bluetooth Headset
+ * Service.
+ *
+ * Creating a BluetoothHeadset object will create a binding with the
+ * BluetoothHeadset service. Users of this object should call close() when they
+ * are finished with the BluetoothHeadset, so that this proxy object can unbind
+ * from the service.
+ *
+ * BlueoothHeadset objects are not guarenteed to be connected to the
+ * BluetoothHeadsetService at all times. Calls on this object while not
+ * connected to the service will result in default error return values. Even
+ * after object construction, there is a short delay (~10ms) before this proxy
+ * object is actually connected to the Service.
+ *
+ * Android only supports one connected Bluetooth Headset at a time.
+ *
+ * Note that in this context, Headset includes both Bluetooth Headset's and
+ * Handsfree devices.
+ *
+ * @hide
+ */
+public class BluetoothHeadset {
+
+ private final static String TAG = "BluetoothHeadset";
+
+ private final Context mContext;
+ private IBluetoothHeadset mService;
+
+ /** There was an error trying to obtain the state */
+ public static final int STATE_ERROR = -1;
+ /** No headset currently connected */
+ public static final int STATE_DISCONNECTED = 0;
+ /** Connection attempt in progress */
+ public static final int STATE_CONNECTING = 1;
+ /** A headset is currently connected */
+ public static final int STATE_CONNECTED = 2;
+
+ public static final int RESULT_FAILURE = 0;
+ public static final int RESULT_SUCCESS = 1;
+ /** Connection cancelled before completetion. */
+ public static final int RESULT_CANCELLED = 2;
+
+ private ServiceConnection mConnection = new ServiceConnection() {
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ mService = IBluetoothHeadset.Stub.asInterface(service);
+ Log.i(TAG, "Proxy object is now connected to Bluetooth Headset Service");
+ }
+ public void onServiceDisconnected(ComponentName className) {
+ mService = null;
+ }
+ };
+
+ /**
+ * Create a BluetoothHeadset proxy object.
+ * Remeber to call close() when you are done with this object, so that it
+ * can unbind from the BluetoothHeadsetService.
+ */
+ public BluetoothHeadset(Context context) {
+ mContext = context;
+ if (!context.bindService(
+ new Intent(IBluetoothHeadset.class.getName()), mConnection, 0)) {
+ Log.e(TAG, "Could not bind to Bluetooth Headset Service");
+ }
+ }
+
+ protected void finalize() throws Throwable {
+ try {
+ close();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ /**
+ * Close the connection to the backing service.
+ * Other public functions of BluetoothHeadset will return default error
+ * results once close() has been called. Multiple invocations of close()
+ * are ok.
+ */
+ public synchronized void close() {
+ if (mConnection != null) {
+ mContext.unbindService(mConnection);
+ mConnection = null;
+ }
+ }
+
+ /**
+ * Get the current state of the Bluetooth Headset service.
+ * @return One of the STATE_ return codes, or STATE_ERROR if this proxy
+ * object is currently not connected to the Headset service.
+ */
+ public int getState() {
+ if (mService != null) {
+ try {
+ return mService.getState();
+ } catch (RemoteException e) {Log.e(TAG, e.toString());}
+ }
+ return BluetoothHeadset.STATE_ERROR;
+ }
+
+ /**
+ * Get the Bluetooth address of the current headset.
+ * @return The Bluetooth address, or null if not in connected or connecting
+ * state, or if this proxy object is not connected to the Headset
+ * service.
+ */
+ public String getHeadsetAddress() {
+ if (mService != null) {
+ try {
+ return mService.getHeadsetAddress();
+ } catch (RemoteException e) {Log.e(TAG, e.toString());}
+ }
+ return null;
+ }
+
+ /**
+ * Request to initiate a connection to a headset.
+ * This call does not block. Fails if a headset is already connecting
+ * or connected.
+ * Will connect to the last connected headset if address is null.
+ * @param address The Bluetooth Address to connect to, or null to connect
+ * to the last connected headset.
+ * @param callback A callback with onCreateBondingResult() defined, or
+ * null.
+ * @return False if there was a problem initiating the connection
+ * procedure, and your callback will not be used. True if
+ * the connection procedure was initiated, in which case
+ * your callback is guarenteed to be called.
+ */
+ public boolean connectHeadset(String address, IBluetoothHeadsetCallback callback) {
+ if (mService != null) {
+ try {
+ return mService.connectHeadset(address, callback);
+ } catch (RemoteException e) {Log.e(TAG, e.toString());}
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if the specified headset is connected (does not include
+ * connecting). Returns false if not connected, or if this proxy object
+ * if not currently connected to the headset service.
+ */
+ public boolean isConnected(String address) {
+ if (mService != null) {
+ try {
+ return mService.isConnected(address);
+ } catch (RemoteException e) {Log.e(TAG, e.toString());}
+ }
+ return false;
+ }
+
+ /**
+ * Disconnects the current headset. Currently this call blocks, it may soon
+ * be made asynchornous. Returns false if this proxy object is
+ * not currently connected to the Headset service.
+ */
+ public boolean disconnectHeadset() {
+ if (mService != null) {
+ try {
+ mService.disconnectHeadset();
+ } catch (RemoteException e) {Log.e(TAG, e.toString());}
+ }
+ return false;
+ }
+}
diff --git a/core/java/android/bluetooth/BluetoothIntent.java b/core/java/android/bluetooth/BluetoothIntent.java
new file mode 100644
index 0000000..8e22791
--- /dev/null
+++ b/core/java/android/bluetooth/BluetoothIntent.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+
+/**
+ * The Android Bluetooth API is not finalized, and *will* change. Use at your
+ * own risk.
+ *
+ * Manages the local Bluetooth device. Scan for devices, create bondings,
+ * power up and down the adapter.
+ *
+ * @hide
+ */
+public interface BluetoothIntent {
+ public static final String MODE =
+ "android.bluetooth.intent.MODE";
+ public static final String ADDRESS =
+ "android.bluetooth.intent.ADDRESS";
+ public static final String NAME =
+ "android.bluetooth.intent.NAME";
+ public static final String ALIAS =
+ "android.bluetooth.intent.ALIAS";
+ public static final String RSSI =
+ "android.bluetooth.intent.RSSI";
+ public static final String CLASS =
+ "android.bluetooth.intent.CLASS";
+ public static final String HEADSET_STATE =
+ "android.bluetooth.intent.HEADSET_STATE";
+ public static final String HEADSET_PREVIOUS_STATE =
+ "android.bluetooth.intent.HEADSET_PREVIOUS_STATE";
+
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ENABLED_ACTION =
+ "android.bluetooth.intent.action.ENABLED";
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String DISABLED_ACTION =
+ "android.bluetooth.intent.action.DISABLED";
+
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String NAME_CHANGED_ACTION =
+ "android.bluetooth.intent.action.NAME_CHANGED";
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String MODE_CHANGED_ACTION =
+ "android.bluetooth.intent.action.MODE_CHANGED";
+
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String DISCOVERY_STARTED_ACTION =
+ "android.bluetooth.intent.action.DISCOVERY_STARTED";
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String DISCOVERY_COMPLETED_ACTION =
+ "android.bluetooth.intent.action.DISCOVERY_COMPLETED";
+
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String PAIRING_REQUEST_ACTION =
+ "android.bluetooth.intent.action.PAIRING_REQUEST";
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String PAIRING_CANCEL_ACTION =
+ "android.bluetooth.intent.action.PAIRING_CANCEL";
+
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String REMOTE_DEVICE_FOUND_ACTION =
+ "android.bluetooth.intent.action.REMOTE_DEVICE_FOUND";
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String REMOTE_DEVICE_DISAPPEARED_ACTION =
+ "android.bluetooth.intent.action.REMOTE_DEVICE_DISAPPEARED";
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String REMOTE_DEVICE_CLASS_UPDATED_ACTION =
+ "android.bluetooth.intent.action.REMOTE_DEVICE_DISAPPEARED";
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String REMOTE_DEVICE_CONNECTED_ACTION =
+ "android.bluetooth.intent.action.REMOTE_DEVICE_CONNECTED";
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String REMOTE_DEVICE_DISCONNECT_REQUESTED_ACTION =
+ "android.bluetooth.intent.action.REMOTE_DEVICE_DISCONNECT_REQUESTED";
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String REMOTE_DEVICE_DISCONNECTED_ACTION =
+ "android.bluetooth.intent.action.REMOTE_DEVICE_DISCONNECTED";
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String REMOTE_NAME_UPDATED_ACTION =
+ "android.bluetooth.intent.action.REMOTE_NAME_UPDATED";
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String REMOTE_NAME_FAILED_ACTION =
+ "android.bluetooth.intent.action.REMOTE_NAME_FAILED";
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String REMOTE_ALIAS_CHANGED_ACTION =
+ "android.bluetooth.intent.action.REMOTE_ALIAS_CHANGED";
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String REMOTE_ALIAS_CLEARED_ACTION =
+ "android.bluetooth.intent.action.REMOTE_ALIAS_CLEARED";
+
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String BONDING_CREATED_ACTION =
+ "android.bluetooth.intent.action.BONDING_CREATED";
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String BONDING_REMOVED_ACTION =
+ "android.bluetooth.intent.action.BONDING_REMOVED";
+
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String HEADSET_STATE_CHANGED_ACTION =
+ "android.bluetooth.intent.action.HEADSET_STATE_CHANGED";
+}
diff --git a/core/java/android/bluetooth/Database.java b/core/java/android/bluetooth/Database.java
new file mode 100644
index 0000000..fef641a
--- /dev/null
+++ b/core/java/android/bluetooth/Database.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+import android.bluetooth.RfcommSocket;
+
+import android.util.Log;
+
+import java.io.*;
+import java.util.*;
+
+/**
+ * The Android Bluetooth API is not finalized, and *will* change. Use at your
+ * own risk.
+ *
+ * A low-level API to the Service Discovery Protocol (SDP) Database.
+ *
+ * Allows service records to be added to the local SDP database. Once added,
+ * these services will be advertised to remote devices when they make SDP
+ * queries on this device.
+ *
+ * Currently this API is a thin wrapper to the bluez SDP Database API. See:
+ * http://wiki.bluez.org/wiki/Database
+ * http://wiki.bluez.org/wiki/HOWTO/ManagingServiceRecords
+ * @hide
+ */
+public final class Database {
+ private static Database mInstance;
+
+ private static final String sLogName = "android.bluetooth.Database";
+
+ /**
+ * Class load time initialization
+ */
+ static {
+ classInitNative();
+ }
+ private native static void classInitNative();
+
+ /**
+ * Private to enforce singleton property
+ */
+ private Database() {
+ initializeNativeDataNative();
+ }
+ private native void initializeNativeDataNative();
+
+ protected void finalize() throws Throwable {
+ try {
+ cleanupNativeDataNative();
+ } finally {
+ super.finalize();
+ }
+ }
+ private native void cleanupNativeDataNative();
+
+ /**
+ * Singelton accessor
+ * @return The singleton instance of Database
+ */
+ public static synchronized Database getInstance() {
+ if (mInstance == null) {
+ mInstance = new Database();
+ }
+ return mInstance;
+ }
+
+ /**
+ * Advertise a service with an RfcommSocket.
+ *
+ * This adds the service the SDP Database with the following attributes
+ * set: Service Name, Protocol Descriptor List, Service Class ID List
+ * TODO: Construct a byte[] record directly, rather than via XML.
+ * @param socket The rfcomm socket to advertise (by channel).
+ * @param serviceName A short name for this service
+ * @param uuid
+ * Unique identifier for this service, by which clients
+ * can search for your service
+ * @return Handle to the new service record
+ */
+ public int advertiseRfcommService(RfcommSocket socket,
+ String serviceName,
+ UUID uuid) throws IOException {
+ String xmlRecord =
+ "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n" +
+ "<record>\n" +
+ " <attribute id=\"0x0001\">\n" + // ServiceClassIDList
+ " <sequence>\n" +
+ " <uuid value=\""
+ + uuid.toString() + // UUID for this service
+ "\"/>\n" +
+ " </sequence>\n" +
+ " </attribute>\n" +
+ " <attribute id=\"0x0004\">\n" + // ProtocolDescriptorList
+ " <sequence>\n" +
+ " <sequence>\n" +
+ " <uuid value=\"0x0100\"/>\n" + // L2CAP
+ " </sequence>\n" +
+ " <sequence>\n" +
+ " <uuid value=\"0x0003\"/>\n" + // RFCOMM
+ " <uint8 value=\"" +
+ socket.getPort() + // RFCOMM port
+ "\" name=\"channel\"/>\n" +
+ " </sequence>\n" +
+ " </sequence>\n" +
+ " </attribute>\n" +
+ " <attribute id=\"0x0100\">\n" + // ServiceName
+ " <text value=\"" + serviceName + "\"/>\n" +
+ " </attribute>\n" +
+ "</record>\n";
+ Log.i(sLogName, xmlRecord);
+ return addServiceRecordFromXml(xmlRecord);
+ }
+
+
+ /**
+ * Add a new service record.
+ * @param record The byte[] record
+ * @return A handle to the new record
+ */
+ public synchronized int addServiceRecord(byte[] record) throws IOException {
+ int handle = addServiceRecordNative(record);
+ Log.i(sLogName, "Added SDP record: " + Integer.toHexString(handle));
+ return handle;
+ }
+ private native int addServiceRecordNative(byte[] record)
+ throws IOException;
+
+ /**
+ * Add a new service record, using XML.
+ * @param record The record as an XML string
+ * @return A handle to the new record
+ */
+ public synchronized int addServiceRecordFromXml(String record) throws IOException {
+ int handle = addServiceRecordFromXmlNative(record);
+ Log.i(sLogName, "Added SDP record: " + Integer.toHexString(handle));
+ return handle;
+ }
+ private native int addServiceRecordFromXmlNative(String record)
+ throws IOException;
+
+ /**
+ * Update an exisiting service record.
+ * @param handle Handle to exisiting record
+ * @param record The updated byte[] record
+ */
+ public synchronized void updateServiceRecord(int handle, byte[] record) {
+ try {
+ updateServiceRecordNative(handle, record);
+ } catch (IOException e) {
+ Log.e(getClass().toString(), e.getMessage());
+ }
+ }
+ private native void updateServiceRecordNative(int handle, byte[] record)
+ throws IOException;
+
+ /**
+ * Update an exisiting record, using XML.
+ * @param handle Handle to exisiting record
+ * @param record The record as an XML string.
+ */
+ public synchronized void updateServiceRecordFromXml(int handle, String record) {
+ try {
+ updateServiceRecordFromXmlNative(handle, record);
+ } catch (IOException e) {
+ Log.e(getClass().toString(), e.getMessage());
+ }
+ }
+ private native void updateServiceRecordFromXmlNative(int handle, String record)
+ throws IOException;
+
+ /**
+ * Remove a service record.
+ * It is only possible to remove service records that were added by the
+ * current connection.
+ * @param handle Handle to exisiting record to be removed
+ */
+ public synchronized void removeServiceRecord(int handle) {
+ try {
+ removeServiceRecordNative(handle);
+ } catch (IOException e) {
+ Log.e(getClass().toString(), e.getMessage());
+ }
+ }
+ private native void removeServiceRecordNative(int handle) throws IOException;
+}
diff --git a/core/java/android/bluetooth/DeviceClass.java b/core/java/android/bluetooth/DeviceClass.java
new file mode 100644
index 0000000..36035ca
--- /dev/null
+++ b/core/java/android/bluetooth/DeviceClass.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+/**
+ * The Android Bluetooth API is not finalized, and *will* change. Use at your
+ * own risk.
+ *
+ * Static helper methods and constants to decode the device class bit vector
+ * returned by the Bluetooth API.
+ *
+ * The Android Bluetooth API returns a 32-bit integer to represent the device
+ * class. This is actually a bit vector, the format defined at
+ * http://www.bluetooth.org/Technical/AssignedNumbers/baseband.htm
+ * (login required). This class provides static helper methods and constants to
+ * determine what Service Class(es), Major Class, and Minor Class are encoded
+ * in a 32-bit device class.
+ *
+ * Each of the helper methods takes the 32-bit integer device class as an
+ * argument.
+ *
+ * @hide
+ */
+public class DeviceClass {
+
+ // Baseband class information
+ // See http://www.bluetooth.org/Technical/AssignedNumbers/baseband.htm
+
+ public static final int SERVICE_CLASS_BITMASK = 0xFFE000;
+ public static final int SERVICE_CLASS_LIMITED_DISCOVERABILITY = 0x002000;
+ public static final int SERVICE_CLASS_POSITIONING = 0x010000;
+ public static final int SERVICE_CLASS_NETWORKING = 0x020000;
+ public static final int SERVICE_CLASS_RENDER = 0x040000;
+ public static final int SERVICE_CLASS_CAPTURE = 0x080000;
+ public static final int SERVICE_CLASS_OBJECT_TRANSFER = 0x100000;
+ public static final int SERVICE_CLASS_AUDIO = 0x200000;
+ public static final int SERVICE_CLASS_TELEPHONY = 0x400000;
+ public static final int SERVICE_CLASS_INFORMATION = 0x800000;
+
+ public static final int MAJOR_CLASS_BITMASK = 0x001F00;
+ public static final int MAJOR_CLASS_MISC = 0x000000;
+ public static final int MAJOR_CLASS_COMPUTER = 0x000100;
+ public static final int MAJOR_CLASS_PHONE = 0x000200;
+ public static final int MAJOR_CLASS_NETWORKING = 0x000300;
+ public static final int MAJOR_CLASS_AUDIO_VIDEO = 0x000400;
+ public static final int MAJOR_CLASS_PERIPHERAL = 0x000500;
+ public static final int MAJOR_CLASS_IMAGING = 0x000600;
+ public static final int MAJOR_CLASS_WEARABLE = 0x000700;
+ public static final int MAJOR_CLASS_TOY = 0x000800;
+ public static final int MAJOR_CLASS_MEDICAL = 0x000900;
+ public static final int MAJOR_CLASS_UNCATEGORIZED = 0x001F00;
+
+ // Minor classes for the AUDIO_VIDEO major class
+ public static final int MINOR_CLASS_AUDIO_VIDEO_BITMASK = 0x0000FC;
+ public static final int MINOR_CLASS_AUDIO_VIDEO_UNCATEGORIZED = 0x000000;
+ public static final int MINOR_CLASS_AUDIO_VIDEO_HEADSET = 0x000004;
+ public static final int MINOR_CLASS_AUDIO_VIDEO_HANDSFREE = 0x000008;
+ public static final int MINOR_CLASS_AUDIO_VIDEO_MICROPHONE = 0x000010;
+ public static final int MINOR_CLASS_AUDIO_VIDEO_LOUDSPEAKER = 0x000014;
+ public static final int MINOR_CLASS_AUDIO_VIDEO_HEADPHONES = 0x000018;
+ public static final int MINOR_CLASS_AUDIO_VIDEO_PORTABLE_AUDIO = 0x00001C;
+ public static final int MINOR_CLASS_AUDIO_VIDEO_CAR_AUDIO = 0x000020;
+ public static final int MINOR_CLASS_AUDIO_VIDEO_SET_TOP_BOX = 0x000024;
+ public static final int MINOR_CLASS_AUDIO_VIDEO_HIFI_AUDIO = 0x000028;
+ public static final int MINOR_CLASS_AUDIO_VIDEO_VCR = 0x00002C;
+ public static final int MINOR_CLASS_AUDIO_VIDEO_VIDEO_CAMERA = 0x000030;
+ public static final int MINOR_CLASS_AUDIO_VIDEO_CAMCORDER = 0x000034;
+ public static final int MINOR_CLASS_AUDIO_VIDEO_VIDEO_MONITOR = 0x000038;
+ public static final int MINOR_CLASS_AUDIO_VIDEO_VIDEO_DISPLAY_AND_LOUDSPEAKER = 0x00003C;
+ public static final int MINOR_CLASS_AUDIO_VIDEO_VIDEO_CONFERENCING = 0x000040;
+ public static final int MINOR_CLASS_AUDIO_VIDEO_VIDEO_GAMING_TOY = 0x000048;
+
+ // Indicates the Bluetooth API could not retrieve the class
+ public static final int CLASS_UNKNOWN = 0xFF000000;
+
+ /** Returns true if the given device class supports the given Service Class.
+ * A bluetooth device can claim to support zero or more service classes.
+ * @param deviceClass The bluetooth device class.
+ * @param serviceClassType The service class constant to test for. For
+ * example, DeviceClass.SERVICE_CLASS_AUDIO. This
+ * must be one of the SERVICE_CLASS_xxx constants,
+ * results of this function are undefined
+ * otherwise.
+ * @return If the deviceClass claims to support the serviceClassType.
+ */
+ public static boolean hasServiceClass(int deviceClass, int serviceClassType) {
+ if (deviceClass == CLASS_UNKNOWN) {
+ return false;
+ }
+ return ((deviceClass & SERVICE_CLASS_BITMASK & serviceClassType) != 0);
+ }
+
+ /** Returns the Major Class of a bluetooth device class.
+ * Values returned from this function can be compared with the constants
+ * MAJOR_CLASS_xxx. A bluetooth device can only be associated
+ * with one major class.
+ */
+ public static int getMajorClass(int deviceClass) {
+ if (deviceClass == CLASS_UNKNOWN) {
+ return CLASS_UNKNOWN;
+ }
+ return (deviceClass & MAJOR_CLASS_BITMASK);
+ }
+
+ /** Returns the Minor Class of a bluetooth device class.
+ * Values returned from this function can be compared with the constants
+ * MINOR_CLASS_xxx_yyy, where xxx is the Major Class. A bluetooth
+ * device can only be associated with one minor class within its major
+ * class.
+ */
+ public static int getMinorClass(int deviceClass) {
+ if (deviceClass == CLASS_UNKNOWN) {
+ return CLASS_UNKNOWN;
+ }
+ return (deviceClass & MINOR_CLASS_AUDIO_VIDEO_BITMASK);
+ }
+}
diff --git a/core/java/android/bluetooth/HeadsetBase.java b/core/java/android/bluetooth/HeadsetBase.java
new file mode 100644
index 0000000..bce3388
--- /dev/null
+++ b/core/java/android/bluetooth/HeadsetBase.java
@@ -0,0 +1,310 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+import android.os.Handler;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.util.Log;
+
+import java.io.IOException;
+import java.lang.Thread;
+
+/**
+ * The Android Bluetooth API is not finalized, and *will* change. Use at your
+ * own risk.
+ *
+ * The base RFCOMM (service) connection for a headset or handsfree device.
+ *
+ * In the future this class will be removed.
+ *
+ * @hide
+ */
+public class HeadsetBase {
+ private static final String TAG = "Bluetooth HeadsetBase";
+ private static final boolean DBG = false;
+
+ public static final int RFCOMM_DISCONNECTED = 1;
+
+ public static final int DIRECTION_INCOMING = 1;
+ public static final int DIRECTION_OUTGOING = 2;
+
+ private final BluetoothDevice mBluetooth;
+ private final String mAddress;
+ private final int mRfcommChannel;
+ private int mNativeData;
+ private Thread mEventThread;
+ private volatile boolean mEventThreadInterrupted;
+ private Handler mEventThreadHandler;
+ private int mTimeoutRemainingMs;
+ private final int mDirection;
+ private final long mConnectTimestamp;
+
+ protected AtParser mAtParser;
+
+ private WakeLock mWakeLock; // held while processing an AT command
+
+ private native static void classInitNative();
+ static {
+ classInitNative();
+ }
+
+ protected void finalize() throws Throwable {
+ try {
+ cleanupNativeDataNative();
+ releaseWakeLock();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ private native void cleanupNativeDataNative();
+
+ public HeadsetBase(PowerManager pm, BluetoothDevice bluetooth, String address,
+ int rfcommChannel) {
+ mDirection = DIRECTION_OUTGOING;
+ mConnectTimestamp = System.currentTimeMillis();
+ mBluetooth = bluetooth;
+ mAddress = address;
+ mRfcommChannel = rfcommChannel;
+ mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "HeadsetBase");
+ mWakeLock.setReferenceCounted(false);
+ initializeAtParser();
+ // Must be called after this.mAddress is set.
+ initializeNativeDataNative(-1);
+ }
+
+ /* Create from an already exisiting rfcomm connection */
+ public HeadsetBase(PowerManager pm, BluetoothDevice bluetooth, String address, int socketFd,
+ int rfcommChannel, Handler handler) {
+ mDirection = DIRECTION_INCOMING;
+ mConnectTimestamp = System.currentTimeMillis();
+ mBluetooth = bluetooth;
+ mAddress = address;
+ mRfcommChannel = rfcommChannel;
+ mEventThreadHandler = handler;
+ mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "HeadsetBase");
+ mWakeLock.setReferenceCounted(false);
+ initializeAtParser();
+ // Must be called after this.mAddress is set.
+ initializeNativeDataNative(socketFd);
+ }
+
+ private native void initializeNativeDataNative(int socketFd);
+
+ /* Process an incoming AT command line
+ */
+ protected synchronized void handleInput(String input) {
+ acquireWakeLock();
+ long timestamp;
+
+ if (DBG) timestamp = System.currentTimeMillis();
+ AtCommandResult result = mAtParser.process(input);
+ if (DBG) Log.d(TAG, "Processing " + input + " took " +
+ (System.currentTimeMillis() - timestamp) + " ms");
+
+ if (result.getResultCode() == AtCommandResult.ERROR) {
+ Log.i(TAG, "Error pocessing <" + input + ">");
+ }
+
+ sendURC(result.toString());
+
+ releaseWakeLock();
+ }
+
+ /**
+ * Register AT commands that are common to all Headset / Handsets. This
+ * function is called by the HeadsetBase constructor.
+ */
+ protected void initializeAtParser() {
+ mAtParser = new AtParser();
+
+ // Microphone Gain
+ mAtParser.register("+VGM", new AtCommandHandler() {
+ @Override
+ public AtCommandResult handleSetCommand(Object[] args) {
+ // AT+VGM=<gain> in range [0,15]
+ // Headset/Handsfree is reporting its current gain setting
+ //TODO: sync to android UI
+ //TODO: Send unsolicited +VGM when volume changed on AG
+ return new AtCommandResult(AtCommandResult.OK);
+ }
+ });
+
+ // Speaker Gain
+ mAtParser.register("+VGS", new AtCommandHandler() {
+ @Override
+ public AtCommandResult handleSetCommand(Object[] args) {
+ // AT+VGS=<gain> in range [0,15]
+ // Headset/Handsfree is reporting its current gain to Android
+ //TODO: sync to AG UI
+ //TODO: Send unsolicited +VGS when volume changed on AG
+ return new AtCommandResult(AtCommandResult.OK);
+ }
+ });
+ }
+
+ public AtParser getAtParser() {
+ return mAtParser;
+ }
+
+ public void startEventThread() {
+ mEventThread =
+ new Thread("HeadsetBase Event Thread") {
+ public void run() {
+ int last_read_error;
+ while (!mEventThreadInterrupted) {
+ String input = readNative(500);
+ if (input != null) {
+ handleInput(input);
+ }
+ else {
+ last_read_error = getLastReadStatusNative();
+ if (last_read_error != 0) {
+ Log.i(TAG, "headset read error " + last_read_error);
+ if (mEventThreadHandler != null) {
+ mEventThreadHandler.obtainMessage(RFCOMM_DISCONNECTED)
+ .sendToTarget();
+ }
+ disconnectNative();
+ break;
+ }
+ }
+ }
+ }
+ };
+ mEventThreadInterrupted = false;
+ mEventThread.start();
+ }
+
+
+
+ private native String readNative(int timeout_ms);
+ private native int getLastReadStatusNative();
+
+ private void stopEventThread() {
+ mEventThreadInterrupted = true;
+ mEventThread.interrupt();
+ try {
+ mEventThread.join();
+ } catch (java.lang.InterruptedException e) {
+ // FIXME: handle this,
+ }
+ mEventThread = null;
+ }
+
+ public boolean connect(Handler handler) {
+ if (mEventThread == null) {
+ if (!connectNative()) return false;
+ mEventThreadHandler = handler;
+ }
+ return true;
+ }
+ private native boolean connectNative();
+
+ /*
+ * Returns true when either the asynchronous connect is in progress, or
+ * the connect is complete. Call waitForAsyncConnect() to find out whether
+ * the connect is actually complete, or disconnect() to cancel.
+ */
+
+ public boolean connectAsync() {
+ return connectAsyncNative();
+ }
+ private native boolean connectAsyncNative();
+
+ public int getRemainingAsyncConnectWaitingTimeMs() {
+ return mTimeoutRemainingMs;
+ }
+
+ /*
+ * Returns 1 when an async connect is complete, 0 on timeout, and -1 on
+ * error. On error, handler will be called, and you need to re-initiate
+ * the async connect.
+ */
+ public int waitForAsyncConnect(int timeout_ms, Handler handler) {
+ int res = waitForAsyncConnectNative(timeout_ms);
+ if (res > 0) {
+ mEventThreadHandler = handler;
+ }
+ return res;
+ }
+ private native int waitForAsyncConnectNative(int timeout_ms);
+
+ public void disconnect() {
+ if (mEventThread != null) {
+ stopEventThread();
+ }
+ disconnectNative();
+ }
+ private native void disconnectNative();
+
+
+ /*
+ * Note that if a remote side disconnects, this method will still return
+ * true until disconnect() is called. You know when a remote side
+ * disconnects because you will receive the intent
+ * IBluetoothService.REMOTE_DEVICE_DISCONNECTED_ACTION. If, when you get
+ * this intent, method isConnected() returns true, you know that the
+ * disconnect was initiated by the remote device.
+ */
+
+ public boolean isConnected() {
+ return mEventThread != null;
+ }
+
+ public String getAddress() {
+ return mAddress;
+ }
+
+ public String getName() {
+ return mBluetooth.getRemoteName(mAddress);
+ }
+
+ public int getDirection() {
+ return mDirection;
+ }
+
+ public long getConnectTimestamp() {
+ return mConnectTimestamp;
+ }
+
+ public synchronized boolean sendURC(String urc) {
+ if (urc.length() > 0) {
+ boolean ret = sendURCNative(urc);
+ return ret;
+ }
+ return true;
+ }
+ private native boolean sendURCNative(String urc);
+
+ private void acquireWakeLock() {
+ if (!mWakeLock.isHeld()) {
+ mWakeLock.acquire();
+ }
+ }
+
+ private void releaseWakeLock() {
+ if (mWakeLock.isHeld()) {
+ mWakeLock.release();
+ }
+ }
+
+ private void log(String msg) {
+ Log.d(TAG, msg);
+ }
+}
diff --git a/core/java/android/bluetooth/IBluetoothDevice.aidl b/core/java/android/bluetooth/IBluetoothDevice.aidl
new file mode 100644
index 0000000..e7cc8ed
--- /dev/null
+++ b/core/java/android/bluetooth/IBluetoothDevice.aidl
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2008, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+import android.bluetooth.IBluetoothDeviceCallback;
+
+/**
+ * System private API for talking with the Bluetooth service.
+ *
+ * {@hide}
+ */
+interface IBluetoothDevice
+{
+ boolean isEnabled();
+ boolean enable(in IBluetoothDeviceCallback callback); // async
+ boolean disable();
+
+ String getAddress();
+ String getName();
+ boolean setName(in String name);
+ String getMajorClass();
+ String getMinorClass();
+ String getVersion();
+ String getRevision();
+ String getManufacturer();
+ String getCompany();
+
+ int getMode();
+ boolean setMode(int mode);
+
+ int getDiscoverableTimeout();
+ boolean setDiscoverableTimeout(int timeout);
+
+ boolean startDiscovery(boolean resolveNames);
+ boolean cancelDiscovery();
+ boolean isDiscovering();
+ boolean startPeriodicDiscovery();
+ boolean stopPeriodicDiscovery();
+ boolean isPeriodicDiscovery();
+ String[] listRemoteDevices();
+
+ String[] listAclConnections();
+ boolean isAclConnected(in String address);
+ boolean disconnectRemoteDeviceAcl(in String address);
+
+ boolean createBonding(in String address, in IBluetoothDeviceCallback callback);
+ boolean cancelBondingProcess(in String address);
+ String[] listBondings();
+ boolean hasBonding(in String address);
+ boolean removeBonding(in String address);
+
+ String getRemoteName(in String address);
+ String getRemoteAlias(in String address);
+ boolean setRemoteAlias(in String address, in String alias);
+ boolean clearRemoteAlias(in String address);
+ String getRemoteVersion(in String address);
+ String getRemoteRevision(in String address);
+ int getRemoteClass(in String address);
+ String getRemoteManufacturer(in String address);
+ String getRemoteCompany(in String address);
+ String getRemoteMajorClass(in String address);
+ String getRemoteMinorClass(in String address);
+ String[] getRemoteServiceClasses(in String address);
+ boolean getRemoteServiceChannel(in String address, int uuid16, in IBluetoothDeviceCallback callback);
+ byte[] getRemoteFeatures(in String adddress);
+ String lastSeen(in String address);
+ String lastUsed(in String address);
+
+ boolean setPin(in String address, in byte[] pin);
+ boolean cancelPin(in String address);
+}
diff --git a/core/java/android/bluetooth/IBluetoothDeviceCallback.aidl b/core/java/android/bluetooth/IBluetoothDeviceCallback.aidl
new file mode 100644
index 0000000..86f44dd
--- /dev/null
+++ b/core/java/android/bluetooth/IBluetoothDeviceCallback.aidl
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2008, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+/**
+ * {@hide}
+ */
+oneway interface IBluetoothDeviceCallback
+{
+ void onCreateBondingResult(in String address, int result);
+ void onGetRemoteServiceChannelResult(in String address, int channel);
+
+ void onEnableResult(int result);
+}
diff --git a/core/java/android/bluetooth/IBluetoothHeadset.aidl b/core/java/android/bluetooth/IBluetoothHeadset.aidl
new file mode 100644
index 0000000..7b6030b
--- /dev/null
+++ b/core/java/android/bluetooth/IBluetoothHeadset.aidl
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+import android.bluetooth.IBluetoothHeadsetCallback;
+
+/**
+ * System private API for Bluetooth Headset service
+ *
+ * {@hide}
+ */
+interface IBluetoothHeadset {
+ int getState();
+
+ String getHeadsetAddress();
+
+ // Request that the given headset be connected
+ // Assumes the given headset is already bonded
+ // Will disconnect any currently connected headset
+ // returns false if cannot start a connection (for example, there is
+ // already a pending connect). callback will always be called iff this
+ // returns true
+ boolean connectHeadset(in String address, in IBluetoothHeadsetCallback callback);
+
+ boolean isConnected(in String address);
+
+ void disconnectHeadset();
+}
diff --git a/core/java/android/bluetooth/IBluetoothHeadsetCallback.aidl b/core/java/android/bluetooth/IBluetoothHeadsetCallback.aidl
new file mode 100644
index 0000000..03e884b
--- /dev/null
+++ b/core/java/android/bluetooth/IBluetoothHeadsetCallback.aidl
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2008, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+/**
+ * {@hide}
+ */
+oneway interface IBluetoothHeadsetCallback
+{
+ void onConnectHeadsetResult(in String address, int resultCode);
+}
diff --git a/core/java/android/bluetooth/RfcommSocket.java b/core/java/android/bluetooth/RfcommSocket.java
new file mode 100644
index 0000000..a33263f
--- /dev/null
+++ b/core/java/android/bluetooth/RfcommSocket.java
@@ -0,0 +1,674 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+import java.io.IOException;
+import java.io.FileOutputStream;
+import java.io.FileInputStream;
+import java.io.OutputStream;
+import java.io.InputStream;
+import java.io.FileDescriptor;
+
+/**
+ * The Android Bluetooth API is not finalized, and *will* change. Use at your
+ * own risk.
+ *
+ * This class implements an API to the Bluetooth RFCOMM layer. An RFCOMM socket
+ * is similar to a normal socket in that it takes an address and a port number.
+ * The difference is of course that the address is a Bluetooth-device address,
+ * and the port number is an RFCOMM channel. The API allows for the
+ * establishment of listening sockets via methods
+ * {@link #bind(String, int) bind}, {@link #listen(int) listen}, and
+ * {@link #accept(RfcommSocket, int) accept}, as well as for the making of
+ * outgoing connections with {@link #connect(String, int) connect},
+ * {@link #connectAsync(String, int) connectAsync}, and
+ * {@link #waitForAsyncConnect(int) waitForAsyncConnect}.
+ *
+ * After constructing a socket, you need to {@link #create() create} it and then
+ * {@link #destroy() destroy} it when you are done using it. Both
+ * {@link #create() create} and {@link #accept(RfcommSocket, int) accept} return
+ * a {@link java.io.FileDescriptor FileDescriptor} for the actual data.
+ * Alternatively, you may call {@link #getInputStream() getInputStream} and
+ * {@link #getOutputStream() getOutputStream} to retrieve the respective streams
+ * without going through the FileDescriptor.
+ *
+ * @hide
+ */
+public class RfcommSocket {
+
+ /**
+ * Used by the native implementation of the class.
+ */
+ private int mNativeData;
+
+ /**
+ * Used by the native implementation of the class.
+ */
+ private int mPort;
+
+ /**
+ * Used by the native implementation of the class.
+ */
+ private String mAddress;
+
+ /**
+ * We save the return value of {@link #create() create} and
+ * {@link #accept(RfcommSocket,int) accept} in this variable, and use it to
+ * retrieve the I/O streams.
+ */
+ private FileDescriptor mFd;
+
+ /**
+ * After a call to {@link #waitForAsyncConnect(int) waitForAsyncConnect},
+ * if the return value is zero, then, the the remaining time left to wait is
+ * written into this variable (by the native implementation). It is possible
+ * that {@link #waitForAsyncConnect(int) waitForAsyncConnect} returns before
+ * the user-specified timeout expires, which is why we save the remaining
+ * time in this member variable for the user to retrieve by calling method
+ * {@link #getRemainingAsyncConnectWaitingTimeMs() getRemainingAsyncConnectWaitingTimeMs}.
+ */
+ private int mTimeoutRemainingMs;
+
+ /**
+ * Set to true when an asynchronous (nonblocking) connect is in progress.
+ * {@see #connectAsync(String,int)}.
+ */
+ private boolean mIsConnecting;
+
+ /**
+ * Set to true after a successful call to {@link #bind(String,int) bind} and
+ * used for error checking in {@link #listen(int) listen}. Reset to false
+ * on {@link #destroy() destroy}.
+ */
+ private boolean mIsBound = false;
+
+ /**
+ * Set to true after a successful call to {@link #listen(int) listen} and
+ * used for error checking in {@link #accept(RfcommSocket,int) accept}.
+ * Reset to false on {@link #destroy() destroy}.
+ */
+ private boolean mIsListening = false;
+
+ /**
+ * Used to store the remaining time after an accept with a non-negative
+ * timeout returns unsuccessfully. It is possible that a blocking
+ * {@link #accept(int) accept} may wait for less than the time specified by
+ * the user, which is why we store the remainder in this member variable for
+ * it to be retrieved with method
+ * {@link #getRemainingAcceptWaitingTimeMs() getRemainingAcceptWaitingTimeMs}.
+ */
+ private int mAcceptTimeoutRemainingMs;
+
+ /**
+ * Maintained by {@link #getInputStream() getInputStream}.
+ */
+ protected FileInputStream mInputStream;
+
+ /**
+ * Maintained by {@link #getOutputStream() getOutputStream}.
+ */
+ protected FileOutputStream mOutputStream;
+
+ private native void initializeNativeDataNative();
+
+ /**
+ * Constructor.
+ */
+ public RfcommSocket() {
+ initializeNativeDataNative();
+ }
+
+ private native void cleanupNativeDataNative();
+
+ /**
+ * Called by the GC to clean up the native data that we set up when we
+ * construct the object.
+ */
+ protected void finalize() throws Throwable {
+ try {
+ cleanupNativeDataNative();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ private native static void classInitNative();
+
+ static {
+ classInitNative();
+ }
+
+ /**
+ * Creates a socket. You need to call this method before performing any
+ * other operation on a socket.
+ *
+ * @return FileDescriptor for the data stream.
+ * @throws IOException
+ * @see #destroy()
+ */
+ public FileDescriptor create() throws IOException {
+ if (mFd == null) {
+ mFd = createNative();
+ }
+ if (mFd == null) {
+ throw new IOException("socket not created");
+ }
+ return mFd;
+ }
+
+ private native FileDescriptor createNative();
+
+ /**
+ * Destroys a socket created by {@link #create() create}. Call this
+ * function when you no longer use the socket in order to release the
+ * underlying OS resources.
+ *
+ * @see #create()
+ */
+ public void destroy() {
+ synchronized (this) {
+ destroyNative();
+ mFd = null;
+ mIsBound = false;
+ mIsListening = false;
+ }
+ }
+
+ private native void destroyNative();
+
+ /**
+ * Returns the {@link java.io.FileDescriptor FileDescriptor} of the socket.
+ *
+ * @return the FileDescriptor
+ * @throws IOException
+ * when the socket has not been {@link #create() created}.
+ */
+ public FileDescriptor getFileDescriptor() throws IOException {
+ if (mFd == null) {
+ throw new IOException("socket not created");
+ }
+ return mFd;
+ }
+
+ /**
+ * Retrieves the input stream from the socket. Alternatively, you can do
+ * that from the FileDescriptor returned by {@link #create() create} or
+ * {@link #accept(RfcommSocket, int) accept}.
+ *
+ * @return InputStream
+ * @throws IOException
+ * if you have not called {@link #create() create} on the
+ * socket.
+ */
+ public InputStream getInputStream() throws IOException {
+ if (mFd == null) {
+ throw new IOException("socket not created");
+ }
+
+ synchronized (this) {
+ if (mInputStream == null) {
+ mInputStream = new FileInputStream(mFd);
+ }
+
+ return mInputStream;
+ }
+ }
+
+ /**
+ * Retrieves the output stream from the socket. Alternatively, you can do
+ * that from the FileDescriptor returned by {@link #create() create} or
+ * {@link #accept(RfcommSocket, int) accept}.
+ *
+ * @return OutputStream
+ * @throws IOException
+ * if you have not called {@link #create() create} on the
+ * socket.
+ */
+ public OutputStream getOutputStream() throws IOException {
+ if (mFd == null) {
+ throw new IOException("socket not created");
+ }
+
+ synchronized (this) {
+ if (mOutputStream == null) {
+ mOutputStream = new FileOutputStream(mFd);
+ }
+
+ return mOutputStream;
+ }
+ }
+
+ /**
+ * Starts a blocking connect to a remote RFCOMM socket. It takes the address
+ * of a device and the RFCOMM channel (port) to which to connect.
+ *
+ * @param address
+ * is the Bluetooth address of the remote device.
+ * @param port
+ * is the RFCOMM channel
+ * @return true on success, false on failure
+ * @throws IOException
+ * if {@link #create() create} has not been called.
+ * @see #connectAsync(String, int)
+ */
+ public boolean connect(String address, int port) throws IOException {
+ synchronized (this) {
+ if (mFd == null) {
+ throw new IOException("socket not created");
+ }
+ return connectNative(address, port);
+ }
+ }
+
+ private native boolean connectNative(String address, int port);
+
+ /**
+ * Starts an asynchronous (nonblocking) connect to a remote RFCOMM socket.
+ * It takes the address of the device to connect to, as well as the RFCOMM
+ * channel (port). On successful return (return value is true), you need to
+ * call method {@link #waitForAsyncConnect(int) waitForAsyncConnect} to
+ * block for up to a specified number of milliseconds while waiting for the
+ * asyncronous connect to complete.
+ *
+ * @param address
+ * of remote device
+ * @param port
+ * the RFCOMM channel
+ * @return true when the asynchronous connect has successfully started,
+ * false if there was an error.
+ * @throws IOException
+ * is you have not called {@link #create() create}
+ * @see #waitForAsyncConnect(int)
+ * @see #getRemainingAsyncConnectWaitingTimeMs()
+ * @see #connect(String, int)
+ */
+ public boolean connectAsync(String address, int port) throws IOException {
+ synchronized (this) {
+ if (mFd == null) {
+ throw new IOException("socket not created");
+ }
+ mIsConnecting = connectAsyncNative(address, port);
+ return mIsConnecting;
+ }
+ }
+
+ private native boolean connectAsyncNative(String address, int port);
+
+ /**
+ * Interrupts an asynchronous connect in progress. This method does nothing
+ * when there is no asynchronous connect in progress.
+ *
+ * @throws IOException
+ * if you have not called {@link #create() create}.
+ * @see #connectAsync(String, int)
+ */
+ public void interruptAsyncConnect() throws IOException {
+ synchronized (this) {
+ if (mFd == null) {
+ throw new IOException("socket not created");
+ }
+ if (mIsConnecting) {
+ mIsConnecting = !interruptAsyncConnectNative();
+ }
+ }
+ }
+
+ private native boolean interruptAsyncConnectNative();
+
+ /**
+ * Tells you whether there is an asynchronous connect in progress. This
+ * method returns an undefined value when there is a synchronous connect in
+ * progress.
+ *
+ * @return true if there is an asyc connect in progress, false otherwise
+ * @see #connectAsync(String, int)
+ */
+ public boolean isConnecting() {
+ return mIsConnecting;
+ }
+
+ /**
+ * Blocks for a specified amount of milliseconds while waiting for an
+ * asynchronous connect to complete. Returns an integer value to indicate
+ * one of the following: the connect succeeded, the connect is still in
+ * progress, or the connect failed. It is possible for this method to block
+ * for less than the time specified by the user, and still return zero
+ * (i.e., async connect is still in progress.) For this reason, if the
+ * return value is zero, you need to call method
+ * {@link #getRemainingAsyncConnectWaitingTimeMs() getRemainingAsyncConnectWaitingTimeMs}
+ * to retrieve the remaining time.
+ *
+ * @param timeoutMs
+ * the time to block while waiting for the async connect to
+ * complete.
+ * @return a positive value if the connect succeeds; zero, if the connect is
+ * still in progress, and a negative value if the connect failed.
+ *
+ * @throws IOException
+ * @see #getRemainingAsyncConnectWaitingTimeMs()
+ * @see #connectAsync(String, int)
+ */
+ public int waitForAsyncConnect(int timeoutMs) throws IOException {
+ synchronized (this) {
+ if (mFd == null) {
+ throw new IOException("socket not created");
+ }
+ int ret = waitForAsyncConnectNative(timeoutMs);
+ if (ret != 0) {
+ mIsConnecting = false;
+ }
+ return ret;
+ }
+ }
+
+ private native int waitForAsyncConnectNative(int timeoutMs);
+
+ /**
+ * Returns the number of milliseconds left to wait after the last call to
+ * {@link #waitForAsyncConnect(int) waitForAsyncConnect}.
+ *
+ * It is possible that waitForAsyncConnect() waits for less than the time
+ * specified by the user, and still returns zero (i.e., async connect is
+ * still in progress.) For this reason, if the return value is zero, you
+ * need to call this method to retrieve the remaining time before you call
+ * waitForAsyncConnect again.
+ *
+ * @return the remaining timeout in milliseconds.
+ * @see #waitForAsyncConnect(int)
+ * @see #connectAsync(String, int)
+ */
+ public int getRemainingAsyncConnectWaitingTimeMs() {
+ return mTimeoutRemainingMs;
+ }
+
+ /**
+ * Shuts down both directions on a socket.
+ *
+ * @return true on success, false on failure; if the return value is false,
+ * the socket might be left in a patially shut-down state (i.e. one
+ * direction is shut down, but the other is still open.) In this
+ * case, you should {@link #destroy() destroy} and then
+ * {@link #create() create} the socket again.
+ * @throws IOException
+ * is you have not caled {@link #create() create}.
+ * @see #shutdownInput()
+ * @see #shutdownOutput()
+ */
+ public boolean shutdown() throws IOException {
+ synchronized (this) {
+ if (mFd == null) {
+ throw new IOException("socket not created");
+ }
+ if (shutdownNative(true)) {
+ return shutdownNative(false);
+ }
+
+ return false;
+ }
+ }
+
+ /**
+ * Shuts down the input stream of the socket, but leaves the output stream
+ * in its current state.
+ *
+ * @return true on success, false on failure
+ * @throws IOException
+ * is you have not called {@link #create() create}
+ * @see #shutdown()
+ * @see #shutdownOutput()
+ */
+ public boolean shutdownInput() throws IOException {
+ synchronized (this) {
+ if (mFd == null) {
+ throw new IOException("socket not created");
+ }
+ return shutdownNative(true);
+ }
+ }
+
+ /**
+ * Shut down the output stream of the socket, but leaves the input stream in
+ * its current state.
+ *
+ * @return true on success, false on failure
+ * @throws IOException
+ * is you have not called {@link #create() create}
+ * @see #shutdown()
+ * @see #shutdownInput()
+ */
+ public boolean shutdownOutput() throws IOException {
+ synchronized (this) {
+ if (mFd == null) {
+ throw new IOException("socket not created");
+ }
+ return shutdownNative(false);
+ }
+ }
+
+ private native boolean shutdownNative(boolean shutdownInput);
+
+ /**
+ * Tells you whether a socket is connected to another socket. This could be
+ * for input or output or both.
+ *
+ * @return true if connected, false otherwise.
+ * @see #isInputConnected()
+ * @see #isOutputConnected()
+ */
+ public boolean isConnected() {
+ return isConnectedNative() > 0;
+ }
+
+ /**
+ * Determines whether input is connected (i.e., whether you can receive data
+ * on this socket.)
+ *
+ * @return true if input is connected, false otherwise.
+ * @see #isConnected()
+ * @see #isOutputConnected()
+ */
+ public boolean isInputConnected() {
+ return (isConnectedNative() & 1) != 0;
+ }
+
+ /**
+ * Determines whether output is connected (i.e., whether you can send data
+ * on this socket.)
+ *
+ * @return true if output is connected, false otherwise.
+ * @see #isConnected()
+ * @see #isInputConnected()
+ */
+ public boolean isOutputConnected() {
+ return (isConnectedNative() & 2) != 0;
+ }
+
+ private native int isConnectedNative();
+
+ /**
+ * Binds a listening socket to the local device, or a non-listening socket
+ * to a remote device. The port is automatically selected as the first
+ * available port in the range 12 to 30.
+ *
+ * NOTE: Currently we ignore the device parameter and always bind the socket
+ * to the local device, assuming that it is a listening socket.
+ *
+ * TODO: Use bind(0) in native code to have the kernel select an unused
+ * port.
+ *
+ * @param device
+ * Bluetooth address of device to bind to (currently ignored).
+ * @return true on success, false on failure
+ * @throws IOException
+ * if you have not called {@link #create() create}
+ * @see #listen(int)
+ * @see #accept(RfcommSocket,int)
+ */
+ public boolean bind(String device) throws IOException {
+ if (mFd == null) {
+ throw new IOException("socket not created");
+ }
+ for (int port = 12; port <= 30; port++) {
+ if (bindNative(device, port)) {
+ mIsBound = true;
+ return true;
+ }
+ }
+ mIsBound = false;
+ return false;
+ }
+
+ /**
+ * Binds a listening socket to the local device, or a non-listening socket
+ * to a remote device.
+ *
+ * NOTE: Currently we ignore the device parameter and always bind the socket
+ * to the local device, assuming that it is a listening socket.
+ *
+ * @param device
+ * Bluetooth address of device to bind to (currently ignored).
+ * @param port
+ * RFCOMM channel to bind socket to.
+ * @return true on success, false on failure
+ * @throws IOException
+ * if you have not called {@link #create() create}
+ * @see #listen(int)
+ * @see #accept(RfcommSocket,int)
+ */
+ public boolean bind(String device, int port) throws IOException {
+ if (mFd == null) {
+ throw new IOException("socket not created");
+ }
+ mIsBound = bindNative(device, port);
+ return mIsBound;
+ }
+
+ private native boolean bindNative(String device, int port);
+
+ /**
+ * Starts listening for incoming connections on this socket, after it has
+ * been bound to an address and RFCOMM channel with
+ * {@link #bind(String,int) bind}.
+ *
+ * @param backlog
+ * the number of pending incoming connections to queue for
+ * {@link #accept(RfcommSocket, int) accept}.
+ * @return true on success, false on failure
+ * @throws IOException
+ * if you have not called {@link #create() create} or if the
+ * socket has not been bound to a device and RFCOMM channel.
+ */
+ public boolean listen(int backlog) throws IOException {
+ if (mFd == null) {
+ throw new IOException("socket not created");
+ }
+ if (!mIsBound) {
+ throw new IOException("socket not bound");
+ }
+ mIsListening = listenNative(backlog);
+ return mIsListening;
+ }
+
+ private native boolean listenNative(int backlog);
+
+ /**
+ * Accepts incoming-connection requests for a listening socket bound to an
+ * RFCOMM channel. The user may provide a time to wait for an incoming
+ * connection.
+ *
+ * Note that this method may return null (i.e., no incoming connection)
+ * before the user-specified timeout expires. For this reason, on a null
+ * return value, you need to call
+ * {@link #getRemainingAcceptWaitingTimeMs() getRemainingAcceptWaitingTimeMs}
+ * in order to see how much time is left to wait, before you call this
+ * method again.
+ *
+ * @param newSock
+ * is set to the new socket that is created as a result of a
+ * successful accept.
+ * @param timeoutMs
+ * time (in milliseconds) to block while waiting to an
+ * incoming-connection request. A negative value is an infinite
+ * wait.
+ * @return FileDescriptor of newSock on success, null on failure. Failure
+ * occurs if the timeout expires without a successful connect.
+ * @throws IOException
+ * if the socket has not been {@link #create() create}ed, is
+ * not bound, or is not a listening socket.
+ * @see #bind(String, int)
+ * @see #listen(int)
+ * @see #getRemainingAcceptWaitingTimeMs()
+ */
+ public FileDescriptor accept(RfcommSocket newSock, int timeoutMs)
+ throws IOException {
+ synchronized (newSock) {
+ if (mFd == null) {
+ throw new IOException("socket not created");
+ }
+ if (mIsListening == false) {
+ throw new IOException("not listening on socket");
+ }
+ newSock.mFd = acceptNative(newSock, timeoutMs);
+ return newSock.mFd;
+ }
+ }
+
+ /**
+ * Returns the number of milliseconds left to wait after the last call to
+ * {@link #accept(RfcommSocket, int) accept}.
+ *
+ * Since accept() may return null (i.e., no incoming connection) before the
+ * user-specified timeout expires, you need to call this method in order to
+ * see how much time is left to wait, and wait for that amount of time
+ * before you call accept again.
+ *
+ * @return the remaining time, in milliseconds.
+ */
+ public int getRemainingAcceptWaitingTimeMs() {
+ return mAcceptTimeoutRemainingMs;
+ }
+
+ private native FileDescriptor acceptNative(RfcommSocket newSock,
+ int timeoutMs);
+
+ /**
+ * Get the port (rfcomm channel) associated with this socket.
+ *
+ * This is only valid if the port has been set via a successful call to
+ * {@link #bind(String, int)}, {@link #connect(String, int)}
+ * or {@link #connectAsync(String, int)}. This can be checked
+ * with {@link #isListening()} and {@link #isConnected()}.
+ * @return Port (rfcomm channel)
+ */
+ public int getPort() throws IOException {
+ if (mFd == null) {
+ throw new IOException("socket not created");
+ }
+ if (!mIsListening && !isConnected()) {
+ throw new IOException("not listening or connected on socket");
+ }
+ return mPort;
+ }
+
+ /**
+ * Return true if this socket is listening ({@link #listen(int)}
+ * has been called successfully).
+ */
+ public boolean isListening() {
+ return mIsListening;
+ }
+}
diff --git a/core/java/android/bluetooth/ScoSocket.java b/core/java/android/bluetooth/ScoSocket.java
new file mode 100644
index 0000000..75b3329
--- /dev/null
+++ b/core/java/android/bluetooth/ScoSocket.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.util.Log;
+
+import java.io.IOException;
+import java.lang.Thread;
+
+
+/**
+ * The Android Bluetooth API is not finalized, and *will* change. Use at your
+ * own risk.
+ *
+ * Simple SCO Socket.
+ * Currently in Android, there is no support for sending data over a SCO
+ * socket - this is managed by the hardware link to the Bluetooth Chip. This
+ * class is instead intended for management of the SCO socket lifetime,
+ * and is tailored for use with the headset / handsfree profiles.
+ * @hide
+ */
+public class ScoSocket {
+ private static final String TAG = "ScoSocket";
+ private static final boolean DBG = true;
+ private static final boolean VDBG = false; // even more logging
+
+ public static final int STATE_READY = 1; // Ready for use. No threads or sockets
+ public static final int STATE_ACCEPT = 2; // accept() thread running
+ public static final int STATE_CONNECTING = 3; // connect() thread running
+ public static final int STATE_CONNECTED = 4; // connected, waiting for close()
+ public static final int STATE_CLOSED = 5; // was connected, now closed.
+
+ private int mState;
+ private int mNativeData;
+ private Handler mHandler;
+ private int mAcceptedCode;
+ private int mConnectedCode;
+ private int mClosedCode;
+
+ private WakeLock mWakeLock; // held while STATE_CONNECTING or STATE_CONNECTED
+
+ static {
+ classInitNative();
+ }
+ private native static void classInitNative();
+
+ public ScoSocket(PowerManager pm, Handler handler, int acceptedCode, int connectedCode,
+ int closedCode) {
+ initNative();
+ mState = STATE_READY;
+ mHandler = handler;
+ mAcceptedCode = acceptedCode;
+ mConnectedCode = connectedCode;
+ mClosedCode = closedCode;
+ mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "ScoSocket");
+ mWakeLock.setReferenceCounted(false);
+ if (VDBG) log(this + " SCO OBJECT CTOR");
+ }
+ private native void initNative();
+
+ protected void finalize() throws Throwable {
+ try {
+ if (VDBG) log(this + " SCO OBJECT DTOR");
+ destroyNative();
+ releaseWakeLock();
+ } finally {
+ super.finalize();
+ }
+ }
+ private native void destroyNative();
+
+ /** Connect this SCO socket to the given BT address.
+ * Does not block.
+ */
+ public synchronized boolean connect(String address) {
+ if (VDBG) log("connect() " + this);
+ if (mState != STATE_READY) {
+ if (DBG) log("connect(): Bad state");
+ return false;
+ }
+ acquireWakeLock();
+ if (connectNative(address)) {
+ mState = STATE_CONNECTING;
+ return true;
+ } else {
+ mState = STATE_CLOSED;
+ releaseWakeLock();
+ return false;
+ }
+ }
+ private native boolean connectNative(String address);
+
+ /** Accept incoming SCO connections.
+ * Does not block.
+ */
+ public synchronized boolean accept() {
+ if (VDBG) log("accept() " + this);
+ if (mState != STATE_READY) {
+ if (DBG) log("Bad state");
+ return false;
+ }
+ if (acceptNative()) {
+ mState = STATE_ACCEPT;
+ return true;
+ } else {
+ mState = STATE_CLOSED;
+ return false;
+ }
+ }
+ private native boolean acceptNative();
+
+ public synchronized void close() {
+ if (DBG) log(this + " SCO OBJECT close() mState = " + mState);
+ mState = STATE_CLOSED;
+ closeNative();
+ releaseWakeLock();
+ }
+ private native void closeNative();
+
+ public synchronized int getState() {
+ return mState;
+ }
+
+ private synchronized void onConnected(int result) {
+ if (VDBG) log(this + " onConnected() mState = " + mState + " " + this);
+ if (mState != STATE_CONNECTING) {
+ if (DBG) log("Strange state, closing " + mState + " " + this);
+ return;
+ }
+ if (result >= 0) {
+ mState = STATE_CONNECTED;
+ } else {
+ mState = STATE_CLOSED;
+ }
+ mHandler.obtainMessage(mConnectedCode, mState, -1, this).sendToTarget();
+ if (result < 0) {
+ releaseWakeLock();
+ }
+ }
+
+ private synchronized void onAccepted(int result) {
+ if (VDBG) log("onAccepted() " + this);
+ if (mState != STATE_ACCEPT) {
+ if (DBG) log("Strange state" + this);
+ return;
+ }
+ if (result >= 0) {
+ acquireWakeLock();
+ mState = STATE_CONNECTED;
+ } else {
+ mState = STATE_CLOSED;
+ }
+ mHandler.obtainMessage(mAcceptedCode, mState, -1, this).sendToTarget();
+ }
+
+ private synchronized void onClosed() {
+ if (DBG) log("onClosed() " + this);
+ if (mState != STATE_CLOSED) {
+ mState = STATE_CLOSED;
+ mHandler.obtainMessage(mClosedCode, mState, -1, this).sendToTarget();
+ releaseWakeLock();
+ }
+ }
+
+ private void acquireWakeLock() {
+ if (!mWakeLock.isHeld()) {
+ mWakeLock.acquire();
+ if (VDBG) log("mWakeLock.acquire()" + this);
+ }
+ }
+
+ private void releaseWakeLock() {
+ if (mWakeLock.isHeld()) {
+ if (VDBG) log("mWakeLock.release()" + this);
+ mWakeLock.release();
+ }
+ }
+
+ private void log(String msg) {
+ Log.d(TAG, msg);
+ }
+}
diff --git a/core/java/android/bluetooth/package.html b/core/java/android/bluetooth/package.html
new file mode 100644
index 0000000..ccd8fec
--- /dev/null
+++ b/core/java/android/bluetooth/package.html
@@ -0,0 +1,14 @@
+<HTML>
+<BODY>
+Provides classes that manage Bluetooth functionality on the device.
+<p>
+The Bluetooth APIs allow applications can connect and disconnect headsets, or scan
+for other kinds of Bluetooth devices and pair them. Further control includes the
+ability to write and modify the local Service Discovery Protocol (SDP) database,
+query the SDP database of other Bluetooth devices, establish RFCOMM
+channels/sockets on Android, and connect to specified sockets on other devices.
+</p>
+<p>Remember, not all Android devices are guaranteed to have Bluetooth functionality.</p>
+{@hide}
+</BODY>
+</HTML>
diff --git a/core/java/android/content/AbstractTableMerger.java b/core/java/android/content/AbstractTableMerger.java
new file mode 100644
index 0000000..56e5d4a
--- /dev/null
+++ b/core/java/android/content/AbstractTableMerger.java
@@ -0,0 +1,577 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.os.Debug;
+import static android.provider.SyncConstValue._SYNC_ACCOUNT;
+import static android.provider.SyncConstValue._SYNC_DIRTY;
+import static android.provider.SyncConstValue._SYNC_ID;
+import static android.provider.SyncConstValue._SYNC_LOCAL_ID;
+import static android.provider.SyncConstValue._SYNC_MARK;
+import static android.provider.SyncConstValue._SYNC_VERSION;
+import android.text.TextUtils;
+import android.util.Log;
+
+/**
+ * @hide
+ */
+public abstract class AbstractTableMerger
+{
+ private ContentValues mValues;
+
+ protected SQLiteDatabase mDb;
+ protected String mTable;
+ protected Uri mTableURL;
+ protected String mDeletedTable;
+ protected Uri mDeletedTableURL;
+ static protected ContentValues mSyncMarkValues;
+ static private boolean TRACE;
+
+ static {
+ mSyncMarkValues = new ContentValues();
+ mSyncMarkValues.put(_SYNC_MARK, 1);
+ TRACE = false;
+ }
+
+ private static final String TAG = "AbstractTableMerger";
+ private static final String[] syncDirtyProjection =
+ new String[] {_SYNC_DIRTY, "_id", _SYNC_ID, _SYNC_VERSION};
+ private static final String[] syncIdAndVersionProjection =
+ new String[] {_SYNC_ID, _SYNC_VERSION};
+
+ private volatile boolean mIsMergeCancelled;
+
+ private static final String SELECT_MARKED = _SYNC_MARK + "> 0 and " + _SYNC_ACCOUNT + "=?";
+
+ private static final String SELECT_BY_ID_AND_ACCOUNT =
+ _SYNC_ID +"=? and " + _SYNC_ACCOUNT + "=?";
+
+ private static final String SELECT_UNSYNCED = ""
+ + _SYNC_DIRTY + " > 0 and (" + _SYNC_ACCOUNT + "=? or " + _SYNC_ACCOUNT + " is null)";
+
+ public AbstractTableMerger(SQLiteDatabase database,
+ String table, Uri tableURL, String deletedTable,
+ Uri deletedTableURL)
+ {
+ mDb = database;
+ mTable = table;
+ mTableURL = tableURL;
+ mDeletedTable = deletedTable;
+ mDeletedTableURL = deletedTableURL;
+ mValues = new ContentValues();
+ }
+
+ public abstract void insertRow(ContentProvider diffs,
+ Cursor diffsCursor);
+ public abstract void updateRow(long localPersonID,
+ ContentProvider diffs, Cursor diffsCursor);
+ public abstract void resolveRow(long localPersonID,
+ String syncID, ContentProvider diffs, Cursor diffsCursor);
+
+ /**
+ * This is called when it is determined that a row should be deleted from the
+ * ContentProvider. The localCursor is on a table from the local ContentProvider
+ * and its current position is of the row that should be deleted. The localCursor
+ * contains the complete projection of the table.
+ * <p>
+ * It is the responsibility of the implementation of this method to ensure that the cursor
+ * points to the next row when this method returns, either by calling Cursor.deleteRow() or
+ * Cursor.next().
+ *
+ * @param localCursor The Cursor into the local table, which points to the row that
+ * is to be deleted.
+ */
+ public void deleteRow(Cursor localCursor) {
+ localCursor.deleteRow();
+ }
+
+ /**
+ * After {@link #merge} has completed, this method is called to send
+ * notifications to {@link android.database.ContentObserver}s of changes
+ * to the containing {@link ContentProvider}. These notifications likely
+ * do not want to request a sync back to the network.
+ */
+ protected abstract void notifyChanges();
+
+ private static boolean findInCursor(Cursor cursor, int column, String id) {
+ while (!cursor.isAfterLast() && !cursor.isNull(column)) {
+ int comp = id.compareTo(cursor.getString(column));
+ if (comp > 0) {
+ cursor.moveToNext();
+ continue;
+ }
+ return comp == 0;
+ }
+ return false;
+ }
+
+ public void onMergeCancelled() {
+ mIsMergeCancelled = true;
+ }
+
+ /**
+ * Carry out a merge of the given diffs, and add the results to
+ * the given MergeResult. If we are the first merge to find
+ * client-side diffs, we'll use the given ContentProvider to
+ * construct a temporary instance to hold them.
+ */
+ public void merge(final SyncContext context,
+ final String account,
+ final SyncableContentProvider serverDiffs,
+ TempProviderSyncResult result,
+ SyncResult syncResult, SyncableContentProvider temporaryInstanceFactory) {
+ mIsMergeCancelled = false;
+ if (serverDiffs != null) {
+ if (!mDb.isDbLockedByCurrentThread()) {
+ throw new IllegalStateException("this must be called from within a DB transaction");
+ }
+ mergeServerDiffs(context, account, serverDiffs, syncResult);
+ notifyChanges();
+ }
+
+ if (result != null) {
+ findLocalChanges(result, temporaryInstanceFactory, account, syncResult);
+ }
+ if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "merge complete");
+ }
+
+ private void mergeServerDiffs(SyncContext context,
+ String account, SyncableContentProvider serverDiffs, SyncResult syncResult) {
+ boolean diffsArePartial = serverDiffs.getContainsDiffs();
+ // mark the current rows so that we can distinguish these from new
+ // inserts that occur during the merge
+ mDb.update(mTable, mSyncMarkValues, null, null);
+ if (mDeletedTable != null) {
+ mDb.update(mDeletedTable, mSyncMarkValues, null, null);
+ }
+
+ // load the local database entries, so we can merge them with the server
+ final String[] accountSelectionArgs = new String[]{account};
+ Cursor localCursor = mDb.query(mTable, syncDirtyProjection,
+ SELECT_MARKED, accountSelectionArgs, null, null,
+ mTable + "." + _SYNC_ID);
+ Cursor deletedCursor;
+ if (mDeletedTable != null) {
+ deletedCursor = mDb.query(mDeletedTable, syncIdAndVersionProjection,
+ SELECT_MARKED, accountSelectionArgs, null, null,
+ mDeletedTable + "." + _SYNC_ID);
+ } else {
+ deletedCursor =
+ mDb.rawQuery("select 'a' as _sync_id, 'b' as _sync_version limit 0", null);
+ }
+
+ // Apply updates and insertions from the server
+ Cursor diffsCursor = serverDiffs.query(mTableURL,
+ null, null, null, mTable + "." + _SYNC_ID);
+ int deletedSyncIDColumn = deletedCursor.getColumnIndexOrThrow(_SYNC_ID);
+ int deletedSyncVersionColumn = deletedCursor.getColumnIndexOrThrow(_SYNC_VERSION);
+ int serverSyncIDColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_ID);
+ int serverSyncVersionColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_VERSION);
+ int serverSyncLocalIdColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_LOCAL_ID);
+
+ String lastSyncId = null;
+ int diffsCount = 0;
+ int localCount = 0;
+ localCursor.moveToFirst();
+ deletedCursor.moveToFirst();
+ while (diffsCursor.moveToNext()) {
+ if (mIsMergeCancelled) {
+ localCursor.close();
+ deletedCursor.close();
+ diffsCursor.close();
+ return;
+ }
+ mDb.yieldIfContended();
+ String serverSyncId = diffsCursor.getString(serverSyncIDColumn);
+ String serverSyncVersion = diffsCursor.getString(serverSyncVersionColumn);
+ long localPersonID = 0;
+ String localSyncVersion = null;
+
+ diffsCount++;
+ context.setStatusText("Processing " + diffsCount + "/"
+ + diffsCursor.getCount());
+ if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "processing server entry " +
+ diffsCount + ", " + serverSyncId);
+
+ if (TRACE) {
+ if (diffsCount == 10) {
+ Debug.startMethodTracing("atmtrace");
+ }
+ if (diffsCount == 20) {
+ Debug.stopMethodTracing();
+ }
+ }
+
+ boolean conflict = false;
+ boolean update = false;
+ boolean insert = false;
+
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "found event with serverSyncID " + serverSyncId);
+ }
+ if (TextUtils.isEmpty(serverSyncId)) {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.e(TAG, "server entry doesn't have a serverSyncID");
+ }
+ continue;
+ }
+
+ // It is possible that the sync adapter wrote the same record multiple times,
+ // e.g. if the same record came via multiple feeds. If this happens just ignore
+ // the duplicate records.
+ if (serverSyncId.equals(lastSyncId)) {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "skipping record with duplicate remote server id " + lastSyncId);
+ }
+ continue;
+ }
+ lastSyncId = serverSyncId;
+
+ String localSyncID = null;
+ boolean localSyncDirty = false;
+
+ while (!localCursor.isAfterLast()) {
+ if (mIsMergeCancelled) {
+ localCursor.deactivate();
+ deletedCursor.deactivate();
+ diffsCursor.deactivate();
+ return;
+ }
+ localCount++;
+ localSyncID = localCursor.getString(2);
+
+ // If the local record doesn't have a _sync_id then
+ // it is new. Ignore it for now, we will send an insert
+ // the the server later.
+ if (TextUtils.isEmpty(localSyncID)) {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "local record " +
+ localCursor.getLong(1) +
+ " has no _sync_id, ignoring");
+ }
+ localCursor.moveToNext();
+ localSyncID = null;
+ continue;
+ }
+
+ int comp = serverSyncId.compareTo(localSyncID);
+
+ // the local DB has a record that the server doesn't have
+ if (comp > 0) {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "local record " +
+ localCursor.getLong(1) +
+ " has _sync_id " + localSyncID +
+ " that is < server _sync_id " + serverSyncId);
+ }
+ if (diffsArePartial) {
+ localCursor.moveToNext();
+ } else {
+ deleteRow(localCursor);
+ if (mDeletedTable != null) {
+ mDb.delete(mDeletedTable, _SYNC_ID +"=?", new String[] {localSyncID});
+ }
+ syncResult.stats.numDeletes++;
+ mDb.yieldIfContended();
+ }
+ localSyncID = null;
+ continue;
+ }
+
+ // the server has a record that the local DB doesn't have
+ if (comp < 0) {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "local record " +
+ localCursor.getLong(1) +
+ " has _sync_id " + localSyncID +
+ " that is > server _sync_id " + serverSyncId);
+ }
+ localSyncID = null;
+ }
+
+ // the server and the local DB both have this record
+ if (comp == 0) {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "local record " +
+ localCursor.getLong(1) +
+ " has _sync_id " + localSyncID +
+ " that matches the server _sync_id");
+ }
+ localSyncDirty = localCursor.getInt(0) != 0;
+ localPersonID = localCursor.getLong(1);
+ localSyncVersion = localCursor.getString(3);
+ localCursor.moveToNext();
+ }
+
+ break;
+ }
+
+ // If this record is in the deleted table then update the server version
+ // in the deleted table, if necessary, and then ignore it here.
+ // We will send a deletion indication to the server down a
+ // little further.
+ if (findInCursor(deletedCursor, deletedSyncIDColumn, serverSyncId)) {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "remote record " + serverSyncId + " is in the deleted table");
+ }
+ final String deletedSyncVersion = deletedCursor.getString(deletedSyncVersionColumn);
+ if (!TextUtils.equals(deletedSyncVersion, serverSyncVersion)) {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "setting version of deleted record " + serverSyncId + " to "
+ + serverSyncVersion);
+ }
+ ContentValues values = new ContentValues();
+ values.put(_SYNC_VERSION, serverSyncVersion);
+ mDb.update(mDeletedTable, values, "_sync_id=?", new String[]{serverSyncId});
+ }
+ continue;
+ }
+
+ // If the _sync_local_id is set and > -1 in the diffsCursor
+ // then this record corresponds to a local record that was just
+ // inserted into the server and the _sync_local_id is the row id
+ // of the local record. Set these fields so that the next check
+ // treats this record as an update, which will allow the
+ // merger to update the record with the server's sync id
+ long serverLocalSyncId =
+ diffsCursor.isNull(serverSyncLocalIdColumn)
+ ? -1
+ : diffsCursor.getLong(serverSyncLocalIdColumn);
+ if (serverLocalSyncId > -1) {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "the remote record with sync id "
+ + serverSyncId + " has a local sync id, "
+ + serverLocalSyncId);
+ localSyncID = serverSyncId;
+ localSyncDirty = false;
+ localPersonID = serverLocalSyncId;
+ localSyncVersion = null;
+ }
+
+ if (!TextUtils.isEmpty(localSyncID)) {
+ // An existing server item has changed
+ boolean recordChanged = (localSyncVersion == null) ||
+ !serverSyncVersion.equals(localSyncVersion);
+ if (recordChanged) {
+ if (localSyncDirty) {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG,
+ "remote record " +
+ serverSyncId +
+ " conflicts with local _sync_id " +
+ localSyncID + ", local _id " +
+ localPersonID);
+ }
+ conflict = true;
+ } else {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG,
+ "remote record " +
+ serverSyncId +
+ " updates local _sync_id " +
+ localSyncID + ", local _id " +
+ localPersonID);
+ }
+ update = true;
+ }
+ }
+ } else {
+ // the local db doesn't know about this record so add it
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "remote record "
+ + serverSyncId + " is new, inserting");
+ }
+ insert = true;
+ }
+
+ if (update) {
+ updateRow(localPersonID, serverDiffs, diffsCursor);
+ syncResult.stats.numUpdates++;
+ } else if (conflict) {
+ resolveRow(localPersonID, serverSyncId, serverDiffs,
+ diffsCursor);
+ syncResult.stats.numUpdates++;
+ } else if (insert) {
+ insertRow(serverDiffs, diffsCursor);
+ syncResult.stats.numInserts++;
+ }
+ }
+
+ if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "processed " + diffsCount +
+ " server entries");
+
+ // If tombstones aren't in use delete any remaining local rows that
+ // don't have corresponding server rows. Keep the rows that don't
+ // have a sync id since those were created locally and haven't been
+ // synced to the server yet.
+ if (!diffsArePartial) {
+ while (!localCursor.isAfterLast() &&
+ !TextUtils.isEmpty(localCursor.getString(2))) {
+ if (mIsMergeCancelled) {
+ localCursor.deactivate();
+ deletedCursor.deactivate();
+ diffsCursor.deactivate();
+ return;
+ }
+ localCount++;
+ final String localSyncId = localCursor.getString(2);
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG,
+ "deleting local record " +
+ localCursor.getLong(1) +
+ " _sync_id " + localSyncId);
+ }
+ deleteRow(localCursor);
+ if (mDeletedTable != null) {
+ mDb.delete(mDeletedTable, _SYNC_ID + "=?", new String[] {localSyncId});
+ }
+ syncResult.stats.numDeletes++;
+ mDb.yieldIfContended();
+ }
+ }
+
+ if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "checked " + localCount +
+ " local entries");
+ diffsCursor.deactivate();
+ localCursor.deactivate();
+ deletedCursor.deactivate();
+
+ if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "applying deletions from the server");
+
+ // Apply deletions from the server
+ if (mDeletedTableURL != null) {
+ diffsCursor = serverDiffs.query(mDeletedTableURL, null, null, null, null);
+ serverSyncIDColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_ID);
+
+ while (diffsCursor.moveToNext()) {
+ if (mIsMergeCancelled) {
+ diffsCursor.deactivate();
+ return;
+ }
+ // delete all rows that match each element in the diffsCursor
+ fullyDeleteRowsWithSyncId(diffsCursor.getString(serverSyncIDColumn), account,
+ syncResult);
+ mDb.yieldIfContended();
+ }
+ diffsCursor.deactivate();
+ }
+ }
+
+ private void fullyDeleteRowsWithSyncId(String syncId, String account, SyncResult syncResult) {
+ final String[] selectionArgs = new String[]{syncId, account};
+ // delete the rows explicitly so that the delete operation can be overridden
+ Cursor c = mDb.query(mTable, new String[]{"_id"}, SELECT_BY_ID_AND_ACCOUNT,
+ selectionArgs, null, null, null);
+ try {
+ c.moveToFirst();
+ while (!c.isAfterLast()) {
+ deleteRow(c); // advances the cursor
+ syncResult.stats.numDeletes++;
+ }
+ } finally {
+ c.deactivate();
+ }
+ if (mDeletedTable != null) {
+ mDb.delete(mDeletedTable, SELECT_BY_ID_AND_ACCOUNT, selectionArgs);
+ }
+ }
+
+ /**
+ * Converts cursor into a Map, using the correct types for the values.
+ */
+ protected void cursorRowToContentValues(Cursor cursor, ContentValues map) {
+ DatabaseUtils.cursorRowToContentValues(cursor, map);
+ }
+
+ /**
+ * Finds local changes, placing the results in the given result object.
+ * @param temporaryInstanceFactory As an optimization for the case
+ * where there are no client-side diffs, mergeResult may initially
+ * have no {@link android.content.TempProviderSyncResult#tempContentProvider}. If this is
+ * the first in the sequence of AbstractTableMergers to find
+ * client-side diffs, it will use the given ContentProvider to
+ * create a temporary instance and store its {@link
+ * ContentProvider} in the mergeResult.
+ * @param account
+ * @param syncResult
+ */
+ private void findLocalChanges(TempProviderSyncResult mergeResult,
+ SyncableContentProvider temporaryInstanceFactory, String account,
+ SyncResult syncResult) {
+ SyncableContentProvider clientDiffs = mergeResult.tempContentProvider;
+ if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "generating client updates");
+
+ final String[] accountSelectionArgs = new String[]{account};
+
+ // Generate the client updates and insertions
+ // Create a cursor for dirty records
+ Cursor localChangesCursor = mDb.query(mTable, null, SELECT_UNSYNCED, accountSelectionArgs,
+ null, null, null);
+ long numInsertsOrUpdates = localChangesCursor.getCount();
+ while (localChangesCursor.moveToNext()) {
+ if (mIsMergeCancelled) {
+ localChangesCursor.close();
+ return;
+ }
+ if (clientDiffs == null) {
+ clientDiffs = temporaryInstanceFactory.getTemporaryInstance();
+ }
+ mValues.clear();
+ cursorRowToContentValues(localChangesCursor, mValues);
+ mValues.remove("_id");
+ DatabaseUtils.cursorLongToContentValues(localChangesCursor, "_id", mValues,
+ _SYNC_LOCAL_ID);
+ clientDiffs.insert(mTableURL, mValues);
+ }
+ localChangesCursor.close();
+
+ // Generate the client deletions
+ if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "generating client deletions");
+ long numEntries = DatabaseUtils.queryNumEntries(mDb, mTable);
+ long numDeletedEntries = 0;
+ if (mDeletedTable != null) {
+ Cursor deletedCursor = mDb.query(mDeletedTable,
+ syncIdAndVersionProjection, _SYNC_ACCOUNT + "=?", accountSelectionArgs,
+ null, null, mDeletedTable + "." + _SYNC_ID);
+
+ numDeletedEntries = deletedCursor.getCount();
+ while (deletedCursor.moveToNext()) {
+ if (mIsMergeCancelled) {
+ deletedCursor.close();
+ return;
+ }
+ if (clientDiffs == null) {
+ clientDiffs = temporaryInstanceFactory.getTemporaryInstance();
+ }
+ mValues.clear();
+ DatabaseUtils.cursorRowToContentValues(deletedCursor, mValues);
+ clientDiffs.insert(mDeletedTableURL, mValues);
+ }
+ deletedCursor.close();
+ }
+
+ if (clientDiffs != null) {
+ mergeResult.tempContentProvider = clientDiffs;
+ }
+ syncResult.stats.numDeletes += numDeletedEntries;
+ syncResult.stats.numUpdates += numInsertsOrUpdates;
+ syncResult.stats.numEntries += numEntries;
+ }
+}
diff --git a/core/java/android/content/ActivityNotFoundException.java b/core/java/android/content/ActivityNotFoundException.java
new file mode 100644
index 0000000..16149bb
--- /dev/null
+++ b/core/java/android/content/ActivityNotFoundException.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+/**
+ * This exception is thrown when a call to {@link Context#startActivity} or
+ * one of its variants fails because an Activity can not be found to execute
+ * the given Intent.
+ */
+public class ActivityNotFoundException extends RuntimeException
+{
+ public ActivityNotFoundException()
+ {
+ }
+
+ public ActivityNotFoundException(String name)
+ {
+ super(name);
+ }
+};
+
diff --git a/core/java/android/content/AsyncQueryHandler.java b/core/java/android/content/AsyncQueryHandler.java
new file mode 100644
index 0000000..48f1bc7
--- /dev/null
+++ b/core/java/android/content/AsyncQueryHandler.java
@@ -0,0 +1,338 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Log;
+
+/**
+ * A helper class to help make handling asynchronous {@link ContentResolver}
+ * queries easier.
+ */
+public abstract class AsyncQueryHandler extends Handler {
+ private static final String TAG = "AsyncQuery";
+ private static final boolean localLOGV = false;
+
+ private static final int EVENT_ARG_QUERY = 1;
+ private static final int EVENT_ARG_INSERT = 2;
+ private static final int EVENT_ARG_UPDATE = 3;
+ private static final int EVENT_ARG_DELETE = 4;
+
+ /* package */ ContentResolver mResolver;
+
+ private static Looper sLooper = null;
+
+ private Handler mWorkerThreadHandler;
+
+ protected static final class WorkerArgs {
+ public Uri uri;
+ public Handler handler;
+ public String[] projection;
+ public String selection;
+ public String[] selectionArgs;
+ public String orderBy;
+ public Object result;
+ public Object cookie;
+ public ContentValues values;
+ }
+
+ protected class WorkerHandler extends Handler {
+ public WorkerHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ WorkerArgs args = (WorkerArgs) msg.obj;
+
+ int token = msg.what;
+ int event = msg.arg1;
+
+ switch (event) {
+ case EVENT_ARG_QUERY:
+ Cursor cursor;
+ try {
+ cursor = mResolver.query(args.uri, args.projection,
+ args.selection, args.selectionArgs,
+ args.orderBy);
+ } catch (Exception e) {
+ cursor = null;
+ }
+
+ args.result = cursor;
+ break;
+
+ case EVENT_ARG_INSERT:
+ args.result = mResolver.insert(args.uri, args.values);
+ break;
+
+ case EVENT_ARG_UPDATE:
+ int r = mResolver.update(args.uri, args.values, args.selection,
+ args.selectionArgs);
+ args.result = new Integer(r);
+ break;
+
+ case EVENT_ARG_DELETE:
+ int r2 = mResolver.delete(args.uri, args.selection, args.selectionArgs);
+ args.result = new Integer(r2);
+ break;
+
+ }
+
+ // passing the original token value back to the caller
+ // on top of the event values in arg1.
+ Message reply = args.handler.obtainMessage(token);
+ reply.obj = args;
+ reply.arg1 = msg.arg1;
+
+ if (localLOGV) {
+ Log.d(TAG, "WorkerHandler.handleMsg: msg.arg1=" + msg.arg1
+ + ", reply.what=" + reply.what);
+ }
+
+ reply.sendToTarget();
+ }
+ }
+
+ public AsyncQueryHandler(ContentResolver cr) {
+ super();
+ mResolver = cr;
+ synchronized (AsyncQueryHandler.class) {
+ if (sLooper == null) {
+ HandlerThread thread = new HandlerThread("AsyncQueryWorker");
+ thread.start();
+
+ sLooper = thread.getLooper();
+ }
+ }
+ mWorkerThreadHandler = createHandler(sLooper);
+ }
+
+ protected Handler createHandler(Looper looper) {
+ return new WorkerHandler(looper);
+ }
+
+ /**
+ * This method begins an asynchronous query. When the query is done
+ * {@link #onQueryComplete} is called.
+ *
+ * @param token A token passed into {@link #onQueryComplete} to identify
+ * the query.
+ * @param cookie An object that gets passed into {@link #onQueryComplete}
+ */
+ public void startQuery(int token, Object cookie, Uri uri,
+ String[] projection, String selection, String[] selectionArgs,
+ String orderBy) {
+ // Use the token as what so cancelOperations works properly
+ Message msg = mWorkerThreadHandler.obtainMessage(token);
+ msg.arg1 = EVENT_ARG_QUERY;
+
+ WorkerArgs args = new WorkerArgs();
+ args.handler = this;
+ args.uri = uri;
+ args.projection = projection;
+ args.selection = selection;
+ args.selectionArgs = selectionArgs;
+ args.orderBy = orderBy;
+ args.cookie = cookie;
+ msg.obj = args;
+
+ mWorkerThreadHandler.sendMessage(msg);
+ }
+
+ /**
+ * Attempts to cancel operation that has not already started. Note that
+ * there is no guarantee that the operation will be canceled. They still may
+ * result in a call to on[Query/Insert/Update/Delete]Complete after this
+ * call has completed.
+ *
+ * @param token The token representing the operation to be canceled.
+ * If multiple operations have the same token they will all be canceled.
+ */
+ public final void cancelOperation(int token) {
+ mWorkerThreadHandler.removeMessages(token);
+ }
+
+ /**
+ * This method begins an asynchronous insert. When the insert operation is
+ * done {@link #onInsertComplete} is called.
+ *
+ * @param token A token passed into {@link #onInsertComplete} to identify
+ * the insert operation.
+ * @param cookie An object that gets passed into {@link #onInsertComplete}
+ * @param uri the Uri passed to the insert operation.
+ * @param initialValues the ContentValues parameter passed to the insert operation.
+ */
+ public final void startInsert(int token, Object cookie, Uri uri,
+ ContentValues initialValues) {
+ // Use the token as what so cancelOperations works properly
+ Message msg = mWorkerThreadHandler.obtainMessage(token);
+ msg.arg1 = EVENT_ARG_INSERT;
+
+ WorkerArgs args = new WorkerArgs();
+ args.handler = this;
+ args.uri = uri;
+ args.cookie = cookie;
+ args.values = initialValues;
+ msg.obj = args;
+
+ mWorkerThreadHandler.sendMessage(msg);
+ }
+
+ /**
+ * This method begins an asynchronous update. When the update operation is
+ * done {@link #onUpdateComplete} is called.
+ *
+ * @param token A token passed into {@link #onUpdateComplete} to identify
+ * the update operation.
+ * @param cookie An object that gets passed into {@link #onUpdateComplete}
+ * @param uri the Uri passed to the update operation.
+ * @param values the ContentValues parameter passed to the update operation.
+ */
+ public final void startUpdate(int token, Object cookie, Uri uri,
+ ContentValues values, String selection, String[] selectionArgs) {
+ // Use the token as what so cancelOperations works properly
+ Message msg = mWorkerThreadHandler.obtainMessage(token);
+ msg.arg1 = EVENT_ARG_UPDATE;
+
+ WorkerArgs args = new WorkerArgs();
+ args.handler = this;
+ args.uri = uri;
+ args.cookie = cookie;
+ args.values = values;
+ args.selection = selection;
+ args.selectionArgs = selectionArgs;
+ msg.obj = args;
+
+ mWorkerThreadHandler.sendMessage(msg);
+ }
+
+ /**
+ * This method begins an asynchronous delete. When the delete operation is
+ * done {@link #onDeleteComplete} is called.
+ *
+ * @param token A token passed into {@link #onDeleteComplete} to identify
+ * the delete operation.
+ * @param cookie An object that gets passed into {@link #onDeleteComplete}
+ * @param uri the Uri passed to the delete operation.
+ * @param selection the where clause.
+ */
+ public final void startDelete(int token, Object cookie, Uri uri,
+ String selection, String[] selectionArgs) {
+ // Use the token as what so cancelOperations works properly
+ Message msg = mWorkerThreadHandler.obtainMessage(token);
+ msg.arg1 = EVENT_ARG_DELETE;
+
+ WorkerArgs args = new WorkerArgs();
+ args.handler = this;
+ args.uri = uri;
+ args.cookie = cookie;
+ args.selection = selection;
+ args.selectionArgs = selectionArgs;
+ msg.obj = args;
+
+ mWorkerThreadHandler.sendMessage(msg);
+ }
+
+ /**
+ * Called when an asynchronous query is completed.
+ *
+ * @param token the token to identify the query, passed in from
+ * {@link #startQuery}.
+ * @param cookie the cookie object that's passed in from {@link #startQuery}.
+ * @param cursor The cursor holding the results from the query.
+ */
+ protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
+ // Empty
+ }
+
+ /**
+ * Called when an asynchronous insert is completed.
+ *
+ * @param token the token to identify the query, passed in from
+ * {@link #startInsert}.
+ * @param cookie the cookie object that's passed in from
+ * {@link #startInsert}.
+ * @param uri the uri returned from the insert operation.
+ */
+ protected void onInsertComplete(int token, Object cookie, Uri uri) {
+ // Empty
+ }
+
+ /**
+ * Called when an asynchronous update is completed.
+ *
+ * @param token the token to identify the query, passed in from
+ * {@link #startUpdate}.
+ * @param cookie the cookie object that's passed in from
+ * {@link #startUpdate}.
+ * @param result the result returned from the update operation
+ */
+ protected void onUpdateComplete(int token, Object cookie, int result) {
+ // Empty
+ }
+
+ /**
+ * Called when an asynchronous delete is completed.
+ *
+ * @param token the token to identify the query, passed in from
+ * {@link #startDelete}.
+ * @param cookie the cookie object that's passed in from
+ * {@link #startDelete}.
+ * @param result the result returned from the delete operation
+ */
+ protected void onDeleteComplete(int token, Object cookie, int result) {
+ // Empty
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ WorkerArgs args = (WorkerArgs) msg.obj;
+
+ if (localLOGV) {
+ Log.d(TAG, "AsyncQueryHandler.handleMessage: msg.what=" + msg.what
+ + ", msg.arg1=" + msg.arg1);
+ }
+
+ int token = msg.what;
+ int event = msg.arg1;
+
+ // pass token back to caller on each callback.
+ switch (event) {
+ case EVENT_ARG_QUERY:
+ onQueryComplete(token, args.cookie, (Cursor) args.result);
+ break;
+
+ case EVENT_ARG_INSERT:
+ onInsertComplete(token, args.cookie, (Uri) args.result);
+ break;
+
+ case EVENT_ARG_UPDATE:
+ onUpdateComplete(token, args.cookie, (Integer) args.result);
+ break;
+
+ case EVENT_ARG_DELETE:
+ onDeleteComplete(token, args.cookie, (Integer) args.result);
+ break;
+ }
+ }
+}
diff --git a/core/java/android/content/BroadcastReceiver.java b/core/java/android/content/BroadcastReceiver.java
new file mode 100644
index 0000000..6a6f4f9
--- /dev/null
+++ b/core/java/android/content/BroadcastReceiver.java
@@ -0,0 +1,399 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+import android.os.Bundle;
+import android.util.Log;
+
+/**
+ * Base class for code that will receive intents sent by sendBroadcast().
+ * You can either dynamically register an instance of this class with
+ * {@link Context#registerReceiver Context.registerReceiver()}
+ * or statically publish an implementation through the
+ * {@link android.R.styleable#AndroidManifestReceiver &lt;receiver&gt;}
+ * tag in your <code>AndroidManifest.xml</code>. <em><strong>Note:</strong></em>
+ * &nbsp;&nbsp;&nbsp;If registering a receiver in your
+ * {@link android.app.Activity#onResume() Activity.onResume()}
+ * implementation, you should unregister it in
+ * {@link android.app.Activity#onPause() Activity.onPause()}.
+ * (You won't receive intents when paused,
+ * and this will cut down on unnecessary system overhead). Do not unregister in
+ * {@link android.app.Activity#onSaveInstanceState(android.os.Bundle) Activity.onSaveInstanceState()},
+ * because this won't be called if the user moves back in the history
+ * stack.
+ *
+ * <p>There are two major classes of broadcasts that can be received:</p>
+ * <ul>
+ * <li> <b>Normal broadcasts</b> (sent with {@link Context#sendBroadcast(Intent)
+ * Context.sendBroadcast}) are completely asynchronous. All receivers of the
+ * broadcast are run, in an undefined order, often at the same time. This is
+ * more efficient, but means that receivers can not use the result or abort
+ * APIs included here.
+ * <li> <b>Ordered broadcasts</b> (sent with {@link Context#sendOrderedBroadcast(Intent, String)
+ * Context.sendOrderedBroadcast}) are delivered to one receiver at a time.
+ * As each receiver executes in turn, it can propagate a result to the next
+ * receiver, or it can completely abort the broadcast so that it won't be passed
+ * to other receivers. The order receivers runs in can be controlled with the
+ * {@link android.R.styleable#AndroidManifestIntentFilter_priority
+ * android:priority} attribute of the matching intent-filter; receivers with
+ * the same priority will be run in an arbitrary order.
+ * </ul>
+ *
+ * <p>Even in the case of normal broadcasts, the system may in some
+ * situations revert to delivering the broadcast one receiver at a time. In
+ * particular, for receivers that may require the creation of a process, only
+ * one will be run at a time to avoid overloading the system with new processes.
+ * In this situation, however, the non-ordered semantics hold: these receivers
+ * can not return results or abort their broadcast.</p>
+ *
+ * <p>Note that, although the Intent class is used for sending and receiving
+ * these broadcasts, the Intent broadcast mechanism here is completely separate
+ * from Intents that are used to start Activities with
+ * {@link Context#startActivity Context.startActivity()}.
+ * There is no way for an BroadcastReceiver
+ * to see or capture Intents used with startActivity(); likewise, when
+ * you broadcast an Intent, you will never find or start an Activity.
+ * These two operations are semantically very different: starting an
+ * Activity with an Intent is a foreground operation that modifies what the
+ * user is currently interacting with; broadcasting an Intent is a background
+ * operation that the user is not normally aware of.
+ *
+ * <p>The BroadcastReceiver class (when launched as a component through
+ * a manifest's {@link android.R.styleable#AndroidManifestReceiver &lt;receiver&gt;}
+ * tag) is an important part of an
+ * <a href="{@docRoot}intro/lifecycle.html">application's overall lifecycle</a>.</p>
+ *
+ * <p>Topics covered here:
+ * <ol>
+ * <li><a href="#ReceiverLifecycle">Receiver Lifecycle</a>
+ * <li><a href="#Permissions">Permissions</a>
+ * <li><a href="#ProcessLifecycle">Process Lifecycle</a>
+ * </ol>
+ *
+ * <a name="ReceiverLifecycle"></a>
+ * <h3>Receiver Lifecycle</h3>
+ *
+ * <p>A BroadcastReceiver object is only valid for the duration of the call
+ * to {@link #onReceive}. Once your code returns from this function,
+ * the system considers the object to be finished and no longer active.
+ *
+ * <p>This has important repercussions to what you can do in an
+ * {@link #onReceive} implementation: anything that requires asynchronous
+ * operation is not available, because you will need to return from the
+ * function to handle the asynchronous operation, but at that point the
+ * BroadcastReceiver is no longer active and thus the system is free to kill
+ * its process before the asynchronous operation completes.
+ *
+ * <p>In particular, you may <i>not</i> show a dialog or bind to a service from
+ * within an BroadcastReceiver. For the former, you should instead use the
+ * {@link android.app.NotificationManager} API. For the latter, you can
+ * use {@link android.content.Context#startService Context.startService()} to
+ * send a command to the service.
+ *
+ * <a name="Permissions"></a>
+ * <h3>Permissions</h3>
+ *
+ * <p>Access permissions can be enforced by either the sender or receiver
+ * of an Intent.
+ *
+ * <p>To enforce a permission when sending, you supply a non-null
+ * <var>permission</var> argument to
+ * {@link Context#sendBroadcast(Intent, String)} or
+ * {@link Context#sendOrderedBroadcast(Intent, String, BroadcastReceiver, android.os.Handler, int, String, Bundle)}.
+ * Only receivers who have been granted this permission
+ * (by requesting it with the
+ * {@link android.R.styleable#AndroidManifestUsesPermission &lt;uses-permission&gt;}
+ * tag in their <code>AndroidManifest.xml</code>) will be able to receive
+ * the broadcast.
+ *
+ * <p>To enforce a permission when receiving, you supply a non-null
+ * <var>permission</var> when registering your receiver -- either when calling
+ * {@link Context#registerReceiver(BroadcastReceiver, IntentFilter, String, android.os.Handler)}
+ * or in the static
+ * {@link android.R.styleable#AndroidManifestReceiver &lt;receiver&gt;}
+ * tag in your <code>AndroidManifest.xml</code>. Only broadcasters who have
+ * been granted this permission (by requesting it with the
+ * {@link android.R.styleable#AndroidManifestUsesPermission &lt;uses-permission&gt;}
+ * tag in their <code>AndroidManifest.xml</code>) will be able to send an
+ * Intent to the receiver.
+ *
+ * <p>See the <a href="{@docRoot}devel/security.html">Security Model</a>
+ * document for more information on permissions and security in general.
+ *
+ * <a name="ProcessLifecycle"></a>
+ * <h3>Process Lifecycle</h3>
+ *
+ * <p>A process that is currently executing an BroadcastReceiver (that is,
+ * currently running the code in its {@link #onReceive} method) is
+ * considered to be a foreground process and will be kept running by the
+ * system except under cases of extreme memory pressure.
+ *
+ * <p>Once you return from onReceive(), the BroadcastReceiver is no longer
+ * active, and its hosting process is only as important as any other application
+ * components that are running in it. This is especially important because if
+ * that process was only hosting the BroadcastReceiver (a common case for
+ * applications that the user has never or not recently interacted with), then
+ * upon returning from onReceive() the system will consider its process
+ * to be empty and aggressively kill it so that resources are available for other
+ * more important processes.
+ *
+ * <p>This means that for longer-running operations you will often use
+ * a {@link android.app.Service} in conjunction with an BroadcastReceiver to keep
+ * the containing process active for the entire time of your operation.
+ */
+public abstract class BroadcastReceiver {
+ public BroadcastReceiver() {
+ }
+
+ /**
+ * This method is called when the BroadcastReceiver is receiving an Intent
+ * broadcast. During this time you can use the other methods on
+ * BroadcastReceiver to view/modify the current result values. The function
+ * is normally called from the main thread of its process, so you should
+ * never perform long-running operations in it (there is a timeout of
+ * 10 seconds that the system allows before considering the receiver to
+ * be blocked and a candidate to be killed). You cannot launch a popup dialog
+ * in your implementation of onReceive().
+ *
+ * <p><b>If this BroadcastReceiver was launched through a &lt;receiver&gt; tag,
+ * then the object is no longer alive after returning from this
+ * function.</b> This means you should not perform any operations that
+ * return a result to you asynchronously -- in particular, for interacting
+ * with services, you should use
+ * {@link Context#startService(Intent)} instead of
+ * {@link Context#bindService(Intent, ServiceConnection, int)}.
+ *
+ * @param context The Context in which the receiver is running.
+ * @param intent The Intent being received.
+ */
+ public abstract void onReceive(Context context, Intent intent);
+
+ /**
+ * Change the current result code of this broadcast; only works with
+ * broadcasts sent through
+ * {@link Context#sendOrderedBroadcast(Intent, String)
+ * Context.sendOrderedBroadcast}. Often uses the
+ * Activity {@link android.app.Activity#RESULT_CANCELED} and
+ * {@link android.app.Activity#RESULT_OK} constants, though the
+ * actual meaning of this value is ultimately up to the broadcaster.
+ *
+ * <p><strong>This method does not work with non-ordered broadcasts such
+ * as those sent with {@link Context#sendBroadcast(Intent)
+ * Context.sendBroadcast}</strong></p>
+ *
+ * @param code The new result code.
+ *
+ * @see #setResult(int, String, Bundle)
+ */
+ public final void setResultCode(int code) {
+ checkSynchronousHint();
+ mResultCode = code;
+ }
+
+ /**
+ * Retrieve the current result code, as set by the previous receiver.
+ *
+ * @return int The current result code.
+ */
+ public final int getResultCode() {
+ return mResultCode;
+ }
+
+ /**
+ * Change the current result data of this broadcast; only works with
+ * broadcasts sent through
+ * {@link Context#sendOrderedBroadcast(Intent, String)
+ * Context.sendOrderedBroadcast}. This is an arbitrary
+ * string whose interpretation is up to the broadcaster.
+ *
+ * <p><strong>This method does not work with non-ordered broadcasts such
+ * as those sent with {@link Context#sendBroadcast(Intent)
+ * Context.sendBroadcast}</strong></p>
+ *
+ * @param data The new result data; may be null.
+ *
+ * @see #setResult(int, String, Bundle)
+ */
+ public final void setResultData(String data) {
+ checkSynchronousHint();
+ mResultData = data;
+ }
+
+ /**
+ * Retrieve the current result data, as set by the previous receiver.
+ * Often this is null.
+ *
+ * @return String The current result data; may be null.
+ */
+ public final String getResultData() {
+ return mResultData;
+ }
+
+ /**
+ * Change the current result extras of this broadcast; only works with
+ * broadcasts sent through
+ * {@link Context#sendOrderedBroadcast(Intent, String)
+ * Context.sendOrderedBroadcast}. This is a Bundle
+ * holding arbitrary data, whose interpretation is up to the
+ * broadcaster. Can be set to null. Calling this method completely
+ * replaces the current map (if any).
+ *
+ * <p><strong>This method does not work with non-ordered broadcasts such
+ * as those sent with {@link Context#sendBroadcast(Intent)
+ * Context.sendBroadcast}</strong></p>
+ *
+ * @param extras The new extra data map; may be null.
+ *
+ * @see #setResult(int, String, Bundle)
+ */
+ public final void setResultExtras(Bundle extras) {
+ checkSynchronousHint();
+ mResultExtras = extras;
+ }
+
+ /**
+ * Retrieve the current result extra data, as set by the previous receiver.
+ * Any changes you make to the returned Map will be propagated to the next
+ * receiver.
+ *
+ * @param makeMap If true then a new empty Map will be made for you if the
+ * current Map is null; if false you should be prepared to
+ * receive a null Map.
+ *
+ * @return Map The current extras map.
+ */
+ public final Bundle getResultExtras(boolean makeMap) {
+ Bundle e = mResultExtras;
+ if (!makeMap) return e;
+ if (e == null) mResultExtras = e = new Bundle();
+ return e;
+ }
+
+ /**
+ * Change all of the result data returned from this broadcasts; only works
+ * with broadcasts sent through
+ * {@link Context#sendOrderedBroadcast(Intent, String)
+ * Context.sendOrderedBroadcast}. All current result data is replaced
+ * by the value given to this method.
+ *
+ * <p><strong>This method does not work with non-ordered broadcasts such
+ * as those sent with {@link Context#sendBroadcast(Intent)
+ * Context.sendBroadcast}</strong></p>
+ *
+ * @param code The new result code. Often uses the
+ * Activity {@link android.app.Activity#RESULT_CANCELED} and
+ * {@link android.app.Activity#RESULT_OK} constants, though the
+ * actual meaning of this value is ultimately up to the broadcaster.
+ * @param data The new result data. This is an arbitrary
+ * string whose interpretation is up to the broadcaster; may be null.
+ * @param extras The new extra data map. This is a Bundle
+ * holding arbitrary data, whose interpretation is up to the
+ * broadcaster. Can be set to null. This completely
+ * replaces the current map (if any).
+ */
+ public final void setResult(int code, String data, Bundle extras) {
+ checkSynchronousHint();
+ mResultCode = code;
+ mResultData = data;
+ mResultExtras = extras;
+ }
+
+ /**
+ * Returns the flag indicating whether or not this receiver should
+ * abort the current broadcast.
+ *
+ * @return True if the broadcast should be aborted.
+ */
+ public final boolean getAbortBroadcast() {
+ return mAbortBroadcast;
+ }
+
+ /**
+ * Sets the flag indicating that this receiver should abort the
+ * current broadcast; only works with broadcasts sent through
+ * {@link Context#sendOrderedBroadcast(Intent, String)
+ * Context.sendOrderedBroadcast}. This will prevent
+ * any other intent receivers from receiving the broadcast. It will still
+ * call {@link #onReceive} of the BroadcastReceiver that the caller of
+ * {@link Context#sendOrderedBroadcast(Intent, String)
+ * Context.sendOrderedBroadcast} passed in.
+ *
+ * <p><strong>This method does not work with non-ordered broadcasts such
+ * as those sent with {@link Context#sendBroadcast(Intent)
+ * Context.sendBroadcast}</strong></p>
+ */
+ public final void abortBroadcast() {
+ checkSynchronousHint();
+ mAbortBroadcast = true;
+ }
+
+ /**
+ * Clears the flag indicating that this receiver should abort the current
+ * broadcast.
+ */
+ public final void clearAbortBroadcast() {
+ mAbortBroadcast = false;
+ }
+
+ /**
+ * For internal use, sets the hint about whether this BroadcastReceiver is
+ * running in ordered mode.
+ */
+ public final void setOrderedHint(boolean isOrdered) {
+ mOrderedHint = isOrdered;
+ }
+
+ /**
+ * Control inclusion of debugging help for mismatched
+ * calls to {@ Context#registerReceiver(BroadcastReceiver, IntentFilter)
+ * Context.registerReceiver()}.
+ * If called with true, before given to registerReceiver(), then the
+ * callstack of the following {@link Context#unregisterReceiver(BroadcastReceiver)
+ * Context.unregisterReceiver()} call is retained, to be printed if a later
+ * incorrect unregister call is made. Note that doing this requires retaining
+ * information about the BroadcastReceiver for the lifetime of the app,
+ * resulting in a leak -- this should only be used for debugging.
+ */
+ public final void setDebugUnregister(boolean debug) {
+ mDebugUnregister = debug;
+ }
+
+ /**
+ * Return the last value given to {@link #setDebugUnregister}.
+ */
+ public final boolean getDebugUnregister() {
+ return mDebugUnregister;
+ }
+
+ void checkSynchronousHint() {
+ if (mOrderedHint) {
+ return;
+ }
+ RuntimeException e = new RuntimeException(
+ "BroadcastReceiver trying to return result during a non-ordered broadcast");
+ e.fillInStackTrace();
+ Log.e("BroadcastReceiver", e.getMessage(), e);
+ }
+
+ private int mResultCode;
+ private String mResultData;
+ private Bundle mResultExtras;
+ private boolean mAbortBroadcast;
+ private boolean mDebugUnregister;
+ private boolean mOrderedHint;
+}
+
diff --git a/core/java/android/content/ComponentCallbacks.java b/core/java/android/content/ComponentCallbacks.java
new file mode 100644
index 0000000..dad60b0
--- /dev/null
+++ b/core/java/android/content/ComponentCallbacks.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+import android.content.res.Configuration;
+
+/**
+ * The set of callback APIs that are common to all application components
+ * ({@link android.app.Activity}, {@link android.app.Service},
+ * {@link ContentProvider}, and {@link android.app.Application}).
+ */
+public interface ComponentCallbacks {
+ /**
+ * Called by the system when the device configuration changes while your
+ * component is running. Note that, unlike activities, other components
+ * are never restarted when a configuration changes: they must always deal
+ * with the results of the change, such as by re-retrieving resources.
+ *
+ * <p>At the time that this function has been called, your Resources
+ * object will have been updated to return resource values matching the
+ * new configuration.
+ *
+ * @param newConfig The new device configuration.
+ */
+ void onConfigurationChanged(Configuration newConfig);
+
+ /**
+ * This is called when the overall system is running low on memory, and
+ * would like actively running process to try to tighten their belt. While
+ * the exact point at which this will be called is not defined, generally
+ * it will happen around the time all background process have been killed,
+ * that is before reaching the point of killing processes hosting
+ * service and foreground UI that we would like to avoid killing.
+ *
+ * <p>Applications that want to be nice can implement this method to release
+ * any caches or other unnecessary resources they may be holding on to.
+ * The system will perform a gc for you after returning from this method.
+ */
+ void onLowMemory();
+}
diff --git a/core/java/android/content/ComponentName.aidl b/core/java/android/content/ComponentName.aidl
new file mode 100644
index 0000000..40dc8de
--- /dev/null
+++ b/core/java/android/content/ComponentName.aidl
@@ -0,0 +1,19 @@
+/**
+ * Copyright (c) 2007, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+parcelable ComponentName;
diff --git a/core/java/android/content/ComponentName.java b/core/java/android/content/ComponentName.java
new file mode 100644
index 0000000..32c6864
--- /dev/null
+++ b/core/java/android/content/ComponentName.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Identifier for a specific application component
+ * ({@link android.app.Activity}, {@link android.app.Service},
+ * {@link android.content.BroadcastReceiver}, or
+ * {@link android.content.ContentProvider}) that is available. Two
+ * pieces of information, encapsulated here, are required to identify
+ * a component: the package (a String) it exists in, and the class (a String)
+ * name inside of that package.
+ *
+ */
+public final class ComponentName implements Parcelable {
+ private final String mPackage;
+ private final String mClass;
+
+ /**
+ * Create a new component identifier.
+ *
+ * @param pkg The name of the package that the component exists in. Can
+ * not be null.
+ * @param cls The name of the class inside of <var>pkg</var> that
+ * implements the component. Can not be null.
+ */
+ public ComponentName(String pkg, String cls) {
+ if (pkg == null) throw new NullPointerException("package name is null");
+ if (cls == null) throw new NullPointerException("class name is null");
+ mPackage = pkg;
+ mClass = cls;
+ }
+
+ /**
+ * Create a new component identifier from a Context and class name.
+ *
+ * @param pkg A Context for the package implementing the component,
+ * from which the actual package name will be retrieved.
+ * @param cls The name of the class inside of <var>pkg</var> that
+ * implements the component.
+ */
+ public ComponentName(Context pkg, String cls) {
+ if (cls == null) throw new NullPointerException("class name is null");
+ mPackage = pkg.getPackageName();
+ mClass = cls;
+ }
+
+ /**
+ * Create a new component identifier from a Context and Class object.
+ *
+ * @param pkg A Context for the package implementing the component, from
+ * which the actual package name will be retrieved.
+ * @param cls The Class object of the desired component, from which the
+ * actual class name will be retrieved.
+ */
+ public ComponentName(Context pkg, Class<?> cls) {
+ mPackage = pkg.getPackageName();
+ mClass = cls.getName();
+ }
+
+ /**
+ * Return the package name of this component.
+ */
+ public String getPackageName() {
+ return mPackage;
+ }
+
+ /**
+ * Return the class name of this component.
+ */
+ public String getClassName() {
+ return mClass;
+ }
+
+ /**
+ * Return the class name, either fully qualified or in a shortened form
+ * (with a leading '.') if it is a suffix of the package.
+ */
+ public String getShortClassName() {
+ if (mClass.startsWith(mPackage)) {
+ int PN = mPackage.length();
+ int CN = mClass.length();
+ if (CN > PN && mClass.charAt(PN) == '.') {
+ return mClass.substring(PN, CN);
+ }
+ }
+ return mClass;
+ }
+
+ /**
+ * Return a String that unambiguously describes both the package and
+ * class names contained in the ComponentName. You can later recover
+ * the ComponentName from this string through
+ * {@link #unflattenFromString(String)}.
+ *
+ * @return Returns a new String holding the package and class names. This
+ * is represented as the package name, concatenated with a '/' and then the
+ * class name.
+ *
+ * @see #unflattenFromString(String)
+ */
+ public String flattenToString() {
+ return mPackage + "/" + mClass;
+ }
+
+ /**
+ * The samee as {@link #flattenToString()}, but abbreviates the class
+ * name if it is a suffix of the package. The result can still be used
+ * with {@link #unflattenFromString(String)}.
+ *
+ * @return Returns a new String holding the package and class names. This
+ * is represented as the package name, concatenated with a '/' and then the
+ * class name.
+ *
+ * @see #unflattenFromString(String)
+ */
+ public String flattenToShortString() {
+ return mPackage + "/" + getShortClassName();
+ }
+
+ /**
+ * Recover a ComponentName from a String that was previously created with
+ * {@link #flattenToString()}. It splits the string at the first '/',
+ * taking the part before as the package name and the part after as the
+ * class name. As a special convenience (to use, for example, when
+ * parsing component names on the command line), if the '/' is immediately
+ * followed by a '.' then the final class name will be the concatenation
+ * of the package name with the string following the '/'. Thus
+ * "com.foo/.Blah" becomes package="com.foo" class="com.foo.Blah".
+ *
+ * @param str The String that was returned by flattenToString().
+ * @return Returns a new ComponentName containing the package and class
+ * names that were encoded in <var>str</var>
+ *
+ * @see #flattenToString()
+ */
+ public static ComponentName unflattenFromString(String str) {
+ int sep = str.indexOf('/');
+ if (sep < 0 || (sep+1) >= str.length()) {
+ return null;
+ }
+ String pkg = str.substring(0, sep);
+ String cls = str.substring(sep+1);
+ if (cls.length() > 0 && cls.charAt(0) == '.') {
+ cls = pkg + cls;
+ }
+ return new ComponentName(pkg, cls);
+ }
+
+ /**
+ * Return string representation of this class without the class's name
+ * as a prefix.
+ */
+ public String toShortString() {
+ return "{" + mPackage + "/" + mClass + "}";
+ }
+
+ @Override
+ public String toString() {
+ return "ComponentInfo{" + mPackage + "/" + mClass + "}";
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ try {
+ if (obj != null) {
+ ComponentName other = (ComponentName)obj;
+ // Note: no null checks, because mPackage and mClass can
+ // never be null.
+ return mPackage.equals(other.mPackage)
+ && mClass.equals(other.mClass);
+ }
+ } catch (ClassCastException e) {
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return mPackage.hashCode() + mClass.hashCode();
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeString(mPackage);
+ out.writeString(mClass);
+ }
+
+ /**
+ * Write a ComponentName to a Parcel, handling null pointers. Must be
+ * read with {@link #readFromParcel(Parcel)}.
+ *
+ * @param c The ComponentName to be written.
+ * @param out The Parcel in which the ComponentName will be placed.
+ *
+ * @see #readFromParcel(Parcel)
+ */
+ public static void writeToParcel(ComponentName c, Parcel out) {
+ if (c != null) {
+ c.writeToParcel(out, 0);
+ } else {
+ out.writeString(null);
+ }
+ }
+
+ /**
+ * Read a ComponentName from a Parcel that was previously written
+ * with {@link #writeToParcel(ComponentName, Parcel)}, returning either
+ * a null or new object as appropriate.
+ *
+ * @param in The Parcel from which to read the ComponentName
+ * @return Returns a new ComponentName matching the previously written
+ * object, or null if a null had been written.
+ *
+ * @see #writeToParcel(ComponentName, Parcel)
+ */
+ public static ComponentName readFromParcel(Parcel in) {
+ String pkg = in.readString();
+ return pkg != null ? new ComponentName(pkg, in) : null;
+ }
+
+ public static final Parcelable.Creator<ComponentName> CREATOR
+ = new Parcelable.Creator<ComponentName>() {
+ public ComponentName createFromParcel(Parcel in) {
+ return new ComponentName(in);
+ }
+
+ public ComponentName[] newArray(int size) {
+ return new ComponentName[size];
+ }
+ };
+
+ /**
+ * Instantiate a new ComponentName from the data in a Parcel that was
+ * previously written with {@link #writeToParcel(Parcel, int)}. Note that you
+ * must not use this with data written by
+ * {@link #writeToParcel(ComponentName, Parcel)} since it is not possible
+ * to handle a null ComponentObject here.
+ *
+ * @param in The Parcel containing the previously written ComponentName,
+ * positioned at the location in the buffer where it was written.
+ */
+ public ComponentName(Parcel in) {
+ mPackage = in.readString();
+ if (mPackage == null) throw new NullPointerException(
+ "package name is null");
+ mClass = in.readString();
+ if (mClass == null) throw new NullPointerException(
+ "class name is null");
+ }
+
+ private ComponentName(String pkg, Parcel in) {
+ mPackage = pkg;
+ mClass = in.readString();
+ }
+}
diff --git a/core/java/android/content/ContentInsertHandler.java b/core/java/android/content/ContentInsertHandler.java
new file mode 100644
index 0000000..fbf726e
--- /dev/null
+++ b/core/java/android/content/ContentInsertHandler.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+
+import org.xml.sax.ContentHandler;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Interface to insert data to ContentResolver
+ * @hide
+ */
+public interface ContentInsertHandler extends ContentHandler {
+ /**
+ * insert data from InputStream to ContentResolver
+ * @param contentResolver
+ * @param in InputStream
+ * @throws IOException
+ * @throws SAXException
+ */
+ public void insert(ContentResolver contentResolver, InputStream in)
+ throws IOException, SAXException;
+
+ /**
+ * insert data from String to ContentResolver
+ * @param contentResolver
+ * @param in input string
+ * @throws SAXException
+ */
+ public void insert(ContentResolver contentResolver, String in)
+ throws SAXException;
+
+}
diff --git a/core/java/android/content/ContentProvider.java b/core/java/android/content/ContentProvider.java
new file mode 100644
index 0000000..226c5ab
--- /dev/null
+++ b/core/java/android/content/ContentProvider.java
@@ -0,0 +1,562 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+import android.content.pm.PackageManager;
+import android.content.pm.ProviderInfo;
+import android.content.res.Configuration;
+import android.database.Cursor;
+import android.database.CursorToBulkCursorAdaptor;
+import android.database.CursorWindow;
+import android.database.IBulkCursor;
+import android.database.IContentObserver;
+import android.database.SQLException;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.ParcelFileDescriptor;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+
+/**
+ * Content providers are one of the primary building blocks of Android applications, providing
+ * content to applications. They encapsulate data and provide it to applications through the single
+ * {@link ContentResolver} interface. A content provider is only required if you need to share
+ * data between multiple applications. For example, the contacts data is used by multiple
+ * applications and must be stored in a content provider. If you don't need to share data amongst
+ * multiple applications you can use a database directly via
+ * {@link android.database.sqlite.SQLiteDatabase}.
+ *
+ * <p>See <a href="{@docRoot}devel/data/contentproviders.html">this page</a> for more information on
+ * content providers.</p>
+ *
+ * <p>When a request is made via
+ * a {@link ContentResolver} the system inspects the authority of the given URI and passes the
+ * request to the content provider registered with the authority. The content provider can interpret
+ * the rest of the URI however it wants. The {@link UriMatcher} class is helpful for parsing
+ * URIs.</p>
+ *
+ * <p>The primary methods that need to be implemented are:
+ * <ul>
+ * <li>{@link #query} which returns data to the caller</li>
+ * <li>{@link #insert} which inserts new data into the content provider</li>
+ * <li>{@link #update} which updates existing data in the content provider</li>
+ * <li>{@link #delete} which deletes data from the content provider</li>
+ * <li>{@link #getType} which returns the MIME type of data in the content provider</li>
+ * </ul></p>
+ *
+ * <p>This class takes care of cross process calls so subclasses don't have to worry about which
+ * process a request is coming from.</p>
+ */
+public abstract class ContentProvider implements ComponentCallbacks {
+ private Context mContext = null;
+ private String mReadPermission;
+ private String mWritePermission;
+
+ private Transport mTransport = new Transport();
+
+ /**
+ * Given an IContentProvider, try to coerce it back to the real
+ * ContentProvider object if it is running in the local process. This can
+ * be used if you know you are running in the same process as a provider,
+ * and want to get direct access to its implementation details. Most
+ * clients should not nor have a reason to use it.
+ *
+ * @param abstractInterface The ContentProvider interface that is to be
+ * coerced.
+ * @return If the IContentProvider is non-null and local, returns its actual
+ * ContentProvider instance. Otherwise returns null.
+ * @hide
+ */
+ public static ContentProvider coerceToLocalContentProvider(
+ IContentProvider abstractInterface) {
+ if (abstractInterface instanceof Transport) {
+ return ((Transport)abstractInterface).getContentProvider();
+ }
+ return null;
+ }
+
+ /**
+ * Binder object that deals with remoting.
+ *
+ * @hide
+ */
+ class Transport extends ContentProviderNative {
+ ContentProvider getContentProvider() {
+ return ContentProvider.this;
+ }
+
+ /**
+ * Remote version of a query, which returns an IBulkCursor. The bulk
+ * cursor should be wrapped with BulkCursorToCursorAdaptor before use.
+ */
+ public IBulkCursor bulkQuery(Uri uri, String[] projection,
+ String selection, String[] selectionArgs, String sortOrder,
+ IContentObserver observer, CursorWindow window) {
+ checkReadPermission(uri);
+ Cursor cursor = ContentProvider.this.query(uri, projection,
+ selection, selectionArgs, sortOrder);
+ if (cursor == null) {
+ return null;
+ }
+ String wperm = getWritePermission();
+ return new CursorToBulkCursorAdaptor(cursor, observer,
+ ContentProvider.this.getClass().getName(),
+ wperm == null ||
+ getContext().checkCallingOrSelfPermission(getWritePermission())
+ == PackageManager.PERMISSION_GRANTED,
+ window);
+ }
+
+ public Cursor query(Uri uri, String[] projection,
+ String selection, String[] selectionArgs, String sortOrder) {
+ checkReadPermission(uri);
+ return ContentProvider.this.query(uri, projection, selection,
+ selectionArgs, sortOrder);
+ }
+
+ public String getType(Uri uri) {
+ return ContentProvider.this.getType(uri);
+ }
+
+
+ public Uri insert(Uri uri, ContentValues initialValues) {
+ checkWritePermission(uri);
+ return ContentProvider.this.insert(uri, initialValues);
+ }
+
+ public int bulkInsert(Uri uri, ContentValues[] initialValues) {
+ checkWritePermission(uri);
+ return ContentProvider.this.bulkInsert(uri, initialValues);
+ }
+
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ checkWritePermission(uri);
+ return ContentProvider.this.delete(uri, selection, selectionArgs);
+ }
+
+ public int update(Uri uri, ContentValues values, String selection,
+ String[] selectionArgs) {
+ checkWritePermission(uri);
+ return ContentProvider.this.update(uri, values, selection, selectionArgs);
+ }
+
+ public ParcelFileDescriptor openFile(Uri uri, String mode)
+ throws FileNotFoundException {
+ if (mode != null && mode.startsWith("rw")) checkWritePermission(uri);
+ else checkReadPermission(uri);
+ return ContentProvider.this.openFile(uri, mode);
+ }
+
+ public ISyncAdapter getSyncAdapter() {
+ checkWritePermission(null);
+ return ContentProvider.this.getSyncAdapter().getISyncAdapter();
+ }
+
+ private void checkReadPermission(Uri uri) {
+ final String rperm = getReadPermission();
+ final int pid = Binder.getCallingPid();
+ final int uid = Binder.getCallingUid();
+ if (getContext().checkUriPermission(uri, rperm, null, pid, uid,
+ Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ == PackageManager.PERMISSION_GRANTED) {
+ return;
+ }
+ String msg = "Permission Denial: reading "
+ + ContentProvider.this.getClass().getName()
+ + " uri " + uri + " from pid=" + Binder.getCallingPid()
+ + ", uid=" + Binder.getCallingUid()
+ + " requires " + rperm;
+ throw new SecurityException(msg);
+ }
+
+ private void checkWritePermission(Uri uri) {
+ final String wperm = getWritePermission();
+ final int pid = Binder.getCallingPid();
+ final int uid = Binder.getCallingUid();
+ if (getContext().checkUriPermission(uri, null, wperm, pid, uid,
+ Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
+ == PackageManager.PERMISSION_GRANTED) {
+ return;
+ }
+ String msg = "Permission Denial: writing "
+ + ContentProvider.this.getClass().getName()
+ + " uri " + uri + " from pid=" + Binder.getCallingPid()
+ + ", uid=" + Binder.getCallingUid()
+ + " requires " + wperm;
+ throw new SecurityException(msg);
+ }
+ }
+
+
+ /**
+ * Retrieve the Context this provider is running in. Only available once
+ * onCreate(Map icicle) has been called -- this will be null in the
+ * constructor.
+ */
+ public final Context getContext() {
+ return mContext;
+ }
+
+ /**
+ * Change the permission required to read data from the content
+ * provider. This is normally set for you from its manifest information
+ * when the provider is first created.
+ *
+ * @param permission Name of the permission required for read-only access.
+ */
+ protected final void setReadPermission(String permission) {
+ mReadPermission = permission;
+ }
+
+ /**
+ * Return the name of the permission required for read-only access to
+ * this content provider. This method can be called from multiple
+ * threads, as described in the
+ * <a href="{@docRoot}intro/appmodel.html#Threads">Threading section of
+ * the Application Model overview</a>.
+ */
+ public final String getReadPermission() {
+ return mReadPermission;
+ }
+
+ /**
+ * Change the permission required to read and write data in the content
+ * provider. This is normally set for you from its manifest information
+ * when the provider is first created.
+ *
+ * @param permission Name of the permission required for read/write access.
+ */
+ protected final void setWritePermission(String permission) {
+ mWritePermission = permission;
+ }
+
+ /**
+ * Return the name of the permission required for read/write access to
+ * this content provider. This method can be called from multiple
+ * threads, as described in the
+ * <a href="{@docRoot}intro/appmodel.html#Threads">Threading section of
+ * the Application Model overview</a>.
+ */
+ public final String getWritePermission() {
+ return mWritePermission;
+ }
+
+ /**
+ * Called when the provider is being started.
+ *
+ * @return true if the provider was successfully loaded, false otherwise
+ */
+ public abstract boolean onCreate();
+
+ public void onConfigurationChanged(Configuration newConfig) {
+ }
+
+ public void onLowMemory() {
+ }
+
+ /**
+ * Receives a query request from a client in a local process, and
+ * returns a Cursor. This is called internally by the {@link ContentResolver}.
+ * This method can be called from multiple
+ * threads, as described in the
+ * <a href="{@docRoot}intro/appmodel.html#Threads">Threading section of
+ * the Application Model overview</a>.
+ * <p>
+ * Example client call:<p>
+ * <pre>// Request a specific record.
+ * Cursor managedCursor = managedQuery(
+ Contacts.People.CONTENT_URI.addId(2),
+ projection, // Which columns to return.
+ null, // WHERE clause.
+ 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>
+ *
+ * @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 sortOrder How the rows in the cursor should be sorted.
+ * If null then the provider is free to define the sort order.
+ * @return a Cursor or null.
+ */
+ public abstract Cursor query(Uri uri, String[] projection,
+ String selection, String[] selectionArgs, String sortOrder);
+
+ /**
+ * Return the MIME type of the data at the given URI. This should start with
+ * <code>vnd.android.cursor.item</code> for a single record,
+ * or <code>vnd.android.cursor.dir/</code> for multiple items.
+ * This method can be called from multiple
+ * threads, as described in the
+ * <a href="{@docRoot}intro/appmodel.html#Threads">Threading section of
+ * the Application Model overview</a>.
+ *
+ * @param uri the URI to query.
+ * @return a MIME type string, or null if there is no type.
+ */
+ public abstract String getType(Uri uri);
+
+ /**
+ * Implement this to insert a new row.
+ * As a courtesy, call {@link ContentResolver#notifyChange(android.net.Uri ,android.database.ContentObserver) notifyChange()}
+ * after inserting.
+ * This method can be called from multiple
+ * threads, as described in the
+ * <a href="{@docRoot}intro/appmodel.html#Threads">Threading section of the
+ * Application Model overview</a>.
+ * @param uri The content:// URI of the insertion request.
+ * @param values A set of column_name/value pairs to add to the database.
+ * @return The URI for the newly inserted item.
+ */
+ public abstract Uri insert(Uri uri, ContentValues values);
+
+ /**
+ * Implement this to insert a set of new rows, or the default implementation will
+ * iterate over the values and call {@link #insert} on each of them.
+ * As a courtesy, call {@link ContentResolver#notifyChange(android.net.Uri ,android.database.ContentObserver) notifyChange()}
+ * after inserting.
+ * This method can be called from multiple
+ * threads, as described in the
+ * <a href="{@docRoot}intro/appmodel.html#Threads">Threading section of
+ * the Application Model overview</a>.
+ *
+ * @param uri The content:// URI of the insertion request.
+ * @param values An array of sets of column_name/value pairs to add to the database.
+ * @return The number of values that were inserted.
+ */
+ public int bulkInsert(Uri uri, ContentValues[] values) {
+ int numValues = values.length;
+ for (int i = 0; i < numValues; i++) {
+ insert(uri, values[i]);
+ }
+ return numValues;
+ }
+
+ /**
+ * A request to delete one or more rows. The selection clause is applied when performing
+ * the deletion, allowing the operation to affect multiple rows in a
+ * directory.
+ * As a courtesy, call {@link ContentResolver#notifyChange(android.net.Uri ,android.database.ContentObserver) notifyDelete()}
+ * after deleting.
+ * This method can be called from multiple
+ * threads, as described in the
+ * <a href="{@docRoot}intro/appmodel.html#Threads">Threading section of the
+ * Application Model overview</a>.
+ *
+ * <p>The implementation is responsible for parsing out a row ID at the end
+ * of the URI, if a specific row is being deleted. That is, the client would
+ * pass in <code>content://contacts/people/22</code> and the implementation is
+ * responsible for parsing the record number (22) when creating a SQL statement.
+ *
+ * @param uri The full URI to query, including a row ID (if a specific record is requested).
+ * @param selection An optional restriction to apply to rows when deleting.
+ * @return The number of rows affected.
+ * @throws SQLException
+ */
+ public abstract int delete(Uri uri, String selection, String[] selectionArgs);
+
+ /**
+ * Update a content URI. All rows matching the optionally provided selection
+ * will have their columns listed as the keys in the values map with the
+ * values of those keys.
+ * As a courtesy, call {@link ContentResolver#notifyChange(android.net.Uri ,android.database.ContentObserver) notifyChange()}
+ * after updating.
+ * This method can be called from multiple
+ * threads, as described in the
+ * <a href="{@docRoot}intro/appmodel.html#Threads">Threading section of the
+ * Application Model overview</a>.
+ *
+ * @param uri The URI to query. This can potentially have a record ID if this
+ * is an update request for a specific record.
+ * @param values A Bundle mapping from column names to new column values (NULL is a
+ * valid value).
+ * @param selection An optional filter to match rows to update.
+ * @return the number of rows affected.
+ */
+ public abstract int update(Uri uri, ContentValues values, String selection,
+ String[] selectionArgs);
+
+ /**
+ * Open a file blob associated with a content URI.
+ * This method can be called from multiple
+ * threads, as described in the
+ * <a href="{@docRoot}intro/appmodel.html#Threads">Threading section of the
+ * Application Model overview</a>.
+ *
+ * <p>Returns a
+ * ParcelFileDescriptor, from which you can obtain a
+ * {@link java.io.FileDescriptor} for use with
+ * {@link java.io.FileInputStream}, {@link java.io.FileOutputStream}, etc.
+ * This can be used to store large data (such as an image) associated with
+ * a particular piece of content.
+ *
+ * <p>The returned ParcelFileDescriptor is owned by the caller, so it is
+ * their responsibility to close it when done. That is, the implementation
+ * of this method should create a new ParcelFileDescriptor for each call.
+ *
+ * @param uri The URI whose file is to be opened.
+ * @param mode Access mode for the file. May be "r" for read-only access
+ * or "rw" for read and write access.
+ *
+ * @return Returns a new ParcelFileDescriptor which you can use to access
+ * the file.
+ *
+ * @throws FileNotFoundException Throws FileNotFoundException if there is
+ * no file associated with the given URI or the mode is invalid.
+ * @throws SecurityException Throws SecurityException if the caller does
+ * not have permission to access the file.
+ */
+ public ParcelFileDescriptor openFile(Uri uri, String mode)
+ throws FileNotFoundException {
+ throw new FileNotFoundException("No files supported by provider at "
+ + uri);
+ }
+
+ /**
+ * Convenience for subclasses that wish to implement {@link #openFile}
+ * by looking up a column named "_data" at the given URI.
+ *
+ * @param uri The URI to be opened.
+ * @param mode The file mode.
+ *
+ * @return Returns a new ParcelFileDescriptor that can be used by the
+ * client to access the file.
+ */
+ protected final ParcelFileDescriptor openFileHelper(Uri uri,
+ String mode) throws FileNotFoundException {
+ Cursor c = query(uri, new String[]{"_data"}, null, null, null);
+ int count = (c != null) ? c.getCount() : 0;
+ if (count != 1) {
+ // If there is not exactly one result, throw an appropriate
+ // exception.
+ if (c != null) {
+ c.close();
+ }
+ if (count == 0) {
+ throw new FileNotFoundException("No entry for " + uri);
+ }
+ throw new FileNotFoundException("Multiple items at " + uri);
+ }
+
+ c.moveToFirst();
+ int i = c.getColumnIndex("_data");
+ String path = (i >= 0 ? c.getString(i) : null);
+ c.close();
+ if (path == null) {
+ throw new FileNotFoundException("Column _data not found.");
+ }
+
+ int modeBits;
+ if ("r".equals(mode)) {
+ modeBits = ParcelFileDescriptor.MODE_READ_ONLY;
+ } else if ("rw".equals(mode)) {
+ modeBits = ParcelFileDescriptor.MODE_READ_WRITE
+ | ParcelFileDescriptor.MODE_CREATE;
+ } else {
+ throw new FileNotFoundException("Bad mode for " + uri + ": "
+ + mode);
+ }
+ return ParcelFileDescriptor.open(new File(path), modeBits);
+ }
+
+ /**
+ * Get the sync adapter that is to be used by this content provider.
+ * This is intended for use by the sync system. If null then this
+ * content provider is considered not syncable.
+ * This method can be called from multiple
+ * threads, as described in the
+ * <a href="{@docRoot}intro/appmodel.html#Threads">Threading section of
+ * the Application Model overview</a>.
+ *
+ * @return the SyncAdapter that is to be used by this ContentProvider, or null
+ * if this ContentProvider is not syncable
+ * @hide
+ */
+ public SyncAdapter getSyncAdapter() {
+ return null;
+ }
+
+ /**
+ * Returns true if this instance is a temporary content provider.
+ * @return true if this instance is a temporary content provider
+ */
+ protected boolean isTemporary() {
+ return false;
+ }
+
+ /**
+ * Returns the Binder object for this provider.
+ *
+ * @return the Binder object for this provider
+ * @hide
+ */
+ public IContentProvider getIContentProvider() {
+ return mTransport;
+ }
+
+ /**
+ * After being instantiated, this is called to tell the content provider
+ * about itself.
+ *
+ * @param context The context this provider is running in
+ * @param info Registered information about this content provider
+ */
+ public void attachInfo(Context context, ProviderInfo info) {
+
+ /*
+ * Only allow it to be set once, so after the content service gives
+ * this to us clients can't change it.
+ */
+ if (mContext == null) {
+ mContext = context;
+ if (info != null) {
+ setReadPermission(info.readPermission);
+ setWritePermission(info.writePermission);
+ }
+ ContentProvider.this.onCreate();
+ }
+ }
+}
diff --git a/core/java/android/content/ContentProviderNative.java b/core/java/android/content/ContentProviderNative.java
new file mode 100644
index 0000000..ede2c9b
--- /dev/null
+++ b/core/java/android/content/ContentProviderNative.java
@@ -0,0 +1,435 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+import android.database.BulkCursorNative;
+import android.database.BulkCursorToCursorAdaptor;
+import android.database.Cursor;
+import android.database.CursorWindow;
+import android.database.DatabaseUtils;
+import android.database.IBulkCursor;
+import android.database.IContentObserver;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.RemoteException;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.ParcelFileDescriptor;
+import android.os.Parcelable;
+
+import java.io.FileNotFoundException;
+
+/**
+ * {@hide}
+ */
+abstract public class ContentProviderNative extends Binder implements IContentProvider {
+ private static final String TAG = "ContentProvider";
+
+ public ContentProviderNative()
+ {
+ attachInterface(this, descriptor);
+ }
+
+ /**
+ * Cast a Binder object into a content resolver interface, generating
+ * a proxy if needed.
+ */
+ static public IContentProvider asInterface(IBinder obj)
+ {
+ if (obj == null) {
+ return null;
+ }
+ IContentProvider in =
+ (IContentProvider)obj.queryLocalInterface(descriptor);
+ if (in != null) {
+ return in;
+ }
+
+ return new ContentProviderProxy(obj);
+ }
+
+ @Override
+ public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
+ throws RemoteException {
+ try {
+ switch (code) {
+ case QUERY_TRANSACTION:
+ {
+ data.enforceInterface(IContentProvider.descriptor);
+ Uri url = Uri.CREATOR.createFromParcel(data);
+ int num = data.readInt();
+ String[] projection = null;
+ if (num > 0) {
+ projection = new String[num];
+ for (int i = 0; i < num; i++) {
+ projection[i] = data.readString();
+ }
+ }
+ String selection = data.readString();
+ num = data.readInt();
+ String[] selectionArgs = null;
+ if (num > 0) {
+ selectionArgs = new String[num];
+ for (int i = 0; i < num; i++) {
+ selectionArgs[i] = data.readString();
+ }
+ }
+ String sortOrder = data.readString();
+ IContentObserver observer = IContentObserver.Stub.
+ asInterface(data.readStrongBinder());
+ CursorWindow window = CursorWindow.CREATOR.createFromParcel(data);
+
+ IBulkCursor bulkCursor = bulkQuery(url, projection, selection,
+ selectionArgs, sortOrder, observer, window);
+ reply.writeNoException();
+ if (bulkCursor != null) {
+ reply.writeStrongBinder(bulkCursor.asBinder());
+ } else {
+ reply.writeStrongBinder(null);
+ }
+ return true;
+ }
+
+ case GET_TYPE_TRANSACTION:
+ {
+ data.enforceInterface(IContentProvider.descriptor);
+ Uri url = Uri.CREATOR.createFromParcel(data);
+ String type = getType(url);
+ reply.writeNoException();
+ reply.writeString(type);
+
+ return true;
+ }
+
+ case INSERT_TRANSACTION:
+ {
+ data.enforceInterface(IContentProvider.descriptor);
+ Uri url = Uri.CREATOR.createFromParcel(data);
+ ContentValues values = ContentValues.CREATOR.createFromParcel(data);
+
+ Uri out = insert(url, values);
+ reply.writeNoException();
+ Uri.writeToParcel(reply, out);
+ return true;
+ }
+
+ case BULK_INSERT_TRANSACTION:
+ {
+ data.enforceInterface(IContentProvider.descriptor);
+ Uri url = Uri.CREATOR.createFromParcel(data);
+ ContentValues[] values = data.createTypedArray(ContentValues.CREATOR);
+
+ int count = bulkInsert(url, values);
+ reply.writeNoException();
+ reply.writeInt(count);
+ return true;
+ }
+
+ case DELETE_TRANSACTION:
+ {
+ data.enforceInterface(IContentProvider.descriptor);
+ Uri url = Uri.CREATOR.createFromParcel(data);
+ String selection = data.readString();
+ String[] selectionArgs = data.readStringArray();
+
+ int count = delete(url, selection, selectionArgs);
+
+ reply.writeNoException();
+ reply.writeInt(count);
+ return true;
+ }
+
+ case UPDATE_TRANSACTION:
+ {
+ data.enforceInterface(IContentProvider.descriptor);
+ Uri url = Uri.CREATOR.createFromParcel(data);
+ ContentValues values = ContentValues.CREATOR.createFromParcel(data);
+ String selection = data.readString();
+ String[] selectionArgs = data.readStringArray();
+
+ int count = update(url, values, selection, selectionArgs);
+
+ reply.writeNoException();
+ reply.writeInt(count);
+ return true;
+ }
+
+ case OPEN_FILE_TRANSACTION:
+ {
+ data.enforceInterface(IContentProvider.descriptor);
+ Uri url = Uri.CREATOR.createFromParcel(data);
+ String mode = data.readString();
+
+ ParcelFileDescriptor fd;
+ fd = openFile(url, mode);
+ reply.writeNoException();
+ if (fd != null) {
+ reply.writeInt(1);
+ fd.writeToParcel(reply,
+ Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
+ } else {
+ reply.writeInt(0);
+ }
+ return true;
+ }
+
+ case GET_SYNC_ADAPTER_TRANSACTION:
+ {
+ data.enforceInterface(IContentProvider.descriptor);
+ ISyncAdapter sa = getSyncAdapter();
+ reply.writeNoException();
+ reply.writeStrongBinder(sa != null ? sa.asBinder() : null);
+ return true;
+ }
+ }
+ } catch (Exception e) {
+ DatabaseUtils.writeExceptionToParcel(reply, e);
+ return true;
+ }
+
+ return super.onTransact(code, data, reply, flags);
+ }
+
+ public IBinder asBinder()
+ {
+ return this;
+ }
+}
+
+
+final class ContentProviderProxy implements IContentProvider
+{
+ public ContentProviderProxy(IBinder remote)
+ {
+ mRemote = remote;
+ }
+
+ public IBinder asBinder()
+ {
+ return mRemote;
+ }
+
+ public IBulkCursor bulkQuery(Uri url, String[] projection,
+ String selection, String[] selectionArgs, String sortOrder, IContentObserver observer,
+ CursorWindow window) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+
+ data.writeInterfaceToken(IContentProvider.descriptor);
+
+ url.writeToParcel(data, 0);
+ int length = 0;
+ if (projection != null) {
+ length = projection.length;
+ }
+ data.writeInt(length);
+ for (int i = 0; i < length; i++) {
+ data.writeString(projection[i]);
+ }
+ data.writeString(selection);
+ if (selectionArgs != null) {
+ length = selectionArgs.length;
+ } else {
+ length = 0;
+ }
+ data.writeInt(length);
+ for (int i = 0; i < length; i++) {
+ data.writeString(selectionArgs[i]);
+ }
+ data.writeString(sortOrder);
+ data.writeStrongBinder(observer.asBinder());
+ window.writeToParcel(data, 0);
+
+ mRemote.transact(IContentProvider.QUERY_TRANSACTION, data, reply, 0);
+
+ DatabaseUtils.readExceptionFromParcel(reply);
+
+ IBulkCursor bulkCursor = null;
+ IBinder bulkCursorBinder = reply.readStrongBinder();
+ if (bulkCursorBinder != null) {
+ bulkCursor = BulkCursorNative.asInterface(bulkCursorBinder);
+ }
+
+ data.recycle();
+ reply.recycle();
+
+ return bulkCursor;
+ }
+
+ public Cursor query(Uri url, String[] projection, String selection,
+ String[] selectionArgs, String sortOrder) throws RemoteException {
+ //TODO make a pool of windows so we can reuse memory dealers
+ CursorWindow window = new CursorWindow(false /* window will be used remotely */);
+ BulkCursorToCursorAdaptor adaptor = new BulkCursorToCursorAdaptor();
+ IBulkCursor bulkCursor = bulkQuery(url, projection, selection, selectionArgs, sortOrder,
+ adaptor.getObserver(), window);
+
+ if (bulkCursor == null) {
+ return null;
+ }
+ adaptor.set(bulkCursor);
+ return adaptor;
+ }
+
+ public String getType(Uri url) throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+
+ data.writeInterfaceToken(IContentProvider.descriptor);
+
+ url.writeToParcel(data, 0);
+
+ mRemote.transact(IContentProvider.GET_TYPE_TRANSACTION, data, reply, 0);
+
+ DatabaseUtils.readExceptionFromParcel(reply);
+ String out = reply.readString();
+
+ data.recycle();
+ reply.recycle();
+
+ return out;
+ }
+
+ public Uri insert(Uri url, ContentValues values) throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+
+ data.writeInterfaceToken(IContentProvider.descriptor);
+
+ url.writeToParcel(data, 0);
+ values.writeToParcel(data, 0);
+
+ mRemote.transact(IContentProvider.INSERT_TRANSACTION, data, reply, 0);
+
+ DatabaseUtils.readExceptionFromParcel(reply);
+ Uri out = Uri.CREATOR.createFromParcel(reply);
+
+ data.recycle();
+ reply.recycle();
+
+ return out;
+ }
+
+ public int bulkInsert(Uri url, ContentValues[] values) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+
+ data.writeInterfaceToken(IContentProvider.descriptor);
+
+ url.writeToParcel(data, 0);
+ data.writeTypedArray(values, 0);
+
+ mRemote.transact(IContentProvider.BULK_INSERT_TRANSACTION, data, reply, 0);
+
+ DatabaseUtils.readExceptionFromParcel(reply);
+ int count = reply.readInt();
+
+ data.recycle();
+ reply.recycle();
+
+ return count;
+ }
+
+ public int delete(Uri url, String selection, String[] selectionArgs)
+ throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+
+ data.writeInterfaceToken(IContentProvider.descriptor);
+
+ url.writeToParcel(data, 0);
+ data.writeString(selection);
+ data.writeStringArray(selectionArgs);
+
+ mRemote.transact(IContentProvider.DELETE_TRANSACTION, data, reply, 0);
+
+ DatabaseUtils.readExceptionFromParcel(reply);
+ int count = reply.readInt();
+
+ data.recycle();
+ reply.recycle();
+
+ return count;
+ }
+
+ public int update(Uri url, ContentValues values, String selection,
+ String[] selectionArgs) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+
+ data.writeInterfaceToken(IContentProvider.descriptor);
+
+ url.writeToParcel(data, 0);
+ values.writeToParcel(data, 0);
+ data.writeString(selection);
+ data.writeStringArray(selectionArgs);
+
+ mRemote.transact(IContentProvider.UPDATE_TRANSACTION, data, reply, 0);
+
+ DatabaseUtils.readExceptionFromParcel(reply);
+ int count = reply.readInt();
+
+ data.recycle();
+ reply.recycle();
+
+ return count;
+ }
+
+ public ParcelFileDescriptor openFile(Uri url, String mode)
+ throws RemoteException, FileNotFoundException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+
+ data.writeInterfaceToken(IContentProvider.descriptor);
+
+ url.writeToParcel(data, 0);
+ data.writeString(mode);
+
+ mRemote.transact(IContentProvider.OPEN_FILE_TRANSACTION, data, reply, 0);
+
+ DatabaseUtils.readExceptionWithFileNotFoundExceptionFromParcel(reply);
+ int has = reply.readInt();
+ ParcelFileDescriptor fd = has != 0 ? reply.readFileDescriptor() : null;
+
+ data.recycle();
+ reply.recycle();
+
+ return fd;
+ }
+
+ public ISyncAdapter getSyncAdapter() throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+
+ data.writeInterfaceToken(IContentProvider.descriptor);
+
+ mRemote.transact(IContentProvider.GET_SYNC_ADAPTER_TRANSACTION, data, reply, 0);
+
+ DatabaseUtils.readExceptionFromParcel(reply);
+ ISyncAdapter syncAdapter = ISyncAdapter.Stub.asInterface(reply.readStrongBinder());
+
+ data.recycle();
+ reply.recycle();
+
+ return syncAdapter;
+ }
+
+ private IBinder mRemote;
+}
+
diff --git a/core/java/android/content/ContentQueryMap.java b/core/java/android/content/ContentQueryMap.java
new file mode 100644
index 0000000..dbcb4a7
--- /dev/null
+++ b/core/java/android/content/ContentQueryMap.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.os.Handler;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Observable;
+
+/**
+ * Caches the contents of a cursor into a Map of String->ContentValues and optionally
+ * keeps the cache fresh by registering for updates on the content backing the cursor. The column of
+ * the database that is to be used as the key of the map is user-configurable, and the
+ * ContentValues contains all columns other than the one that is designated the key.
+ * <p>
+ * The cursor data is accessed by row key and column name via getValue().
+ */
+public class ContentQueryMap extends Observable {
+ private Cursor mCursor;
+ private String[] mColumnNames;
+ private int mKeyColumn;
+
+ private Handler mHandlerForUpdateNotifications = null;
+ private boolean mKeepUpdated = false;
+
+ private Map<String, ContentValues> mValues = null;
+
+ private ContentObserver mContentObserver;
+
+ /** Set when a cursor change notification is received and is cleared on a call to requery(). */
+ private boolean mDirty = false;
+
+ /**
+ * Creates a ContentQueryMap that caches the content backing the cursor
+ *
+ * @param cursor the cursor whose contents should be cached
+ * @param columnNameOfKey the column that is to be used as the key of the values map
+ * @param keepUpdated true if the cursor's ContentProvider should be monitored for changes and
+ * the map updated when changes do occur
+ * @param handlerForUpdateNotifications the Handler that should be used to receive
+ * notifications of changes (if requested). Normally you pass null here, but if
+ * you know that the thread that is creating this isn't a thread that can receive
+ * messages then you can create your own handler and use that here.
+ */
+ public ContentQueryMap(Cursor cursor, String columnNameOfKey, boolean keepUpdated,
+ Handler handlerForUpdateNotifications) {
+ mCursor = cursor;
+ mColumnNames = mCursor.getColumnNames();
+ mKeyColumn = mCursor.getColumnIndexOrThrow(columnNameOfKey);
+ mHandlerForUpdateNotifications = handlerForUpdateNotifications;
+ setKeepUpdated(keepUpdated);
+
+ // If we aren't keeping the cache updated with the current state of the cursor's
+ // ContentProvider then read it once into the cache. Otherwise the cache will be filled
+ // automatically.
+ if (!keepUpdated) {
+ readCursorIntoCache();
+ }
+ }
+
+ /**
+ * Change whether or not the ContentQueryMap will register with the cursor's ContentProvider
+ * for change notifications. If you use a ContentQueryMap in an activity you should call this
+ * with false in onPause(), which means you need to call it with true in onResume()
+ * if want it to be kept updated.
+ * @param keepUpdated if true the ContentQueryMap should be registered with the cursor's
+ * ContentProvider, false otherwise
+ */
+ public void setKeepUpdated(boolean keepUpdated) {
+ if (keepUpdated == mKeepUpdated) return;
+ mKeepUpdated = keepUpdated;
+
+ if (!mKeepUpdated) {
+ mCursor.unregisterContentObserver(mContentObserver);
+ mContentObserver = null;
+ } else {
+ if (mHandlerForUpdateNotifications == null) {
+ mHandlerForUpdateNotifications = new Handler();
+ }
+ if (mContentObserver == null) {
+ mContentObserver = new ContentObserver(mHandlerForUpdateNotifications) {
+ @Override
+ public void onChange(boolean selfChange) {
+ // If anyone is listening, we need to do this now to broadcast
+ // to the observers. Otherwise, we'll just set mDirty and
+ // let it query lazily when they ask for the values.
+ if (countObservers() != 0) {
+ requery();
+ } else {
+ mDirty = true;
+ }
+ }
+ };
+ }
+ mCursor.registerContentObserver(mContentObserver);
+ // mark dirty, since it is possible the cursor's backing data had changed before we
+ // registered for changes
+ mDirty = true;
+ }
+ }
+
+ /**
+ * Access the ContentValues for the row specified by rowName
+ * @param rowName which row to read
+ * @return the ContentValues for the row, or null if the row wasn't present in the cursor
+ */
+ public synchronized ContentValues getValues(String rowName) {
+ if (mDirty) requery();
+ return mValues.get(rowName);
+ }
+
+ /** Requeries the cursor and reads the contents into the cache */
+ public void requery() {
+ mDirty = false;
+ mCursor.requery();
+ readCursorIntoCache();
+ setChanged();
+ notifyObservers();
+ }
+
+ private synchronized void readCursorIntoCache() {
+ // Make a new map so old values returned by getRows() are undisturbed.
+ int capacity = mValues != null ? mValues.size() : 0;
+ mValues = new HashMap<String, ContentValues>(capacity);
+ while (mCursor.moveToNext()) {
+ ContentValues values = new ContentValues();
+ for (int i = 0; i < mColumnNames.length; i++) {
+ if (i != mKeyColumn) {
+ values.put(mColumnNames[i], mCursor.getString(i));
+ }
+ }
+ mValues.put(mCursor.getString(mKeyColumn), values);
+ }
+ }
+
+ public synchronized Map<String, ContentValues> getRows() {
+ if (mDirty) requery();
+ return mValues;
+ }
+
+ public synchronized void close() {
+ if (mContentObserver != null) {
+ mCursor.unregisterContentObserver(mContentObserver);
+ mContentObserver = null;
+ }
+ mCursor.close();
+ mCursor = null;
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ if (mCursor != null) close();
+ super.finalize();
+ }
+}
diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java
new file mode 100644
index 0000000..52f55b6
--- /dev/null
+++ b/core/java/android/content/ContentResolver.java
@@ -0,0 +1,644 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.database.CursorWrapper;
+import android.database.IContentObserver;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.text.TextUtils;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.List;
+
+
+/**
+ * This class provides applications access to the content model.
+ */
+public abstract class ContentResolver {
+ public final static String SYNC_EXTRAS_ACCOUNT = "account";
+ public static final String SYNC_EXTRAS_EXPEDITED = "expedited";
+ public static final String SYNC_EXTRAS_FORCE = "force";
+ public static final String SYNC_EXTRAS_UPLOAD = "upload";
+ public static final String SYNC_EXTRAS_OVERRIDE_TOO_MANY_DELETIONS = "deletions_override";
+ public static final String SYNC_EXTRAS_DISCARD_LOCAL_DELETIONS = "discard_deletions";
+
+ public static final String SCHEME_CONTENT = "content";
+ public static final String SCHEME_ANDROID_RESOURCE = "android.resource";
+ public static final String SCHEME_FILE = "file";
+
+ /**
+ * This is the Android platform's base MIME type for a content: URI
+ * containing a Cursor of a single item. Applications should use this
+ * as the base type along with their own sub-type of their content: URIs
+ * that represent a particular item. For example, hypothetical IMAP email
+ * client may have a URI
+ * <code>content://com.company.provider.imap/inbox/1</code> for a particular
+ * message in the inbox, whose MIME type would be reported as
+ * <code>CURSOR_ITEM_BASE_TYPE + "/vnd.company.imap-msg"</code>
+ *
+ * <p>Compare with {@link #CURSOR_DIR_BASE_TYPE}.
+ */
+ public static final String CURSOR_ITEM_BASE_TYPE = "vnd.android.cursor.item";
+
+ /**
+ * This is the Android platform's base MIME type for a content: URI
+ * containing a Cursor of zero or more items. Applications should use this
+ * as the base type along with their own sub-type of their content: URIs
+ * that represent a directory of items. For example, hypothetical IMAP email
+ * client may have a URI
+ * <code>content://com.company.provider.imap/inbox</code> for all of the
+ * messages in its inbox, whose MIME type would be reported as
+ * <code>CURSOR_DIR_BASE_TYPE + "/vnd.company.imap-msg"</code>
+ *
+ * <p>Note how the base MIME type varies between this and
+ * {@link #CURSOR_ITEM_BASE_TYPE} depending on whether there is
+ * one single item or multiple items in the data set, while the sub-type
+ * remains the same because in either case the data structure contained
+ * in the cursor is the same.
+ */
+ public static final String CURSOR_DIR_BASE_TYPE = "vnd.android.cursor.dir";
+
+ public ContentResolver(Context context)
+ {
+ mContext = context;
+ }
+
+ /** @hide */
+ protected abstract IContentProvider acquireProvider(Context c, String name);
+ /** @hide */
+ public abstract boolean releaseProvider(IContentProvider icp);
+
+ /**
+ * Return the MIME type of the given content URL.
+ *
+ * @param url A Uri identifying content (either a list or specific type),
+ * using the content:// scheme.
+ * @return A MIME type for the content, or null if the URL is invalid or the type is unknown
+ */
+ public final String getType(Uri url)
+ {
+ IContentProvider provider = acquireProvider(url);
+ if (provider == null) {
+ return null;
+ }
+ try {
+ return provider.getType(url);
+ } catch (RemoteException e) {
+ return null;
+ } catch (java.lang.Exception e) {
+ return null;
+ } finally {
+ releaseProvider(provider);
+ }
+ }
+
+ /**
+ * Query the given URI, returning a {@link Cursor} over the result set.
+ *
+ * @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 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 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.
+ * @return A Cursor object, which is positioned before the first entry, or null
+ * @see Cursor
+ */
+ public final Cursor query(Uri uri, String[] projection,
+ String selection, String[] selectionArgs, String sortOrder) {
+ IContentProvider provider = acquireProvider(uri);
+ if (provider == null) {
+ return null;
+ }
+ try {
+ Cursor qCursor = provider.query(uri, projection, selection, selectionArgs, sortOrder);
+ if(qCursor == null) {
+ releaseProvider(provider);
+ return null;
+ }
+ //Wrap the cursor object into CursorWrapperInner object
+ return new CursorWrapperInner(qCursor, provider);
+ } catch (RemoteException e) {
+ releaseProvider(provider);
+ return null;
+ } catch(RuntimeException e) {
+ releaseProvider(provider);
+ throw e;
+ }
+ }
+
+ /**
+ * Open a stream on to the content associated with a content URI. If there
+ * is no data associated with the URI, FileNotFoundException is thrown.
+ *
+ * <h5>Accepts the following URI schemes:</h5>
+ * <ul>
+ * <li>content ({@link #SCHEME_CONTENT})</li>
+ * <li>android.resource ({@link #SCHEME_ANDROID_RESOURCE})</li>
+ * <li>file ({@link #SCHEME_FILE})</li>
+ * </ul>
+ * <h5>The android.resource ({@link #SCHEME_ANDROID_RESOURCE}) Scheme</h5>
+ * <p>
+ * A Uri object can be used to reference a resource in an APK file. The
+ * Uri should be one of the following formats:
+ * <ul>
+ * <li><code>android.resource://package_name/id_number</code><br/>
+ * <code>package_name</code> is your package name as listed in your AndroidManifest.xml.
+ * For example <code>com.example.myapp</code><br/>
+ * <code>id_number</code> is the int form of the ID.<br/>
+ * The easiest way to construct this form is
+ * <pre>Uri uri = Uri.parse("android.resource://com.example.myapp/" + R.raw.my_resource");</pre>
+ * </li>
+ * <li><code>android.resource://package_name/type/name</code><br/>
+ * <code>package_name</code> is your package name as listed in your AndroidManifest.xml.
+ * For example <code>com.example.myapp</code><br/>
+ * <code>type</code> is the string form of the resource type. For example, <code>raw</code>
+ * or <code>drawable</code>.
+ * <code>name</code> is the string form of the resource name. That is, whatever the file
+ * name was in your res directory, without the type extension.
+ * The easiest way to construct this form is
+ * <pre>Uri uri = Uri.parse("android.resource://com.example.myapp/raw/my_resource");</pre>
+ * </li>
+ * </ul>
+ * @param uri The desired "content:" URI.
+ * @return InputStream
+ * @throws FileNotFoundException if the provided URI could not be opened.
+ */
+ public final InputStream openInputStream(Uri uri)
+ throws FileNotFoundException {
+ String scheme = uri.getScheme();
+ if (SCHEME_CONTENT.equals(scheme)) {
+ ParcelFileDescriptor fd = openFileDescriptor(uri, "r");
+ return fd != null ? new ParcelFileDescriptor.AutoCloseInputStream(fd) : null;
+ } else if (SCHEME_ANDROID_RESOURCE.equals(scheme)) {
+ String authority = uri.getAuthority();
+ Resources r;
+ if (TextUtils.isEmpty(authority)) {
+ throw new FileNotFoundException("No authority: " + uri);
+ } else {
+ try {
+ r = mContext.getPackageManager().getResourcesForApplication(authority);
+ } catch (NameNotFoundException ex) {
+ throw new FileNotFoundException("No package found for authority: " + uri);
+ }
+ }
+ List<String> path = uri.getPathSegments();
+ if (path == null) {
+ throw new FileNotFoundException("No path: " + uri);
+ }
+ int len = path.size();
+ int id;
+ if (len == 1) {
+ try {
+ id = Integer.parseInt(path.get(0));
+ } catch (NumberFormatException e) {
+ throw new FileNotFoundException("Single path segment is not a resource ID: " + uri);
+ }
+ } else if (len == 2) {
+ id = r.getIdentifier(path.get(1), path.get(0), authority);
+ } else {
+ throw new FileNotFoundException("More than two path segments: " + uri);
+ }
+ if (id == 0) {
+ throw new FileNotFoundException("No resource found for: " + uri);
+ }
+ try {
+ InputStream stream = r.openRawResource(id);
+ return stream;
+ } catch (Resources.NotFoundException ex) {
+ throw new FileNotFoundException("Resource ID does not exist: " + uri);
+ }
+ } else if (SCHEME_FILE.equals(scheme)) {
+ return new FileInputStream(uri.getPath());
+ } else {
+ throw new FileNotFoundException("Unknown scheme: " + uri);
+ }
+ }
+
+ /**
+ * Open a stream on to the content associated with a content URI. If there
+ * is no data associated with the URI, FileNotFoundException is thrown.
+ *
+ * <h5>Accepts the following URI schemes:</h5>
+ * <ul>
+ * <li>content ({@link #SCHEME_CONTENT})</li>
+ * </ul>
+ *
+ * @param uri The desired "content:" URI.
+ * @return OutputStream
+ */
+ public final OutputStream openOutputStream(Uri uri)
+ throws FileNotFoundException {
+ String scheme = uri.getScheme();
+ if (SCHEME_CONTENT.equals(scheme)) {
+ ParcelFileDescriptor fd = openFileDescriptor(uri, "rw");
+ return fd != null
+ ? new ParcelFileDescriptor.AutoCloseOutputStream(fd) : null;
+ } else {
+ throw new FileNotFoundException("Unknown scheme: " + uri);
+ }
+ }
+
+ /**
+ * Open a raw file descriptor to access data under a "content:" URI. This
+ * interacts with the underlying {@link ContentProvider#openFile}
+ * ContentProvider.openFile()} method of the provider associated with the
+ * given URI, to retrieve any file stored there.
+ *
+ * <h5>Accepts the following URI schemes:</h5>
+ * <ul>
+ * <li>content ({@link #SCHEME_CONTENT})</li>
+ * </ul>
+ *
+ * @param uri The desired URI to open.
+ * @param mode The file mode to use, as per {@link ContentProvider#openFile
+ * ContentProvider.openFile}.
+ * @return Returns a new ParcelFileDescriptor pointing to the file. You
+ * own this descriptor and are responsible for closing it when done.
+ * @throws FileNotFoundException Throws FileNotFoundException of no
+ * file exists under the URI or the mode is invalid.
+ */
+ public final ParcelFileDescriptor openFileDescriptor(Uri uri,
+ String mode) throws FileNotFoundException {
+ IContentProvider provider = acquireProvider(uri);
+ if (provider == null) {
+ throw new FileNotFoundException("No content provider: " + uri);
+ }
+ try {
+ ParcelFileDescriptor fd = provider.openFile(uri, mode);
+ if(fd == null) {
+ releaseProvider(provider);
+ return null;
+ }
+ return new ParcelFileDescriptorInner(fd, provider);
+ } catch (RemoteException e) {
+ releaseProvider(provider);
+ throw new FileNotFoundException("Dead content provider: " + uri);
+ } catch (FileNotFoundException e) {
+ releaseProvider(provider);
+ throw e;
+ } catch (RuntimeException e) {
+ releaseProvider(provider);
+ throw e;
+ }
+ }
+
+ /**
+ * Inserts a row into a table at the given URL.
+ *
+ * If the content provider supports transactions the insertion will be atomic.
+ *
+ * @param url The URL of the table to insert into.
+ * @param values The initial values for the newly inserted row. The key is the column name for
+ * the field. Passing an empty ContentValues will create an empty row.
+ * @return the URL of the newly created row.
+ */
+ public final Uri insert(Uri url, ContentValues values)
+ {
+ IContentProvider provider = acquireProvider(url);
+ if (provider == null) {
+ throw new IllegalArgumentException("Unknown URL " + url);
+ }
+ try {
+ return provider.insert(url, values);
+ } catch (RemoteException e) {
+ return null;
+ } finally {
+ releaseProvider(provider);
+ }
+ }
+
+ /**
+ * Inserts multiple rows into a table at the given URL.
+ *
+ * This function make no guarantees about the atomicity of the insertions.
+ *
+ * @param url The URL of the table to insert into.
+ * @param values The initial values for the newly inserted rows. The key is the column name for
+ * the field. Passing null will create an empty row.
+ * @return the number of newly created rows.
+ */
+ public final int bulkInsert(Uri url, ContentValues[] values)
+ {
+ IContentProvider provider = acquireProvider(url);
+ if (provider == null) {
+ throw new IllegalArgumentException("Unknown URL " + url);
+ }
+ try {
+ return provider.bulkInsert(url, values);
+ } catch (RemoteException e) {
+ return 0;
+ } finally {
+ releaseProvider(provider);
+ }
+ }
+
+ /**
+ * Deletes row(s) specified by a content URI.
+ *
+ * If the content provider supports transactions, the deletion will be atomic.
+ *
+ * @param url The URL of the row to delete.
+ * @param where A filter to apply to rows before deleting, formatted as an SQL WHERE clause
+ (excluding the WHERE itself).
+ * @return The number of rows deleted.
+ */
+ public final int delete(Uri url, String where, String[] selectionArgs)
+ {
+ IContentProvider provider = acquireProvider(url);
+ if (provider == null) {
+ throw new IllegalArgumentException("Unknown URL " + url);
+ }
+ try {
+ return provider.delete(url, where, selectionArgs);
+ } catch (RemoteException e) {
+ return -1;
+ } finally {
+ releaseProvider(provider);
+ }
+ }
+
+ /**
+ * Update row(s) in a content URI.
+ *
+ * If the content provider supports transactions the update will be atomic.
+ *
+ * @param uri The URI to modify.
+ * @param values The new field values. The key is the column name for the field.
+ A null value will remove an existing field value.
+ * @param where A filter to apply to rows before deleting, formatted as an SQL WHERE clause
+ (excluding the WHERE itself).
+ * @return the URL of the newly created row
+ * @throws NullPointerException if uri or values are null
+ */
+ public final int update(Uri uri, ContentValues values, String where,
+ String[] selectionArgs) {
+ IContentProvider provider = acquireProvider(uri);
+ if (provider == null) {
+ throw new IllegalArgumentException("Unknown URI " + uri);
+ }
+ try {
+ return provider.update(uri, values, where, selectionArgs);
+ } catch (RemoteException e) {
+ return -1;
+ } finally {
+ releaseProvider(provider);
+ }
+ }
+
+ /**
+ * Returns the content provider for the given content URI..
+ *
+ * @param uri The URI to a content provider
+ * @return The ContentProvider for the given URI, or null if no content provider is found.
+ * @hide
+ */
+ public final IContentProvider acquireProvider(Uri uri)
+ {
+ if (!SCHEME_CONTENT.equals(uri.getScheme())) {
+ return null;
+ }
+ String auth = uri.getAuthority();
+ if (auth != null) {
+ return acquireProvider(mContext, uri.getAuthority());
+ }
+ return null;
+ }
+
+ /**
+ * @hide
+ */
+ public final IContentProvider acquireProvider(String name) {
+ if(name == null) {
+ return null;
+ }
+ return acquireProvider(mContext, name);
+ }
+
+ /**
+ * Register an observer class that gets callbacks when data identified by a
+ * given content URI changes.
+ *
+ * @param uri The URI to watch for changes. This can be a specific row URI, or a base URI
+ * for a whole class of content.
+ * @param notifyForDescendents If <code>true</code> changes to URIs beginning with <code>uri</code>
+ * will also cause notifications to be sent. If <code>false</code> only changes to the exact URI
+ * specified by <em>uri</em> will cause notifications to be sent. If true, than any URI values
+ * at or below the specified URI will also trigger a match.
+ * @param observer The object that receives callbacks when changes occur.
+ * @see #unregisterContentObserver
+ */
+ public final void registerContentObserver(Uri uri, boolean notifyForDescendents,
+ ContentObserver observer)
+ {
+ try {
+ ContentServiceNative.getDefault().registerContentObserver(uri, notifyForDescendents,
+ observer.getContentObserver());
+ } catch (RemoteException e) {
+ }
+ }
+
+ /**
+ * Unregisters a change observer.
+ *
+ * @param observer The previously registered observer that is no longer needed.
+ * @see #registerContentObserver
+ */
+ public final void unregisterContentObserver(ContentObserver observer) {
+ try {
+ IContentObserver contentObserver = observer.releaseContentObserver();
+ if (contentObserver != null) {
+ ContentServiceNative.getDefault().unregisterContentObserver(
+ contentObserver);
+ }
+ } catch (RemoteException e) {
+ }
+ }
+
+ /**
+ * Notify registered observers that a row was updated.
+ * 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>
+ */
+ public void notifyChange(Uri uri, ContentObserver observer) {
+ notifyChange(uri, observer, true /* sync to network */);
+ }
+
+ /**
+ * Notify registered observers that a row was updated.
+ * 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 syncToNetwork If true, attempt to sync the change to the network.
+ */
+ public void notifyChange(Uri uri, ContentObserver observer, boolean syncToNetwork) {
+ try {
+ ContentServiceNative.getDefault().notifyChange(
+ uri, observer == null ? null : observer.getContentObserver(),
+ observer != null && observer.deliverSelfNotifications(), syncToNetwork);
+ } catch (RemoteException e) {
+ }
+ }
+
+ /**
+ * Start an asynchronous sync operation. If you want to monitor the progress
+ * of the sync you may register a SyncObserver. Only values of the following
+ * types may be used in the extras bundle:
+ * <ul>
+ * <li>Integer</li>
+ * <li>Long</li>
+ * <li>Boolean</li>
+ * <li>Float</li>
+ * <li>Double</li>
+ * <li>String</li>
+ * </ul>
+ *
+ * @param uri the uri of the provider to sync or null to sync all providers.
+ * @param extras any extras to pass to the SyncAdapter.
+ */
+ public void startSync(Uri uri, Bundle extras) {
+ validateSyncExtrasBundle(extras);
+ try {
+ ContentServiceNative.getDefault().startSync(uri, extras);
+ } catch (RemoteException e) {
+ }
+ }
+
+ /**
+ * Check that only values of the following types are in the Bundle:
+ * <ul>
+ * <li>Integer</li>
+ * <li>Long</li>
+ * <li>Boolean</li>
+ * <li>Float</li>
+ * <li>Double</li>
+ * <li>String</li>
+ * <li>null</li>
+ * </ul>
+ * @param extras the Bundle to check
+ */
+ public static void validateSyncExtrasBundle(Bundle extras) {
+ try {
+ for (String key : extras.keySet()) {
+ Object value = extras.get(key);
+ if (value == null) continue;
+ if (value instanceof Long) continue;
+ if (value instanceof Integer) continue;
+ if (value instanceof Boolean) continue;
+ if (value instanceof Float) continue;
+ if (value instanceof Double) continue;
+ if (value instanceof String) continue;
+ throw new IllegalArgumentException("unexpected value type: "
+ + value.getClass().getName());
+ }
+ } catch (IllegalArgumentException e) {
+ throw e;
+ } catch (RuntimeException exc) {
+ throw new IllegalArgumentException("error unparceling Bundle", exc);
+ }
+ }
+
+ public void cancelSync(Uri uri) {
+ try {
+ ContentServiceNative.getDefault().cancelSync(uri);
+ } catch (RemoteException e) {
+ }
+ }
+
+ private final class CursorWrapperInner extends CursorWrapper {
+ private IContentProvider mContentProvider;
+ public static final String TAG="CursorWrapperInner";
+ private boolean mCloseFlag = false;
+
+ CursorWrapperInner(Cursor cursor, IContentProvider icp) {
+ super(cursor);
+ mContentProvider = icp;
+ }
+
+ @Override
+ public void close() {
+ super.close();
+ ContentResolver.this.releaseProvider(mContentProvider);
+ mCloseFlag = true;
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ if(!mCloseFlag) {
+ ContentResolver.this.releaseProvider(mContentProvider);
+ }
+ } finally {
+ super.finalize();
+ }
+ }
+ }
+
+ private final class ParcelFileDescriptorInner extends ParcelFileDescriptor {
+ private IContentProvider mContentProvider;
+ public static final String TAG="ParcelFileDescriptorInner";
+ private boolean mReleaseProviderFlag = false;
+
+ ParcelFileDescriptorInner(ParcelFileDescriptor pfd, IContentProvider icp) {
+ super(pfd);
+ mContentProvider = icp;
+ }
+
+ @Override
+ public void close() throws IOException {
+ if(!mReleaseProviderFlag) {
+ super.close();
+ ContentResolver.this.releaseProvider(mContentProvider);
+ mReleaseProviderFlag = true;
+ }
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ if (!mReleaseProviderFlag) {
+ close();
+ }
+ }
+ }
+
+ private final Context mContext;
+ private static final String TAG = "ContentResolver";
+}
diff --git a/core/java/android/content/ContentService.java b/core/java/android/content/ContentService.java
new file mode 100644
index 0000000..b028868
--- /dev/null
+++ b/core/java/android/content/ContentService.java
@@ -0,0 +1,376 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+import android.database.IContentObserver;
+import android.database.sqlite.SQLiteException;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.Config;
+import android.util.Log;
+import android.Manifest;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
+/**
+ * {@hide}
+ */
+public final class ContentService extends ContentServiceNative {
+ private static final String TAG = "ContentService";
+ private Context mContext;
+ private boolean mFactoryTest;
+ private final ObserverNode mRootNode = new ObserverNode("");
+ private SyncManager mSyncManager = null;
+ private final Object mSyncManagerLock = new Object();
+
+ private SyncManager getSyncManager() {
+ synchronized(mSyncManagerLock) {
+ try {
+ // Try to create the SyncManager, return null if it fails (e.g. the disk is full).
+ if (mSyncManager == null) mSyncManager = new SyncManager(mContext, mFactoryTest);
+ } catch (SQLiteException e) {
+ Log.e(TAG, "Can't create SyncManager", e);
+ }
+ return mSyncManager;
+ }
+ }
+
+ @Override
+ protected synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ mContext.enforceCallingOrSelfPermission(Manifest.permission.DUMP,
+ "caller doesn't have the DUMP permission");
+
+ // This makes it so that future permission checks will be in the context of this
+ // process rather than the caller's process. We will restore this before returning.
+ long identityToken = clearCallingIdentity();
+ try {
+ if (mSyncManager == null) {
+ pw.println("No SyncManager created! (Disk full?)");
+ } else {
+ mSyncManager.dump(fd, pw);
+ }
+ } finally {
+ restoreCallingIdentity(identityToken);
+ }
+ }
+
+ /*package*/ ContentService(Context context, boolean factoryTest) {
+ mContext = context;
+ mFactoryTest = factoryTest;
+ getSyncManager();
+ }
+
+ public void registerContentObserver(Uri uri, boolean notifyForDescendents,
+ IContentObserver observer) {
+ if (observer == null || uri == null) {
+ throw new IllegalArgumentException("You must pass a valid uri and observer");
+ }
+ synchronized (mRootNode) {
+ mRootNode.addObserver(uri, observer, notifyForDescendents);
+ if (Config.LOGV) Log.v(TAG, "Registered observer " + observer + " at " + uri +
+ " with notifyForDescendents " + notifyForDescendents);
+ }
+ }
+
+ public void unregisterContentObserver(IContentObserver observer) {
+ if (observer == null) {
+ throw new IllegalArgumentException("You must pass a valid observer");
+ }
+ synchronized (mRootNode) {
+ mRootNode.removeObserver(observer);
+ if (Config.LOGV) Log.v(TAG, "Unregistered observer " + observer);
+ }
+ }
+
+ public void notifyChange(Uri uri, IContentObserver observer,
+ boolean observerWantsSelfNotifications, boolean syncToNetwork) {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "Notifying update of " + uri + " from observer " + observer
+ + ", syncToNetwork " + syncToNetwork);
+ }
+ // This makes it so that future permission checks will be in the context of this
+ // process rather than the caller's process. We will restore this before returning.
+ long identityToken = clearCallingIdentity();
+ try {
+ ArrayList<ObserverCall> calls = new ArrayList<ObserverCall>();
+ synchronized (mRootNode) {
+ mRootNode.collectObservers(uri, 0, observer, observerWantsSelfNotifications,
+ calls);
+ }
+ final int numCalls = calls.size();
+ for (int i=0; i<numCalls; i++) {
+ ObserverCall oc = calls.get(i);
+ try {
+ oc.mObserver.onChange(oc.mSelfNotify);
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "Notified " + oc.mObserver + " of " + "update at " + uri);
+ }
+ } catch (RemoteException ex) {
+ synchronized (mRootNode) {
+ Log.w(TAG, "Found dead observer, removing");
+ IBinder binder = oc.mObserver.asBinder();
+ final ArrayList<ObserverNode.ObserverEntry> list
+ = oc.mNode.mObservers;
+ int numList = list.size();
+ for (int j=0; j<numList; j++) {
+ ObserverNode.ObserverEntry oe = list.get(j);
+ if (oe.observer.asBinder() == binder) {
+ list.remove(j);
+ j--;
+ numList--;
+ }
+ }
+ }
+ }
+ }
+ if (syncToNetwork) {
+ SyncManager syncManager = getSyncManager();
+ if (syncManager != null) syncManager.scheduleLocalSync(uri);
+ }
+ } finally {
+ restoreCallingIdentity(identityToken);
+ }
+ }
+
+ /**
+ * Hide this class since it is not part of api,
+ * but current unittest framework requires it to be public
+ * @hide
+ *
+ */
+ public static final class ObserverCall {
+ final ObserverNode mNode;
+ final IContentObserver mObserver;
+ final boolean mSelfNotify;
+
+ ObserverCall(ObserverNode node, IContentObserver observer,
+ boolean selfNotify) {
+ mNode = node;
+ mObserver = observer;
+ mSelfNotify = selfNotify;
+ }
+ }
+
+ public void startSync(Uri url, Bundle extras) {
+ ContentResolver.validateSyncExtrasBundle(extras);
+ // This makes it so that future permission checks will be in the context of this
+ // process rather than the caller's process. We will restore this before returning.
+ long identityToken = clearCallingIdentity();
+ try {
+ SyncManager syncManager = getSyncManager();
+ if (syncManager != null) syncManager.startSync(url, extras);
+ } finally {
+ restoreCallingIdentity(identityToken);
+ }
+ }
+
+ /**
+ * Clear all scheduled sync operations that match the uri and cancel the active sync
+ * if it matches the uri. If the uri is null, clear all scheduled syncs and cancel
+ * the active one, if there is one.
+ * @param uri Filter on the sync operations to cancel, or all if null.
+ */
+ public void cancelSync(Uri uri) {
+ // This makes it so that future permission checks will be in the context of this
+ // process rather than the caller's process. We will restore this before returning.
+ long identityToken = clearCallingIdentity();
+ try {
+ SyncManager syncManager = getSyncManager();
+ if (syncManager != null) {
+ syncManager.clearScheduledSyncOperations(uri);
+ syncManager.cancelActiveSync(uri);
+ }
+ } finally {
+ restoreCallingIdentity(identityToken);
+ }
+ }
+
+ public static IContentService main(Context context, boolean factoryTest) {
+ ContentService service = new ContentService(context, factoryTest);
+ ServiceManager.addService("content", service);
+ return service;
+ }
+
+ /**
+ * Hide this class since it is not part of api,
+ * but current unittest framework requires it to be public
+ * @hide
+ */
+ public static final class ObserverNode {
+ private class ObserverEntry implements IBinder.DeathRecipient {
+ public IContentObserver observer;
+ public boolean notifyForDescendents;
+
+ public ObserverEntry(IContentObserver o, boolean n) {
+ observer = o;
+ notifyForDescendents = n;
+ try {
+ observer.asBinder().linkToDeath(this, 0);
+ } catch (RemoteException e) {
+ binderDied();
+ }
+ }
+
+ public void binderDied() {
+ removeObserver(observer);
+ }
+ }
+
+ public static final int INSERT_TYPE = 0;
+ public static final int UPDATE_TYPE = 1;
+ public static final int DELETE_TYPE = 2;
+
+ private String mName;
+ private ArrayList<ObserverNode> mChildren = new ArrayList<ObserverNode>();
+ private ArrayList<ObserverEntry> mObservers = new ArrayList<ObserverEntry>();
+
+ public ObserverNode(String name) {
+ mName = name;
+ }
+
+ private String getUriSegment(Uri uri, int index) {
+ if (uri != null) {
+ if (index == 0) {
+ return uri.getAuthority();
+ } else {
+ return uri.getPathSegments().get(index - 1);
+ }
+ } else {
+ return null;
+ }
+ }
+
+ private int countUriSegments(Uri uri) {
+ if (uri == null) {
+ return 0;
+ }
+ return uri.getPathSegments().size() + 1;
+ }
+
+ public void addObserver(Uri uri, IContentObserver observer, boolean notifyForDescendents) {
+ addObserver(uri, 0, observer, notifyForDescendents);
+ }
+
+ private void addObserver(Uri uri, int index, IContentObserver observer,
+ boolean notifyForDescendents) {
+
+ // If this is the leaf node add the observer
+ if (index == countUriSegments(uri)) {
+ mObservers.add(new ObserverEntry(observer, notifyForDescendents));
+ return;
+ }
+
+ // Look to see if the proper child already exists
+ String segment = getUriSegment(uri, index);
+ int N = mChildren.size();
+ for (int i = 0; i < N; i++) {
+ ObserverNode node = mChildren.get(i);
+ if (node.mName.equals(segment)) {
+ node.addObserver(uri, index + 1, observer, notifyForDescendents);
+ return;
+ }
+ }
+
+ // No child found, create one
+ ObserverNode node = new ObserverNode(segment);
+ mChildren.add(node);
+ node.addObserver(uri, index + 1, observer, notifyForDescendents);
+ }
+
+ public boolean removeObserver(IContentObserver observer) {
+ int size = mChildren.size();
+ for (int i = 0; i < size; i++) {
+ boolean empty = mChildren.get(i).removeObserver(observer);
+ if (empty) {
+ mChildren.remove(i);
+ i--;
+ size--;
+ }
+ }
+
+ IBinder observerBinder = observer.asBinder();
+ size = mObservers.size();
+ for (int i = 0; i < size; i++) {
+ ObserverEntry entry = mObservers.get(i);
+ if (entry.observer.asBinder() == observerBinder) {
+ mObservers.remove(i);
+ // We no longer need to listen for death notifications. Remove it.
+ observerBinder.unlinkToDeath(entry, 0);
+ break;
+ }
+ }
+
+ if (mChildren.size() == 0 && mObservers.size() == 0) {
+ return true;
+ }
+ return false;
+ }
+
+ private void collectMyObservers(Uri uri,
+ boolean leaf, IContentObserver observer, boolean selfNotify,
+ ArrayList<ObserverCall> calls)
+ {
+ int N = mObservers.size();
+ IBinder observerBinder = observer == null ? null : observer.asBinder();
+ for (int i = 0; i < N; i++) {
+ ObserverEntry entry = mObservers.get(i);
+
+ // Don't notify the observer if it sent the notification and isn't interesed
+ // in self notifications
+ if (entry.observer.asBinder() == observerBinder && !selfNotify) {
+ continue;
+ }
+
+ // Make sure the observer is interested in the notification
+ if (leaf || (!leaf && entry.notifyForDescendents)) {
+ calls.add(new ObserverCall(this, entry.observer, selfNotify));
+ }
+ }
+ }
+
+ public void collectObservers(Uri uri, int index, IContentObserver observer,
+ boolean selfNotify, ArrayList<ObserverCall> calls) {
+ String segment = null;
+ int segmentCount = countUriSegments(uri);
+ if (index >= segmentCount) {
+ // This is the leaf node, notify all observers
+ collectMyObservers(uri, true, observer, selfNotify, calls);
+ } else if (index < segmentCount){
+ segment = getUriSegment(uri, index);
+ // Notify any observers at this level who are interested in descendents
+ collectMyObservers(uri, false, observer, selfNotify, calls);
+ }
+
+ int N = mChildren.size();
+ for (int i = 0; i < N; i++) {
+ ObserverNode node = mChildren.get(i);
+ if (segment == null || node.mName.equals(segment)) {
+ // We found the child,
+ node.collectObservers(uri, index + 1, observer, selfNotify, calls);
+ if (segment != null) {
+ break;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/core/java/android/content/ContentServiceNative.java b/core/java/android/content/ContentServiceNative.java
new file mode 100644
index 0000000..f050501
--- /dev/null
+++ b/core/java/android/content/ContentServiceNative.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+import android.database.IContentObserver;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.RemoteException;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.ServiceManager;
+import android.os.Bundle;
+import android.util.Config;
+import android.util.Log;
+
+/**
+ * {@hide}
+ */
+abstract class ContentServiceNative extends Binder implements IContentService
+{
+ public ContentServiceNative()
+ {
+ attachInterface(this, descriptor);
+ }
+
+ /**
+ * Cast a Binder object into a content resolver interface, generating
+ * a proxy if needed.
+ */
+ static public IContentService asInterface(IBinder obj)
+ {
+ if (obj == null) {
+ return null;
+ }
+ IContentService in =
+ (IContentService)obj.queryLocalInterface(descriptor);
+ if (in != null) {
+ return in;
+ }
+
+ return new ContentServiceProxy(obj);
+ }
+
+ /**
+ * Retrieve the system's default/global content service.
+ */
+ static public IContentService getDefault()
+ {
+ if (gDefault != null) {
+ return gDefault;
+ }
+ IBinder b = ServiceManager.getService("content");
+ if (Config.LOGV) Log.v("ContentService", "default service binder = " + b);
+ gDefault = asInterface(b);
+ if (Config.LOGV) Log.v("ContentService", "default service = " + gDefault);
+ return gDefault;
+ }
+
+ @Override
+ public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
+ {
+ try {
+ switch (code) {
+ case REGISTER_CONTENT_OBSERVER_TRANSACTION: {
+ Uri uri = Uri.CREATOR.createFromParcel(data);
+ boolean notifyForDescendents = data.readInt() != 0;
+ IContentObserver observer = IContentObserver.Stub.asInterface(data.readStrongBinder());
+ registerContentObserver(uri, notifyForDescendents, observer);
+ return true;
+ }
+
+ case UNREGISTER_CHANGE_OBSERVER_TRANSACTION: {
+ IContentObserver observer = IContentObserver.Stub.asInterface(data.readStrongBinder());
+ unregisterContentObserver(observer);
+ return true;
+ }
+
+ case NOTIFY_CHANGE_TRANSACTION: {
+ Uri uri = Uri.CREATOR.createFromParcel(data);
+ IContentObserver observer = IContentObserver.Stub.asInterface(data.readStrongBinder());
+ boolean observerWantsSelfNotifications = data.readInt() != 0;
+ boolean syncToNetwork = data.readInt() != 0;
+ notifyChange(uri, observer, observerWantsSelfNotifications, syncToNetwork);
+ return true;
+ }
+
+ case START_SYNC_TRANSACTION: {
+ Uri url = null;
+ int hasUrl = data.readInt();
+ if (hasUrl != 0) {
+ url = Uri.CREATOR.createFromParcel(data);
+ }
+ startSync(url, data.readBundle());
+ return true;
+ }
+
+ case CANCEL_SYNC_TRANSACTION: {
+ Uri url = null;
+ int hasUrl = data.readInt();
+ if (hasUrl != 0) {
+ url = Uri.CREATOR.createFromParcel(data);
+ }
+ cancelSync(url);
+ return true;
+ }
+
+ default:
+ return super.onTransact(code, data, reply, flags);
+ }
+ } catch (Exception e) {
+ Log.e("ContentServiceNative", "Caught exception in transact", e);
+ }
+
+ return false;
+ }
+
+ public IBinder asBinder()
+ {
+ return this;
+ }
+
+ private static IContentService gDefault;
+}
+
+
+final class ContentServiceProxy implements IContentService
+{
+ public ContentServiceProxy(IBinder remote)
+ {
+ mRemote = remote;
+ }
+
+ public IBinder asBinder()
+ {
+ return mRemote;
+ }
+
+ public void registerContentObserver(Uri uri, boolean notifyForDescendents,
+ IContentObserver observer) throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ uri.writeToParcel(data, 0);
+ data.writeInt(notifyForDescendents ? 1 : 0);
+ data.writeStrongInterface(observer);
+ mRemote.transact(REGISTER_CONTENT_OBSERVER_TRANSACTION, data, null, 0);
+ data.recycle();
+ }
+
+ public void unregisterContentObserver(IContentObserver observer) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ data.writeStrongInterface(observer);
+ mRemote.transact(UNREGISTER_CHANGE_OBSERVER_TRANSACTION, data, null, 0);
+ data.recycle();
+ }
+
+ public void notifyChange(Uri uri, IContentObserver observer,
+ boolean observerWantsSelfNotifications, boolean syncToNetwork)
+ throws RemoteException {
+ Parcel data = Parcel.obtain();
+ uri.writeToParcel(data, 0);
+ data.writeStrongInterface(observer);
+ data.writeInt(observerWantsSelfNotifications ? 1 : 0);
+ data.writeInt(syncToNetwork ? 1 : 0);
+ mRemote.transact(NOTIFY_CHANGE_TRANSACTION, data, null, IBinder.FLAG_ONEWAY);
+ data.recycle();
+ }
+
+ public void startSync(Uri url, Bundle extras) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ if (url == null) {
+ data.writeInt(0);
+ } else {
+ data.writeInt(1);
+ url.writeToParcel(data, 0);
+ }
+ extras.writeToParcel(data, 0);
+ mRemote.transact(START_SYNC_TRANSACTION, data, null, IBinder.FLAG_ONEWAY);
+ data.recycle();
+ }
+
+ public void cancelSync(Uri url) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ if (url == null) {
+ data.writeInt(0);
+ } else {
+ data.writeInt(1);
+ url.writeToParcel(data, 0);
+ }
+ mRemote.transact(CANCEL_SYNC_TRANSACTION, data, null /* reply */, IBinder.FLAG_ONEWAY);
+ data.recycle();
+ }
+
+ private IBinder mRemote;
+}
+
diff --git a/core/java/android/content/ContentUris.java b/core/java/android/content/ContentUris.java
new file mode 100644
index 0000000..aa76034
--- /dev/null
+++ b/core/java/android/content/ContentUris.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+import android.net.Uri;
+
+/**
+ * Utility methods useful for working with content {@link android.net.Uri}s,
+ * those with a "content" scheme.
+ */
+public class ContentUris {
+
+ /**
+ * Converts the last path segment to a long.
+ *
+ * <p>This supports a common convention for content URIs where an ID is
+ * stored in the last segment.
+ *
+ * @throws UnsupportedOperationException if this isn't a hierarchical URI
+ * @throws NumberFormatException if the last segment isn't a number
+ *
+ * @return the long conversion of the last segment or -1 if the path is
+ * empty
+ */
+ public static long parseId(Uri contentUri) {
+ String last = contentUri.getLastPathSegment();
+ return last == null ? -1 : Long.parseLong(last);
+ }
+
+ /**
+ * Appends the given ID to the end of the path.
+ *
+ * @param builder to append the ID to
+ * @param id to append
+ *
+ * @return the given builder
+ */
+ public static Uri.Builder appendId(Uri.Builder builder, long id) {
+ return builder.appendEncodedPath(String.valueOf(id));
+ }
+
+ /**
+ * Appends the given ID to the end of the path.
+ *
+ * @param contentUri to start with
+ * @param id to append
+ *
+ * @return a new URI with the given ID appended to the end of the path
+ */
+ public static Uri withAppendedId(Uri contentUri, long id) {
+ return appendId(contentUri.buildUpon(), id).build();
+ }
+}
diff --git a/core/java/android/content/ContentValues.java b/core/java/android/content/ContentValues.java
new file mode 100644
index 0000000..532cc03
--- /dev/null
+++ b/core/java/android/content/ContentValues.java
@@ -0,0 +1,501 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * This class is used to store a set of values that the {@link ContentResolver}
+ * can process.
+ */
+public final class ContentValues implements Parcelable {
+ public static final String TAG = "ContentValues";
+
+ /** Holds the actual values */
+ private HashMap<String, Object> mValues;
+
+ /**
+ * Creates an empty set of values using the default initial size
+ */
+ public ContentValues() {
+ // Choosing a default size of 8 based on analysis of typical
+ // consumption by applications.
+ mValues = new HashMap<String, Object>(8);
+ }
+
+ /**
+ * Creates an empty set of values using the given initial size
+ *
+ * @param size the initial size of the set of values
+ */
+ public ContentValues(int size) {
+ mValues = new HashMap<String, Object>(size, 1.0f);
+ }
+
+ /**
+ * Creates a set of values copied from the given set
+ *
+ * @param from the values to copy
+ */
+ public ContentValues(ContentValues from) {
+ mValues = new HashMap<String, Object>(from.mValues);
+ }
+
+ /**
+ * Creates a set of values copied from the given HashMap. This is used
+ * by the Parcel unmarshalling code.
+ *
+ * @param from the values to start with
+ * {@hide}
+ */
+ private ContentValues(HashMap<String, Object> values) {
+ mValues = values;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (!(object instanceof ContentValues)) {
+ return false;
+ }
+ return mValues.equals(((ContentValues) object).mValues);
+ }
+
+ @Override
+ public int hashCode() {
+ return mValues.hashCode();
+ }
+
+ /**
+ * Adds a value to the set.
+ *
+ * @param key the name of the value to put
+ * @param value the data for the value to put
+ */
+ public void put(String key, String value) {
+ mValues.put(key, value);
+ }
+
+ /**
+ * Adds all values from the passed in ContentValues.
+ *
+ * @param other the ContentValues from which to copy
+ */
+ public void putAll(ContentValues other) {
+ mValues.putAll(other.mValues);
+ }
+
+ /**
+ * Adds a value to the set.
+ *
+ * @param key the name of the value to put
+ * @param value the data for the value to put
+ */
+ public void put(String key, Byte value) {
+ mValues.put(key, value);
+ }
+
+ /**
+ * Adds a value to the set.
+ *
+ * @param key the name of the value to put
+ * @param value the data for the value to put
+ */
+ public void put(String key, Short value) {
+ mValues.put(key, value);
+ }
+
+ /**
+ * Adds a value to the set.
+ *
+ * @param key the name of the value to put
+ * @param value the data for the value to put
+ */
+ public void put(String key, Integer value) {
+ mValues.put(key, value);
+ }
+
+ /**
+ * Adds a value to the set.
+ *
+ * @param key the name of the value to put
+ * @param value the data for the value to put
+ */
+ public void put(String key, Long value) {
+ mValues.put(key, value);
+ }
+
+ /**
+ * Adds a value to the set.
+ *
+ * @param key the name of the value to put
+ * @param value the data for the value to put
+ */
+ public void put(String key, Float value) {
+ mValues.put(key, value);
+ }
+
+ /**
+ * Adds a value to the set.
+ *
+ * @param key the name of the value to put
+ * @param value the data for the value to put
+ */
+ public void put(String key, Double value) {
+ mValues.put(key, value);
+ }
+
+ /**
+ * Adds a value to the set.
+ *
+ * @param key the name of the value to put
+ * @param value the data for the value to put
+ */
+ public void put(String key, Boolean value) {
+ mValues.put(key, value);
+ }
+
+ /**
+ * Adds a value to the set.
+ *
+ * @param key the name of the value to put
+ * @param value the data for the value to put
+ */
+ public void put(String key, byte[] value) {
+ mValues.put(key, value);
+ }
+
+ /**
+ * Adds a null value to the set.
+ *
+ * @param key the name of the value to make null
+ */
+ public void putNull(String key) {
+ mValues.put(key, null);
+ }
+
+ /**
+ * Returns the number of values.
+ *
+ * @return the number of values
+ */
+ public int size() {
+ return mValues.size();
+ }
+
+ /**
+ * Remove a single value.
+ *
+ * @param key the name of the value to remove
+ */
+ public void remove(String key) {
+ mValues.remove(key);
+ }
+
+ /**
+ * Removes all values.
+ */
+ public void clear() {
+ mValues.clear();
+ }
+
+ /**
+ * Returns true if this object has the named value.
+ *
+ * @param key the value to check for
+ * @return {@code true} if the value is present, {@code false} otherwise
+ */
+ public boolean containsKey(String key) {
+ return mValues.containsKey(key);
+ }
+
+ /**
+ * Gets a value. Valid value types are {@link String}, {@link Boolean}, and
+ * {@link Number} implementations.
+ *
+ * @param key the value to get
+ * @return the data for the value
+ */
+ public Object get(String key) {
+ return mValues.get(key);
+ }
+
+ /**
+ * Gets a value and converts it to a String.
+ *
+ * @param key the value to get
+ * @return the String for the value
+ */
+ public String getAsString(String key) {
+ Object value = mValues.get(key);
+ return value != null ? mValues.get(key).toString() : null;
+ }
+
+ /**
+ * Gets a value and converts it to a Long.
+ *
+ * @param key the value to get
+ * @return the Long value, or null if the value is missing or cannot be converted
+ */
+ public Long getAsLong(String key) {
+ Object value = mValues.get(key);
+ try {
+ return value != null ? ((Number) value).longValue() : null;
+ } catch (ClassCastException e) {
+ if (value instanceof CharSequence) {
+ try {
+ return Long.valueOf(value.toString());
+ } catch (NumberFormatException e2) {
+ Log.e(TAG, "Cannot parse Long value for " + value + " at key " + key);
+ return null;
+ }
+ } else {
+ Log.e(TAG, "Cannot cast value for " + key + " to a Long");
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Gets a value and converts it to an Integer.
+ *
+ * @param key the value to get
+ * @return the Integer value, or null if the value is missing or cannot be converted
+ */
+ public Integer getAsInteger(String key) {
+ Object value = mValues.get(key);
+ try {
+ return value != null ? ((Number) value).intValue() : null;
+ } catch (ClassCastException e) {
+ if (value instanceof CharSequence) {
+ try {
+ return Integer.valueOf(value.toString());
+ } catch (NumberFormatException e2) {
+ Log.e(TAG, "Cannot parse Integer value for " + value + " at key " + key);
+ return null;
+ }
+ } else {
+ Log.e(TAG, "Cannot cast value for " + key + " to a Integer");
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Gets a value and converts it to a Short.
+ *
+ * @param key the value to get
+ * @return the Short value, or null if the value is missing or cannot be converted
+ */
+ public Short getAsShort(String key) {
+ Object value = mValues.get(key);
+ try {
+ return value != null ? ((Number) value).shortValue() : null;
+ } catch (ClassCastException e) {
+ if (value instanceof CharSequence) {
+ try {
+ return Short.valueOf(value.toString());
+ } catch (NumberFormatException e2) {
+ Log.e(TAG, "Cannot parse Short value for " + value + " at key " + key);
+ return null;
+ }
+ } else {
+ Log.e(TAG, "Cannot cast value for " + key + " to a Short");
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Gets a value and converts it to a Byte.
+ *
+ * @param key the value to get
+ * @return the Byte value, or null if the value is missing or cannot be converted
+ */
+ public Byte getAsByte(String key) {
+ Object value = mValues.get(key);
+ try {
+ return value != null ? ((Number) value).byteValue() : null;
+ } catch (ClassCastException e) {
+ if (value instanceof CharSequence) {
+ try {
+ return Byte.valueOf(value.toString());
+ } catch (NumberFormatException e2) {
+ Log.e(TAG, "Cannot parse Byte value for " + value + " at key " + key);
+ return null;
+ }
+ } else {
+ Log.e(TAG, "Cannot cast value for " + key + " to a Byte");
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Gets a value and converts it to a Double.
+ *
+ * @param key the value to get
+ * @return the Double value, or null if the value is missing or cannot be converted
+ */
+ public Double getAsDouble(String key) {
+ Object value = mValues.get(key);
+ try {
+ return value != null ? ((Number) value).doubleValue() : null;
+ } catch (ClassCastException e) {
+ if (value instanceof CharSequence) {
+ try {
+ return Double.valueOf(value.toString());
+ } catch (NumberFormatException e2) {
+ Log.e(TAG, "Cannot parse Double value for " + value + " at key " + key);
+ return null;
+ }
+ } else {
+ Log.e(TAG, "Cannot cast value for " + key + " to a Double");
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Gets a value and converts it to a Float.
+ *
+ * @param key the value to get
+ * @return the Float value, or null if the value is missing or cannot be converted
+ */
+ public Float getAsFloat(String key) {
+ Object value = mValues.get(key);
+ try {
+ return value != null ? ((Number) value).floatValue() : null;
+ } catch (ClassCastException e) {
+ if (value instanceof CharSequence) {
+ try {
+ return Float.valueOf(value.toString());
+ } catch (NumberFormatException e2) {
+ Log.e(TAG, "Cannot parse Float value for " + value + " at key " + key);
+ return null;
+ }
+ } else {
+ Log.e(TAG, "Cannot cast value for " + key + " to a Float");
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Gets a value and converts it to a Boolean.
+ *
+ * @param key the value to get
+ * @return the Boolean value, or null if the value is missing or cannot be converted
+ */
+ public Boolean getAsBoolean(String key) {
+ Object value = mValues.get(key);
+ try {
+ return (Boolean) value;
+ } catch (ClassCastException e) {
+ if (value instanceof CharSequence) {
+ return Boolean.valueOf(value.toString());
+ } else {
+ Log.e(TAG, "Cannot cast value for " + key + " to a Boolean");
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Gets a value that is a byte array. Note that this method will not convert
+ * any other types to byte arrays.
+ *
+ * @param key the value to get
+ * @return the byte[] value, or null is the value is missing or not a byte[]
+ */
+ public byte[] getAsByteArray(String key) {
+ Object value = mValues.get(key);
+ if (value instanceof byte[]) {
+ return (byte[]) value;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Returns a set of all of the keys and values
+ *
+ * @return a set of all of the keys and values
+ */
+ public Set<Map.Entry<String, Object>> valueSet() {
+ return mValues.entrySet();
+ }
+
+ public static final Parcelable.Creator<ContentValues> CREATOR =
+ new Parcelable.Creator<ContentValues>() {
+ @SuppressWarnings({"deprecation", "unchecked"})
+ public ContentValues createFromParcel(Parcel in) {
+ // TODO - what ClassLoader should be passed to readHashMap?
+ HashMap<String, Object> values = in.readHashMap(null);
+ return new ContentValues(values);
+ }
+
+ public ContentValues[] newArray(int size) {
+ return new ContentValues[size];
+ }
+ };
+
+ public int describeContents() {
+ return 0;
+ }
+
+ @SuppressWarnings("deprecation")
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeMap(mValues);
+ }
+
+ /**
+ * Unsupported, here until we get proper bulk insert APIs.
+ * {@hide}
+ */
+ @Deprecated
+ public void putStringArrayList(String key, ArrayList<String> value) {
+ mValues.put(key, value);
+ }
+
+ /**
+ * Unsupported, here until we get proper bulk insert APIs.
+ * {@hide}
+ */
+ @SuppressWarnings("unchecked")
+ @Deprecated
+ public ArrayList<String> getStringArrayList(String key) {
+ return (ArrayList<String>) mValues.get(key);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ for (String name : mValues.keySet()) {
+ String value = getAsString(name);
+ if (sb.length() > 0) sb.append(" ");
+ sb.append(name + "=" + value);
+ }
+ return sb.toString();
+ }
+}
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
new file mode 100644
index 0000000..00a6d31
--- /dev/null
+++ b/core/java/android/content/Context.java
@@ -0,0 +1,1631 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+import android.content.pm.PackageManager;
+import android.content.res.AssetManager;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteDatabase.CursorFactory;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.AttributeSet;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Interface to global information about an application environment. This is
+ * an abstract class whose implementation is provided by
+ * the Android system. It
+ * allows access to application-specific resources and classes, as well as
+ * up-calls for application-level operations such as launching activities,
+ * broadcasting and receiving intents, etc.
+ */
+public abstract class Context {
+ /**
+ * File creation mode: the default mode, where the created file can only
+ * be accessed by the calling application (or all applications sharing the
+ * same user ID).
+ * @see #MODE_WORLD_READABLE
+ * @see #MODE_WORLD_WRITEABLE
+ */
+ public static final int MODE_PRIVATE = 0x0000;
+ /**
+ * File creation mode: allow all other applications to have read access
+ * to the created file.
+ * @see #MODE_PRIVATE
+ * @see #MODE_WORLD_WRITEABLE
+ */
+ public static final int MODE_WORLD_READABLE = 0x0001;
+ /**
+ * File creation mode: allow all other applications to have write access
+ * to the created file.
+ * @see #MODE_PRIVATE
+ * @see #MODE_WORLD_READABLE
+ */
+ public static final int MODE_WORLD_WRITEABLE = 0x0002;
+ /**
+ * File creation mode: for use with {@link #openFileOutput}, if the file
+ * already exists then write data to the end of the existing file
+ * instead of erasing it.
+ * @see #openFileOutput
+ */
+ public static final int MODE_APPEND = 0x8000;
+
+ /**
+ * Flag for {@link #bindService}: automatically create the service as long
+ * as the binding exists. Note that while this will create the service,
+ * its {@link android.app.Service#onStart} method will still only be called due to an
+ * explicit call to {@link #startService}. Even without that, though,
+ * this still provides you with access to the service object while the
+ * service is created.
+ *
+ * <p>Specifying this flag also tells the system to treat the service
+ * as being as important as your own process -- that is, when deciding
+ * which process should be killed to free memory, the service will only
+ * be considered a candidate as long as the processes of any such bindings
+ * is also a candidate to be killed. This is to avoid situations where
+ * the service is being continually created and killed due to low memory.
+ */
+ public static final int BIND_AUTO_CREATE = 0x0001;
+
+ /**
+ * Flag for {@link #bindService}: include debugging help for mismatched
+ * calls to unbind. When this flag is set, the callstack of the following
+ * {@link #unbindService} call is retained, to be printed if a later
+ * incorrect unbind call is made. Note that doing this requires retaining
+ * information about the binding that was made for the lifetime of the app,
+ * resulting in a leak -- this should only be used for debugging.
+ */
+ public static final int BIND_DEBUG_UNBIND = 0x0002;
+
+ /** Return an AssetManager instance for your application's package. */
+ public abstract AssetManager getAssets();
+
+ /** Return a Resources instance for your application's package. */
+ public abstract Resources getResources();
+
+ /** Return PackageManager instance to find global package information. */
+ public abstract PackageManager getPackageManager();
+
+ /** Return a ContentResolver instance for your application's package. */
+ public abstract ContentResolver getContentResolver();
+
+ /**
+ * Return the Looper for the main thread of the current process. This is
+ * the thread used to dispatch calls to application components (activities,
+ * services, etc).
+ */
+ public abstract Looper getMainLooper();
+
+ /**
+ * Return the context of the single, global Application object of the
+ * current process.
+ */
+ public abstract Context getApplicationContext();
+
+ /**
+ * Return a localized, styled CharSequence from the application's package's
+ * default string table.
+ *
+ * @param resId Resource id for the CharSequence text
+ */
+ public final CharSequence getText(int resId) {
+ return getResources().getText(resId);
+ }
+
+ /**
+ * Return a localized string from the application's package's
+ * default string table.
+ *
+ * @param resId Resource id for the string
+ */
+ public final String getString(int resId) {
+ return getResources().getString(resId);
+ }
+
+ /**
+ * Return a localized formatted string from the application's package's
+ * default string table, substituting the format arguments as defined in
+ * {@link java.util.Formatter} and {@link java.lang.String#format}.
+ *
+ * @param resId Resource id for the format string
+ * @param formatArgs The format arguments that will be used for substitution.
+ */
+
+ public final String getString(int resId, Object... formatArgs) {
+ return getResources().getString(resId, formatArgs);
+ }
+
+ /**
+ * Set the base theme for this context. Note that this should be called
+ * before any views are instantiated in the Context (for example before
+ * calling {@link android.app.Activity#setContentView} or
+ * {@link android.view.LayoutInflater#inflate}).
+ *
+ * @param resid The style resource describing the theme.
+ */
+ public abstract void setTheme(int resid);
+
+ /**
+ * Return the Theme object associated with this Context.
+ */
+ public abstract Resources.Theme getTheme();
+
+ /**
+ * Retrieve styled attribute information in this Context's theme. See
+ * {@link Resources.Theme#obtainStyledAttributes(int[])}
+ * for more information.
+ *
+ * @see Resources.Theme#obtainStyledAttributes(int[])
+ */
+ public final TypedArray obtainStyledAttributes(
+ int[] attrs) {
+ return getTheme().obtainStyledAttributes(attrs);
+ }
+
+ /**
+ * Retrieve styled attribute information in this Context's theme. See
+ * {@link Resources.Theme#obtainStyledAttributes(int, int[])}
+ * for more information.
+ *
+ * @see Resources.Theme#obtainStyledAttributes(int, int[])
+ */
+ public final TypedArray obtainStyledAttributes(
+ int resid, int[] attrs) throws Resources.NotFoundException {
+ return getTheme().obtainStyledAttributes(resid, attrs);
+ }
+
+ /**
+ * Retrieve styled attribute information in this Context's theme. See
+ * {@link Resources.Theme#obtainStyledAttributes(AttributeSet, int[], int, int)}
+ * for more information.
+ *
+ * @see Resources.Theme#obtainStyledAttributes(AttributeSet, int[], int, int)
+ */
+ public final TypedArray obtainStyledAttributes(
+ AttributeSet set, int[] attrs) {
+ return getTheme().obtainStyledAttributes(set, attrs, 0, 0);
+ }
+
+ /**
+ * Retrieve styled attribute information in this Context's theme. See
+ * {@link Resources.Theme#obtainStyledAttributes(AttributeSet, int[], int, int)}
+ * for more information.
+ *
+ * @see Resources.Theme#obtainStyledAttributes(AttributeSet, int[], int, int)
+ */
+ public final TypedArray obtainStyledAttributes(
+ AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes) {
+ return getTheme().obtainStyledAttributes(
+ set, attrs, defStyleAttr, defStyleRes);
+ }
+
+ /**
+ * Return a class loader you can use to retrieve classes in this package.
+ */
+ public abstract ClassLoader getClassLoader();
+
+ /** Return the name of this application's package. */
+ public abstract String getPackageName();
+
+ /**
+ * {@hide}
+ * Return the full path to this context's resource files. This is the ZIP files
+ * containing the application's resources.
+ *
+ * <p>Note: this is not generally useful for applications, since they should
+ * not be directly accessing the file system.
+ *
+ *
+ * @return String Path to the resources.
+ */
+ public abstract String getPackageResourcePath();
+
+ /**
+ * {@hide}
+ * Return the full path to this context's code and asset files. This is the ZIP files
+ * containing the application's code and assets.
+ *
+ * <p>Note: this is not generally useful for applications, since they should
+ * not be directly accessing the file system.
+ *
+ *
+ * @return String Path to the code and assets.
+ */
+ public abstract String getPackageCodePath();
+
+ /**
+ * Retrieve and hold the contents of the preferences file 'name', returning
+ * a SharedPreferences through which you can retrieve and modify its
+ * values. Only one instance of the SharedPreferences object is returned
+ * to any callers for the same name, meaning they will see each other's
+ * edits as soon as they are made.
+ *
+ * @param name Desired preferences file. If a preferences file by this name
+ * does not exist, it will be created when you retrieve an
+ * editor (SharedPreferences.edit()) and then commit changes (Editor.commit()).
+ * @param mode Operating mode. Use 0 or {@link #MODE_PRIVATE} for the
+ * default operation, {@link #MODE_WORLD_READABLE}
+ * and {@link #MODE_WORLD_WRITEABLE} to control permissions.
+ *
+ * @return Returns the single SharedPreferences instance that can be used
+ * to retrieve and modify the preference values.
+ *
+ * @see #MODE_PRIVATE
+ * @see #MODE_WORLD_READABLE
+ * @see #MODE_WORLD_WRITEABLE
+ */
+ public abstract SharedPreferences getSharedPreferences(String name,
+ int mode);
+
+ /**
+ * Open a private file associated with this Context's application package
+ * for reading.
+ *
+ * @param name The name of the file to open; can not contain path
+ * separators.
+ *
+ * @return FileInputStream Resulting input stream.
+ *
+ * @see #openFileOutput
+ * @see #fileList
+ * @see #deleteFile
+ * @see java.io.FileInputStream#FileInputStream(String)
+ */
+ public abstract FileInputStream openFileInput(String name)
+ throws FileNotFoundException;
+
+ /**
+ * Open a private file associated with this Context's application package
+ * for writing. Creates the file if it doesn't already exist.
+ *
+ * @param name The name of the file to open; can not contain path
+ * separators.
+ * @param mode Operating mode. Use 0 or {@link #MODE_PRIVATE} for the
+ * default operation, {@link #MODE_APPEND} to append to an existing file,
+ * {@link #MODE_WORLD_READABLE} and {@link #MODE_WORLD_WRITEABLE} to control
+ * permissions.
+ *
+ * @return FileOutputStream Resulting output stream.
+ *
+ * @see #MODE_APPEND
+ * @see #MODE_PRIVATE
+ * @see #MODE_WORLD_READABLE
+ * @see #MODE_WORLD_WRITEABLE
+ * @see #openFileInput
+ * @see #fileList
+ * @see #deleteFile
+ * @see java.io.FileOutputStream#FileOutputStream(String)
+ */
+ public abstract FileOutputStream openFileOutput(String name, int mode)
+ throws FileNotFoundException;
+
+ /**
+ * Delete the given private file associated with this Context's
+ * application package.
+ *
+ * @param name The name of the file to delete; can not contain path
+ * separators.
+ *
+ * @return True if the file was successfully deleted; else
+ * false.
+ *
+ * @see #openFileInput
+ * @see #openFileOutput
+ * @see #fileList
+ * @see java.io.File#delete()
+ */
+ public abstract boolean deleteFile(String name);
+
+ /**
+ * Returns the absolute path on the filesystem where a file created with
+ * {@link #openFileOutput} is stored.
+ *
+ * @param name The name of the file for which you would like to get
+ * its path.
+ *
+ * @return Returns an absolute path to the given file.
+ *
+ * @see #openFileOutput
+ * @see #getFilesDir
+ * @see #getDir
+ */
+ public abstract File getFileStreamPath(String name);
+
+ /**
+ * Returns the absolute path to the directory on the filesystem where
+ * files created with {@link #openFileOutput} are stored.
+ *
+ * @return Returns the path of the directory holding application files.
+ *
+ * @see #openFileOutput
+ * @see #getFileStreamPath
+ * @see #getDir
+ */
+ public abstract File getFilesDir();
+
+ /**
+ * Returns the absolute path to the application specific cache directory
+ * on the filesystem. These files will be ones that get deleted first when the
+ * device runs low on storage
+ * There is no guarantee when these files will be deleted.
+ *
+ * @return Returns the path of the directory holding application cache files.
+ *
+ * @see #openFileOutput
+ * @see #getFileStreamPath
+ * @see #getDir
+ */
+ public abstract File getCacheDir();
+
+ /**
+ * Returns an array of strings naming the private files associated with
+ * this Context's application package.
+ *
+ * @return Array of strings naming the private files.
+ *
+ * @see #openFileInput
+ * @see #openFileOutput
+ * @see #deleteFile
+ */
+ public abstract String[] fileList();
+
+ /**
+ * Retrieve, creating if needed, a new directory in which the application
+ * can place its own custom data files. You can use the returned File
+ * object to create and access files in this directory. Note that files
+ * created through a File object will only be accessible by your own
+ * application; you can only set the mode of the entire directory, not
+ * of individual files.
+ *
+ * @param name Name of the directory to retrieve. This is a directory
+ * that is created as part of your application data.
+ * @param mode Operating mode. Use 0 or {@link #MODE_PRIVATE} for the
+ * default operation, {@link #MODE_WORLD_READABLE} and
+ * {@link #MODE_WORLD_WRITEABLE} to control permissions.
+ *
+ * @return Returns a File object for the requested directory. The directory
+ * will have been created if it does not already exist.
+ *
+ * @see #openFileOutput(String, int)
+ */
+ public abstract File getDir(String name, int mode);
+
+ /**
+ * Open a new private SQLiteDatabase associated with this Context's
+ * application package. Create the database file if it doesn't exist.
+ *
+ * @param name The name (unique in the application package) of the database.
+ * @param mode Operating mode. Use 0 or {@link #MODE_PRIVATE} for the
+ * default operation, {@link #MODE_WORLD_READABLE}
+ * and {@link #MODE_WORLD_WRITEABLE} to control permissions.
+ * @param factory An optional factory class that is called to instantiate a
+ * cursor when query is called.
+ *
+ * @return The contents of a newly created database with the given name.
+ * @throws SQLiteException if the database file could not be opened.
+ *
+ * @see #MODE_PRIVATE
+ * @see #MODE_WORLD_READABLE
+ * @see #MODE_WORLD_WRITEABLE
+ * @see #deleteDatabase
+ */
+ public abstract SQLiteDatabase openOrCreateDatabase(String name,
+ int mode, CursorFactory factory);
+
+ /**
+ * Delete an existing private SQLiteDatabase associated with this Context's
+ * application package.
+ *
+ * @param name The name (unique in the application package) of the
+ * database.
+ *
+ * @return True if the database was successfully deleted; else false.
+ *
+ * @see #openOrCreateDatabase
+ */
+ public abstract boolean deleteDatabase(String name);
+
+ /**
+ * Returns the absolute path on the filesystem where a database created with
+ * {@link #openOrCreateDatabase} is stored.
+ *
+ * @param name The name of the database for which you would like to get
+ * its path.
+ *
+ * @return Returns an absolute path to the given database.
+ *
+ * @see #openOrCreateDatabase
+ */
+ public abstract File getDatabasePath(String name);
+
+ /**
+ * Returns an array of strings naming the private databases associated with
+ * this Context's application package.
+ *
+ * @return Array of strings naming the private databases.
+ *
+ * @see #openOrCreateDatabase
+ * @see #deleteDatabase
+ */
+ public abstract String[] databaseList();
+
+ /**
+ * Like {@link #peekWallpaper}, but always returns a valid Drawable. If
+ * no wallpaper is set, the system default wallpaper is returned.
+ *
+ * @return Returns a Drawable object that will draw the wallpaper.
+ */
+ public abstract Drawable getWallpaper();
+
+ /**
+ * Retrieve the current system wallpaper. This is returned as an
+ * abstract Drawable that you can install in a View to display whatever
+ * wallpaper the user has currently set. If there is no wallpaper set,
+ * a null pointer is returned.
+ *
+ * @return Returns a Drawable object that will draw the wallpaper or a
+ * null pointer if these is none.
+ */
+ public abstract Drawable peekWallpaper();
+
+ /**
+ * Returns the desired minimum width for the wallpaper. Callers of
+ * {@link #setWallpaper(android.graphics.Bitmap)} or
+ * {@link #setWallpaper(java.io.InputStream)} should check this value
+ * beforehand to make sure the supplied wallpaper respects the desired
+ * minimum width.
+ *
+ * If the returned value is <= 0, the caller should use the width of
+ * the default display instead.
+ *
+ * @return The desired minimum width for the wallpaper. This value should
+ * be honored by applications that set the wallpaper but it is not
+ * mandatory.
+ */
+ public abstract int getWallpaperDesiredMinimumWidth();
+
+ /**
+ * Returns the desired minimum height for the wallpaper. Callers of
+ * {@link #setWallpaper(android.graphics.Bitmap)} or
+ * {@link #setWallpaper(java.io.InputStream)} should check this value
+ * beforehand to make sure the supplied wallpaper respects the desired
+ * minimum height.
+ *
+ * If the returned value is <= 0, the caller should use the height of
+ * the default display instead.
+ *
+ * @return The desired minimum height for the wallpaper. This value should
+ * be honored by applications that set the wallpaper but it is not
+ * mandatory.
+ */
+ public abstract int getWallpaperDesiredMinimumHeight();
+
+ /**
+ * Change the current system wallpaper to a bitmap. The given bitmap is
+ * converted to a PNG and stored as the wallpaper. On success, the intent
+ * {@link Intent#ACTION_WALLPAPER_CHANGED} is broadcast.
+ *
+ * @param bitmap The bitmap to save.
+ *
+ * @throws IOException If an error occurs reverting to the default
+ * wallpaper.
+ */
+ public abstract void setWallpaper(Bitmap bitmap) throws IOException;
+
+ /**
+ * Change the current system wallpaper to a specific byte stream. The
+ * give InputStream is copied into persistent storage and will now be
+ * used as the wallpaper. Currently it must be either a JPEG or PNG
+ * image. On success, the intent {@link Intent#ACTION_WALLPAPER_CHANGED}
+ * is broadcast.
+ *
+ * @param data A stream containing the raw data to install as a wallpaper.
+ *
+ * @throws IOException If an error occurs reverting to the default
+ * wallpaper.
+ */
+ public abstract void setWallpaper(InputStream data) throws IOException;
+
+ /**
+ * Remove any currently set wallpaper, reverting to the system's default
+ * wallpaper. On success, the intent {@link Intent#ACTION_WALLPAPER_CHANGED}
+ * is broadcast.
+ *
+ * @throws IOException If an error occurs reverting to the default
+ * wallpaper.
+ */
+ public abstract void clearWallpaper() throws IOException;
+
+ /**
+ * Launch a new activity. You will not receive any information about when
+ * the activity exits.
+ *
+ * <p>Note that if this method is being called from outside of an
+ * {@link android.app.Activity} Context, then the Intent must include
+ * the {@link Intent#FLAG_ACTIVITY_NEW_TASK} launch flag. This is because,
+ * without being started from an existing Activity, there is no existing
+ * task in which to place the new activity and thus it needs to be placed
+ * in its own separate task.
+ *
+ * <p>This method throws {@link ActivityNotFoundException}
+ * if there was no Activity found to run the given Intent.
+ *
+ * @param intent The description of the activity to start.
+ *
+ * @throws ActivityNotFoundException
+ *
+ * @see PackageManager#resolveActivity
+ */
+ public abstract void startActivity(Intent intent);
+
+ /**
+ * Broadcast the given intent to all interested BroadcastReceivers. This
+ * call is asynchronous; it returns immediately, and you will continue
+ * executing while the receivers are run. No results are propagated from
+ * receivers and receivers can not abort the broadcast. If you want
+ * to allow receivers to propagate results or abort the broadcast, you must
+ * send an ordered broadcast using
+ * {@link #sendOrderedBroadcast(Intent, String)}.
+ *
+ * <p>See {@link BroadcastReceiver} for more information on Intent broadcasts.
+ *
+ * @param intent The Intent to broadcast; all receivers matching this
+ * Intent will receive the broadcast.
+ *
+ * @see android.content.BroadcastReceiver
+ * @see #registerReceiver
+ * @see #sendBroadcast(Intent, String)
+ * @see #sendOrderedBroadcast(Intent, String)
+ * @see #sendOrderedBroadcast(Intent, String, BroadcastReceiver, Handler, int, String, Bundle)
+ */
+ public abstract void sendBroadcast(Intent intent);
+
+ /**
+ * Broadcast the given intent to all interested BroadcastReceivers, allowing
+ * an optional required permission to be enforced. This
+ * call is asynchronous; it returns immediately, and you will continue
+ * executing while the receivers are run. No results are propagated from
+ * receivers and receivers can not abort the broadcast. If you want
+ * to allow receivers to propagate results or abort the broadcast, you must
+ * send an ordered broadcast using
+ * {@link #sendOrderedBroadcast(Intent, String)}.
+ *
+ * <p>See {@link BroadcastReceiver} for more information on Intent broadcasts.
+ *
+ * @param intent The Intent to broadcast; all receivers matching this
+ * Intent will receive the broadcast.
+ * @param receiverPermission (optional) String naming a permissions that
+ * a receiver must hold in order to receive your broadcast.
+ * If null, no permission is required.
+ *
+ * @see android.content.BroadcastReceiver
+ * @see #registerReceiver
+ * @see #sendBroadcast(Intent)
+ * @see #sendOrderedBroadcast(Intent, String)
+ * @see #sendOrderedBroadcast(Intent, String, BroadcastReceiver, Handler, int, String, Bundle)
+ */
+ public abstract void sendBroadcast(Intent intent,
+ String receiverPermission);
+
+ /**
+ * Broadcast the given intent to all interested BroadcastReceivers, delivering
+ * them one at a time to allow more preferred receivers to consume the
+ * broadcast before it is delivered to less preferred receivers. This
+ * call is asynchronous; it returns immediately, and you will continue
+ * executing while the receivers are run.
+ *
+ * <p>See {@link BroadcastReceiver} for more information on Intent broadcasts.
+ *
+ * @param intent The Intent to broadcast; all receivers matching this
+ * Intent will receive the broadcast.
+ * @param receiverPermission (optional) String naming a permissions that
+ * a receiver must hold in order to receive your broadcast.
+ * If null, no permission is required.
+ *
+ * @see android.content.BroadcastReceiver
+ * @see #registerReceiver
+ * @see #sendBroadcast(Intent)
+ * @see #sendOrderedBroadcast(Intent, String, BroadcastReceiver, Handler, int, String, Bundle)
+ */
+ public abstract void sendOrderedBroadcast(Intent intent,
+ String receiverPermission);
+
+ /**
+ * Version of {@link #sendBroadcast(Intent)} that allows you to
+ * receive data back from the broadcast. This is accomplished by
+ * supplying your own BroadcastReceiver when calling, which will be
+ * treated as a final receiver at the end of the broadcast -- its
+ * {@link BroadcastReceiver#onReceive} method will be called with
+ * the result values collected from the other receivers. If you use
+ * an <var>resultReceiver</var> with this method, then the broadcast will
+ * be serialized in the same way as calling
+ * {@link #sendOrderedBroadcast(Intent, String)}.
+ *
+ * <p>Like {@link #sendBroadcast(Intent)}, this method is
+ * asynchronous; it will return before
+ * resultReceiver.onReceive() is called.
+ *
+ * <p>See {@link BroadcastReceiver} for more information on Intent broadcasts.
+ *
+ * @param intent The Intent to broadcast; all receivers matching this
+ * Intent will receive the broadcast.
+ * @param receiverPermission String naming a permissions that
+ * a receiver must hold in order to receive your broadcast.
+ * If null, no permission is required.
+ * @param resultReceiver Your own BroadcastReceiver to treat as the final
+ * receiver of the broadcast.
+ * @param scheduler A custom Handler with which to schedule the
+ * resultReceiver callback; if null it will be
+ * scheduled in the Context's main thread.
+ * @param initialCode An initial value for the result code. Often
+ * Activity.RESULT_OK.
+ * @param initialData An initial value for the result data. Often
+ * null.
+ * @param initialExtras An initial value for the result extras. Often
+ * null.
+ *
+ * @see #sendBroadcast(Intent)
+ * @see #sendBroadcast(Intent, String)
+ * @see #sendOrderedBroadcast(Intent, String)
+ * @see #sendStickyBroadcast(Intent)
+ * @see android.content.BroadcastReceiver
+ * @see #registerReceiver
+ * @see android.app.Activity#RESULT_OK
+ */
+ public abstract void sendOrderedBroadcast(Intent intent,
+ String receiverPermission, BroadcastReceiver resultReceiver,
+ Handler scheduler, int initialCode, String initialData,
+ Bundle initialExtras);
+
+ /**
+ * Perform a {@link #sendBroadcast(Intent)} that is "sticky," meaning the
+ * Intent you are sending stays around after the broadcast is complete,
+ * so that others can quickly retrieve that data through the return
+ * value of {@link #registerReceiver(BroadcastReceiver, IntentFilter)}. In
+ * all other ways, this behaves the same as
+ * {@link #sendBroadcast(Intent)}.
+ *
+ * <p>You must hold the {@link android.Manifest.permission#BROADCAST_STICKY}
+ * permission in order to use this API. If you do not hold that
+ * permission, {@link SecurityException} will be thrown.
+ *
+ * @param intent The Intent to broadcast; all receivers matching this
+ * Intent will receive the broadcast, and the Intent will be held to
+ * be re-broadcast to future receivers.
+ *
+ * @see #sendBroadcast(Intent)
+ */
+ public abstract void sendStickyBroadcast(Intent intent);
+
+ /**
+ * Remove the data previously sent with {@link #sendStickyBroadcast},
+ * so that it is as if the sticky broadcast had never happened.
+ *
+ * <p>You must hold the {@link android.Manifest.permission#BROADCAST_STICKY}
+ * permission in order to use this API. If you do not hold that
+ * permission, {@link SecurityException} will be thrown.
+ *
+ * @param intent The Intent that was previously broadcast.
+ *
+ * @see #sendStickyBroadcast
+ */
+ public abstract void removeStickyBroadcast(Intent intent);
+
+ /**
+ * Register an BroadcastReceiver to be run in the main activity thread. The
+ * <var>receiver</var> will be called with any broadcast Intent that
+ * matches <var>filter</var>, in the main application thread.
+ *
+ * <p>The system may broadcast Intents that are "sticky" -- these stay
+ * around after the broadcast as finished, to be sent to any later
+ * registrations. If your IntentFilter matches one of these sticky
+ * Intents, that Intent will be returned by this function
+ * <strong>and</strong> sent to your <var>receiver</var> as if it had just
+ * been broadcast.
+ *
+ * <p>There may be multiple sticky Intents that match <var>filter</var>,
+ * in which case each of these will be sent to <var>receiver</var>. In
+ * this case, only one of these can be returned directly by the function;
+ * which of these that is returned is arbitrarily decided by the system.
+ *
+ * <p>If you know the Intent your are registering for is sticky, you can
+ * supply null for your <var>receiver</var>. In this case, no receiver is
+ * registered -- the function simply returns the sticky Intent that
+ * matches <var>filter</var>. In the case of multiple matches, the same
+ * rules as described above apply.
+ *
+ * <p>See {@link BroadcastReceiver} for more information on Intent broadcasts.
+ *
+ * <p class="note">Note: this method <em>can not be called from an
+ * {@link BroadcastReceiver} component</em>. It is okay, however, to use
+ * this method from another BroadcastReceiver that has itself been registered with
+ * {@link #registerReceiver}, since the lifetime of such an BroadcastReceiver
+ * is tied to another object (the one that registered it).</p>
+ *
+ * @param receiver The BroadcastReceiver to handle the broadcast.
+ * @param filter Selects the Intent broadcasts to be received.
+ *
+ * @return The first sticky intent found that matches <var>filter</var>,
+ * or null if there are none.
+ *
+ * @see #registerReceiver(BroadcastReceiver, IntentFilter, String, Handler)
+ * @see #sendBroadcast
+ * @see #unregisterReceiver
+ */
+ public abstract Intent registerReceiver(BroadcastReceiver receiver,
+ IntentFilter filter);
+
+ /**
+ * Register to receive intent broadcasts, to run in the context of
+ * <var>scheduler</var>. See
+ * {@link #registerReceiver(BroadcastReceiver, IntentFilter)} for more
+ * information. This allows you to enforce permissions on who can
+ * broadcast intents to your receiver, or have the receiver run in
+ * a different thread than the main application thread.
+ *
+ * <p>See {@link BroadcastReceiver} for more information on Intent broadcasts.
+ *
+ * @param receiver The BroadcastReceiver to handle the broadcast.
+ * @param filter Selects the Intent broadcasts to be received.
+ * @param broadcastPermission String naming a permissions that a
+ * broadcaster must hold in order to send an Intent to you. If null,
+ * no permission is required.
+ * @param scheduler Handler identifying the thread that will receive
+ * the Intent. If null, the main thread of the process will be used.
+ *
+ * @return The first sticky intent found that matches <var>filter</var>,
+ * or null if there are none.
+ *
+ * @see #registerReceiver(BroadcastReceiver, IntentFilter)
+ * @see #sendBroadcast
+ * @see #unregisterReceiver
+ */
+ public abstract Intent registerReceiver(BroadcastReceiver receiver,
+ IntentFilter filter,
+ String broadcastPermission,
+ Handler scheduler);
+
+ /**
+ * Unregister a previously registered BroadcastReceiver. <em>All</em>
+ * filters that have been registered for this BroadcastReceiver will be
+ * removed.
+ *
+ * @param receiver The BroadcastReceiver to unregister.
+ *
+ * @see #registerReceiver
+ */
+ public abstract void unregisterReceiver(BroadcastReceiver receiver);
+
+ /**
+ * Request that a given application service be started. The Intent
+ * can either contain the complete class name of a specific service
+ * implementation to start, or an abstract definition through the
+ * action and other fields of the kind of service to start. If this service
+ * is not already running, it will be instantiated and started (creating a
+ * process for it if needed); if it is running then it remains running.
+ *
+ * <p>Every call to this method will result in a corresponding call to
+ * the target service's {@link android.app.Service#onStart} method,
+ * with the <var>intent</var> given here. This provides a convenient way
+ * to submit jobs to a service without having to bind and call on to its
+ * interface.
+ *
+ * <p>Using startService() overrides the default service lifetime that is
+ * managed by {@link #bindService}: it requires the service to remain
+ * running until {@link #stopService} is called, regardless of whether
+ * any clients are connected to it. Note that calls to startService()
+ * are not nesting: no matter how many times you call startService(),
+ * a single call to {@link #stopService} will stop it.
+ *
+ * <p>The system attempts to keep running services around as much as
+ * possible. The only time they should be stopped is if the current
+ * foreground application is using so many resources that the service needs
+ * to be killed. If any errors happen in the service's process, it will
+ * automatically be restarted.
+ *
+ * <p>This function will throw {@link SecurityException} if you do not
+ * have permission to start the given service.
+ *
+ * @param service Identifies the service to be started. The Intent may
+ * specify either an explicit component name to start, or a logical
+ * description (action, category, etc) to match an
+ * {@link IntentFilter} published by a service. Additional values
+ * may be included in the Intent extras to supply arguments along with
+ * this specific start call.
+ *
+ * @return If the service is being started or is already running, the
+ * {@link ComponentName} of the actual service that was started is
+ * returned; else if the service does not exist null is returned.
+ *
+ * @throws SecurityException
+ *
+ * @see #stopService
+ * @see #bindService
+ */
+ public abstract ComponentName startService(Intent service);
+
+ /**
+ * Request that a given application service be stopped. If the service is
+ * not running, nothing happens. Otherwise it is stopped. Note that calls
+ * to startService() are not counted -- this stops the service no matter
+ * how many times it was started.
+ *
+ * <p>Note that if a stopped service still has {@link ServiceConnection}
+ * objects bound to it with the {@link #BIND_AUTO_CREATE} set, it will
+ * not be destroyed until all of these bindings are removed. See
+ * the {@link android.app.Service} documentation for more details on a
+ * service's lifecycle.
+ *
+ * <p>This function will throw {@link SecurityException} if you do not
+ * have permission to stop the given service.
+ *
+ * @param service Description of the service to be stopped. The Intent may
+ * specify either an explicit component name to start, or a logical
+ * description (action, category, etc) to match an
+ * {@link IntentFilter} published by a service.
+ *
+ * @return If there is a service matching the given Intent that is already
+ * running, then it is stopped and true is returned; else false is returned.
+ *
+ * @throws SecurityException
+ *
+ * @see #startService
+ */
+ public abstract boolean stopService(Intent service);
+
+ /**
+ * Connect to an application service, creating it if needed. This defines
+ * a dependency between your application and the service. The given
+ * <var>conn</var> will receive the service object when its created and be
+ * told if it dies and restarts. The service will be considered required
+ * by the system only for as long as the calling context exists. For
+ * example, if this Context is an Activity that is stopped, the service will
+ * not be required to continue running until the Activity is resumed.
+ *
+ * <p>This function will throw {@link SecurityException} if you do not
+ * have permission to bind to the given service.
+ *
+ * <p class="note">Note: this method <em>can not be called from an
+ * {@link BroadcastReceiver} component</em>. A pattern you can use to
+ * communicate from an BroadcastReceiver to a Service is to call
+ * {@link #startService} with the arguments containing the command to be
+ * sent, with the service calling its
+ * {@link android.app.Service#stopSelf(int)} method when done executing
+ * that command. See the API demo App/Service/Service Start Arguments
+ * Controller for an illustration of this. It is okay, however, to use
+ * this method from an BroadcastReceiver that has been registered with
+ * {@link #registerReceiver}, since the lifetime of this BroadcastReceiver
+ * is tied to another object (the one that registered it).</p>
+ *
+ * @param service Identifies the service to connect to. The Intent may
+ * specify either an explicit component name, or a logical
+ * description (action, category, etc) to match an
+ * {@link IntentFilter} published by a service.
+ * @param conn Receives information as the service is started and stopped.
+ * @param flags Operation options for the binding. May be 0 or
+ * {@link #BIND_AUTO_CREATE}.
+ * @return If you have successfully bound to the service, true is returned;
+ * false is returned if the connection is not made so you will not
+ * receive the service object.
+ *
+ * @throws SecurityException
+ *
+ * @see #unbindService
+ * @see #startService
+ * @see #BIND_AUTO_CREATE
+ */
+ public abstract boolean bindService(Intent service, ServiceConnection conn,
+ int flags);
+
+ /**
+ * Disconnect from an application service. You will no longer receive
+ * calls as the service is restarted, and the service is now allowed to
+ * stop at any time.
+ *
+ * @param conn The connection interface previously supplied to
+ * bindService().
+ *
+ * @see #bindService
+ */
+ public abstract void unbindService(ServiceConnection conn);
+
+ /**
+ * Start executing an {@link android.app.Instrumentation} class. The given
+ * Instrumentation component will be run by killing its target application
+ * (if currently running), starting the target process, instantiating the
+ * instrumentation component, and then letting it drive the application.
+ *
+ * <p>This function is not synchronous -- it returns as soon as the
+ * instrumentation has started and while it is running.
+ *
+ * <p>Instrumentation is normally only allowed to run against a package
+ * that is either unsigned or signed with a signature that the
+ * the instrumentation package is also signed with (ensuring the target
+ * trusts the instrumentation).
+ *
+ * @param className Name of the Instrumentation component to be run.
+ * @param profileFile Optional path to write profiling data as the
+ * instrumentation runs, or null for no profiling.
+ * @param arguments Additional optional arguments to pass to the
+ * instrumentation, or null.
+ *
+ * @return Returns true if the instrumentation was successfully started,
+ * else false if it could not be found.
+ */
+ public abstract boolean startInstrumentation(ComponentName className,
+ String profileFile, Bundle arguments);
+
+ /**
+ * Return the handle to a system-level service by name. The class of the
+ * returned object varies by the requested name. Currently available names
+ * are:
+ *
+ * <dl>
+ * <dt> {@link #WINDOW_SERVICE} ("window")
+ * <dd> The top-level window manager in which you can place custom
+ * windows. The returned object is a {@link android.view.WindowManager}.
+ * <dt> {@link #LAYOUT_INFLATER_SERVICE} ("layout_inflater")
+ * <dd> A {@link android.view.LayoutInflater} for inflating layout resources
+ * in this context.
+ * <dt> {@link #ACTIVITY_SERVICE} ("activity")
+ * <dd> A {@link android.app.ActivityManager} for interacting with the
+ * global activity state of the system.
+ * <dt> {@link #POWER_SERVICE} ("power")
+ * <dd> A {@link android.os.PowerManager} for controlling power
+ * management.
+ * <dt> {@link #ALARM_SERVICE} ("alarm")
+ * <dd> A {@link android.app.AlarmManager} for receiving intents at the
+ * time of your choosing.
+ * <dt> {@link #NOTIFICATION_SERVICE} ("notification")
+ * <dd> A {@link android.app.NotificationManager} for informing the user
+ * of background events.
+ * <dt> {@link #KEYGUARD_SERVICE} ("keyguard")
+ * <dd> A {@link android.app.KeyguardManager} for controlling keyguard.
+ * <dt> {@link #LOCATION_SERVICE} ("location")
+ * <dd> A {@link android.location.LocationManager} for controlling location
+ * (e.g., GPS) updates.
+ * <dt> {@link #SEARCH_SERVICE} ("search")
+ * <dd> A {@link android.app.SearchManager} for handling search.
+ * <dt> {@link #VIBRATOR_SERVICE} ("vibrator")
+ * <dd> A {@link android.os.Vibrator} for interacting with the vibrator
+ * hardware.
+ * <dt> {@link #CONNECTIVITY_SERVICE} ("connection")
+ * <dd> A {@link android.net.ConnectivityManager ConnectivityManager} for
+ * handling management of network connections.
+ * <dt> {@link #WIFI_SERVICE} ("wifi")
+ * <dd> A {@link android.net.wifi.WifiManager WifiManager} for management of
+ * Wi-Fi connectivity.
+ * </dl>
+ *
+ * <p>Note: System services obtained via this API may be closely associated with
+ * the Context in which they are obtained from. In general, do not share the
+ * service objects between various different contexts (Activities, Applications,
+ * Services, Providers, etc.)
+ *
+ * @param name The name of the desired service.
+ *
+ * @return The service or null if the name does not exist.
+ *
+ * @see #WINDOW_SERVICE
+ * @see android.view.WindowManager
+ * @see #LAYOUT_INFLATER_SERVICE
+ * @see android.view.LayoutInflater
+ * @see #ACTIVITY_SERVICE
+ * @see android.app.ActivityManager
+ * @see #POWER_SERVICE
+ * @see android.os.PowerManager
+ * @see #ALARM_SERVICE
+ * @see android.app.AlarmManager
+ * @see #NOTIFICATION_SERVICE
+ * @see android.app.NotificationManager
+ * @see #KEYGUARD_SERVICE
+ * @see android.app.KeyguardManager
+ * @see #LOCATION_SERVICE
+ * @see android.location.LocationManager
+ * @see #SEARCH_SERVICE
+ * @see android.app.SearchManager
+ * @see #SENSOR_SERVICE
+ * @see android.hardware.SensorManager
+ * @see #VIBRATOR_SERVICE
+ * @see android.os.Vibrator
+ * @see #CONNECTIVITY_SERVICE
+ * @see android.net.ConnectivityManager
+ * @see #WIFI_SERVICE
+ * @see android.net.wifi.WifiManager
+ * @see #AUDIO_SERVICE
+ * @see android.media.AudioManager
+ * @see #TELEPHONY_SERVICE
+ * @see android.internal.TelephonyManager
+ */
+ public abstract Object getSystemService(String name);
+
+ /**
+ * Use with {@link #getSystemService} to retrieve a
+ * {@link android.os.PowerManager} for controlling power management,
+ * including "wake locks," which let you keep the device on while
+ * you're running long tasks.
+ */
+ public static final String POWER_SERVICE = "power";
+ /**
+ * Use with {@link #getSystemService} to retrieve a
+ * {@link android.view.WindowManager} for accessing the system's window
+ * manager.
+ *
+ * @see #getSystemService
+ * @see android.view.WindowManager
+ */
+ public static final String WINDOW_SERVICE = "window";
+ /**
+ * Use with {@link #getSystemService} to retrieve a
+ * {@link android.view.LayoutInflater} for inflating layout resources in this
+ * context.
+ *
+ * @see #getSystemService
+ * @see android.view.LayoutInflater
+ */
+ public static final String LAYOUT_INFLATER_SERVICE = "layout_inflater";
+ /**
+ * Use with {@link #getSystemService} to retrieve a
+ * {@link android.app.ActivityManager} for interacting with the global
+ * system state.
+ *
+ * @see #getSystemService
+ * @see android.app.ActivityManager
+ */
+ public static final String ACTIVITY_SERVICE = "activity";
+ /**
+ * Use with {@link #getSystemService} to retrieve a
+ * {@link android.app.AlarmManager} for receiving intents at a
+ * time of your choosing.
+ *
+ * @see #getSystemService
+ * @see android.app.AlarmManager
+ */
+ public static final String ALARM_SERVICE = "alarm";
+ /**
+ * Use with {@link #getSystemService} to retrieve a
+ * {@link android.app.NotificationManager} for informing the user of
+ * background events.
+ *
+ * @see #getSystemService
+ * @see android.app.NotificationManager
+ */
+ public static final String NOTIFICATION_SERVICE = "notification";
+ /**
+ * Use with {@link #getSystemService} to retrieve a
+ * {@link android.app.NotificationManager} for controlling keyguard.
+ *
+ * @see #getSystemService
+ * @see android.app.KeyguardManager
+ */
+ public static final String KEYGUARD_SERVICE = "keyguard";
+ /**
+ * Use with {@link #getSystemService} to retrieve a {@link
+ * android.location.LocationManager} for controlling location
+ * updates.
+ *
+ * @see #getSystemService
+ * @see android.location.LocationManager
+ */
+ public static final String LOCATION_SERVICE = "location";
+ /**
+ * Use with {@link #getSystemService} to retrieve a {@link
+ * android.app.SearchManager} for handling searches.
+ *
+ * @see #getSystemService
+ * @see android.app.SearchManager
+ */
+ public static final String SEARCH_SERVICE = "search";
+ /**
+ * Use with {@link #getSystemService} to retrieve a {@link
+ * android.hardware.SensorManager} for accessing sensors.
+ *
+ * @see #getSystemService
+ * @see android.hardware.SensorManager
+ */
+ public static final String SENSOR_SERVICE = "sensor";
+ /**
+ * Use with {@link #getSystemService} to retrieve a {@link
+ * android.bluetooth.BluetoothDevice} for interacting with Bluetooth.
+ *
+ * @see #getSystemService
+ * @see android.bluetooth.BluetoothDevice
+ * @hide
+ */
+ public static final String BLUETOOTH_SERVICE = "bluetooth";
+ /**
+ * Use with {@link #getSystemService} to retrieve a
+ * com.android.server.WallpaperService for accessing wallpapers.
+ *
+ * @see #getSystemService
+ */
+ public static final String WALLPAPER_SERVICE = "wallpaper";
+ /**
+ * Use with {@link #getSystemService} to retrieve a {@link
+ * android.os.Vibrator} for interacting with the vibration hardware.
+ *
+ * @see #getSystemService
+ * @see android.os.Vibrator
+ */
+ public static final String VIBRATOR_SERVICE = "vibrator";
+ /**
+ * Use with {@link #getSystemService} to retrieve a {@link
+ * android.app.StatusBarManager} for interacting with the status bar.
+ *
+ * @see #getSystemService
+ * @see android.app.StatusBarManager
+ * @hide
+ */
+ public static final String STATUS_BAR_SERVICE = "statusbar";
+
+ /**
+ * Use with {@link #getSystemService} to retrieve a {@link
+ * android.net.ConnectivityManager} for handling management of
+ * network connections.
+ *
+ * @see #getSystemService
+ * @see android.net.ConnectivityManager
+ */
+ public static final String CONNECTIVITY_SERVICE = "connectivity";
+
+ /**
+ * Use with {@link #getSystemService} to retrieve a {@link
+ * android.net.wifi.WifiManager} for handling management of
+ * Wi-Fi access.
+ *
+ * @see #getSystemService
+ * @see android.net.wifi.WifiManager
+ */
+ public static final String WIFI_SERVICE = "wifi";
+
+ /**
+ * Use with {@link #getSystemService} to retrieve a
+ * {@link android.media.AudioManager} for handling management of volume,
+ * ringer modes and audio routing.
+ *
+ * @see #getSystemService
+ * @see android.media.AudioManager
+ */
+ public static final String AUDIO_SERVICE = "audio";
+
+ /**
+ * Use with {@link #getSystemService} to retrieve a
+ * {@link android.telephony.TelephonyManager} for handling management the
+ * telephony features of the device.
+ *
+ * @see #getSystemService
+ * @see android.telephony.TelephonyManager
+ */
+ public static final String TELEPHONY_SERVICE = "phone";
+
+ /**
+ * Use with {@link #getSystemService} to retrieve a
+ * {@link android.text.ClipboardManager} for accessing and modifying
+ * the contents of the global clipboard.
+ *
+ * @see #getSystemService
+ * @see android.text.ClipboardManager
+ */
+ public static final String CLIPBOARD_SERVICE = "clipboard";
+
+ /**
+ * Determine whether the given permission is allowed for a particular
+ * process and user ID running in the system.
+ *
+ * @param permission The name of the permission being checked.
+ * @param pid The process ID being checked against. Must be > 0.
+ * @param uid The user ID being checked against. A uid of 0 is the root
+ * user, which will pass every permission check.
+ *
+ * @return Returns {@link PackageManager#PERMISSION_GRANTED} if the given
+ * pid/uid is allowed that permission, or
+ * {@link PackageManager#PERMISSION_DENIED} if it is not.
+ *
+ * @see PackageManager#checkPermission(String, String)
+ * @see #checkCallingPermission
+ */
+ public abstract int checkPermission(String permission, int pid, int uid);
+
+ /**
+ * Determine whether the calling process of an IPC you are handling has been
+ * granted a particular permission. This is basically the same as calling
+ * {@link #checkPermission(String, int, int)} with the pid and uid returned
+ * by {@link android.os.Binder#getCallingPid} and
+ * {@link android.os.Binder#getCallingUid}. One important difference
+ * is that if you are not currently processing an IPC, this function
+ * will always fail. This is done to protect against accidentally
+ * leaking permissions; you can use {@link #checkCallingOrSelfPermission}
+ * to avoid this protection.
+ *
+ * @param permission The name of the permission being checked.
+ *
+ * @return Returns {@link PackageManager#PERMISSION_GRANTED} if the calling
+ * pid/uid is allowed that permission, or
+ * {@link PackageManager#PERMISSION_DENIED} if it is not.
+ *
+ * @see PackageManager#checkPermission(String, String)
+ * @see #checkPermission
+ * @see #checkCallingOrSelfPermission
+ */
+ public abstract int checkCallingPermission(String permission);
+
+ /**
+ * Determine whether the calling process of an IPC <em>or you</em> have been
+ * granted a particular permission. This is the same as
+ * {@link #checkCallingPermission}, except it grants your own permissions
+ * if you are not currently processing an IPC. Use with care!
+ *
+ * @param permission The name of the permission being checked.
+ *
+ * @return Returns {@link PackageManager#PERMISSION_GRANTED} if the calling
+ * pid/uid is allowed that permission, or
+ * {@link PackageManager#PERMISSION_DENIED} if it is not.
+ *
+ * @see PackageManager#checkPermission(String, String)
+ * @see #checkPermission
+ * @see #checkCallingPermission
+ */
+ public abstract int checkCallingOrSelfPermission(String permission);
+
+ /**
+ * If the given permission is not allowed for a particular process
+ * and user ID running in the system, throw a {@link SecurityException}.
+ *
+ * @param permission The name of the permission being checked.
+ * @param pid The process ID being checked against. Must be &gt; 0.
+ * @param uid The user ID being checked against. A uid of 0 is the root
+ * user, which will pass every permission check.
+ * @param message A message to include in the exception if it is thrown.
+ *
+ * @see #checkPermission(String, int, int)
+ */
+ public abstract void enforcePermission(
+ String permission, int pid, int uid, String message);
+
+ /**
+ * If the calling process of an IPC you are handling has not been
+ * granted a particular permission, throw a {@link
+ * SecurityException}. This is basically the same as calling
+ * {@link #enforcePermission(String, int, int, String)} with the
+ * pid and uid returned by {@link android.os.Binder#getCallingPid}
+ * and {@link android.os.Binder#getCallingUid}. One important
+ * difference is that if you are not currently processing an IPC,
+ * this function will always throw the SecurityException. This is
+ * done to protect against accidentally leaking permissions; you
+ * can use {@link #enforceCallingOrSelfPermission} to avoid this
+ * protection.
+ *
+ * @param permission The name of the permission being checked.
+ * @param message A message to include in the exception if it is thrown.
+ *
+ * @see #checkCallingPermission(String)
+ */
+ public abstract void enforceCallingPermission(
+ String permission, String message);
+
+ /**
+ * If neither you nor the calling process of an IPC you are
+ * handling has been granted a particular permission, throw a
+ * {@link SecurityException}. This is the same as {@link
+ * #enforceCallingPermission}, except it grants your own
+ * permissions if you are not currently processing an IPC. Use
+ * with care!
+ *
+ * @param permission The name of the permission being checked.
+ * @param message A message to include in the exception if it is thrown.
+ *
+ * @see #checkCallingOrSelfPermission(String)
+ */
+ public abstract void enforceCallingOrSelfPermission(
+ String permission, String message);
+
+ /**
+ * Grant permission to access a specific Uri to another package, regardless
+ * of whether that package has general permission to access the Uri's
+ * content provider. This can be used to grant specific, temporary
+ * permissions, typically in response to user interaction (such as the
+ * user opening an attachment that you would like someone else to
+ * display).
+ *
+ * <p>Normally you should use {@link Intent#FLAG_GRANT_READ_URI_PERMISSION
+ * Intent.FLAG_GRANT_READ_URI_PERMISSION} or
+ * {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION
+ * Intent.FLAG_GRANT_WRITE_URI_PERMISSION} with the Intent being used to
+ * start an activity instead of this function directly. If you use this
+ * function directly, you should be sure to call
+ * {@link #revokeUriPermission} when the target should no longer be allowed
+ * to access it.
+ *
+ * <p>To succeed, the content provider owning the Uri must have set the
+ * {@link android.R.styleable#AndroidManifestProvider_grantUriPermissions
+ * grantUriPermissions} attribute in its manifest or included the
+ * {@link android.R.styleable#AndroidManifestGrantUriPermission
+ * &lt;grant-uri-permissions&gt;} tag.
+ *
+ * @param toPackage The package you would like to allow to access the Uri.
+ * @param uri The Uri you would like to grant access to.
+ * @param modeFlags The desired access modes. Any combination of
+ * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION
+ * Intent.FLAG_GRANT_READ_URI_PERMISSION} or
+ * {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION
+ * Intent.FLAG_GRANT_WRITE_URI_PERMISSION}.
+ *
+ * @see #revokeUriPermission
+ */
+ public abstract void grantUriPermission(String toPackage, Uri uri,
+ int modeFlags);
+
+ /**
+ * Remove all permissions to access a particular content provider Uri
+ * that were previously added with {@link #grantUriPermission}. The given
+ * Uri will match all previously granted Uris that are the same or a
+ * sub-path of the given Uri. That is, revoking "content://foo/one" will
+ * revoke both "content://foo/target" and "content://foo/target/sub", but not
+ * "content://foo".
+ *
+ * @param uri The Uri you would like to revoke access to.
+ * @param modeFlags The desired access modes. Any combination of
+ * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION
+ * Intent.FLAG_GRANT_READ_URI_PERMISSION} or
+ * {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION
+ * Intent.FLAG_GRANT_WRITE_URI_PERMISSION}.
+ *
+ * @see #grantUriPermission
+ */
+ public abstract void revokeUriPermission(Uri uri, int modeFlags);
+
+ /**
+ * Determine whether a particular process and user ID has been granted
+ * permission to access a specific URI. This only checks for permissions
+ * that have been explicitly granted -- if the given process/uid has
+ * more general access to the URI's content provider then this check will
+ * always fail.
+ *
+ * @param uri The uri that is being checked.
+ * @param pid The process ID being checked against. Must be &gt; 0.
+ * @param uid The user ID being checked against. A uid of 0 is the root
+ * user, which will pass every permission check.
+ * @param modeFlags The type of access to grant. May be one or both of
+ * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION Intent.FLAG_GRANT_READ_URI_PERMISSION} or
+ * {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION Intent.FLAG_GRANT_WRITE_URI_PERMISSION}.
+ *
+ * @return Returns {@link PackageManager#PERMISSION_GRANTED} if the given
+ * pid/uid is allowed to access that uri, or
+ * {@link PackageManager#PERMISSION_DENIED} if it is not.
+ *
+ * @see #checkCallingUriPermission
+ */
+ public abstract int checkUriPermission(Uri uri, int pid, int uid, int modeFlags);
+
+ /**
+ * Determine whether the calling process and user ID has been
+ * granted permission to access a specific URI. This is basically
+ * the same as calling {@link #checkUriPermission(Uri, int, int,
+ * int)} with the pid and uid returned by {@link
+ * android.os.Binder#getCallingPid} and {@link
+ * android.os.Binder#getCallingUid}. One important difference is
+ * that if you are not currently processing an IPC, this function
+ * will always fail.
+ *
+ * @param uri The uri that is being checked.
+ * @param modeFlags The type of access to grant. May be one or both of
+ * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION Intent.FLAG_GRANT_READ_URI_PERMISSION} or
+ * {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION Intent.FLAG_GRANT_WRITE_URI_PERMISSION}.
+ *
+ * @return Returns {@link PackageManager#PERMISSION_GRANTED} if the caller
+ * is allowed to access that uri, or
+ * {@link PackageManager#PERMISSION_DENIED} if it is not.
+ *
+ * @see #checkUriPermission(Uri, int, int, int)
+ */
+ public abstract int checkCallingUriPermission(Uri uri, int modeFlags);
+
+ /**
+ * Determine whether the calling process of an IPC <em>or you</em> has been granted
+ * permission to access a specific URI. This is the same as
+ * {@link #checkCallingUriPermission}, except it grants your own permissions
+ * if you are not currently processing an IPC. Use with care!
+ *
+ * @param uri The uri that is being checked.
+ * @param modeFlags The type of access to grant. May be one or both of
+ * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION Intent.FLAG_GRANT_READ_URI_PERMISSION} or
+ * {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION Intent.FLAG_GRANT_WRITE_URI_PERMISSION}.
+ *
+ * @return Returns {@link PackageManager#PERMISSION_GRANTED} if the caller
+ * is allowed to access that uri, or
+ * {@link PackageManager#PERMISSION_DENIED} if it is not.
+ *
+ * @see #checkCallingUriPermission
+ */
+ public abstract int checkCallingOrSelfUriPermission(Uri uri, int modeFlags);
+
+ /**
+ * Check both a Uri and normal permission. This allows you to perform
+ * both {@link #checkPermission} and {@link #checkUriPermission} in one
+ * call.
+ *
+ * @param uri The Uri whose permission is to be checked, or null to not
+ * do this check.
+ * @param readPermission The permission that provides overall read access,
+ * or null to not do this check.
+ * @param writePermission The permission that provides overall write
+ * acess, or null to not do this check.
+ * @param pid The process ID being checked against. Must be &gt; 0.
+ * @param uid The user ID being checked against. A uid of 0 is the root
+ * user, which will pass every permission check.
+ * @param modeFlags The type of access to grant. May be one or both of
+ * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION Intent.FLAG_GRANT_READ_URI_PERMISSION} or
+ * {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION Intent.FLAG_GRANT_WRITE_URI_PERMISSION}.
+ *
+ * @return Returns {@link PackageManager#PERMISSION_GRANTED} if the caller
+ * is allowed to access that uri or holds one of the given permissions, or
+ * {@link PackageManager#PERMISSION_DENIED} if it is not.
+ */
+ public abstract int checkUriPermission(Uri uri, String readPermission,
+ String writePermission, int pid, int uid, int modeFlags);
+
+ /**
+ * If a particular process and user ID has not been granted
+ * permission to access a specific URI, throw {@link
+ * SecurityException}. This only checks for permissions that have
+ * been explicitly granted -- if the given process/uid has more
+ * general access to the URI's content provider then this check
+ * will always fail.
+ *
+ * @param uri The uri that is being checked.
+ * @param pid The process ID being checked against. Must be &gt; 0.
+ * @param uid The user ID being checked against. A uid of 0 is the root
+ * user, which will pass every permission check.
+ * @param modeFlags The type of access to grant. May be one or both of
+ * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION Intent.FLAG_GRANT_READ_URI_PERMISSION} or
+ * {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION Intent.FLAG_GRANT_WRITE_URI_PERMISSION}.
+ * @param message A message to include in the exception if it is thrown.
+ *
+ * @see #checkUriPermission(Uri, int, int, int)
+ */
+ public abstract void enforceUriPermission(
+ Uri uri, int pid, int uid, int modeFlags, String message);
+
+ /**
+ * If the calling process and user ID has not been granted
+ * permission to access a specific URI, throw {@link
+ * SecurityException}. This is basically the same as calling
+ * {@link #enforceUriPermission(Uri, int, int, int, String)} with
+ * the pid and uid returned by {@link
+ * android.os.Binder#getCallingPid} and {@link
+ * android.os.Binder#getCallingUid}. One important difference is
+ * that if you are not currently processing an IPC, this function
+ * will always throw a SecurityException.
+ *
+ * @param uri The uri that is being checked.
+ * @param modeFlags The type of access to grant. May be one or both of
+ * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION Intent.FLAG_GRANT_READ_URI_PERMISSION} or
+ * {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION Intent.FLAG_GRANT_WRITE_URI_PERMISSION}.
+ * @param message A message to include in the exception if it is thrown.
+ *
+ * @see #checkCallingUriPermission(Uri, int)
+ */
+ public abstract void enforceCallingUriPermission(
+ Uri uri, int modeFlags, String message);
+
+ /**
+ * If the calling process of an IPC <em>or you</em> has not been
+ * granted permission to access a specific URI, throw {@link
+ * SecurityException}. This is the same as {@link
+ * #enforceCallingUriPermission}, except it grants your own
+ * permissions if you are not currently processing an IPC. Use
+ * with care!
+ *
+ * @param uri The uri that is being checked.
+ * @param modeFlags The type of access to grant. May be one or both of
+ * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION Intent.FLAG_GRANT_READ_URI_PERMISSION} or
+ * {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION Intent.FLAG_GRANT_WRITE_URI_PERMISSION}.
+ * @param message A message to include in the exception if it is thrown.
+ *
+ * @see #checkCallingOrSelfUriPermission(Uri, int)
+ */
+ public abstract void enforceCallingOrSelfUriPermission(
+ Uri uri, int modeFlags, String message);
+
+ /**
+ * Enforce both a Uri and normal permission. This allows you to perform
+ * both {@link #enforcePermission} and {@link #enforceUriPermission} in one
+ * call.
+ *
+ * @param uri The Uri whose permission is to be checked, or null to not
+ * do this check.
+ * @param readPermission The permission that provides overall read access,
+ * or null to not do this check.
+ * @param writePermission The permission that provides overall write
+ * acess, or null to not do this check.
+ * @param pid The process ID being checked against. Must be &gt; 0.
+ * @param uid The user ID being checked against. A uid of 0 is the root
+ * user, which will pass every permission check.
+ * @param modeFlags The type of access to grant. May be one or both of
+ * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION Intent.FLAG_GRANT_READ_URI_PERMISSION} or
+ * {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION Intent.FLAG_GRANT_WRITE_URI_PERMISSION}.
+ * @param message A message to include in the exception if it is thrown.
+ *
+ * @see #checkUriPermission(Uri, String, String, int, int, int)
+ */
+ public abstract void enforceUriPermission(
+ Uri uri, String readPermission, String writePermission,
+ int pid, int uid, int modeFlags, String message);
+
+ /**
+ * Flag for use with {@link #createPackageContext}: include the application
+ * code with the context. This means loading code into the caller's
+ * process, so that {@link #getClassLoader()} can be used to instantiate
+ * the application's classes. Setting this flags imposes security
+ * restrictions on what application context you can access; if the
+ * requested application can not be safely loaded into your process,
+ * java.lang.SecurityException will be thrown. If this flag is not set,
+ * there will be no restrictions on the packages that can be loaded,
+ * but {@link #getClassLoader} will always return the default system
+ * class loader.
+ */
+ public static final int CONTEXT_INCLUDE_CODE = 0x00000001;
+
+ /**
+ * Flag for use with {@link #createPackageContext}: ignore any security
+ * restrictions on the Context being requested, allowing it to always
+ * be loaded. For use with {@link #CONTEXT_INCLUDE_CODE} to allow code
+ * to be loaded into a process even when it isn't safe to do so. Use
+ * with extreme care!
+ */
+ public static final int CONTEXT_IGNORE_SECURITY = 0x00000002;
+
+ /**
+ * Return a new Context object for the given application name. This
+ * Context is the same as what the named application gets when it is
+ * launched, containing the same resources and class loader. Each call to
+ * this method returns a new instance of a Context object; Context objects
+ * are not shared, however they share common state (Resources, ClassLoader,
+ * etc) so the Context instance itself is fairly lightweight.
+ *
+ * <p>Throws {@link PackageManager.NameNotFoundException} if there is no
+ * application with the given package name.
+ *
+ * <p>Throws {@link java.lang.SecurityException} if the Context requested
+ * can not be loaded into the caller's process for security reasons (see
+ * {@link #CONTEXT_INCLUDE_CODE} for more information}.
+ *
+ * @param packageName Name of the application's package.
+ * @param flags Option flags, one of {@link #CONTEXT_INCLUDE_CODE}
+ * or {@link #CONTEXT_IGNORE_SECURITY}.
+ *
+ * @return A Context for the application.
+ *
+ * @throws java.lang.SecurityException
+ * @throws PackageManager.NameNotFoundException if there is no application with
+ * the given package name
+ */
+ public abstract Context createPackageContext(String packageName,
+ int flags) throws PackageManager.NameNotFoundException;
+}
diff --git a/core/java/android/content/ContextWrapper.java b/core/java/android/content/ContextWrapper.java
new file mode 100644
index 0000000..36e1c34
--- /dev/null
+++ b/core/java/android/content/ContextWrapper.java
@@ -0,0 +1,422 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+import android.content.pm.PackageManager;
+import android.content.res.AssetManager;
+import android.content.res.Resources;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteDatabase.CursorFactory;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Proxying implementation of Context that simply delegates all of its calls to
+ * another Context. Can be subclassed to modify behavior without changing
+ * the original Context.
+ */
+public class ContextWrapper extends Context {
+ Context mBase;
+
+ public ContextWrapper(Context base) {
+ mBase = base;
+ }
+
+ /**
+ * Set the base context for this ContextWrapper. All calls will then be
+ * delegated to the base context. Throws
+ * IllegalStateException if a base context has already been set.
+ *
+ * @param base The new base context for this wrapper.
+ */
+ protected void attachBaseContext(Context base) {
+ if (mBase != null) {
+ throw new IllegalStateException("Base context already set");
+ }
+ mBase = base;
+ }
+
+ /**
+ * @return the base context as set by the constructor or setBaseContext
+ */
+ public Context getBaseContext() {
+ return mBase;
+ }
+
+ @Override
+ public AssetManager getAssets() {
+ return mBase.getAssets();
+ }
+
+ @Override
+ public Resources getResources()
+ {
+ return mBase.getResources();
+ }
+
+ @Override
+ public PackageManager getPackageManager() {
+ return mBase.getPackageManager();
+ }
+
+ @Override
+ public ContentResolver getContentResolver() {
+ return mBase.getContentResolver();
+ }
+
+ @Override
+ public Looper getMainLooper() {
+ return mBase.getMainLooper();
+ }
+
+ @Override
+ public Context getApplicationContext() {
+ return mBase.getApplicationContext();
+ }
+
+ @Override
+ public void setTheme(int resid) {
+ mBase.setTheme(resid);
+ }
+
+ @Override
+ public Resources.Theme getTheme() {
+ return mBase.getTheme();
+ }
+
+ @Override
+ public ClassLoader getClassLoader() {
+ return mBase.getClassLoader();
+ }
+
+ @Override
+ public String getPackageName() {
+ return mBase.getPackageName();
+ }
+
+ @Override
+ public String getPackageResourcePath() {
+ return mBase.getPackageResourcePath();
+ }
+
+ @Override
+ public String getPackageCodePath() {
+ return mBase.getPackageCodePath();
+ }
+
+ @Override
+ public SharedPreferences getSharedPreferences(String name, int mode) {
+ return mBase.getSharedPreferences(name, mode);
+ }
+
+ @Override
+ public FileInputStream openFileInput(String name)
+ throws FileNotFoundException {
+ return mBase.openFileInput(name);
+ }
+
+ @Override
+ public FileOutputStream openFileOutput(String name, int mode)
+ throws FileNotFoundException {
+ return mBase.openFileOutput(name, mode);
+ }
+
+ @Override
+ public boolean deleteFile(String name) {
+ return mBase.deleteFile(name);
+ }
+
+ @Override
+ public File getFileStreamPath(String name) {
+ return mBase.getFileStreamPath(name);
+ }
+
+ @Override
+ public String[] fileList() {
+ return mBase.fileList();
+ }
+
+ @Override
+ public File getFilesDir() {
+ return mBase.getFilesDir();
+ }
+
+ @Override
+ public File getCacheDir() {
+ return mBase.getCacheDir();
+ }
+
+ @Override
+ public File getDir(String name, int mode) {
+ return mBase.getDir(name, mode);
+ }
+
+ @Override
+ public SQLiteDatabase openOrCreateDatabase(String name, int mode, CursorFactory factory) {
+ return mBase.openOrCreateDatabase(name, mode, factory);
+ }
+
+ @Override
+ public boolean deleteDatabase(String name) {
+ return mBase.deleteDatabase(name);
+ }
+
+ @Override
+ public File getDatabasePath(String name) {
+ return mBase.getDatabasePath(name);
+ }
+
+ @Override
+ public String[] databaseList() {
+ return mBase.databaseList();
+ }
+
+ @Override
+ public Drawable getWallpaper() {
+ return mBase.getWallpaper();
+ }
+
+ @Override
+ public Drawable peekWallpaper() {
+ return mBase.peekWallpaper();
+ }
+
+ @Override
+ public int getWallpaperDesiredMinimumWidth() {
+ return mBase.getWallpaperDesiredMinimumWidth();
+ }
+
+ @Override
+ public int getWallpaperDesiredMinimumHeight() {
+ return mBase.getWallpaperDesiredMinimumHeight();
+ }
+
+ @Override
+ public void setWallpaper(Bitmap bitmap) throws IOException {
+ mBase.setWallpaper(bitmap);
+ }
+
+ @Override
+ public void setWallpaper(InputStream data) throws IOException {
+ mBase.setWallpaper(data);
+ }
+
+ @Override
+ public void clearWallpaper() throws IOException {
+ mBase.clearWallpaper();
+ }
+
+ @Override
+ public void startActivity(Intent intent) {
+ mBase.startActivity(intent);
+ }
+
+ @Override
+ public void sendBroadcast(Intent intent) {
+ mBase.sendBroadcast(intent);
+ }
+
+ @Override
+ public void sendBroadcast(Intent intent, String receiverPermission) {
+ mBase.sendBroadcast(intent, receiverPermission);
+ }
+
+ @Override
+ public void sendOrderedBroadcast(Intent intent,
+ String receiverPermission) {
+ mBase.sendOrderedBroadcast(intent, receiverPermission);
+ }
+
+ @Override
+ public void sendOrderedBroadcast(
+ Intent intent, String receiverPermission, BroadcastReceiver resultReceiver,
+ Handler scheduler, int initialCode, String initialData,
+ Bundle initialExtras) {
+ mBase.sendOrderedBroadcast(intent, receiverPermission,
+ resultReceiver, scheduler, initialCode,
+ initialData, initialExtras);
+ }
+
+ @Override
+ public void sendStickyBroadcast(Intent intent) {
+ mBase.sendStickyBroadcast(intent);
+ }
+
+ @Override
+ public void removeStickyBroadcast(Intent intent) {
+ mBase.removeStickyBroadcast(intent);
+ }
+
+ @Override
+ public Intent registerReceiver(
+ BroadcastReceiver receiver, IntentFilter filter) {
+ return mBase.registerReceiver(receiver, filter);
+ }
+
+ @Override
+ public Intent registerReceiver(
+ BroadcastReceiver receiver, IntentFilter filter,
+ String broadcastPermission, Handler scheduler) {
+ return mBase.registerReceiver(receiver, filter, broadcastPermission,
+ scheduler);
+ }
+
+ @Override
+ public void unregisterReceiver(BroadcastReceiver receiver) {
+ mBase.unregisterReceiver(receiver);
+ }
+
+ @Override
+ public ComponentName startService(Intent service) {
+ return mBase.startService(service);
+ }
+
+ @Override
+ public boolean stopService(Intent name) {
+ return mBase.stopService(name);
+ }
+
+ @Override
+ public boolean bindService(Intent service, ServiceConnection conn,
+ int flags) {
+ return mBase.bindService(service, conn, flags);
+ }
+
+ @Override
+ public void unbindService(ServiceConnection conn) {
+ mBase.unbindService(conn);
+ }
+
+ @Override
+ public boolean startInstrumentation(ComponentName className,
+ String profileFile, Bundle arguments) {
+ return mBase.startInstrumentation(className, profileFile, arguments);
+ }
+
+ @Override
+ public Object getSystemService(String name) {
+ return mBase.getSystemService(name);
+ }
+
+ @Override
+ public int checkPermission(String permission, int pid, int uid) {
+ return mBase.checkPermission(permission, pid, uid);
+ }
+
+ @Override
+ public int checkCallingPermission(String permission) {
+ return mBase.checkCallingPermission(permission);
+ }
+
+ @Override
+ public int checkCallingOrSelfPermission(String permission) {
+ return mBase.checkCallingOrSelfPermission(permission);
+ }
+
+ @Override
+ public void enforcePermission(
+ String permission, int pid, int uid, String message) {
+ mBase.enforcePermission(permission, pid, uid, message);
+ }
+
+ @Override
+ public void enforceCallingPermission(String permission, String message) {
+ mBase.enforceCallingPermission(permission, message);
+ }
+
+ @Override
+ public void enforceCallingOrSelfPermission(
+ String permission, String message) {
+ mBase.enforceCallingOrSelfPermission(permission, message);
+ }
+
+ @Override
+ public void grantUriPermission(String toPackage, Uri uri, int modeFlags) {
+ mBase.grantUriPermission(toPackage, uri, modeFlags);
+ }
+
+ @Override
+ public void revokeUriPermission(Uri uri, int modeFlags) {
+ mBase.revokeUriPermission(uri, modeFlags);
+ }
+
+ @Override
+ public int checkUriPermission(Uri uri, int pid, int uid, int modeFlags) {
+ return mBase.checkUriPermission(uri, pid, uid, modeFlags);
+ }
+
+ @Override
+ public int checkCallingUriPermission(Uri uri, int modeFlags) {
+ return mBase.checkCallingUriPermission(uri, modeFlags);
+ }
+
+ @Override
+ public int checkCallingOrSelfUriPermission(Uri uri, int modeFlags) {
+ return mBase.checkCallingOrSelfUriPermission(uri, modeFlags);
+ }
+
+ @Override
+ public int checkUriPermission(Uri uri, String readPermission,
+ String writePermission, int pid, int uid, int modeFlags) {
+ return mBase.checkUriPermission(uri, readPermission, writePermission,
+ pid, uid, modeFlags);
+ }
+
+ @Override
+ public void enforceUriPermission(
+ Uri uri, int pid, int uid, int modeFlags, String message) {
+ mBase.enforceUriPermission(uri, pid, uid, modeFlags, message);
+ }
+
+ @Override
+ public void enforceCallingUriPermission(
+ Uri uri, int modeFlags, String message) {
+ mBase.enforceCallingUriPermission(uri, modeFlags, message);
+ }
+
+ @Override
+ public void enforceCallingOrSelfUriPermission(
+ Uri uri, int modeFlags, String message) {
+ mBase.enforceCallingOrSelfUriPermission(uri, modeFlags, message);
+ }
+
+ @Override
+ public void enforceUriPermission(
+ Uri uri, String readPermission, String writePermission,
+ int pid, int uid, int modeFlags, String message) {
+ mBase.enforceUriPermission(
+ uri, readPermission, writePermission, pid, uid, modeFlags,
+ message);
+ }
+
+ @Override
+ public Context createPackageContext(String packageName, int flags)
+ throws PackageManager.NameNotFoundException {
+ return mBase.createPackageContext(packageName, flags);
+ }
+}
diff --git a/core/java/android/content/DefaultDataHandler.java b/core/java/android/content/DefaultDataHandler.java
new file mode 100644
index 0000000..7dc71b8
--- /dev/null
+++ b/core/java/android/content/DefaultDataHandler.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+import android.net.Uri;
+import android.util.Xml;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.Locator;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Stack;
+
+/**
+ * insert default data from InputStream, should be in XML format:
+ * if the provider syncs data to the server, the imported data will be synced to the server
+ * Samples:
+ * insert one row
+ * <row uri="content://contacts/people">
+ * <Col column = "name" value = "foo feebe "/>
+ * <Col column = "addr" value = "Tx"/>
+ * </row>
+ *
+ * delete, it must be in order of uri, select and arg
+ * <del uri="content://contacts/people" select="name=? and addr=?"
+ * arg1 = "foo feebe" arg2 ="Tx"/>
+ *
+ * use first row's uri to insert into another table
+ * content://contacts/people/1/phones
+ * <row uri="content://contacts/people">
+ * <col column = "name" value = "foo feebe"/>
+ * <col column = "addr" value = "Tx"/>
+ * <row postfix="phones">
+ * <col column="number" value="512-514-6535"/>
+ * </row>
+ * <row postfix="phones">
+ * <col column="cell" value="512-514-6535"/>
+ * </row>
+ * </row>
+ *
+ * insert multiple rows in to same table and same attributes:
+ * <row uri="content://contacts/people" >
+ * <row>
+ * <col column= "name" value = "foo feebe"/>
+ * <col column= "addr" value = "Tx"/>
+ * </row>
+ * <row>
+ * </row>
+ * </row>
+ *
+ * @hide
+ */
+public class DefaultDataHandler implements ContentInsertHandler {
+ private final static String ROW = "row";
+ private final static String COL = "col";
+ private final static String URI_STR = "uri";
+ private final static String POSTFIX = "postfix";
+ private final static String DEL = "del";
+ private final static String SELECT = "select";
+ private final static String ARG = "arg";
+
+ private Stack<Uri> mUris = new Stack<Uri>();
+ private ContentValues mValues;
+ private ContentResolver mContentResolver;
+
+ public void insert(ContentResolver contentResolver, InputStream in)
+ throws IOException, SAXException {
+ mContentResolver = contentResolver;
+ Xml.parse(in, Xml.Encoding.UTF_8, this);
+ }
+
+ public void insert(ContentResolver contentResolver, String in)
+ throws SAXException {
+ mContentResolver = contentResolver;
+ Xml.parse(in, this);
+ }
+
+ private void parseRow(Attributes atts) throws SAXException {
+ String uriStr = atts.getValue(URI_STR);
+ Uri uri;
+ if (uriStr != null) {
+ // case 1
+ uri = Uri.parse(uriStr);
+ if (uri == null) {
+ throw new SAXException("attribute " +
+ atts.getValue(URI_STR) + " parsing failure");
+ }
+
+ } else if (mUris.size() > 0){
+ // case 2
+ String postfix = atts.getValue(POSTFIX);
+ if (postfix != null) {
+ uri = Uri.withAppendedPath(mUris.lastElement(),
+ postfix);
+ } else {
+ uri = mUris.lastElement();
+ }
+ } else {
+ throw new SAXException("attribute parsing failure");
+ }
+
+ mUris.push(uri);
+
+ }
+
+ private Uri insertRow() {
+ Uri u = mContentResolver.insert(mUris.lastElement(), mValues);
+ mValues = null;
+ return u;
+ }
+
+ public void startElement(String uri, String localName, String name,
+ Attributes atts) throws SAXException {
+ if (ROW.equals(localName)) {
+ if (mValues != null) {
+ // case 2, <Col> before <Row> insert last uri
+ if (mUris.empty()) {
+ throw new SAXException("uri is empty");
+ }
+ Uri nextUri = insertRow();
+ if (nextUri == null) {
+ throw new SAXException("insert to uri " +
+ mUris.lastElement().toString() + " failure");
+ } else {
+ // make sure the stack lastElement save uri for more than one row
+ mUris.pop();
+ mUris.push(nextUri);
+ parseRow(atts);
+ }
+ } else {
+ int attrLen = atts.getLength();
+ if (attrLen == 0) {
+ // case 3, share same uri as last level
+ mUris.push(mUris.lastElement());
+ } else {
+ parseRow(atts);
+ }
+ }
+ } else if (COL.equals(localName)) {
+ int attrLen = atts.getLength();
+ if (attrLen != 2) {
+ throw new SAXException("illegal attributes number " + attrLen);
+ }
+ String key = atts.getValue(0);
+ String value = atts.getValue(1);
+ if (key != null && key.length() > 0 && value != null && value.length() > 0) {
+ if (mValues == null) {
+ mValues = new ContentValues();
+ }
+ mValues.put(key, value);
+ } else {
+ throw new SAXException("illegal attributes value");
+ }
+ } else if (DEL.equals(localName)){
+ Uri u = Uri.parse(atts.getValue(URI_STR));
+ if (u == null) {
+ throw new SAXException("attribute " +
+ atts.getValue(URI_STR) + " parsing failure");
+ }
+ int attrLen = atts.getLength() - 2;
+ if (attrLen > 0) {
+ String[] selectionArgs = new String[attrLen];
+ for (int i = 0; i < attrLen; i++) {
+ selectionArgs[i] = atts.getValue(i+2);
+ }
+ mContentResolver.delete(u, atts.getValue(1), selectionArgs);
+ } else if (attrLen == 0){
+ mContentResolver.delete(u, atts.getValue(1), null);
+ } else {
+ mContentResolver.delete(u, null, null);
+ }
+
+ } else {
+ throw new SAXException("unknown element: " + localName);
+ }
+ }
+
+ public void endElement(String uri, String localName, String name)
+ throws SAXException {
+ if (ROW.equals(localName)) {
+ if (mUris.empty()) {
+ throw new SAXException("uri mismatch");
+ }
+ if (mValues != null) {
+ insertRow();
+ }
+ mUris.pop();
+ }
+ }
+
+
+ public void characters(char[] ch, int start, int length)
+ throws SAXException {
+ // TODO Auto-generated method stub
+
+ }
+
+ public void endDocument() throws SAXException {
+ // TODO Auto-generated method stub
+
+ }
+
+ public void endPrefixMapping(String prefix) throws SAXException {
+ // TODO Auto-generated method stub
+
+ }
+
+ public void ignorableWhitespace(char[] ch, int start, int length)
+ throws SAXException {
+ // TODO Auto-generated method stub
+
+ }
+
+ public void processingInstruction(String target, String data)
+ throws SAXException {
+ // TODO Auto-generated method stub
+
+ }
+
+ public void setDocumentLocator(Locator locator) {
+ // TODO Auto-generated method stub
+
+ }
+
+ public void skippedEntity(String name) throws SAXException {
+ // TODO Auto-generated method stub
+
+ }
+
+ public void startDocument() throws SAXException {
+ // TODO Auto-generated method stub
+
+ }
+
+ public void startPrefixMapping(String prefix, String uri)
+ throws SAXException {
+ // TODO Auto-generated method stub
+
+ }
+
+}
diff --git a/core/java/android/content/DialogInterface.java b/core/java/android/content/DialogInterface.java
new file mode 100644
index 0000000..fc94aa6
--- /dev/null
+++ b/core/java/android/content/DialogInterface.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+import android.view.KeyEvent;
+
+/**
+ *
+ */
+public interface DialogInterface {
+ public static final int BUTTON1 = -1;
+ public static final int BUTTON2 = -2;
+ public static final int BUTTON3 = -3;
+
+ public void cancel();
+
+ public void dismiss();
+
+ /**
+ * Interface used to allow the creator of a dialog to run some code when the
+ * dialog is canceled.
+ * <p>
+ * This will only be called when the dialog is canceled, if the creator
+ * needs to know when it is dismissed in general, use
+ * {@link DialogInterface.OnDismissListener}.
+ */
+ interface OnCancelListener {
+ /**
+ * This method will be invoked when the dialog is canceled.
+ *
+ * @param dialog The dialog that was canceled will be passed into the
+ * method.
+ */
+ public void onCancel(DialogInterface dialog);
+ }
+
+ /**
+ * Interface used to allow the creator of a dialog to run some code when the
+ * dialog is dismissed.
+ */
+ interface OnDismissListener {
+ /**
+ * This method will be invoked when the dialog is dismissed.
+ *
+ * @param dialog The dialog that was dismissed will be passed into the
+ * method.
+ */
+ public void onDismiss(DialogInterface dialog);
+ }
+
+ /**
+ * Interface used to allow the creator of a dialog to run some code when an
+ * item on the dialog is clicked..
+ */
+ interface OnClickListener {
+ /**
+ * This method will be invoked when a button in the dialog is clicked.
+ *
+ * @param dialog The dialog that received the click.
+ * @param which The button that was clicked, i.e. BUTTON1 or BUTTON2 or
+ * the position of the item clicked.
+ */
+ public void onClick(DialogInterface dialog, int which);
+ }
+
+ /**
+ * Interface used to allow the creator of a dialog to run some code when an
+ * item in a multi-choice dialog is clicked.
+ */
+ interface OnMultiChoiceClickListener {
+ /**
+ * This method will be invoked when an item in the dialog is clicked.
+ *
+ * @param dialog The dialog where the selection was made.
+ * @param which The position of the item in the list that was clicked.
+ * @param isChecked True if the click checked the item, else false.
+ */
+ public void onClick(DialogInterface dialog, int which, boolean isChecked);
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when a key event is
+ * dispatched to this dialog. The callback will be invoked before the key
+ * event is given to the dialog.
+ */
+ interface OnKeyListener {
+ /**
+ * Called when a key is dispatched to a dialog. This allows listeners to
+ * get a chance to respond before the dialog.
+ *
+ * @param dialog The dialog the key has been dispatched to.
+ * @param keyCode The code for the physical key that was pressed
+ * @param event The KeyEvent object containing full information about
+ * the event.
+ * @return True if the listener has consumed the event, false otherwise.
+ */
+ public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event);
+ }
+}
diff --git a/core/java/android/content/Formatter.java b/core/java/android/content/Formatter.java
new file mode 100644
index 0000000..8ad9f40
--- /dev/null
+++ b/core/java/android/content/Formatter.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+/**
+ * Utility class to aid in formatting common values that are not covered
+ * by the standard java.util.Formatter
+ * @hide
+ */
+public final class Formatter {
+
+ /**
+ * Formats a content size to be in the form of bytes, kilobytes,
+ * megabytes, etc
+ *
+ * @param context Context to use to load the localized units
+ * @param number size value to be formated
+ * @return formated string with the number
+ */
+ public static String formatFileSize(Context context, long number) {
+ if (context == null) {
+ return "";
+ }
+
+ float result = number;
+ int suffix = com.android.internal.R.string.byteShort;
+ if (result > 900) {
+ suffix = com.android.internal.R.string.kilobyteShort;
+ result = result / 1024;
+ }
+ if (result > 900) {
+ suffix = com.android.internal.R.string.megabyteShort;
+ result = result / 1024;
+ }
+ if (result > 900) {
+ suffix = com.android.internal.R.string.gigabyteShort;
+ result = result / 1024;
+ }
+ if (result > 900) {
+ suffix = com.android.internal.R.string.terabyteShort;
+ result = result / 1024;
+ }
+ if (result > 900) {
+ suffix = com.android.internal.R.string.petabyteShort;
+ result = result / 1024;
+ }
+ if (result < 100) {
+ return String.format("%.2f%s", result, context.getText(suffix).toString());
+ }
+ return String.format("%.0f%s", result, context.getText(suffix).toString());
+ }
+}
diff --git a/core/java/android/content/IContentProvider.java b/core/java/android/content/IContentProvider.java
new file mode 100644
index 0000000..a6ef46f
--- /dev/null
+++ b/core/java/android/content/IContentProvider.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+import android.database.Cursor;
+import android.database.CursorWindow;
+import android.database.IBulkCursor;
+import android.database.IContentObserver;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.os.IBinder;
+import android.os.IInterface;
+import android.os.ParcelFileDescriptor;
+
+import java.io.FileNotFoundException;
+
+/**
+ * The ipc interface to talk to a content provider.
+ * @hide
+ */
+public interface IContentProvider extends IInterface {
+ /**
+ * @hide - hide this because return type IBulkCursor and parameter
+ * IContentObserver are system private classes.
+ */
+ public IBulkCursor bulkQuery(Uri url, String[] projection,
+ String selection, String[] selectionArgs, String sortOrder, IContentObserver observer,
+ CursorWindow window) throws RemoteException;
+ public Cursor query(Uri url, String[] projection, String selection,
+ String[] selectionArgs, String sortOrder) throws RemoteException;
+ public String getType(Uri url) throws RemoteException;
+ public Uri insert(Uri url, ContentValues initialValues)
+ throws RemoteException;
+ public int bulkInsert(Uri url, ContentValues[] initialValues) throws RemoteException;
+ public int delete(Uri url, String selection, String[] selectionArgs)
+ throws RemoteException;
+ public int update(Uri url, ContentValues values, String selection,
+ String[] selectionArgs) throws RemoteException;
+ public ParcelFileDescriptor openFile(Uri url, String mode)
+ throws RemoteException, FileNotFoundException;
+ public ISyncAdapter getSyncAdapter() throws RemoteException;
+
+ /* IPC constants */
+ static final String descriptor = "android.content.IContentProvider";
+
+ static final int QUERY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION;
+ static final int GET_TYPE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 1;
+ static final int INSERT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 2;
+ static final int DELETE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 3;
+ static final int UPDATE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 9;
+ static final int GET_SYNC_ADAPTER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 10;
+ static final int BULK_INSERT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 12;
+ static final int OPEN_FILE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 13;
+}
diff --git a/core/java/android/content/IContentService.java b/core/java/android/content/IContentService.java
new file mode 100644
index 0000000..a3047da
--- /dev/null
+++ b/core/java/android/content/IContentService.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+import android.database.IContentObserver;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.os.IBinder;
+import android.os.IInterface;
+import android.os.Bundle;
+
+/**
+ * {@hide}
+ */
+public interface IContentService extends IInterface
+{
+ public void registerContentObserver(Uri uri, boolean notifyForDescendentsn,
+ IContentObserver observer) throws RemoteException;
+ public void unregisterContentObserver(IContentObserver observer) throws RemoteException;
+
+ public void notifyChange(Uri uri, IContentObserver observer,
+ boolean observerWantsSelfNotifications, boolean syncToNetwork)
+ throws RemoteException;
+
+ public void startSync(Uri url, Bundle extras) throws RemoteException;
+ public void cancelSync(Uri uri) throws RemoteException;
+
+ static final String SERVICE_NAME = "content";
+
+ /* IPC constants */
+ static final String descriptor = "android.content.IContentService";
+
+ static final int REGISTER_CONTENT_OBSERVER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 1;
+ static final int UNREGISTER_CHANGE_OBSERVER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 2;
+ static final int NOTIFY_CHANGE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 3;
+ static final int START_SYNC_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 4;
+ static final int CANCEL_SYNC_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 5;
+}
+
diff --git a/core/java/android/content/ISyncAdapter.aidl b/core/java/android/content/ISyncAdapter.aidl
new file mode 100644
index 0000000..671188c
--- /dev/null
+++ b/core/java/android/content/ISyncAdapter.aidl
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+import android.os.Bundle;
+import android.content.ISyncContext;
+
+/**
+ * Interface used to control the sync activity on a SyncAdapter
+ * @hide
+ */
+oneway interface ISyncAdapter {
+ /**
+ * Initiate a sync for this account. SyncAdapter-specific parameters may
+ * be specified in extras, which is guaranteed to not be null.
+ *
+ * @param syncContext the ISyncContext used to indicate the progress of the sync. When
+ * the sync is finished (successfully or not) ISyncContext.onFinished() must be called.
+ * @param account the account that should be synced
+ * @param extras SyncAdapter-specific parameters
+ */
+ void startSync(ISyncContext syncContext, String account, in Bundle extras);
+
+ /**
+ * Cancel the most recently initiated sync. Due to race conditions, this may arrive
+ * after the ISyncContext.onFinished() for that sync was called.
+ */
+ void cancelSync();
+}
diff --git a/core/java/android/content/ISyncContext.aidl b/core/java/android/content/ISyncContext.aidl
new file mode 100644
index 0000000..6d18a1c
--- /dev/null
+++ b/core/java/android/content/ISyncContext.aidl
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+import android.content.SyncResult;
+
+/**
+ * Interface used by the SyncAdapter to indicate its progress.
+ * @hide
+ */
+interface ISyncContext {
+ /**
+ * Call to indicate that the SyncAdapter is making progress. E.g., if this SyncAdapter
+ * downloads or sends records to/from the server, this may be called after each record
+ * is downloaded or uploaded.
+ */
+ void sendHeartbeat();
+
+ /**
+ * Signal that the corresponding sync session is completed.
+ * @param result information about this sync session
+ */
+ void onFinished(in SyncResult result);
+}
diff --git a/core/java/android/content/Intent.aidl b/core/java/android/content/Intent.aidl
new file mode 100644
index 0000000..568986b
--- /dev/null
+++ b/core/java/android/content/Intent.aidl
@@ -0,0 +1,20 @@
+/* //device/java/android/android/content/Intent.aidl
+**
+** Copyright 2007, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.content;
+
+parcelable Intent;
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
new file mode 100644
index 0000000..c76158c
--- /dev/null
+++ b/core/java/android/content/Intent.java
@@ -0,0 +1,4391 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.TypedValue;
+import com.android.internal.util.XmlUtils;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+/**
+ * An intent is an abstract description of an operation to be performed. It
+ * can be used with {@link Context#startActivity(Intent) startActivity} to
+ * launch an {@link android.app.Activity},
+ * {@link android.content.Context#sendBroadcast(Intent) broadcastIntent} to
+ * send it to any interested {@link BroadcastReceiver BroadcastReceiver} components,
+ * and {@link android.content.Context#startService} or
+ * {@link android.content.Context#bindService} to communicate with a
+ * background {@link android.app.Service}.
+ *
+ * <p>An Intent provides a facility for performing late runtime binding between
+ * the code in different applications. Its most significant use is in the
+ * launching of activities, where it can be thought of as the glue between
+ * activities. It is
+ * basically a passive data structure holding an abstract description of an
+ * action to be performed. The primary pieces of information in an intent
+ * are:</p>
+ *
+ * <ul>
+ * <li> <p><b>action</b> -- The general action to be performed, such as
+ * {@link #ACTION_VIEW}, {@link #ACTION_EDIT}, {@link #ACTION_MAIN},
+ * etc.</p>
+ * </li>
+ * <li> <p><b>data</b> -- The data to operate on, such as a person record
+ * in the contacts database, expressed as a {@link android.net.Uri}.</p>
+ * </li>
+ * </ul>
+ *
+ *
+ * <p>Some examples of action/data pairs are:</p>
+ *
+ * <ul>
+ * <li> <p><b>{@link #ACTION_VIEW} <i>content://contacts/1</i></b> -- Display
+ * information about the person whose identifier is "1".</p>
+ * </li>
+ * <li> <p><b>{@link #ACTION_DIAL} <i>content://contacts/1</i></b> -- Display
+ * the phone dialer with the person filled in.</p>
+ * </li>
+ * <li> <p><b>{@link #ACTION_VIEW} <i>tel:123</i></b> -- Display
+ * the phone dialer with the given number filled in. Note how the
+ * VIEW action does what what is considered the most reasonable thing for
+ * a particular URI.</p>
+ * </li>
+ * <li> <p><b>{@link #ACTION_DIAL} <i>tel:123</i></b> -- Display
+ * the phone dialer with the given number filled in.</p>
+ * </li>
+ * <li> <p><b>{@link #ACTION_EDIT} <i>content://contacts/1</i></b> -- Edit
+ * information about the person whose identifier is "1".</p>
+ * </li>
+ * <li> <p><b>{@link #ACTION_VIEW} <i>content://contacts/</i></b> -- Display
+ * a list of people, which the user can browse through. This example is a
+ * typical top-level entry into the Contacts application, showing you the
+ * list of people. Selecting a particular person to view would result in a
+ * new intent { <b>{@link #ACTION_VIEW} <i>content://contacts/N</i></b> }
+ * being used to start an activity to display that person.</p>
+ * </li>
+ * </ul>
+ *
+ * <p>In addition to these primary attributes, there are a number of secondary
+ * attributes that you can also include with an intent:</p>
+ *
+ * <ul>
+ * <li> <p><b>category</b> -- Gives additional information about the action
+ * to execute. For example, {@link #CATEGORY_LAUNCHER} means it should
+ * appear in the Launcher as a top-level application, while
+ * {@link #CATEGORY_ALTERNATIVE} means it should be included in a list
+ * of alternative actions the user can perform on a piece of data.</p>
+ * <li> <p><b>type</b> -- Specifies an explicit type (a MIME type) of the
+ * intent data. Normally the type is inferred from the data itself.
+ * By setting this attribute, you disable that evaluation and force
+ * an explicit type.</p>
+ * <li> <p><b>component</b> -- Specifies an explicit name of a component
+ * class to use for the intent. Normally this is determined by looking
+ * at the other information in the intent (the action, data/type, and
+ * categories) and matching that with a component that can handle it.
+ * If this attribute is set then none of the evaluation is performed,
+ * and this component is used exactly as is. By specifying this attribute,
+ * all of the other Intent attributes become optional.</p>
+ * <li> <p><b>extras</b> -- This is a {@link Bundle} of any additional information.
+ * This can be used to provide extended information to the component.
+ * For example, if we have a action to send an e-mail message, we could
+ * also include extra pieces of data here to supply a subject, body,
+ * etc.</p>
+ * </ul>
+ *
+ * <p>Here are some examples of other operations you can specify as intents
+ * using these additional parameters:</p>
+ *
+ * <ul>
+ * <li> <p><b>{@link #ACTION_MAIN} with category {@link #CATEGORY_HOME}</b> --
+ * Launch the home screen.</p>
+ * </li>
+ * <li> <p><b>{@link #ACTION_GET_CONTENT} with MIME type
+ * <i>{@link android.provider.Contacts.Phones#CONTENT_URI
+ * vnd.android.cursor.item/phone}</i></b>
+ * -- Display the list of people's phone numbers, allowing the user to
+ * browse through them and pick one and return it to the parent activity.</p>
+ * </li>
+ * <li> <p><b>{@link #ACTION_GET_CONTENT} with MIME type
+ * <i>*{@literal /}*</i> and category {@link #CATEGORY_OPENABLE}</b>
+ * -- Display all pickers for data that can be opened with
+ * {@link ContentResolver#openInputStream(Uri) ContentResolver.openInputStream()},
+ * allowing the user to pick one of them and then some data inside of it
+ * and returning the resulting URI to the caller. This can be used,
+ * for example, in an e-mail application to allow the user to pick some
+ * data to include as an attachment.</p>
+ * </li>
+ * </ul>
+ *
+ * <p>There are a variety of standard Intent action and category constants
+ * defined in the Intent class, but applications can also define their own.
+ * These strings use java style scoping, to ensure they are unique -- for
+ * example, the standard {@link #ACTION_VIEW} is called
+ * "android.app.action.VIEW".</p>
+ *
+ * <p>Put together, the set of actions, data types, categories, and extra data
+ * defines a language for the system allowing for the expression of phrases
+ * such as "call john smith's cell". As applications are added to the system,
+ * they can extend this language by adding new actions, types, and categories, or
+ * they can modify the behavior of existing phrases by supplying their own
+ * activities that handle them.</p>
+ *
+ * <a name="IntentResolution"></a>
+ * <h3>Intent Resolution</h3>
+ *
+ * <p>There are two primary forms of intents you will use.
+ *
+ * <ul>
+ * <li> <p><b>Explicit Intents</b> have specified a component (via
+ * {@link #setComponent} or {@link #setClass}), which provides the exact
+ * class to be run. Often these will not include any other information,
+ * simply being a way for an application to launch various internal
+ * activities it has as the user interacts with the application.
+ *
+ * <li> <p><b>Implicit Intents</b> have not specified a component;
+ * instead, they must include enough information for the system to
+ * determine which of the available components is best to run for that
+ * intent.
+ * </ul>
+ *
+ * <p>When using implicit intents, given such an arbitrary intent we need to
+ * know what to do with it. This is handled by the process of <em>Intent
+ * resolution</em>, which maps an Intent to an {@link android.app.Activity},
+ * {@link BroadcastReceiver}, or {@link android.app.Service} (or sometimes two or
+ * more activities/receivers) that can handle it.</p>
+ *
+ * <p>The intent resolution mechanism basically revolves around matching an
+ * Intent against all of the &lt;intent-filter&gt; descriptions in the
+ * installed application packages. (Plus, in the case of broadcasts, any {@link BroadcastReceiver}
+ * objects explicitly registered with {@link Context#registerReceiver}.) More
+ * details on this can be found in the documentation on the {@link
+ * IntentFilter} class.</p>
+ *
+ * <p>There are three pieces of information in the Intent that are used for
+ * resolution: the action, type, and category. Using this information, a query
+ * is done on the {@link PackageManager} for a component that can handle the
+ * intent. The appropriate component is determined based on the intent
+ * information supplied in the <code>AndroidManifest.xml</code> file as
+ * follows:</p>
+ *
+ * <ul>
+ * <li> <p>The <b>action</b>, if given, must be listed by the component as
+ * one it handles.</p>
+ * <li> <p>The <b>type</b> is retrieved from the Intent's data, if not
+ * already supplied in the Intent. Like the action, if a type is
+ * included in the intent (either explicitly or implicitly in its
+ * data), then this must be listed by the component as one it handles.</p>
+ * <li> For data that is not a <code>content:</code> URI and where no explicit
+ * type is included in the Intent, instead the <b>scheme</b> of the
+ * intent data (such as <code>http:</code> or <code>mailto:</code>) is
+ * considered. Again like the action, if we are matching a scheme it
+ * must be listed by the component as one it can handle.
+ * <li> <p>The <b>categories</b>, if supplied, must <em>all</em> be listed
+ * by the activity as categories it handles. That is, if you include
+ * the categories {@link #CATEGORY_LAUNCHER} and
+ * {@link #CATEGORY_ALTERNATIVE}, then you will only resolve to components
+ * with an intent that lists <em>both</em> of those categories.
+ * Activities will very often need to support the
+ * {@link #CATEGORY_DEFAULT} so that they can be found by
+ * {@link Context#startActivity Context.startActivity()}.</p>
+ * </ul>
+ *
+ * <p>For example, consider the Note Pad sample application that
+ * allows user to browse through a list of notes data and view details about
+ * individual items. Text in italics indicate places were you would replace a
+ * name with one specific to your own package.</p>
+ *
+ * <pre> &lt;manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ * package="<i>com.android.notepad</i>"&gt;
+ * &lt;application android:icon="@drawable/app_notes"
+ * android:label="@string/app_name"&gt;
+ *
+ * &lt;provider class=".NotePadProvider"
+ * android:authorities="<i>com.google.provider.NotePad</i>" /&gt;
+ *
+ * &lt;activity class=".NotesList" android:label="@string/title_notes_list"&gt;
+ * &lt;intent-filter&gt;
+ * &lt;action android:value="android.intent.action.MAIN" /&gt;
+ * &lt;category android:value="android.intent.category.LAUNCHER" /&gt;
+ * &lt;/intent-filter&gt;
+ * &lt;intent-filter&gt;
+ * &lt;action android:value="android.intent.action.VIEW" /&gt;
+ * &lt;action android:value="android.intent.action.EDIT" /&gt;
+ * &lt;action android:value="android.intent.action.PICK" /&gt;
+ * &lt;category android:value="android.intent.category.DEFAULT" /&gt;
+ * &lt;type android:value="vnd.android.cursor.dir/<i>vnd.google.note</i>" /&gt;
+ * &lt;/intent-filter&gt;
+ * &lt;intent-filter&gt;
+ * &lt;action android:value="android.intent.action.GET_CONTENT" /&gt;
+ * &lt;category android:value="android.intent.category.DEFAULT" /&gt;
+ * &lt;type android:value="vnd.android.cursor.item/<i>vnd.google.note</i>" /&gt;
+ * &lt;/intent-filter&gt;
+ * &lt;/activity&gt;
+ *
+ * &lt;activity class=".NoteEditor" android:label="@string/title_note"&gt;
+ * &lt;intent-filter android:label="@string/resolve_edit"&gt;
+ * &lt;action android:value="android.intent.action.VIEW" /&gt;
+ * &lt;action android:value="android.intent.action.EDIT" /&gt;
+ * &lt;category android:value="android.intent.category.DEFAULT" /&gt;
+ * &lt;type android:value="vnd.android.cursor.item/<i>vnd.google.note</i>" /&gt;
+ * &lt;/intent-filter&gt;
+ *
+ * &lt;intent-filter&gt;
+ * &lt;action android:value="android.intent.action.INSERT" /&gt;
+ * &lt;category android:value="android.intent.category.DEFAULT" /&gt;
+ * &lt;type android:value="vnd.android.cursor.dir/<i>vnd.google.note</i>" /&gt;
+ * &lt;/intent-filter&gt;
+ *
+ * &lt;/activity&gt;
+ *
+ * &lt;activity class=".TitleEditor" android:label="@string/title_edit_title"
+ * android:theme="@android:style/Theme.Dialog"&gt;
+ * &lt;intent-filter android:label="@string/resolve_title"&gt;
+ * &lt;action android:value="<i>com.android.notepad.action.EDIT_TITLE</i>" /&gt;
+ * &lt;category android:value="android.intent.category.DEFAULT" /&gt;
+ * &lt;category android:value="android.intent.category.ALTERNATIVE" /&gt;
+ * &lt;category android:value="android.intent.category.SELECTED_ALTERNATIVE" /&gt;
+ * &lt;type android:value="vnd.android.cursor.item/<i>vnd.google.note</i>" /&gt;
+ * &lt;/intent-filter&gt;
+ * &lt;/activity&gt;
+ *
+ * &lt;/application&gt;
+ * &lt;/manifest&gt;</pre>
+ *
+ * <p>The first activity,
+ * <code>com.android.notepad.NotesList</code>, serves as our main
+ * entry into the app. It can do three things as described by its three intent
+ * templates:
+ * <ol>
+ * <li><pre>
+ * &lt;intent-filter&gt;
+ * &lt;action android:value="{@link #ACTION_MAIN android.intent.action.MAIN}" /&gt;
+ * &lt;category android:value="{@link #CATEGORY_LAUNCHER android.intent.category.LAUNCHER}" /&gt;
+ * &lt;/intent-filter&gt;</pre>
+ * <p>This provides a top-level entry into the NotePad application: the standard
+ * MAIN action is a main entry point (not requiring any other information in
+ * the Intent), and the LAUNCHER category says that this entry point should be
+ * listed in the application launcher.</p>
+ * <li><pre>
+ * &lt;intent-filter&gt;
+ * &lt;action android:value="{@link #ACTION_VIEW android.intent.action.VIEW}" /&gt;
+ * &lt;action android:value="{@link #ACTION_EDIT android.intent.action.EDIT}" /&gt;
+ * &lt;action android:value="{@link #ACTION_PICK android.intent.action.PICK}" /&gt;
+ * &lt;category android:value="{@link #CATEGORY_DEFAULT android.intent.category.DEFAULT}" /&gt;
+ * &lt;type android:value="vnd.android.cursor.dir/<i>vnd.google.note</i>" /&gt;
+ * &lt;/intent-filter&gt;</pre>
+ * <p>This declares the things that the activity can do on a directory of
+ * notes. The type being supported is given with the &lt;type&gt; tag, where
+ * <code>vnd.android.cursor.dir/vnd.google.note</code> is a URI from which
+ * a Cursor of zero or more items (<code>vnd.android.cursor.dir</code>) can
+ * be retrieved which holds our note pad data (<code>vnd.google.note</code>).
+ * The activity allows the user to view or edit the directory of data (via
+ * the VIEW and EDIT actions), or to pick a particular note and return it
+ * to the caller (via the PICK action). Note also the DEFAULT category
+ * supplied here: this is <em>required</em> for the
+ * {@link Context#startActivity Context.startActivity} method to resolve your
+ * activity when its component name is not explicitly specified.</p>
+ * <li><pre>
+ * &lt;intent-filter&gt;
+ * &lt;action android:value="{@link #ACTION_GET_CONTENT android.intent.action.GET_CONTENT}" /&gt;
+ * &lt;category android:value="{@link #CATEGORY_DEFAULT android.intent.category.DEFAULT}" /&gt;
+ * &lt;type android:value="vnd.android.cursor.item/<i>vnd.google.note</i>" /&gt;
+ * &lt;/intent-filter&gt;</pre>
+ * <p>This filter describes the ability return to the caller a note selected by
+ * the user without needing to know where it came from. The data type
+ * <code>vnd.android.cursor.item/vnd.google.note</code> is a URI from which
+ * a Cursor of exactly one (<code>vnd.android.cursor.item</code>) item can
+ * be retrieved which contains our note pad data (<code>vnd.google.note</code>).
+ * The GET_CONTENT action is similar to the PICK action, where the activity
+ * will return to its caller a piece of data selected by the user. Here,
+ * however, the caller specifies the type of data they desire instead of
+ * the type of data the user will be picking from.</p>
+ * </ol>
+ *
+ * <p>Given these capabilities, the following intents will resolve to the
+ * NotesList activity:</p>
+ *
+ * <ul>
+ * <li> <p><b>{ action=android.app.action.MAIN }</b> matches all of the
+ * activities that can be used as top-level entry points into an
+ * application.</p>
+ * <li> <p><b>{ action=android.app.action.MAIN,
+ * category=android.app.category.LAUNCHER }</b> is the actual intent
+ * used by the Launcher to populate its top-level list.</p>
+ * <li> <p><b>{ action=android.app.action.VIEW
+ * data=content://com.google.provider.NotePad/notes }</b>
+ * displays a list of all the notes under
+ * "content://com.google.provider.NotePad/notes", which
+ * the user can browse through and see the details on.</p>
+ * <li> <p><b>{ action=android.app.action.PICK
+ * data=content://com.google.provider.NotePad/notes }</b>
+ * provides a list of the notes under
+ * "content://com.google.provider.NotePad/notes", from which
+ * the user can pick a note whose data URL is returned back to the caller.</p>
+ * <li> <p><b>{ action=android.app.action.GET_CONTENT
+ * type=vnd.android.cursor.item/vnd.google.note }</b>
+ * is similar to the pick action, but allows the caller to specify the
+ * kind of data they want back so that the system can find the appropriate
+ * activity to pick something of that data type.</p>
+ * </ul>
+ *
+ * <p>The second activity,
+ * <code>com.android.notepad.NoteEditor</code>, shows the user a single
+ * note entry and allows them to edit it. It can do two things as described
+ * by its two intent templates:
+ * <ol>
+ * <li><pre>
+ * &lt;intent-filter android:label="@string/resolve_edit"&gt;
+ * &lt;action android:value="{@link #ACTION_VIEW android.intent.action.VIEW}" /&gt;
+ * &lt;action android:value="{@link #ACTION_EDIT android.intent.action.EDIT}" /&gt;
+ * &lt;category android:value="{@link #CATEGORY_DEFAULT android.intent.category.DEFAULT}" /&gt;
+ * &lt;type android:value="vnd.android.cursor.item/<i>vnd.google.note</i>" /&gt;
+ * &lt;/intent-filter&gt;</pre>
+ * <p>The first, primary, purpose of this activity is to let the user interact
+ * with a single note, as decribed by the MIME type
+ * <code>vnd.android.cursor.item/vnd.google.note</code>. The activity can
+ * either VIEW a note or allow the user to EDIT it. Again we support the
+ * DEFAULT category to allow the activity to be launched without explicitly
+ * specifying its component.</p>
+ * <li><pre>
+ * &lt;intent-filter&gt;
+ * &lt;action android:value="{@link #ACTION_INSERT android.intent.action.INSERT}" /&gt;
+ * &lt;category android:value="{@link #CATEGORY_DEFAULT android.intent.category.DEFAULT}" /&gt;
+ * &lt;type android:value="vnd.android.cursor.dir/<i>vnd.google.note</i>" /&gt;
+ * &lt;/intent-filter&gt;</pre>
+ * <p>The secondary use of this activity is to insert a new note entry into
+ * an existing directory of notes. This is used when the user creates a new
+ * note: the INSERT action is executed on the directory of notes, causing
+ * this activity to run and have the user create the new note data which
+ * it then adds to the content provider.</p>
+ * </ol>
+ *
+ * <p>Given these capabilities, the following intents will resolve to the
+ * NoteEditor activity:</p>
+ *
+ * <ul>
+ * <li> <p><b>{ action=android.app.action.VIEW
+ * data=content://com.google.provider.NotePad/notes/<var>{ID}</var> }</b>
+ * shows the user the content of note <var>{ID}</var>.</p>
+ * <li> <p><b>{ action=android.app.action.EDIT
+ * data=content://com.google.provider.NotePad/notes/<var>{ID}</var> }</b>
+ * allows the user to edit the content of note <var>{ID}</var>.</p>
+ * <li> <p><b>{ action=android.app.action.INSERT
+ * data=content://com.google.provider.NotePad/notes }</b>
+ * creates a new, empty note in the notes list at
+ * "content://com.google.provider.NotePad/notes"
+ * and allows the user to edit it. If they keep their changes, the URI
+ * of the newly created note is returned to the caller.</p>
+ * </ul>
+ *
+ * <p>The last activity,
+ * <code>com.android.notepad.TitleEditor</code>, allows the user to
+ * edit the title of a note. This could be implemented as a class that the
+ * application directly invokes (by explicitly setting its component in
+ * the Intent), but here we show a way you can publish alternative
+ * operations on existing data:</p>
+ *
+ * <pre>
+ * &lt;intent-filter android:label="@string/resolve_title"&gt;
+ * &lt;action android:value="<i>com.android.notepad.action.EDIT_TITLE</i>" /&gt;
+ * &lt;category android:value="{@link #CATEGORY_DEFAULT android.intent.category.DEFAULT}" /&gt;
+ * &lt;category android:value="{@link #CATEGORY_ALTERNATIVE android.intent.category.ALTERNATIVE}" /&gt;
+ * &lt;category android:value="{@link #CATEGORY_SELECTED_ALTERNATIVE android.intent.category.SELECTED_ALTERNATIVE}" /&gt;
+ * &lt;type android:value="vnd.android.cursor.item/<i>vnd.google.note</i>" /&gt;
+ * &lt;/intent-filter&gt;</pre>
+ *
+ * <p>In the single intent template here, we
+ * have created our own private action called
+ * <code>com.android.notepad.action.EDIT_TITLE</code> which means to
+ * edit the title of a note. It must be invoked on a specific note
+ * (data type <code>vnd.android.cursor.item/vnd.google.note</code>) like the previous
+ * view and edit actions, but here displays and edits the title contained
+ * in the note data.
+ *
+ * <p>In addition to supporting the default category as usual, our title editor
+ * also supports two other standard categories: ALTERNATIVE and
+ * SELECTED_ALTERNATIVE. Implementing
+ * these categories allows others to find the special action it provides
+ * without directly knowing about it, through the
+ * {@link android.content.pm.PackageManager#queryIntentActivityOptions} method, or
+ * more often to build dynamic menu items with
+ * {@link android.view.Menu#addIntentOptions}. Note that in the intent
+ * template here was also supply an explicit name for the template
+ * (via <code>android:label="@string/resolve_title"</code>) to better control
+ * what the user sees when presented with this activity as an alternative
+ * action to the data they are viewing.
+ *
+ * <p>Given these capabilities, the following intent will resolve to the
+ * TitleEditor activity:</p>
+ *
+ * <ul>
+ * <li> <p><b>{ action=com.android.notepad.action.EDIT_TITLE
+ * data=content://com.google.provider.NotePad/notes/<var>{ID}</var> }</b>
+ * displays and allows the user to edit the title associated
+ * with note <var>{ID}</var>.</p>
+ * </ul>
+ *
+ * <h3>Standard Activity Actions</h3>
+ *
+ * <p>These are the current standard actions that Intent defines for launching
+ * activities (usually through {@link Context#startActivity}. The most
+ * important, and by far most frequently used, are {@link #ACTION_MAIN} and
+ * {@link #ACTION_EDIT}.
+ *
+ * <ul>
+ * <li> {@link #ACTION_MAIN}
+ * <li> {@link #ACTION_VIEW}
+ * <li> {@link #ACTION_ATTACH_DATA}
+ * <li> {@link #ACTION_EDIT}
+ * <li> {@link #ACTION_PICK}
+ * <li> {@link #ACTION_CHOOSER}
+ * <li> {@link #ACTION_GET_CONTENT}
+ * <li> {@link #ACTION_DIAL}
+ * <li> {@link #ACTION_CALL}
+ * <li> {@link #ACTION_SEND}
+ * <li> {@link #ACTION_SENDTO}
+ * <li> {@link #ACTION_ANSWER}
+ * <li> {@link #ACTION_INSERT}
+ * <li> {@link #ACTION_DELETE}
+ * <li> {@link #ACTION_RUN}
+ * <li> {@link #ACTION_SYNC}
+ * <li> {@link #ACTION_PICK_ACTIVITY}
+ * <li> {@link #ACTION_SEARCH}
+ * <li> {@link #ACTION_WEB_SEARCH}
+ * <li> {@link #ACTION_FACTORY_TEST}
+ * </ul>
+ *
+ * <h3>Standard Broadcast Actions</h3>
+ *
+ * <p>These are the current standard actions that Intent defines for receiving
+ * broadcasts (usually through {@link Context#registerReceiver} or a
+ * &lt;receiver&gt; tag in a manifest).
+ *
+ * <ul>
+ * <li> {@link #ACTION_TIME_TICK}
+ * <li> {@link #ACTION_TIME_CHANGED}
+ * <li> {@link #ACTION_TIMEZONE_CHANGED}
+ * <li> {@link #ACTION_BOOT_COMPLETED}
+ * <li> {@link #ACTION_PACKAGE_ADDED}
+ * <li> {@link #ACTION_PACKAGE_CHANGED}
+ * <li> {@link #ACTION_PACKAGE_REMOVED}
+ * <li> {@link #ACTION_UID_REMOVED}
+ * <li> {@link #ACTION_BATTERY_CHANGED}
+ * </ul>
+ *
+ * <h3>Standard Categories</h3>
+ *
+ * <p>These are the current standard categories that can be used to further
+ * clarify an Intent via {@link #addCategory}.
+ *
+ * <ul>
+ * <li> {@link #CATEGORY_DEFAULT}
+ * <li> {@link #CATEGORY_BROWSABLE}
+ * <li> {@link #CATEGORY_TAB}
+ * <li> {@link #CATEGORY_ALTERNATIVE}
+ * <li> {@link #CATEGORY_SELECTED_ALTERNATIVE}
+ * <li> {@link #CATEGORY_LAUNCHER}
+ * <li> {@link #CATEGORY_HOME}
+ * <li> {@link #CATEGORY_PREFERENCE}
+ * <li> {@link #CATEGORY_GADGET}
+ * <li> {@link #CATEGORY_TEST}
+ * </ul>
+ *
+ * <h3>Standard Extra Data</h3>
+ *
+ * <p>These are the current standard fields that can be used as extra data via
+ * {@link #putExtra}.
+ *
+ * <ul>
+ * <li> {@link #EXTRA_TEMPLATE}
+ * <li> {@link #EXTRA_INTENT}
+ * <li> {@link #EXTRA_STREAM}
+ * <li> {@link #EXTRA_TEXT}
+ * </ul>
+ *
+ * <h3>Flags</h3>
+ *
+ * <p>These are the possible flags that can be used in the Intent via
+ * {@link #setFlags} and {@link #addFlags}.
+ *
+ * <ul>
+ * <li> {@link #FLAG_GRANT_READ_URI_PERMISSION}
+ * <li> {@link #FLAG_GRANT_WRITE_URI_PERMISSION}
+ * <li> {@link #FLAG_FROM_BACKGROUND}
+ * <li> {@link #FLAG_DEBUG_LOG_RESOLUTION}
+ * <li> {@link #FLAG_ACTIVITY_NO_HISTORY}
+ * <li> {@link #FLAG_ACTIVITY_SINGLE_TOP}
+ * <li> {@link #FLAG_ACTIVITY_NEW_TASK}
+ * <li> {@link #FLAG_ACTIVITY_MULTIPLE_TASK}
+ * <li> {@link #FLAG_ACTIVITY_FORWARD_RESULT}
+ * <li> {@link #FLAG_ACTIVITY_PREVIOUS_IS_TOP}
+ * <li> {@link #FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS}
+ * <li> {@link #FLAG_ACTIVITY_BROUGHT_TO_FRONT}
+ * <li> {@link #FLAG_RECEIVER_REGISTERED_ONLY}
+ * </ul>
+ */
+public class Intent implements Parcelable {
+ // ---------------------------------------------------------------------
+ // ---------------------------------------------------------------------
+ // Standard intent activity actions (see action variable).
+
+ /**
+ * Activity Action: Start as a main entry point, does not expect to
+ * receive data.
+ * <p>Input: nothing
+ * <p>Output: nothing
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_MAIN = "android.intent.action.MAIN";
+ /**
+ * Activity Action: Display the data to the user. This is the most common
+ * action performed on data -- it is the generic action you can use on
+ * a piece of data to get the most reasonable thing to occur. For example,
+ * when used on a contacts entry it will view the entry; when used on a
+ * mailto: URI it will bring up a compose window filled with the information
+ * supplied by the URI; when used with a tel: URI it will invoke the
+ * dialer.
+ * <p>Input: {@link #getData} is URI from which to retrieve data.
+ * <p>Output: nothing.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_VIEW = "android.intent.action.VIEW";
+ /**
+ * A synonym for {@link #ACTION_VIEW}, the "standard" action that is
+ * performed on a piece of data.
+ */
+ public static final String ACTION_DEFAULT = ACTION_VIEW;
+ /**
+ * Used to indicate that some piece of data should be attached to some other
+ * place. For example, image data could be attached to a contact. It is up
+ * to the recipient to decide where the data should be attached; the intent
+ * does not specify the ultimate destination.
+ * <p>Input: {@link #getData} is URI of data to be attached.
+ * <p>Output: nothing.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_ATTACH_DATA = "android.intent.action.ATTACH_DATA";
+ /**
+ * Activity Action: Provide explicit editable access to the given data.
+ * <p>Input: {@link #getData} is URI of data to be edited.
+ * <p>Output: nothing.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_EDIT = "android.intent.action.EDIT";
+ /**
+ * Activity Action: Pick an existing item, or insert a new item, and then edit it.
+ * <p>Input: {@link #getType} is the desired MIME type of the item to create or edit.
+ * The extras can contain type specific data to pass through to the editing/creating
+ * activity.
+ * <p>Output: The URI of the item that was picked. This must be a content:
+ * URI so that any receiver can access it.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_INSERT_OR_EDIT = "android.intent.action.INSERT_OR_EDIT";
+ /**
+ * Activity Action: Pick an item from the data, returning what was selected.
+ * <p>Input: {@link #getData} is URI containing a directory of data
+ * (vnd.android.cursor.dir/*) from which to pick an item.
+ * <p>Output: The URI of the item that was picked.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_PICK = "android.intent.action.PICK";
+ /**
+ * Activity Action: Creates a shortcut.
+ * <p>Input: Nothing.
+ * <p>Output: An Intent representing the shortcut. The intent must contain three
+ * extras: SHORTCUT_INTENT (value: Intent), SHORTCUT_NAME (value: String),
+ * and SHORTCUT_ICON (value: Bitmap) or SHORTCUT_ICON_RESOURCE
+ * (value: ShortcutIconResource).
+ * @see #EXTRA_SHORTCUT_INTENT
+ * @see #EXTRA_SHORTCUT_NAME
+ * @see #EXTRA_SHORTCUT_ICON
+ * @see #EXTRA_SHORTCUT_ICON_RESOURCE
+ * @see android.content.Intent.ShortcutIconResource
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_CREATE_SHORTCUT = "android.intent.action.CREATE_SHORTCUT";
+
+ /**
+ * The name of the extra used to define the Intent of a shortcut.
+ *
+ * @see #ACTION_CREATE_SHORTCUT
+ */
+ public static final String EXTRA_SHORTCUT_INTENT = "android.intent.extra.shortcut.INTENT";
+ /**
+ * The name of the extra used to define the name of a shortcut.
+ *
+ * @see #ACTION_CREATE_SHORTCUT
+ */
+ public static final String EXTRA_SHORTCUT_NAME = "android.intent.extra.shortcut.NAME";
+ /**
+ * The name of the extra used to define the icon, as a Bitmap, of a shortcut.
+ *
+ * @see #ACTION_CREATE_SHORTCUT
+ */
+ public static final String EXTRA_SHORTCUT_ICON = "android.intent.extra.shortcut.ICON";
+ /**
+ * The name of the extra used to define the icon, as a ShortcutIconResource, of a shortcut.
+ *
+ * @see #ACTION_CREATE_SHORTCUT
+ * @see android.content.Intent.ShortcutIconResource
+ */
+ public static final String EXTRA_SHORTCUT_ICON_RESOURCE =
+ "android.intent.extra.shortcut.ICON_RESOURCE";
+
+ /**
+ * Represents a shortcut icon resource.
+ *
+ * @see Intent#ACTION_CREATE_SHORTCUT
+ * @see Intent#EXTRA_SHORTCUT_ICON_RESOURCE
+ */
+ public static class ShortcutIconResource implements Parcelable {
+ /**
+ * The package name of the application containing the icon.
+ */
+ public String packageName;
+
+ /**
+ * The resource name of the icon, including package, name and type.
+ */
+ public String resourceName;
+
+ /**
+ * Creates a new ShortcutIconResource for the specified context and resource
+ * identifier.
+ *
+ * @param context The context of the application.
+ * @param resourceId The resource idenfitier for the icon.
+ * @return A new ShortcutIconResource with the specified's context package name
+ * and icon resource idenfitier.
+ */
+ public static ShortcutIconResource fromContext(Context context, int resourceId) {
+ ShortcutIconResource icon = new ShortcutIconResource();
+ icon.packageName = context.getPackageName();
+ icon.resourceName = context.getResources().getResourceName(resourceId);
+ return icon;
+ }
+
+ /**
+ * Used to read a ShortcutIconResource from a Parcel.
+ */
+ public static final Parcelable.Creator<ShortcutIconResource> CREATOR =
+ new Parcelable.Creator<ShortcutIconResource>() {
+
+ public ShortcutIconResource createFromParcel(Parcel source) {
+ ShortcutIconResource icon = new ShortcutIconResource();
+ icon.packageName = source.readString();
+ icon.resourceName = source.readString();
+ return icon;
+ }
+
+ public ShortcutIconResource[] newArray(int size) {
+ return new ShortcutIconResource[size];
+ }
+ };
+
+ /**
+ * No special parcel contents.
+ */
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(packageName);
+ dest.writeString(resourceName);
+ }
+
+ @Override
+ public String toString() {
+ return resourceName;
+ }
+ }
+
+ /**
+ * Activity Action: Display an activity chooser, allowing the user to pick
+ * what they want to before proceeding. This can be used as an alternative
+ * to the standard activity picker that is displayed by the system when
+ * you try to start an activity with multiple possible matches, with these
+ * differences in behavior:
+ * <ul>
+ * <li>You can specify the title that will appear in the activity chooser.
+ * <li>The user does not have the option to make one of the matching
+ * activities a preferred activity, and all possible activities will
+ * always be shown even if one of them is currently marked as the
+ * preferred activity.
+ * </ul>
+ * <p>
+ * This action should be used when the user will naturally expect to
+ * select an activity in order to proceed. An example if when not to use
+ * it is when the user clicks on a "mailto:" link. They would naturally
+ * expect to go directly to their mail app, so startActivity() should be
+ * called directly: it will
+ * either launch the current preferred app, or put up a dialog allowing the
+ * user to pick an app to use and optionally marking that as preferred.
+ * <p>
+ * In contrast, if the user is selecting a menu item to send a picture
+ * they are viewing to someone else, there are many different things they
+ * may want to do at this point: send it through e-mail, upload it to a
+ * web service, etc. In this case the CHOOSER action should be used, to
+ * always present to the user a list of the things they can do, with a
+ * nice title given by the caller such as "Send this photo with:".
+ * <p>
+ * As a convenience, an Intent of this form can be created with the
+ * {@link #createChooser} function.
+ * <p>Input: No data should be specified. get*Extra must have
+ * a {@link #EXTRA_INTENT} field containing the Intent being executed,
+ * and can optionally have a {@link #EXTRA_TITLE} field containing the
+ * title text to display in the chooser.
+ * <p>Output: Depends on the protocol of {@link #EXTRA_INTENT}.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_CHOOSER = "android.intent.action.CHOOSER";
+
+ /**
+ * Convenience function for creating a {@link #ACTION_CHOOSER} Intent.
+ *
+ * @param target The Intent that the user will be selecting an activity
+ * to perform.
+ * @param title Optional title that will be displayed in the chooser.
+ * @return Return a new Intent object that you can hand to
+ * {@link Context#startActivity(Intent) Context.startActivity()} and
+ * related methods.
+ */
+ public static Intent createChooser(Intent target, CharSequence title) {
+ Intent intent = new Intent(ACTION_CHOOSER);
+ intent.putExtra(EXTRA_INTENT, target);
+ if (title != null) {
+ intent.putExtra(EXTRA_TITLE, title);
+ }
+ return intent;
+ }
+ /**
+ * Activity Action: Allow the user to select a particular kind of data and
+ * return it. This is different than {@link #ACTION_PICK} in that here we
+ * just say what kind of data is desired, not a URI of existing data from
+ * which the user can pick. A ACTION_GET_CONTENT could allow the user to
+ * create the data as it runs (for example taking a picture or recording a
+ * sound), let them browser over the web and download the desired data,
+ * etc.
+ * <p>
+ * There are two main ways to use this action: if you want an specific kind
+ * of data, such as a person contact, you set the MIME type to the kind of
+ * data you want and launch it with {@link Context#startActivity(Intent)}.
+ * The system will then launch the best application to select that kind
+ * of data for you.
+ * <p>
+ * You may also be interested in any of a set of types of content the user
+ * can pick. For example, an e-mail application that wants to allow the
+ * user to add an attachment to an e-mail message can use this action to
+ * bring up a list of all of the types of content the user can attach.
+ * <p>
+ * In this case, you should wrap the GET_CONTENT intent with a chooser
+ * (through {@link #createChooser}), which will give the proper interface
+ * for the user to pick how to send your data and allow you to specify
+ * a prompt indicating what they are doing. You will usually specify a
+ * broad MIME type (such as image/* or {@literal *}/*), resulting in a
+ * broad range of content types the user can select from.
+ * <p>
+ * When using such a broad GET_CONTENT action, it is often desireable to
+ * only pick from data that can be represented as a stream. This is
+ * accomplished by requiring the {@link #CATEGORY_OPENABLE} in the Intent.
+ * <p>
+ * Input: {@link #getType} is the desired MIME type to retrieve. Note
+ * that no URI is supplied in the intent, as there are no constraints on
+ * where the returned data originally comes from. You may also include the
+ * {@link #CATEGORY_OPENABLE} if you can only accept data that can be
+ * opened as a stream.
+ * <p>
+ * Output: The URI of the item that was picked. This must be a content:
+ * URI so that any receiver can access it.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_GET_CONTENT = "android.intent.action.GET_CONTENT";
+ /**
+ * Activity Action: Dial a number as specified by the data. This shows a
+ * UI with the number being dialed, allowing the user to explicitly
+ * initiate the call.
+ * <p>Input: If nothing, an empty dialer is started; else {@link #getData}
+ * is URI of a phone number to be dialed or a tel: URI of an explicit phone
+ * number.
+ * <p>Output: nothing.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_DIAL = "android.intent.action.DIAL";
+ /**
+ * Activity Action: Perform a call to someone specified by the data.
+ * <p>Input: If nothing, an empty dialer is started; else {@link #getData}
+ * is URI of a phone number to be dialed or a tel: URI of an explicit phone
+ * number.
+ * <p>Output: nothing.
+ *
+ * <p>Note: there will be restrictions on which applications can initiate a
+ * call; most applications should use the {@link #ACTION_DIAL}.
+ * <p>Note: this Intent <strong>cannot</strong> be used to call emergency
+ * numbers. Applications can <strong>dial</strong> emergency numbers using
+ * {@link #ACTION_DIAL}, however.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_CALL = "android.intent.action.CALL";
+ /**
+ * Activity Action: Perform a call to an emergency number specified by the
+ * data.
+ * <p>Input: {@link #getData} is URI of a phone number to be dialed or a
+ * tel: URI of an explicit phone number.
+ * <p>Output: nothing.
+ * @hide
+ */
+ public static final String ACTION_CALL_EMERGENCY = "android.intent.action.CALL_EMERGENCY";
+ /**
+ * Activity action: Perform a call to any number (emergency or not)
+ * specified by the data.
+ * <p>Input: {@link #getData} is URI of a phone number to be dialed or a
+ * tel: URI of an explicit phone number.
+ * <p>Output: nothing.
+ * @hide
+ */
+ public static final String ACTION_CALL_PRIVILEGED = "android.intent.action.CALL_PRIVILEGED";
+ /**
+ * Activity Action: Send a message to someone specified by the data.
+ * <p>Input: {@link #getData} is URI describing the target.
+ * <p>Output: nothing.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_SENDTO = "android.intent.action.SENDTO";
+ /**
+ * Activity Action: Deliver some data to someone else. Who the data is
+ * being delivered to is not specified; it is up to the receiver of this
+ * action to ask the user where the data should be sent.
+ * <p>
+ * When launching a SEND intent, you should usually wrap it in a chooser
+ * (through {@link #createChooser}), which will give the proper interface
+ * for the user to pick how to send your data and allow you to specify
+ * a prompt indicating what they are doing.
+ * <p>
+ * Input: {@link #getType} is the MIME type of the data being sent.
+ * get*Extra can have either a {@link #EXTRA_TEXT}
+ * or {@link #EXTRA_STREAM} field, containing the data to be sent. If
+ * using EXTRA_TEXT, the MIME type should be "text/plain"; otherwise it
+ * should be the MIME type of the data in EXTRA_STREAM. Use {@literal *}/*
+ * if the MIME type is unknown (this will only allow senders that can
+ * handle generic data streams).
+ * <p>
+ * Optional standard extras, which may be interpreted by some recipients as
+ * appropriate, are: {@link #EXTRA_EMAIL}, {@link #EXTRA_CC},
+ * {@link #EXTRA_BCC}, {@link #EXTRA_SUBJECT}.
+ * <p>
+ * Output: nothing.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_SEND = "android.intent.action.SEND";
+ /**
+ * Activity Action: Handle an incoming phone call.
+ * <p>Input: nothing.
+ * <p>Output: nothing.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_ANSWER = "android.intent.action.ANSWER";
+ /**
+ * Activity Action: Insert an empty item into the given container.
+ * <p>Input: {@link #getData} is URI of the directory (vnd.android.cursor.dir/*)
+ * in which to place the data.
+ * <p>Output: URI of the new data that was created.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_INSERT = "android.intent.action.INSERT";
+ /**
+ * Activity Action: Delete the given data from its container.
+ * <p>Input: {@link #getData} is URI of data to be deleted.
+ * <p>Output: nothing.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_DELETE = "android.intent.action.DELETE";
+ /**
+ * Activity Action: Run the data, whatever that means.
+ * <p>Input: ? (Note: this is currently specific to the test harness.)
+ * <p>Output: nothing.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_RUN = "android.intent.action.RUN";
+ /**
+ * Activity Action: Perform a data synchronization.
+ * <p>Input: ?
+ * <p>Output: ?
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_SYNC = "android.intent.action.SYNC";
+ /**
+ * Activity Action: Pick an activity given an intent, returning the class
+ * selected.
+ * <p>Input: get*Extra field {@link #EXTRA_INTENT} is an Intent
+ * used with {@link PackageManager#queryIntentActivities} to determine the
+ * set of activities from which to pick.
+ * <p>Output: Class name of the activity that was selected.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_PICK_ACTIVITY = "android.intent.action.PICK_ACTIVITY";
+ /**
+ * Activity Action: Perform a search.
+ * <p>Input: {@link android.app.SearchManager#QUERY getStringExtra(SearchManager.QUERY)}
+ * is the text to search for. If empty, simply
+ * enter your search results Activity with the search UI activated.
+ * <p>Output: nothing.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_SEARCH = "android.intent.action.SEARCH";
+ /**
+ * Activity Action: Perform a web search.
+ * <p>Input: {@link #getData} is URI of data. If it is a url
+ * starts with http or https, the site will be opened. If it is plain text,
+ * Google search will be applied.
+ * <p>Output: nothing.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_WEB_SEARCH = "android.intent.action.WEB_SEARCH";
+ /**
+ * Activity Action: List all available applications
+ * <p>Input: Nothing.
+ * <p>Output: nothing.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_ALL_APPS = "android.intent.action.ALL_APPS";
+ /**
+ * Activity Action: Show settings for choosing wallpaper
+ * <p>Input: Nothing.
+ * <p>Output: Nothing.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_SET_WALLPAPER = "android.intent.action.SET_WALLPAPER";
+
+ /**
+ * Activity Action: Show activity for reporting a bug.
+ * <p>Input: Nothing.
+ * <p>Output: Nothing.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_BUG_REPORT = "android.intent.action.BUG_REPORT";
+
+ /**
+ * Activity Action: Main entry point for factory tests. Only used when
+ * the device is booting in factory test node. The implementing package
+ * must be installed in the system image.
+ * <p>Input: nothing
+ * <p>Output: nothing
+ */
+ public static final String ACTION_FACTORY_TEST = "android.intent.action.FACTORY_TEST";
+
+ /**
+ * Activity Action: The user pressed the "call" button to go to the dialer
+ * or other appropriate UI for placing a call.
+ * <p>Input: Nothing.
+ * <p>Output: Nothing.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_CALL_BUTTON = "android.intent.action.CALL_BUTTON";
+
+ /**
+ * Activity Action: Start Voice Command.
+ * <p>Input: Nothing.
+ * <p>Output: Nothing.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_VOICE_COMMAND = "android.intent.action.VOICE_COMMAND";
+
+ // ---------------------------------------------------------------------
+ // ---------------------------------------------------------------------
+ // Standard intent broadcast actions (see action variable).
+
+ /**
+ * Broadcast Action: Sent after the screen turns off.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_SCREEN_OFF = "android.intent.action.SCREEN_OFF";
+ /**
+ * Broadcast Action: Sent after the screen turns on.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_SCREEN_ON = "android.intent.action.SCREEN_ON";
+ /**
+ * Broadcast Action: The current time has changed. Sent every
+ * minute. You can <em>not</em> receive this through components declared
+ * in manifests, only by exlicitly registering for it with
+ * {@link Context#registerReceiver(BroadcastReceiver, IntentFilter)
+ * Context.registerReceiver()}.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_TIME_TICK = "android.intent.action.TIME_TICK";
+ /**
+ * Broadcast Action: The time was set.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_TIME_CHANGED = "android.intent.action.TIME_SET";
+ /**
+ * Broadcast Action: The date has changed.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_DATE_CHANGED = "android.intent.action.DATE_CHANGED";
+ /**
+ * Broadcast Action: The timezone has changed. The intent will have the following extra values:</p>
+ * <ul>
+ * <li><em>time-zone</em> - The java.util.TimeZone.getID() value identifying the new time zone.</li>
+ * </ul>
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_TIMEZONE_CHANGED = "android.intent.action.TIMEZONE_CHANGED";
+ /**
+ * Alarm Changed Action: This is broadcast when the AlarmClock
+ * application's alarm is set or unset. It is used by the
+ * AlarmClock application and the StatusBar service.
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_ALARM_CHANGED = "android.intent.action.ALARM_CHANGED";
+ /**
+ * Sync State Changed Action: This is broadcast when the sync starts or stops or when one has
+ * been failing for a long time. It is used by the SyncManager and the StatusBar service.
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_SYNC_STATE_CHANGED
+ = "android.intent.action.SYNC_STATE_CHANGED";
+ /**
+ * Broadcast Action: This is broadcast once, after the system has finished
+ * booting. It can be used to perform application-specific initialization,
+ * such as installing alarms. You must hold the
+ * {@link android.Manifest.permission#RECEIVE_BOOT_COMPLETED} permission
+ * in order to receive this broadcast.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_BOOT_COMPLETED = "android.intent.action.BOOT_COMPLETED";
+ /**
+ * Broadcast Action: This is broadcast when a user action should request a
+ * temporary system dialog to dismiss. Some examples of temporary system
+ * dialogs are the notification window-shade and the recent tasks dialog.
+ */
+ public static final String ACTION_CLOSE_SYSTEM_DIALOGS = "android.intent.action.CLOSE_SYSTEM_DIALOGS";
+ /**
+ * Broadcast Action: Trigger the download and eventual installation
+ * of a package.
+ * <p>Input: {@link #getData} is the URI of the package file to download.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_PACKAGE_INSTALL = "android.intent.action.PACKAGE_INSTALL";
+ /**
+ * Broadcast Action: A new application package has been installed on the
+ * device. The data contains the name of the package.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_PACKAGE_ADDED = "android.intent.action.PACKAGE_ADDED";
+ /**
+ * Broadcast Action: An existing application package has been removed from
+ * the device. The data contains the name of the package. The package
+ * that is being installed does <em>not</em> receive this Intent.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_PACKAGE_REMOVED = "android.intent.action.PACKAGE_REMOVED";
+ /**
+ * Broadcast Action: An existing application package has been changed (e.g. a component has been
+ * enabled or disabled. The data contains the name of the package.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_PACKAGE_CHANGED = "android.intent.action.PACKAGE_CHANGED";
+ /**
+ * Broadcast Action: The user has restarted a package, all runtime state
+ * associated with it (processes, alarms, notifications, etc) should
+ * be remove. The data contains the name of the package.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_PACKAGE_RESTARTED = "android.intent.action.PACKAGE_RESTARTED";
+ /**
+ * Broadcast Action: A user ID has been removed from the system. The user
+ * ID number is stored in the extra data under {@link #EXTRA_UID}.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_UID_REMOVED = "android.intent.action.UID_REMOVED";
+ /**
+ * Broadcast Action: The current system wallpaper has changed. See
+ * {@link Context#getWallpaper} for retrieving the new wallpaper.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_WALLPAPER_CHANGED = "android.intent.action.WALLPAPER_CHANGED";
+ /**
+ * Broadcast Action: The current device {@link android.content.res.Configuration}
+ * (orientation, locale, etc) has changed. When such a change happens, the
+ * UIs (view hierarchy) will need to be rebuilt based on this new
+ * information; for the most part, applications don't need to worry about
+ * this, because the system will take care of stopping and restarting the
+ * application to make sure it sees the new changes. Some system code that
+ * can not be restarted will need to watch for this action and handle it
+ * appropriately.
+ *
+ * @see android.content.res.Configuration
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_CONFIGURATION_CHANGED = "android.intent.action.CONFIGURATION_CHANGED";
+ /**
+ * Broadcast Action: The charging state, or charge level of the battery has
+ * changed.
+ *
+ * <p class="note">
+ * You can <em>not</em> receive this through components declared
+ * in manifests, only by exlicitly registering for it with
+ * {@link Context#registerReceiver(BroadcastReceiver, IntentFilter)
+ * Context.registerReceiver()}.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_BATTERY_CHANGED = "android.intent.action.BATTERY_CHANGED";
+ /**
+ * Broadcast Action: Indicates low battery condition on the device.
+ * This broadcast corresponds to the "Low battery warning" system dialog.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_BATTERY_LOW = "android.intent.action.BATTERY_LOW";
+ /**
+ * Broadcast Action: Indicates low memory condition on the device
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_DEVICE_STORAGE_LOW = "android.intent.action.DEVICE_STORAGE_LOW";
+ /**
+ * Broadcast Action: Indicates low memory condition on the device no longer exists
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_DEVICE_STORAGE_OK = "android.intent.action.DEVICE_STORAGE_OK";
+ /**
+ * Broadcast Action: Indicates low memory condition notification acknowledged by user
+ * and package management should be started.
+ * This is triggered by the user from the ACTION_DEVICE_STORAGE_LOW
+ * notification.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_MANAGE_PACKAGE_STORAGE = "android.intent.action.MANAGE_PACKAGE_STORAGE";
+ /**
+ * Broadcast Action: The device has entered USB Mass Storage mode.
+ * This is used mainly for the USB Settings panel.
+ * Apps should listen for ACTION_MEDIA_MOUNTED and ACTION_MEDIA_UNMOUNTED broadcasts to be notified
+ * when the SD card file system is mounted or unmounted
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_UMS_CONNECTED = "android.intent.action.UMS_CONNECTED";
+
+ /**
+ * Broadcast Action: The device has exited USB Mass Storage mode.
+ * This is used mainly for the USB Settings panel.
+ * Apps should listen for ACTION_MEDIA_MOUNTED and ACTION_MEDIA_UNMOUNTED broadcasts to be notified
+ * when the SD card file system is mounted or unmounted
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_UMS_DISCONNECTED = "android.intent.action.UMS_DISCONNECTED";
+
+ /**
+ * Broadcast Action: External media has been removed.
+ * The path to the mount point for the removed media is contained in the Intent.mData field.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_MEDIA_REMOVED = "android.intent.action.MEDIA_REMOVED";
+
+ /**
+ * Broadcast Action: External media is present, but not mounted at its mount point.
+ * The path to the mount point for the removed media is contained in the Intent.mData field.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_MEDIA_UNMOUNTED = "android.intent.action.MEDIA_UNMOUNTED";
+
+ /**
+ * Broadcast Action: External media is present and mounted at its mount point.
+ * The path to the mount point for the removed media is contained in the Intent.mData field.
+ * The Intent contains an extra with name "read-only" and Boolean value to indicate if the
+ * media was mounted read only.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_MEDIA_MOUNTED = "android.intent.action.MEDIA_MOUNTED";
+
+ /**
+ * Broadcast Action: External media is unmounted because it is being shared via USB mass storage.
+ * The path to the mount point for the removed media is contained in the Intent.mData field.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_MEDIA_SHARED = "android.intent.action.MEDIA_SHARED";
+
+ /**
+ * Broadcast Action: External media was removed from SD card slot, but mount point was not unmounted.
+ * The path to the mount point for the removed media is contained in the Intent.mData field.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_MEDIA_BAD_REMOVAL = "android.intent.action.MEDIA_BAD_REMOVAL";
+
+ /**
+ * Broadcast Action: External media is present but cannot be mounted.
+ * The path to the mount point for the removed media is contained in the Intent.mData field.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_MEDIA_UNMOUNTABLE = "android.intent.action.MEDIA_UNMOUNTABLE";
+
+ /**
+ * Broadcast Action: User has expressed the desire to remove the external storage media.
+ * Applications should close all files they have open within the mount point when they receive this intent.
+ * The path to the mount point for the media to be ejected is contained in the Intent.mData field.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_MEDIA_EJECT = "android.intent.action.MEDIA_EJECT";
+
+ /**
+ * Broadcast Action: The media scanner has started scanning a directory.
+ * The path to the directory being scanned is contained in the Intent.mData field.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_MEDIA_SCANNER_STARTED = "android.intent.action.MEDIA_SCANNER_STARTED";
+
+ /**
+ * Broadcast Action: The media scanner has finished scanning a directory.
+ * The path to the scanned directory is contained in the Intent.mData field.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_MEDIA_SCANNER_FINISHED = "android.intent.action.MEDIA_SCANNER_FINISHED";
+
+ /**
+ * Broadcast Action: Request the media scanner to scan a file and add it to the media database.
+ * The path to the file is contained in the Intent.mData field.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_MEDIA_SCANNER_SCAN_FILE = "android.intent.action.MEDIA_SCANNER_SCAN_FILE";
+
+ /**
+ * Broadcast Action: The "Media Button" was pressed. Includes a single
+ * extra field, {@link #EXTRA_KEY_EVENT}, containing the key event that
+ * caused the broadcast.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_MEDIA_BUTTON = "android.intent.action.MEDIA_BUTTON";
+
+ /**
+ * Broadcast Action: The "Camera Button" was pressed. Includes a single
+ * extra field, {@link #EXTRA_KEY_EVENT}, containing the key event that
+ * caused the broadcast.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_CAMERA_BUTTON = "android.intent.action.CAMERA_BUTTON";
+
+ // *** NOTE: @todo(*) The following really should go into a more domain-specific
+ // location; they are not general-purpose actions.
+
+ /**
+ * Broadcast Action: An GTalk connection has been established.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_GTALK_SERVICE_CONNECTED =
+ "android.intent.action.GTALK_CONNECTED";
+
+ /**
+ * Broadcast Action: An GTalk connection has been disconnected.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_GTALK_SERVICE_DISCONNECTED =
+ "android.intent.action.GTALK_DISCONNECTED";
+
+ /**
+ * <p>Broadcast Action: The user has switched the phone into or out of Airplane Mode. One or
+ * more radios have been turned off or on. The intent will have the following extra value:</p>
+ * <ul>
+ * <li><em>state</em> - A boolean value indicating whether Airplane Mode is on. If true,
+ * then cell radio and possibly other radios such as bluetooth or WiFi may have also been
+ * turned off</li>
+ * </ul>
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_AIRPLANE_MODE_CHANGED = "android.intent.action.AIRPLANE_MODE";
+
+ /**
+ * Broadcast Action: Some content providers have parts of their namespace
+ * where they publish new events or items that the user may be especially
+ * interested in. For these things, they may broadcast this action when the
+ * set of interesting items change.
+ *
+ * For example, GmailProvider sends this notification when the set of unread
+ * mail in the inbox changes.
+ *
+ * <p>The data of the intent identifies which part of which provider
+ * changed. When queried through the content resolver, the data URI will
+ * return the data set in question.
+ *
+ * <p>The intent will have the following extra values:
+ * <ul>
+ * <li><em>count</em> - The number of items in the data set. This is the
+ * same as the number of items in the cursor returned by querying the
+ * data URI. </li>
+ * </ul>
+ *
+ * This intent will be sent at boot (if the count is non-zero) and when the
+ * data set changes. It is possible for the data set to change without the
+ * count changing (for example, if a new unread message arrives in the same
+ * sync operation in which a message is archived). The phone should still
+ * ring/vibrate/etc as normal in this case.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_PROVIDER_CHANGED =
+ "android.intent.action.PROVIDER_CHANGED";
+
+ /**
+ * Broadcast Action: Wired Headset plugged in or unplugged.
+ *
+ * <p>The intent will have the following extra values:
+ * <ul>
+ * <li><em>state</em> - 0 for unplugged, 1 for plugged. </li>
+ * <li><em>name</em> - Headset type, human readable string </li>
+ * </ul>
+ * </ul>
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_HEADSET_PLUG =
+ "android.intent.action.HEADSET_PLUG";
+
+ /**
+ * Broadcast Action: An outgoing call is about to be placed.
+ *
+ * <p>The Intent will have the following extra value:
+ * <ul>
+ * <li><em>{@link android.content.Intent#EXTRA_PHONE_NUMBER}</em> -
+ * the phone number originally intended to be dialed.</li>
+ * </ul>
+ * <p>Once the broadcast is finished, the resultData is used as the actual
+ * number to call. If <code>null</code>, no call will be placed.</p>
+ * <p>It is perfectly acceptable for multiple receivers to process the
+ * outgoing call in turn: for example, a parental control application
+ * might verify that the user is authorized to place the call at that
+ * time, then a number-rewriting application might add an area code if
+ * one was not specified.</p>
+ * <p>For consistency, any receiver whose purpose is to prohibit phone
+ * calls should have a priority of 0, to ensure it will see the final
+ * phone number to be dialed.
+ * Any receiver whose purpose is to rewrite phone numbers to be called
+ * should have a positive priority.
+ * Negative priorities are reserved for the system for this broadcast;
+ * using them may cause problems.</p>
+ * <p>Any BroadcastReceiver receiving this Intent <em>must not</em>
+ * abort the broadcast.</p>
+ * <p>Emergency calls cannot be intercepted using this mechanism, and
+ * other calls cannot be modified to call emergency numbers using this
+ * mechanism.
+ * <p>You must hold the
+ * {@link android.Manifest.permission#PROCESS_OUTGOING_CALLS}
+ * permission to receive this Intent.</p>
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_NEW_OUTGOING_CALL =
+ "android.intent.action.NEW_OUTGOING_CALL";
+
+ /**
+ * Broadcast Action: Have the device reboot. This is only for use by
+ * system code.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_REBOOT =
+ "android.intent.action.REBOOT";
+
+ // ---------------------------------------------------------------------
+ // ---------------------------------------------------------------------
+ // Standard intent categories (see addCategory()).
+
+ /**
+ * Set if the activity should be an option for the default action
+ * (center press) to perform on a piece of data. Setting this will
+ * hide from the user any activities without it set when performing an
+ * action on some data. Note that this is normal -not- set in the
+ * Intent when initiating an action -- it is for use in intent filters
+ * specified in packages.
+ */
+ @SdkConstant(SdkConstantType.INTENT_CATEGORY)
+ public static final String CATEGORY_DEFAULT = "android.intent.category.DEFAULT";
+ /**
+ * Activities that can be safely invoked from a browser must support this
+ * category. For example, if the user is viewing a web page or an e-mail
+ * and clicks on a link in the text, the Intent generated execute that
+ * link will require the BROWSABLE category, so that only activities
+ * supporting this category will be considered as possible actions. By
+ * supporting this category, you are promising that there is nothing
+ * damaging (without user intervention) that can happen by invoking any
+ * matching Intent.
+ */
+ @SdkConstant(SdkConstantType.INTENT_CATEGORY)
+ public static final String CATEGORY_BROWSABLE = "android.intent.category.BROWSABLE";
+ /**
+ * Set if the activity should be considered as an alternative action to
+ * the data the user is currently viewing. See also
+ * {@link #CATEGORY_SELECTED_ALTERNATIVE} for an alternative action that
+ * applies to the selection in a list of items.
+ *
+ * <p>Supporting this category means that you would like your activity to be
+ * displayed in the set of alternative things the user can do, usually as
+ * part of the current activity's options menu. You will usually want to
+ * include a specific label in the &lt;intent-filter&gt; of this action
+ * describing to the user what it does.
+ *
+ * <p>The action of IntentFilter with this category is important in that it
+ * describes the specific action the target will perform. This generally
+ * should not be a generic action (such as {@link #ACTION_VIEW}, but rather
+ * a specific name such as "com.android.camera.action.CROP. Only one
+ * alternative of any particular action will be shown to the user, so using
+ * a specific action like this makes sure that your alternative will be
+ * displayed while also allowing other applications to provide their own
+ * overrides of that particular action.
+ */
+ @SdkConstant(SdkConstantType.INTENT_CATEGORY)
+ public static final String CATEGORY_ALTERNATIVE = "android.intent.category.ALTERNATIVE";
+ /**
+ * Set if the activity should be considered as an alternative selection
+ * action to the data the user has currently selected. This is like
+ * {@link #CATEGORY_ALTERNATIVE}, but is used in activities showing a list
+ * of items from which the user can select, giving them alternatives to the
+ * default action that will be performed on it.
+ */
+ @SdkConstant(SdkConstantType.INTENT_CATEGORY)
+ public static final String CATEGORY_SELECTED_ALTERNATIVE = "android.intent.category.SELECTED_ALTERNATIVE";
+ /**
+ * Intended to be used as a tab inside of an containing TabActivity.
+ */
+ @SdkConstant(SdkConstantType.INTENT_CATEGORY)
+ public static final String CATEGORY_TAB = "android.intent.category.TAB";
+ /**
+ * This activity can be embedded inside of another activity that is hosting
+ * gadgets.
+ */
+ @SdkConstant(SdkConstantType.INTENT_CATEGORY)
+ public static final String CATEGORY_GADGET = "android.intent.category.GADGET";
+ /**
+ * Should be displayed in the top-level launcher.
+ */
+ @SdkConstant(SdkConstantType.INTENT_CATEGORY)
+ public static final String CATEGORY_LAUNCHER = "android.intent.category.LAUNCHER";
+ /**
+ * This is the home activity, that is the first activity that is displayed
+ * when the device boots.
+ */
+ @SdkConstant(SdkConstantType.INTENT_CATEGORY)
+ public static final String CATEGORY_HOME = "android.intent.category.HOME";
+ /**
+ * This activity is a preference panel.
+ */
+ @SdkConstant(SdkConstantType.INTENT_CATEGORY)
+ public static final String CATEGORY_PREFERENCE = "android.intent.category.PREFERENCE";
+ /**
+ * This activity is a development preference panel.
+ */
+ @SdkConstant(SdkConstantType.INTENT_CATEGORY)
+ public static final String CATEGORY_DEVELOPMENT_PREFERENCE = "android.intent.category.DEVELOPMENT_PREFERENCE";
+ /**
+ * Capable of running inside a parent activity container.
+ *
+ * <p>Note: being removed in favor of more explicit categories such as
+ * CATEGORY_GADGET
+ */
+ @SdkConstant(SdkConstantType.INTENT_CATEGORY)
+ public static final String CATEGORY_EMBED = "android.intent.category.EMBED";
+ /**
+ * This activity may be exercised by the monkey or other automated test tools.
+ */
+ @SdkConstant(SdkConstantType.INTENT_CATEGORY)
+ public static final String CATEGORY_MONKEY = "android.intent.category.MONKEY";
+ /**
+ * To be used as a test (not part of the normal user experience).
+ */
+ public static final String CATEGORY_TEST = "android.intent.category.TEST";
+ /**
+ * To be used as a unit test (run through the Test Harness).
+ */
+ public static final String CATEGORY_UNIT_TEST = "android.intent.category.UNIT_TEST";
+ /**
+ * To be used as an sample code example (not part of the normal user
+ * experience).
+ */
+ public static final String CATEGORY_SAMPLE_CODE = "android.intent.category.SAMPLE_CODE";
+ /**
+ * Used to indicate that a GET_CONTENT intent only wants URIs that can be opened with
+ * ContentResolver.openInputStream. Openable URIs must support the columns in OpenableColumns
+ * when queried, though it is allowable for those columns to be blank.
+ */
+ @SdkConstant(SdkConstantType.INTENT_CATEGORY)
+ public static final String CATEGORY_OPENABLE = "android.intent.category.OPENABLE";
+
+ /**
+ * To be used as code under test for framework instrumentation tests.
+ */
+ public static final String CATEGORY_FRAMEWORK_INSTRUMENTATION_TEST =
+ "android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST";
+ // ---------------------------------------------------------------------
+ // ---------------------------------------------------------------------
+ // Standard extra data keys.
+
+ /**
+ * The initial data to place in a newly created record. Use with
+ * {@link #ACTION_INSERT}. The data here is a Map containing the same
+ * fields as would be given to the underlying ContentProvider.insert()
+ * call.
+ */
+ public static final String EXTRA_TEMPLATE = "android.intent.extra.TEMPLATE";
+
+ /**
+ * A constant CharSequence that is associated with the Intent, used with
+ * {@link #ACTION_SEND} to supply the literal data to be sent. Note that
+ * this may be a styled CharSequence, so you must use
+ * {@link Bundle#getCharSequence(String) Bundle.getCharSequence()} to
+ * retrieve it.
+ */
+ public static final String EXTRA_TEXT = "android.intent.extra.TEXT";
+
+ /**
+ * A content: URI holding a stream of data associated with the Intent,
+ * used with {@link #ACTION_SEND} to supply the data being sent.
+ */
+ public static final String EXTRA_STREAM = "android.intent.extra.STREAM";
+
+ /**
+ * A String[] holding e-mail addresses that should be delivered to.
+ */
+ public static final String EXTRA_EMAIL = "android.intent.extra.EMAIL";
+
+ /**
+ * A String[] holding e-mail addresses that should be carbon copied.
+ */
+ public static final String EXTRA_CC = "android.intent.extra.CC";
+
+ /**
+ * A String[] holding e-mail addresses that should be blind carbon copied.
+ */
+ public static final String EXTRA_BCC = "android.intent.extra.BCC";
+
+ /**
+ * A constant string holding the desired subject line of a message.
+ */
+ public static final String EXTRA_SUBJECT = "android.intent.extra.SUBJECT";
+
+ /**
+ * An Intent describing the choices you would like shown with
+ * {@link #ACTION_PICK_ACTIVITY}.
+ */
+ public static final String EXTRA_INTENT = "android.intent.extra.INTENT";
+
+ /**
+ * A CharSequence dialog title to provide to the user when used with a
+ * {@link #ACTION_CHOOSER}.
+ */
+ public static final String EXTRA_TITLE = "android.intent.extra.TITLE";
+
+ /**
+ * A {@link android.view.KeyEvent} object containing the event that
+ * triggered the creation of the Intent it is in.
+ */
+ public static final String EXTRA_KEY_EVENT = "android.intent.extra.KEY_EVENT";
+
+ /**
+ * Used as an boolean extra field in {@link android.content.Intent#ACTION_PACKAGE_REMOVED} or
+ * {@link android.content.Intent#ACTION_PACKAGE_CHANGED} intents to override the default action
+ * of restarting the application.
+ */
+ public static final String EXTRA_DONT_KILL_APP = "android.intent.extra.DONT_KILL_APP";
+
+ /**
+ * A String holding the phone number originally entered in
+ * {@link android.content.Intent#ACTION_NEW_OUTGOING_CALL}, or the actual
+ * number to call in a {@link android.content.Intent#ACTION_CALL}.
+ */
+ public static final String EXTRA_PHONE_NUMBER = "android.intent.extra.PHONE_NUMBER";
+ /**
+ * Used as an int extra field in {@link android.content.Intent#ACTION_UID_REMOVED}
+ * intents to supply the uid the package had been assigned. Also an optional
+ * extra in {@link android.content.Intent#ACTION_PACKAGE_REMOVED} or
+ * {@link android.content.Intent#ACTION_PACKAGE_CHANGED} for the same
+ * purpose.
+ */
+ public static final String EXTRA_UID = "android.intent.extra.UID";
+
+ /**
+ * Used as an int extra field in {@link android.app.AlarmManager} intents
+ * to tell the application being invoked how many pending alarms are being
+ * delievered with the intent. For one-shot alarms this will always be 1.
+ * For recurring alarms, this might be greater than 1 if the device was
+ * asleep or powered off at the time an earlier alarm would have been
+ * delivered.
+ */
+ public static final String EXTRA_ALARM_COUNT = "android.intent.extra.ALARM_COUNT";
+
+ // ---------------------------------------------------------------------
+ // ---------------------------------------------------------------------
+ // Intent flags (see mFlags variable).
+
+ /**
+ * If set, the recipient of this Intent will be granted permission to
+ * perform read operations on the Uri in the Intent's data.
+ */
+ public static final int FLAG_GRANT_READ_URI_PERMISSION = 0x00000001;
+ /**
+ * If set, the recipient of this Intent will be granted permission to
+ * perform write operations on the Uri in the Intent's data.
+ */
+ public static final int FLAG_GRANT_WRITE_URI_PERMISSION = 0x00000002;
+ /**
+ * Can be set by the caller to indicate that this Intent is coming from
+ * a background operation, not from direct user interaction.
+ */
+ public static final int FLAG_FROM_BACKGROUND = 0x00000004;
+ /**
+ * A flag you can enable for debugging: when set, log messages will be
+ * printed during the resolution of this intent to show you what has
+ * been found to create the final resolved list.
+ */
+ public static final int FLAG_DEBUG_LOG_RESOLUTION = 0x00000008;
+
+ /**
+ * If set, the new activity is not kept in the history stack.
+ */
+ public static final int FLAG_ACTIVITY_NO_HISTORY = 0x40000000;
+ /**
+ * If set, the activity will not be launched if it is already running
+ * at the top of the history stack.
+ */
+ public static final int FLAG_ACTIVITY_SINGLE_TOP = 0x20000000;
+ /**
+ * If set, this activity will become the start of a new task on this
+ * history stack. A task (from the activity that started it to the
+ * next task activity) defines an atomic group of activities that the
+ * user can move to. Tasks can be moved to the foreground and background;
+ * all of the activities inside of a particular task always remain in
+ * the same order. See the
+ * <a href="{@docRoot}intro/appmodel.html">Application Model</a>
+ * documentation for more details on tasks.
+ *
+ * <p>This flag is generally used by activities that want
+ * to present a "launcher" style behavior: they give the user a list of
+ * separate things that can be done, which otherwise run completely
+ * independently of the activity launching them.
+ *
+ * <p>When using this flag, if a task is already running for the activity
+ * you are now starting, then a new activity will not be started; instead,
+ * the current task will simply be brought to the front of the screen with
+ * the state it was last in. See {@link #FLAG_ACTIVITY_MULTIPLE_TASK} for a flag
+ * to disable this behavior.
+ *
+ * <p>This flag can not be used when the caller is requesting a result from
+ * the activity being launched.
+ */
+ public static final int FLAG_ACTIVITY_NEW_TASK = 0x10000000;
+ /**
+ * <strong>Do not use this flag unless you are implementing your own
+ * top-level application launcher.</strong> Used in conjunction with
+ * {@link #FLAG_ACTIVITY_NEW_TASK} to disable the
+ * behavior of bringing an existing task to the foreground. When set,
+ * a new task is <em>always</em> started to host the Activity for the
+ * Intent, regardless of whether there is already an existing task running
+ * the same thing.
+ *
+ * <p><strong>Because the default system does not include graphical task management,
+ * you should not use this flag unless you provide some way for a user to
+ * return back to the tasks you have launched.</strong>
+ *
+ * <p>This flag is ignored if
+ * {@link #FLAG_ACTIVITY_NEW_TASK} is not set.
+ *
+ * <p>See the
+ * <a href="{@docRoot}intro/appmodel.html">Application Model</a>
+ * documentation for more details on tasks.
+ */
+ public static final int FLAG_ACTIVITY_MULTIPLE_TASK = 0x08000000;
+ /**
+ * If set, and the activity being launched is already running in the
+ * current task, then instead of launching a new instance of that activity,
+ * all of the other activities on top of it will be closed and this Intent
+ * will be delivered to the (now on top) old activity as a new Intent.
+ *
+ * <p>For example, consider a task consisting of the activities: A, B, C, D.
+ * If D calls startActivity() with an Intent that resolves to the component
+ * of activity B, then C and D will be finished and B receive the given
+ * Intent, resulting in the stack now being: A, B.
+ *
+ * <p>The currently running instance of task B in the above example will
+ * either receiving the new intent you are starting here in its
+ * onNewIntent() method, or be itself finished and restarting with the
+ * new intent. If it has declared its launch mode to be "multiple" (the
+ * default) it will be finished and re-created; for all other launch modes
+ * it will receive the Intent in the current instance.
+ *
+ * <p>This launch mode can also be used to good effect in conjunction with
+ * {@link #FLAG_ACTIVITY_NEW_TASK}: if used to start the root activity
+ * of a task, it will bring any currently running instance of that task
+ * to the foreground, and then clear it to its root state. This is
+ * especially useful, for example, when launching an activity from the
+ * notification manager.
+ *
+ * <p>See the
+ * <a href="{@docRoot}intro/appmodel.html">Application Model</a>
+ * documentation for more details on tasks.
+ */
+ public static final int FLAG_ACTIVITY_CLEAR_TOP = 0x04000000;
+ /**
+ * If set and this intent is being used to launch a new activity from an
+ * existing one, then the reply target of the existing activity will be
+ * transfered to the new activity. This way the new activity can call
+ * {@link android.app.Activity#setResult} and have that result sent back to
+ * the reply target of the original activity.
+ */
+ public static final int FLAG_ACTIVITY_FORWARD_RESULT = 0x02000000;
+ /**
+ * If set and this intent is being used to launch a new activity from an
+ * existing one, the current activity will not be counted as the top
+ * activity for deciding whether the new intent should be delivered to
+ * the top instead of starting a new one. The previous activity will
+ * be used as the top, with the assumption being that the current activity
+ * will finish itself immediately.
+ */
+ public static final int FLAG_ACTIVITY_PREVIOUS_IS_TOP = 0x01000000;
+ /**
+ * If set, the new activity is not kept in the list of recently launched
+ * activities.
+ */
+ public static final int FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS = 0x00800000;
+ /**
+ * This flag is not normally set by application code, but set for you by
+ * the system as described in the
+ * {@link android.R.styleable#AndroidManifestActivity_launchMode
+ * launchMode} documentation for the singleTask mode.
+ */
+ public static final int FLAG_ACTIVITY_BROUGHT_TO_FRONT = 0x00400000;
+ /**
+ * If set, and this activity is either being started in a new task or
+ * bringing to the top an existing task, then it will be launched as
+ * the front door of the task. This will result in the application of
+ * any affinities needed to have that task in the proper state (either
+ * moving activities to or from it), or simply resetting that task to
+ * its initial state if needed.
+ */
+ public static final int FLAG_ACTIVITY_RESET_TASK_IF_NEEDED = 0x00200000;
+ /**
+ * If set, this activity is being launched from history (longpress home key).
+ */
+ public static final int FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY = 0x00100000;
+
+ /**
+ * If set, when sending a broadcast only registered receivers will be
+ * called -- no BroadcastReceiver components will be launched.
+ */
+ public static final int FLAG_RECEIVER_REGISTERED_ONLY = 0x40000000;
+
+ // ---------------------------------------------------------------------
+
+ private String mAction;
+ private Uri mData;
+ private String mType;
+ private ComponentName mComponent;
+ private int mFlags;
+ private HashSet<String> mCategories;
+ private Bundle mExtras;
+
+ // ---------------------------------------------------------------------
+
+ /**
+ * Create an empty intent.
+ */
+ public Intent() {
+ }
+
+ /**
+ * Copy constructor.
+ */
+ public Intent(Intent o) {
+ this.mAction = o.mAction;
+ this.mData = o.mData;
+ this.mType = o.mType;
+ this.mComponent = o.mComponent;
+ this.mFlags = o.mFlags;
+ if (o.mCategories != null) {
+ this.mCategories = new HashSet<String>(o.mCategories);
+ }
+ if (o.mExtras != null) {
+ this.mExtras = new Bundle(o.mExtras);
+ }
+ }
+
+ @Override
+ public Object clone() {
+ return new Intent(this);
+ }
+
+ private Intent(Intent o, boolean all) {
+ this.mAction = o.mAction;
+ this.mData = o.mData;
+ this.mType = o.mType;
+ this.mComponent = o.mComponent;
+ if (o.mCategories != null) {
+ this.mCategories = new HashSet<String>(o.mCategories);
+ }
+ }
+
+ /**
+ * Make a clone of only the parts of the Intent that are relevant for
+ * filter matching: the action, data, type, component, and categories.
+ */
+ public Intent cloneFilter() {
+ return new Intent(this, false);
+ }
+
+ /**
+ * Create an intent with a given action. All other fields (data, type,
+ * class) are null. Note that the action <em>must</em> be in a
+ * namespace because Intents are used globally in the system -- for
+ * example the system VIEW action is android.intent.action.VIEW; an
+ * application's custom action would be something like
+ * com.google.app.myapp.CUSTOM_ACTION.
+ *
+ * @param action The Intent action, such as ACTION_VIEW.
+ */
+ public Intent(String action) {
+ mAction = action;
+ }
+
+ /**
+ * Create an intent with a given action and for a given data url. Note
+ * that the action <em>must</em> be in a namespace because Intents are
+ * used globally in the system -- for example the system VIEW action is
+ * android.intent.action.VIEW; an application's custom action would be
+ * something like com.google.app.myapp.CUSTOM_ACTION.
+ *
+ * @param action The Intent action, such as ACTION_VIEW.
+ * @param uri The Intent data URI.
+ */
+ public Intent(String action, Uri uri) {
+ mAction = action;
+ mData = uri;
+ }
+
+ /**
+ * Create an intent for a specific component. All other fields (action, data,
+ * type, class) are null, though they can be modified later with explicit
+ * calls. This provides a convenient way to create an intent that is
+ * intended to execute a hard-coded class name, rather than relying on the
+ * system to find an appropriate class for you; see {@link #setComponent}
+ * for more information on the repercussions of this.
+ *
+ * @param packageContext A Context of the application package implementing
+ * this class.
+ * @param cls The component class that is to be used for the intent.
+ *
+ * @see #setClass
+ * @see #setComponent
+ * @see #Intent(String, android.net.Uri , Context, Class)
+ */
+ public Intent(Context packageContext, Class<?> cls) {
+ mComponent = new ComponentName(packageContext, cls);
+ }
+
+ /**
+ * Create an intent for a specific component with a specified action and data.
+ * This is equivalent using {@link #Intent(String, android.net.Uri)} to
+ * construct the Intent and then calling {@link #setClass} to set its
+ * class.
+ *
+ * @param action The Intent action, such as ACTION_VIEW.
+ * @param uri The Intent data URI.
+ * @param packageContext A Context of the application package implementing
+ * this class.
+ * @param cls The component class that is to be used for the intent.
+ *
+ * @see #Intent(String, android.net.Uri)
+ * @see #Intent(Context, Class)
+ * @see #setClass
+ * @see #setComponent
+ */
+ public Intent(String action, Uri uri,
+ Context packageContext, Class<?> cls) {
+ mAction = action;
+ mData = uri;
+ mComponent = new ComponentName(packageContext, cls);
+ }
+
+ /**
+ * Create an intent from a URI. This URI may encode the action,
+ * category, and other intent fields, if it was returned by toURI(). If
+ * the Intent was not generate by toURI(), its data will be the entire URI
+ * and its action will be ACTION_VIEW.
+ *
+ * <p>The URI given here must not be relative -- that is, it must include
+ * the scheme and full path.
+ *
+ * @param uri The URI to turn into an Intent.
+ *
+ * @return Intent The newly created Intent object.
+ *
+ * @see #toURI
+ */
+ public static Intent getIntent(String uri) throws URISyntaxException {
+ int i = 0;
+ try {
+ // simple case
+ i = uri.lastIndexOf("#");
+ if (i == -1) return new Intent(ACTION_VIEW, Uri.parse(uri));
+
+ // old format Intent URI
+ if (!uri.startsWith("#Intent;", i)) return getIntentOld(uri);
+
+ // new format
+ Intent intent = new Intent(ACTION_VIEW);
+
+ // fetch data part, if present
+ if (i > 0) {
+ intent.mData = Uri.parse(uri.substring(0, i));
+ }
+ i += "#Intent;".length();
+
+ // loop over contents of Intent, all name=value;
+ while (!uri.startsWith("end", i)) {
+ int eq = uri.indexOf('=', i);
+ int semi = uri.indexOf(';', eq);
+ String value = uri.substring(eq + 1, semi);
+
+ // action
+ if (uri.startsWith("action=", i)) {
+ intent.mAction = value;
+ }
+
+ // categories
+ else if (uri.startsWith("category=", i)) {
+ intent.addCategory(value);
+ }
+
+ // type
+ else if (uri.startsWith("type=", i)) {
+ intent.mType = value;
+ }
+
+ // launch flags
+ else if (uri.startsWith("launchFlags=", i)) {
+ intent.mFlags = Integer.decode(value).intValue();
+ }
+
+ // component
+ else if (uri.startsWith("component=", i)) {
+ intent.mComponent = ComponentName.unflattenFromString(value);
+ }
+
+ // extra
+ else {
+ String key = Uri.decode(uri.substring(i + 2, eq));
+ value = Uri.decode(value);
+ // create Bundle if it doesn't already exist
+ if (intent.mExtras == null) intent.mExtras = new Bundle();
+ Bundle b = intent.mExtras;
+ // add EXTRA
+ if (uri.startsWith("S.", i)) b.putString(key, value);
+ else if (uri.startsWith("B.", i)) b.putBoolean(key, Boolean.parseBoolean(value));
+ else if (uri.startsWith("b.", i)) b.putByte(key, Byte.parseByte(value));
+ else if (uri.startsWith("c.", i)) b.putChar(key, value.charAt(0));
+ else if (uri.startsWith("d.", i)) b.putDouble(key, Double.parseDouble(value));
+ else if (uri.startsWith("f.", i)) b.putFloat(key, Float.parseFloat(value));
+ else if (uri.startsWith("i.", i)) b.putInt(key, Integer.parseInt(value));
+ else if (uri.startsWith("l.", i)) b.putLong(key, Long.parseLong(value));
+ else if (uri.startsWith("s.", i)) b.putShort(key, Short.parseShort(value));
+ else throw new URISyntaxException(uri, "unknown EXTRA type", i);
+ }
+
+ // move to the next item
+ i = semi + 1;
+ }
+
+ return intent;
+
+ } catch (IndexOutOfBoundsException e) {
+ throw new URISyntaxException(uri, "illegal Intent URI format", i);
+ }
+ }
+
+ public static Intent getIntentOld(String uri) throws URISyntaxException {
+ Intent intent;
+
+ int i = uri.lastIndexOf('#');
+ if (i >= 0) {
+ Uri data = null;
+ String action = null;
+ if (i > 0) {
+ data = Uri.parse(uri.substring(0, i));
+ }
+
+ i++;
+
+ if (uri.regionMatches(i, "action(", 0, 7)) {
+ i += 7;
+ int j = uri.indexOf(')', i);
+ action = uri.substring(i, j);
+ i = j + 1;
+ }
+
+ intent = new Intent(action, data);
+
+ if (uri.regionMatches(i, "categories(", 0, 11)) {
+ i += 11;
+ int j = uri.indexOf(')', i);
+ while (i < j) {
+ int sep = uri.indexOf('!', i);
+ if (sep < 0) sep = j;
+ if (i < sep) {
+ intent.addCategory(uri.substring(i, sep));
+ }
+ i = sep + 1;
+ }
+ i = j + 1;
+ }
+
+ if (uri.regionMatches(i, "type(", 0, 5)) {
+ i += 5;
+ int j = uri.indexOf(')', i);
+ intent.mType = uri.substring(i, j);
+ i = j + 1;
+ }
+
+ if (uri.regionMatches(i, "launchFlags(", 0, 12)) {
+ i += 12;
+ int j = uri.indexOf(')', i);
+ intent.mFlags = Integer.decode(uri.substring(i, j)).intValue();
+ i = j + 1;
+ }
+
+ if (uri.regionMatches(i, "component(", 0, 10)) {
+ i += 10;
+ int j = uri.indexOf(')', i);
+ int sep = uri.indexOf('!', i);
+ if (sep >= 0 && sep < j) {
+ String pkg = uri.substring(i, sep);
+ String cls = uri.substring(sep + 1, j);
+ intent.mComponent = new ComponentName(pkg, cls);
+ }
+ i = j + 1;
+ }
+
+ if (uri.regionMatches(i, "extras(", 0, 7)) {
+ i += 7;
+
+ final int closeParen = uri.indexOf(')', i);
+ if (closeParen == -1) throw new URISyntaxException(uri,
+ "EXTRA missing trailing ')'", i);
+
+ while (i < closeParen) {
+ // fetch the key value
+ int j = uri.indexOf('=', i);
+ if (j <= i + 1 || i >= closeParen) {
+ throw new URISyntaxException(uri, "EXTRA missing '='", i);
+ }
+ char type = uri.charAt(i);
+ i++;
+ String key = uri.substring(i, j);
+ i = j + 1;
+
+ // get type-value
+ j = uri.indexOf('!', i);
+ if (j == -1 || j >= closeParen) j = closeParen;
+ if (i >= j) throw new URISyntaxException(uri, "EXTRA missing '!'", i);
+ String value = uri.substring(i, j);
+ i = j;
+
+ // create Bundle if it doesn't already exist
+ if (intent.mExtras == null) intent.mExtras = new Bundle();
+
+ // add item to bundle
+ try {
+ switch (type) {
+ case 'S':
+ intent.mExtras.putString(key, Uri.decode(value));
+ break;
+ case 'B':
+ intent.mExtras.putBoolean(key, Boolean.parseBoolean(value));
+ break;
+ case 'b':
+ intent.mExtras.putByte(key, Byte.parseByte(value));
+ break;
+ case 'c':
+ intent.mExtras.putChar(key, Uri.decode(value).charAt(0));
+ break;
+ case 'd':
+ intent.mExtras.putDouble(key, Double.parseDouble(value));
+ break;
+ case 'f':
+ intent.mExtras.putFloat(key, Float.parseFloat(value));
+ break;
+ case 'i':
+ intent.mExtras.putInt(key, Integer.parseInt(value));
+ break;
+ case 'l':
+ intent.mExtras.putLong(key, Long.parseLong(value));
+ break;
+ case 's':
+ intent.mExtras.putShort(key, Short.parseShort(value));
+ break;
+ default:
+ throw new URISyntaxException(uri, "EXTRA has unknown type", i);
+ }
+ } catch (NumberFormatException e) {
+ throw new URISyntaxException(uri, "EXTRA value can't be parsed", i);
+ }
+
+ char ch = uri.charAt(i);
+ if (ch == ')') break;
+ if (ch != '!') throw new URISyntaxException(uri, "EXTRA missing '!'", i);
+ i++;
+ }
+ }
+
+ if (intent.mAction == null) {
+ // By default, if no action is specified, then use VIEW.
+ intent.mAction = ACTION_VIEW;
+ }
+
+ } else {
+ intent = new Intent(ACTION_VIEW, Uri.parse(uri));
+ }
+
+ return intent;
+ }
+
+ /**
+ * Retrieve the general action to be performed, such as
+ * {@link #ACTION_VIEW}. The action describes the general way the rest of
+ * the information in the intent should be interpreted -- most importantly,
+ * what to do with the data returned by {@link #getData}.
+ *
+ * @return The action of this intent or null if none is specified.
+ *
+ * @see #setAction
+ */
+ public String getAction() {
+ return mAction;
+ }
+
+ /**
+ * Retrieve data this intent is operating on. This URI specifies the name
+ * of the data; often it uses the content: scheme, specifying data in a
+ * content provider. Other schemes may be handled by specific activities,
+ * such as http: by the web browser.
+ *
+ * @return The URI of the data this intent is targeting or null.
+ *
+ * @see #getScheme
+ * @see #setData
+ */
+ public Uri getData() {
+ return mData;
+ }
+
+ /**
+ * The same as {@link #getData()}, but returns the URI as an encoded
+ * String.
+ */
+ public String getDataString() {
+ return mData != null ? mData.toString() : null;
+ }
+
+ /**
+ * Return the scheme portion of the intent's data. If the data is null or
+ * does not include a scheme, null is returned. Otherwise, the scheme
+ * prefix without the final ':' is returned, i.e. "http".
+ *
+ * <p>This is the same as calling getData().getScheme() (and checking for
+ * null data).
+ *
+ * @return The scheme of this intent.
+ *
+ * @see #getData
+ */
+ public String getScheme() {
+ return mData != null ? mData.getScheme() : null;
+ }
+
+ /**
+ * Retrieve any explicit MIME type included in the intent. This is usually
+ * null, as the type is determined by the intent data.
+ *
+ * @return If a type was manually set, it is returned; else null is
+ * returned.
+ *
+ * @see #resolveType(ContentResolver)
+ * @see #setType
+ */
+ public String getType() {
+ return mType;
+ }
+
+ /**
+ * Return the MIME data type of this intent. If the type field is
+ * explicitly set, that is simply returned. Otherwise, if the data is set,
+ * the type of that data is returned. If neither fields are set, a null is
+ * returned.
+ *
+ * @return The MIME type of this intent.
+ *
+ * @see #getType
+ * @see #resolveType(ContentResolver)
+ */
+ public String resolveType(Context context) {
+ return resolveType(context.getContentResolver());
+ }
+
+ /**
+ * Return the MIME data type of this intent. If the type field is
+ * explicitly set, that is simply returned. Otherwise, if the data is set,
+ * the type of that data is returned. If neither fields are set, a null is
+ * returned.
+ *
+ * @param resolver A ContentResolver that can be used to determine the MIME
+ * type of the intent's data.
+ *
+ * @return The MIME type of this intent.
+ *
+ * @see #getType
+ * @see #resolveType(Context)
+ */
+ public String resolveType(ContentResolver resolver) {
+ if (mType != null) {
+ return mType;
+ }
+ if (mData != null) {
+ if ("content".equals(mData.getScheme())) {
+ return resolver.getType(mData);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Return the MIME data type of this intent, only if it will be needed for
+ * intent resolution. This is not generally useful for application code;
+ * it is used by the frameworks for communicating with back-end system
+ * services.
+ *
+ * @param resolver A ContentResolver that can be used to determine the MIME
+ * type of the intent's data.
+ *
+ * @return The MIME type of this intent, or null if it is unknown or not
+ * needed.
+ */
+ public String resolveTypeIfNeeded(ContentResolver resolver) {
+ if (mComponent != null) {
+ return mType;
+ }
+ return resolveType(resolver);
+ }
+
+ /**
+ * Check if an category exists in the intent.
+ *
+ * @param category The category to check.
+ *
+ * @return boolean True if the intent contains the category, else false.
+ *
+ * @see #getCategories
+ * @see #addCategory
+ */
+ public boolean hasCategory(String category) {
+ return mCategories != null && mCategories.contains(category);
+ }
+
+ /**
+ * Return the set of all categories in the intent. If there are no categories,
+ * returns NULL.
+ *
+ * @return Set The set of categories you can examine. Do not modify!
+ *
+ * @see #hasCategory
+ * @see #addCategory
+ */
+ public Set<String> getCategories() {
+ return mCategories;
+ }
+
+ /**
+ * Sets the ClassLoader that will be used when unmarshalling
+ * any Parcelable values from the extras of this Intent.
+ *
+ * @param loader a ClassLoader, or null to use the default loader
+ * at the time of unmarshalling.
+ */
+ public void setExtrasClassLoader(ClassLoader loader) {
+ if (mExtras != null) {
+ mExtras.setClassLoader(loader);
+ }
+ }
+
+ /**
+ * Returns true if an extra value is associated with the given name.
+ * @param name the extra's name
+ * @return true if the given extra is present.
+ */
+ public boolean hasExtra(String name) {
+ return mExtras != null && mExtras.containsKey(name);
+ }
+
+ /**
+ * Returns true if the Intent's extras contain a parcelled file descriptor.
+ * @return true if the Intent contains a parcelled file descriptor.
+ */
+ public boolean hasFileDescriptors() {
+ return mExtras != null && mExtras.hasFileDescriptors();
+ }
+
+ /**
+ * Retrieve extended data from the intent.
+ *
+ * @param name The name of the desired item.
+ *
+ * @return the value of an item that previously added with putExtra()
+ * or null if none was found.
+ *
+ * @deprecated
+ * @hide
+ */
+ @Deprecated
+ public Object getExtra(String name) {
+ return getExtra(name, null);
+ }
+
+ /**
+ * Retrieve extended data from the intent.
+ *
+ * @param name The name of the desired item.
+ * @param defaultValue the value to be returned if no value of the desired
+ * type is stored with the given name.
+ *
+ * @return the value of an item that previously added with putExtra()
+ * or the default value if none was found.
+ *
+ * @see #putExtra(String, boolean)
+ */
+ public boolean getBooleanExtra(String name, boolean defaultValue) {
+ return mExtras == null ? defaultValue :
+ mExtras.getBoolean(name, defaultValue);
+ }
+
+ /**
+ * Retrieve extended data from the intent.
+ *
+ * @param name The name of the desired item.
+ * @param defaultValue the value to be returned if no value of the desired
+ * type is stored with the given name.
+ *
+ * @return the value of an item that previously added with putExtra()
+ * or the default value if none was found.
+ *
+ * @see #putExtra(String, byte)
+ */
+ public byte getByteExtra(String name, byte defaultValue) {
+ return mExtras == null ? defaultValue :
+ mExtras.getByte(name, defaultValue);
+ }
+
+ /**
+ * Retrieve extended data from the intent.
+ *
+ * @param name The name of the desired item.
+ * @param defaultValue the value to be returned if no value of the desired
+ * type is stored with the given name.
+ *
+ * @return the value of an item that previously added with putExtra()
+ * or the default value if none was found.
+ *
+ * @see #putExtra(String, short)
+ */
+ public short getShortExtra(String name, short defaultValue) {
+ return mExtras == null ? defaultValue :
+ mExtras.getShort(name, defaultValue);
+ }
+
+ /**
+ * Retrieve extended data from the intent.
+ *
+ * @param name The name of the desired item.
+ * @param defaultValue the value to be returned if no value of the desired
+ * type is stored with the given name.
+ *
+ * @return the value of an item that previously added with putExtra()
+ * or the default value if none was found.
+ *
+ * @see #putExtra(String, char)
+ */
+ public char getCharExtra(String name, char defaultValue) {
+ return mExtras == null ? defaultValue :
+ mExtras.getChar(name, defaultValue);
+ }
+
+ /**
+ * Retrieve extended data from the intent.
+ *
+ * @param name The name of the desired item.
+ * @param defaultValue the value to be returned if no value of the desired
+ * type is stored with the given name.
+ *
+ * @return the value of an item that previously added with putExtra()
+ * or the default value if none was found.
+ *
+ * @see #putExtra(String, int)
+ */
+ public int getIntExtra(String name, int defaultValue) {
+ return mExtras == null ? defaultValue :
+ mExtras.getInt(name, defaultValue);
+ }
+
+ /**
+ * Retrieve extended data from the intent.
+ *
+ * @param name The name of the desired item.
+ * @param defaultValue the value to be returned if no value of the desired
+ * type is stored with the given name.
+ *
+ * @return the value of an item that previously added with putExtra()
+ * or the default value if none was found.
+ *
+ * @see #putExtra(String, long)
+ */
+ public long getLongExtra(String name, long defaultValue) {
+ return mExtras == null ? defaultValue :
+ mExtras.getLong(name, defaultValue);
+ }
+
+ /**
+ * Retrieve extended data from the intent.
+ *
+ * @param name The name of the desired item.
+ * @param defaultValue the value to be returned if no value of the desired
+ * type is stored with the given name.
+ *
+ * @return the value of an item that previously added with putExtra(),
+ * or the default value if no such item is present
+ *
+ * @see #putExtra(String, float)
+ */
+ public float getFloatExtra(String name, float defaultValue) {
+ return mExtras == null ? defaultValue :
+ mExtras.getFloat(name, defaultValue);
+ }
+
+ /**
+ * Retrieve extended data from the intent.
+ *
+ * @param name The name of the desired item.
+ * @param defaultValue the value to be returned if no value of the desired
+ * type is stored with the given name.
+ *
+ * @return the value of an item that previously added with putExtra()
+ * or the default value if none was found.
+ *
+ * @see #putExtra(String, double)
+ */
+ public double getDoubleExtra(String name, double defaultValue) {
+ return mExtras == null ? defaultValue :
+ mExtras.getDouble(name, defaultValue);
+ }
+
+ /**
+ * Retrieve extended data from the intent.
+ *
+ * @param name The name of the desired item.
+ *
+ * @return the value of an item that previously added with putExtra()
+ * or null if no String value was found.
+ *
+ * @see #putExtra(String, String)
+ */
+ public String getStringExtra(String name) {
+ return mExtras == null ? null : mExtras.getString(name);
+ }
+
+ /**
+ * Retrieve extended data from the intent.
+ *
+ * @param name The name of the desired item.
+ *
+ * @return the value of an item that previously added with putExtra()
+ * or null if no CharSequence value was found.
+ *
+ * @see #putExtra(String, CharSequence)
+ */
+ public CharSequence getCharSequenceExtra(String name) {
+ return mExtras == null ? null : mExtras.getCharSequence(name);
+ }
+
+ /**
+ * Retrieve extended data from the intent.
+ *
+ * @param name The name of the desired item.
+ *
+ * @return the value of an item that previously added with putExtra()
+ * or null if no Parcelable value was found.
+ *
+ * @see #putExtra(String, Parcelable)
+ */
+ public <T extends Parcelable> T getParcelableExtra(String name) {
+ return mExtras == null ? null : mExtras.<T>getParcelable(name);
+ }
+
+ /**
+ * Retrieve extended data from the intent.
+ *
+ * @param name The name of the desired item.
+ *
+ * @return the value of an item that previously added with putExtra()
+ * or null if no Parcelable[] value was found.
+ *
+ * @see #putExtra(String, Parcelable[])
+ */
+ public Parcelable[] getParcelableArrayExtra(String name) {
+ return mExtras == null ? null : mExtras.getParcelableArray(name);
+ }
+
+ /**
+ * Retrieve extended data from the intent.
+ *
+ * @param name The name of the desired item.
+ *
+ * @return the value of an item that previously added with putExtra()
+ * or null if no ArrayList<Parcelable> value was found.
+ *
+ * @see #putParcelableArrayListExtra(String, ArrayList)
+ */
+ public <T extends Parcelable> ArrayList<T> getParcelableArrayListExtra(String name) {
+ return mExtras == null ? null : mExtras.<T>getParcelableArrayList(name);
+ }
+
+ /**
+ * Retrieve extended data from the intent.
+ *
+ * @param name The name of the desired item.
+ *
+ * @return the value of an item that previously added with putExtra()
+ * or null if no Serializable value was found.
+ *
+ * @see #putExtra(String, Serializable)
+ */
+ public Serializable getSerializableExtra(String name) {
+ return mExtras == null ? null : mExtras.getSerializable(name);
+ }
+
+ /**
+ * Retrieve extended data from the intent.
+ *
+ * @param name The name of the desired item.
+ *
+ * @return the value of an item that previously added with putExtra()
+ * or null if no ArrayList<Integer> value was found.
+ *
+ * @see #putIntegerArrayListExtra(String, ArrayList)
+ */
+ public ArrayList<Integer> getIntegerArrayListExtra(String name) {
+ return mExtras == null ? null : mExtras.getIntegerArrayList(name);
+ }
+
+ /**
+ * Retrieve extended data from the intent.
+ *
+ * @param name The name of the desired item.
+ *
+ * @return the value of an item that previously added with putExtra()
+ * or null if no ArrayList<String> value was found.
+ *
+ * @see #putStringArrayListExtra(String, ArrayList)
+ */
+ public ArrayList<String> getStringArrayListExtra(String name) {
+ return mExtras == null ? null : mExtras.getStringArrayList(name);
+ }
+
+ /**
+ * Retrieve extended data from the intent.
+ *
+ * @param name The name of the desired item.
+ *
+ * @return the value of an item that previously added with putExtra()
+ * or null if no boolean array value was found.
+ *
+ * @see #putExtra(String, boolean[])
+ */
+ public boolean[] getBooleanArrayExtra(String name) {
+ return mExtras == null ? null : mExtras.getBooleanArray(name);
+ }
+
+ /**
+ * Retrieve extended data from the intent.
+ *
+ * @param name The name of the desired item.
+ *
+ * @return the value of an item that previously added with putExtra()
+ * or null if no byte array value was found.
+ *
+ * @see #putExtra(String, byte[])
+ */
+ public byte[] getByteArrayExtra(String name) {
+ return mExtras == null ? null : mExtras.getByteArray(name);
+ }
+
+ /**
+ * Retrieve extended data from the intent.
+ *
+ * @param name The name of the desired item.
+ *
+ * @return the value of an item that previously added with putExtra()
+ * or null if no short array value was found.
+ *
+ * @see #putExtra(String, short[])
+ */
+ public short[] getShortArrayExtra(String name) {
+ return mExtras == null ? null : mExtras.getShortArray(name);
+ }
+
+ /**
+ * Retrieve extended data from the intent.
+ *
+ * @param name The name of the desired item.
+ *
+ * @return the value of an item that previously added with putExtra()
+ * or null if no char array value was found.
+ *
+ * @see #putExtra(String, char[])
+ */
+ public char[] getCharArrayExtra(String name) {
+ return mExtras == null ? null : mExtras.getCharArray(name);
+ }
+
+ /**
+ * Retrieve extended data from the intent.
+ *
+ * @param name The name of the desired item.
+ *
+ * @return the value of an item that previously added with putExtra()
+ * or null if no int array value was found.
+ *
+ * @see #putExtra(String, int[])
+ */
+ public int[] getIntArrayExtra(String name) {
+ return mExtras == null ? null : mExtras.getIntArray(name);
+ }
+
+ /**
+ * Retrieve extended data from the intent.
+ *
+ * @param name The name of the desired item.
+ *
+ * @return the value of an item that previously added with putExtra()
+ * or null if no long array value was found.
+ *
+ * @see #putExtra(String, long[])
+ */
+ public long[] getLongArrayExtra(String name) {
+ return mExtras == null ? null : mExtras.getLongArray(name);
+ }
+
+ /**
+ * Retrieve extended data from the intent.
+ *
+ * @param name The name of the desired item.
+ *
+ * @return the value of an item that previously added with putExtra()
+ * or null if no float array value was found.
+ *
+ * @see #putExtra(String, float[])
+ */
+ public float[] getFloatArrayExtra(String name) {
+ return mExtras == null ? null : mExtras.getFloatArray(name);
+ }
+
+ /**
+ * Retrieve extended data from the intent.
+ *
+ * @param name The name of the desired item.
+ *
+ * @return the value of an item that previously added with putExtra()
+ * or null if no double array value was found.
+ *
+ * @see #putExtra(String, double[])
+ */
+ public double[] getDoubleArrayExtra(String name) {
+ return mExtras == null ? null : mExtras.getDoubleArray(name);
+ }
+
+ /**
+ * Retrieve extended data from the intent.
+ *
+ * @param name The name of the desired item.
+ *
+ * @return the value of an item that previously added with putExtra()
+ * or null if no String array value was found.
+ *
+ * @see #putExtra(String, String[])
+ */
+ public String[] getStringArrayExtra(String name) {
+ return mExtras == null ? null : mExtras.getStringArray(name);
+ }
+
+ /**
+ * Retrieve extended data from the intent.
+ *
+ * @param name The name of the desired item.
+ *
+ * @return the value of an item that previously added with putExtra()
+ * or null if no Bundle value was found.
+ *
+ * @see #putExtra(String, Bundle)
+ */
+ public Bundle getBundleExtra(String name) {
+ return mExtras == null ? null : mExtras.getBundle(name);
+ }
+
+ /**
+ * Retrieve extended data from the intent.
+ *
+ * @param name The name of the desired item.
+ *
+ * @return the value of an item that previously added with putExtra()
+ * or null if no IBinder value was found.
+ *
+ * @see #putExtra(String, IBinder)
+ *
+ * @deprecated
+ * @hide
+ */
+ @Deprecated
+ public IBinder getIBinderExtra(String name) {
+ return mExtras == null ? null : mExtras.getIBinder(name);
+ }
+
+ /**
+ * Retrieve extended data from the intent.
+ *
+ * @param name The name of the desired item.
+ * @param defaultValue The default value to return in case no item is
+ * associated with the key 'name'
+ *
+ * @return the value of an item that previously added with putExtra()
+ * or defaultValue if none was found.
+ *
+ * @see #putExtra
+ *
+ * @deprecated
+ * @hide
+ */
+ @Deprecated
+ public Object getExtra(String name, Object defaultValue) {
+ Object result = defaultValue;
+ if (mExtras != null) {
+ Object result2 = mExtras.get(name);
+ if (result2 != null) {
+ result = result2;
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Retrieves a map of extended data from the intent.
+ *
+ * @return the map of all extras previously added with putExtra(),
+ * or null if none have been added.
+ */
+ public Bundle getExtras() {
+ return (mExtras != null)
+ ? new Bundle(mExtras)
+ : null;
+ }
+
+ /**
+ * Retrieve any special flags associated with this intent. You will
+ * normally just set them with {@link #setFlags} and let the system
+ * take the appropriate action with them.
+ *
+ * @return int The currently set flags.
+ *
+ * @see #setFlags
+ */
+ public int getFlags() {
+ return mFlags;
+ }
+
+ /**
+ * Retrieve the concrete component associated with the intent. When receiving
+ * an intent, this is the component that was found to best handle it (that is,
+ * yourself) and will always be non-null; in all other cases it will be
+ * null unless explicitly set.
+ *
+ * @return The name of the application component to handle the intent.
+ *
+ * @see #resolveActivity
+ * @see #setComponent
+ */
+ public ComponentName getComponent() {
+ return mComponent;
+ }
+
+ /**
+ * Return the Activity component that should be used to handle this intent.
+ * The appropriate component is determined based on the information in the
+ * intent, evaluated as follows:
+ *
+ * <p>If {@link #getComponent} returns an explicit class, that is returned
+ * without any further consideration.
+ *
+ * <p>The activity must handle the {@link Intent#CATEGORY_DEFAULT} Intent
+ * category to be considered.
+ *
+ * <p>If {@link #getAction} is non-NULL, the activity must handle this
+ * action.
+ *
+ * <p>If {@link #resolveType} returns non-NULL, the activity must handle
+ * this type.
+ *
+ * <p>If {@link #addCategory} has added any categories, the activity must
+ * handle ALL of the categories specified.
+ *
+ * <p>If there are no activities that satisfy all of these conditions, a
+ * null string is returned.
+ *
+ * <p>If multiple activities are found to satisfy the intent, the one with
+ * the highest priority will be used. If there are multiple activities
+ * with the same priority, the system will either pick the best activity
+ * based on user preference, or resolve to a system class that will allow
+ * the user to pick an activity and forward from there.
+ *
+ * <p>This method is implemented simply by calling
+ * {@link PackageManager#resolveActivity} with the "defaultOnly" parameter
+ * true.</p>
+ * <p> This API is called for you as part of starting an activity from an
+ * intent. You do not normally need to call it yourself.</p>
+ *
+ * @param pm The package manager with which to resolve the Intent.
+ *
+ * @return Name of the component implementing an activity that can
+ * display the intent.
+ *
+ * @see #setComponent
+ * @see #getComponent
+ * @see #resolveActivityInfo
+ */
+ public ComponentName resolveActivity(PackageManager pm) {
+ if (mComponent != null) {
+ return mComponent;
+ }
+
+ ResolveInfo info = pm.resolveActivity(
+ this, PackageManager.MATCH_DEFAULT_ONLY);
+ if (info != null) {
+ return new ComponentName(
+ info.activityInfo.applicationInfo.packageName,
+ info.activityInfo.name);
+ }
+
+ return null;
+ }
+
+ /**
+ * Resolve the Intent into an {@link ActivityInfo}
+ * describing the activity that should execute the intent. Resolution
+ * follows the same rules as described for {@link #resolveActivity}, but
+ * you get back the completely information about the resolved activity
+ * instead of just its class name.
+ *
+ * @param pm The package manager with which to resolve the Intent.
+ * @param flags Addition information to retrieve as per
+ * {@link PackageManager#getActivityInfo(ComponentName, int)
+ * PackageManager.getActivityInfo()}.
+ *
+ * @return PackageManager.ActivityInfo
+ *
+ * @see #resolveActivity
+ */
+ public ActivityInfo resolveActivityInfo(PackageManager pm, int flags) {
+ ActivityInfo ai = null;
+ if (mComponent != null) {
+ try {
+ ai = pm.getActivityInfo(mComponent, flags);
+ } catch (PackageManager.NameNotFoundException e) {
+ // ignore
+ }
+ } else {
+ ResolveInfo info = pm.resolveActivity(
+ this, PackageManager.MATCH_DEFAULT_ONLY);
+ if (info != null) {
+ ai = info.activityInfo;
+ }
+ }
+
+ return ai;
+ }
+
+ /**
+ * Set the general action to be performed.
+ *
+ * @param action An action name, such as ACTION_VIEW. Application-specific
+ * actions should be prefixed with the vendor's package name.
+ *
+ * @return Returns the same Intent object, for chaining multiple calls
+ * into a single statement.
+ *
+ * @see #getAction
+ */
+ public Intent setAction(String action) {
+ mAction = action;
+ return this;
+ }
+
+ /**
+ * Set the data this intent is operating on. This method automatically
+ * clears any type that was previously set by {@link #setType}.
+ *
+ * @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
+ */
+ public Intent setData(Uri data) {
+ mData = data;
+ mType = null;
+ return this;
+ }
+
+ /**
+ * 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}.
+ *
+ * @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 #setDataAndType
+ */
+ public Intent setType(String type) {
+ mData = null;
+ mType = type;
+ return this;
+ }
+
+ /**
+ * (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.
+ *
+ * @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
+ */
+ public Intent setDataAndType(Uri data, String type) {
+ mData = data;
+ mType = type;
+ return this;
+ }
+
+ /**
+ * 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
+ * used.
+ *
+ * @param category The desired category. This can be either one of the
+ * predefined Intent categories, or a custom category in your own
+ * namespace.
+ *
+ * @return Returns the same Intent object, for chaining multiple calls
+ * into a single statement.
+ *
+ * @see #hasCategory
+ * @see #removeCategory
+ */
+ public Intent addCategory(String category) {
+ if (mCategories == null) {
+ mCategories = new HashSet<String>();
+ }
+ mCategories.add(category);
+ return this;
+ }
+
+ /**
+ * Remove an category from an intent.
+ *
+ * @param category The category to remove.
+ *
+ * @see #addCategory
+ */
+ public void removeCategory(String category) {
+ if (mCategories != null) {
+ mCategories.remove(category);
+ if (mCategories.size() == 0) {
+ mCategories = null;
+ }
+ }
+ }
+
+ /**
+ * Add extended data to the intent. The name must include a package
+ * prefix, for example the app com.android.contacts would use names
+ * like "com.android.contacts.ShowAll".
+ *
+ * @param name The name of the extra data, with package prefix.
+ * @param value The boolean data value.
+ *
+ * @return Returns the same Intent object, for chaining multiple calls
+ * into a single statement.
+ *
+ * @see #putExtras
+ * @see #removeExtra
+ * @see #getBooleanExtra(String, boolean)
+ */
+ public Intent putExtra(String name, boolean value) {
+ if (mExtras == null) {
+ mExtras = new Bundle();
+ }
+ mExtras.putBoolean(name, value);
+ return this;
+ }
+
+ /**
+ * Add extended data to the intent. The name must include a package
+ * prefix, for example the app com.android.contacts would use names
+ * like "com.android.contacts.ShowAll".
+ *
+ * @param name The name of the extra data, with package prefix.
+ * @param value The byte data value.
+ *
+ * @return Returns the same Intent object, for chaining multiple calls
+ * into a single statement.
+ *
+ * @see #putExtras
+ * @see #removeExtra
+ * @see #getByteExtra(String, byte)
+ */
+ public Intent putExtra(String name, byte value) {
+ if (mExtras == null) {
+ mExtras = new Bundle();
+ }
+ mExtras.putByte(name, value);
+ return this;
+ }
+
+ /**
+ * Add extended data to the intent. The name must include a package
+ * prefix, for example the app com.android.contacts would use names
+ * like "com.android.contacts.ShowAll".
+ *
+ * @param name The name of the extra data, with package prefix.
+ * @param value The char data value.
+ *
+ * @return Returns the same Intent object, for chaining multiple calls
+ * into a single statement.
+ *
+ * @see #putExtras
+ * @see #removeExtra
+ * @see #getCharExtra(String, char)
+ */
+ public Intent putExtra(String name, char value) {
+ if (mExtras == null) {
+ mExtras = new Bundle();
+ }
+ mExtras.putChar(name, value);
+ return this;
+ }
+
+ /**
+ * Add extended data to the intent. The name must include a package
+ * prefix, for example the app com.android.contacts would use names
+ * like "com.android.contacts.ShowAll".
+ *
+ * @param name The name of the extra data, with package prefix.
+ * @param value The short data value.
+ *
+ * @return Returns the same Intent object, for chaining multiple calls
+ * into a single statement.
+ *
+ * @see #putExtras
+ * @see #removeExtra
+ * @see #getShortExtra(String, short)
+ */
+ public Intent putExtra(String name, short value) {
+ if (mExtras == null) {
+ mExtras = new Bundle();
+ }
+ mExtras.putShort(name, value);
+ return this;
+ }
+
+ /**
+ * Add extended data to the intent. The name must include a package
+ * prefix, for example the app com.android.contacts would use names
+ * like "com.android.contacts.ShowAll".
+ *
+ * @param name The name of the extra data, with package prefix.
+ * @param value The integer data value.
+ *
+ * @return Returns the same Intent object, for chaining multiple calls
+ * into a single statement.
+ *
+ * @see #putExtras
+ * @see #removeExtra
+ * @see #getIntExtra(String, int)
+ */
+ public Intent putExtra(String name, int value) {
+ if (mExtras == null) {
+ mExtras = new Bundle();
+ }
+ mExtras.putInt(name, value);
+ return this;
+ }
+
+ /**
+ * Add extended data to the intent. The name must include a package
+ * prefix, for example the app com.android.contacts would use names
+ * like "com.android.contacts.ShowAll".
+ *
+ * @param name The name of the extra data, with package prefix.
+ * @param value The long data value.
+ *
+ * @return Returns the same Intent object, for chaining multiple calls
+ * into a single statement.
+ *
+ * @see #putExtras
+ * @see #removeExtra
+ * @see #getLongExtra(String, long)
+ */
+ public Intent putExtra(String name, long value) {
+ if (mExtras == null) {
+ mExtras = new Bundle();
+ }
+ mExtras.putLong(name, value);
+ return this;
+ }
+
+ /**
+ * Add extended data to the intent. The name must include a package
+ * prefix, for example the app com.android.contacts would use names
+ * like "com.android.contacts.ShowAll".
+ *
+ * @param name The name of the extra data, with package prefix.
+ * @param value The float data value.
+ *
+ * @return Returns the same Intent object, for chaining multiple calls
+ * into a single statement.
+ *
+ * @see #putExtras
+ * @see #removeExtra
+ * @see #getFloatExtra(String, float)
+ */
+ public Intent putExtra(String name, float value) {
+ if (mExtras == null) {
+ mExtras = new Bundle();
+ }
+ mExtras.putFloat(name, value);
+ return this;
+ }
+
+ /**
+ * Add extended data to the intent. The name must include a package
+ * prefix, for example the app com.android.contacts would use names
+ * like "com.android.contacts.ShowAll".
+ *
+ * @param name The name of the extra data, with package prefix.
+ * @param value The double data value.
+ *
+ * @return Returns the same Intent object, for chaining multiple calls
+ * into a single statement.
+ *
+ * @see #putExtras
+ * @see #removeExtra
+ * @see #getDoubleExtra(String, double)
+ */
+ public Intent putExtra(String name, double value) {
+ if (mExtras == null) {
+ mExtras = new Bundle();
+ }
+ mExtras.putDouble(name, value);
+ return this;
+ }
+
+ /**
+ * Add extended data to the intent. The name must include a package
+ * prefix, for example the app com.android.contacts would use names
+ * like "com.android.contacts.ShowAll".
+ *
+ * @param name The name of the extra data, with package prefix.
+ * @param value The String data value.
+ *
+ * @return Returns the same Intent object, for chaining multiple calls
+ * into a single statement.
+ *
+ * @see #putExtras
+ * @see #removeExtra
+ * @see #getStringExtra(String)
+ */
+ public Intent putExtra(String name, String value) {
+ if (mExtras == null) {
+ mExtras = new Bundle();
+ }
+ mExtras.putString(name, value);
+ return this;
+ }
+
+ /**
+ * Add extended data to the intent. The name must include a package
+ * prefix, for example the app com.android.contacts would use names
+ * like "com.android.contacts.ShowAll".
+ *
+ * @param name The name of the extra data, with package prefix.
+ * @param value The CharSequence data value.
+ *
+ * @return Returns the same Intent object, for chaining multiple calls
+ * into a single statement.
+ *
+ * @see #putExtras
+ * @see #removeExtra
+ * @see #getCharSequenceExtra(String)
+ */
+ public Intent putExtra(String name, CharSequence value) {
+ if (mExtras == null) {
+ mExtras = new Bundle();
+ }
+ mExtras.putCharSequence(name, value);
+ return this;
+ }
+
+ /**
+ * Add extended data to the intent. The name must include a package
+ * prefix, for example the app com.android.contacts would use names
+ * like "com.android.contacts.ShowAll".
+ *
+ * @param name The name of the extra data, with package prefix.
+ * @param value The Parcelable data value.
+ *
+ * @return Returns the same Intent object, for chaining multiple calls
+ * into a single statement.
+ *
+ * @see #putExtras
+ * @see #removeExtra
+ * @see #getParcelableExtra(String)
+ */
+ public Intent putExtra(String name, Parcelable value) {
+ if (mExtras == null) {
+ mExtras = new Bundle();
+ }
+ mExtras.putParcelable(name, value);
+ return this;
+ }
+
+ /**
+ * Add extended data to the intent. The name must include a package
+ * prefix, for example the app com.android.contacts would use names
+ * like "com.android.contacts.ShowAll".
+ *
+ * @param name The name of the extra data, with package prefix.
+ * @param value The Parcelable[] data value.
+ *
+ * @return Returns the same Intent object, for chaining multiple calls
+ * into a single statement.
+ *
+ * @see #putExtras
+ * @see #removeExtra
+ * @see #getParcelableArrayExtra(String)
+ */
+ public Intent putExtra(String name, Parcelable[] value) {
+ if (mExtras == null) {
+ mExtras = new Bundle();
+ }
+ mExtras.putParcelableArray(name, value);
+ return this;
+ }
+
+ /**
+ * Add extended data to the intent. The name must include a package
+ * prefix, for example the app com.android.contacts would use names
+ * like "com.android.contacts.ShowAll".
+ *
+ * @param name The name of the extra data, with package prefix.
+ * @param value The ArrayList<Parcelable> data value.
+ *
+ * @return Returns the same Intent object, for chaining multiple calls
+ * into a single statement.
+ *
+ * @see #putExtras
+ * @see #removeExtra
+ * @see #getParcelableArrayListExtra(String)
+ */
+ public Intent putParcelableArrayListExtra(String name, ArrayList<? extends Parcelable> value) {
+ if (mExtras == null) {
+ mExtras = new Bundle();
+ }
+ mExtras.putParcelableArrayList(name, value);
+ return this;
+ }
+
+ /**
+ * Add extended data to the intent. The name must include a package
+ * prefix, for example the app com.android.contacts would use names
+ * like "com.android.contacts.ShowAll".
+ *
+ * @param name The name of the extra data, with package prefix.
+ * @param value The ArrayList<Integer> data value.
+ *
+ * @return Returns the same Intent object, for chaining multiple calls
+ * into a single statement.
+ *
+ * @see #putExtras
+ * @see #removeExtra
+ * @see #getIntegerArrayListExtra(String)
+ */
+ public Intent putIntegerArrayListExtra(String name, ArrayList<Integer> value) {
+ if (mExtras == null) {
+ mExtras = new Bundle();
+ }
+ mExtras.putIntegerArrayList(name, value);
+ return this;
+ }
+
+ /**
+ * Add extended data to the intent. The name must include a package
+ * prefix, for example the app com.android.contacts would use names
+ * like "com.android.contacts.ShowAll".
+ *
+ * @param name The name of the extra data, with package prefix.
+ * @param value The ArrayList<String> data value.
+ *
+ * @return Returns the same Intent object, for chaining multiple calls
+ * into a single statement.
+ *
+ * @see #putExtras
+ * @see #removeExtra
+ * @see #getStringArrayListExtra(String)
+ */
+ public Intent putStringArrayListExtra(String name, ArrayList<String> value) {
+ if (mExtras == null) {
+ mExtras = new Bundle();
+ }
+ mExtras.putStringArrayList(name, value);
+ return this;
+ }
+
+ /**
+ * Add extended data to the intent. The name must include a package
+ * prefix, for example the app com.android.contacts would use names
+ * like "com.android.contacts.ShowAll".
+ *
+ * @param name The name of the extra data, with package prefix.
+ * @param value The Serializable data value.
+ *
+ * @return Returns the same Intent object, for chaining multiple calls
+ * into a single statement.
+ *
+ * @see #putExtras
+ * @see #removeExtra
+ * @see #getSerializableExtra(String)
+ */
+ public Intent putExtra(String name, Serializable value) {
+ if (mExtras == null) {
+ mExtras = new Bundle();
+ }
+ mExtras.putSerializable(name, value);
+ return this;
+ }
+
+ /**
+ * Add extended data to the intent. The name must include a package
+ * prefix, for example the app com.android.contacts would use names
+ * like "com.android.contacts.ShowAll".
+ *
+ * @param name The name of the extra data, with package prefix.
+ * @param value The boolean array data value.
+ *
+ * @return Returns the same Intent object, for chaining multiple calls
+ * into a single statement.
+ *
+ * @see #putExtras
+ * @see #removeExtra
+ * @see #getBooleanArrayExtra(String)
+ */
+ public Intent putExtra(String name, boolean[] value) {
+ if (mExtras == null) {
+ mExtras = new Bundle();
+ }
+ mExtras.putBooleanArray(name, value);
+ return this;
+ }
+
+ /**
+ * Add extended data to the intent. The name must include a package
+ * prefix, for example the app com.android.contacts would use names
+ * like "com.android.contacts.ShowAll".
+ *
+ * @param name The name of the extra data, with package prefix.
+ * @param value The byte array data value.
+ *
+ * @return Returns the same Intent object, for chaining multiple calls
+ * into a single statement.
+ *
+ * @see #putExtras
+ * @see #removeExtra
+ * @see #getByteArrayExtra(String)
+ */
+ public Intent putExtra(String name, byte[] value) {
+ if (mExtras == null) {
+ mExtras = new Bundle();
+ }
+ mExtras.putByteArray(name, value);
+ return this;
+ }
+
+ /**
+ * Add extended data to the intent. The name must include a package
+ * prefix, for example the app com.android.contacts would use names
+ * like "com.android.contacts.ShowAll".
+ *
+ * @param name The name of the extra data, with package prefix.
+ * @param value The short array data value.
+ *
+ * @return Returns the same Intent object, for chaining multiple calls
+ * into a single statement.
+ *
+ * @see #putExtras
+ * @see #removeExtra
+ * @see #getShortArrayExtra(String)
+ */
+ public Intent putExtra(String name, short[] value) {
+ if (mExtras == null) {
+ mExtras = new Bundle();
+ }
+ mExtras.putShortArray(name, value);
+ return this;
+ }
+
+ /**
+ * Add extended data to the intent. The name must include a package
+ * prefix, for example the app com.android.contacts would use names
+ * like "com.android.contacts.ShowAll".
+ *
+ * @param name The name of the extra data, with package prefix.
+ * @param value The char array data value.
+ *
+ * @return Returns the same Intent object, for chaining multiple calls
+ * into a single statement.
+ *
+ * @see #putExtras
+ * @see #removeExtra
+ * @see #getCharArrayExtra(String)
+ */
+ public Intent putExtra(String name, char[] value) {
+ if (mExtras == null) {
+ mExtras = new Bundle();
+ }
+ mExtras.putCharArray(name, value);
+ return this;
+ }
+
+ /**
+ * Add extended data to the intent. The name must include a package
+ * prefix, for example the app com.android.contacts would use names
+ * like "com.android.contacts.ShowAll".
+ *
+ * @param name The name of the extra data, with package prefix.
+ * @param value The int array data value.
+ *
+ * @return Returns the same Intent object, for chaining multiple calls
+ * into a single statement.
+ *
+ * @see #putExtras
+ * @see #removeExtra
+ * @see #getIntArrayExtra(String)
+ */
+ public Intent putExtra(String name, int[] value) {
+ if (mExtras == null) {
+ mExtras = new Bundle();
+ }
+ mExtras.putIntArray(name, value);
+ return this;
+ }
+
+ /**
+ * Add extended data to the intent. The name must include a package
+ * prefix, for example the app com.android.contacts would use names
+ * like "com.android.contacts.ShowAll".
+ *
+ * @param name The name of the extra data, with package prefix.
+ * @param value The byte array data value.
+ *
+ * @return Returns the same Intent object, for chaining multiple calls
+ * into a single statement.
+ *
+ * @see #putExtras
+ * @see #removeExtra
+ * @see #getLongArrayExtra(String)
+ */
+ public Intent putExtra(String name, long[] value) {
+ if (mExtras == null) {
+ mExtras = new Bundle();
+ }
+ mExtras.putLongArray(name, value);
+ return this;
+ }
+
+ /**
+ * Add extended data to the intent. The name must include a package
+ * prefix, for example the app com.android.contacts would use names
+ * like "com.android.contacts.ShowAll".
+ *
+ * @param name The name of the extra data, with package prefix.
+ * @param value The float array data value.
+ *
+ * @return Returns the same Intent object, for chaining multiple calls
+ * into a single statement.
+ *
+ * @see #putExtras
+ * @see #removeExtra
+ * @see #getFloatArrayExtra(String)
+ */
+ public Intent putExtra(String name, float[] value) {
+ if (mExtras == null) {
+ mExtras = new Bundle();
+ }
+ mExtras.putFloatArray(name, value);
+ return this;
+ }
+
+ /**
+ * Add extended data to the intent. The name must include a package
+ * prefix, for example the app com.android.contacts would use names
+ * like "com.android.contacts.ShowAll".
+ *
+ * @param name The name of the extra data, with package prefix.
+ * @param value The double array data value.
+ *
+ * @return Returns the same Intent object, for chaining multiple calls
+ * into a single statement.
+ *
+ * @see #putExtras
+ * @see #removeExtra
+ * @see #getDoubleArrayExtra(String)
+ */
+ public Intent putExtra(String name, double[] value) {
+ if (mExtras == null) {
+ mExtras = new Bundle();
+ }
+ mExtras.putDoubleArray(name, value);
+ return this;
+ }
+
+ /**
+ * Add extended data to the intent. The name must include a package
+ * prefix, for example the app com.android.contacts would use names
+ * like "com.android.contacts.ShowAll".
+ *
+ * @param name The name of the extra data, with package prefix.
+ * @param value The String array data value.
+ *
+ * @return Returns the same Intent object, for chaining multiple calls
+ * into a single statement.
+ *
+ * @see #putExtras
+ * @see #removeExtra
+ * @see #getStringArrayExtra(String)
+ */
+ public Intent putExtra(String name, String[] value) {
+ if (mExtras == null) {
+ mExtras = new Bundle();
+ }
+ mExtras.putStringArray(name, value);
+ return this;
+ }
+
+ /**
+ * Add extended data to the intent. The name must include a package
+ * prefix, for example the app com.android.contacts would use names
+ * like "com.android.contacts.ShowAll".
+ *
+ * @param name The name of the extra data, with package prefix.
+ * @param value The Bundle data value.
+ *
+ * @return Returns the same Intent object, for chaining multiple calls
+ * into a single statement.
+ *
+ * @see #putExtras
+ * @see #removeExtra
+ * @see #getBundleExtra(String)
+ */
+ public Intent putExtra(String name, Bundle value) {
+ if (mExtras == null) {
+ mExtras = new Bundle();
+ }
+ mExtras.putBundle(name, value);
+ return this;
+ }
+
+ /**
+ * Add extended data to the intent. The name must include a package
+ * prefix, for example the app com.android.contacts would use names
+ * like "com.android.contacts.ShowAll".
+ *
+ * @param name The name of the extra data, with package prefix.
+ * @param value The IBinder data value.
+ *
+ * @return Returns the same Intent object, for chaining multiple calls
+ * into a single statement.
+ *
+ * @see #putExtras
+ * @see #removeExtra
+ * @see #getIBinderExtra(String)
+ *
+ * @deprecated
+ * @hide
+ */
+ @Deprecated
+ public Intent putExtra(String name, IBinder value) {
+ if (mExtras == null) {
+ mExtras = new Bundle();
+ }
+ mExtras.putIBinder(name, value);
+ return this;
+ }
+
+ /**
+ * Copy all extras in 'src' in to this intent.
+ *
+ * @param src Contains the extras to copy.
+ *
+ * @see #putExtra
+ */
+ public Intent putExtras(Intent src) {
+ if (src.mExtras != null) {
+ if (mExtras == null) {
+ mExtras = new Bundle(src.mExtras);
+ } else {
+ mExtras.putAll(src.mExtras);
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Add a set of extended data to the intent. The keys must include a package
+ * prefix, for example the app com.android.contacts would use names
+ * like "com.android.contacts.ShowAll".
+ *
+ * @param extras The Bundle of extras to add to this intent.
+ *
+ * @see #putExtra
+ * @see #removeExtra
+ */
+ public Intent putExtras(Bundle extras) {
+ if (mExtras == null) {
+ mExtras = new Bundle();
+ }
+ mExtras.putAll(extras);
+ return this;
+ }
+
+ /**
+ * Remove extended data from the intent.
+ *
+ * @see #putExtra
+ */
+ public void removeExtra(String name) {
+ if (mExtras != null) {
+ mExtras.remove(name);
+ if (mExtras.size() == 0) {
+ mExtras = null;
+ }
+ }
+ }
+
+ /**
+ * Set special flags controlling how this intent is handled. Most values
+ * here depend on the type of component being executed by the Intent,
+ * specifically the FLAG_ACTIVITY_* flags are all for use with
+ * {@link Context#startActivity Context.startActivity()} and the
+ * FLAG_RECEIVER_* flags are all for use with
+ * {@link Context#sendBroadcast(Intent) Context.sendBroadcast()}.
+ *
+ * <p>See the <a href="{@docRoot}intro/appmodel.html">Application Model</a>
+ * documentation for important information on how some of these options impact
+ * the behavior of your application.
+ *
+ * @param flags The desired flags.
+ *
+ * @return Returns the same Intent object, for chaining multiple calls
+ * into a single statement.
+ *
+ * @see #getFlags
+ * @see #addFlags
+ *
+ * @see #FLAG_GRANT_READ_URI_PERMISSION
+ * @see #FLAG_GRANT_WRITE_URI_PERMISSION
+ * @see #FLAG_DEBUG_LOG_RESOLUTION
+ * @see #FLAG_FROM_BACKGROUND
+ * @see #FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
+ * @see #FLAG_ACTIVITY_BROUGHT_TO_FRONT
+ * @see #FLAG_ACTIVITY_CLEAR_TOP
+ * @see #FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
+ * @see #FLAG_ACTIVITY_FORWARD_RESULT
+ * @see #FLAG_ACTIVITY_MULTIPLE_TASK
+ * @see #FLAG_ACTIVITY_NEW_TASK
+ * @see #FLAG_ACTIVITY_NO_HISTORY
+ * @see #FLAG_ACTIVITY_SINGLE_TOP
+ * @see #FLAG_RECEIVER_REGISTERED_ONLY
+ */
+ public Intent setFlags(int flags) {
+ mFlags = flags;
+ return this;
+ }
+
+ /**
+ * Add additional flags to the intent (or with existing flags
+ * value).
+ *
+ * @param flags The new flags to set.
+ *
+ * @return Returns the same Intent object, for chaining multiple calls
+ * into a single statement.
+ *
+ * @see #setFlags
+ */
+ public Intent addFlags(int flags) {
+ mFlags |= flags;
+ return this;
+ }
+
+ /**
+ * (Usually optional) Explicitly set the component to handle the intent.
+ * If left with the default value of null, the system will determine the
+ * appropriate class to use based on the other fields (action, data,
+ * type, categories) in the Intent. If this class is defined, the
+ * specified class will always be used regardless of the other fields. You
+ * should only set this value when you know you absolutely want a specific
+ * class to be used; otherwise it is better to let the system find the
+ * appropriate class so that you will respect the installed applications
+ * and user preferences.
+ *
+ * @param component The name of the application component to handle the
+ * intent, or null to let the system find one for you.
+ *
+ * @return Returns the same Intent object, for chaining multiple calls
+ * into a single statement.
+ *
+ * @see #setClass
+ * @see #setClassName(Context, String)
+ * @see #setClassName(String, String)
+ * @see #getComponent
+ * @see #resolveActivity
+ */
+ public Intent setComponent(ComponentName component) {
+ mComponent = component;
+ return this;
+ }
+
+ /**
+ * Convenience for calling {@link #setComponent} with an
+ * explicit class name.
+ *
+ * @param packageContext A Context of the application package implementing
+ * this class.
+ * @param className The name of a class inside of the application package
+ * that will be used as the component for this Intent.
+ *
+ * @return Returns the same Intent object, for chaining multiple calls
+ * into a single statement.
+ *
+ * @see #setComponent
+ * @see #setClass
+ */
+ public Intent setClassName(Context packageContext, String className) {
+ mComponent = new ComponentName(packageContext, className);
+ return this;
+ }
+
+ /**
+ * Convenience for calling {@link #setComponent} with an
+ * explicit application package name and class name.
+ *
+ * @param packageName The name of the package implementing the desired
+ * component.
+ * @param className The name of a class inside of the application package
+ * that will be used as the component for this Intent.
+ *
+ * @return Returns the same Intent object, for chaining multiple calls
+ * into a single statement.
+ *
+ * @see #setComponent
+ * @see #setClass
+ */
+ public Intent setClassName(String packageName, String className) {
+ mComponent = new ComponentName(packageName, className);
+ return this;
+ }
+
+ /**
+ * Convenience for calling {@link #setComponent(ComponentName)} with the
+ * name returned by a {@link Class} object.
+ *
+ * @param packageContext A Context of the application package implementing
+ * this class.
+ * @param cls The class name to set, equivalent to
+ * <code>setClassName(context, cls.getName())</code>.
+ *
+ * @return Returns the same Intent object, for chaining multiple calls
+ * into a single statement.
+ *
+ * @see #setComponent
+ */
+ public Intent setClass(Context packageContext, Class<?> cls) {
+ mComponent = new ComponentName(packageContext, cls);
+ return this;
+ }
+
+ /**
+ * Use with {@link #fillIn} to allow the current action value to be
+ * overwritten, even if it is already set.
+ */
+ public static final int FILL_IN_ACTION = 1<<0;
+
+ /**
+ * Use with {@link #fillIn} to allow the current data or type value
+ * overwritten, even if it is already set.
+ */
+ public static final int FILL_IN_DATA = 1<<1;
+
+ /**
+ * Use with {@link #fillIn} to allow the current categories to be
+ * overwritten, even if they are already set.
+ */
+ public static final int FILL_IN_CATEGORIES = 1<<2;
+
+ /**
+ * Use with {@link #fillIn} to allow the current component value to be
+ * overwritten, even if it is already set.
+ */
+ public static final int FILL_IN_COMPONENT = 1<<3;
+
+ /**
+ * Copy the contents of <var>other</var> in to this object, but only
+ * where fields are not defined by this object. For purposes of a field
+ * being defined, the following pieces of data in the Intent are
+ * considered to be separate fields:
+ *
+ * <ul>
+ * <li> action, as set by {@link #setAction}.
+ * <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> component, as set by {@link #setComponent(ComponentName)} or
+ * related methods.
+ * <li> each top-level name in the associated extras.
+ * </ul>
+ *
+ * <p>In addition, you can use the {@link #FILL_IN_ACTION},
+ * {@link #FILL_IN_DATA}, {@link #FILL_IN_CATEGORIES}, and
+ * {@link #FILL_IN_COMPONENT} to override the restriction where the
+ * corresponding field will not be replaced if it is already set.
+ *
+ * <p>For example, consider Intent A with {data="foo", categories="bar"}
+ * and Intent B with {action="gotit", data-type="some/thing",
+ * categories="one","two"}.
+ *
+ * <p>Calling A.fillIn(B, Intent.FILL_IN_DATA) will result in A now
+ * containing: {action="gotit", data-type="some/thing",
+ * categories="bar"}.
+ *
+ * @param other Another Intent whose values are to be used to fill in
+ * the current one.
+ * @param flags Options to control which fields can be filled in.
+ *
+ * @return Returns a bit mask of {@link #FILL_IN_ACTION},
+ * {@link #FILL_IN_DATA}, {@link #FILL_IN_CATEGORIES}, and
+ * {@link #FILL_IN_COMPONENT} indicating which fields were changed.
+ */
+ public int fillIn(Intent other, int flags) {
+ int changes = 0;
+ if ((mAction == null && other.mAction == null)
+ || (flags&FILL_IN_ACTION) != 0) {
+ mAction = other.mAction;
+ changes |= FILL_IN_ACTION;
+ }
+ if ((mData == null && mType == null &&
+ (other.mData != null || other.mType != null))
+ || (flags&FILL_IN_DATA) != 0) {
+ mData = other.mData;
+ mType = other.mType;
+ changes |= FILL_IN_DATA;
+ }
+ if ((mCategories == null && other.mCategories == null)
+ || (flags&FILL_IN_CATEGORIES) != 0) {
+ if (other.mCategories != null) {
+ mCategories = new HashSet<String>(other.mCategories);
+ }
+ changes |= FILL_IN_CATEGORIES;
+ }
+ if ((mComponent == null && other.mComponent == null)
+ || (flags&FILL_IN_COMPONENT) != 0) {
+ mComponent = other.mComponent;
+ changes |= FILL_IN_COMPONENT;
+ }
+ mFlags |= other.mFlags;
+ if (mExtras == null) {
+ if (other.mExtras != null) {
+ mExtras = new Bundle(other.mExtras);
+ }
+ } else if (other.mExtras != null) {
+ try {
+ Bundle newb = new Bundle(other.mExtras);
+ newb.putAll(mExtras);
+ mExtras = newb;
+ } catch (RuntimeException e) {
+ // Modifying the extras can cause us to unparcel the contents
+ // of the bundle, and if we do this in the system process that
+ // may fail. We really should handle this (i.e., the Bundle
+ // impl shouldn't be on top of a plain map), but for now just
+ // ignore it and keep the original contents. :(
+ Log.w("Intent", "Failure filling in extras", e);
+ }
+ }
+ return changes;
+ }
+
+ /**
+ * Wrapper class holding an Intent and implementing comparisons on it for
+ * the purpose of filtering. The class implements its
+ * {@link #equals equals()} and {@link #hashCode hashCode()} methods as
+ * simple calls to {@link Intent#filterEquals(Intent)} filterEquals()} and
+ * {@link android.content.Intent#filterHashCode()} filterHashCode()}
+ * on the wrapped Intent.
+ */
+ public static final class FilterComparison {
+ private final Intent mIntent;
+ private final int mHashCode;
+
+ public FilterComparison(Intent intent) {
+ mIntent = intent;
+ mHashCode = intent.filterHashCode();
+ }
+
+ /**
+ * Return the Intent that this FilterComparison represents.
+ * @return Returns the Intent held by the FilterComparison. Do
+ * not modify!
+ */
+ public Intent getIntent() {
+ return mIntent;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ Intent other;
+ try {
+ other = ((FilterComparison)obj).mIntent;
+ } catch (ClassCastException e) {
+ return false;
+ }
+
+ return mIntent.filterEquals(other);
+ }
+
+ @Override
+ public int hashCode() {
+ return mHashCode;
+ }
+ }
+
+ /**
+ * Determine if two intents are the same for the purposes of intent
+ * resolution (filtering). That is, if their action, data, type,
+ * class, and categories are the same. This does <em>not</em> compare
+ * any extra data included in the intents.
+ *
+ * @param other The other Intent to compare against.
+ *
+ * @return Returns true if action, data, type, class, and categories
+ * are the same.
+ */
+ public boolean filterEquals(Intent other) {
+ if (other == null) {
+ return false;
+ }
+ if (mAction != other.mAction) {
+ if (mAction != null) {
+ if (!mAction.equals(other.mAction)) {
+ return false;
+ }
+ } else {
+ if (!other.mAction.equals(mAction)) {
+ return false;
+ }
+ }
+ }
+ if (mData != other.mData) {
+ if (mData != null) {
+ if (!mData.equals(other.mData)) {
+ return false;
+ }
+ } else {
+ if (!other.mData.equals(mData)) {
+ return false;
+ }
+ }
+ }
+ if (mType != other.mType) {
+ if (mType != null) {
+ if (!mType.equals(other.mType)) {
+ return false;
+ }
+ } else {
+ if (!other.mType.equals(mType)) {
+ return false;
+ }
+ }
+ }
+ if (mComponent != other.mComponent) {
+ if (mComponent != null) {
+ if (!mComponent.equals(other.mComponent)) {
+ return false;
+ }
+ } else {
+ if (!other.mComponent.equals(mComponent)) {
+ return false;
+ }
+ }
+ }
+ if (mCategories != other.mCategories) {
+ if (mCategories != null) {
+ if (!mCategories.equals(other.mCategories)) {
+ return false;
+ }
+ } else {
+ if (!other.mCategories.equals(mCategories)) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Generate hash code that matches semantics of filterEquals().
+ *
+ * @return Returns the hash value of the action, data, type, class, and
+ * categories.
+ *
+ * @see #filterEquals
+ */
+ public int filterHashCode() {
+ int code = 0;
+ if (mAction != null) {
+ code += mAction.hashCode();
+ }
+ if (mData != null) {
+ code += mData.hashCode();
+ }
+ if (mType != null) {
+ code += mType.hashCode();
+ }
+ if (mComponent != null) {
+ code += mComponent.hashCode();
+ }
+ if (mCategories != null) {
+ code += mCategories.hashCode();
+ }
+ return code;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder b = new StringBuilder();
+
+ b.append("Intent {");
+ if (mAction != null) b.append(" action=").append(mAction);
+ if (mCategories != null) {
+ b.append(" categories={");
+ Iterator<String> i = mCategories.iterator();
+ boolean didone = false;
+ while (i.hasNext()) {
+ if (didone) b.append(",");
+ didone = true;
+ b.append(i.next());
+ }
+ b.append("}");
+ }
+ if (mData != null) b.append(" data=").append(mData);
+ if (mType != null) b.append(" type=").append(mType);
+ if (mFlags != 0) b.append(" flags=0x").append(Integer.toHexString(mFlags));
+ if (mComponent != null) b.append(" comp=").append(mComponent.toShortString());
+ if (mExtras != null) b.append(" (has extras)");
+ b.append(" }");
+
+ return b.toString();
+ }
+
+ public String toURI() {
+ StringBuilder uri = new StringBuilder(mData != null ? mData.toString() : "");
+
+ uri.append("#Intent;");
+
+ if (mAction != null) {
+ uri.append("action=").append(mAction).append(';');
+ }
+ if (mCategories != null) {
+ for (String category : mCategories) {
+ uri.append("category=").append(category).append(';');
+ }
+ }
+ if (mType != null) {
+ uri.append("type=").append(mType).append(';');
+ }
+ if (mFlags != 0) {
+ uri.append("launchFlags=0x").append(Integer.toHexString(mFlags)).append(';');
+ }
+ if (mComponent != null) {
+ uri.append("component=").append(mComponent.flattenToShortString()).append(';');
+ }
+ if (mExtras != null) {
+ for (String key : mExtras.keySet()) {
+ final Object value = mExtras.get(key);
+ char entryType =
+ value instanceof String ? 'S' :
+ value instanceof Boolean ? 'B' :
+ value instanceof Byte ? 'b' :
+ value instanceof Character ? 'c' :
+ value instanceof Double ? 'd' :
+ value instanceof Float ? 'f' :
+ value instanceof Integer ? 'i' :
+ value instanceof Long ? 'l' :
+ value instanceof Short ? 's' :
+ '\0';
+
+ if (entryType != '\0') {
+ uri.append(entryType);
+ uri.append('.');
+ uri.append(Uri.encode(key));
+ uri.append('=');
+ uri.append(Uri.encode(value.toString()));
+ uri.append(';');
+ }
+ }
+ }
+
+ uri.append("end");
+
+ return uri.toString();
+ }
+
+ public int describeContents() {
+ return (mExtras != null) ? mExtras.describeContents() : 0;
+ }
+
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeString(mAction);
+ Uri.writeToParcel(out, mData);
+ out.writeString(mType);
+ out.writeInt(mFlags);
+ ComponentName.writeToParcel(mComponent, out);
+
+ if (mCategories != null) {
+ out.writeInt(mCategories.size());
+ for (String category : mCategories) {
+ out.writeString(category);
+ }
+ } else {
+ out.writeInt(0);
+ }
+
+ out.writeBundle(mExtras);
+ }
+
+ public static final Parcelable.Creator<Intent> CREATOR
+ = new Parcelable.Creator<Intent>() {
+ public Intent createFromParcel(Parcel in) {
+ return new Intent(in);
+ }
+ public Intent[] newArray(int size) {
+ return new Intent[size];
+ }
+ };
+
+ private Intent(Parcel in) {
+ readFromParcel(in);
+ }
+
+ public void readFromParcel(Parcel in) {
+ mAction = in.readString();
+ mData = Uri.CREATOR.createFromParcel(in);
+ mType = in.readString();
+ mFlags = in.readInt();
+ mComponent = ComponentName.readFromParcel(in);
+
+ int N = in.readInt();
+ if (N > 0) {
+ mCategories = new HashSet<String>();
+ int i;
+ for (i=0; i<N; i++) {
+ mCategories.add(in.readString());
+ }
+ } else {
+ mCategories = null;
+ }
+
+ mExtras = in.readBundle();
+ }
+
+ /**
+ * Parses the "intent" element (and its children) from XML and instantiates
+ * an Intent object. The given XML parser should be located at the tag
+ * where parsing should start (often named "intent"), from which the
+ * basic action, data, type, and package and class name will be
+ * retrieved. The function will then parse in to any child elements,
+ * looking for <category android:name="xxx"> tags to add categories and
+ * <extra android:name="xxx" android:value="yyy"> to attach extra data
+ * to the intent.
+ *
+ * @param resources The Resources to use when inflating resources.
+ * @param parser The XML parser pointing at an "intent" tag.
+ * @param attrs The AttributeSet interface for retrieving extended
+ * attribute data at the current <var>parser</var> location.
+ * @return An Intent object matching the XML data.
+ * @throws XmlPullParserException If there was an XML parsing error.
+ * @throws IOException If there was an I/O error.
+ */
+ public static Intent parseIntent(Resources resources, XmlPullParser parser, AttributeSet attrs)
+ throws XmlPullParserException, IOException {
+ Intent intent = new Intent();
+
+ TypedArray sa = resources.obtainAttributes(attrs,
+ com.android.internal.R.styleable.Intent);
+
+ intent.setAction(sa.getString(com.android.internal.R.styleable.Intent_action));
+
+ String data = sa.getString(com.android.internal.R.styleable.Intent_data);
+ String mimeType = sa.getString(com.android.internal.R.styleable.Intent_mimeType);
+ intent.setDataAndType(data != null ? Uri.parse(data) : null, mimeType);
+
+ String packageName = sa.getString(com.android.internal.R.styleable.Intent_targetPackage);
+ String className = sa.getString(com.android.internal.R.styleable.Intent_targetClass);
+ if (packageName != null && className != null) {
+ intent.setComponent(new ComponentName(packageName, className));
+ }
+
+ sa.recycle();
+
+ int outerDepth = parser.getDepth();
+ int type;
+ while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
+ && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+ if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+ continue;
+ }
+
+ String nodeName = parser.getName();
+ if (nodeName.equals("category")) {
+ sa = resources.obtainAttributes(attrs,
+ com.android.internal.R.styleable.IntentCategory);
+ String cat = sa.getString(com.android.internal.R.styleable.IntentCategory_name);
+ sa.recycle();
+
+ if (cat != null) {
+ intent.addCategory(cat);
+ }
+ XmlUtils.skipCurrentTag(parser);
+
+ } else if (nodeName.equals("extra")) {
+ parseExtra(resources, intent, parser, attrs);
+
+ } else {
+ XmlUtils.skipCurrentTag(parser);
+ }
+ }
+
+ return intent;
+ }
+
+ private static void parseExtra(Resources resources, Intent intent, XmlPullParser parser,
+ AttributeSet attrs) throws XmlPullParserException, IOException {
+ TypedArray sa = resources.obtainAttributes(attrs,
+ com.android.internal.R.styleable.IntentExtra);
+
+ String name = sa.getString(
+ com.android.internal.R.styleable.IntentExtra_name);
+ if (name == null) {
+ sa.recycle();
+ throw new RuntimeException(
+ "<extra> requires an android:name attribute at "
+ + parser.getPositionDescription());
+ }
+
+ TypedValue v = sa.peekValue(
+ com.android.internal.R.styleable.IntentExtra_value);
+ if (v != null) {
+ if (v.type == TypedValue.TYPE_STRING) {
+ CharSequence cs = v.coerceToString();
+ intent.putExtra(name, cs != null ? cs.toString() : null);
+ } else if (v.type == TypedValue.TYPE_INT_BOOLEAN) {
+ intent.putExtra(name, v.data != 0);
+ } else if (v.type >= TypedValue.TYPE_FIRST_INT
+ && v.type <= TypedValue.TYPE_LAST_INT) {
+ intent.putExtra(name, v.data);
+ } else if (v.type == TypedValue.TYPE_FLOAT) {
+ intent.putExtra(name, v.getFloat());
+ } else {
+ sa.recycle();
+ throw new RuntimeException(
+ "<extra> only supports string, integer, float, color, and boolean at "
+ + parser.getPositionDescription());
+ }
+ } else {
+ sa.recycle();
+ throw new RuntimeException(
+ "<extra> requires an android:value or android:resource attribute at "
+ + parser.getPositionDescription());
+ }
+
+ sa.recycle();
+
+ XmlUtils.skipCurrentTag(parser);
+ }
+}
diff --git a/core/java/android/content/IntentFilter.aidl b/core/java/android/content/IntentFilter.aidl
new file mode 100644
index 0000000..a9bcd5e
--- /dev/null
+++ b/core/java/android/content/IntentFilter.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+parcelable IntentFilter;
diff --git a/core/java/android/content/IntentFilter.java b/core/java/android/content/IntentFilter.java
new file mode 100644
index 0000000..e81bc86
--- /dev/null
+++ b/core/java/android/content/IntentFilter.java
@@ -0,0 +1,1408 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.Set;
+
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.PatternMatcher;
+import android.util.AndroidException;
+import android.util.Config;
+import android.util.Log;
+import android.util.Printer;
+import com.android.internal.util.XmlUtils;
+
+/**
+ * Structured description of Intent values to be matched. An IntentFilter can
+ * match against actions, categories, and data (either via its type, scheme,
+ * and/or path) in an Intent. It also includes a "priority" value which is
+ * used to order multiple matching filters.
+ *
+ * <p>IntentFilter objects are often created in XML as part of a package's
+ * {@link android.R.styleable#AndroidManifest AndroidManifest.xml} file,
+ * using {@link android.R.styleable#AndroidManifestIntentFilter intent-filter}
+ * tags.
+ *
+ * <p>There are three Intent characteristics you can filter on: the
+ * <em>action</em>, <em>data</em>, and <em>categories</em>. For each of these
+ * characteristics you can provide
+ * multiple possible matching values (via {@link #addAction},
+ * {@link #addDataType}, {@link #addDataScheme} {@link #addDataAuthority},
+ * {@link #addDataPath}, and {@link #addCategory}, respectively).
+ * For actions, the field
+ * will not be tested if no values have been given (treating it as a wildcard);
+ * if no data characteristics are specified, however, then the filter will
+ * only match intents that contain no data.
+ *
+ * <p>The data characteristic is
+ * itself divided into three attributes: type, scheme, authority, and path.
+ * Any that are
+ * specified must match the contents of the Intent. If you specify a scheme
+ * but no type, only Intent that does not have a type (such as mailto:) will
+ * match; a content: URI will never match because they always have a MIME type
+ * that is supplied by their content provider. Specifying a type with no scheme
+ * has somewhat special meaning: it will match either an Intent with no URI
+ * field, or an Intent with a content: or file: URI. If you specify neither,
+ * then only an Intent with no data or type will match. To specify an authority,
+ * you must also specify one or more schemes that it is associated with.
+ * To specify a path, you also must specify both one or more authorities and
+ * one or more schemes it is associated with.
+ *
+ * <p>A match is based on the following rules. Note that
+ * for an IntentFilter to match an Intent, three conditions must hold:
+ * the <strong>action</strong> and <strong>category</strong> must match, and
+ * the data (both the <strong>data type</strong> and
+ * <strong>data scheme+authority+path</strong> if specified) must match.
+ *
+ * <p><strong>Action</strong> matches if any of the given values match the
+ * Intent action, <em>or</em> if no actions were specified in the filter.
+ *
+ * <p><strong>Data Type</strong> matches if any of the given values match the
+ * Intent type. The Intent
+ * type is determined by calling {@link Intent#resolveType}. A wildcard can be
+ * used for the MIME sub-type, in both the Intent and IntentFilter, so that the
+ * type "audio/*" will match "audio/mpeg", "audio/aiff", "audio/*", etc.
+ *
+ * <p><strong>Data Scheme</strong> matches if any of the given values match the
+ * Intent data's scheme.
+ * The Intent scheme is determined by calling {@link Intent#getData}
+ * and {@link android.net.Uri#getScheme} on that URI.
+ *
+ * <p><strong>Data Authority</strong> matches if any of the given values match
+ * the Intent's data authority <em>and</em> one of the data scheme's in the filter
+ * has matched the Intent, <em>or</em> no authories were supplied in the filter.
+ * The Intent authority is determined by calling
+ * {@link Intent#getData} and {@link android.net.Uri#getAuthority} on that URI.
+ *
+ * <p><strong>Data Path</strong> matches if any of the given values match the
+ * Intent's data path <em>and</em> both a scheme and authority in the filter
+ * has matched against the Intent, <em>or</em> no paths were supplied in the
+ * filter. The Intent authority is determined by calling
+ * {@link Intent#getData} and {@link android.net.Uri#getPath} on that URI.
+ *
+ * <p><strong>Categories</strong> match if <em>all</em> of the categories in
+ * the Intent match categories given in the filter. Extra categories in the
+ * filter that are not in the Intent will not cause the match to fail. Note
+ * that unlike the action, an IntentFilter with no categories
+ * will only match an Intent that does not have any categories.
+ */
+public class IntentFilter implements Parcelable {
+ private static final String SGLOB_STR = "sglob";
+ private static final String PREFIX_STR = "prefix";
+ private static final String LITERAL_STR = "literal";
+ private static final String PATH_STR = "path";
+ private static final String PORT_STR = "port";
+ private static final String HOST_STR = "host";
+ private static final String AUTH_STR = "auth";
+ private static final String SCHEME_STR = "scheme";
+ private static final String TYPE_STR = "type";
+ private static final String CAT_STR = "cat";
+ private static final String NAME_STR = "name";
+ private static final String ACTION_STR = "action";
+
+ /**
+ * The filter {@link #setPriority} value at which system high-priority
+ * receivers are placed; that is, receivers that should execute before
+ * application code. Applications should never use filters with this or
+ * higher priorities.
+ *
+ * @see #setPriority
+ */
+ public static final int SYSTEM_HIGH_PRIORITY = 1000;
+
+ /**
+ * The filter {@link #setPriority} value at which system low-priority
+ * receivers are placed; that is, receivers that should execute after
+ * application code. Applications should never use filters with this or
+ * lower priorities.
+ *
+ * @see #setPriority
+ */
+ public static final int SYSTEM_LOW_PRIORITY = -1000;
+
+ /**
+ * The part of a match constant that describes the category of match
+ * that occurred. May be either {@link #MATCH_CATEGORY_EMPTY},
+ * {@link #MATCH_CATEGORY_SCHEME}, {@link #MATCH_CATEGORY_HOST},
+ * {@link #MATCH_CATEGORY_PORT},
+ * {@link #MATCH_CATEGORY_PATH}, or {@link #MATCH_CATEGORY_TYPE}. Higher
+ * values indicate a better match.
+ */
+ public static final int MATCH_CATEGORY_MASK = 0xfff0000;
+
+ /**
+ * The part of a match constant that applies a quality adjustment to the
+ * basic category of match. The value {@link #MATCH_ADJUSTMENT_NORMAL}
+ * is no adjustment; higher numbers than that improve the quality, while
+ * lower numbers reduce it.
+ */
+ public static final int MATCH_ADJUSTMENT_MASK = 0x000ffff;
+
+ /**
+ * Quality adjustment applied to the category of match that signifies
+ * the default, base value; higher numbers improve the quality while
+ * lower numbers reduce it.
+ */
+ public static final int MATCH_ADJUSTMENT_NORMAL = 0x8000;
+
+ /**
+ * The filter matched an intent that had no data specified.
+ */
+ public static final int MATCH_CATEGORY_EMPTY = 0x0100000;
+ /**
+ * The filter matched an intent with the same data URI scheme.
+ */
+ public static final int MATCH_CATEGORY_SCHEME = 0x0200000;
+ /**
+ * The filter matched an intent with the same data URI scheme and
+ * authority host.
+ */
+ public static final int MATCH_CATEGORY_HOST = 0x0300000;
+ /**
+ * The filter matched an intent with the same data URI scheme and
+ * authority host and port.
+ */
+ public static final int MATCH_CATEGORY_PORT = 0x0400000;
+ /**
+ * The filter matched an intent with the same data URI scheme,
+ * authority, and path.
+ */
+ public static final int MATCH_CATEGORY_PATH = 0x0500000;
+ /**
+ * The filter matched an intent with the same data MIME type.
+ */
+ public static final int MATCH_CATEGORY_TYPE = 0x0600000;
+
+ /**
+ * The filter didn't match due to different MIME types.
+ */
+ public static final int NO_MATCH_TYPE = -1;
+ /**
+ * The filter didn't match due to different data URIs.
+ */
+ public static final int NO_MATCH_DATA = -2;
+ /**
+ * The filter didn't match due to different actions.
+ */
+ public static final int NO_MATCH_ACTION = -3;
+ /**
+ * The filter didn't match because it required one or more categories
+ * that were not in the Intent.
+ */
+ public static final int NO_MATCH_CATEGORY = -4;
+
+ private int mPriority;
+ private final ArrayList<String> mActions;
+ private ArrayList<String> mCategories = null;
+ private ArrayList<String> mDataSchemes = null;
+ private ArrayList<AuthorityEntry> mDataAuthorities = null;
+ private ArrayList<PatternMatcher> mDataPaths = null;
+ private ArrayList<String> mDataTypes = null;
+ private boolean mHasPartialTypes = false;
+
+ // These functions are the start of more optimized code for managing
+ // the string sets... not yet implemented.
+
+ private static int findStringInSet(String[] set, String string,
+ int[] lengths, int lenPos) {
+ if (set == null) return -1;
+ final int N = lengths[lenPos];
+ for (int i=0; i<N; i++) {
+ if (set[i].equals(string)) return i;
+ }
+ return -1;
+ }
+
+ private static String[] addStringToSet(String[] set, String string,
+ int[] lengths, int lenPos) {
+ if (findStringInSet(set, string, lengths, lenPos) >= 0) return set;
+ if (set == null) {
+ set = new String[2];
+ set[0] = string;
+ lengths[lenPos] = 1;
+ return set;
+ }
+ final int N = lengths[lenPos];
+ if (N < set.length) {
+ set[N] = string;
+ lengths[lenPos] = N+1;
+ return set;
+ }
+
+ String[] newSet = new String[(N*3)/2 + 2];
+ System.arraycopy(set, 0, newSet, 0, N);
+ set = newSet;
+ set[N] = string;
+ lengths[lenPos] = N+1;
+ return set;
+ }
+
+ private static String[] removeStringFromSet(String[] set, String string,
+ int[] lengths, int lenPos) {
+ int pos = findStringInSet(set, string, lengths, lenPos);
+ if (pos < 0) return set;
+ final int N = lengths[lenPos];
+ if (N > (set.length/4)) {
+ int copyLen = N-(pos+1);
+ if (copyLen > 0) {
+ System.arraycopy(set, pos+1, set, pos, copyLen);
+ }
+ set[N-1] = null;
+ lengths[lenPos] = N-1;
+ return set;
+ }
+
+ String[] newSet = new String[set.length/3];
+ if (pos > 0) System.arraycopy(set, 0, newSet, 0, pos);
+ if ((pos+1) < N) System.arraycopy(set, pos+1, newSet, pos, N-(pos+1));
+ return newSet;
+ }
+
+ /**
+ * This exception is thrown when a given MIME type does not have a valid
+ * syntax.
+ */
+ public static class MalformedMimeTypeException extends AndroidException {
+ public MalformedMimeTypeException() {
+ }
+
+ public MalformedMimeTypeException(String name) {
+ super(name);
+ }
+ };
+
+ /**
+ * Create a new IntentFilter instance with a specified action and MIME
+ * type, where you know the MIME type is correctly formatted. This catches
+ * the {@link MalformedMimeTypeException} exception that the constructor
+ * can call and turns it into a runtime exception.
+ *
+ * @param action The action to match, i.e. Intent.ACTION_VIEW.
+ * @param dataType The type to match, i.e. "vnd.android.cursor.dir/person".
+ *
+ * @return A new IntentFilter for the given action and type.
+ *
+ * @see #IntentFilter(String, String)
+ */
+ public static IntentFilter create(String action, String dataType) {
+ try {
+ return new IntentFilter(action, dataType);
+ } catch (MalformedMimeTypeException e) {
+ throw new RuntimeException("Bad MIME type", e);
+ }
+ }
+
+ /**
+ * New empty IntentFilter.
+ */
+ public IntentFilter() {
+ mPriority = 0;
+ mActions = new ArrayList<String>();
+ }
+
+ /**
+ * New IntentFilter that matches a single action with no data. If
+ * no data characteristics are subsequently specified, then the
+ * filter will only match intents that contain no data.
+ *
+ * @param action The action to match, i.e. Intent.ACTION_MAIN.
+ */
+ public IntentFilter(String action) {
+ mPriority = 0;
+ mActions = new ArrayList<String>();
+ addAction(action);
+ }
+
+ /**
+ * New IntentFilter that matches a single action and data type.
+ *
+ * <p>Throws {@link MalformedMimeTypeException} if the given MIME type is
+ * not syntactically correct.
+ *
+ * @param action The action to match, i.e. Intent.ACTION_VIEW.
+ * @param dataType The type to match, i.e. "vnd.android.cursor.dir/person".
+ *
+ */
+ public IntentFilter(String action, String dataType)
+ throws MalformedMimeTypeException {
+ mPriority = 0;
+ mActions = new ArrayList<String>();
+ addDataType(dataType);
+ }
+
+ /**
+ * New IntentFilter containing a copy of an existing filter.
+ *
+ * @param o The original filter to copy.
+ */
+ public IntentFilter(IntentFilter o) {
+ mPriority = o.mPriority;
+ mActions = new ArrayList<String>(o.mActions);
+ if (o.mCategories != null) {
+ mCategories = new ArrayList<String>(o.mCategories);
+ }
+ if (o.mDataTypes != null) {
+ mDataTypes = new ArrayList<String>(o.mDataTypes);
+ }
+ if (o.mDataSchemes != null) {
+ mDataSchemes = new ArrayList<String>(o.mDataSchemes);
+ }
+ if (o.mDataAuthorities != null) {
+ mDataAuthorities = new ArrayList<AuthorityEntry>(o.mDataAuthorities);
+ }
+ if (o.mDataPaths != null) {
+ mDataPaths = new ArrayList<PatternMatcher>(o.mDataPaths);
+ }
+ mHasPartialTypes = o.mHasPartialTypes;
+ }
+
+ /**
+ * Modify priority of this filter. The default priority is 0. Positive
+ * values will be before the default, lower values will be after it.
+ * Applications must use a value that is larger than
+ * {@link #SYSTEM_LOW_PRIORITY} and smaller than
+ * {@link #SYSTEM_HIGH_PRIORITY} .
+ *
+ * @param priority The new priority value.
+ *
+ * @see #getPriority
+ * @see #SYSTEM_LOW_PRIORITY
+ * @see #SYSTEM_HIGH_PRIORITY
+ */
+ public final void setPriority(int priority) {
+ mPriority = priority;
+ }
+
+ /**
+ * Return the priority of this filter.
+ *
+ * @return The priority of the filter.
+ *
+ * @see #setPriority
+ */
+ public final int getPriority() {
+ return mPriority;
+ }
+
+ /**
+ * Add a new Intent action to match against. If any actions are included
+ * in the filter, then an Intent's action must be one of those values for
+ * it to match. If no actions are included, the Intent action is ignored.
+ *
+ * @param action Name of the action to match, i.e. Intent.ACTION_VIEW.
+ */
+ public final void addAction(String action) {
+ if (!mActions.contains(action)) {
+ mActions.add(action.intern());
+ }
+ }
+
+ /**
+ * Return the number of actions in the filter.
+ */
+ public final int countActions() {
+ return mActions.size();
+ }
+
+ /**
+ * Return an action in the filter.
+ */
+ public final String getAction(int index) {
+ return mActions.get(index);
+ }
+
+ /**
+ * Is the given action included in the filter? Note that if the filter
+ * does not include any actions, false will <em>always</em> be returned.
+ *
+ * @param action The action to look for.
+ *
+ * @return True if the action is explicitly mentioned in the filter.
+ */
+ public final boolean hasAction(String action) {
+ return mActions.contains(action);
+ }
+
+ /**
+ * Match this filter against an Intent's action. If the filter does not
+ * specify any actions, the match will always fail.
+ *
+ * @param action The desired action to look for.
+ *
+ * @return True if the action is listed in the filter or the filter does
+ * not specify any actions.
+ */
+ public final boolean matchAction(String action) {
+ if (action == null || mActions == null || mActions.size() == 0) {
+ return false;
+ }
+ return mActions.contains(action);
+ }
+
+ /**
+ * Return an iterator over the filter's actions. If there are no actions,
+ * returns null.
+ */
+ public final Iterator<String> actionsIterator() {
+ return mActions != null ? mActions.iterator() : null;
+ }
+
+ /**
+ * Add a new Intent data type to match against. If any types are
+ * included in the filter, then an Intent's data must be <em>either</em>
+ * one of these types <em>or</em> a matching scheme. If no data types
+ * are included, then an Intent will only match if it specifies no data.
+ *
+ * <p>Throws {@link MalformedMimeTypeException} if the given MIME type is
+ * not syntactically correct.
+ *
+ * @param type Name of the data type to match, i.e. "vnd.android.cursor.dir/person".
+ *
+ * @see #matchData
+ */
+ public final void addDataType(String type)
+ throws MalformedMimeTypeException {
+ final int slashpos = type.indexOf('/');
+ final int typelen = type.length();
+ if (slashpos > 0 && typelen >= slashpos+2) {
+ if (mDataTypes == null) mDataTypes = new ArrayList<String>();
+ if (typelen == slashpos+2 && type.charAt(slashpos+1) == '*') {
+ String str = type.substring(0, slashpos);
+ if (!mDataTypes.contains(str)) {
+ mDataTypes.add(str.intern());
+ }
+ mHasPartialTypes = true;
+ } else {
+ if (!mDataTypes.contains(type)) {
+ mDataTypes.add(type.intern());
+ }
+ }
+ return;
+ }
+
+ throw new MalformedMimeTypeException(type);
+ }
+
+ /**
+ * Is the given data type included in the filter? Note that if the filter
+ * does not include any type, false will <em>always</em> be returned.
+ *
+ * @param type The data type to look for.
+ *
+ * @return True if the type is explicitly mentioned in the filter.
+ */
+ public final boolean hasDataType(String type) {
+ return mDataTypes != null && findMimeType(type);
+ }
+
+ /**
+ * Return the number of data types in the filter.
+ */
+ public final int countDataTypes() {
+ return mDataTypes != null ? mDataTypes.size() : 0;
+ }
+
+ /**
+ * Return a data type in the filter.
+ */
+ public final String getDataType(int index) {
+ return mDataTypes.get(index);
+ }
+
+ /**
+ * Return an iterator over the filter's data types.
+ */
+ public final Iterator<String> typesIterator() {
+ return mDataTypes != null ? mDataTypes.iterator() : null;
+ }
+
+ /**
+ * Add a new Intent data scheme to match against. If any schemes are
+ * included in the filter, then an Intent's data must be <em>either</em>
+ * one of these schemes <em>or</em> a matching data type. If no schemes
+ * are included, then an Intent will match only if it includes no data.
+ *
+ * @param scheme Name of the scheme to match, i.e. "http".
+ *
+ * @see #matchData
+ */
+ public final void addDataScheme(String scheme) {
+ if (mDataSchemes == null) mDataSchemes = new ArrayList<String>();
+ if (!mDataSchemes.contains(scheme)) {
+ mDataSchemes.add(scheme.intern());
+ }
+ }
+
+ /**
+ * Return the number of data schemes in the filter.
+ */
+ public final int countDataSchemes() {
+ return mDataSchemes != null ? mDataSchemes.size() : 0;
+ }
+
+ /**
+ * Return a data scheme in the filter.
+ */
+ public final String getDataScheme(int index) {
+ return mDataSchemes.get(index);
+ }
+
+ /**
+ * Is the given data scheme included in the filter? Note that if the
+ * filter does not include any scheme, false will <em>always</em> be
+ * returned.
+ *
+ * @param scheme The data scheme to look for.
+ *
+ * @return True if the scheme is explicitly mentioned in the filter.
+ */
+ public final boolean hasDataScheme(String scheme) {
+ return mDataSchemes != null && mDataSchemes.contains(scheme);
+ }
+
+ /**
+ * Return an iterator over the filter's data schemes.
+ */
+ public final Iterator<String> schemesIterator() {
+ return mDataSchemes != null ? mDataSchemes.iterator() : null;
+ }
+
+ /**
+ * This is an entry for a single authority in the Iterator returned by
+ * {@link #authoritiesIterator()}.
+ */
+ public final static class AuthorityEntry {
+ private final String mOrigHost;
+ private final String mHost;
+ private final boolean mWild;
+ private final int mPort;
+
+ public AuthorityEntry(String host, String port) {
+ mOrigHost = host;
+ mWild = host.length() > 0 && host.charAt(0) == '*';
+ mHost = mWild ? host.substring(1).intern() : host;
+ mPort = port != null ? Integer.parseInt(port) : -1;
+ }
+
+ AuthorityEntry(Parcel src) {
+ mOrigHost = src.readString();
+ mHost = src.readString();
+ mWild = src.readInt() != 0;
+ mPort = src.readInt();
+ }
+
+ void writeToParcel(Parcel dest) {
+ dest.writeString(mOrigHost);
+ dest.writeString(mHost);
+ dest.writeInt(mWild ? 1 : 0);
+ dest.writeInt(mPort);
+ }
+
+ public String getHost() {
+ return mOrigHost;
+ }
+
+ public int getPort() {
+ return mPort;
+ }
+
+ public int match(Uri data) {
+ String host = data.getHost();
+ if (host == null) {
+ return NO_MATCH_DATA;
+ }
+ if (Config.LOGV) Log.v("IntentFilter",
+ "Match host " + host + ": " + mHost);
+ if (mWild) {
+ if (host.length() < mHost.length()) {
+ return NO_MATCH_DATA;
+ }
+ host = host.substring(host.length()-mHost.length());
+ }
+ if (host.compareToIgnoreCase(mHost) != 0) {
+ return NO_MATCH_DATA;
+ }
+ if (mPort >= 0) {
+ if (mPort != data.getPort()) {
+ return NO_MATCH_DATA;
+ }
+ return MATCH_CATEGORY_PORT;
+ }
+ return MATCH_CATEGORY_HOST;
+ }
+ };
+
+ /**
+ * Add a new Intent data authority to match against. The filter must
+ * include one or more schemes (via {@link #addDataScheme}) for the
+ * authority to be considered. If any authorities are
+ * included in the filter, then an Intent's data must match one of
+ * them. If no authorities are included, then only the scheme must match.
+ *
+ * @param host The host part of the authority to match. May start with a
+ * single '*' to wildcard the front of the host name.
+ * @param port Optional port part of the authority to match. If null, any
+ * port is allowed.
+ *
+ * @see #matchData
+ * @see #addDataScheme
+ */
+ public final void addDataAuthority(String host, String port) {
+ if (mDataAuthorities == null) mDataAuthorities =
+ new ArrayList<AuthorityEntry>();
+ if (port != null) port = port.intern();
+ mDataAuthorities.add(new AuthorityEntry(host.intern(), port));
+ }
+
+ /**
+ * Return the number of data authorities in the filter.
+ */
+ public final int countDataAuthorities() {
+ return mDataAuthorities != null ? mDataAuthorities.size() : 0;
+ }
+
+ /**
+ * Return a data authority in the filter.
+ */
+ public final AuthorityEntry getDataAuthority(int index) {
+ return mDataAuthorities.get(index);
+ }
+
+ /**
+ * Is the given data authority included in the filter? Note that if the
+ * filter does not include any authorities, false will <em>always</em> be
+ * returned.
+ *
+ * @param data The data whose authority is being looked for.
+ *
+ * @return Returns true if the data string matches an authority listed in the
+ * filter.
+ */
+ public final boolean hasDataAuthority(Uri data) {
+ return matchDataAuthority(data) >= 0;
+ }
+
+ /**
+ * Return an iterator over the filter's data authorities.
+ */
+ public final Iterator<AuthorityEntry> authoritiesIterator() {
+ return mDataAuthorities != null ? mDataAuthorities.iterator() : null;
+ }
+
+ /**
+ * Add a new Intent data oath to match against. The filter must
+ * include one or more schemes (via {@link #addDataScheme}) <em>and</em>
+ * one or more authorities (via {@link #addDataAuthority}) for the
+ * path to be considered. If any paths are
+ * included in the filter, then an Intent's data must match one of
+ * them. If no paths are included, then only the scheme/authority must
+ * match.
+ *
+ * <p>The path given here can either be a literal that must directly
+ * match or match against a prefix, or it can be a simple globbing pattern.
+ * If the latter, you can use '*' anywhere in the pattern to match zero
+ * or more instances of the previous character, '.' as a wildcard to match
+ * any character, and '\' to escape the next character.
+ *
+ * @param path Either a raw string that must exactly match the file
+ * path, or a simple pattern, depending on <var>type</var>.
+ * @param type Determines how <var>path</var> will be compared to
+ * determine a match: either {@link PatternMatcher#PATTERN_LITERAL},
+ * {@link PatternMatcher#PATTERN_PREFIX}, or
+ * {@link PatternMatcher#PATTERN_SIMPLE_GLOB}.
+ *
+ * @see #matchData
+ * @see #addDataScheme
+ * @see #addDataAuthority
+ */
+ public final void addDataPath(String path, int type) {
+ if (mDataPaths == null) mDataPaths = new ArrayList<PatternMatcher>();
+ mDataPaths.add(new PatternMatcher(path.intern(), type));
+ }
+
+ /**
+ * Return the number of data paths in the filter.
+ */
+ public final int countDataPaths() {
+ return mDataPaths != null ? mDataPaths.size() : 0;
+ }
+
+ /**
+ * Return a data path in the filter.
+ */
+ public final PatternMatcher getDataPath(int index) {
+ return mDataPaths.get(index);
+ }
+
+ /**
+ * Is the given data path included in the filter? Note that if the
+ * filter does not include any paths, false will <em>always</em> be
+ * returned.
+ *
+ * @param data The data path to look for. This is without the scheme
+ * prefix.
+ *
+ * @return True if the data string matches a path listed in the
+ * filter.
+ */
+ public final boolean hasDataPath(String data) {
+ if (mDataPaths == null) {
+ return false;
+ }
+ Iterator<PatternMatcher> i = mDataPaths.iterator();
+ while (i.hasNext()) {
+ final PatternMatcher pe = i.next();
+ if (pe.match(data)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Return an iterator over the filter's data paths.
+ */
+ public final Iterator<PatternMatcher> pathsIterator() {
+ return mDataPaths != null ? mDataPaths.iterator() : null;
+ }
+
+ /**
+ * Match this intent filter against the given Intent data. This ignores
+ * the data scheme -- unlike {@link #matchData}, the authority will match
+ * regardless of whether there is a matching scheme.
+ *
+ * @param data The data whose authority is being looked for.
+ *
+ * @return Returns either {@link #MATCH_CATEGORY_HOST},
+ * {@link #MATCH_CATEGORY_PORT}, {@link #NO_MATCH_DATA}.
+ */
+ public final int matchDataAuthority(Uri data) {
+ if (mDataAuthorities == null) {
+ return NO_MATCH_DATA;
+ }
+ Iterator<AuthorityEntry> i = mDataAuthorities.iterator();
+ while (i.hasNext()) {
+ final AuthorityEntry ae = i.next();
+ int match = ae.match(data);
+ if (match >= 0) {
+ return match;
+ }
+ }
+ return NO_MATCH_DATA;
+ }
+
+ /**
+ * Match this filter against an Intent's data (type, scheme and path). If
+ * the filter does not specify any types and does not specify any
+ * schemes/paths, the match will only succeed if the intent does not
+ * also specify a type or data.
+ *
+ * <p>Note that to match against an authority, you must also specify a base
+ * scheme the authority is in. To match against a data path, both a scheme
+ * and authority must be specified. If the filter does not specify any
+ * types or schemes that it matches against, it is considered to be empty
+ * (any authority or data path given is ignored, as if it were empty as
+ * well).
+ *
+ * @param type The desired data type to look for, as returned by
+ * Intent.resolveType().
+ * @param scheme The desired data scheme to look for, as returned by
+ * Intent.getScheme().
+ * @param data The full data string to match against, as supplied in
+ * Intent.data.
+ *
+ * @return Returns either a valid match constant (a combination of
+ * {@link #MATCH_CATEGORY_MASK} and {@link #MATCH_ADJUSTMENT_MASK}),
+ * or one of the error codes {@link #NO_MATCH_TYPE} if the type didn't match
+ * or {@link #NO_MATCH_DATA} if the scheme/path didn't match.
+ *
+ * @see #match
+ */
+ public final int matchData(String type, String scheme, Uri data) {
+ final ArrayList<String> types = mDataTypes;
+ final ArrayList<String> schemes = mDataSchemes;
+ final ArrayList<AuthorityEntry> authorities = mDataAuthorities;
+ final ArrayList<PatternMatcher> paths = mDataPaths;
+
+ int match = MATCH_CATEGORY_EMPTY;
+
+ if (types == null && schemes == null) {
+ return ((type == null && data == null)
+ ? (MATCH_CATEGORY_EMPTY+MATCH_ADJUSTMENT_NORMAL) : NO_MATCH_DATA);
+ }
+
+ if (schemes != null) {
+ if (schemes.contains(scheme != null ? scheme : "")) {
+ match = MATCH_CATEGORY_SCHEME;
+ } else {
+ return NO_MATCH_DATA;
+ }
+
+ if (authorities != null) {
+ int authMatch = matchDataAuthority(data);
+ if (authMatch >= 0) {
+ if (paths == null) {
+ match = authMatch;
+ } else if (hasDataPath(data.getPath())) {
+ match = MATCH_CATEGORY_PATH;
+ } else {
+ return NO_MATCH_DATA;
+ }
+ } else {
+ return NO_MATCH_DATA;
+ }
+ }
+ } else {
+ // Special case: match either an Intent with no data URI,
+ // or with a scheme: URI. This is to give a convenience for
+ // the common case where you want to deal with data in a
+ // content provider, which is done by type, and we don't want
+ // to force everyone to say they handle content: or file: URIs.
+ if (scheme != null && !"".equals(scheme)
+ && !"content".equals(scheme)
+ && !"file".equals(scheme)) {
+ return NO_MATCH_DATA;
+ }
+ }
+
+ if (types != null) {
+ if (findMimeType(type)) {
+ match = MATCH_CATEGORY_TYPE;
+ } else {
+ return NO_MATCH_TYPE;
+ }
+ } else {
+ // If no MIME types are specified, then we will only match against
+ // an Intent that does not have a MIME type.
+ if (type != null) {
+ return NO_MATCH_TYPE;
+ }
+ }
+
+ return match + MATCH_ADJUSTMENT_NORMAL;
+ }
+
+ /**
+ * Add a new Intent category to match against. The semantics of
+ * categories is the opposite of actions -- an Intent includes the
+ * categories that it requires, all of which must be included in the
+ * filter in order to match. In other words, adding a category to the
+ * filter has no impact on matching unless that category is specified in
+ * the intent.
+ *
+ * @param category Name of category to match, i.e. Intent.CATEGORY_EMBED.
+ */
+ public final void addCategory(String category) {
+ if (mCategories == null) mCategories = new ArrayList<String>();
+ if (!mCategories.contains(category)) {
+ mCategories.add(category.intern());
+ }
+ }
+
+ /**
+ * Return the number of categories in the filter.
+ */
+ public final int countCategories() {
+ return mCategories != null ? mCategories.size() : 0;
+ }
+
+ /**
+ * Return a category in the filter.
+ */
+ public final String getCategory(int index) {
+ return mCategories.get(index);
+ }
+
+ /**
+ * Is the given category included in the filter?
+ *
+ * @param category The category that the filter supports.
+ *
+ * @return True if the category is explicitly mentioned in the filter.
+ */
+ public final boolean hasCategory(String category) {
+ return mCategories != null && mCategories.contains(category);
+ }
+
+ /**
+ * Return an iterator over the filter's categories.
+ */
+ public final Iterator<String> categoriesIterator() {
+ return mCategories != null ? mCategories.iterator() : null;
+ }
+
+ /**
+ * Match this filter against an Intent's categories. Each category in
+ * the Intent must be specified by the filter; if any are not in the
+ * filter, the match fails.
+ *
+ * @param categories The categories included in the intent, as returned by
+ * Intent.getCategories().
+ *
+ * @return If all categories match (success), null; else the name of the
+ * first category that didn't match.
+ */
+ public final String matchCategories(Set<String> categories) {
+ if (categories == null) {
+ return null;
+ }
+
+ Iterator<String> it = categories.iterator();
+
+ if (mCategories == null) {
+ return it.hasNext() ? it.next() : null;
+ }
+
+ while (it.hasNext()) {
+ final String category = it.next();
+ if (!mCategories.contains(category)) {
+ return category;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Test whether this filter matches the given <var>intent</var>.
+ *
+ * @param intent The Intent to compare against.
+ * @param resolve If true, the intent's type will be resolved by calling
+ * Intent.resolveType(); otherwise a simple match against
+ * Intent.type will be performed.
+ * @param logTag Tag to use in debugging messages.
+ *
+ * @return Returns either a valid match constant (a combination of
+ * {@link #MATCH_CATEGORY_MASK} and {@link #MATCH_ADJUSTMENT_MASK}),
+ * or one of the error codes {@link #NO_MATCH_TYPE} if the type didn't match,
+ * {@link #NO_MATCH_DATA} if the scheme/path didn't match,
+ * {@link #NO_MATCH_ACTION if the action didn't match, or
+ * {@link #NO_MATCH_CATEGORY} if one or more categories didn't match.
+ *
+ * @return How well the filter matches. Negative if it doesn't match,
+ * zero or positive positive value if it does with a higher
+ * value representing a better match.
+ *
+ * @see #match(String, String, String, android.net.Uri , Set, String)
+ */
+ public final int match(ContentResolver resolver, Intent intent,
+ boolean resolve, String logTag) {
+ String type = resolve ? intent.resolveType(resolver) : intent.getType();
+ return match(intent.getAction(), type, intent.getScheme(),
+ intent.getData(), intent.getCategories(), logTag);
+ }
+
+ /**
+ * Test whether this filter matches the given intent data. A match is
+ * only successful if the actions and categories in the Intent match
+ * against the filter, as described in {@link IntentFilter}; in that case,
+ * the match result returned will be as per {@link #matchData}.
+ *
+ * @param action The intent action to match against (Intent.getAction).
+ * @param type The intent type to match against (Intent.resolveType()).
+ * @param scheme The data scheme to match against (Intent.getScheme()).
+ * @param data The data URI to match against (Intent.getData()).
+ * @param categories The categories to match against
+ * (Intent.getCategories()).
+ * @param logTag Tag to use in debugging messages.
+ *
+ * @return Returns either a valid match constant (a combination of
+ * {@link #MATCH_CATEGORY_MASK} and {@link #MATCH_ADJUSTMENT_MASK}),
+ * or one of the error codes {@link #NO_MATCH_TYPE} if the type didn't match,
+ * {@link #NO_MATCH_DATA} if the scheme/path didn't match,
+ * {@link #NO_MATCH_ACTION if the action didn't match, or
+ * {@link #NO_MATCH_CATEGORY} if one or more categories didn't match.
+ *
+ * @see #matchData
+ * @see Intent#getAction
+ * @see Intent#resolveType
+ * @see Intent#getScheme
+ * @see Intent#getData
+ * @see Intent#getCategories
+ */
+ public final int match(String action, String type, String scheme,
+ Uri data, Set<String> categories, String logTag) {
+ if (action != null && !matchAction(action)) {
+ if (Config.LOGV) Log.v(
+ logTag, "No matching action " + action + " for " + this);
+ return NO_MATCH_ACTION;
+ }
+
+ int dataMatch = matchData(type, scheme, data);
+ if (dataMatch < 0) {
+ if (Config.LOGV) {
+ if (dataMatch == NO_MATCH_TYPE) {
+ Log.v(logTag, "No matching type " + type
+ + " for " + this);
+ }
+ if (dataMatch == NO_MATCH_DATA) {
+ Log.v(logTag, "No matching scheme/path " + data
+ + " for " + this);
+ }
+ }
+ return dataMatch;
+ }
+
+ String categoryMatch = matchCategories(categories);
+ if (categoryMatch != null) {
+ if (Config.LOGV) Log.v(
+ logTag, "No matching category "
+ + categoryMatch + " for " + this);
+ return NO_MATCH_CATEGORY;
+ }
+
+ // It would be nice to treat container activities as more
+ // important than ones that can be embedded, but this is not the way...
+ if (false) {
+ if (categories != null) {
+ dataMatch -= mCategories.size() - categories.size();
+ }
+ }
+
+ return dataMatch;
+ }
+
+ /**
+ * Write the contents of the IntentFilter as an XML stream.
+ */
+ public void writeToXml(XmlSerializer serializer) throws IOException {
+ int N = countActions();
+ for (int i=0; i<N; i++) {
+ serializer.startTag(null, ACTION_STR);
+ serializer.attribute(null, NAME_STR, mActions.get(i));
+ serializer.endTag(null, ACTION_STR);
+ }
+ N = countCategories();
+ for (int i=0; i<N; i++) {
+ serializer.startTag(null, CAT_STR);
+ serializer.attribute(null, NAME_STR, mCategories.get(i));
+ serializer.endTag(null, CAT_STR);
+ }
+ N = countDataTypes();
+ for (int i=0; i<N; i++) {
+ serializer.startTag(null, TYPE_STR);
+ String type = mDataTypes.get(i);
+ if (type.indexOf('/') < 0) type = type + "/*";
+ serializer.attribute(null, NAME_STR, type);
+ serializer.endTag(null, TYPE_STR);
+ }
+ N = countDataSchemes();
+ for (int i=0; i<N; i++) {
+ serializer.startTag(null, SCHEME_STR);
+ serializer.attribute(null, NAME_STR, mDataSchemes.get(i));
+ serializer.endTag(null, SCHEME_STR);
+ }
+ N = countDataAuthorities();
+ for (int i=0; i<N; i++) {
+ serializer.startTag(null, AUTH_STR);
+ AuthorityEntry ae = mDataAuthorities.get(i);
+ serializer.attribute(null, HOST_STR, ae.getHost());
+ if (ae.getPort() >= 0) {
+ serializer.attribute(null, PORT_STR, Integer.toString(ae.getPort()));
+ }
+ serializer.endTag(null, AUTH_STR);
+ }
+ N = countDataPaths();
+ for (int i=0; i<N; i++) {
+ serializer.startTag(null, PATH_STR);
+ PatternMatcher pe = mDataPaths.get(i);
+ switch (pe.getType()) {
+ case PatternMatcher.PATTERN_LITERAL:
+ serializer.attribute(null, LITERAL_STR, pe.getPath());
+ break;
+ case PatternMatcher.PATTERN_PREFIX:
+ serializer.attribute(null, PREFIX_STR, pe.getPath());
+ break;
+ case PatternMatcher.PATTERN_SIMPLE_GLOB:
+ serializer.attribute(null, SGLOB_STR, pe.getPath());
+ break;
+ }
+ serializer.endTag(null, PATH_STR);
+ }
+ }
+
+ public void readFromXml(XmlPullParser parser) throws XmlPullParserException,
+ IOException {
+ int outerDepth = parser.getDepth();
+ int type;
+ while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
+ && (type != XmlPullParser.END_TAG
+ || parser.getDepth() > outerDepth)) {
+ if (type == XmlPullParser.END_TAG
+ || type == XmlPullParser.TEXT) {
+ continue;
+ }
+
+ String tagName = parser.getName();
+ if (tagName.equals(ACTION_STR)) {
+ String name = parser.getAttributeValue(null, NAME_STR);
+ if (name != null) {
+ addAction(name);
+ }
+ } else if (tagName.equals(CAT_STR)) {
+ String name = parser.getAttributeValue(null, NAME_STR);
+ if (name != null) {
+ addCategory(name);
+ }
+ } else if (tagName.equals(TYPE_STR)) {
+ String name = parser.getAttributeValue(null, NAME_STR);
+ if (name != null) {
+ try {
+ addDataType(name);
+ } catch (MalformedMimeTypeException e) {
+ }
+ }
+ } else if (tagName.equals(SCHEME_STR)) {
+ String name = parser.getAttributeValue(null, NAME_STR);
+ if (name != null) {
+ addDataScheme(name);
+ }
+ } else if (tagName.equals(AUTH_STR)) {
+ String host = parser.getAttributeValue(null, HOST_STR);
+ String port = parser.getAttributeValue(null, PORT_STR);
+ if (host != null) {
+ addDataAuthority(host, port);
+ }
+ } else if (tagName.equals(PATH_STR)) {
+ String path = parser.getAttributeValue(null, LITERAL_STR);
+ if (path != null) {
+ addDataPath(path, PatternMatcher.PATTERN_LITERAL);
+ } else if ((path=parser.getAttributeValue(null, PREFIX_STR)) != null) {
+ addDataPath(path, PatternMatcher.PATTERN_PREFIX);
+ } else if ((path=parser.getAttributeValue(null, SGLOB_STR)) != null) {
+ addDataPath(path, PatternMatcher.PATTERN_SIMPLE_GLOB);
+ }
+ } else {
+ Log.w("IntentFilter", "Unknown tag parsing IntentFilter: " + tagName);
+ }
+ XmlUtils.skipCurrentTag(parser);
+ }
+ }
+
+ public void dump(Printer du, String prefix) {
+ if (mActions.size() > 0) {
+ Iterator<String> it = mActions.iterator();
+ while (it.hasNext()) {
+ du.println(prefix + "Action: \"" + it.next() + "\"");
+ }
+ }
+ if (mCategories != null) {
+ Iterator<String> it = mCategories.iterator();
+ while (it.hasNext()) {
+ du.println(prefix + "Category: \"" + it.next() + "\"");
+ }
+ }
+ if (mDataSchemes != null) {
+ Iterator<String> it = mDataSchemes.iterator();
+ while (it.hasNext()) {
+ du.println(prefix + "Data Scheme: \"" + it.next() + "\"");
+ }
+ }
+ if (mDataAuthorities != null) {
+ Iterator<AuthorityEntry> it = mDataAuthorities.iterator();
+ while (it.hasNext()) {
+ AuthorityEntry ae = it.next();
+ du.println(prefix + "Data Authority: \"" + ae.mHost + "\":"
+ + ae.mPort + (ae.mWild ? " WILD" : ""));
+ }
+ }
+ if (mDataPaths != null) {
+ Iterator<PatternMatcher> it = mDataPaths.iterator();
+ while (it.hasNext()) {
+ PatternMatcher pe = it.next();
+ du.println(prefix + "Data Path: \"" + pe + "\"");
+ }
+ }
+ if (mDataTypes != null) {
+ Iterator<String> it = mDataTypes.iterator();
+ while (it.hasNext()) {
+ du.println(prefix + "Data Type: \"" + it.next() + "\"");
+ }
+ }
+ du.println(prefix + "mPriority=" + mPriority
+ + ", mHasPartialTypes=" + mHasPartialTypes);
+ }
+
+ public static final Parcelable.Creator<IntentFilter> CREATOR
+ = new Parcelable.Creator<IntentFilter>() {
+ public IntentFilter createFromParcel(Parcel source) {
+ return new IntentFilter(source);
+ }
+
+ public IntentFilter[] newArray(int size) {
+ return new IntentFilter[size];
+ }
+ };
+
+ public final int describeContents() {
+ return 0;
+ }
+
+ public final void writeToParcel(Parcel dest, int flags) {
+ dest.writeStringList(mActions);
+ if (mCategories != null) {
+ dest.writeInt(1);
+ dest.writeStringList(mCategories);
+ } else {
+ dest.writeInt(0);
+ }
+ if (mDataSchemes != null) {
+ dest.writeInt(1);
+ dest.writeStringList(mDataSchemes);
+ } else {
+ dest.writeInt(0);
+ }
+ if (mDataTypes != null) {
+ dest.writeInt(1);
+ dest.writeStringList(mDataTypes);
+ } else {
+ dest.writeInt(0);
+ }
+ if (mDataAuthorities != null) {
+ final int N = mDataAuthorities.size();
+ dest.writeInt(N);
+ for (int i=0; i<N; i++) {
+ mDataAuthorities.get(i).writeToParcel(dest);
+ }
+ } else {
+ dest.writeInt(0);
+ }
+ if (mDataPaths != null) {
+ final int N = mDataPaths.size();
+ dest.writeInt(N);
+ for (int i=0; i<N; i++) {
+ mDataPaths.get(i).writeToParcel(dest, 0);
+ }
+ } else {
+ dest.writeInt(0);
+ }
+ dest.writeInt(mPriority);
+ dest.writeInt(mHasPartialTypes ? 1 : 0);
+ }
+
+ /**
+ * For debugging -- perform a check on the filter, return true if it passed
+ * or false if it failed.
+ *
+ * {@hide}
+ */
+ public boolean debugCheck() {
+ return true;
+
+ // This code looks for intent filters that do not specify data.
+ /*
+ if (mActions != null && mActions.size() == 1
+ && mActions.contains(Intent.ACTION_MAIN)) {
+ return true;
+ }
+
+ if (mDataTypes == null && mDataSchemes == null) {
+ Log.w("IntentFilter", "QUESTIONABLE INTENT FILTER:");
+ dump(Log.WARN, "IntentFilter", " ");
+ return false;
+ }
+
+ return true;
+ */
+ }
+
+ private IntentFilter(Parcel source) {
+ mActions = new ArrayList<String>();
+ source.readStringList(mActions);
+ if (source.readInt() != 0) {
+ mCategories = new ArrayList<String>();
+ source.readStringList(mCategories);
+ }
+ if (source.readInt() != 0) {
+ mDataSchemes = new ArrayList<String>();
+ source.readStringList(mDataSchemes);
+ }
+ if (source.readInt() != 0) {
+ mDataTypes = new ArrayList<String>();
+ source.readStringList(mDataTypes);
+ }
+ int N = source.readInt();
+ if (N > 0) {
+ mDataAuthorities = new ArrayList<AuthorityEntry>();
+ for (int i=0; i<N; i++) {
+ mDataAuthorities.add(new AuthorityEntry(source));
+ }
+ }
+ N = source.readInt();
+ if (N > 0) {
+ mDataPaths = new ArrayList<PatternMatcher>();
+ for (int i=0; i<N; i++) {
+ mDataPaths.add(new PatternMatcher(source));
+ }
+ }
+ mPriority = source.readInt();
+ mHasPartialTypes = source.readInt() > 0;
+ }
+
+ private final boolean findMimeType(String type) {
+ final ArrayList<String> t = mDataTypes;
+
+ if (type == null) {
+ return false;
+ }
+
+ if (t.contains(type)) {
+ return true;
+ }
+
+ // Deal with an Intent wanting to match every type in the IntentFilter.
+ final int typeLength = type.length();
+ if (typeLength == 3 && type.equals("*/*")) {
+ return !t.isEmpty();
+ }
+
+ // Deal with this IntentFilter wanting to match every Intent type.
+ if (mHasPartialTypes && t.contains("*")) {
+ return true;
+ }
+
+ final int slashpos = type.indexOf('/');
+ if (slashpos > 0) {
+ if (mHasPartialTypes && t.contains(type.substring(0, slashpos))) {
+ return true;
+ }
+ if (typeLength == slashpos+2 && type.charAt(slashpos+1) == '*') {
+ // Need to look through all types for one that matches
+ // our base...
+ final Iterator<String> it = t.iterator();
+ while (it.hasNext()) {
+ String v = it.next();
+ if (type.regionMatches(0, v, 0, slashpos+1)) {
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/core/java/android/content/MutableContextWrapper.java b/core/java/android/content/MutableContextWrapper.java
new file mode 100644
index 0000000..820479c
--- /dev/null
+++ b/core/java/android/content/MutableContextWrapper.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+/**
+ * Special version of {@link ContextWrapper} that allows the base context to
+ * be modified after it is initially set.
+ */
+public class MutableContextWrapper extends ContextWrapper {
+ public MutableContextWrapper(Context base) {
+ super(base);
+ }
+
+ /**
+ * Change the base context for this ContextWrapper. All calls will then be
+ * delegated to the base context. Unlike ContextWrapper, the base context
+ * can be changed even after one is already set.
+ *
+ * @param base The new base context for this wrapper.
+ */
+ public void setBaseContext(Context base) {
+ mBase = base;
+ }
+}
diff --git a/core/java/android/content/ReceiverCallNotAllowedException.java b/core/java/android/content/ReceiverCallNotAllowedException.java
new file mode 100644
index 0000000..96b269c
--- /dev/null
+++ b/core/java/android/content/ReceiverCallNotAllowedException.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+import android.util.AndroidRuntimeException;
+
+/**
+ * This exception is thrown from {@link Context#registerReceiver} and
+ * {@link Context#bindService} when these methods are being used from
+ * an {@link BroadcastReceiver} component. In this case, the component will no
+ * longer be active upon returning from receiving the Intent, so it is
+ * not valid to use asynchronous APIs.
+ */
+public class ReceiverCallNotAllowedException extends AndroidRuntimeException {
+ public ReceiverCallNotAllowedException(String msg) {
+ super(msg);
+ }
+}
diff --git a/core/java/android/content/SearchRecentSuggestionsProvider.java b/core/java/android/content/SearchRecentSuggestionsProvider.java
new file mode 100644
index 0000000..3d89e92
--- /dev/null
+++ b/core/java/android/content/SearchRecentSuggestionsProvider.java
@@ -0,0 +1,385 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+import android.app.SearchManager;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.Log;
+
+/**
+ * This superclass can be used to create a simple search suggestions provider for your application.
+ * It creates suggestions (as the user types) based on recent queries and/or recent views.
+ *
+ * <p>In order to use this class, you must do the following.
+ *
+ * <ul>
+ * <li>Implement and test query search, as described in {@link android.app.SearchManager}. (This
+ * provider will send any suggested queries via the standard
+ * {@link android.content.Intent#ACTION_SEARCH ACTION_SEARCH} Intent, which you'll already
+ * support once you have implemented and tested basic searchability.)</li>
+ * <li>Create a Content Provider within your application by extending
+ * {@link android.content.SearchRecentSuggestionsProvider}. The class you create will be
+ * very simple - typically, it will have only a constructor. But the constructor has a very
+ * important responsibility: When it calls {@link #setupSuggestions(String, int)}, it
+ * <i>configures</i> the provider to match the requirements of your searchable activity.</li>
+ * <li>Create a manifest entry describing your provider. Typically this would be as simple
+ * as adding the following lines:
+ * <pre class="prettyprint">
+ * &lt;!-- Content provider for search suggestions --&gt;
+ * &lt;provider android:name="YourSuggestionProviderClass"
+ * android:authorities="your.suggestion.authority" /&gt;</pre>
+ * </li>
+ * <li>Please note that you <i>do not</i> instantiate this content provider directly from within
+ * your code. This is done automatically by the system Content Resolver, when the search dialog
+ * looks for suggestions.</li>
+ * <li>In order for the Content Resolver to do this, you must update your searchable activity's
+ * XML configuration file with information about your content provider. The following additions
+ * are usually sufficient:
+ * <pre class="prettyprint">
+ * android:searchSuggestAuthority="your.suggestion.authority"
+ * android:searchSuggestSelection=" ? "</pre>
+ * </li>
+ * <li>In your searchable activities, capture any user-generated queries and record them
+ * for future searches by calling {@link android.provider.SearchRecentSuggestions#saveRecentQuery
+ * SearchRecentSuggestions.saveRecentQuery()}.</li>
+ * </ul>
+ *
+ * @see android.provider.SearchRecentSuggestions
+ */
+public class SearchRecentSuggestionsProvider extends ContentProvider {
+ // debugging support
+ private static final String TAG = "SuggestionsProvider";
+
+ // client-provided configuration values
+ private String mAuthority;
+ private int mMode;
+ private boolean mTwoLineDisplay;
+
+ // general database configuration and tables
+ private SQLiteOpenHelper mOpenHelper;
+ private static final String sDatabaseName = "suggestions.db";
+ private static final String sSuggestions = "suggestions";
+ private static final String ORDER_BY = "date DESC";
+ private static final String NULL_COLUMN = "query";
+
+ // Table of database versions. Don't forget to update!
+ // NOTE: These version values are shifted left 8 bits (x 256) in order to create space for
+ // a small set of mode bitflags in the version int.
+ //
+ // 1 original implementation with queries, and 1 or 2 display columns
+ // 1->2 added UNIQUE constraint to display1 column
+ private static final int DATABASE_VERSION = 2 * 256;
+
+ /**
+ * This mode bit configures the database to record recent queries. <i>required</i>
+ *
+ * @see #setupSuggestions(String, int)
+ */
+ public static final int DATABASE_MODE_QUERIES = 1;
+ /**
+ * This mode bit configures the database to include a 2nd annotation line with each entry.
+ * <i>optional</i>
+ *
+ * @see #setupSuggestions(String, int)
+ */
+ public static final int DATABASE_MODE_2LINES = 2;
+
+ // Uri and query support
+ private static final int URI_MATCH_SUGGEST = 1;
+
+ private Uri mSuggestionsUri;
+ private UriMatcher mUriMatcher;
+
+ private String mSuggestSuggestionClause;
+ private String[] mSuggestionProjection;
+
+ /**
+ * Builds the database. This version has extra support for using the version field
+ * as a mode flags field, and configures the database columns depending on the mode bits
+ * (features) requested by the extending class.
+ *
+ * @hide
+ */
+ private static class DatabaseHelper extends SQLiteOpenHelper {
+
+ private int mNewVersion;
+
+ public DatabaseHelper(Context context, int newVersion) {
+ super(context, sDatabaseName, null, newVersion);
+ mNewVersion = newVersion;
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ StringBuilder builder = new StringBuilder();
+ builder.append("CREATE TABLE suggestions (" +
+ "_id INTEGER PRIMARY KEY" +
+ ",display1 TEXT UNIQUE ON CONFLICT REPLACE");
+ if (0 != (mNewVersion & DATABASE_MODE_2LINES)) {
+ builder.append(",display2 TEXT");
+ }
+ builder.append(",query TEXT" +
+ ",date LONG" +
+ ");");
+ db.execSQL(builder.toString());
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
+ + newVersion + ", which will destroy all old data");
+ db.execSQL("DROP TABLE IF EXISTS suggestions");
+ onCreate(db);
+ }
+ }
+
+ /**
+ * In order to use this class, you must extend it, and call this setup function from your
+ * constructor. In your application or activities, you must provide the same values when
+ * you create the {@link android.provider.SearchRecentSuggestions} helper.
+ *
+ * @param authority This must match the authority that you've declared in your manifest.
+ * @param mode You can use mode flags here to determine certain functional aspects of your
+ * database. Note, this value should not change from run to run, because when it does change,
+ * your suggestions database may be wiped.
+ *
+ * @see #DATABASE_MODE_QUERIES
+ * @see #DATABASE_MODE_2LINES
+ */
+ protected void setupSuggestions(String authority, int mode) {
+ if (TextUtils.isEmpty(authority) ||
+ ((mode & DATABASE_MODE_QUERIES) == 0)) {
+ throw new IllegalArgumentException();
+ }
+ // unpack mode flags
+ mTwoLineDisplay = (0 != (mode & DATABASE_MODE_2LINES));
+
+ // saved values
+ mAuthority = new String(authority);
+ mMode = mode;
+
+ // derived values
+ mSuggestionsUri = Uri.parse("content://" + mAuthority + "/suggestions");
+ mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+ mUriMatcher.addURI(mAuthority, SearchManager.SUGGEST_URI_PATH_QUERY, URI_MATCH_SUGGEST);
+
+ if (mTwoLineDisplay) {
+ mSuggestSuggestionClause = "display1 LIKE ? OR display2 LIKE ?";
+
+ mSuggestionProjection = new String [] {
+ "0 AS " + SearchManager.SUGGEST_COLUMN_FORMAT,
+ "display1 AS " + SearchManager.SUGGEST_COLUMN_TEXT_1,
+ "display2 AS " + SearchManager.SUGGEST_COLUMN_TEXT_2,
+ "query AS " + SearchManager.SUGGEST_COLUMN_QUERY,
+ "_id"
+ };
+ } else {
+ mSuggestSuggestionClause = "display1 LIKE ?";
+
+ mSuggestionProjection = new String [] {
+ "0 AS " + SearchManager.SUGGEST_COLUMN_FORMAT,
+ "display1 AS " + SearchManager.SUGGEST_COLUMN_TEXT_1,
+ "query AS " + SearchManager.SUGGEST_COLUMN_QUERY,
+ "_id"
+ };
+ }
+
+
+ }
+
+ /**
+ * This method is provided for use by the ContentResolver. Do not override, or directly
+ * call from your own code.
+ */
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+
+ final int length = uri.getPathSegments().size();
+ if (length != 1) {
+ throw new IllegalArgumentException("Unknown Uri");
+ }
+
+ final String base = uri.getPathSegments().get(0);
+ int count = 0;
+ if (base.equals(sSuggestions)) {
+ count = db.delete(sSuggestions, selection, selectionArgs);
+ } else {
+ throw new IllegalArgumentException("Unknown Uri");
+ }
+ getContext().getContentResolver().notifyChange(uri, null);
+ return count;
+ }
+
+ /**
+ * This method is provided for use by the ContentResolver. Do not override, or directly
+ * call from your own code.
+ */
+ @Override
+ public String getType(Uri uri) {
+ if (mUriMatcher.match(uri) == URI_MATCH_SUGGEST) {
+ return SearchManager.SUGGEST_MIME_TYPE;
+ }
+ int length = uri.getPathSegments().size();
+ if (length >= 1) {
+ String base = uri.getPathSegments().get(0);
+ if (base.equals(sSuggestions)) {
+ if (length == 1) {
+ return "vnd.android.cursor.dir/suggestion";
+ } else if (length == 2) {
+ return "vnd.android.cursor.item/suggestion";
+ }
+ }
+ }
+ throw new IllegalArgumentException("Unknown Uri");
+ }
+
+ /**
+ * This method is provided for use by the ContentResolver. Do not override, or directly
+ * call from your own code.
+ */
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+
+ int length = uri.getPathSegments().size();
+ if (length < 1) {
+ throw new IllegalArgumentException("Unknown Uri");
+ }
+ // Note: This table has on-conflict-replace semantics, so insert() may actually replace()
+ long rowID = -1;
+ String base = uri.getPathSegments().get(0);
+ Uri newUri = null;
+ if (base.equals(sSuggestions)) {
+ if (length == 1) {
+ rowID = db.insert(sSuggestions, NULL_COLUMN, values);
+ if (rowID > 0) {
+ newUri = Uri.withAppendedPath(mSuggestionsUri, String.valueOf(rowID));
+ }
+ }
+ }
+ if (rowID < 0) {
+ throw new IllegalArgumentException("Unknown Uri");
+ }
+ getContext().getContentResolver().notifyChange(newUri, null);
+ return newUri;
+ }
+
+ /**
+ * This method is provided for use by the ContentResolver. Do not override, or directly
+ * call from your own code.
+ */
+ @Override
+ public boolean onCreate() {
+ if (mAuthority == null || mMode == 0) {
+ throw new IllegalArgumentException("Provider not configured");
+ }
+ int mWorkingDbVersion = DATABASE_VERSION + mMode;
+ mOpenHelper = new DatabaseHelper(getContext(), mWorkingDbVersion);
+
+ return true;
+ }
+
+ /**
+ * This method is provided for use by the ContentResolver. Do not override, or directly
+ * call from your own code.
+ */
+ // TODO: Confirm no injection attacks here, or rewrite.
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder) {
+ SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+
+ // special case for actual suggestions (from search manager)
+ if (mUriMatcher.match(uri) == URI_MATCH_SUGGEST) {
+ String suggestSelection;
+ String[] myArgs;
+ if (TextUtils.isEmpty(selectionArgs[0])) {
+ suggestSelection = null;
+ myArgs = null;
+ } else {
+ String like = "%" + selectionArgs[0] + "%";
+ if (mTwoLineDisplay) {
+ myArgs = new String [] { like, like };
+ } else {
+ myArgs = new String [] { like };
+ }
+ suggestSelection = mSuggestSuggestionClause;
+ }
+ // Suggestions are always performed with the default sort order
+ Cursor c = db.query(sSuggestions, mSuggestionProjection,
+ suggestSelection, myArgs, null, null, ORDER_BY, null);
+ c.setNotificationUri(getContext().getContentResolver(), uri);
+ return c;
+ }
+
+ // otherwise process arguments and perform a standard query
+ int length = uri.getPathSegments().size();
+ if (length != 1 && length != 2) {
+ throw new IllegalArgumentException("Unknown Uri");
+ }
+
+ String base = uri.getPathSegments().get(0);
+ if (!base.equals(sSuggestions)) {
+ throw new IllegalArgumentException("Unknown Uri");
+ }
+
+ String[] useProjection = null;
+ if (projection != null && projection.length > 0) {
+ useProjection = new String[projection.length + 1];
+ System.arraycopy(projection, 0, useProjection, 0, projection.length);
+ useProjection[projection.length] = "_id AS _id";
+ }
+
+ StringBuilder whereClause = new StringBuilder(256);
+ if (length == 2) {
+ whereClause.append("(_id = ").append(uri.getPathSegments().get(1)).append(")");
+ }
+
+ // Tack on the user's selection, if present
+ if (selection != null && selection.length() > 0) {
+ if (whereClause.length() > 0) {
+ whereClause.append(" AND ");
+ }
+
+ whereClause.append('(');
+ whereClause.append(selection);
+ whereClause.append(')');
+ }
+
+ // And perform the generic query as requested
+ Cursor c = db.query(base, useProjection, whereClause.toString(),
+ selectionArgs, null, null, sortOrder,
+ null);
+ c.setNotificationUri(getContext().getContentResolver(), uri);
+ return c;
+ }
+
+ /**
+ * This method is provided for use by the ContentResolver. Do not override, or directly
+ * call from your own code.
+ */
+ @Override
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+}
diff --git a/core/java/android/content/ServiceConnection.java b/core/java/android/content/ServiceConnection.java
new file mode 100644
index 0000000..d115ce4
--- /dev/null
+++ b/core/java/android/content/ServiceConnection.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+import android.os.IBinder;
+
+/**
+ * Interface for monitoring the state of an application service. See
+ * {@link android.app.Service} and
+ * {@link Context#bindService Context.bindService()} for more information.
+ * <p>Like many callbacks from the system, the methods on this class are called
+ * from the main thread of your process.
+ */
+public interface ServiceConnection {
+ /**
+ * Called when a connection to the Service has been established, with
+ * the {@link android.os.IBinder} of the communication channel to the
+ * Service.
+ *
+ * @param name The concrete component name of the service that has
+ * been connected.
+ *
+ * @param service The IBinder of the Service's communication channel,
+ * which you can now make calls on.
+ */
+ public void onServiceConnected(ComponentName name, IBinder service);
+
+ /**
+ * Called when a connection to the Service has been lost. This typically
+ * happens when the process hosting the service has crashed or been killed.
+ * This does <em>not</em> remove the ServiceConnection itself -- this
+ * binding to the service will remain active, and you will receive a call
+ * to {@link #onServiceConnected} when the Service is next running.
+ *
+ * @param name The concrete component name of the service whose
+ * connection has been lost.
+ */
+ public void onServiceDisconnected(ComponentName name);
+}
diff --git a/core/java/android/content/SharedPreferences.java b/core/java/android/content/SharedPreferences.java
new file mode 100644
index 0000000..a15e29e
--- /dev/null
+++ b/core/java/android/content/SharedPreferences.java
@@ -0,0 +1,282 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+import java.util.Map;
+
+/**
+ * Interface for accessing and modifying preference data returned by {@link
+ * Context#getSharedPreferences}. For any particular set of preferences,
+ * there is a single instance of this class that all clients share.
+ * Modifications to the preferences must go through an {@link Editor} object
+ * to ensure the preference values remain in a consistent state and control
+ * when they are committed to storage.
+ *
+ * <p><em>Note: currently this class does not support use across multiple
+ * processes. This will be added later.</em>
+ *
+ * @see Context#getSharedPreferences
+ */
+public interface SharedPreferences {
+ /**
+ * Interface definition for a callback to be invoked when a shared
+ * preference is changed.
+ */
+ public interface OnSharedPreferenceChangeListener {
+ /**
+ * Called when a shared preference is changed, added, or removed. This
+ * may be called even if a preference is set to its existing value.
+ *
+ * @param sharedPreferences The {@link SharedPreferences} that received
+ * the change.
+ * @param key The key of the preference that was changed, added, or
+ * removed.
+ */
+ void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key);
+ }
+
+ /**
+ * Interface used for modifying values in a {@link SharedPreferences}
+ * object. All changes you make in an editor are batched, and not copied
+ * back to the original {@link SharedPreferences} or persistent storage
+ * until you call {@link #commit}.
+ */
+ public interface Editor {
+ /**
+ * Set a String value in the preferences editor, to be written back once
+ * {@link #commit} is called.
+ *
+ * @param key The name of the preference to modify.
+ * @param value The new value for the preference.
+ *
+ * @return Returns a reference to the same Editor object, so you can
+ * chain put calls together.
+ */
+ Editor putString(String key, String value);
+
+ /**
+ * Set an int value in the preferences editor, to be written back once
+ * {@link #commit} is called.
+ *
+ * @param key The name of the preference to modify.
+ * @param value The new value for the preference.
+ *
+ * @return Returns a reference to the same Editor object, so you can
+ * chain put calls together.
+ */
+ Editor putInt(String key, int value);
+
+ /**
+ * Set a long value in the preferences editor, to be written back once
+ * {@link #commit} is called.
+ *
+ * @param key The name of the preference to modify.
+ * @param value The new value for the preference.
+ *
+ * @return Returns a reference to the same Editor object, so you can
+ * chain put calls together.
+ */
+ Editor putLong(String key, long value);
+
+ /**
+ * Set a float value in the preferences editor, to be written back once
+ * {@link #commit} is called.
+ *
+ * @param key The name of the preference to modify.
+ * @param value The new value for the preference.
+ *
+ * @return Returns a reference to the same Editor object, so you can
+ * chain put calls together.
+ */
+ Editor putFloat(String key, float value);
+
+ /**
+ * Set a boolean value in the preferences editor, to be written back
+ * once {@link #commit} is called.
+ *
+ * @param key The name of the preference to modify.
+ * @param value The new value for the preference.
+ *
+ * @return Returns a reference to the same Editor object, so you can
+ * chain put calls together.
+ */
+ Editor putBoolean(String key, boolean value);
+
+ /**
+ * Mark in the editor that a preference value should be removed, which
+ * will be done in the actual preferences once {@link #commit} is
+ * called.
+ *
+ * <p>Note that when committing back to the preferences, all removals
+ * are done first, regardless of whether you called remove before
+ * or after put methods on this editor.
+ *
+ * @param key The name of the preference to remove.
+ *
+ * @return Returns a reference to the same Editor object, so you can
+ * chain put calls together.
+ */
+ Editor remove(String key);
+
+ /**
+ * Mark in the editor to remove <em>all</em> values from the
+ * preferences. Once commit is called, the only remaining preferences
+ * will be any that you have defined in this editor.
+ *
+ * <p>Note that when committing back to the preferences, the clear
+ * is done first, regardless of whether you called clear before
+ * or after put methods on this editor.
+ *
+ * @return Returns a reference to the same Editor object, so you can
+ * chain put calls together.
+ */
+ Editor clear();
+
+ /**
+ * Commit your preferences changes back from this Editor to the
+ * {@link SharedPreferences} object it is editing. This atomically
+ * performs the requested modifications, replacing whatever is currently
+ * in the SharedPreferences.
+ *
+ * <p>Note that when two editors are modifying preferences at the same
+ * time, the last one to call commit wins.
+ *
+ * @return Returns true if the new values were successfully written
+ * to persistent storage.
+ */
+ boolean commit();
+ }
+
+ /**
+ * Retrieve all values from the preferences.
+ *
+ * @return Returns a map containing a list of pairs key/value representing
+ * the preferences.
+ *
+ * @throws NullPointerException
+ */
+ Map<String, ?> getAll();
+
+ /**
+ * Retrieve a String value from the preferences.
+ *
+ * @param key The name of the preference to retrieve.
+ * @param defValue Value to return if this preference does not exist.
+ *
+ * @return Returns the preference value if it exists, or defValue. Throws
+ * ClassCastException if there is a preference with this name that is not
+ * a String.
+ *
+ * @throws ClassCastException
+ */
+ String getString(String key, String defValue);
+
+ /**
+ * Retrieve an int value from the preferences.
+ *
+ * @param key The name of the preference to retrieve.
+ * @param defValue Value to return if this preference does not exist.
+ *
+ * @return Returns the preference value if it exists, or defValue. Throws
+ * ClassCastException if there is a preference with this name that is not
+ * an int.
+ *
+ * @throws ClassCastException
+ */
+ int getInt(String key, int defValue);
+
+ /**
+ * Retrieve a long value from the preferences.
+ *
+ * @param key The name of the preference to retrieve.
+ * @param defValue Value to return if this preference does not exist.
+ *
+ * @return Returns the preference value if it exists, or defValue. Throws
+ * ClassCastException if there is a preference with this name that is not
+ * a long.
+ *
+ * @throws ClassCastException
+ */
+ long getLong(String key, long defValue);
+
+ /**
+ * Retrieve a float value from the preferences.
+ *
+ * @param key The name of the preference to retrieve.
+ * @param defValue Value to return if this preference does not exist.
+ *
+ * @return Returns the preference value if it exists, or defValue. Throws
+ * ClassCastException if there is a preference with this name that is not
+ * a float.
+ *
+ * @throws ClassCastException
+ */
+ float getFloat(String key, float defValue);
+
+ /**
+ * Retrieve a boolean value from the preferences.
+ *
+ * @param key The name of the preference to retrieve.
+ * @param defValue Value to return if this preference does not exist.
+ *
+ * @return Returns the preference value if it exists, or defValue. Throws
+ * ClassCastException if there is a preference with this name that is not
+ * a boolean.
+ *
+ * @throws ClassCastException
+ */
+ boolean getBoolean(String key, boolean defValue);
+
+ /**
+ * Checks whether the preferences contains a preference.
+ *
+ * @param key The name of the preference to check.
+ * @return Returns true if the preference exists in the preferences,
+ * otherwise false.
+ */
+ boolean contains(String key);
+
+ /**
+ * Create a new Editor for these preferences, through which you can make
+ * modifications to the data in the preferences and atomically commit those
+ * changes back to the SharedPreferences object.
+ *
+ * <p>Note that you <em>must</em> call {@link Editor#commit} to have any
+ * changes you perform in the Editor actually show up in the
+ * SharedPreferences.
+ *
+ * @return Returns a new instance of the {@link Editor} interface, allowing
+ * you to modify the values in this SharedPreferences object.
+ */
+ Editor edit();
+
+ /**
+ * Registers a callback to be invoked when a change happens to a preference.
+ *
+ * @param listener The callback that will run.
+ * @see #unregisterOnSharedPreferenceChangeListener
+ */
+ void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);
+
+ /**
+ * Unregisters a previous callback.
+ *
+ * @param listener The callback that should be unregistered.
+ * @see #registerOnSharedPreferenceChangeListener
+ */
+ void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);
+}
diff --git a/core/java/android/content/SyncAdapter.java b/core/java/android/content/SyncAdapter.java
new file mode 100644
index 0000000..7826e50
--- /dev/null
+++ b/core/java/android/content/SyncAdapter.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+import android.os.Bundle;
+import android.os.RemoteException;
+
+/**
+ * @hide
+ */
+public abstract class SyncAdapter {
+ private static final String TAG = "SyncAdapter";
+
+ /** Kernel event log tag. Also listed in data/etc/event-log-tags. */
+ public static final int LOG_SYNC_DETAILS = 2743;
+
+ class Transport extends ISyncAdapter.Stub {
+ public void startSync(ISyncContext syncContext, String account,
+ Bundle extras) throws RemoteException {
+ SyncAdapter.this.startSync(new SyncContext(syncContext), account, extras);
+ }
+
+ public void cancelSync() throws RemoteException {
+ SyncAdapter.this.cancelSync();
+ }
+ }
+
+ Transport mTransport = new Transport();
+
+ /**
+ * Get the Transport object. (note this is package private).
+ */
+ final ISyncAdapter getISyncAdapter()
+ {
+ return mTransport;
+ }
+
+ /**
+ * Initiate a sync for this account. SyncAdapter-specific parameters may
+ * be specified in extras, which is guaranteed to not be null. IPC invocations
+ * of this method and cancelSync() are guaranteed to be serialized.
+ *
+ * @param syncContext the ISyncContext used to indicate the progress of the sync. When
+ * the sync is finished (successfully or not) ISyncContext.onFinished() must be called.
+ * @param account the account that should be synced
+ * @param extras SyncAdapter-specific parameters
+ */
+ public abstract void startSync(SyncContext syncContext, String account, Bundle extras);
+
+ /**
+ * Cancel the most recently initiated sync. Due to race conditions, this may arrive
+ * after the ISyncContext.onFinished() for that sync was called. IPC invocations
+ * of this method and startSync() are guaranteed to be serialized.
+ */
+ public abstract void cancelSync();
+}
diff --git a/core/java/android/content/SyncContext.java b/core/java/android/content/SyncContext.java
new file mode 100644
index 0000000..f4faa04
--- /dev/null
+++ b/core/java/android/content/SyncContext.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+import android.os.RemoteException;
+import android.os.SystemClock;
+
+/**
+ * @hide
+ */
+public class SyncContext {
+ private ISyncContext mSyncContext;
+ private long mLastHeartbeatSendTime;
+
+ private static final long HEARTBEAT_SEND_INTERVAL_IN_MS = 1000;
+
+ public SyncContext(ISyncContext syncContextInterface) {
+ mSyncContext = syncContextInterface;
+ mLastHeartbeatSendTime = 0;
+ }
+
+ /**
+ * Call to update the status text for this sync. This internally invokes
+ * {@link #updateHeartbeat}, so it also takes the place of a call to that.
+ *
+ * @param message the current status message for this sync
+ */
+ public void setStatusText(String message) {
+ updateHeartbeat();
+ }
+
+ /**
+ * Call to indicate that the SyncAdapter is making progress. E.g., if this SyncAdapter
+ * downloads or sends records to/from the server, this may be called after each record
+ * is downloaded or uploaded.
+ */
+ public void updateHeartbeat() {
+ final long now = SystemClock.elapsedRealtime();
+ if (now < mLastHeartbeatSendTime + HEARTBEAT_SEND_INTERVAL_IN_MS) return;
+ try {
+ mLastHeartbeatSendTime = now;
+ mSyncContext.sendHeartbeat();
+ } catch (RemoteException e) {
+ // this should never happen
+ }
+ }
+
+ public void onFinished(SyncResult result) {
+ try {
+ mSyncContext.onFinished(result);
+ } catch (RemoteException e) {
+ // this should never happen
+ }
+ }
+
+ public ISyncContext getISyncContext() {
+ return mSyncContext;
+ }
+}
diff --git a/core/java/android/content/SyncManager.java b/core/java/android/content/SyncManager.java
new file mode 100644
index 0000000..2bf84e7
--- /dev/null
+++ b/core/java/android/content/SyncManager.java
@@ -0,0 +1,2170 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+import com.google.android.collect.Maps;
+
+import com.android.internal.R;
+import com.android.internal.util.ArrayUtils;
+
+import android.accounts.AccountMonitor;
+import android.accounts.AccountMonitorListener;
+import android.app.AlarmManager;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageManager;
+import android.content.pm.ProviderInfo;
+import android.content.pm.ResolveInfo;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Parcel;
+import android.os.PowerManager;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.pim.DateUtils;
+import android.pim.Time;
+import android.preference.Preference;
+import android.preference.PreferenceGroup;
+import android.provider.Sync;
+import android.provider.Settings;
+import android.provider.Sync.History;
+import android.text.TextUtils;
+import android.util.Config;
+import android.util.EventLog;
+import android.util.Log;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.PriorityQueue;
+import java.util.Random;
+import java.util.Observer;
+import java.util.Observable;
+
+/**
+ * @hide
+ */
+class SyncManager {
+ private static final String TAG = "SyncManager";
+
+ // used during dumping of the Sync history
+ private static final long MILLIS_IN_HOUR = 1000 * 60 * 60;
+ private static final long MILLIS_IN_DAY = MILLIS_IN_HOUR * 24;
+ private static final long MILLIS_IN_WEEK = MILLIS_IN_DAY * 7;
+ private static final long MILLIS_IN_4WEEKS = MILLIS_IN_WEEK * 4;
+
+ /** Delay a sync due to local changes this long. In milliseconds */
+ private static final long LOCAL_SYNC_DELAY = 30 * 1000; // 30 seconds
+
+ /**
+ * If a sync takes longer than this and the sync queue is not empty then we will
+ * cancel it and add it back to the end of the sync queue. In milliseconds.
+ */
+ private static final long MAX_TIME_PER_SYNC = 5 * 60 * 1000; // 5 minutes
+
+ private static final long SYNC_NOTIFICATION_DELAY = 30 * 1000; // 30 seconds
+
+ /**
+ * When retrying a sync for the first time use this delay. After that
+ * the retry time will double until it reached MAX_SYNC_RETRY_TIME.
+ * In milliseconds.
+ */
+ private static final long INITIAL_SYNC_RETRY_TIME_IN_MS = 30 * 1000; // 30 seconds
+
+ /**
+ * Default the max sync retry time to this value.
+ */
+ private static final long DEFAULT_MAX_SYNC_RETRY_TIME_IN_SECONDS = 60 * 60; // one hour
+
+ /**
+ * An error notification is sent if sync of any of the providers has been failing for this long.
+ */
+ private static final long ERROR_NOTIFICATION_DELAY_MS = 1000 * 60 * 10; // 10 minutes
+
+ private static final String SYNC_WAKE_LOCK = "SyncManagerSyncWakeLock";
+ private static final String HANDLE_SYNC_ALARM_WAKE_LOCK = "SyncManagerHandleSyncAlarmWakeLock";
+
+ private Context mContext;
+ private ContentResolver mContentResolver;
+
+ private String mStatusText = "";
+ private long mHeartbeatTime = 0;
+
+ private AccountMonitor mAccountMonitor;
+
+ private volatile String[] mAccounts = null;
+
+ volatile private PowerManager.WakeLock mSyncWakeLock;
+ volatile private PowerManager.WakeLock mHandleAlarmWakeLock;
+ volatile private boolean mDataConnectionIsConnected = false;
+ volatile private boolean mStorageIsLow = false;
+ private Sync.Settings.QueryMap mSyncSettings;
+
+ private final NotificationManager mNotificationMgr;
+ private AlarmManager mAlarmService = null;
+ private HandlerThread mSyncThread;
+
+ private volatile IPackageManager mPackageManager;
+
+ private final SyncStorageEngine mSyncStorageEngine;
+ private final SyncQueue mSyncQueue;
+
+ private ActiveSyncContext mActiveSyncContext = null;
+
+ // set if the sync error indicator should be reported.
+ private boolean mNeedSyncErrorNotification = false;
+ // set if the sync active indicator should be reported
+ private boolean mNeedSyncActiveNotification = false;
+
+ private volatile boolean mSyncPollInitialized;
+ private final PendingIntent mSyncAlarmIntent;
+ private final PendingIntent mSyncPollAlarmIntent;
+
+ private BroadcastReceiver mStorageIntentReceiver =
+ new BroadcastReceiver() {
+ public void onReceive(Context context, Intent intent) {
+ ensureContentResolver();
+ String action = intent.getAction();
+ if (Intent.ACTION_DEVICE_STORAGE_LOW.equals(action)) {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "Internal storage is low.");
+ }
+ mStorageIsLow = true;
+ cancelActiveSync(null /* no url */);
+ } else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(action)) {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "Internal storage is ok.");
+ }
+ mStorageIsLow = false;
+ sendCheckAlarmsMessage();
+ }
+ }
+ };
+
+ private BroadcastReceiver mConnectivityIntentReceiver =
+ new BroadcastReceiver() {
+ public void onReceive(Context context, Intent intent) {
+ NetworkInfo networkInfo =
+ intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO);
+ NetworkInfo.State state = (networkInfo == null ? NetworkInfo.State.UNKNOWN :
+ networkInfo.getState());
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "received connectivity action. network info: " + networkInfo);
+ }
+
+ // only pay attention to the CONNECTED and DISCONNECTED states.
+ // if connected, we are connected.
+ // if disconnected, we may not be connected. in some cases, we may be connected on
+ // a different network.
+ // e.g., if switching from GPRS to WiFi, we may receive the CONNECTED to WiFi and
+ // DISCONNECTED for GPRS in any order. if we receive the CONNECTED first, and then
+ // a DISCONNECTED, we want to make sure we set mDataConnectionIsConnected to true
+ // since we still have a WiFi connection.
+ switch (state) {
+ case CONNECTED:
+ mDataConnectionIsConnected = true;
+ break;
+ case DISCONNECTED:
+ if (intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false)) {
+ mDataConnectionIsConnected = false;
+ } else {
+ mDataConnectionIsConnected = true;
+ }
+ break;
+ default:
+ // ignore the rest of the states -- leave our boolean alone.
+ }
+ if (mDataConnectionIsConnected) {
+ initializeSyncPoll();
+ sendCheckAlarmsMessage();
+ }
+ }
+ };
+
+ private static final String ACTION_SYNC_ALARM = "android.content.syncmanager.SYNC_ALARM";
+ private static final String SYNC_POLL_ALARM = "android.content.syncmanager.SYNC_POLL_ALARM";
+ private final SyncHandler mSyncHandler;
+
+ private static final String[] SYNC_ACTIVE_PROJECTION = new String[]{
+ Sync.Active.ACCOUNT,
+ Sync.Active.AUTHORITY,
+ Sync.Active.START_TIME,
+ };
+
+ private static final String[] SYNC_PENDING_PROJECTION = new String[]{
+ Sync.Pending.ACCOUNT,
+ Sync.Pending.AUTHORITY
+ };
+
+ private static final int MAX_SYNC_POLL_DELAY_SECONDS = 36 * 60 * 60; // 36 hours
+ private static final int MIN_SYNC_POLL_DELAY_SECONDS = 24 * 60 * 60; // 24 hours
+
+ private static final String SYNCMANAGER_PREFS_FILENAME = "/data/system/syncmanager.prefs";
+
+ public SyncManager(Context context, boolean factoryTest) {
+ // Initialize the SyncStorageEngine first, before registering observers
+ // and creating threads and so on; it may fail if the disk is full.
+ SyncStorageEngine.init(context);
+ mSyncStorageEngine = SyncStorageEngine.getSingleton();
+ mSyncQueue = new SyncQueue(mSyncStorageEngine);
+
+ mContext = context;
+
+ mSyncThread = new HandlerThread("SyncHandlerThread", Process.THREAD_PRIORITY_BACKGROUND);
+ mSyncThread.start();
+ mSyncHandler = new SyncHandler(mSyncThread.getLooper());
+
+ mPackageManager = null;
+
+ mSyncAlarmIntent = PendingIntent.getBroadcast(
+ mContext, 0 /* ignored */, new Intent(ACTION_SYNC_ALARM), 0);
+
+ mSyncPollAlarmIntent = PendingIntent.getBroadcast(
+ mContext, 0 /* ignored */, new Intent(SYNC_POLL_ALARM), 0);
+
+ IntentFilter intentFilter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
+ context.registerReceiver(mConnectivityIntentReceiver, intentFilter);
+
+ intentFilter = new IntentFilter(Intent.ACTION_DEVICE_STORAGE_LOW);
+ intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_OK);
+ context.registerReceiver(mStorageIntentReceiver, intentFilter);
+
+ if (!factoryTest) {
+ mNotificationMgr = (NotificationManager)
+ context.getSystemService(Context.NOTIFICATION_SERVICE);
+ context.registerReceiver(new SyncAlarmIntentReceiver(),
+ new IntentFilter(ACTION_SYNC_ALARM));
+ } else {
+ mNotificationMgr = null;
+ }
+ PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+ mSyncWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, SYNC_WAKE_LOCK);
+ mSyncWakeLock.setReferenceCounted(false);
+
+ // This WakeLock is used to ensure that we stay awake between the time that we receive
+ // a sync alarm notification and when we finish processing it. We need to do this
+ // because we don't do the work in the alarm handler, rather we do it in a message
+ // handler.
+ mHandleAlarmWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
+ HANDLE_SYNC_ALARM_WAKE_LOCK);
+ mHandleAlarmWakeLock.setReferenceCounted(false);
+
+ if (!factoryTest) {
+ AccountMonitorListener listener = new AccountMonitorListener() {
+ public void onAccountsUpdated(String[] accounts) {
+ final boolean hadAccountsAlready = mAccounts != null;
+ // copy the accounts into a new array and change mAccounts to point to it
+ String[] newAccounts = new String[accounts.length];
+ System.arraycopy(accounts, 0, newAccounts, 0, accounts.length);
+ mAccounts = newAccounts;
+
+ // if a sync is in progress yet it is no longer in the accounts list, cancel it
+ ActiveSyncContext activeSyncContext = mActiveSyncContext;
+ if (activeSyncContext != null) {
+ if (!ArrayUtils.contains(newAccounts,
+ activeSyncContext.mSyncOperation.account)) {
+ Log.d(TAG, "canceling sync since the account has been removed");
+ sendSyncFinishedOrCanceledMessage(activeSyncContext,
+ null /* no result since this is a cancel */);
+ }
+ }
+
+ // we must do this since we don't bother scheduling alarms when
+ // the accounts are not set yet
+ sendCheckAlarmsMessage();
+
+ mSyncStorageEngine.doDatabaseCleanup(accounts);
+
+ if (hadAccountsAlready && mAccounts.length > 0) {
+ // request a sync so that if the password was changed we will retry any sync
+ // that failed when it was wrong
+ startSync(null /* all providers */, null /* no extras */);
+ }
+ }
+ };
+ mAccountMonitor = new AccountMonitor(context, listener);
+ }
+ }
+
+ private synchronized void initializeSyncPoll() {
+ if (mSyncPollInitialized) return;
+ mSyncPollInitialized = true;
+
+ mContext.registerReceiver(new SyncPollAlarmReceiver(), new IntentFilter(SYNC_POLL_ALARM));
+
+ // load the next poll time from shared preferences
+ long absoluteAlarmTime = readSyncPollTime();
+
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "initializeSyncPoll: absoluteAlarmTime is " + absoluteAlarmTime);
+ }
+
+ // Convert absoluteAlarmTime to elapsed realtime. If this time was in the past then
+ // schedule the poll immediately, if it is too far in the future then cap it at
+ // MAX_SYNC_POLL_DELAY_SECONDS.
+ long absoluteNow = System.currentTimeMillis();
+ long relativeNow = SystemClock.elapsedRealtime();
+ long relativeAlarmTime = relativeNow;
+ if (absoluteAlarmTime > absoluteNow) {
+ long delayInMs = absoluteAlarmTime - absoluteNow;
+ final int maxDelayInMs = MAX_SYNC_POLL_DELAY_SECONDS * 1000;
+ if (delayInMs > maxDelayInMs) {
+ delayInMs = MAX_SYNC_POLL_DELAY_SECONDS * 1000;
+ }
+ relativeAlarmTime += delayInMs;
+ }
+
+ // schedule an alarm for the next poll time
+ scheduleSyncPollAlarm(relativeAlarmTime);
+ }
+
+ private void scheduleSyncPollAlarm(long relativeAlarmTime) {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "scheduleSyncPollAlarm: relativeAlarmTime is " + relativeAlarmTime
+ + ", now is " + SystemClock.elapsedRealtime()
+ + ", delay is " + (relativeAlarmTime - SystemClock.elapsedRealtime()));
+ }
+ ensureAlarmService();
+ mAlarmService.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, relativeAlarmTime,
+ mSyncPollAlarmIntent);
+ }
+
+ /**
+ * Return a random value v that satisfies minValue <= v < maxValue. The difference between
+ * maxValue and minValue must be less than Integer.MAX_VALUE.
+ */
+ private long jitterize(long minValue, long maxValue) {
+ Random random = new Random(SystemClock.elapsedRealtime());
+ long spread = maxValue - minValue;
+ if (spread > Integer.MAX_VALUE) {
+ throw new IllegalArgumentException("the difference between the maxValue and the "
+ + "minValue must be less than " + Integer.MAX_VALUE);
+ }
+ return minValue + random.nextInt((int)spread);
+ }
+
+ private void handleSyncPollAlarm() {
+ // determine the next poll time
+ long delayMs = jitterize(MIN_SYNC_POLL_DELAY_SECONDS, MAX_SYNC_POLL_DELAY_SECONDS) * 1000;
+ long nextRelativePollTimeMs = SystemClock.elapsedRealtime() + delayMs;
+
+ if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "handleSyncPollAlarm: delay " + delayMs);
+
+ // write the absolute time to shared preferences
+ writeSyncPollTime(System.currentTimeMillis() + delayMs);
+
+ // schedule an alarm for the next poll time
+ scheduleSyncPollAlarm(nextRelativePollTimeMs);
+
+ // perform a poll
+ scheduleSync(null /* sync all syncable providers */, new Bundle(), 0 /* no delay */);
+ }
+
+ private void writeSyncPollTime(long when) {
+ File f = new File(SYNCMANAGER_PREFS_FILENAME);
+ DataOutputStream str = null;
+ try {
+ str = new DataOutputStream(new FileOutputStream(f));
+ str.writeLong(when);
+ } catch (FileNotFoundException e) {
+ Log.w(TAG, "error writing to file " + f, e);
+ } catch (IOException e) {
+ Log.w(TAG, "error writing to file " + f, e);
+ } finally {
+ if (str != null) {
+ try {
+ str.close();
+ } catch (IOException e) {
+ Log.w(TAG, "error closing file " + f, e);
+ }
+ }
+ }
+ }
+
+ private long readSyncPollTime() {
+ File f = new File(SYNCMANAGER_PREFS_FILENAME);
+
+ DataInputStream str = null;
+ try {
+ str = new DataInputStream(new FileInputStream(f));
+ return str.readLong();
+ } catch (FileNotFoundException e) {
+ writeSyncPollTime(0);
+ } catch (IOException e) {
+ Log.w(TAG, "error reading file " + f, e);
+ } finally {
+ if (str != null) {
+ try {
+ str.close();
+ } catch (IOException e) {
+ Log.w(TAG, "error closing file " + f, e);
+ }
+ }
+ }
+ return 0;
+ }
+
+ public ActiveSyncContext getActiveSyncContext() {
+ return mActiveSyncContext;
+ }
+
+ private Sync.Settings.QueryMap getSyncSettings() {
+ if (mSyncSettings == null) {
+ mSyncSettings = new Sync.Settings.QueryMap(mContext.getContentResolver(), true,
+ new Handler());
+ mSyncSettings.addObserver(new Observer(){
+ public void update(Observable o, Object arg) {
+ // force the sync loop to run if the settings change
+ sendCheckAlarmsMessage();
+ }
+ });
+ }
+ return mSyncSettings;
+ }
+
+ private void ensureContentResolver() {
+ if (mContentResolver == null) {
+ mContentResolver = mContext.getContentResolver();
+ }
+ }
+
+ private void ensureAlarmService() {
+ if (mAlarmService == null) {
+ mAlarmService = (AlarmManager)mContext.getSystemService(Context.ALARM_SERVICE);
+ }
+ }
+
+ public String getSyncingAccount() {
+ ActiveSyncContext activeSyncContext = mActiveSyncContext;
+ return (activeSyncContext != null) ? activeSyncContext.mSyncOperation.account : null;
+ }
+
+ /**
+ * Returns whether or not sync is enabled. Sync can be enabled by
+ * setting the system property "ro.config.sync" to the value "yes".
+ * This is normally done at boot time on builds that support sync.
+ * @return true if sync is enabled
+ */
+ private boolean isSyncEnabled() {
+ // Require the precise value "yes" to discourage accidental activation.
+ return "yes".equals(SystemProperties.get("ro.config.sync"));
+ }
+
+ /**
+ * Initiate a sync. This can start a sync for all providers
+ * (pass null to url, set onlyTicklable to false), only those
+ * providers that are marked as ticklable (pass null to url,
+ * set onlyTicklable to true), or a specific provider (set url
+ * to the content url of the provider).
+ *
+ * <p>If the ContentResolver.SYNC_EXTRAS_UPLOAD boolean in extras is
+ * true then initiate a sync that just checks for local changes to send
+ * to the server, otherwise initiate a sync that first gets any
+ * changes from the server before sending local changes back to
+ * the server.
+ *
+ * <p>If a specific provider is being synced (the url is non-null)
+ * then the extras can contain SyncAdapter-specific information
+ * to control what gets synced (e.g. which specific feed to sync).
+ *
+ * <p>You'll start getting callbacks after this.
+ *
+ * @param url The Uri of a specific provider to be synced, or
+ * null to sync all providers.
+ * @param extras a Map of SyncAdapter-specific information to control
+* syncs of a specific provider. Can be null. Is ignored
+* if the url is null.
+ * @param delay how many milliseconds in the future to wait before performing this
+ * sync. -1 means to make this the next sync to perform.
+ */
+ public void scheduleSync(Uri url, Bundle extras, long delay) {
+ boolean isLoggable = Log.isLoggable(TAG, Log.VERBOSE);
+ if (isLoggable) {
+ Log.v(TAG, "scheduleSync:"
+ + " delay " + delay
+ + ", url " + ((url == null) ? "(null)" : url)
+ + ", extras " + ((extras == null) ? "(null)" : extras));
+ }
+
+ if (!isSyncEnabled()) {
+ if (isLoggable) {
+ Log.v(TAG, "not syncing because sync is disabled");
+ }
+ setStatusText("Sync is disabled.");
+ return;
+ }
+
+ if (mAccounts == null) setStatusText("The accounts aren't known yet.");
+ if (!mDataConnectionIsConnected) setStatusText("No data connection");
+ if (mStorageIsLow) setStatusText("Memory low");
+
+ if (extras == null) extras = new Bundle();
+
+ Boolean expedited = extras.getBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, false);
+ if (expedited) {
+ delay = -1; // this means schedule at the front of the queue
+ }
+
+ String[] accounts;
+ String accountFromExtras = extras.getString(ContentResolver.SYNC_EXTRAS_ACCOUNT);
+ if (!TextUtils.isEmpty(accountFromExtras)) {
+ accounts = new String[]{accountFromExtras};
+ } else {
+ // if the accounts aren't configured yet then we can't support an account-less
+ // sync request
+ accounts = mAccounts;
+ if (accounts == null) {
+ // not ready yet
+ if (isLoggable) {
+ Log.v(TAG, "scheduleSync: no accounts yet, dropping");
+ }
+ return;
+ }
+ if (accounts.length == 0) {
+ if (isLoggable) {
+ Log.v(TAG, "scheduleSync: no accounts configured, dropping");
+ }
+ setStatusText("No accounts are configured.");
+ return;
+ }
+ }
+
+ final boolean uploadOnly = extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, false);
+ final boolean force = extras.getBoolean(ContentResolver.SYNC_EXTRAS_FORCE, false);
+
+ int source;
+ if (uploadOnly) {
+ source = Sync.History.SOURCE_LOCAL;
+ } else if (force) {
+ source = Sync.History.SOURCE_USER;
+ } else if (url == null) {
+ source = Sync.History.SOURCE_POLL;
+ } else {
+ // this isn't strictly server, since arbitrary callers can (and do) request
+ // a non-forced two-way sync on a specific url
+ source = Sync.History.SOURCE_SERVER;
+ }
+
+ List<String> names = new ArrayList<String>();
+ List<ProviderInfo> providers = new ArrayList<ProviderInfo>();
+ populateProvidersList(url, names, providers);
+
+ final int numProviders = providers.size();
+ for (int i = 0; i < numProviders; i++) {
+ if (!providers.get(i).isSyncable) continue;
+ final String name = names.get(i);
+ for (String account : accounts) {
+ scheduleSyncOperation(new SyncOperation(account, source, name, extras, delay));
+ // TODO: remove this when Calendar supports multiple accounts. Until then
+ // pretend that only the first account exists when syncing calendar.
+ if ("calendar".equals(name)) {
+ break;
+ }
+ }
+ }
+ }
+
+ private void setStatusText(String message) {
+ mStatusText = message;
+ }
+
+ private void populateProvidersList(Uri url, List<String> names, List<ProviderInfo> providers) {
+ try {
+ final IPackageManager packageManager = getPackageManager();
+ if (url == null) {
+ packageManager.querySyncProviders(names, providers);
+ } else {
+ final String authority = url.getAuthority();
+ ProviderInfo info = packageManager.resolveContentProvider(url.getAuthority(), 0);
+ if (info != null) {
+ // only set this provider if the requested authority is the primary authority
+ String[] providerNames = info.authority.split(";");
+ if (url.getAuthority().equals(providerNames[0])) {
+ names.add(authority);
+ providers.add(info);
+ }
+ }
+ }
+ } catch (RemoteException ex) {
+ // we should really never get this, but if we do then clear the lists, which
+ // will result in the dropping of the sync request
+ Log.e(TAG, "error trying to get the ProviderInfo for " + url, ex);
+ names.clear();
+ providers.clear();
+ }
+ }
+
+ public void scheduleLocalSync(Uri url) {
+ final Bundle extras = new Bundle();
+ extras.putBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, true);
+ scheduleSync(url, extras, LOCAL_SYNC_DELAY);
+ }
+
+ private IPackageManager getPackageManager() {
+ // Don't bother synchronizing on this. The worst that can happen is that two threads
+ // can try to get the package manager at the same time but only one result gets
+ // used. Since there is only one package manager in the system this doesn't matter.
+ if (mPackageManager == null) {
+ IBinder b = ServiceManager.getService("package");
+ mPackageManager = IPackageManager.Stub.asInterface(b);
+ }
+ return mPackageManager;
+ }
+
+ /**
+ * Initiate a sync for this given URL, or pass null for a full sync.
+ *
+ * <p>You'll start getting callbacks after this.
+ *
+ * @param url The Uri of a specific provider to be synced, or
+ * null to sync all providers.
+ * @param extras a Map of SyncAdapter specific information to control
+ * syncs of a specific provider. Can be null. Is ignored
+ */
+ public void startSync(Uri url, Bundle extras) {
+ scheduleSync(url, extras, 0 /* no delay */);
+ }
+
+ public void updateHeartbeatTime() {
+ mHeartbeatTime = SystemClock.elapsedRealtime();
+ ensureContentResolver();
+ mContentResolver.notifyChange(Sync.Active.CONTENT_URI,
+ null /* this change wasn't made through an observer */);
+ }
+
+ private void sendSyncAlarmMessage() {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "sending MESSAGE_SYNC_ALARM");
+ mSyncHandler.sendEmptyMessage(SyncHandler.MESSAGE_SYNC_ALARM);
+ }
+
+ private void sendCheckAlarmsMessage() {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "sending MESSAGE_CHECK_ALARMS");
+ mSyncHandler.sendEmptyMessage(SyncHandler.MESSAGE_CHECK_ALARMS);
+ }
+
+ private void sendSyncFinishedOrCanceledMessage(ActiveSyncContext syncContext,
+ SyncResult syncResult) {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "sending MESSAGE_SYNC_FINISHED");
+ Message msg = mSyncHandler.obtainMessage();
+ msg.what = SyncHandler.MESSAGE_SYNC_FINISHED;
+ msg.obj = new SyncHandlerMessagePayload(syncContext, syncResult);
+ mSyncHandler.sendMessage(msg);
+ }
+
+ class SyncHandlerMessagePayload {
+ public final ActiveSyncContext activeSyncContext;
+ public final SyncResult syncResult;
+
+ SyncHandlerMessagePayload(ActiveSyncContext syncContext, SyncResult syncResult) {
+ this.activeSyncContext = syncContext;
+ this.syncResult = syncResult;
+ }
+ }
+
+ class SyncAlarmIntentReceiver extends BroadcastReceiver {
+ public void onReceive(Context context, Intent intent) {
+ mHandleAlarmWakeLock.acquire();
+ sendSyncAlarmMessage();
+ }
+ }
+
+ class SyncPollAlarmReceiver extends BroadcastReceiver {
+ public void onReceive(Context context, Intent intent) {
+ handleSyncPollAlarm();
+ }
+ }
+
+ private void rescheduleImmediately(SyncOperation syncOperation) {
+ SyncOperation rescheduledSyncOperation = new SyncOperation(syncOperation);
+ rescheduledSyncOperation.setDelay(0);
+ scheduleSyncOperation(rescheduledSyncOperation);
+ }
+
+ private long rescheduleWithDelay(SyncOperation syncOperation) {
+ long newDelayInMs;
+
+ if (syncOperation.delay == 0) {
+ // The initial delay is the jitterized INITIAL_SYNC_RETRY_TIME_IN_MS
+ newDelayInMs = jitterize(INITIAL_SYNC_RETRY_TIME_IN_MS,
+ (long)(INITIAL_SYNC_RETRY_TIME_IN_MS * 1.1));
+ } else {
+ // Subsequent delays are the double of the previous delay
+ newDelayInMs = syncOperation.delay * 2;
+ }
+
+ // Cap the delay
+ ensureContentResolver();
+ long maxSyncRetryTimeInSeconds = Settings.Gservices.getLong(mContentResolver,
+ Settings.Gservices.SYNC_MAX_RETRY_DELAY_IN_SECONDS,
+ DEFAULT_MAX_SYNC_RETRY_TIME_IN_SECONDS);
+ if (newDelayInMs > maxSyncRetryTimeInSeconds * 1000) {
+ newDelayInMs = maxSyncRetryTimeInSeconds * 1000;
+ }
+
+ SyncOperation rescheduledSyncOperation = new SyncOperation(syncOperation);
+ rescheduledSyncOperation.setDelay(newDelayInMs);
+ scheduleSyncOperation(rescheduledSyncOperation);
+ return newDelayInMs;
+ }
+
+ /**
+ * Cancel the active sync if it matches the uri. The uri corresponds to the one passed
+ * in to startSync().
+ * @param uri If non-null, the active sync is only canceled if it matches the uri.
+ * If null, any active sync is canceled.
+ */
+ public void cancelActiveSync(Uri uri) {
+ ActiveSyncContext activeSyncContext = mActiveSyncContext;
+ if (activeSyncContext != null) {
+ // if a Uri was specified then only cancel the sync if it matches the the uri
+ if (uri != null) {
+ if (!uri.getAuthority().equals(activeSyncContext.mSyncOperation.authority)) {
+ return;
+ }
+ }
+ sendSyncFinishedOrCanceledMessage(activeSyncContext,
+ null /* no result since this is a cancel */);
+ }
+ }
+
+ /**
+ * Create and schedule a SyncOperation.
+ *
+ * @param syncOperation the SyncOperation to schedule
+ */
+ public void scheduleSyncOperation(SyncOperation syncOperation) {
+ // If this operation is expedited and there is a sync in progress then
+ // reschedule the current operation and send a cancel for it.
+ final boolean expedited = syncOperation.delay < 0;
+ final ActiveSyncContext activeSyncContext = mActiveSyncContext;
+ if (expedited && activeSyncContext != null) {
+ final boolean activeIsExpedited = activeSyncContext.mSyncOperation.delay < 0;
+ final boolean hasSameKey =
+ activeSyncContext.mSyncOperation.key.equals(syncOperation.key);
+ // This request is expedited and there is a sync in progress.
+ // Interrupt the current sync only if it is not expedited and if it has a different
+ // key than the one we are scheduling.
+ if (!activeIsExpedited && !hasSameKey) {
+ rescheduleImmediately(activeSyncContext.mSyncOperation);
+ sendSyncFinishedOrCanceledMessage(activeSyncContext,
+ null /* no result since this is a cancel */);
+ }
+ }
+
+ boolean operationEnqueued;
+ synchronized (mSyncQueue) {
+ operationEnqueued = mSyncQueue.add(syncOperation);
+ }
+
+ if (operationEnqueued) {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "scheduleSyncOperation: enqueued " + syncOperation);
+ }
+ sendCheckAlarmsMessage();
+ } else {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "scheduleSyncOperation: dropping duplicate sync operation "
+ + syncOperation);
+ }
+ }
+ }
+
+ /**
+ * Remove any scheduled sync operations that match uri. The uri corresponds to the one passed
+ * in to startSync().
+ * @param uri If non-null, only operations that match the uri are cleared.
+ * If null, all operations are cleared.
+ */
+ public void clearScheduledSyncOperations(Uri uri) {
+ synchronized (mSyncQueue) {
+ mSyncQueue.clear(null, uri != null ? uri.getAuthority() : null);
+ }
+ }
+
+ void maybeRescheduleSync(SyncResult syncResult, SyncOperation previousSyncOperation) {
+ boolean isLoggable = Log.isLoggable(TAG, Log.DEBUG);
+ if (isLoggable) {
+ Log.d(TAG, "encountered error(s) during the sync: " + syncResult + ", "
+ + previousSyncOperation);
+ }
+
+ // If the operation succeeded to some extent then retry immediately.
+ // If this was a two-way sync then retry soft errors with an exponential backoff.
+ // If this was an upward sync then schedule a two-way sync immediately.
+ // Otherwise do not reschedule.
+
+ if (syncResult.madeSomeProgress()) {
+ if (isLoggable) {
+ Log.d(TAG, "retrying sync operation immediately because "
+ + "even though it had an error it achieved some success");
+ }
+ rescheduleImmediately(previousSyncOperation);
+ } else if (previousSyncOperation.extras.getBoolean(
+ ContentResolver.SYNC_EXTRAS_UPLOAD, false)) {
+ final SyncOperation newSyncOperation = new SyncOperation(previousSyncOperation);
+ newSyncOperation.extras.putBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, false);
+ newSyncOperation.setDelay(0);
+ if (Config.LOGD) {
+ Log.d(TAG, "retrying sync operation as a two-way sync because an upload-only sync "
+ + "encountered an error: " + previousSyncOperation);
+ }
+ scheduleSyncOperation(newSyncOperation);
+ } else if (syncResult.hasSoftError()) {
+ long delay = rescheduleWithDelay(previousSyncOperation);
+ if (delay >= 0) {
+ if (isLoggable) {
+ Log.d(TAG, "retrying sync operation in " + delay + " ms because "
+ + "it encountered a soft error: " + previousSyncOperation);
+ }
+ }
+ } else {
+ if (Config.LOGD) {
+ Log.d(TAG, "not retrying sync operation because the error is a hard error: "
+ + previousSyncOperation);
+ }
+ }
+ }
+
+ /**
+ * Value type that represents a sync operation.
+ */
+ static class SyncOperation implements Comparable {
+ final String account;
+ int syncSource;
+ String authority;
+ Bundle extras;
+ final String key;
+ long earliestRunTime;
+ long delay;
+ Long rowId = null;
+
+ SyncOperation(String account, int source, String authority, Bundle extras, long delay) {
+ this.account = account;
+ this.syncSource = source;
+ this.authority = authority;
+ this.extras = new Bundle(extras);
+ this.setDelay(delay);
+ this.key = toKey();
+ }
+
+ SyncOperation(SyncOperation other) {
+ this.account = other.account;
+ this.syncSource = other.syncSource;
+ this.authority = other.authority;
+ this.extras = new Bundle(other.extras);
+ this.delay = other.delay;
+ this.earliestRunTime = other.earliestRunTime;
+ this.key = toKey();
+ }
+
+ public void setDelay(long delay) {
+ this.delay = delay;
+ if (delay >= 0) {
+ this.earliestRunTime = SystemClock.elapsedRealtime() + delay;
+ } else {
+ this.earliestRunTime = 0;
+ }
+ }
+
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("authority: ").append(authority);
+ sb.append(" account: ").append(account);
+ sb.append(" extras: ");
+ extrasToStringBuilder(extras, sb);
+ sb.append(" syncSource: ").append(syncSource);
+ sb.append(" when: ").append(earliestRunTime);
+ sb.append(" delay: ").append(delay);
+ sb.append(" key: {").append(key).append("}");
+ if (rowId != null) sb.append(" rowId: ").append(rowId);
+ return sb.toString();
+ }
+
+ private String toKey() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("authority: ").append(authority);
+ sb.append(" account: ").append(account);
+ sb.append(" extras: ");
+ extrasToStringBuilder(extras, sb);
+ return sb.toString();
+ }
+
+ private static void extrasToStringBuilder(Bundle bundle, StringBuilder sb) {
+ sb.append("[");
+ for (String key : bundle.keySet()) {
+ sb.append(key).append("=").append(bundle.get(key)).append(" ");
+ }
+ sb.append("]");
+ }
+
+ public int compareTo(Object o) {
+ SyncOperation other = (SyncOperation)o;
+ if (earliestRunTime == other.earliestRunTime) {
+ return 0;
+ }
+ return (earliestRunTime < other.earliestRunTime) ? -1 : 1;
+ }
+ }
+
+ /**
+ * @hide
+ */
+ class ActiveSyncContext extends ISyncContext.Stub {
+ final SyncOperation mSyncOperation;
+ final long mHistoryRowId;
+ final IContentProvider mContentProvider;
+ final ISyncAdapter mSyncAdapter;
+ final long mStartTime;
+ long mTimeoutStartTime;
+
+ public ActiveSyncContext(SyncOperation syncOperation, IContentProvider contentProvider,
+ ISyncAdapter syncAdapter, long historyRowId) {
+ super();
+ mSyncOperation = syncOperation;
+ mHistoryRowId = historyRowId;
+ mContentProvider = contentProvider;
+ mSyncAdapter = syncAdapter;
+ mStartTime = SystemClock.elapsedRealtime();
+ mTimeoutStartTime = mStartTime;
+ }
+
+ public void sendHeartbeat() {
+ // ignore this call if it corresponds to an old sync session
+ if (mActiveSyncContext == this) {
+ SyncManager.this.updateHeartbeatTime();
+ }
+ }
+
+ public void onFinished(SyncResult result) {
+ // include "this" in the message so that the handler can ignore it if this
+ // ActiveSyncContext is no longer the mActiveSyncContext at message handling
+ // time
+ sendSyncFinishedOrCanceledMessage(this, result);
+ }
+
+ public void toString(StringBuilder sb) {
+ sb.append("startTime ").append(mStartTime)
+ .append(", mTimeoutStartTime ").append(mTimeoutStartTime)
+ .append(", mHistoryRowId ").append(mHistoryRowId)
+ .append(", syncOperation ").append(mSyncOperation);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ toString(sb);
+ return sb.toString();
+ }
+ }
+
+ protected void dump(FileDescriptor fd, PrintWriter pw) {
+ StringBuilder sb = new StringBuilder();
+ dumpSyncState(sb);
+ sb.append("\n");
+ if (isSyncEnabled()) {
+ dumpSyncHistory(sb);
+ }
+ pw.println(sb.toString());
+ }
+
+ protected void dumpSyncState(StringBuilder sb) {
+ sb.append("sync enabled: ").append(isSyncEnabled()).append("\n");
+ sb.append("data connected: ").append(mDataConnectionIsConnected).append("\n");
+ sb.append("memory low: ").append(mStorageIsLow).append("\n");
+
+ final String[] accounts = mAccounts;
+ sb.append("accounts: ");
+ if (accounts != null) {
+ sb.append(accounts.length);
+ } else {
+ sb.append("none");
+ }
+ sb.append("\n");
+ final long now = SystemClock.elapsedRealtime();
+ sb.append("now: ").append(now).append("\n");
+ sb.append("uptime: ").append(DateUtils.formatElapsedTime(now/1000)).append(" (HH:MM:SS)\n");
+ sb.append("time spent syncing : ")
+ .append(DateUtils.formatElapsedTime(
+ mSyncHandler.mSyncTimeTracker.timeSpentSyncing() / 1000))
+ .append(" (HH:MM:SS), sync ")
+ .append(mSyncHandler.mSyncTimeTracker.mLastWasSyncing ? "" : "not ")
+ .append("in progress").append("\n");
+ if (mSyncHandler.mAlarmScheduleTime != null) {
+ sb.append("next alarm time: ").append(mSyncHandler.mAlarmScheduleTime)
+ .append(" (")
+ .append(DateUtils.formatElapsedTime((mSyncHandler.mAlarmScheduleTime-now)/1000))
+ .append(" (HH:MM:SS) from now)\n");
+ } else {
+ sb.append("no alarm is scheduled (there had better not be any pending syncs)\n");
+ }
+
+ sb.append("active sync: ").append(mActiveSyncContext).append("\n");
+
+ sb.append("notification info: ");
+ mSyncHandler.mSyncNotificationInfo.toString(sb);
+ sb.append("\n");
+
+ synchronized (mSyncQueue) {
+ sb.append("sync queue: ");
+ mSyncQueue.dump(sb);
+ }
+
+ Cursor c = mSyncStorageEngine.query(Sync.Active.CONTENT_URI,
+ SYNC_ACTIVE_PROJECTION, null, null, null);
+ sb.append("\n");
+ try {
+ if (c.moveToNext()) {
+ final long durationInSeconds = (now - c.getLong(2)) / 1000;
+ sb.append("Active sync: ").append(c.getString(0))
+ .append(" ").append(c.getString(1))
+ .append(", duration is ")
+ .append(DateUtils.formatElapsedTime(durationInSeconds)).append(".\n");
+ } else {
+ sb.append("No sync is in progress.\n");
+ }
+ } finally {
+ c.close();
+ }
+
+ c = mSyncStorageEngine.query(Sync.Pending.CONTENT_URI,
+ SYNC_PENDING_PROJECTION, null, null, "account, authority");
+ sb.append("\nPending Syncs\n");
+ try {
+ if (c.getCount() != 0) {
+ dumpSyncPendingHeader(sb);
+ while (c.moveToNext()) {
+ dumpSyncPendingRow(sb, c);
+ }
+ dumpSyncPendingFooter(sb);
+ } else {
+ sb.append("none\n");
+ }
+ } finally {
+ c.close();
+ }
+
+ String currentAccount = null;
+ c = mSyncStorageEngine.query(Sync.Status.CONTENT_URI,
+ STATUS_PROJECTION, null, null, "account, authority");
+ sb.append("\nSync history by account and authority\n");
+ try {
+ while (c.moveToNext()) {
+ if (!TextUtils.equals(currentAccount, c.getString(0))) {
+ if (currentAccount != null) {
+ dumpSyncHistoryFooter(sb);
+ }
+ currentAccount = c.getString(0);
+ dumpSyncHistoryHeader(sb, currentAccount);
+ }
+
+ dumpSyncHistoryRow(sb, c);
+ }
+ if (c.getCount() > 0) dumpSyncHistoryFooter(sb);
+ } finally {
+ c.close();
+ }
+ }
+
+ private void dumpSyncHistoryHeader(StringBuilder sb, String account) {
+ sb.append(" Account: ").append(account).append("\n");
+ sb.append(" ___________________________________________________________________________________________________________________________\n");
+ sb.append(" | | num times synced | total | last success | |\n");
+ sb.append(" | authority | local | poll | server | user | total | duration | source | time | result if failing |\n");
+ }
+
+ private static String[] STATUS_PROJECTION = new String[]{
+ Sync.Status.ACCOUNT, // 0
+ Sync.Status.AUTHORITY, // 1
+ Sync.Status.NUM_SYNCS, // 2
+ Sync.Status.TOTAL_ELAPSED_TIME, // 3
+ Sync.Status.NUM_SOURCE_LOCAL, // 4
+ Sync.Status.NUM_SOURCE_POLL, // 5
+ Sync.Status.NUM_SOURCE_SERVER, // 6
+ Sync.Status.NUM_SOURCE_USER, // 7
+ Sync.Status.LAST_SUCCESS_SOURCE, // 8
+ Sync.Status.LAST_SUCCESS_TIME, // 9
+ Sync.Status.LAST_FAILURE_SOURCE, // 10
+ Sync.Status.LAST_FAILURE_TIME, // 11
+ Sync.Status.LAST_FAILURE_MESG // 12
+ };
+
+ private void dumpSyncHistoryRow(StringBuilder sb, Cursor c) {
+ boolean hasSuccess = !c.isNull(9);
+ boolean hasFailure = !c.isNull(11);
+ Time timeSuccess = new Time();
+ if (hasSuccess) timeSuccess.set(c.getLong(9));
+ Time timeFailure = new Time();
+ if (hasFailure) timeFailure.set(c.getLong(11));
+ sb.append(String.format(" | %-15s | %5d | %5d | %6d | %5d | %5d | %8s | %7s | %19s | %19s |\n",
+ c.getString(1),
+ c.getLong(4),
+ c.getLong(5),
+ c.getLong(6),
+ c.getLong(7),
+ c.getLong(2),
+ DateUtils.formatElapsedTime(c.getLong(3)/1000),
+ hasSuccess ? Sync.History.SOURCES[c.getInt(8)] : "",
+ hasSuccess ? timeSuccess.format("%Y-%m-%d %H:%M:%S") : "",
+ hasFailure ? History.mesgToString(c.getString(12)) : ""));
+ }
+
+ private void dumpSyncHistoryFooter(StringBuilder sb) {
+ sb.append(" |___________________________________________________________________________________________________________________________|\n");
+ }
+
+ private void dumpSyncPendingHeader(StringBuilder sb) {
+ sb.append(" ____________________________________________________\n");
+ sb.append(" | account | authority |\n");
+ }
+
+ private void dumpSyncPendingRow(StringBuilder sb, Cursor c) {
+ sb.append(String.format(" | %-30s | %-15s |\n", c.getString(0), c.getString(1)));
+ }
+
+ private void dumpSyncPendingFooter(StringBuilder sb) {
+ sb.append(" |__________________________________________________|\n");
+ }
+
+ protected void dumpSyncHistory(StringBuilder sb) {
+ Cursor c = mSyncStorageEngine.query(Sync.History.CONTENT_URI, null, "event=?",
+ new String[]{String.valueOf(Sync.History.EVENT_STOP)},
+ Sync.HistoryColumns.EVENT_TIME + " desc");
+ try {
+ long numSyncsLastHour = 0, durationLastHour = 0;
+ long numSyncsLastDay = 0, durationLastDay = 0;
+ long numSyncsLastWeek = 0, durationLastWeek = 0;
+ long numSyncsLast4Weeks = 0, durationLast4Weeks = 0;
+ long numSyncsTotal = 0, durationTotal = 0;
+
+ long now = System.currentTimeMillis();
+ int indexEventTime = c.getColumnIndexOrThrow(Sync.History.EVENT_TIME);
+ int indexElapsedTime = c.getColumnIndexOrThrow(Sync.History.ELAPSED_TIME);
+ while (c.moveToNext()) {
+ long duration = c.getLong(indexElapsedTime);
+ long endTime = c.getLong(indexEventTime) + duration;
+ long millisSinceStart = now - endTime;
+ numSyncsTotal++;
+ durationTotal += duration;
+ if (millisSinceStart < MILLIS_IN_HOUR) {
+ numSyncsLastHour++;
+ durationLastHour += duration;
+ }
+ if (millisSinceStart < MILLIS_IN_DAY) {
+ numSyncsLastDay++;
+ durationLastDay += duration;
+ }
+ if (millisSinceStart < MILLIS_IN_WEEK) {
+ numSyncsLastWeek++;
+ durationLastWeek += duration;
+ }
+ if (millisSinceStart < MILLIS_IN_4WEEKS) {
+ numSyncsLast4Weeks++;
+ durationLast4Weeks += duration;
+ }
+ }
+ dumpSyncIntervalHeader(sb);
+ dumpSyncInterval(sb, "hour", MILLIS_IN_HOUR, numSyncsLastHour, durationLastHour);
+ dumpSyncInterval(sb, "day", MILLIS_IN_DAY, numSyncsLastDay, durationLastDay);
+ dumpSyncInterval(sb, "week", MILLIS_IN_WEEK, numSyncsLastWeek, durationLastWeek);
+ dumpSyncInterval(sb, "4 weeks",
+ MILLIS_IN_4WEEKS, numSyncsLast4Weeks, durationLast4Weeks);
+ dumpSyncInterval(sb, "total", 0, numSyncsTotal, durationTotal);
+ dumpSyncIntervalFooter(sb);
+ } finally {
+ c.close();
+ }
+ }
+
+ private void dumpSyncIntervalHeader(StringBuilder sb) {
+ sb.append("Sync Stats\n");
+ sb.append(" ___________________________________________________________\n");
+ sb.append(" | | | duration in sec | |\n");
+ sb.append(" | interval | count | average | total | % of interval |\n");
+ }
+
+ private void dumpSyncInterval(StringBuilder sb, String label,
+ long interval, long numSyncs, long duration) {
+ sb.append(String.format(" | %-8s | %6d | %8.1f | %8.1f",
+ label, numSyncs, ((float)duration/numSyncs)/1000, (float)duration/1000));
+ if (interval > 0) {
+ sb.append(String.format(" | %13.2f |\n", ((float)duration/interval)*100.0));
+ } else {
+ sb.append(String.format(" | %13s |\n", "na"));
+ }
+ }
+
+ private void dumpSyncIntervalFooter(StringBuilder sb) {
+ sb.append(" |_________________________________________________________|\n");
+ }
+
+ /**
+ * A helper object to keep track of the time we have spent syncing since the last boot
+ */
+ private class SyncTimeTracker {
+ /** True if a sync was in progress on the most recent call to update() */
+ boolean mLastWasSyncing = false;
+ /** Used to track when lastWasSyncing was last set */
+ long mWhenSyncStarted = 0;
+ /** The cumulative time we have spent syncing */
+ private long mTimeSpentSyncing;
+
+ /** Call to let the tracker know that the sync state may have changed */
+ public synchronized void update() {
+ final boolean isSyncInProgress = mActiveSyncContext != null;
+ if (isSyncInProgress == mLastWasSyncing) return;
+ final long now = SystemClock.elapsedRealtime();
+ if (isSyncInProgress) {
+ mWhenSyncStarted = now;
+ } else {
+ mTimeSpentSyncing += now - mWhenSyncStarted;
+ }
+ mLastWasSyncing = isSyncInProgress;
+ }
+
+ /** Get how long we have been syncing, in ms */
+ public synchronized long timeSpentSyncing() {
+ if (!mLastWasSyncing) return mTimeSpentSyncing;
+
+ final long now = SystemClock.elapsedRealtime();
+ return mTimeSpentSyncing + (now - mWhenSyncStarted);
+ }
+ }
+
+ /**
+ * Handles SyncOperation Messages that are posted to the associated
+ * HandlerThread.
+ */
+ class SyncHandler extends Handler {
+ // Messages that can be sent on mHandler
+ private static final int MESSAGE_SYNC_FINISHED = 1;
+ private static final int MESSAGE_SYNC_ALARM = 2;
+ private static final int MESSAGE_CHECK_ALARMS = 3;
+
+ public final SyncNotificationInfo mSyncNotificationInfo = new SyncNotificationInfo();
+ private Long mAlarmScheduleTime = null;
+ public final SyncTimeTracker mSyncTimeTracker = new SyncTimeTracker();
+
+ // used to track if we have installed the error notification so that we don't reinstall
+ // it if sync is still failing
+ private boolean mErrorNotificationInstalled = false;
+
+ /**
+ * Used to keep track of whether a sync notification is active and who it is for.
+ */
+ class SyncNotificationInfo {
+ // only valid if isActive is true
+ public String account;
+
+ // only valid if isActive is true
+ public String authority;
+
+ // true iff the notification manager has been asked to send the notification
+ public boolean isActive = false;
+
+ // Set when we transition from not running a sync to running a sync, and cleared on
+ // the opposite transition.
+ public Long startTime = null;
+
+ public void toString(StringBuilder sb) {
+ sb.append("account ").append(account)
+ .append(", authority ").append(authority)
+ .append(", isActive ").append(isActive)
+ .append(", startTime ").append(startTime);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ toString(sb);
+ return sb.toString();
+ }
+ }
+
+ public SyncHandler(Looper looper) {
+ super(looper);
+ }
+
+ public void handleMessage(Message msg) {
+ handleSyncHandlerMessage(msg);
+ }
+
+ private void handleSyncHandlerMessage(Message msg) {
+ try {
+ switch (msg.what) {
+ case SyncHandler.MESSAGE_SYNC_FINISHED:
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "handleSyncHandlerMessage: MESSAGE_SYNC_FINISHED");
+ }
+ SyncHandlerMessagePayload payload = (SyncHandlerMessagePayload)msg.obj;
+ if (mActiveSyncContext != payload.activeSyncContext) {
+ if (Config.LOGD) {
+ Log.d(TAG, "handleSyncHandlerMessage: sync context doesn't match, "
+ + "dropping: mActiveSyncContext " + mActiveSyncContext
+ + " != " + payload.activeSyncContext);
+ }
+ return;
+ }
+ runSyncFinishedOrCanceled(payload.syncResult);
+
+ // since we are no longer syncing, check if it is time to start a new sync
+ runStateIdle();
+ break;
+
+ case SyncHandler.MESSAGE_SYNC_ALARM: {
+ boolean isLoggable = Log.isLoggable(TAG, Log.VERBOSE);
+ if (isLoggable) {
+ Log.v(TAG, "handleSyncHandlerMessage: MESSAGE_SYNC_ALARM");
+ }
+ mAlarmScheduleTime = null;
+ try {
+ if (mActiveSyncContext != null) {
+ if (isLoggable) {
+ Log.v(TAG, "handleSyncHandlerMessage: sync context is active");
+ }
+ runStateSyncing();
+ }
+
+ // if the above call to runStateSyncing() resulted in the end of a sync,
+ // check if it is time to start a new sync
+ if (mActiveSyncContext == null) {
+ if (isLoggable) {
+ Log.v(TAG, "handleSyncHandlerMessage: "
+ + "sync context is not active");
+ }
+ runStateIdle();
+ }
+ } finally {
+ mHandleAlarmWakeLock.release();
+ }
+ break;
+ }
+
+ case SyncHandler.MESSAGE_CHECK_ALARMS:
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "handleSyncHandlerMessage: MESSAGE_CHECK_ALARMS");
+ }
+ // we do all the work for this case in the finally block
+ break;
+ }
+ } finally {
+ final boolean isSyncInProgress = mActiveSyncContext != null;
+ if (!isSyncInProgress) {
+ mSyncWakeLock.release();
+ }
+ manageSyncNotification();
+ manageErrorNotification();
+ manageSyncAlarm();
+ mSyncTimeTracker.update();
+ }
+ }
+
+ private void runStateSyncing() {
+ // if the sync timeout has been reached then cancel it
+
+ ActiveSyncContext activeSyncContext = mActiveSyncContext;
+
+ final long now = SystemClock.elapsedRealtime();
+ if (now > activeSyncContext.mTimeoutStartTime + MAX_TIME_PER_SYNC) {
+ SyncOperation nextSyncOperation;
+ synchronized (mSyncQueue) {
+ nextSyncOperation = mSyncQueue.head();
+ }
+ if (nextSyncOperation != null && nextSyncOperation.earliestRunTime <= now) {
+ if (Config.LOGD) {
+ Log.d(TAG, "canceling and rescheduling sync because it ran too long: "
+ + activeSyncContext.mSyncOperation);
+ }
+ rescheduleImmediately(activeSyncContext.mSyncOperation);
+ sendSyncFinishedOrCanceledMessage(activeSyncContext,
+ null /* no result since this is a cancel */);
+ } else {
+ activeSyncContext.mTimeoutStartTime = now + MAX_TIME_PER_SYNC;
+ }
+ }
+
+ // no need to schedule an alarm, as that will be done by our caller.
+ }
+
+ private void runStateIdle() {
+ boolean isLoggable = Log.isLoggable(TAG, Log.VERBOSE);
+ if (isLoggable) Log.v(TAG, "runStateIdle");
+
+ // If we aren't ready to run (e.g. the data connection is down), get out.
+ if (!mDataConnectionIsConnected) {
+ if (isLoggable) {
+ Log.v(TAG, "runStateIdle: no data connection, skipping");
+ }
+ setStatusText("No data connection");
+ return;
+ }
+
+ if (mStorageIsLow) {
+ if (isLoggable) {
+ Log.v(TAG, "runStateIdle: memory low, skipping");
+ }
+ setStatusText("Memory low");
+ return;
+ }
+
+ // If the accounts aren't known yet then we aren't ready to run. We will be kicked
+ // when the account lookup request does complete.
+ String[] accounts = mAccounts;
+ if (accounts == null) {
+ if (isLoggable) {
+ Log.v(TAG, "runStateIdle: accounts not known, skipping");
+ }
+ setStatusText("Accounts not known yet");
+ return;
+ }
+
+ // Otherwise consume SyncOperations from the head of the SyncQueue until one is
+ // found that is runnable (not disabled, etc). If that one is ready to run then
+ // start it, otherwise just get out.
+ SyncOperation syncOperation;
+ final Sync.Settings.QueryMap syncSettings = getSyncSettings();
+ synchronized (mSyncQueue) {
+ while (true) {
+ syncOperation = mSyncQueue.head();
+ if (syncOperation == null) {
+ if (isLoggable) {
+ Log.v(TAG, "runStateIdle: no more sync operations, returning");
+ }
+ return;
+ }
+
+ // Sync is disabled, drop this operation.
+ if (!isSyncEnabled()) {
+ if (isLoggable) {
+ Log.v(TAG, "runStateIdle: sync disabled, dropping " + syncOperation);
+ }
+ mSyncQueue.popHead();
+ continue;
+ }
+
+ // skip the sync if it isn't a force and the settings are off for this provider
+ final boolean force = syncOperation.extras.getBoolean(
+ ContentResolver.SYNC_EXTRAS_FORCE, false);
+ if (!force && (!syncSettings.getListenForNetworkTickles()
+ || !syncSettings.getSyncProviderAutomatically(
+ syncOperation.authority))) {
+ if (isLoggable) {
+ Log.v(TAG, "runStateIdle: sync off, dropping " + syncOperation);
+ }
+ mSyncQueue.popHead();
+ continue;
+ }
+
+ // skip the sync if the account of this operation no longer exists
+ if (!ArrayUtils.contains(accounts, syncOperation.account)) {
+ mSyncQueue.popHead();
+ if (isLoggable) {
+ Log.v(TAG, "runStateIdle: account not present, dropping "
+ + syncOperation);
+ }
+ continue;
+ }
+
+ // go ahead and try to sync this syncOperation
+ if (isLoggable) {
+ Log.v(TAG, "runStateIdle: found sync candidate: " + syncOperation);
+ }
+ break;
+ }
+
+ // If the first SyncOperation isn't ready to run schedule a wakeup and
+ // get out.
+ final long now = SystemClock.elapsedRealtime();
+ if (syncOperation.earliestRunTime > now) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "runStateIdle: the time is " + now + " yet the next "
+ + "sync operation is for " + syncOperation.earliestRunTime
+ + ": " + syncOperation);
+ }
+ return;
+ }
+
+ // We will do this sync. Remove it from the queue and run it outside of the
+ // synchronized block.
+ if (isLoggable) {
+ Log.v(TAG, "runStateIdle: we are going to sync " + syncOperation);
+ }
+ mSyncQueue.popHead();
+ }
+
+ String providerName = syncOperation.authority;
+ ensureContentResolver();
+ IContentProvider contentProvider;
+
+ // acquire the provider and update the sync history
+ try {
+ contentProvider = mContentResolver.acquireProvider(providerName);
+ if (contentProvider == null) {
+ Log.e(TAG, "Provider " + providerName + " doesn't exist");
+ return;
+ }
+ if (contentProvider.getSyncAdapter() == null) {
+ Log.e(TAG, "Provider " + providerName + " isn't syncable, " + contentProvider);
+ return;
+ }
+ } catch (RemoteException remoteExc) {
+ Log.e(TAG, "Caught a RemoteException while preparing for sync, rescheduling "
+ + syncOperation, remoteExc);
+ rescheduleWithDelay(syncOperation);
+ return;
+ } catch (RuntimeException exc) {
+ Log.e(TAG, "Caught a RuntimeException while validating sync of " + providerName,
+ exc);
+ return;
+ }
+
+ final long historyRowId = insertStartSyncEvent(syncOperation);
+
+ try {
+ ISyncAdapter syncAdapter = contentProvider.getSyncAdapter();
+ ActiveSyncContext activeSyncContext = new ActiveSyncContext(syncOperation,
+ contentProvider, syncAdapter, historyRowId);
+ mSyncWakeLock.acquire();
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "starting sync of " + syncOperation);
+ }
+ syncAdapter.startSync(activeSyncContext, syncOperation.account,
+ syncOperation.extras);
+ mActiveSyncContext = activeSyncContext;
+ mSyncStorageEngine.setActiveSync(mActiveSyncContext);
+ } catch (RemoteException remoteExc) {
+ if (Config.LOGD) {
+ Log.d(TAG, "runStateIdle: caught a RemoteException, rescheduling", remoteExc);
+ }
+ mActiveSyncContext = null;
+ mSyncStorageEngine.setActiveSync(mActiveSyncContext);
+ rescheduleWithDelay(syncOperation);
+ } catch (RuntimeException exc) {
+ mActiveSyncContext = null;
+ mSyncStorageEngine.setActiveSync(mActiveSyncContext);
+ Log.e(TAG, "Caught a RuntimeException while starting the sync " + syncOperation,
+ exc);
+ }
+
+ // no need to schedule an alarm, as that will be done by our caller.
+ }
+
+ private void runSyncFinishedOrCanceled(SyncResult syncResult) {
+ boolean isLoggable = Log.isLoggable(TAG, Log.VERBOSE);
+ if (isLoggable) Log.v(TAG, "runSyncFinishedOrCanceled");
+ ActiveSyncContext activeSyncContext = mActiveSyncContext;
+ mActiveSyncContext = null;
+ mSyncStorageEngine.setActiveSync(mActiveSyncContext);
+
+ final SyncOperation syncOperation = activeSyncContext.mSyncOperation;
+
+ final long elapsedTime = SystemClock.elapsedRealtime() - activeSyncContext.mStartTime;
+
+ String historyMessage;
+ int downstreamActivity;
+ int upstreamActivity;
+ if (syncResult != null) {
+ if (isLoggable) {
+ Log.v(TAG, "runSyncFinishedOrCanceled: is a finished: operation "
+ + syncOperation + ", result " + syncResult);
+ }
+
+ if (!syncResult.hasError()) {
+ if (isLoggable) {
+ Log.v(TAG, "finished sync operation " + syncOperation);
+ }
+ historyMessage = History.MESG_SUCCESS;
+ // TODO: set these correctly when the SyncResult is extended to include it
+ downstreamActivity = 0;
+ upstreamActivity = 0;
+ } else {
+ maybeRescheduleSync(syncResult, syncOperation);
+ if (Config.LOGD) {
+ Log.d(TAG, "failed sync operation " + syncOperation);
+ }
+ historyMessage = Integer.toString(syncResultToErrorNumber(syncResult));
+ // TODO: set these correctly when the SyncResult is extended to include it
+ downstreamActivity = 0;
+ upstreamActivity = 0;
+ }
+ } else {
+ if (isLoggable) {
+ Log.v(TAG, "runSyncFinishedOrCanceled: is a cancel: operation "
+ + syncOperation);
+ }
+ try {
+ activeSyncContext.mSyncAdapter.cancelSync();
+ } catch (RemoteException e) {
+ // we don't need to retry this in this case
+ }
+ historyMessage = History.MESG_CANCELED;
+ downstreamActivity = 0;
+ upstreamActivity = 0;
+ }
+
+ stopSyncEvent(activeSyncContext.mHistoryRowId, syncOperation, historyMessage,
+ upstreamActivity, downstreamActivity, elapsedTime);
+
+ mContentResolver.releaseProvider(activeSyncContext.mContentProvider);
+
+ if (syncResult != null && syncResult.tooManyDeletions) {
+ installHandleTooManyDeletesNotification(syncOperation.account,
+ syncOperation.authority, syncResult.stats.numDeletes);
+ } else {
+ mNotificationMgr.cancel(
+ syncOperation.account.hashCode() ^ syncOperation.authority.hashCode());
+ }
+
+ if (syncResult != null && syncResult.fullSyncRequested) {
+ scheduleSyncOperation(new SyncOperation(syncOperation.account,
+ syncOperation.syncSource, syncOperation.authority, new Bundle(), 0));
+ }
+ // no need to schedule an alarm, as that will be done by our caller.
+ }
+
+ /**
+ * Convert the error-containing SyncResult into the Sync.History error number. Since
+ * the SyncResult may indicate multiple errors at once, this method just returns the
+ * most "serious" error.
+ * @param syncResult the SyncResult from which to read
+ * @return the most "serious" error set in the SyncResult
+ * @throws IllegalStateException if the SyncResult does not indicate any errors.
+ * If SyncResult.error() is true then it is safe to call this.
+ */
+ private int syncResultToErrorNumber(SyncResult syncResult) {
+ if (syncResult.syncAlreadyInProgress) return History.ERROR_SYNC_ALREADY_IN_PROGRESS;
+ if (syncResult.stats.numAuthExceptions > 0) return History.ERROR_AUTHENTICATION;
+ if (syncResult.stats.numIoExceptions > 0) return History.ERROR_IO;
+ if (syncResult.stats.numParseExceptions > 0) return History.ERROR_PARSE;
+ if (syncResult.stats.numConflictDetectedExceptions > 0) return History.ERROR_CONFLICT;
+ if (syncResult.tooManyDeletions) return History.ERROR_TOO_MANY_DELETIONS;
+ if (syncResult.tooManyRetries) return History.ERROR_TOO_MANY_RETRIES;
+ throw new IllegalStateException("we are not in an error state, " + toString());
+ }
+
+ private void manageSyncNotification() {
+ boolean shouldCancel;
+ boolean shouldInstall;
+
+ if (mActiveSyncContext == null) {
+ mSyncNotificationInfo.startTime = null;
+
+ // we aren't syncing. if the notification is active then remember that we need
+ // to cancel it and then clear out the info
+ shouldCancel = mSyncNotificationInfo.isActive;
+ shouldInstall = false;
+ } else {
+ // we are syncing
+ final SyncOperation syncOperation = mActiveSyncContext.mSyncOperation;
+
+ final long now = SystemClock.elapsedRealtime();
+ if (mSyncNotificationInfo.startTime == null) {
+ mSyncNotificationInfo.startTime = now;
+ }
+
+ // cancel the notification if it is up and the authority or account is wrong
+ shouldCancel = mSyncNotificationInfo.isActive &&
+ (!syncOperation.authority.equals(mSyncNotificationInfo.authority)
+ || !syncOperation.account.equals(mSyncNotificationInfo.account));
+
+ // there are four cases:
+ // - the notification is up and there is no change: do nothing
+ // - the notification is up but we should cancel since it is stale:
+ // need to install
+ // - the notification is not up but it isn't time yet: don't install
+ // - the notification is not up and it is time: need to install
+
+ if (mSyncNotificationInfo.isActive) {
+ shouldInstall = shouldCancel;
+ } else {
+ final boolean timeToShowNotification =
+ now > mSyncNotificationInfo.startTime + SYNC_NOTIFICATION_DELAY;
+ final boolean syncIsForced = syncOperation.extras
+ .getBoolean(ContentResolver.SYNC_EXTRAS_FORCE, false);
+ shouldInstall = timeToShowNotification || syncIsForced;
+ }
+ }
+
+ if (shouldCancel && !shouldInstall) {
+ mNeedSyncActiveNotification = false;
+ sendSyncStateIntent();
+ mSyncNotificationInfo.isActive = false;
+ }
+
+ if (shouldInstall) {
+ SyncOperation syncOperation = mActiveSyncContext.mSyncOperation;
+ mNeedSyncActiveNotification = true;
+ sendSyncStateIntent();
+ mSyncNotificationInfo.isActive = true;
+ mSyncNotificationInfo.account = syncOperation.account;
+ mSyncNotificationInfo.authority = syncOperation.authority;
+ }
+ }
+
+ /**
+ * Check if there were any long-lasting errors, if so install the error notification,
+ * otherwise cancel the error notification.
+ */
+ private void manageErrorNotification() {
+ //
+ long when = mSyncStorageEngine.getInitialSyncFailureTime();
+ if ((when > 0) && (when + ERROR_NOTIFICATION_DELAY_MS < System.currentTimeMillis())) {
+ if (!mErrorNotificationInstalled) {
+ mNeedSyncErrorNotification = true;
+ sendSyncStateIntent();
+ }
+ mErrorNotificationInstalled = true;
+ } else {
+ if (mErrorNotificationInstalled) {
+ mNeedSyncErrorNotification = false;
+ sendSyncStateIntent();
+ }
+ mErrorNotificationInstalled = false;
+ }
+ }
+
+ private void manageSyncAlarm() {
+ // in each of these cases the sync loop will be kicked, which will cause this
+ // method to be called again
+ if (!mDataConnectionIsConnected) return;
+ if (mAccounts == null) return;
+ if (mStorageIsLow) return;
+
+ // Compute the alarm fire time:
+ // - not syncing: time of the next sync operation
+ // - syncing, no notification: time from sync start to notification create time
+ // - syncing, with notification: time till timeout of the active sync operation
+ Long alarmTime = null;
+ ActiveSyncContext activeSyncContext = mActiveSyncContext;
+ if (activeSyncContext == null) {
+ SyncOperation syncOperation;
+ synchronized (mSyncQueue) {
+ syncOperation = mSyncQueue.head();
+ }
+ if (syncOperation != null) {
+ alarmTime = syncOperation.earliestRunTime;
+ }
+ } else {
+ final long notificationTime =
+ mSyncHandler.mSyncNotificationInfo.startTime + SYNC_NOTIFICATION_DELAY;
+ final long timeoutTime =
+ mActiveSyncContext.mTimeoutStartTime + MAX_TIME_PER_SYNC;
+ if (mSyncHandler.mSyncNotificationInfo.isActive) {
+ alarmTime = timeoutTime;
+ } else {
+ alarmTime = Math.min(notificationTime, timeoutTime);
+ }
+ }
+
+ // adjust the alarmTime so that we will wake up when it is time to
+ // install the error notification
+ if (!mErrorNotificationInstalled) {
+ long when = mSyncStorageEngine.getInitialSyncFailureTime();
+ if (when > 0) {
+ when += ERROR_NOTIFICATION_DELAY_MS;
+ // convert when fron absolute time to elapsed run time
+ long delay = when - System.currentTimeMillis();
+ when = SystemClock.elapsedRealtime() + delay;
+ alarmTime = alarmTime != null ? Math.min(alarmTime, when) : when;
+ }
+ }
+
+ // determine if we need to set or cancel the alarm
+ boolean shouldSet = false;
+ boolean shouldCancel = false;
+ final boolean alarmIsActive = mAlarmScheduleTime != null;
+ final boolean needAlarm = alarmTime != null;
+ if (needAlarm) {
+ if (!alarmIsActive || alarmTime < mAlarmScheduleTime) {
+ shouldSet = true;
+ }
+ } else {
+ shouldCancel = alarmIsActive;
+ }
+
+ // set or cancel the alarm as directed
+ ensureAlarmService();
+ if (shouldSet) {
+ mAlarmScheduleTime = alarmTime;
+ mAlarmService.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, alarmTime,
+ mSyncAlarmIntent);
+ } else if (shouldCancel) {
+ mAlarmScheduleTime = null;
+ mAlarmService.cancel(mSyncAlarmIntent);
+ }
+ }
+
+ private void sendSyncStateIntent() {
+ Intent syncStateIntent = new Intent(Intent.ACTION_SYNC_STATE_CHANGED);
+ syncStateIntent.putExtra("active", mNeedSyncActiveNotification);
+ syncStateIntent.putExtra("failing", mNeedSyncErrorNotification);
+ mContext.sendBroadcast(syncStateIntent);
+ }
+
+ private void installHandleTooManyDeletesNotification(String account, String authority,
+ long numDeletes) {
+ if (mNotificationMgr == null) return;
+ Intent clickIntent = new Intent();
+ clickIntent.setClassName("com.android.providers.subscribedfeeds",
+ "com.android.settings.SyncActivityTooManyDeletes");
+ clickIntent.putExtra("account", account);
+ clickIntent.putExtra("provider", authority);
+ clickIntent.putExtra("numDeletes", numDeletes);
+
+ if (!isActivityAvailable(clickIntent)) {
+ Log.w(TAG, "No activity found to handle too many deletes.");
+ return;
+ }
+
+ final PendingIntent pendingIntent = PendingIntent
+ .getActivity(mContext, 0, clickIntent, PendingIntent.FLAG_CANCEL_CURRENT);
+
+ CharSequence tooManyDeletesDescFormat = mContext.getResources().getText(
+ R.string.contentServiceTooManyDeletesNotificationDesc);
+
+ String[] authorities = authority.split(";");
+ Notification notification =
+ new Notification(R.drawable.stat_notify_sync_error,
+ mContext.getString(R.string.contentServiceSync),
+ System.currentTimeMillis());
+ notification.setLatestEventInfo(mContext,
+ mContext.getString(R.string.contentServiceSyncNotificationTitle),
+ String.format(tooManyDeletesDescFormat.toString(), authorities[0]),
+ pendingIntent);
+ notification.flags |= Notification.FLAG_ONGOING_EVENT;
+ mNotificationMgr.notify(account.hashCode() ^ authority.hashCode(), notification);
+ }
+
+ /**
+ * Checks whether an activity exists on the system image for the given intent.
+ *
+ * @param intent The intent for an activity.
+ * @return Whether or not an activity exists.
+ */
+ private boolean isActivityAvailable(Intent intent) {
+ PackageManager pm = mContext.getPackageManager();
+ List<ResolveInfo> list = pm.queryIntentActivities(intent, 0);
+ int listSize = list.size();
+ for (int i = 0; i < listSize; i++) {
+ ResolveInfo resolveInfo = list.get(i);
+ if ((resolveInfo.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM)
+ != 0) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public long insertStartSyncEvent(SyncOperation syncOperation) {
+ final int source = syncOperation.syncSource;
+ final long now = System.currentTimeMillis();
+
+ EventLog.writeEvent(2720, syncOperation.authority, Sync.History.EVENT_START, source);
+
+ return mSyncStorageEngine.insertStartSyncEvent(
+ syncOperation.account, syncOperation.authority, now, source);
+ }
+
+ public void stopSyncEvent(long rowId, SyncOperation syncOperation, String resultMessage,
+ int upstreamActivity, int downstreamActivity, long elapsedTime) {
+ EventLog.writeEvent(2720, syncOperation.authority, Sync.History.EVENT_STOP, syncOperation.syncSource);
+
+ mSyncStorageEngine.stopSyncEvent(rowId, elapsedTime, resultMessage,
+ downstreamActivity, upstreamActivity);
+ }
+ }
+
+ static class SyncQueue {
+ private SyncStorageEngine mSyncStorageEngine;
+ private final String[] COLUMNS = new String[]{
+ "_id",
+ "authority",
+ "account",
+ "extras",
+ "source"
+ };
+ private static final int COLUMN_ID = 0;
+ private static final int COLUMN_AUTHORITY = 1;
+ private static final int COLUMN_ACCOUNT = 2;
+ private static final int COLUMN_EXTRAS = 3;
+ private static final int COLUMN_SOURCE = 4;
+
+ private static final boolean DEBUG_CHECK_DATA_CONSISTENCY = false;
+
+ // A priority queue of scheduled SyncOperations that is designed to make it quick
+ // to find the next SyncOperation that should be considered for running.
+ private final PriorityQueue<SyncOperation> mOpsByWhen = new PriorityQueue<SyncOperation>();
+
+ // A Map of SyncOperations operationKey -> SyncOperation that is designed for
+ // quick lookup of an enqueued SyncOperation.
+ private final HashMap<String, SyncOperation> mOpsByKey = Maps.newHashMap();
+
+ public SyncQueue(SyncStorageEngine syncStorageEngine) {
+ mSyncStorageEngine = syncStorageEngine;
+ Cursor cursor = mSyncStorageEngine.getPendingSyncsCursor(COLUMNS);
+ try {
+ while (cursor.moveToNext()) {
+ add(cursorToOperation(cursor),
+ true /* this is being added from the database */);
+ }
+ } finally {
+ cursor.close();
+ if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(true /* check the DB */);
+ }
+ }
+
+ public boolean add(SyncOperation operation) {
+ return add(new SyncOperation(operation),
+ false /* this is not coming from the database */);
+ }
+
+ private boolean add(SyncOperation operation, boolean fromDatabase) {
+ if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(!fromDatabase);
+
+ // If this operation is expedited then set its earliestRunTime to be immediately
+ // before the head of the list, or not if none are in the list.
+ if (operation.delay < 0) {
+ SyncOperation headOperation = head();
+ if (headOperation != null) {
+ operation.earliestRunTime = Math.min(SystemClock.elapsedRealtime(),
+ headOperation.earliestRunTime - 1);
+ } else {
+ operation.earliestRunTime = SystemClock.elapsedRealtime();
+ }
+ }
+
+ // - if an operation with the same key exists and this one should run earlier,
+ // delete the old one and add the new one
+ // - if an operation with the same key exists and if this one should run
+ // later, ignore it
+ // - if no operation exists then add the new one
+ final String operationKey = operation.key;
+ SyncOperation existingOperation = mOpsByKey.get(operationKey);
+
+ // if this operation matches an existing operation that is being retried (delay > 0)
+ // and this operation isn't forced, ignore this operation
+ if (existingOperation != null && existingOperation.delay > 0) {
+ if (!operation.extras.getBoolean(ContentResolver.SYNC_EXTRAS_FORCE, false)) {
+ return false;
+ }
+ }
+
+ if (existingOperation != null
+ && operation.earliestRunTime >= existingOperation.earliestRunTime) {
+ if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(!fromDatabase);
+ return false;
+ }
+
+ if (existingOperation != null) {
+ removeByKey(operationKey);
+ }
+
+ if (operation.rowId == null) {
+ byte[] extrasData = null;
+ Parcel parcel = Parcel.obtain();
+ try {
+ operation.extras.writeToParcel(parcel, 0);
+ extrasData = parcel.marshall();
+ } finally {
+ parcel.recycle();
+ }
+ ContentValues values = new ContentValues();
+ values.put("account", operation.account);
+ values.put("authority", operation.authority);
+ values.put("source", operation.syncSource);
+ values.put("extras", extrasData);
+ Uri pendingUri = mSyncStorageEngine.insertIntoPending(values);
+ operation.rowId = pendingUri == null ? null : ContentUris.parseId(pendingUri);
+ if (operation.rowId == null) {
+ throw new IllegalStateException("error adding pending sync operation "
+ + operation);
+ }
+ }
+
+ if (DEBUG_CHECK_DATA_CONSISTENCY) {
+ debugCheckDataStructures(
+ false /* don't compare with the DB, since we know
+ it is inconsistent right now */ );
+ }
+ mOpsByKey.put(operationKey, operation);
+ mOpsByWhen.add(operation);
+ if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(!fromDatabase);
+ return true;
+ }
+
+ public void removeByKey(String operationKey) {
+ if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(true /* check the DB */);
+ SyncOperation operationToRemove = mOpsByKey.remove(operationKey);
+ if (!mOpsByWhen.remove(operationToRemove)) {
+ throw new IllegalStateException(
+ "unable to find " + operationToRemove + " in mOpsByWhen");
+ }
+
+ if (mSyncStorageEngine.deleteFromPending(operationToRemove.rowId) != 1) {
+ throw new IllegalStateException("unable to find pending row for "
+ + operationToRemove);
+ }
+
+ if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(true /* check the DB */);
+ }
+
+ public SyncOperation head() {
+ if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(true /* check the DB */);
+ return mOpsByWhen.peek();
+ }
+
+ public void popHead() {
+ if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(true /* check the DB */);
+ SyncOperation operation = mOpsByWhen.remove();
+ if (mOpsByKey.remove(operation.key) == null) {
+ throw new IllegalStateException("unable to find " + operation + " in mOpsByKey");
+ }
+
+ if (mSyncStorageEngine.deleteFromPending(operation.rowId) != 1) {
+ throw new IllegalStateException("unable to find pending row for " + operation);
+ }
+
+ if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(true /* check the DB */);
+ }
+
+ public void clear(String account, String authority) {
+ Iterator<Map.Entry<String, SyncOperation>> entries = mOpsByKey.entrySet().iterator();
+ while (entries.hasNext()) {
+ Map.Entry<String, SyncOperation> entry = entries.next();
+ SyncOperation syncOperation = entry.getValue();
+ if (account != null && !syncOperation.account.equals(account)) continue;
+ if (authority != null && !syncOperation.authority.equals(authority)) continue;
+
+ if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(true /* check the DB */);
+ entries.remove();
+ if (!mOpsByWhen.remove(syncOperation)) {
+ throw new IllegalStateException(
+ "unable to find " + syncOperation + " in mOpsByWhen");
+ }
+
+ if (mSyncStorageEngine.deleteFromPending(syncOperation.rowId) != 1) {
+ throw new IllegalStateException("unable to find pending row for "
+ + syncOperation);
+ }
+
+ if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(true /* check the DB */);
+ }
+ }
+
+ public void dump(StringBuilder sb) {
+ sb.append("SyncQueue: ").append(mOpsByWhen.size()).append(" operation(s)\n");
+ for (SyncOperation operation : mOpsByWhen) {
+ sb.append(operation).append("\n");
+ }
+ }
+
+ private void debugCheckDataStructures(boolean checkDatabase) {
+ if (mOpsByKey.size() != mOpsByWhen.size()) {
+ throw new IllegalStateException("size mismatch: "
+ + mOpsByKey .size() + " != " + mOpsByWhen.size());
+ }
+ for (SyncOperation operation : mOpsByWhen) {
+ if (!mOpsByKey.containsKey(operation.key)) {
+ throw new IllegalStateException(
+ "operation " + operation + " is in mOpsByWhen but not mOpsByKey");
+ }
+ }
+ for (Map.Entry<String, SyncOperation> entry : mOpsByKey.entrySet()) {
+ final SyncOperation operation = entry.getValue();
+ final String key = entry.getKey();
+ if (!key.equals(operation.key)) {
+ throw new IllegalStateException(
+ "operation " + operation + " in mOpsByKey doesn't match key " + key);
+ }
+ if (!mOpsByWhen.contains(operation)) {
+ throw new IllegalStateException(
+ "operation " + operation + " is in mOpsByKey but not mOpsByWhen");
+ }
+ }
+
+ if (checkDatabase) {
+ // check that the DB contains the same rows as the in-memory data structures
+ Cursor cursor = mSyncStorageEngine.getPendingSyncsCursor(COLUMNS);
+ try {
+ if (mOpsByKey.size() != cursor.getCount()) {
+ StringBuilder sb = new StringBuilder();
+ DatabaseUtils.dumpCursor(cursor, sb);
+ sb.append("\n");
+ dump(sb);
+ throw new IllegalStateException("DB size mismatch: "
+ + mOpsByKey .size() + " != " + cursor.getCount() + "\n"
+ + sb.toString());
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+ }
+
+ private SyncOperation cursorToOperation(Cursor cursor) {
+ byte[] extrasData = cursor.getBlob(COLUMN_EXTRAS);
+ Bundle extras;
+ Parcel parcel = Parcel.obtain();
+ try {
+ parcel.unmarshall(extrasData, 0, extrasData.length);
+ parcel.setDataPosition(0);
+ extras = parcel.readBundle();
+ } catch (RuntimeException e) {
+ // A RuntimeException is thrown if we were unable to parse the parcel.
+ // Create an empty parcel in this case.
+ extras = new Bundle();
+ } finally {
+ parcel.recycle();
+ }
+
+ SyncOperation syncOperation = new SyncOperation(
+ cursor.getString(COLUMN_ACCOUNT),
+ cursor.getInt(COLUMN_SOURCE),
+ cursor.getString(COLUMN_AUTHORITY),
+ extras,
+ 0 /* delay */);
+ syncOperation.rowId = cursor.getLong(COLUMN_ID);
+ return syncOperation;
+ }
+ }
+}
diff --git a/core/java/android/content/SyncProvider.java b/core/java/android/content/SyncProvider.java
new file mode 100644
index 0000000..6ddd046
--- /dev/null
+++ b/core/java/android/content/SyncProvider.java
@@ -0,0 +1,53 @@
+// Copyright 2007 The Android Open Source Project
+package android.content;
+
+import android.database.Cursor;
+import android.net.Uri;
+
+/**
+ * ContentProvider that tracks the sync data and overall sync
+ * history on the device.
+ *
+ * @hide
+ */
+public class SyncProvider extends ContentProvider {
+ public SyncProvider() {
+ }
+
+ private SyncStorageEngine mSyncStorageEngine;
+
+ @Override
+ public boolean onCreate() {
+ mSyncStorageEngine = SyncStorageEngine.getSingleton();
+ return true;
+ }
+
+ @Override
+ public Cursor query(Uri url, String[] projectionIn,
+ String selection, String[] selectionArgs, String sort) {
+ return mSyncStorageEngine.query(url, projectionIn, selection, selectionArgs, sort);
+ }
+
+ @Override
+ public Uri insert(Uri url, ContentValues initialValues) {
+ return mSyncStorageEngine.insert(true /* the caller is the provider */,
+ url, initialValues);
+ }
+
+ @Override
+ public int delete(Uri url, String where, String[] whereArgs) {
+ return mSyncStorageEngine.delete(true /* the caller is the provider */,
+ url, where, whereArgs);
+ }
+
+ @Override
+ public int update(Uri url, ContentValues initialValues, String where, String[] whereArgs) {
+ return mSyncStorageEngine.update(true /* the caller is the provider */,
+ url, initialValues, where, whereArgs);
+ }
+
+ @Override
+ public String getType(Uri url) {
+ return mSyncStorageEngine.getType(url);
+ }
+}
diff --git a/core/java/android/content/SyncResult.aidl b/core/java/android/content/SyncResult.aidl
new file mode 100644
index 0000000..061b81c
--- /dev/null
+++ b/core/java/android/content/SyncResult.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+parcelable SyncResult;
diff --git a/core/java/android/content/SyncResult.java b/core/java/android/content/SyncResult.java
new file mode 100644
index 0000000..f3260f3
--- /dev/null
+++ b/core/java/android/content/SyncResult.java
@@ -0,0 +1,178 @@
+package android.content;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * This class is used to store information about the result of a sync
+ *
+ * @hide
+ */
+public final class SyncResult implements Parcelable {
+ public final boolean syncAlreadyInProgress;
+ public boolean tooManyDeletions;
+ public boolean tooManyRetries;
+ public boolean databaseError;
+ public boolean fullSyncRequested;
+ public boolean partialSyncUnavailable;
+ public boolean moreRecordsToGet;
+ public final SyncStats stats;
+ public static final SyncResult ALREADY_IN_PROGRESS;
+
+ static {
+ ALREADY_IN_PROGRESS = new SyncResult(true);
+ }
+
+ public SyncResult() {
+ this(false);
+ }
+
+ private SyncResult(boolean syncAlreadyInProgress) {
+ this.syncAlreadyInProgress = syncAlreadyInProgress;
+ this.tooManyDeletions = false;
+ this.tooManyRetries = false;
+ this.fullSyncRequested = false;
+ this.partialSyncUnavailable = false;
+ this.moreRecordsToGet = false;
+ this.stats = new SyncStats();
+ }
+
+ private SyncResult(Parcel parcel) {
+ syncAlreadyInProgress = parcel.readInt() != 0;
+ tooManyDeletions = parcel.readInt() != 0;
+ tooManyRetries = parcel.readInt() != 0;
+ databaseError = parcel.readInt() != 0;
+ fullSyncRequested = parcel.readInt() != 0;
+ partialSyncUnavailable = parcel.readInt() != 0;
+ moreRecordsToGet = parcel.readInt() != 0;
+ stats = new SyncStats(parcel);
+ }
+
+ public boolean hasHardError() {
+ return stats.numParseExceptions > 0
+ || stats.numConflictDetectedExceptions > 0
+ || stats.numAuthExceptions > 0
+ || tooManyDeletions
+ || tooManyRetries
+ || databaseError;
+ }
+
+ public boolean hasSoftError() {
+ return syncAlreadyInProgress || stats.numIoExceptions > 0;
+ }
+
+ public boolean hasError() {
+ return hasSoftError() || hasHardError();
+ }
+
+ public boolean madeSomeProgress() {
+ return ((stats.numDeletes > 0) && !tooManyDeletions)
+ || stats.numInserts > 0
+ || stats.numUpdates > 0;
+ }
+
+ public void clear() {
+ if (syncAlreadyInProgress) {
+ throw new UnsupportedOperationException(
+ "you are not allowed to clear the ALREADY_IN_PROGRESS SyncStats");
+ }
+ tooManyDeletions = false;
+ tooManyRetries = false;
+ databaseError = false;
+ fullSyncRequested = false;
+ partialSyncUnavailable = false;
+ moreRecordsToGet = false;
+ stats.clear();
+ }
+
+ public static final Creator<SyncResult> CREATOR = new Creator<SyncResult>() {
+ public SyncResult createFromParcel(Parcel in) {
+ return new SyncResult(in);
+ }
+
+ public SyncResult[] newArray(int size) {
+ return new SyncResult[size];
+ }
+ };
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeInt(syncAlreadyInProgress ? 1 : 0);
+ parcel.writeInt(tooManyDeletions ? 1 : 0);
+ parcel.writeInt(tooManyRetries ? 1 : 0);
+ parcel.writeInt(databaseError ? 1 : 0);
+ parcel.writeInt(fullSyncRequested ? 1 : 0);
+ parcel.writeInt(partialSyncUnavailable ? 1 : 0);
+ parcel.writeInt(moreRecordsToGet ? 1 : 0);
+ stats.writeToParcel(parcel, flags);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(" syncAlreadyInProgress: ").append(syncAlreadyInProgress);
+ sb.append(" tooManyDeletions: ").append(tooManyDeletions);
+ sb.append(" tooManyRetries: ").append(tooManyRetries);
+ sb.append(" databaseError: ").append(databaseError);
+ sb.append(" fullSyncRequested: ").append(fullSyncRequested);
+ sb.append(" partialSyncUnavailable: ").append(partialSyncUnavailable);
+ sb.append(" moreRecordsToGet: ").append(moreRecordsToGet);
+ sb.append(" stats: ").append(stats);
+ return sb.toString();
+ }
+
+ /**
+ * Generates a debugging string indicating the status.
+ * The string consist of a sequence of code letter followed by the count.
+ * Code letters are f - fullSyncRequested, r - partialSyncUnavailable,
+ * X - hardError, e - numParseExceptions, c - numConflictDetectedExceptions,
+ * a - numAuthExceptions, D - tooManyDeletions, R - tooManyRetries,
+ * b - databaseError, x - softError, l - syncAlreadyInProgress,
+ * I - numIoExceptions
+ * @return debugging string.
+ */
+ public String toDebugString() {
+ StringBuffer sb = new StringBuffer();
+
+ if (fullSyncRequested) {
+ sb.append("f1");
+ }
+ if (partialSyncUnavailable) {
+ sb.append("r1");
+ }
+ if (hasHardError()) {
+ sb.append("X1");
+ }
+ if (stats.numParseExceptions > 0) {
+ sb.append("e").append(stats.numParseExceptions);
+ }
+ if (stats.numConflictDetectedExceptions > 0) {
+ sb.append("c").append(stats.numConflictDetectedExceptions);
+ }
+ if (stats.numAuthExceptions > 0) {
+ sb.append("a").append(stats.numAuthExceptions);
+ }
+ if (tooManyDeletions) {
+ sb.append("D1");
+ }
+ if (tooManyRetries) {
+ sb.append("R1");
+ }
+ if (databaseError) {
+ sb.append("b1");
+ }
+ if (hasSoftError()) {
+ sb.append("x1");
+ }
+ if (syncAlreadyInProgress) {
+ sb.append("l1");
+ }
+ if (stats.numIoExceptions > 0) {
+ sb.append("I").append(stats.numIoExceptions);
+ }
+ return sb.toString();
+ }
+}
diff --git a/core/java/android/content/SyncStateContentProviderHelper.java b/core/java/android/content/SyncStateContentProviderHelper.java
new file mode 100644
index 0000000..f503e6f
--- /dev/null
+++ b/core/java/android/content/SyncStateContentProviderHelper.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+import com.android.internal.util.ArrayUtils;
+
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.net.Uri;
+
+/**
+ * Extends the schema of a ContentProvider to include the _sync_state table
+ * and implements query/insert/update/delete to access that table using the
+ * authority "syncstate". This can be used to store the sync state for a
+ * set of accounts.
+ *
+ * @hide
+ */
+public class SyncStateContentProviderHelper {
+ final SQLiteOpenHelper mOpenHelper;
+
+ private static final String SYNC_STATE_AUTHORITY = "syncstate";
+ private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+
+ private static final int STATE = 0;
+
+ private static final Uri CONTENT_URI =
+ Uri.parse("content://" + SYNC_STATE_AUTHORITY + "/state");
+
+ private static final String ACCOUNT_WHERE = "_sync_account = ?";
+
+ private final Provider mInternalProviderInterface;
+
+ private static final String SYNC_STATE_TABLE = "_sync_state";
+ private static long DB_VERSION = 2;
+
+ private static final String[] ACCOUNT_PROJECTION = new String[]{"_sync_account"};
+
+ static {
+ sURIMatcher.addURI(SYNC_STATE_AUTHORITY, "state", STATE);
+ }
+
+ public SyncStateContentProviderHelper(SQLiteOpenHelper openHelper) {
+ mOpenHelper = openHelper;
+ mInternalProviderInterface = new Provider();
+ }
+
+ public ContentProvider asContentProvider() {
+ return mInternalProviderInterface;
+ }
+
+ public void createDatabase(SQLiteDatabase db) {
+ db.execSQL("DROP TABLE IF EXISTS _sync_state");
+ db.execSQL("CREATE TABLE _sync_state (" +
+ "_id INTEGER PRIMARY KEY," +
+ "_sync_account TEXT," +
+ "data TEXT," +
+ "UNIQUE(_sync_account)" +
+ ");");
+
+ db.execSQL("DROP TABLE IF EXISTS _sync_state_metadata");
+ db.execSQL("CREATE TABLE _sync_state_metadata (" +
+ "version INTEGER" +
+ ");");
+ ContentValues values = new ContentValues();
+ values.put("version", DB_VERSION);
+ db.insert("_sync_state_metadata", "version", values);
+ }
+
+ protected void onDatabaseOpened(SQLiteDatabase db) {
+ long version = DatabaseUtils.longForQuery(db,
+ "select version from _sync_state_metadata", null);
+ if (version != DB_VERSION) {
+ createDatabase(db);
+ }
+ }
+
+ class Provider extends ContentProvider {
+ public boolean onCreate() {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ public Cursor query(Uri url, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder) {
+ SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+ int match = sURIMatcher.match(url);
+ switch (match) {
+ case STATE:
+ return db.query(SYNC_STATE_TABLE, projection, selection, selectionArgs,
+ null, null, sortOrder);
+ default:
+ throw new UnsupportedOperationException("Cannot query URL: " + url);
+ }
+ }
+
+ public String getType(Uri uri) {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ public Uri insert(Uri url, ContentValues values) {
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ int match = sURIMatcher.match(url);
+ switch (match) {
+ case STATE: {
+ long id = db.insert(SYNC_STATE_TABLE, "feed", values);
+ return CONTENT_URI.buildUpon().appendPath(String.valueOf(id)).build();
+ }
+ default:
+ throw new UnsupportedOperationException("Cannot insert into URL: " + url);
+ }
+ }
+
+ public int delete(Uri url, String userWhere, String[] whereArgs) {
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ switch (sURIMatcher.match(url)) {
+ case STATE:
+ return db.delete(SYNC_STATE_TABLE, userWhere, whereArgs);
+ default:
+ throw new IllegalArgumentException("Unknown URL " + url);
+ }
+
+ }
+
+ public int update(Uri url, ContentValues values, String selection, String[] selectionArgs) {
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ switch (sURIMatcher.match(url)) {
+ case STATE:
+ return db.update(SYNC_STATE_TABLE, values, selection, selectionArgs);
+ default:
+ throw new UnsupportedOperationException("Cannot update URL: " + url);
+ }
+
+ }
+ }
+
+ /**
+ * Check if the url matches content that this ContentProvider manages.
+ * @param url the Uri to check
+ * @return true if this ContentProvider can handle that Uri.
+ */
+ public boolean matches(Uri url) {
+ return (SYNC_STATE_AUTHORITY.equals(url.getAuthority()));
+ }
+
+ /**
+ * Replaces the contents of the _sync_state table in the destination ContentProvider
+ * with the row that matches account, if any, in the source ContentProvider.
+ * <p>
+ * The ContentProviders must expose the _sync_state table as URI content://syncstate/state.
+ * @param dbSrc the database to read from
+ * @param dbDest the database to write to
+ * @param account the account of the row that should be copied over.
+ */
+ public void copySyncState(SQLiteDatabase dbSrc, SQLiteDatabase dbDest,
+ String account) {
+ final String[] whereArgs = new String[]{account};
+ Cursor c = dbSrc.query(SYNC_STATE_TABLE, new String[]{"_sync_account", "data"},
+ ACCOUNT_WHERE, whereArgs, null, null, null);
+ try {
+ if (c.moveToNext()) {
+ ContentValues values = new ContentValues();
+ values.put("_sync_account", c.getString(0));
+ values.put("data", c.getBlob(1));
+ dbDest.replace(SYNC_STATE_TABLE, "_sync_account", values);
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ public void onAccountsChanged(String[] accounts) {
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ Cursor c = db.query(SYNC_STATE_TABLE, ACCOUNT_PROJECTION, null, null, null, null, null);
+ try {
+ while (c.moveToNext()) {
+ final String account = c.getString(0);
+ if (!ArrayUtils.contains(accounts, account)) {
+ db.delete(SYNC_STATE_TABLE, ACCOUNT_WHERE, new String[]{account});
+ }
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ public void discardSyncData(SQLiteDatabase db, String account) {
+ if (account != null) {
+ db.delete(SYNC_STATE_TABLE, ACCOUNT_WHERE, new String[]{account});
+ } else {
+ db.delete(SYNC_STATE_TABLE, null, null);
+ }
+ }
+
+ /**
+ * Retrieves the SyncData bytes for the given account. The byte array returned may be null.
+ */
+ public byte[] readSyncDataBytes(SQLiteDatabase db, String account) {
+ Cursor c = db.query(SYNC_STATE_TABLE, null, ACCOUNT_WHERE,
+ new String[]{account}, null, null, null);
+ try {
+ if (c.moveToFirst()) {
+ return c.getBlob(c.getColumnIndexOrThrow("data"));
+ }
+ } finally {
+ c.close();
+ }
+ return null;
+ }
+
+ /**
+ * Sets the SyncData bytes for the given account. The bytes array may be null.
+ */
+ public void writeSyncDataBytes(SQLiteDatabase db, String account, byte[] data) {
+ ContentValues values = new ContentValues();
+ values.put("data", data);
+ db.update(SYNC_STATE_TABLE, values, ACCOUNT_WHERE, new String[]{account});
+ }
+}
diff --git a/core/java/android/content/SyncStats.aidl b/core/java/android/content/SyncStats.aidl
new file mode 100644
index 0000000..dff0ebf
--- /dev/null
+++ b/core/java/android/content/SyncStats.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+parcelable SyncStats;
diff --git a/core/java/android/content/SyncStats.java b/core/java/android/content/SyncStats.java
new file mode 100644
index 0000000..b561b05
--- /dev/null
+++ b/core/java/android/content/SyncStats.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+import android.os.Parcelable;
+import android.os.Parcel;
+
+/**
+ * @hide
+ */
+public class SyncStats implements Parcelable {
+ public long numAuthExceptions;
+ public long numIoExceptions;
+ public long numParseExceptions;
+ public long numConflictDetectedExceptions;
+ public long numInserts;
+ public long numUpdates;
+ public long numDeletes;
+ public long numEntries;
+ public long numSkippedEntries;
+
+ public SyncStats() {
+ numAuthExceptions = 0;
+ numIoExceptions = 0;
+ numParseExceptions = 0;
+ numConflictDetectedExceptions = 0;
+ numInserts = 0;
+ numUpdates = 0;
+ numDeletes = 0;
+ numEntries = 0;
+ numSkippedEntries = 0;
+ }
+
+ public SyncStats(Parcel in) {
+ numAuthExceptions = in.readLong();
+ numIoExceptions = in.readLong();
+ numParseExceptions = in.readLong();
+ numConflictDetectedExceptions = in.readLong();
+ numInserts = in.readLong();
+ numUpdates = in.readLong();
+ numDeletes = in.readLong();
+ numEntries = in.readLong();
+ numSkippedEntries = in.readLong();
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("numAuthExceptions: ").append(numAuthExceptions);
+ sb.append(" numIoExceptions: ").append(numIoExceptions);
+ sb.append(" numParseExceptions: ").append(numParseExceptions);
+ sb.append(" numConflictDetectedExceptions: ").append(numConflictDetectedExceptions);
+ sb.append(" numInserts: ").append(numInserts);
+ sb.append(" numUpdates: ").append(numUpdates);
+ sb.append(" numDeletes: ").append(numDeletes);
+ sb.append(" numEntries: ").append(numEntries);
+ sb.append(" numSkippedEntries: ").append(numSkippedEntries);
+ return sb.toString();
+ }
+
+ public void clear() {
+ numAuthExceptions = 0;
+ numIoExceptions = 0;
+ numParseExceptions = 0;
+ numConflictDetectedExceptions = 0;
+ numInserts = 0;
+ numUpdates = 0;
+ numDeletes = 0;
+ numEntries = 0;
+ numSkippedEntries = 0;
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeLong(numAuthExceptions);
+ dest.writeLong(numIoExceptions);
+ dest.writeLong(numParseExceptions);
+ dest.writeLong(numConflictDetectedExceptions);
+ dest.writeLong(numInserts);
+ dest.writeLong(numUpdates);
+ dest.writeLong(numDeletes);
+ dest.writeLong(numEntries);
+ dest.writeLong(numSkippedEntries);
+ }
+
+ public static final Creator<SyncStats> CREATOR = new Creator<SyncStats>() {
+ public SyncStats createFromParcel(Parcel in) {
+ return new SyncStats(in);
+ }
+
+ public SyncStats[] newArray(int size) {
+ return new SyncStats[size];
+ }
+ };
+}
diff --git a/core/java/android/content/SyncStorageEngine.java b/core/java/android/content/SyncStorageEngine.java
new file mode 100644
index 0000000..282f6e7
--- /dev/null
+++ b/core/java/android/content/SyncStorageEngine.java
@@ -0,0 +1,758 @@
+package android.content;
+
+import android.Manifest;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.net.Uri;
+import android.provider.Sync;
+import android.text.TextUtils;
+import android.util.Config;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+
+/**
+ * ContentProvider that tracks the sync data and overall sync
+ * history on the device.
+ *
+ * @hide
+ */
+public class SyncStorageEngine {
+ private static final String TAG = "SyncManager";
+
+ private static final String DATABASE_NAME = "syncmanager.db";
+ private static final int DATABASE_VERSION = 10;
+
+ private static final int STATS = 1;
+ private static final int STATS_ID = 2;
+ private static final int HISTORY = 3;
+ private static final int HISTORY_ID = 4;
+ private static final int SETTINGS = 5;
+ private static final int PENDING = 7;
+ private static final int ACTIVE = 8;
+ private static final int STATUS = 9;
+
+ private static final UriMatcher sURLMatcher =
+ new UriMatcher(UriMatcher.NO_MATCH);
+
+ private static final HashMap<String,String> HISTORY_PROJECTION_MAP;
+ private static final HashMap<String,String> PENDING_PROJECTION_MAP;
+ private static final HashMap<String,String> ACTIVE_PROJECTION_MAP;
+ private static final HashMap<String,String> STATUS_PROJECTION_MAP;
+
+ private final Context mContext;
+ private final SQLiteOpenHelper mOpenHelper;
+ private static SyncStorageEngine sSyncStorageEngine = null;
+
+ static {
+ sURLMatcher.addURI("sync", "stats", STATS);
+ sURLMatcher.addURI("sync", "stats/#", STATS_ID);
+ sURLMatcher.addURI("sync", "history", HISTORY);
+ sURLMatcher.addURI("sync", "history/#", HISTORY_ID);
+ sURLMatcher.addURI("sync", "settings", SETTINGS);
+ sURLMatcher.addURI("sync", "status", STATUS);
+ sURLMatcher.addURI("sync", "active", ACTIVE);
+ sURLMatcher.addURI("sync", "pending", PENDING);
+
+ HashMap<String,String> map;
+ PENDING_PROJECTION_MAP = map = new HashMap<String,String>();
+ map.put(Sync.History._ID, Sync.History._ID);
+ map.put(Sync.History.ACCOUNT, Sync.History.ACCOUNT);
+ map.put(Sync.History.AUTHORITY, Sync.History.AUTHORITY);
+
+ ACTIVE_PROJECTION_MAP = map = new HashMap<String,String>();
+ map.put(Sync.History._ID, Sync.History._ID);
+ map.put(Sync.History.ACCOUNT, Sync.History.ACCOUNT);
+ map.put(Sync.History.AUTHORITY, Sync.History.AUTHORITY);
+ map.put("startTime", "startTime");
+
+ HISTORY_PROJECTION_MAP = map = new HashMap<String,String>();
+ map.put(Sync.History._ID, "history._id as _id");
+ map.put(Sync.History.ACCOUNT, "stats.account as account");
+ map.put(Sync.History.AUTHORITY, "stats.authority as authority");
+ map.put(Sync.History.EVENT, Sync.History.EVENT);
+ map.put(Sync.History.EVENT_TIME, Sync.History.EVENT_TIME);
+ map.put(Sync.History.ELAPSED_TIME, Sync.History.ELAPSED_TIME);
+ map.put(Sync.History.SOURCE, Sync.History.SOURCE);
+ map.put(Sync.History.UPSTREAM_ACTIVITY, Sync.History.UPSTREAM_ACTIVITY);
+ map.put(Sync.History.DOWNSTREAM_ACTIVITY, Sync.History.DOWNSTREAM_ACTIVITY);
+ map.put(Sync.History.MESG, Sync.History.MESG);
+
+ STATUS_PROJECTION_MAP = map = new HashMap<String,String>();
+ map.put(Sync.Status._ID, "status._id as _id");
+ map.put(Sync.Status.ACCOUNT, "stats.account as account");
+ map.put(Sync.Status.AUTHORITY, "stats.authority as authority");
+ map.put(Sync.Status.TOTAL_ELAPSED_TIME, Sync.Status.TOTAL_ELAPSED_TIME);
+ map.put(Sync.Status.NUM_SYNCS, Sync.Status.NUM_SYNCS);
+ map.put(Sync.Status.NUM_SOURCE_LOCAL, Sync.Status.NUM_SOURCE_LOCAL);
+ map.put(Sync.Status.NUM_SOURCE_POLL, Sync.Status.NUM_SOURCE_POLL);
+ map.put(Sync.Status.NUM_SOURCE_SERVER, Sync.Status.NUM_SOURCE_SERVER);
+ map.put(Sync.Status.NUM_SOURCE_USER, Sync.Status.NUM_SOURCE_USER);
+ map.put(Sync.Status.LAST_SUCCESS_SOURCE, Sync.Status.LAST_SUCCESS_SOURCE);
+ map.put(Sync.Status.LAST_SUCCESS_TIME, Sync.Status.LAST_SUCCESS_TIME);
+ map.put(Sync.Status.LAST_FAILURE_SOURCE, Sync.Status.LAST_FAILURE_SOURCE);
+ map.put(Sync.Status.LAST_FAILURE_TIME, Sync.Status.LAST_FAILURE_TIME);
+ map.put(Sync.Status.LAST_FAILURE_MESG, Sync.Status.LAST_FAILURE_MESG);
+ map.put(Sync.Status.PENDING, Sync.Status.PENDING);
+ }
+
+ private static final String[] STATS_ACCOUNT_PROJECTION =
+ new String[] { Sync.Stats.ACCOUNT };
+
+ private static final int MAX_HISTORY_EVENTS_TO_KEEP = 5000;
+
+ private static final String SELECT_INITIAL_FAILURE_TIME_QUERY_STRING = ""
+ + "SELECT min(a) "
+ + "FROM ("
+ + " SELECT initialFailureTime AS a "
+ + " FROM status "
+ + " WHERE stats_id=? AND a IS NOT NULL "
+ + " UNION "
+ + " SELECT ? AS a"
+ + " )";
+
+ private SyncStorageEngine(Context context) {
+ mContext = context;
+ mOpenHelper = new SyncStorageEngine.DatabaseHelper(context);
+ sSyncStorageEngine = this;
+ }
+
+ public static SyncStorageEngine newTestInstance(Context context) {
+ return new SyncStorageEngine(context);
+ }
+
+ public static void init(Context context) {
+ if (sSyncStorageEngine != null) {
+ throw new IllegalStateException("already initialized");
+ }
+ sSyncStorageEngine = new SyncStorageEngine(context);
+ }
+
+ public static SyncStorageEngine getSingleton() {
+ if (sSyncStorageEngine == null) {
+ throw new IllegalStateException("not initialized");
+ }
+ return sSyncStorageEngine;
+ }
+
+ private class DatabaseHelper extends SQLiteOpenHelper {
+ DatabaseHelper(Context context) {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ db.execSQL("CREATE TABLE pending ("
+ + "_id INTEGER PRIMARY KEY,"
+ + "authority TEXT NOT NULL,"
+ + "account TEXT NOT NULL,"
+ + "extras BLOB NOT NULL,"
+ + "source INTEGER NOT NULL"
+ + ");");
+
+ db.execSQL("CREATE TABLE stats (" +
+ "_id INTEGER PRIMARY KEY," +
+ "account TEXT, " +
+ "authority TEXT, " +
+ "syncdata TEXT, " +
+ "UNIQUE (account, authority)" +
+ ");");
+
+ db.execSQL("CREATE TABLE history (" +
+ "_id INTEGER PRIMARY KEY," +
+ "stats_id INTEGER," +
+ "eventTime INTEGER," +
+ "elapsedTime INTEGER," +
+ "source INTEGER," +
+ "event INTEGER," +
+ "upstreamActivity INTEGER," +
+ "downstreamActivity INTEGER," +
+ "mesg TEXT);");
+
+ db.execSQL("CREATE TABLE status ("
+ + "_id INTEGER PRIMARY KEY,"
+ + "stats_id INTEGER NOT NULL,"
+ + "totalElapsedTime INTEGER NOT NULL DEFAULT 0,"
+ + "numSyncs INTEGER NOT NULL DEFAULT 0,"
+ + "numSourcePoll INTEGER NOT NULL DEFAULT 0,"
+ + "numSourceServer INTEGER NOT NULL DEFAULT 0,"
+ + "numSourceLocal INTEGER NOT NULL DEFAULT 0,"
+ + "numSourceUser INTEGER NOT NULL DEFAULT 0,"
+ + "lastSuccessTime INTEGER,"
+ + "lastSuccessSource INTEGER,"
+ + "lastFailureTime INTEGER,"
+ + "lastFailureSource INTEGER,"
+ + "lastFailureMesg STRING,"
+ + "initialFailureTime INTEGER,"
+ + "pending INTEGER NOT NULL DEFAULT 0);");
+
+ db.execSQL("CREATE TABLE active ("
+ + "_id INTEGER PRIMARY KEY,"
+ + "authority TEXT,"
+ + "account TEXT,"
+ + "startTime INTEGER);");
+
+ db.execSQL("CREATE INDEX historyEventTime ON history (eventTime)");
+
+ db.execSQL("CREATE TABLE settings (" +
+ "name TEXT PRIMARY KEY," +
+ "value TEXT);");
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ if (oldVersion == 9 && newVersion == 10) {
+ Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
+ + newVersion + ", which will preserve old data");
+ db.execSQL("ALTER TABLE status ADD COLUMN initialFailureTime INTEGER");
+ return;
+ }
+
+ Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
+ + newVersion + ", which will destroy all old data");
+ db.execSQL("DROP TABLE IF EXISTS pending");
+ db.execSQL("DROP TABLE IF EXISTS stats");
+ db.execSQL("DROP TABLE IF EXISTS history");
+ db.execSQL("DROP TABLE IF EXISTS settings");
+ db.execSQL("DROP TABLE IF EXISTS active");
+ db.execSQL("DROP TABLE IF EXISTS status");
+ onCreate(db);
+ }
+
+ @Override
+ public void onOpen(SQLiteDatabase db) {
+ if (!db.isReadOnly()) {
+ db.delete("active", null, null);
+ db.insert("active", "account", null);
+ }
+ }
+ }
+
+ protected void doDatabaseCleanup(String[] accounts) {
+ HashSet<String> currentAccounts = new HashSet<String>();
+ for (String account : accounts) currentAccounts.add(account);
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ Cursor cursor = db.query("stats", STATS_ACCOUNT_PROJECTION,
+ null /* where */, null /* where args */, Sync.Stats.ACCOUNT,
+ null /* having */, null /* order by */);
+ try {
+ while (cursor.moveToNext()) {
+ String account = cursor.getString(0);
+ if (TextUtils.isEmpty(account)) {
+ continue;
+ }
+ if (!currentAccounts.contains(account)) {
+ String where = Sync.Stats.ACCOUNT + "=?";
+ int numDeleted;
+ numDeleted = db.delete("stats", where, new String[]{account});
+ if (Config.LOGD) {
+ Log.d(TAG, "deleted " + numDeleted
+ + " records from stats table"
+ + " for account " + account);
+ }
+ }
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ protected void setActiveSync(SyncManager.ActiveSyncContext activeSyncContext) {
+ if (activeSyncContext != null) {
+ updateActiveSync(activeSyncContext.mSyncOperation.account,
+ activeSyncContext.mSyncOperation.authority, activeSyncContext.mStartTime);
+ } else {
+ // we indicate that the sync is not active by passing null for all the parameters
+ updateActiveSync(null, null, null);
+ }
+ }
+
+ private int updateActiveSync(String account, String authority, Long startTime) {
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ ContentValues values = new ContentValues();
+ values.put("account", account);
+ values.put("authority", authority);
+ values.put("startTime", startTime);
+ int numChanges = db.update("active", values, null, null);
+ if (numChanges > 0) {
+ mContext.getContentResolver().notifyChange(Sync.Active.CONTENT_URI,
+ null /* this change wasn't made through an observer */);
+ }
+ return numChanges;
+ }
+
+ /**
+ * Implements the {@link ContentProvider#query} method
+ */
+ public Cursor query(Uri url, String[] projectionIn,
+ String selection, String[] selectionArgs, String sort) {
+ SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+
+ // Generate the body of the query
+ int match = sURLMatcher.match(url);
+ String groupBy = null;
+ switch (match) {
+ case STATS:
+ qb.setTables("stats");
+ break;
+ case STATS_ID:
+ qb.setTables("stats");
+ qb.appendWhere("_id=");
+ qb.appendWhere(url.getPathSegments().get(1));
+ break;
+ case HISTORY:
+ // join the stats and history tables, so the caller can get
+ // the account and authority information as part of this query.
+ qb.setTables("stats, history");
+ qb.setProjectionMap(HISTORY_PROJECTION_MAP);
+ qb.appendWhere("stats._id = history.stats_id");
+ break;
+ case ACTIVE:
+ qb.setTables("active");
+ qb.setProjectionMap(ACTIVE_PROJECTION_MAP);
+ qb.appendWhere("account is not null");
+ break;
+ case PENDING:
+ qb.setTables("pending");
+ qb.setProjectionMap(PENDING_PROJECTION_MAP);
+ groupBy = "account, authority";
+ break;
+ case STATUS:
+ // join the stats and status tables, so the caller can get
+ // the account and authority information as part of this query.
+ qb.setTables("stats, status");
+ qb.setProjectionMap(STATUS_PROJECTION_MAP);
+ qb.appendWhere("stats._id = status.stats_id");
+ break;
+ case HISTORY_ID:
+ // join the stats and history tables, so the caller can get
+ // the account and authority information as part of this query.
+ qb.setTables("stats, history");
+ qb.setProjectionMap(HISTORY_PROJECTION_MAP);
+ qb.appendWhere("stats._id = history.stats_id");
+ qb.appendWhere("AND history._id=");
+ qb.appendWhere(url.getPathSegments().get(1));
+ break;
+ case SETTINGS:
+ qb.setTables("settings");
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown URL " + url);
+ }
+
+ if (match == SETTINGS) {
+ mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_SETTINGS,
+ "no permission to read the sync settings");
+ } else {
+ mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_STATS,
+ "no permission to read the sync stats");
+ }
+ SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+ Cursor c = qb.query(db, projectionIn, selection, selectionArgs, groupBy, null, sort);
+ c.setNotificationUri(mContext.getContentResolver(), url);
+ return c;
+ }
+
+ /**
+ * Implements the {@link ContentProvider#insert} method
+ * @param callerIsTheProvider true if this is being called via the
+ * {@link ContentProvider#insert} in method rather than directly.
+ * @throws UnsupportedOperationException if callerIsTheProvider is true and the url isn't
+ * for the Settings table.
+ */
+ public Uri insert(boolean callerIsTheProvider, Uri url, ContentValues values) {
+ String table;
+ long rowID;
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ final int match = sURLMatcher.match(url);
+ checkCaller(callerIsTheProvider, match);
+ switch (match) {
+ case SETTINGS:
+ mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_SYNC_SETTINGS,
+ "no permission to write the sync settings");
+ table = "settings";
+ rowID = db.replace(table, null, values);
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown URL " + url);
+ }
+
+
+ if (rowID > 0) {
+ mContext.getContentResolver().notifyChange(url, null /* observer */);
+ return Uri.parse("content://sync/" + table + "/" + rowID);
+ }
+
+ return null;
+ }
+
+ private static void checkCaller(boolean callerIsTheProvider, int match) {
+ if (callerIsTheProvider && match != SETTINGS) {
+ throw new UnsupportedOperationException(
+ "only the settings are modifiable via the ContentProvider interface");
+ }
+ }
+
+ /**
+ * Implements the {@link ContentProvider#delete} method
+ * @param callerIsTheProvider true if this is being called via the
+ * {@link ContentProvider#delete} in method rather than directly.
+ * @throws UnsupportedOperationException if callerIsTheProvider is true and the url isn't
+ * for the Settings table.
+ */
+ public int delete(boolean callerIsTheProvider, Uri url, String where, String[] whereArgs) {
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ int match = sURLMatcher.match(url);
+
+ int numRows;
+ switch (match) {
+ case SETTINGS:
+ mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_SYNC_SETTINGS,
+ "no permission to write the sync settings");
+ numRows = db.delete("settings", where, whereArgs);
+ break;
+ default:
+ throw new UnsupportedOperationException("Cannot delete URL: " + url);
+ }
+
+ if (numRows > 0) {
+ mContext.getContentResolver().notifyChange(url, null /* observer */);
+ }
+ return numRows;
+ }
+
+ /**
+ * Implements the {@link ContentProvider#update} method
+ * @param callerIsTheProvider true if this is being called via the
+ * {@link ContentProvider#update} in method rather than directly.
+ * @throws UnsupportedOperationException if callerIsTheProvider is true and the url isn't
+ * for the Settings table.
+ */
+ public int update(boolean callerIsTheProvider, Uri url, ContentValues initialValues,
+ String where, String[] whereArgs) {
+ switch (sURLMatcher.match(url)) {
+ case SETTINGS:
+ throw new UnsupportedOperationException("updating url " + url
+ + " is not allowed, use insert instead");
+ default:
+ throw new UnsupportedOperationException("Cannot update URL: " + url);
+ }
+ }
+
+ /**
+ * Implements the {@link ContentProvider#getType} method
+ */
+ public String getType(Uri url) {
+ int match = sURLMatcher.match(url);
+ switch (match) {
+ case SETTINGS:
+ return "vnd.android.cursor.dir/sync-settings";
+ default:
+ throw new IllegalArgumentException("Unknown URL");
+ }
+ }
+
+ protected Uri insertIntoPending(ContentValues values) {
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ try {
+ db.beginTransaction();
+ long rowId = db.insert("pending", Sync.Pending.ACCOUNT, values);
+ if (rowId < 0) return null;
+ String account = values.getAsString(Sync.Pending.ACCOUNT);
+ String authority = values.getAsString(Sync.Pending.AUTHORITY);
+
+ long statsId = createStatsRowIfNecessary(account, authority);
+ createStatusRowIfNecessary(statsId);
+
+ values.clear();
+ values.put(Sync.Status.PENDING, 1);
+ int numUpdatesStatus = db.update("status", values, "stats_id=" + statsId, null);
+
+ db.setTransactionSuccessful();
+
+ mContext.getContentResolver().notifyChange(Sync.Pending.CONTENT_URI,
+ null /* no observer initiated this change */);
+ if (numUpdatesStatus > 0) {
+ mContext.getContentResolver().notifyChange(Sync.Status.CONTENT_URI,
+ null /* no observer initiated this change */);
+ }
+ return ContentUris.withAppendedId(Sync.Pending.CONTENT_URI, rowId);
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ int deleteFromPending(long rowId) {
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ db.beginTransaction();
+ try {
+ String account;
+ String authority;
+ Cursor c = db.query("pending",
+ new String[]{Sync.Pending.ACCOUNT, Sync.Pending.AUTHORITY},
+ "_id=" + rowId, null, null, null, null);
+ try {
+ if (c.getCount() != 1) {
+ return 0;
+ }
+ c.moveToNext();
+ account = c.getString(0);
+ authority = c.getString(1);
+ } finally {
+ c.close();
+ }
+ db.delete("pending", "_id=" + rowId, null /* no where args */);
+ final String[] accountAuthorityWhereArgs = new String[]{account, authority};
+ boolean isPending = 0 < DatabaseUtils.longForQuery(db,
+ "SELECT COUNT(*) FROM PENDING WHERE account=? AND authority=?",
+ accountAuthorityWhereArgs);
+ if (!isPending) {
+ long statsId = createStatsRowIfNecessary(account, authority);
+ db.execSQL("UPDATE status SET pending=0 WHERE stats_id=" + statsId);
+ }
+ db.setTransactionSuccessful();
+
+ mContext.getContentResolver().notifyChange(Sync.Pending.CONTENT_URI,
+ null /* no observer initiated this change */);
+ if (!isPending) {
+ mContext.getContentResolver().notifyChange(Sync.Status.CONTENT_URI,
+ null /* no observer initiated this change */);
+ }
+ return 1;
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ int clearPending() {
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ db.beginTransaction();
+ try {
+ int numChanges = db.delete("pending", null, null /* no where args */);
+ if (numChanges > 0) {
+ db.execSQL("UPDATE status SET pending=0");
+ mContext.getContentResolver().notifyChange(Sync.Pending.CONTENT_URI,
+ null /* no observer initiated this change */);
+ mContext.getContentResolver().notifyChange(Sync.Status.CONTENT_URI,
+ null /* no observer initiated this change */);
+ }
+ db.setTransactionSuccessful();
+ return numChanges;
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ /**
+ * Returns a cursor over all the pending syncs in no particular order. This cursor is not
+ * "live", in that if changes are made to the pending table any observers on this cursor
+ * will not be notified.
+ * @param projection Return only these columns. If null then all columns are returned.
+ * @return the cursor of pending syncs
+ */
+ public Cursor getPendingSyncsCursor(String[] projection) {
+ SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+ return db.query("pending", projection, null, null, null, null, null);
+ }
+
+ // @VisibleForTesting
+ static final long MILLIS_IN_4WEEKS = 1000L * 60 * 60 * 24 * 7 * 4;
+
+ private boolean purgeOldHistoryEvents(long now) {
+ // remove events that are older than MILLIS_IN_4WEEKS
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ int numDeletes = db.delete("history", "eventTime<" + (now - MILLIS_IN_4WEEKS), null);
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ if (numDeletes > 0) {
+ Log.v(TAG, "deleted " + numDeletes + " old event(s) from the sync history");
+ }
+ }
+
+ // keep only the last MAX_HISTORY_EVENTS_TO_KEEP history events
+ numDeletes += db.delete("history", "eventTime < (select min(eventTime) from "
+ + "(select eventTime from history order by eventTime desc limit ?))",
+ new String[]{String.valueOf(MAX_HISTORY_EVENTS_TO_KEEP)});
+
+ return numDeletes > 0;
+ }
+
+ public long insertStartSyncEvent(String account, String authority, long now, int source) {
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ long statsId = createStatsRowIfNecessary(account, authority);
+
+ purgeOldHistoryEvents(now);
+ ContentValues values = new ContentValues();
+ values.put(Sync.History.STATS_ID, statsId);
+ values.put(Sync.History.EVENT_TIME, now);
+ values.put(Sync.History.SOURCE, source);
+ values.put(Sync.History.EVENT, Sync.History.EVENT_START);
+ long rowId = db.insert("history", null, values);
+ mContext.getContentResolver().notifyChange(Sync.History.CONTENT_URI, null /* observer */);
+ mContext.getContentResolver().notifyChange(Sync.Status.CONTENT_URI, null /* observer */);
+ return rowId;
+ }
+
+ public void stopSyncEvent(long historyId, long elapsedTime, String resultMessage,
+ long downstreamActivity, long upstreamActivity) {
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ db.beginTransaction();
+ try {
+ ContentValues values = new ContentValues();
+ values.put(Sync.History.ELAPSED_TIME, elapsedTime);
+ values.put(Sync.History.EVENT, Sync.History.EVENT_STOP);
+ values.put(Sync.History.MESG, resultMessage);
+ values.put(Sync.History.DOWNSTREAM_ACTIVITY, downstreamActivity);
+ values.put(Sync.History.UPSTREAM_ACTIVITY, upstreamActivity);
+
+ int count = db.update("history", values, "_id=?",
+ new String[]{Long.toString(historyId)});
+ // We think that count should always be 1 but don't want to change this until after
+ // launch.
+ if (count > 0) {
+ int source = (int) DatabaseUtils.longForQuery(db,
+ "SELECT source FROM history WHERE _id=" + historyId, null);
+ long eventTime = DatabaseUtils.longForQuery(db,
+ "SELECT eventTime FROM history WHERE _id=" + historyId, null);
+ long statsId = DatabaseUtils.longForQuery(db,
+ "SELECT stats_id FROM history WHERE _id=" + historyId, null);
+
+ createStatusRowIfNecessary(statsId);
+
+ // update the status table to reflect this sync
+ StringBuilder sb = new StringBuilder();
+ ArrayList<String> bindArgs = new ArrayList<String>();
+ sb.append("UPDATE status SET");
+ sb.append(" numSyncs=numSyncs+1");
+ sb.append(", totalElapsedTime=totalElapsedTime+" + elapsedTime);
+ switch (source) {
+ case Sync.History.SOURCE_LOCAL:
+ sb.append(", numSourceLocal=numSourceLocal+1");
+ break;
+ case Sync.History.SOURCE_POLL:
+ sb.append(", numSourcePoll=numSourcePoll+1");
+ break;
+ case Sync.History.SOURCE_USER:
+ sb.append(", numSourceUser=numSourceUser+1");
+ break;
+ case Sync.History.SOURCE_SERVER:
+ sb.append(", numSourceServer=numSourceServer+1");
+ break;
+ }
+
+ final String statsIdString = String.valueOf(statsId);
+ final long lastSyncTime = (eventTime + elapsedTime);
+ if (Sync.History.MESG_SUCCESS.equals(resultMessage)) {
+ // - if successful, update the successful columns
+ sb.append(", lastSuccessTime=" + lastSyncTime);
+ sb.append(", lastSuccessSource=" + source);
+ sb.append(", lastFailureTime=null");
+ sb.append(", lastFailureSource=null");
+ sb.append(", lastFailureMesg=null");
+ sb.append(", initialFailureTime=null");
+ } else if (!Sync.History.MESG_CANCELED.equals(resultMessage)) {
+ sb.append(", lastFailureTime=" + lastSyncTime);
+ sb.append(", lastFailureSource=" + source);
+ sb.append(", lastFailureMesg=?");
+ bindArgs.add(resultMessage);
+ long initialFailureTime = DatabaseUtils.longForQuery(db,
+ SELECT_INITIAL_FAILURE_TIME_QUERY_STRING,
+ new String[]{statsIdString, String.valueOf(lastSyncTime)});
+ sb.append(", initialFailureTime=" + initialFailureTime);
+ }
+ sb.append(" WHERE stats_id=?");
+ bindArgs.add(statsIdString);
+ db.execSQL(sb.toString(), bindArgs.toArray());
+ db.setTransactionSuccessful();
+ mContext.getContentResolver().notifyChange(Sync.History.CONTENT_URI,
+ null /* observer */);
+ mContext.getContentResolver().notifyChange(Sync.Status.CONTENT_URI,
+ null /* observer */);
+ }
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ /**
+ * If sync is failing for any of the provider/accounts then determine the time at which it
+ * started failing and return the earliest time over all the provider/accounts. If none are
+ * failing then return 0.
+ */
+ public long getInitialSyncFailureTime() {
+ SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+ // Join the settings for a provider with the status so that we can easily
+ // check if each provider is enabled for syncing. We also join in the overall
+ // enabled flag ("listen_for_tickles") to each row so that we don't need to
+ // make a separate DB lookup to access it.
+ Cursor c = db.rawQuery(""
+ + "SELECT initialFailureTime, s1.value, s2.value "
+ + "FROM status "
+ + "LEFT JOIN stats ON status.stats_id=stats._id "
+ + "LEFT JOIN settings as s1 ON 'sync_provider_' || authority=s1.name "
+ + "LEFT JOIN settings as s2 ON s2.name='listen_for_tickles' "
+ + "where initialFailureTime is not null "
+ + " AND lastFailureMesg!=" + Sync.History.ERROR_TOO_MANY_DELETIONS
+ + " AND lastFailureMesg!=" + Sync.History.ERROR_AUTHENTICATION
+ + " AND lastFailureMesg!=" + Sync.History.ERROR_SYNC_ALREADY_IN_PROGRESS
+ + " AND authority!='subscribedfeeds' "
+ + " ORDER BY initialFailureTime", null);
+ try {
+ while (c.moveToNext()) {
+ // these settings default to true, so if they are null treat them as enabled
+ final String providerEnabledString = c.getString(1);
+ if (providerEnabledString != null && !Boolean.parseBoolean(providerEnabledString)) {
+ continue;
+ }
+ final String allEnabledString = c.getString(2);
+ if (allEnabledString != null && !Boolean.parseBoolean(allEnabledString)) {
+ continue;
+ }
+ return c.getLong(0);
+ }
+ } finally {
+ c.close();
+ }
+ return 0;
+ }
+
+ private void createStatusRowIfNecessary(long statsId) {
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ boolean statusExists = 0 != DatabaseUtils.longForQuery(db,
+ "SELECT count(*) FROM status WHERE stats_id=" + statsId, null);
+ if (!statusExists) {
+ ContentValues values = new ContentValues();
+ values.put("stats_id", statsId);
+ db.insert("status", null, values);
+ }
+ }
+
+ private long createStatsRowIfNecessary(String account, String authority) {
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ StringBuilder where = new StringBuilder();
+ where.append(Sync.Stats.ACCOUNT + "= ?");
+ where.append(" and " + Sync.Stats.AUTHORITY + "= ?");
+ Cursor cursor = query(Sync.Stats.CONTENT_URI,
+ Sync.Stats.SYNC_STATS_PROJECTION,
+ where.toString(), new String[] { account, authority },
+ null /* order */);
+ try {
+ long id;
+ if (cursor.moveToFirst()) {
+ id = cursor.getLong(cursor.getColumnIndexOrThrow(Sync.Stats._ID));
+ } else {
+ ContentValues values = new ContentValues();
+ values.put(Sync.Stats.ACCOUNT, account);
+ values.put(Sync.Stats.AUTHORITY, authority);
+ id = db.insert("stats", null, values);
+ }
+ return id;
+ } finally {
+ cursor.close();
+ }
+ }
+}
diff --git a/core/java/android/content/SyncUIContext.java b/core/java/android/content/SyncUIContext.java
new file mode 100644
index 0000000..6dde004
--- /dev/null
+++ b/core/java/android/content/SyncUIContext.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+/**
+ * Class with callback methods for SyncAdapters and ContentProviders
+ * that are called in response to the calls on SyncContext. This class
+ * is really only meant to be used by the Sync UI activities.
+ *
+ * <p>All of the onXXX callback methods here are called from a handler
+ * on the thread this object was created in.
+ *
+ * <p>This interface is unused. It should be removed.
+ *
+ * @hide
+ */
+@Deprecated
+public interface SyncUIContext {
+
+ void setStatusText(String text);
+
+ Context context();
+}
diff --git a/core/java/android/content/SyncableContentProvider.java b/core/java/android/content/SyncableContentProvider.java
new file mode 100644
index 0000000..1e55e27
--- /dev/null
+++ b/core/java/android/content/SyncableContentProvider.java
@@ -0,0 +1,620 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+import android.accounts.AccountMonitor;
+import android.accounts.AccountMonitorListener;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.net.Uri;
+import android.provider.SyncConstValue;
+import android.text.TextUtils;
+import android.util.Config;
+import android.util.Log;
+import android.os.Bundle;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Vector;
+
+/**
+ * A specialization of the ContentProvider that centralizes functionality
+ * used by ContentProviders that are syncable. It also wraps calls to the ContentProvider
+ * inside of database transactions.
+ *
+ * @hide
+ */
+public abstract class SyncableContentProvider extends ContentProvider {
+ private static final String TAG = "SyncableContentProvider";
+ protected SQLiteOpenHelper mOpenHelper;
+ protected SQLiteDatabase mDb;
+ private final String mDatabaseName;
+ private final int mDatabaseVersion;
+ private final Uri mContentUri;
+ private AccountMonitor mAccountMonitor;
+
+ /** the account set in the last call to onSyncStart() */
+ private String mSyncingAccount;
+
+ private SyncStateContentProviderHelper mSyncState = null;
+
+ private static final String[] sAccountProjection = new String[] {SyncConstValue._SYNC_ACCOUNT};
+
+ private boolean mIsTemporary;
+
+ private AbstractTableMerger mCurrentMerger = null;
+ private boolean mIsMergeCancelled = false;
+
+ private static final String SYNC_ACCOUNT_WHERE_CLAUSE = SyncConstValue._SYNC_ACCOUNT + "=?";
+
+ protected boolean isTemporary() {
+ return mIsTemporary;
+ }
+
+ /**
+ * Indicates whether or not this ContentProvider contains a full
+ * set of data or just diffs. This knowledge comes in handy when
+ * determining how to incorporate the contents of a temporary
+ * provider into a real provider.
+ */
+ private boolean mContainsDiffs;
+
+ /**
+ * Initializes the SyncableContentProvider
+ * @param dbName the filename of the database
+ * @param dbVersion the current version of the database schema
+ * @param contentUri The base Uri of the syncable content in this provider
+ */
+ public SyncableContentProvider(String dbName, int dbVersion, Uri contentUri) {
+ super();
+
+ mDatabaseName = dbName;
+ mDatabaseVersion = dbVersion;
+ mContentUri = contentUri;
+ mIsTemporary = false;
+ setContainsDiffs(false);
+ if (Config.LOGV) {
+ Log.v(TAG, "created SyncableContentProvider " + this);
+ }
+ }
+
+ /**
+ * Close resources that must be closed. You must call this to properly release
+ * the resources used by the SyncableContentProvider.
+ */
+ public void close() {
+ if (mOpenHelper != null) {
+ mOpenHelper.close(); // OK to call .close() repeatedly.
+ }
+ }
+
+ /**
+ * Override to create your schema and do anything else you need to do with a new database.
+ * This is run inside a transaction (so you don't need to use one).
+ * This method may not use getDatabase(), or call content provider methods, it must only
+ * use the database handle passed to it.
+ */
+ protected void bootstrapDatabase(SQLiteDatabase db) {}
+
+ /**
+ * Override to upgrade your database from an old version to the version you specified.
+ * Don't set the DB version, this will automatically be done after the method returns.
+ * This method may not use getDatabase(), or call content provider methods, it must only
+ * use the database handle passed to it.
+ *
+ * @param oldVersion version of the existing database
+ * @param newVersion current version to upgrade to
+ * @return true if the upgrade was lossless, false if it was lossy
+ */
+ protected abstract boolean upgradeDatabase(SQLiteDatabase db, int oldVersion, int newVersion);
+
+ /**
+ * Override to do anything (like cleanups or checks) you need to do after opening a database.
+ * Does nothing by default. This is run inside a transaction (so you don't need to use one).
+ * This method may not use getDatabase(), or call content provider methods, it must only
+ * use the database handle passed to it.
+ */
+ protected void onDatabaseOpened(SQLiteDatabase db) {}
+
+ private class DatabaseHelper extends SQLiteOpenHelper {
+ DatabaseHelper(Context context, String name) {
+ // Note: context and name may be null for temp providers
+ super(context, name, null, mDatabaseVersion);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ bootstrapDatabase(db);
+ mSyncState.createDatabase(db);
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ if (!upgradeDatabase(db, oldVersion, newVersion)) {
+ mSyncState.discardSyncData(db, null /* all accounts */);
+ getContext().getContentResolver().startSync(mContentUri, new Bundle());
+ }
+ }
+
+ @Override
+ public void onOpen(SQLiteDatabase db) {
+ onDatabaseOpened(db);
+ mSyncState.onDatabaseOpened(db);
+ }
+ }
+
+ @Override
+ public boolean onCreate() {
+ if (isTemporary()) throw new IllegalStateException("onCreate() called for temp provider");
+ mOpenHelper = new DatabaseHelper(getContext(), mDatabaseName);
+ mSyncState = new SyncStateContentProviderHelper(mOpenHelper);
+
+ AccountMonitorListener listener = new AccountMonitorListener() {
+ public void onAccountsUpdated(String[] accounts) {
+ // Some providers override onAccountsChanged(); give them a database to work with.
+ mDb = mOpenHelper.getWritableDatabase();
+ onAccountsChanged(accounts);
+ TempProviderSyncAdapter syncAdapter = (TempProviderSyncAdapter)getSyncAdapter();
+ if (syncAdapter != null) {
+ syncAdapter.onAccountsChanged(accounts);
+ }
+ }
+ };
+ mAccountMonitor = new AccountMonitor(getContext(), listener);
+
+ return true;
+ }
+
+ /**
+ * Get a non-persistent instance of this content provider.
+ * You must call {@link #close} on the returned
+ * SyncableContentProvider when you are done with it.
+ *
+ * @return a non-persistent content provider with the same layout as this
+ * provider.
+ */
+ public SyncableContentProvider getTemporaryInstance() {
+ SyncableContentProvider temp;
+ try {
+ temp = getClass().newInstance();
+ } catch (InstantiationException e) {
+ throw new RuntimeException("unable to instantiate class, "
+ + "this should never happen", e);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(
+ "IllegalAccess while instantiating class, "
+ + "this should never happen", e);
+ }
+
+ // Note: onCreate() isn't run for the temp provider, and it has no Context.
+ temp.mIsTemporary = true;
+ temp.setContainsDiffs(true);
+ temp.mOpenHelper = temp.new DatabaseHelper(null, null);
+ temp.mSyncState = new SyncStateContentProviderHelper(temp.mOpenHelper);
+ if (!isTemporary()) {
+ mSyncState.copySyncState(
+ mOpenHelper.getReadableDatabase(),
+ temp.mOpenHelper.getWritableDatabase(),
+ getSyncingAccount());
+ }
+ return temp;
+ }
+
+ public SQLiteDatabase getDatabase() {
+ if (mDb == null) mDb = mOpenHelper.getWritableDatabase();
+ return mDb;
+ }
+
+ public boolean getContainsDiffs() {
+ return mContainsDiffs;
+ }
+
+ public void setContainsDiffs(boolean containsDiffs) {
+ if (containsDiffs && !isTemporary()) {
+ throw new IllegalStateException(
+ "only a temporary provider can contain diffs");
+ }
+ mContainsDiffs = containsDiffs;
+ }
+
+ /**
+ * Each subclass of this class should define a subclass of {@link
+ * AbstractTableMerger} for each table they wish to merge. It
+ * should then override this method and return one instance of
+ * each merger, in sequence. Their {@link
+ * AbstractTableMerger#merge merge} methods will be called, one at a
+ * time, in the order supplied.
+ *
+ * <p>The default implementation returns an empty list, so that no
+ * merging will occur.
+ * @return A sequence of subclasses of {@link
+ * AbstractTableMerger}, one for each table that should be merged.
+ */
+ protected Iterable<? extends AbstractTableMerger> getMergers() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public final int update(final Uri url, final ContentValues values,
+ final String selection, final String[] selectionArgs) {
+ mDb = mOpenHelper.getWritableDatabase();
+ mDb.beginTransaction();
+ try {
+ if (isTemporary() && mSyncState.matches(url)) {
+ int numRows = mSyncState.asContentProvider().update(
+ url, values, selection, selectionArgs);
+ mDb.setTransactionSuccessful();
+ return numRows;
+ }
+
+ int result = updateInternal(url, values, selection, selectionArgs);
+ mDb.setTransactionSuccessful();
+
+ if (!isTemporary() && result > 0) {
+ getContext().getContentResolver().notifyChange(url, null /* observer */,
+ changeRequiresLocalSync(url));
+ }
+
+ return result;
+ } finally {
+ mDb.endTransaction();
+ }
+ }
+
+ @Override
+ public final int delete(final Uri url, final String selection,
+ final String[] selectionArgs) {
+ mDb = mOpenHelper.getWritableDatabase();
+ mDb.beginTransaction();
+ try {
+ if (isTemporary() && mSyncState.matches(url)) {
+ int numRows = mSyncState.asContentProvider().delete(url, selection, selectionArgs);
+ mDb.setTransactionSuccessful();
+ return numRows;
+ }
+ int result = deleteInternal(url, selection, selectionArgs);
+ mDb.setTransactionSuccessful();
+ if (!isTemporary() && result > 0) {
+ getContext().getContentResolver().notifyChange(url, null /* observer */,
+ changeRequiresLocalSync(url));
+ }
+ return result;
+ } finally {
+ mDb.endTransaction();
+ }
+ }
+
+ @Override
+ public final Uri insert(final Uri url, final ContentValues values) {
+ mDb = mOpenHelper.getWritableDatabase();
+ mDb.beginTransaction();
+ try {
+ if (isTemporary() && mSyncState.matches(url)) {
+ Uri result = mSyncState.asContentProvider().insert(url, values);
+ mDb.setTransactionSuccessful();
+ return result;
+ }
+ Uri result = insertInternal(url, values);
+ mDb.setTransactionSuccessful();
+ if (!isTemporary() && result != null) {
+ getContext().getContentResolver().notifyChange(url, null /* observer */,
+ changeRequiresLocalSync(url));
+ }
+ return result;
+ } finally {
+ mDb.endTransaction();
+ }
+ }
+
+ @Override
+ public final int bulkInsert(final Uri uri, final ContentValues[] values) {
+ int size = values.length;
+ int completed = 0;
+ final boolean isSyncStateUri = mSyncState.matches(uri);
+ mDb = mOpenHelper.getWritableDatabase();
+ mDb.beginTransaction();
+ try {
+ for (int i = 0; i < size; i++) {
+ Uri result;
+ if (isTemporary() && isSyncStateUri) {
+ result = mSyncState.asContentProvider().insert(uri, values[i]);
+ } else {
+ result = insertInternal(uri, values[i]);
+ mDb.yieldIfContended();
+ }
+ if (result != null) {
+ completed++;
+ }
+ }
+ mDb.setTransactionSuccessful();
+ } finally {
+ mDb.endTransaction();
+ }
+ if (!isTemporary() && completed == values.length) {
+ getContext().getContentResolver().notifyChange(uri, null /* observer */,
+ changeRequiresLocalSync(uri));
+ }
+ return completed;
+ }
+
+ /**
+ * Check if changes to this URI can be syncable changes.
+ * @param uri the URI of the resource that was changed
+ * @return true if changes to this URI can be syncable changes, false otherwise
+ */
+ public boolean changeRequiresLocalSync(Uri uri) {
+ return true;
+ }
+
+ @Override
+ public final Cursor query(final Uri url, final String[] projection,
+ final String selection, final String[] selectionArgs,
+ final String sortOrder) {
+ mDb = mOpenHelper.getReadableDatabase();
+ if (isTemporary() && mSyncState.matches(url)) {
+ return mSyncState.asContentProvider().query(
+ url, projection, selection, selectionArgs, sortOrder);
+ }
+ return queryInternal(url, projection, selection, selectionArgs, sortOrder);
+ }
+
+ /**
+ * Called right before a sync is started.
+ *
+ * @param context the sync context for the operation
+ * @param account
+ */
+ public void onSyncStart(SyncContext context, String account) {
+ if (TextUtils.isEmpty(account)) {
+ throw new IllegalArgumentException("you passed in an empty account");
+ }
+ mSyncingAccount = account;
+ }
+
+ /**
+ * Called right after a sync is completed
+ *
+ * @param context the sync context for the operation
+ * @param success true if the sync succeeded, false if an error occurred
+ */
+ public void onSyncStop(SyncContext context, boolean success) {
+ }
+
+ /**
+ * The account of the most recent call to onSyncStart()
+ * @return the account
+ */
+ public String getSyncingAccount() {
+ return mSyncingAccount;
+ }
+
+ /**
+ * Merge diffs from a sync source with this content provider.
+ *
+ * @param context the SyncContext within which this merge is taking place
+ * @param diffs A temporary content provider containing diffs from a sync
+ * source.
+ * @param result a MergeResult that contains information about the merge, including
+ * a temporary content provider with the same layout as this provider containing
+ * @param syncResult
+ */
+ public void merge(SyncContext context, SyncableContentProvider diffs,
+ TempProviderSyncResult result, SyncResult syncResult) {
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ db.beginTransaction();
+ try {
+ synchronized(this) {
+ mIsMergeCancelled = false;
+ }
+ Iterable<? extends AbstractTableMerger> mergers = getMergers();
+ try {
+ for (AbstractTableMerger merger : mergers) {
+ synchronized(this) {
+ if (mIsMergeCancelled) break;
+ mCurrentMerger = merger;
+ }
+ merger.merge(context, getSyncingAccount(), diffs, result, syncResult, this);
+ }
+ if (mIsMergeCancelled) return;
+ if (diffs != null) {
+ mSyncState.copySyncState(
+ diffs.mOpenHelper.getReadableDatabase(),
+ mOpenHelper.getWritableDatabase(),
+ getSyncingAccount());
+ }
+ } finally {
+ synchronized (this) {
+ mCurrentMerger = null;
+ }
+ }
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+
+ /**
+ * Invoked when the active sync has been canceled. The default
+ * implementation doesn't do anything (except ensure that this
+ * provider is syncable). Subclasses of ContentProvider
+ * that support canceling of sync should override this.
+ */
+ public void onSyncCanceled() {
+ synchronized (this) {
+ mIsMergeCancelled = true;
+ if (mCurrentMerger != null) {
+ mCurrentMerger.onMergeCancelled();
+ }
+ }
+ }
+
+
+ public boolean isMergeCancelled() {
+ return mIsMergeCancelled;
+ }
+
+ /**
+ * Subclasses should override this instead of update(). See update()
+ * for details.
+ *
+ * <p> This method is called within a acquireDbLock()/releaseDbLock() block,
+ * which means a database transaction will be active during the call;
+ */
+ protected abstract int updateInternal(Uri url, ContentValues values,
+ String selection, String[] selectionArgs);
+
+ /**
+ * Subclasses should override this instead of delete(). See delete()
+ * for details.
+ *
+ * <p> This method is called within a acquireDbLock()/releaseDbLock() block,
+ * which means a database transaction will be active during the call;
+ */
+ protected abstract int deleteInternal(Uri url, String selection, String[] selectionArgs);
+
+ /**
+ * Subclasses should override this instead of insert(). See insert()
+ * for details.
+ *
+ * <p> This method is called within a acquireDbLock()/releaseDbLock() block,
+ * which means a database transaction will be active during the call;
+ */
+ protected abstract Uri insertInternal(Uri url, ContentValues values);
+
+ /**
+ * Subclasses should override this instead of query(). See query()
+ * for details.
+ *
+ * <p> This method is *not* called within a acquireDbLock()/releaseDbLock()
+ * block for performance reasons. If an implementation needs atomic access
+ * to the database the lock can be acquired then.
+ */
+ protected abstract Cursor queryInternal(Uri url, String[] projection,
+ String selection, String[] selectionArgs, String sortOrder);
+
+ /**
+ * Make sure that there are no entries for accounts that no longer exist
+ * @param accountsArray the array of currently-existing accounts
+ */
+ protected void onAccountsChanged(String[] accountsArray) {
+ Map<String, Boolean> accounts = new HashMap<String, Boolean>();
+ for (String account : accountsArray) {
+ accounts.put(account, false);
+ }
+ accounts.put(SyncConstValue.NON_SYNCABLE_ACCOUNT, false);
+
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ Map<String, String> tableMap = db.getSyncedTables();
+ Vector<String> tables = new Vector<String>();
+ tables.addAll(tableMap.keySet());
+ tables.addAll(tableMap.values());
+
+ db.beginTransaction();
+ try {
+ mSyncState.onAccountsChanged(accountsArray);
+ for (String table : tables) {
+ deleteRowsForRemovedAccounts(accounts, table,
+ SyncConstValue._SYNC_ACCOUNT);
+ }
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ /**
+ * A helper method to delete all rows whose account is not in the accounts
+ * map. The accountColumnName is the name of the column that is expected
+ * to hold the account. If a row has an empty account it is never deleted.
+ *
+ * @param accounts a map of existing accounts
+ * @param table the table to delete from
+ * @param accountColumnName the name of the column that is expected
+ * to hold the account.
+ */
+ protected void deleteRowsForRemovedAccounts(Map<String, Boolean> accounts,
+ String table, String accountColumnName) {
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ Cursor c = db.query(table, sAccountProjection, null, null,
+ accountColumnName, null, null);
+ try {
+ while (c.moveToNext()) {
+ String account = c.getString(0);
+ if (TextUtils.isEmpty(account)) {
+ continue;
+ }
+ if (!accounts.containsKey(account)) {
+ int numDeleted;
+ numDeleted = db.delete(table, accountColumnName + "=?", new String[]{account});
+ if (Config.LOGV) {
+ Log.v(TAG, "deleted " + numDeleted
+ + " records from table " + table
+ + " for account " + account);
+ }
+ }
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ /**
+ * Called when the sync system determines that this provider should no longer
+ * contain records for the specified account.
+ */
+ public void wipeAccount(String account) {
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ Map<String, String> tableMap = db.getSyncedTables();
+ ArrayList<String> tables = new ArrayList<String>();
+ tables.addAll(tableMap.keySet());
+ tables.addAll(tableMap.values());
+
+ db.beginTransaction();
+
+ try {
+ // remote the SyncState data
+ mSyncState.discardSyncData(db, account);
+
+ // remove the data in the synced tables
+ for (String table : tables) {
+ db.delete(table, SYNC_ACCOUNT_WHERE_CLAUSE, new String[]{account});
+ }
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ /**
+ * Retrieves the SyncData bytes for the given account. The byte array returned may be null.
+ */
+ public byte[] readSyncDataBytes(String account) {
+ return mSyncState.readSyncDataBytes(mOpenHelper.getReadableDatabase(), account);
+ }
+
+ /**
+ * Sets the SyncData bytes for the given account. The bytes array may be null.
+ */
+ public void writeSyncDataBytes(String account, byte[] data) {
+ mSyncState.writeSyncDataBytes(mOpenHelper.getWritableDatabase(), account, data);
+ }
+}
+
diff --git a/core/java/android/content/TempProviderSyncAdapter.java b/core/java/android/content/TempProviderSyncAdapter.java
new file mode 100644
index 0000000..78510aa
--- /dev/null
+++ b/core/java/android/content/TempProviderSyncAdapter.java
@@ -0,0 +1,546 @@
+package android.content;
+
+import com.google.android.net.NetStats;
+
+import android.database.SQLException;
+import android.os.Bundle;
+import android.os.Debug;
+import android.os.Parcelable;
+import android.os.SystemProperties;
+import android.text.TextUtils;
+import android.util.Config;
+import android.util.EventLog;
+import android.util.Log;
+import android.util.TimingLogger;
+
+/**
+ * @hide
+ */
+public abstract class TempProviderSyncAdapter extends SyncAdapter {
+ private static final String TAG = "Sync";
+
+ private static final int MAX_GET_SERVER_DIFFS_LOOP_COUNT = 20;
+ private static final int MAX_UPLOAD_CHANGES_LOOP_COUNT = 10;
+ private static final int NUM_ALLOWED_SIMULTANEOUS_DELETIONS = 5;
+ private static final long PERCENT_ALLOWED_SIMULTANEOUS_DELETIONS = 20;
+
+ private volatile SyncableContentProvider mProvider;
+ private volatile SyncThread mSyncThread = null;
+ private volatile boolean mProviderSyncStarted;
+ private volatile boolean mAdapterSyncStarted;
+
+ public TempProviderSyncAdapter(SyncableContentProvider provider) {
+ super();
+ mProvider = provider;
+ }
+
+ /**
+ * Used by getServerDiffs() to track the sync progress for a given
+ * sync adapter. Implementations of SyncAdapter generally specialize
+ * this class in order to track specific data about that SyncAdapter's
+ * sync. If an implementation of SyncAdapter doesn't need to store
+ * any data for a sync it may use TrivialSyncData.
+ */
+ public static abstract class SyncData implements Parcelable {
+
+ }
+
+ public final void setContext(Context context) {
+ mContext = context;
+ }
+
+ /**
+ * Retrieve the Context this adapter is running in. Only available
+ * once onSyncStarting() is called (not available from constructor).
+ */
+ final public Context getContext() {
+ return mContext;
+ }
+
+ /**
+ * Called right before a sync is started.
+ *
+ * @param context allows you to publish status and interact with the
+ * @param account the account to sync
+ * @param forced if true then the sync was forced
+ * @param result information to track what happened during this sync attempt
+ * @return true, if the sync was successfully started. One reason it can
+ * fail to start is if there is no user configured on the device.
+ */
+ public abstract void onSyncStarting(SyncContext context, String account, boolean forced,
+ SyncResult result);
+
+ /**
+ * Called right after a sync is completed
+ *
+ * @param context allows you to publish status and interact with the
+ * user during interactive syncs.
+ * @param success true if the sync suceeded, false if an error occured
+ */
+ public abstract void onSyncEnding(SyncContext context, boolean success);
+
+ /**
+ * Implement this to return true if the data in your content provider
+ * is read only.
+ */
+ public abstract boolean isReadOnly();
+
+ /**
+ * Get diffs from the server since the last completed sync and put them
+ * into a temporary provider.
+ *
+ * @param context allows you to publish status and interact with the
+ * user during interactive syncs.
+ * @param syncData used to track the progress this client has made in syncing data
+ * from the server
+ * @param tempProvider this is where the diffs should be stored
+ * @param extras any extra data describing the sync that is desired
+ * @param syncInfo sync adapter-specific data that is used during a single sync operation
+ * @param syncResult information to track what happened during this sync attempt
+ */
+ public abstract void getServerDiffs(SyncContext context,
+ SyncData syncData, SyncableContentProvider tempProvider,
+ Bundle extras, Object syncInfo, SyncResult syncResult);
+
+ /**
+ * Send client diffs to the server, optionally receiving more diffs from the server
+ *
+ * @param context allows you to publish status and interact with the
+ * user during interactive syncs.
+ * @param clientDiffs the diffs from the client
+ * @param serverDiffs the SyncableContentProvider that should be populated with
+* the entries that were returned in response to an insert/update/delete request
+* to the server
+ * @param syncResult information to track what happened during this sync attempt
+ * @param dontActuallySendDeletes
+ */
+ public abstract void sendClientDiffs(SyncContext context,
+ SyncableContentProvider clientDiffs,
+ SyncableContentProvider serverDiffs, SyncResult syncResult,
+ boolean dontActuallySendDeletes);
+
+ /**
+ * Reads the sync data from the ContentProvider
+ * @param contentProvider the ContentProvider to read from
+ * @return the SyncData for the provider. This may be null.
+ */
+ public SyncData readSyncData(SyncableContentProvider contentProvider) {
+ return null;
+ }
+
+ /**
+ * Create and return a new, empty SyncData object
+ */
+ public SyncData newSyncData() {
+ return null;
+ }
+
+ /**
+ * Stores the sync data in the Sync Stats database, keying it by
+ * the account that was set in the last call to onSyncStarting()
+ */
+ public void writeSyncData(SyncData syncData, SyncableContentProvider contentProvider) {}
+
+ /**
+ * Indicate to the SyncAdapter that the last sync that was started has
+ * been cancelled.
+ */
+ public abstract void onSyncCanceled();
+
+ /**
+ * Initializes the temporary content providers used during
+ * {@link TempProviderSyncAdapter#sendClientDiffs}.
+ * May copy relevant data from the underlying db into this provider so
+ * joins, etc., can work.
+ *
+ * @param cp The ContentProvider to initialize.
+ */
+ protected void initTempProvider(SyncableContentProvider cp) {}
+
+ protected Object createSyncInfo() {
+ return null;
+ }
+
+ /**
+ * Called when the accounts list possibly changed, to give the
+ * SyncAdapter a chance to do any necessary bookkeeping, e.g.
+ * to make sure that any required SubscribedFeeds subscriptions
+ * exist.
+ * @param accounts the list of accounts
+ */
+ public abstract void onAccountsChanged(String[] accounts);
+
+ private Context mContext;
+
+ private class SyncThread extends Thread {
+ private final String mAccount;
+ private final Bundle mExtras;
+ private final SyncContext mSyncContext;
+ private volatile boolean mIsCanceled = false;
+ private long[] mNetStats;
+ private final SyncResult mResult;
+
+ SyncThread(SyncContext syncContext, String account, Bundle extras) {
+ super("SyncThread");
+ mAccount = account;
+ mExtras = extras;
+ mSyncContext = syncContext;
+ mResult = new SyncResult();
+ }
+
+ void cancelSync() {
+ mIsCanceled = true;
+ if (mAdapterSyncStarted) onSyncCanceled();
+ if (mProviderSyncStarted) mProvider.onSyncCanceled();
+ // We may lose the last few sync events when canceling. Oh well.
+ long[] newNetStats = NetStats.getStats();
+ logSyncDetails(newNetStats[0] - mNetStats[0], newNetStats[1] - mNetStats[1], mResult);
+ }
+
+ @Override
+ public void run() {
+ android.os.Process.setThreadPriority(android.os.Process.myTid(),
+ android.os.Process.THREAD_PRIORITY_BACKGROUND);
+ mNetStats = NetStats.getStats();
+ try {
+ sync(mSyncContext, mAccount, mExtras);
+ } catch (SQLException e) {
+ Log.e(TAG, "Sync failed", e);
+ mResult.databaseError = true;
+ } finally {
+ mSyncThread = null;
+ if (!mIsCanceled) {
+ long[] newNetStats = NetStats.getStats();
+ logSyncDetails(newNetStats[0] - mNetStats[0], newNetStats[1] - mNetStats[1], mResult);
+ mSyncContext.onFinished(mResult);
+ }
+ }
+ }
+
+ private void sync(SyncContext syncContext, String account, Bundle extras) {
+ mIsCanceled = false;
+
+ mProviderSyncStarted = false;
+ mAdapterSyncStarted = false;
+ String message = null;
+
+ boolean syncForced = extras.getBoolean(ContentResolver.SYNC_EXTRAS_FORCE, false);
+
+ try {
+ mProvider.onSyncStart(syncContext, account);
+ mProviderSyncStarted = true;
+ onSyncStarting(syncContext, account, syncForced, mResult);
+ if (mResult.hasError()) {
+ message = "SyncAdapter failed while trying to start sync";
+ return;
+ }
+ mAdapterSyncStarted = true;
+ if (mIsCanceled) {
+ return;
+ }
+ final String syncTracingEnabledValue = SystemProperties.get(TAG + "Tracing");
+ final boolean syncTracingEnabled = !TextUtils.isEmpty(syncTracingEnabledValue);
+ try {
+ if (syncTracingEnabled) {
+ System.gc();
+ System.gc();
+ Debug.startMethodTracing("synctrace." + System.currentTimeMillis());
+ }
+ runSyncLoop(syncContext, account, extras);
+ } finally {
+ if (syncTracingEnabled) Debug.stopMethodTracing();
+ }
+ onSyncEnding(syncContext, !mResult.hasError());
+ mAdapterSyncStarted = false;
+ mProvider.onSyncStop(syncContext, true);
+ mProviderSyncStarted = false;
+ } finally {
+ if (mAdapterSyncStarted) {
+ mAdapterSyncStarted = false;
+ onSyncEnding(syncContext, false);
+ }
+ if (mProviderSyncStarted) {
+ mProviderSyncStarted = false;
+ mProvider.onSyncStop(syncContext, false);
+ }
+ if (!mIsCanceled) {
+ if (message != null) syncContext.setStatusText(message);
+ }
+ }
+ }
+
+ private void runSyncLoop(SyncContext syncContext, String account, Bundle extras) {
+ TimingLogger syncTimer = new TimingLogger(TAG + "Profiling", "sync");
+ syncTimer.addSplit("start");
+ int loopCount = 0;
+ boolean tooManyGetServerDiffsAttempts = false;
+
+ final boolean overrideTooManyDeletions =
+ extras.getBoolean(ContentResolver.SYNC_EXTRAS_OVERRIDE_TOO_MANY_DELETIONS,
+ false);
+ final boolean discardLocalDeletions =
+ extras.getBoolean(ContentResolver.SYNC_EXTRAS_DISCARD_LOCAL_DELETIONS, false);
+ boolean uploadOnly = extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD,
+ false /* default this flag to false */);
+ SyncableContentProvider serverDiffs = null;
+ TempProviderSyncResult result = new TempProviderSyncResult();
+ try {
+ if (!uploadOnly) {
+ /**
+ * This loop repeatedly calls SyncAdapter.getServerDiffs()
+ * (to get changes from the feed) followed by
+ * ContentProvider.merge() (to incorporate these changes
+ * into the provider), stopping when the SyncData returned
+ * from getServerDiffs() indicates that all the data was
+ * fetched.
+ */
+ while (!mIsCanceled) {
+ // Don't let a bad sync go forever
+ if (loopCount++ == MAX_GET_SERVER_DIFFS_LOOP_COUNT) {
+ Log.e(TAG, "runSyncLoop: Hit max loop count while getting server diffs "
+ + getClass().getName());
+ // TODO: change the structure here to schedule a new sync
+ // with a backoff time, keeping track to be sure
+ // we don't keep doing this forever (due to some bug or
+ // mismatch between the client and the server)
+ tooManyGetServerDiffsAttempts = true;
+ break;
+ }
+
+ // Get an empty content provider to put the diffs into
+ if (serverDiffs != null) serverDiffs.close();
+ serverDiffs = mProvider.getTemporaryInstance();
+
+ // Get records from the server which will be put into the serverDiffs
+ initTempProvider(serverDiffs);
+ Object syncInfo = createSyncInfo();
+ SyncData syncData = readSyncData(serverDiffs);
+ // syncData will only be null if there was a demarshalling error
+ // while reading the sync data.
+ if (syncData == null) {
+ mProvider.wipeAccount(account);
+ syncData = newSyncData();
+ }
+ mResult.clear();
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "runSyncLoop: running getServerDiffs using syncData "
+ + syncData.toString());
+ }
+ getServerDiffs(syncContext, syncData, serverDiffs, extras, syncInfo,
+ mResult);
+
+ if (mIsCanceled) return;
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "runSyncLoop: result: " + mResult);
+ }
+ if (mResult.hasError()) return;
+ if (mResult.partialSyncUnavailable) {
+ if (Config.LOGD) {
+ Log.d(TAG, "partialSyncUnavailable is set, setting "
+ + "ignoreSyncData and retrying");
+ }
+ mProvider.wipeAccount(account);
+ continue;
+ }
+
+ // write the updated syncData back into the temp provider
+ writeSyncData(syncData, serverDiffs);
+
+ // apply the downloaded changes to the provider
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "runSyncLoop: running merge");
+ }
+ mProvider.merge(syncContext, serverDiffs,
+ null /* don't return client diffs */, mResult);
+ if (mIsCanceled) return;
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "runSyncLoop: result: " + mResult);
+ }
+
+ // if the server has no more changes then break out of the loop
+ if (!mResult.moreRecordsToGet) {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "runSyncLoop: fetched all data, moving on");
+ }
+ break;
+ }
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "runSyncLoop: more data to fetch, looping");
+ }
+ }
+ }
+
+ /**
+ * This loop repeatedly calls ContentProvider.merge() followed
+ * by SyncAdapter.merge() until either indicate that there is
+ * no more work to do by returning null.
+ * <p>
+ * The initial ContentProvider.merge() returns a temporary
+ * ContentProvider that contains any local changes that need
+ * to be committed to the server.
+ * <p>
+ * The SyncAdapter.merge() calls upload the changes to the server
+ * and populates temporary provider (the serverDiffs) with the
+ * result.
+ * <p>
+ * Subsequent calls to ContentProvider.merge() incoporate the
+ * result of previous SyncAdapter.merge() calls into the
+ * real ContentProvider and again return a temporary
+ * ContentProvider that contains any local changes that need
+ * to be committed to the server.
+ */
+ loopCount = 0;
+ boolean readOnly = isReadOnly();
+ long previousNumModifications = 0;
+ if (serverDiffs != null) {
+ serverDiffs.close();
+ serverDiffs = null;
+ }
+
+ // If we are discarding local deletions then we need to redownload all the items
+ // again (since some of them might have been deleted). We do this by deleting the
+ // sync data for the current account by writing in a null one.
+ if (discardLocalDeletions) {
+ serverDiffs = mProvider.getTemporaryInstance();
+ initTempProvider(serverDiffs);
+ writeSyncData(null, serverDiffs);
+ }
+
+ while (!mIsCanceled) {
+ if (Config.LOGV) {
+ Log.v(TAG, "runSyncLoop: Merging diffs from server to client");
+ }
+ if (result.tempContentProvider != null) {
+ result.tempContentProvider.close();
+ result.tempContentProvider = null;
+ }
+ mResult.clear();
+ mProvider.merge(syncContext, serverDiffs, readOnly ? null : result,
+ mResult);
+ if (mIsCanceled) return;
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "runSyncLoop: result: " + mResult);
+ }
+
+ SyncableContentProvider clientDiffs =
+ readOnly ? null : result.tempContentProvider;
+ if (clientDiffs == null) {
+ // Nothing to commit back to the server
+ if (Config.LOGV) Log.v(TAG, "runSyncLoop: No client diffs");
+ break;
+ }
+
+ long numModifications = mResult.stats.numUpdates
+ + mResult.stats.numDeletes
+ + mResult.stats.numInserts;
+
+ // as long as we are making progress keep resetting the loop count
+ if (numModifications < previousNumModifications) {
+ loopCount = 0;
+ }
+ previousNumModifications = numModifications;
+
+ // Don't let a bad sync go forever
+ if (loopCount++ >= MAX_UPLOAD_CHANGES_LOOP_COUNT) {
+ Log.e(TAG, "runSyncLoop: Hit max loop count while syncing "
+ + getClass().getName());
+ mResult.tooManyRetries = true;
+ break;
+ }
+
+ if (!overrideTooManyDeletions && !discardLocalDeletions
+ && hasTooManyDeletions(mResult.stats)) {
+ if (Config.LOGD) {
+ Log.d(TAG, "runSyncLoop: Too many deletions were found in provider "
+ + getClass().getName() + ", not doing any more updates");
+ }
+ long numDeletes = mResult.stats.numDeletes;
+ mResult.stats.clear();
+ mResult.tooManyDeletions = true;
+ mResult.stats.numDeletes = numDeletes;
+ break;
+ }
+
+ if (Config.LOGV) Log.v(TAG, "runSyncLoop: Merging diffs from client to server");
+ if (serverDiffs != null) serverDiffs.close();
+ serverDiffs = clientDiffs.getTemporaryInstance();
+ initTempProvider(serverDiffs);
+ mResult.clear();
+ sendClientDiffs(syncContext, clientDiffs, serverDiffs, mResult,
+ discardLocalDeletions);
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "runSyncLoop: result: " + mResult);
+ }
+
+ if (!mResult.madeSomeProgress()) {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "runSyncLoop: No data from client diffs merge");
+ }
+ break;
+ }
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "runSyncLoop: made some progress, looping");
+ }
+ }
+
+ // add in any status codes that we saved from earlier
+ mResult.tooManyRetries |= tooManyGetServerDiffsAttempts;
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "runSyncLoop: final result: " + mResult);
+ }
+ } finally {
+ // do this in the finally block to guarantee that is is set and not overwritten
+ if (discardLocalDeletions) {
+ mResult.fullSyncRequested = true;
+ }
+ if (serverDiffs != null) serverDiffs.close();
+ if (result.tempContentProvider != null) result.tempContentProvider.close();
+ syncTimer.addSplit("stop");
+ syncTimer.dumpToLog();
+ }
+ }
+ }
+
+ /**
+ * Logs details on the sync.
+ * Normally this will be overridden by a subclass that will provide
+ * provider-specific details.
+ *
+ * @param bytesSent number of bytes the sync sent over the network
+ * @param bytesReceived number of bytes the sync received over the network
+ * @param result The SyncResult object holding info on the sync
+ */
+ protected void logSyncDetails(long bytesSent, long bytesReceived, SyncResult result) {
+ EventLog.writeEvent(SyncAdapter.LOG_SYNC_DETAILS, TAG, bytesSent, bytesReceived, "");
+ }
+
+ public void startSync(SyncContext syncContext, String account, Bundle extras) {
+ if (mSyncThread != null) {
+ syncContext.onFinished(SyncResult.ALREADY_IN_PROGRESS);
+ return;
+ }
+
+ mSyncThread = new SyncThread(syncContext, account, extras);
+ mSyncThread.start();
+ }
+
+ public void cancelSync() {
+ if (mSyncThread != null) {
+ mSyncThread.cancelSync();
+ }
+ }
+
+ protected boolean hasTooManyDeletions(SyncStats stats) {
+ long numEntries = stats.numEntries;
+ long numDeletedEntries = stats.numDeletes;
+
+ long percentDeleted = (numDeletedEntries == 0)
+ ? 0
+ : (100 * numDeletedEntries /
+ (numEntries + numDeletedEntries));
+ boolean tooManyDeletions =
+ (numDeletedEntries > NUM_ALLOWED_SIMULTANEOUS_DELETIONS)
+ && (percentDeleted > PERCENT_ALLOWED_SIMULTANEOUS_DELETIONS);
+ return tooManyDeletions;
+ }
+}
diff --git a/core/java/android/content/TempProviderSyncResult.java b/core/java/android/content/TempProviderSyncResult.java
new file mode 100644
index 0000000..81f6f79
--- /dev/null
+++ b/core/java/android/content/TempProviderSyncResult.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+/**
+ * Used to hold data returned from a given phase of a TempProviderSync.
+ * @hide
+ */
+public class TempProviderSyncResult {
+ /**
+ * An interface to a temporary content provider that contains
+ * the result of updates that were sent to the server. This
+ * provider must be merged into the permanent content provider.
+ * This may be null, which indicates that there is nothing to
+ * merge back into the content provider.
+ */
+ public SyncableContentProvider tempContentProvider;
+
+ public TempProviderSyncResult() {
+ tempContentProvider = null;
+ }
+}
diff --git a/core/java/android/content/UriMatcher.java b/core/java/android/content/UriMatcher.java
new file mode 100644
index 0000000..a98e6d5
--- /dev/null
+++ b/core/java/android/content/UriMatcher.java
@@ -0,0 +1,262 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content;
+
+import android.net.Uri;
+
+import java.util.ArrayList;
+import java.util.regex.Pattern;
+
+/**
+Utility class to aid in matching URIs in content providers.
+
+<p>To use this class, build up a tree of UriMatcher objects.
+Typically, it looks something like this:
+<pre>
+ private static final int PEOPLE = 1;
+ private static final int PEOPLE_ID = 2;
+ private static final int PEOPLE_PHONES = 3;
+ private static final int PEOPLE_PHONES_ID = 4;
+ private static final int PEOPLE_CONTACTMETHODS = 7;
+ private static final int PEOPLE_CONTACTMETHODS_ID = 8;
+
+ private static final int DELETED_PEOPLE = 20;
+
+ private static final int PHONES = 9;
+ private static final int PHONES_ID = 10;
+ private static final int PHONES_FILTER = 14;
+
+ private static final int CONTACTMETHODS = 18;
+ private static final int CONTACTMETHODS_ID = 19;
+
+ private static final int CALLS = 11;
+ private static final int CALLS_ID = 12;
+ private static final int CALLS_FILTER = 15;
+
+ private static final UriMatcher sURIMatcher = new UriMatcher();
+
+ static
+ {
+ sURIMatcher.addURI("contacts", "/people", PEOPLE);
+ sURIMatcher.addURI("contacts", "/people/#", PEOPLE_ID);
+ sURIMatcher.addURI("contacts", "/people/#/phones", PEOPLE_PHONES);
+ sURIMatcher.addURI("contacts", "/people/#/phones/#", PEOPLE_PHONES_ID);
+ sURIMatcher.addURI("contacts", "/people/#/contact_methods", PEOPLE_CONTACTMETHODS);
+ sURIMatcher.addURI("contacts", "/people/#/contact_methods/#", PEOPLE_CONTACTMETHODS_ID);
+ sURIMatcher.addURI("contacts", "/deleted_people", DELETED_PEOPLE);
+ sURIMatcher.addURI("contacts", "/phones", PHONES);
+ sURIMatcher.addURI("contacts", "/phones/filter/*", PHONES_FILTER);
+ sURIMatcher.addURI("contacts", "/phones/#", PHONES_ID);
+ sURIMatcher.addURI("contacts", "/contact_methods", CONTACTMETHODS);
+ sURIMatcher.addURI("contacts", "/contact_methods/#", CONTACTMETHODS_ID);
+ sURIMatcher.addURI("call_log", "/calls", CALLS);
+ sURIMatcher.addURI("call_log", "/calls/filter/*", CALLS_FILTER);
+ sURIMatcher.addURI("call_log", "/calls/#", CALLS_ID);
+ }
+</pre>
+<p>Then when you need to match match against a URI, call {@link #match}, providing
+the tokenized url you've been given, and the value you want if there isn't
+a match. You can use the result to build a query, return a type, insert or
+delete a row, or whatever you need, without duplicating all of the if-else
+logic you'd otherwise need. Like this:
+<pre>
+ public String getType(String[] url)
+ {
+ int match = sURIMatcher.match(url, NO_MATCH);
+ switch (match)
+ {
+ case PEOPLE:
+ return "vnd.android.cursor.dir/person";
+ case PEOPLE_ID:
+ return "vnd.android.cursor.item/person";
+... snip ...
+ return "vnd.android.cursor.dir/snail-mail";
+ case PEOPLE_ADDRESS_ID:
+ return "vnd.android.cursor.item/snail-mail";
+ default:
+ return null;
+ }
+ }
+</pre>
+instead of
+<pre>
+ public String getType(String[] url)
+ {
+ if (url.length >= 2) {
+ if (url[1].equals("people")) {
+ if (url.length == 2) {
+ return "vnd.android.cursor.dir/person";
+ } else if (url.length == 3) {
+ return "vnd.android.cursor.item/person";
+... snip ...
+ return "vnd.android.cursor.dir/snail-mail";
+ } else if (url.length == 3) {
+ return "vnd.android.cursor.item/snail-mail";
+ }
+ }
+ }
+ return null;
+ }
+</pre>
+*/
+public class UriMatcher
+{
+ public static final int NO_MATCH = -1;
+ /**
+ * Creates the root node of the URI tree.
+ *
+ * @param code the code to match for the root URI
+ */
+ public UriMatcher(int code)
+ {
+ mCode = code;
+ mWhich = -1;
+ mChildren = new ArrayList<UriMatcher>();
+ mText = null;
+ }
+
+ private UriMatcher()
+ {
+ mCode = NO_MATCH;
+ mWhich = -1;
+ mChildren = new ArrayList<UriMatcher>();
+ mText = null;
+ }
+
+ /**
+ * Add a URI to match, and the code to return when this URI is
+ * matched. URI nodes may be exact match string, the token "*"
+ * that matches any text, or the token "#" that matches only
+ * numbers.
+ *
+ * @param authority the authority to match
+ * @param path the path to match. * may be used as a wild card for
+ * any text, and # may be used as a wild card for numbers.
+ * @param code the code that is returned when a URI is matched
+ * against the given components. Must be positive.
+ */
+ public void addURI(String authority, String path, int code)
+ {
+ if (code < 0) {
+ throw new IllegalArgumentException("code " + code + " is invalid: it must be positive");
+ }
+ String[] tokens = path != null ? PATH_SPLIT_PATTERN.split(path) : null;
+ int numTokens = tokens != null ? tokens.length : 0;
+ UriMatcher node = this;
+ for (int i = -1; i < numTokens; i++) {
+ String token = i < 0 ? authority : tokens[i];
+ ArrayList<UriMatcher> children = node.mChildren;
+ int numChildren = children.size();
+ UriMatcher child;
+ int j;
+ for (j = 0; j < numChildren; j++) {
+ child = children.get(j);
+ if (token.equals(child.mText)) {
+ node = child;
+ break;
+ }
+ }
+ if (j == numChildren) {
+ // Child not found, create it
+ child = new UriMatcher();
+ if (token.equals("#")) {
+ child.mWhich = NUMBER;
+ } else if (token.equals("*")) {
+ child.mWhich = TEXT;
+ } else {
+ child.mWhich = EXACT;
+ }
+ child.mText = token;
+ node.mChildren.add(child);
+ node = child;
+ }
+ }
+ node.mCode = code;
+ }
+
+ static final Pattern PATH_SPLIT_PATTERN = Pattern.compile("/");
+
+ /**
+ * Try to match against the path in a url.
+ *
+ * @param uri The url whose path we will match against.
+ *
+ * @return The code for the matched node (added using addURI),
+ * or -1 if there is no matched node.
+ */
+ public int match(Uri uri)
+ {
+ final int li = uri.getPathSegments().size();
+
+ UriMatcher node = this;
+
+ if (li == 0 && uri.getAuthority() == null) {
+ return this.mCode;
+ }
+
+ for (int i=-1; i<li; i++) {
+ String u = i < 0 ? uri.getAuthority() : uri.getPathSegments().get(i);
+ ArrayList<UriMatcher> list = node.mChildren;
+ if (list == null) {
+ break;
+ }
+ node = null;
+ int lj = list.size();
+ for (int j=0; j<lj; j++) {
+ UriMatcher n = list.get(j);
+ which_switch:
+ switch (n.mWhich) {
+ case EXACT:
+ if (n.mText.equals(u)) {
+ node = n;
+ }
+ break;
+ case NUMBER:
+ int lk = u.length();
+ for (int k=0; k<lk; k++) {
+ char c = u.charAt(k);
+ if (c < '0' || c > '9') {
+ break which_switch;
+ }
+ }
+ node = n;
+ break;
+ case TEXT:
+ node = n;
+ break;
+ }
+ if (node != null) {
+ break;
+ }
+ }
+ if (node == null) {
+ return NO_MATCH;
+ }
+ }
+
+ return node.mCode;
+ }
+
+ private static final int EXACT = 0;
+ private static final int NUMBER = 1;
+ private static final int TEXT = 2;
+
+ private int mCode;
+ private int mWhich;
+ private String mText;
+ private ArrayList<UriMatcher> mChildren;
+}
diff --git a/core/java/android/content/package.html b/core/java/android/content/package.html
new file mode 100644
index 0000000..7b3e8cf
--- /dev/null
+++ b/core/java/android/content/package.html
@@ -0,0 +1,649 @@
+<html>
+<head>
+<script type="text/javascript" src="http://www.corp.google.com/style/prettify.js"></script>
+<script src="http://www.corp.google.com/eng/techpubs/include/navbar.js" type="text/javascript"></script>
+</head>
+
+<body>
+
+<p>Contains classes for accessing and publishing data
+on the device. It includes three main categories of APIs:
+the {@link android.content.res.Resources Resources} for
+retrieving resource data associated with an application;
+{@link android.content.ContentProvider Content Providers} and
+{@link android.content.ContentResolver ContentResolver} for managing and
+publishing persistent data associated with an application; and
+the {@link android.content.pm.PackageManager Package Manager}
+for finding out information about the application packages installed
+on the device.</p>
+
+<p>In addition, the {@link android.content.Context Context} abstract class
+is a base API for pulling these pieces together, allowing you to access
+an application's resources and transfer data between applications.</p>
+
+<p>This package builds on top of the lower-level Android packages
+{@link android.database}, {@link android.text},
+{@link android.graphics.drawable}, {@link android.graphics},
+{@link android.os}, and {@link android.util}.</p>
+
+<ol>
+ <li> <a href="#Resources">Resources</a>
+ <ol>
+ <li> <a href="#ResourcesTerminology">Terminology</a>
+ <li> <a href="#ResourcesQuickStart">Examples</a>
+ <ol>
+ <li> <a href="#UsingSystemResources">Using System Resources</a>
+ <li> <a href="#StringResources">String Resources</a>
+ <li> <a href="#ColorResources">Color Resources</a>
+ <li> <a href="#DrawableResources">Drawable Resources</a>
+ <li> <a href="#LayoutResources">Layout Resources</a>
+ <li> <a href="#ReferencesToResources">References to Resources</a>
+ <li> <a href="#ReferencesToThemeAttributes">References to Theme Attributes</a>
+ <li> <a href="#StyleResources">Style Resources</a>
+ <li> <a href="#StylesInLayoutResources">Styles in Layout Resources</a>
+ </ol>
+ </ol>
+</ol>
+
+<a name="Resources"></a>
+<h2>Resources</h2>
+
+<p>This topic includes a terminology list associated with resources, and a series
+ of examples of using resources in code. For a complete guide on creating and
+ using resources, see the document on <a href="{@docRoot}devel/resources-i18n.html">Resources
+ and Internationalization</a>. For a reference on the supported Android resource types,
+ see <a href="{@docRoot}reference/available-resources.html">Available Resource Types</a>.</p>
+<p>The Android resource system keeps track of all non-code
+ assets associated with an application. You use the
+ {@link android.content.res.Resources Resources} class to access your
+ application's resources; the Resources instance associated with your
+ application can generally be found through
+ {@link android.content.Context#getResources Context.getResources()}.</p>
+<p>An application's resources are compiled into the application
+binary at build time for you by the build system. To use a resource,
+you must install it correctly in the source tree and build your
+application. As part of the build process, Java symbols for each
+of the resources are generated that you can use in your source
+code -- this allows the compiler to verify that your application code matches
+up with the resources you defined.</p>
+
+<p>The rest of this section is organized as a tutorial on how to
+use resources in an application.</p>
+
+<a name="ResourcesTerminology"></a>
+<h3>Terminology</h3>
+
+<p>The resource system brings a number of different pieces together to
+form the final complete resource functionality. To help understand the
+overall system, here are some brief definitions of the core concepts and
+components you will encounter in using it:</p>
+
+<p><b>Asset</b>: A single blob of data associated with an application. This
+includes Java object files, graphics (such as PNG images), XML files, etc.
+These files are organized in a directory hierarchy that, during final packaging
+of the application, is bundled together into a single ZIP file.</p>
+
+<p><b>aapt</b>: The tool that generates the final ZIP file of application
+assets. In addition to collecting raw assets together, it also parses
+resource definitions into binary asset data.</p>
+
+<p><b>Resource Table</b>: A special asset that aapt generates for you,
+describing all of the resources contained in an application/package.
+This file is accessed for you by the Resources class; it is not touched
+directly by applications.</p>
+
+<p><b>Resource</b>: An entry in the Resource Table describing a single
+named value. Broadly, there are two types of resources: primitives and
+bags.</p>
+
+<p><b>Resource Identifier</b>: In the Resource Table all resources are
+identified by a unique integer number. In source code (resource descriptions,
+XML files, Java code) you can use symbolic names that stand as constants for
+the actual resource identifier integer.</p>
+
+<p><b>Primitive Resource</b>: All primitive resources can be written as a
+simple string, using formatting to describe a variety of primitive types
+included in the resource system: integers, colors, strings, references to
+other resources, etc. Complex resources, such as bitmaps and XML
+describes, are stored as a primitive string resource whose value is the path
+of the underlying Asset holding its actual data.</p>
+
+<p><b>Bag Resource</b>: A special kind of resource entry that, instead of a
+simple string, holds an arbitrary list of name/value pairs. Each name is
+itself a resource identifier, and each value can hold
+the same kinds of string formatted data as a normal resource. Bags also
+support inheritance: a bag can inherit the values from another bag, selectively
+replacing or extending them to generate its own contents.</p>
+
+<p><b>Kind</b>: The resource kind is a way to organize resource identifiers
+for various purposes. For example, drawable resources are used to
+instantiate Drawable objects, so their data is a primitive resource containing
+either a color constant or string path to a bitmap or XML asset. Other
+common resource kinds are string (localized string primitives), color
+(color primitives), layout (a string path to an XML asset describing a view
+layout), and style (a bag resource describing user interface attributes).
+There is also a standard "attr" resource kind, which defines the resource
+identifiers to be used for naming bag items and XML attributes</p>
+
+<p><b>Style</b>: The name of the resource kind containing bags that are used
+to supply a set of user interface attributes. For example, a TextView class may
+be given a style resource that defines its text size, color, and alignment.
+In a layout XML file, you associate a style with a bag using the "style"
+attribute, whose value is the name of the style resource.</p>
+
+<p><b>Style Class</b>: Specifies a related set of attribute resources.
+This data is not placed in the resource table itself, but used to generate
+Java constants that make it easier for you to retrieve values out of
+a style resource and/or XML tag's attributes. For example, the
+Android platform defines a "View" style class that
+contains all of the standard view attributes: padding, visibility,
+background, etc.; when View is inflated it uses this style class to
+retrieve those values from the XML file (at which point style and theme
+information is applied as approriate) and load them into its instance.</p>
+
+<p><b>Configuration</b>: For any particular resource identifier, there may be
+multiple different available values depending on the current configuration.
+The configuration includes the locale (language and country), screen
+orientation, screen density, etc. The current configuration is used to
+select which resource values are in effect when the resource table is
+loaded.</p>
+
+<p><b>Theme</b>: A standard style resource that supplies global
+attribute values for a particular context. For example, when writing a
+Activity the application developer can select a standard theme to use, such
+as the Theme.White or Theme.Black styles; this style supplies information
+such as the screen background image/color, default text color, button style,
+text editor style, text size, etc. When inflating a layout resource, most
+values for widgets (the text color, selector, background) if not explicitly
+set will come from the current theme; style and attribute
+values supplied in the layout can also assign their value from explicitly
+named values in the theme attributes if desired.</p>
+
+<p><b>Overlay</b>: A resource table that does not define a new set of resources,
+but instead replaces the values of resources that are in another resource table.
+Like a configuration, this is applied at load time
+to the resource data; it can add new configuration values (for example
+strings in a new locale), replace existing values (for example change
+the standard white background image to a "Hello Kitty" background image),
+and modify resource bags (for example change the font size of the Theme.White
+style to have an 18 pt font size). This is the facility that allows the
+user to select between different global appearances of their device, or
+download files with new appearances.</p>
+
+<a name="ResourcesQuickStart"></a>
+<h3>Examples</h3>
+
+<p>This section gives a few quick examples you can use to make your own resources.
+ For more details on how to define and use resources, see <a
+ href="{@docRoot}devel/resources-i18n.html">Resources</a>. </p>
+
+<a name="UsingSystemResources"></a>
+<h4>Using System Resources</h4>
+
+<p>Many resources included with the system are available to applications.
+All such resources are defined under the class "android.R". For example,
+you can display the standard application icon in a screen with the following
+code:</p>
+
+<pre class="prettyprint">
+public class MyActivity extends Activity
+{
+ public void onStart()
+ {
+ requestScreenFeatures(FEATURE_BADGE_IMAGE);
+
+ super.onStart();
+
+ setBadgeResource(android.R.drawable.sym_def_app_icon);
+ }
+}
+</pre>
+
+<p>In a similar way, this code will apply to your screen the standard
+"green background" visual treatment defined by the system:</p>
+
+<pre class="prettyprint">
+public class MyActivity extends Activity
+{
+ public void onStart()
+ {
+ super.onStart();
+
+ setTheme(android.R.style.Theme_Black);
+ }
+}
+</pre>
+
+<a name="StringResources"></a>
+<h4>String Resources</h4>
+
+<p>String resources are defined using an XML resource description syntax.
+The file or multiple files containing these resources can be given any name
+(as long as it has a .xml suffix) and placed at an appropriate location in
+the source tree for the desired configuration (locale/orientation/density).
+
+<p>Here is a simple resource file describing a few strings:</p>
+
+<pre>
+&lt;?xml version="1.0" encoding="utf-8"?&gt;
+&lt;resources&gt;
+ &lt;string id="mainLabel"&gt;Hello &lt;u&gt;th&lt;ignore&gt;e&lt;/ignore&gt;re&lt;/u&gt;, &lt;i&gt;you&lt;/i&gt; &lt;b&gt;Activity&lt;/b&gt;!&lt;/string&gt;
+ &lt;string id="back"&gt;Back&lt;/string&gt;
+ &lt;string id="clear"&gt;Clear&lt;/string&gt;
+&lt;/resources&gt;
+</pre>
+
+<p>Typically this file will be called "strings.xml", and must be placed
+in the <code>values</code> directory:</p>
+
+<pre>
+MyApp/res/values/strings.xml
+</pre>
+
+<p>The strings can now be retrieved by your application through the
+symbol specified in the "id" attribute:</p>
+
+<pre class="prettyprint">
+public class MyActivity extends Activity
+{
+ public void onStart()
+ {
+ super.onStart();
+
+ String back = getResources().getString(R.string.back).toString();
+ back = getString(R.string.back).toString(); // synonym
+ }
+}
+</pre>
+
+<p>Unlike system resources, the resource symbol (the R class) we are using
+here comes from our own application's package, not android.R.</p>
+
+<p>Note that the "mainLabel" string is complex, including style information.
+To support this, the <code>getString()</code> method returns a
+<code>CharSequence</code> object that you can pass to a
+<code>TextView</code> to retain those style. This is why code
+must call <code>toString()</code> on the returned resource if it wants
+a raw string.</p>
+
+<a name="ColorResources"></a>
+<h4>Color Resources</h4>
+
+<p>Color resources are created in a way very similar to string resources,
+but with the &lt;color&gt; resource tag. The data for these resources
+must be a hex color constant of the form "#rgb", "#argb", "#rrggbb", or
+"#aarrggbb". The alpha channel is 0xff (or 0xf) for opaque and 0
+for transparent.</p>
+
+<pre>
+&lt;?xml version="1.0" encoding="utf-8"?&gt;
+&lt;resources&gt;
+ &lt;color id="opaque_red"&gt;#ffff0000&lt;/color&gt;
+ &lt;color id="transparent_red"&gt;#80ff0000&lt;/color&gt;
+ &lt;color id="opaque_blue"&gt;#0000ff&lt;/color&gt;
+ &lt;color id="opaque_green"&gt;#0f0&lt;/color&gt;
+&lt;/resources&gt;
+</pre>
+
+<p>While color definitions could be placed in the same resource file
+as the previously shown string data, usually you will place the colors in
+their own file:</p>
+
+<pre>
+MyApp/res/values/colors.xml
+</pre>
+
+<p>The colors can now be retrieved by your application through the
+symbol specified in the "id" attribute:</p>
+
+<pre class="prettyprint">
+public class MyActivity extends Activity
+{
+ public void onStart()
+ {
+ super.onStart();
+
+ int red = getResources().getColor(R.color.opaque_red);
+ }
+}
+</pre>
+
+<a name="DrawableResources"></a>
+<h4>Drawable Resources</h4>
+
+<p>For simple drawable resources, all you need to do is place your
+image in a special resource sub-directory called "drawable". Files here
+are things that can be handled by an implementation of the
+{@link android.graphics.drawable.Drawable Drawable} class, often bitmaps
+(such as PNG images) but also various kinds of XML descriptions
+for selectors, gradients, etc.</p>
+
+<p>The drawable files will be scanned by the
+resource tool, automatically generating a resource entry for each found.
+For example the file <code>res/drawable/&lt;myimage&gt;.&lt;ext&gt;</code>
+will result in a resource symbol named "myimage" (without the extension). Note
+that these file names <em>must</em> be valid Java identifiers, and should
+have only lower-case letters.</p>
+
+<p>For example, to use your own custom image as a badge in a screen,
+you can place the image here:</p>
+
+<pre>
+MyApp/res/drawable/my_badge.png
+</pre>
+
+<p>The image can then be used in your code like this:</p>
+
+<pre class="prettyprint">
+public class MyActivity extends Activity
+{
+ public void onStart()
+ {
+ requestScreenFeatures(FEATURE_BADGE_IMAGE);
+
+ super.onStart();
+
+ setBadgeResource(R.drawable.my_badge);
+ }
+}
+</pre>
+
+<p>For drawables that are a single solid color, you can also define them
+in a resource file very much like colors shown previously. The only
+difference is that here we use the &lt;drawable&gt; tag to create a
+drawable resource.</p>
+
+<pre>
+&lt;?xml version="1.0" encoding="utf-8"?&gt;
+&lt;resources&gt;
+ &lt;drawable id="opaque_red"&gt;#ffff0000&lt;/drawable&gt;
+ &lt;drawable id="transparent_red"&gt;#80ff0000&lt;/drawable&gt;
+ &lt;drawable id="opaque_blue"&gt;#0000ff&lt;/drawable&gt;
+ &lt;drawable id="opaque_green"&gt;#0f0&lt;/drawable&gt;
+&lt;/resources&gt;
+</pre>
+
+<p>These resource entries are often placed in the same resource file
+as color definitions:</p>
+
+<pre>
+MyApp/res/values/colors.xml
+</pre>
+
+<a name="LayoutResources"></a>
+<h4>Layout Resources</h4>
+
+<p>Layout resources describe a view hierarchy configuration that is
+generated at runtime. These resources are XML files placed in the
+resource directory "layout", and are how you should create the content
+views inside of your screen (instead of creating them by hand) so that
+they can be themed, styled, configured, and overlayed.</p>
+
+<p>Here is a simple layout resource consisting of a single view, a text
+editor:</p>
+
+<pre>
+&lt;?xml version="1.0" encoding="utf-8"?&gt;
+&lt;root&gt;
+ &lt;EditText id="text"
+ android:layout_width="fill-parent" android:layout_height="fill-parent"
+ android:text="Hello, World!" /&gt;
+&lt;/root&gt;
+</pre>
+
+<p>To use this layout, it can be placed in a file like this:</p>
+
+<pre>
+MyApp/res/layout/my_layout.xml
+</pre>
+
+<p>The layout can then be instantiated in your screen like this:</p>
+
+<pre class="prettyprint">
+public class MyActivity extends Activity
+{
+ public void onStart()
+ {
+ super.onStart();
+ setContentView(R.layout.my_layout);
+ }
+}
+</pre>
+
+<p>Note that there are a number of visual attributes that can be supplied
+to TextView (including textSize, textColor, and textStyle) that we did
+not define in the previous example; in such a sitation, the default values for
+those attributes come from the theme. If we want to customize them, we
+can supply them explicitly in the XML file:</p>
+
+<pre>
+&lt;?xml version="1.0" encoding="utf-8"?&gt;
+&lt;root&gt;
+ &lt;EditText id="text"
+ android:layout_width="fill_parent" android:layout_height="fill_parent"
+ <b>android:textSize="18" android:textColor="#008"</b>
+ android:text="Hello, World!" /&gt;
+&lt;/root&gt;
+</pre>
+
+<p>However, usually these kinds of attributes (those being attributes that
+usually make sense to vary with theme or overlay) should be defined through
+the theme or separate style resource. Later we will see how this is done.</p>
+
+<a name="ReferencesToResources"></a>
+<h4>References to Resources</h4>
+
+<p>A value supplied in an attribute (or resource) can also be a reference to
+a resource. This is often used in layout files to supply strings (so they
+can be localized) and images (which exist in another file), though a reference
+can be do any resource type including colors and integers.</p>
+
+<p>For example, if we have the previously defined color resources, we can
+write a layout file that sets the text color size to be the value contained in
+one of those resources:</p>
+
+<pre>
+&lt;?xml version="1.0" encoding="utf-8"?&gt;
+&lt;root&gt;
+ &lt;EditText id="text"
+ android:layout_width="fill_parent" android:layout_height="fill_parent"
+ <b>android:textColor="@color/opaque_red"</b>
+ android:text="Hello, World!" /&gt;
+&lt;/root&gt;
+</pre>
+
+<p>Note here the use of the '@' prefix to introduce a resource reference -- the
+text following that is the name of a resource in the form
+of <code>@[package:]type/name</code>. In this case we didn't need to specify
+the package because we are referencing a resource in our own package. To
+reference a system resource, you would need to write:</p>
+
+<pre>
+&lt;?xml version="1.0" encoding="utf-8"?&gt;
+&lt;root&gt;
+ &lt;EditText id="text"
+ android:layout_width="fill_parent" android:layout_height="fill_parent"
+ android:textColor="@<b>android:</b>color/opaque_red"
+ android:text="Hello, World!" /&gt;
+&lt;/root&gt;
+</pre>
+
+<p>As another example, you should always use resource references when supplying
+strings in a layout file so that they can be localized:</p>
+
+<pre>
+&lt;?xml version="1.0" encoding="utf-8"?&gt;
+&lt;root&gt;
+ &lt;EditText id="text"
+ android:layout_width="fill_parent" android:layout_height="fill_parent"
+ android:textColor="@android:color/opaque_red"
+ android:text="@string/hello_world" /&gt;
+&lt;/root&gt;
+</pre>
+
+<p>This facility can also be used to create references between resources.
+For example, we can create new drawable resources that are aliases for
+existing images:</p>
+
+<pre>
+&lt;?xml version="1.0" encoding="utf-8"?&gt;
+&lt;resources&gt;
+ &lt;drawable id="my_background"&gt;@android:drawable/theme2_background&lt;/drawable&gt;
+&lt;/resources&gt;
+</pre>
+
+<a name="ReferencesToThemeAttributes"></a>
+<h4>References to Theme Attributes</h4>
+
+<p>Another kind of resource value allows you to reference the value of an
+attribute in the current theme. This attribute reference can <em>only</em>
+be used in style resources and XML attributes; it allows you to customize the
+look of UI elements by changing them to standard variations supplied by the
+current theme, instead of supplying more concrete values.</p>
+
+<p>As an example, we can use this in our layout to set the text color to
+one of the standard colors defined in the base system theme:</p>
+
+<pre>
+&lt;?xml version="1.0" encoding="utf-8"?&gt;
+&lt;root&gt;
+ &lt;EditText id="text"
+ android:layout_width="fill_parent" android:layout_height="fill_parent"
+ <b>android:textColor="?android:textDisabledColor"</b>
+ android:text="@string/hello_world" /&gt;
+&lt;/root&gt;
+</pre>
+
+<p>Note that this is very similar to a resource reference, except we are using
+an '?' prefix instead of '@'. When you use this markup, you are supplying
+the name of an attribute resource that will be looked up in the theme --
+because the resource tool knows that an attribute resource is expected,
+you do not need to explicitly state the type (which would be
+<code>?android:attr/android:textDisabledColor</code>).</p>
+
+<p>Other than using this resource identifier to find the value in the
+theme instead of raw resources, the name syntax is identical to the '@' format:
+<code>?[package:]type/name</code> with the type here being optional.</p>
+
+<a name="StyleResources"></a>
+<h4>Style Resources</h4>
+
+<p>A style resource is a set of name/value pairs describing a group
+of related attributes. There are two main uses for these resources:
+defining overall visual themes, and describing a set of visual attributes
+to apply to a class in a layout resource. In this section we will look
+at their use to describe themes; later we will look at using them in
+conjunction with layouts.</p>
+
+<p>Like strings, styles are defined through a resource XML file. In the
+situation where we want to define a new theme, we can create a custom theme
+style that inherits from one of the standard system themes:</p>
+
+<pre>
+&lt;?xml version="1.0" encoding="utf-8"?&gt;
+&lt;resources&gt;
+ &lt;style id="Theme" parent="android:Theme.White"&gt;
+ &lt;item id="android:foregroundColor"&gt;#FFF8D96F&lt;/item&gt;
+ &lt;item id="android:textColor"&gt;@color/opaque_blue&lt;/item&gt;
+ &lt;item id="android:textSelectedColor"&gt;?android:textColor&lt;/item&gt;
+ &lt;/style&gt;
+&lt;/resources&gt;
+</pre>
+
+<p>Typically these resource definitions will be placed in a file
+called "styles.xml" , and must be placed in the <code>values</code>
+directory:</p>
+
+<pre>
+MyApp/res/values/styles.xml
+</pre>
+
+<p>Similar to how we previously used a system style for an Activity theme,
+you can apply this style to your Activity:</p>
+
+<pre class="prettyprint">
+public class MyActivity extends Activity
+{
+ public void onStart()
+ {
+ super.onStart();
+
+ setTheme(R.style.Theme);
+ }
+}
+</pre>
+
+<p>In the style resource shown here, we used the <code>parent</code>
+attribute to specify another style resource from which it inherits
+its values -- in this case the <code>Theme.White</code> system resource:</p>
+
+<pre>
+ &lt;style id="Home" parent="android:Theme.White"&gt;
+ ...
+ &lt;/style&gt;
+</pre>
+
+<p>Note, when doing this, that you must use the "android" prefix in front
+to tell the compiler the namespace to look in for the resource --
+the resources you are specifying here are in your application's namespace,
+not the system. This explicit namespace specification ensures that names
+the application uses will not accidentally conflict with those defined by
+the system.</p>
+
+<p>If you don't specify an explicit parent style, it will be inferred
+from the style name -- everything before the final '.' in the name of the
+style being defined is taken as the parent style name. Thus, to make
+another style in your application that inherits from this base Theme style,
+you can write:</p>
+
+<pre>
+&lt;?xml version="1.0" encoding="utf-8"?&gt;
+&lt;resources&gt;
+ &lt;style id="Theme.WhiteText"&gt;
+ &lt;item id="android:foregroundColor"&gt;#FFFFFFFF&lt;/item&gt;
+ &lt;item id="android:textColor"&gt;?android:foregroundColor&lt;/item&gt;
+ &lt;/style&gt;
+&lt;/resources&gt;
+</pre>
+
+<p>This results in the symbol <code>R.style.Theme_WhiteText</code> that
+can be used in Java just like we did with <code>R.style.Theme</code>
+above.</p>
+
+<a name="StylesInLayoutResources"></a>
+<h4>Styles in Layout Resources</h4>
+
+<p>Often you will have a number fo views in a layout that all use the same
+set of attributes, or want to allow resource overlays to modify the values of
+attributes. Style resources can be used for both of these purposes, to put
+attribute definitions in a single place that can be references by multiple
+XML tags and modified by overlays. To do this, you simply define a
+new style resource with the desired values:</p>
+
+<pre>
+&lt;?xml version="1.0" encoding="utf-8"?&gt;
+&lt;resources&gt;
+ &lt;style id="SpecialText"&gt;
+ &lt;item id="android:textSize"&gt;18&lt;/item&gt;
+ &lt;item id="android:textColor"&gt;#008&lt;/item&gt;
+ &lt;/style&gt;
+&lt;/resources&gt;
+</pre>
+
+<p>You can now apply this style to your TextView in the XML file:</p>
+
+<pre>
+&lt;?xml version="1.0" encoding="utf-8"?&gt;
+&lt;root&gt;
+ &lt;EditText id="text1" <b>style="@style/SpecialText"</b>
+ android:layout_width="fill_parent" android:layout_height="wrap_content"
+ android:text="Hello, World!" /&gt;
+ &lt;EditText id="text2" <b>style="@style/SpecialText"</b>
+ android:layout_width="fill_parent" android:layout_height="wrap_content"
+ android:text="I love you all." /&gt;
+&lt;/root&gt;</pre>
+<h4>&nbsp;</h4>
+
+</body>
+</html>
+
diff --git a/core/java/android/content/pm/ActivityInfo.aidl b/core/java/android/content/pm/ActivityInfo.aidl
new file mode 100755
index 0000000..dd90302
--- /dev/null
+++ b/core/java/android/content/pm/ActivityInfo.aidl
@@ -0,0 +1,20 @@
+/* //device/java/android/android/view/WindowManager.aidl
+**
+** Copyright 2007, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.content.pm;
+
+parcelable ActivityInfo;
diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java
new file mode 100644
index 0000000..f577d2d
--- /dev/null
+++ b/core/java/android/content/pm/ActivityInfo.java
@@ -0,0 +1,332 @@
+package android.content.pm;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Printer;
+
+/**
+ * Information you can retrieve about a particular application
+ * activity or receiver. This corresponds to information collected
+ * from the AndroidManifest.xml's &lt;activity&gt; and
+ * &lt;receiver&gt; tags.
+ */
+public class ActivityInfo extends ComponentInfo
+ implements Parcelable {
+ /**
+ * A style resource identifier (in the package's resources) of this
+ * activity's theme. From the "theme" attribute or, if not set, 0.
+ */
+ public int theme;
+
+ /**
+ * Constant corresponding to <code>standard</code> in
+ * the {@link android.R.attr#launchMode} attribute.
+ */
+ public static final int LAUNCH_MULTIPLE = 0;
+ /**
+ * Constant corresponding to <code>singleTop</code> in
+ * the {@link android.R.attr#launchMode} attribute.
+ */
+ public static final int LAUNCH_SINGLE_TOP = 1;
+ /**
+ * Constant corresponding to <code>singleTask</code> in
+ * the {@link android.R.attr#launchMode} attribute.
+ */
+ public static final int LAUNCH_SINGLE_TASK = 2;
+ /**
+ * Constant corresponding to <code>singleInstance</code> in
+ * the {@link android.R.attr#launchMode} attribute.
+ */
+ public static final int LAUNCH_SINGLE_INSTANCE = 3;
+ /**
+ * The launch mode style requested by the activity. From the
+ * {@link android.R.attr#launchMode} attribute, one of
+ * {@link #LAUNCH_MULTIPLE},
+ * {@link #LAUNCH_SINGLE_TOP}, {@link #LAUNCH_SINGLE_TASK}, or
+ * {@link #LAUNCH_SINGLE_INSTANCE}.
+ */
+ public int launchMode;
+
+ /**
+ * Optional name of a permission required to be able to access this
+ * Activity. From the "permission" attribute.
+ */
+ public String permission;
+
+ /**
+ * The affinity this activity has for another task in the system. The
+ * string here is the name of the task, often the package name of the
+ * overall package. If null, the activity has no affinity. Set from the
+ * {@link android.R.attr#taskAffinity} attribute.
+ */
+ public String taskAffinity;
+
+ /**
+ * If this is an activity alias, this is the real activity class to run
+ * for it. Otherwise, this is null.
+ */
+ public String targetActivity;
+
+ /**
+ * Bit in {@link #flags} indicating whether this activity is able to
+ * run in multiple processes. If
+ * true, the system may instantiate it in the some process as the
+ * process starting it in order to conserve resources. If false, the
+ * default, it always runs in {@link #processName}. Set from the
+ * {@link android.R.attr#multiprocess} attribute.
+ */
+ public static final int FLAG_MULTIPROCESS = 0x0001;
+ /**
+ * Bit in {@link #flags} indicating that, when the activity's task is
+ * relaunched from home, this activity should be finished.
+ * Set from the
+ * {@link android.R.attr#finishOnTaskLaunch} attribute.
+ */
+ public static final int FLAG_FINISH_ON_TASK_LAUNCH = 0x0002;
+ /**
+ * Bit in {@link #flags} indicating that, when the activity is the root
+ * of a task, that task's stack should be cleared each time the user
+ * re-launches it from home. As a result, the user will always
+ * return to the original activity at the top of the task.
+ * This flag only applies to activities that
+ * are used to start the root of a new task. Set from the
+ * {@link android.R.attr#clearTaskOnLaunch} attribute.
+ */
+ public static final int FLAG_CLEAR_TASK_ON_LAUNCH = 0x0004;
+ /**
+ * Bit in {@link #flags} indicating that, when the activity is the root
+ * of a task, that task's stack should never be cleared when it is
+ * relaunched from home. Set from the
+ * {@link android.R.attr#alwaysRetainTaskState} attribute.
+ */
+ public static final int FLAG_ALWAYS_RETAIN_TASK_STATE = 0x0008;
+ /**
+ * Bit in {@link #flags} indicating that the activity's state
+ * is not required to be saved, so that if there is a failure the
+ * activity will not be removed from the activity stack. Set from the
+ * {@link android.R.attr#stateNotNeeded} attribute.
+ */
+ public static final int FLAG_STATE_NOT_NEEDED = 0x0010;
+ /**
+ * Bit in {@link #flags} that indicates that the activity should not
+ * appear in the list of recently launched activities. Set from the
+ * {@link android.R.attr#excludeFromRecents} attribute.
+ */
+ public static final int FLAG_EXCLUDE_FROM_RECENTS = 0x0020;
+ /**
+ * Bit in {@link #flags} that indicates that the activity can be moved
+ * between tasks based on its task affinity. Set from the
+ * {@link android.R.attr#allowTaskReparenting} attribute.
+ */
+ public static final int FLAG_ALLOW_TASK_REPARENTING = 0x0040;
+ /**
+ * Options that have been set in the activity declaration in the
+ * manifest: {@link #FLAG_MULTIPROCESS},
+ * {@link #FLAG_FINISH_ON_TASK_LAUNCH}, {@link #FLAG_CLEAR_TASK_ON_LAUNCH},
+ * {@link #FLAG_ALWAYS_RETAIN_TASK_STATE},
+ * {@link #FLAG_STATE_NOT_NEEDED}, {@link #FLAG_EXCLUDE_FROM_RECENTS},
+ * {@link #FLAG_ALLOW_TASK_REPARENTING}.
+ */
+ public int flags;
+
+ /**
+ * Constant corresponding to <code>unspecified</code> in
+ * the {@link android.R.attr#screenOrientation} attribute.
+ */
+ public static final int SCREEN_ORIENTATION_UNSPECIFIED = -1;
+ /**
+ * Constant corresponding to <code>landscape</code> in
+ * the {@link android.R.attr#screenOrientation} attribute.
+ */
+ public static final int SCREEN_ORIENTATION_LANDSCAPE = 0;
+ /**
+ * Constant corresponding to <code>portrait</code> in
+ * the {@link android.R.attr#screenOrientation} attribute.
+ */
+ public static final int SCREEN_ORIENTATION_PORTRAIT = 1;
+ /**
+ * Constant corresponding to <code>user</code> in
+ * the {@link android.R.attr#screenOrientation} attribute.
+ */
+ public static final int SCREEN_ORIENTATION_USER = 2;
+ /**
+ * Constant corresponding to <code>behind</code> in
+ * the {@link android.R.attr#screenOrientation} attribute.
+ */
+ public static final int SCREEN_ORIENTATION_BEHIND = 3;
+ /**
+ * Constant corresponding to <code>sensor</code> in
+ * the {@link android.R.attr#screenOrientation} attribute.
+ */
+ public static final int SCREEN_ORIENTATION_SENSOR = 4;
+
+ /**
+ * Constant corresponding to <code>sensor</code> in
+ * the {@link android.R.attr#screenOrientation} attribute.
+ */
+ public static final int SCREEN_ORIENTATION_NOSENSOR = 5;
+ /**
+ * The preferred screen orientation this activity would like to run in.
+ * From the {@link android.R.attr#screenOrientation} attribute, one of
+ * {@link #SCREEN_ORIENTATION_UNSPECIFIED},
+ * {@link #SCREEN_ORIENTATION_LANDSCAPE},
+ * {@link #SCREEN_ORIENTATION_PORTRAIT},
+ * {@link #SCREEN_ORIENTATION_USER},
+ * {@link #SCREEN_ORIENTATION_BEHIND},
+ * {@link #SCREEN_ORIENTATION_SENSOR},
+ * {@link #SCREEN_ORIENTATION_NOSENSOR}.
+ */
+ public int screenOrientation = SCREEN_ORIENTATION_UNSPECIFIED;
+
+ /**
+ * Bit in {@link #configChanges} that indicates that the activity
+ * can itself handle changes to the IMSI MCC. Set from the
+ * {@link android.R.attr#configChanges} attribute.
+ */
+ public static final int CONFIG_MCC = 0x0001;
+ /**
+ * Bit in {@link #configChanges} that indicates that the activity
+ * can itself handle changes to the IMSI MNC. Set from the
+ * {@link android.R.attr#configChanges} attribute.
+ */
+ public static final int CONFIG_MNC = 0x0002;
+ /**
+ * Bit in {@link #configChanges} that indicates that the activity
+ * can itself handle changes to the locale. Set from the
+ * {@link android.R.attr#configChanges} attribute.
+ */
+ public static final int CONFIG_LOCALE = 0x0004;
+ /**
+ * Bit in {@link #configChanges} that indicates that the activity
+ * can itself handle changes to the touchscreen type. Set from the
+ * {@link android.R.attr#configChanges} attribute.
+ */
+ public static final int CONFIG_TOUCHSCREEN = 0x0008;
+ /**
+ * Bit in {@link #configChanges} that indicates that the activity
+ * can itself handle changes to the keyboard type. Set from the
+ * {@link android.R.attr#configChanges} attribute.
+ */
+ public static final int CONFIG_KEYBOARD = 0x0010;
+ /**
+ * Bit in {@link #configChanges} that indicates that the activity
+ * can itself handle changes to the keyboard being hidden/exposed.
+ * Set from the {@link android.R.attr#configChanges} attribute.
+ */
+ public static final int CONFIG_KEYBOARD_HIDDEN = 0x0020;
+ /**
+ * Bit in {@link #configChanges} that indicates that the activity
+ * can itself handle changes to the navigation type. Set from the
+ * {@link android.R.attr#configChanges} attribute.
+ */
+ public static final int CONFIG_NAVIGATION = 0x0040;
+ /**
+ * Bit in {@link #configChanges} that indicates that the activity
+ * can itself handle changes to the screen orientation. Set from the
+ * {@link android.R.attr#configChanges} attribute.
+ */
+ public static final int CONFIG_ORIENTATION = 0x0080;
+ /**
+ * Bit in {@link #configChanges} that indicates that the activity
+ * can itself handle changes to the font scaling factor. Set from the
+ * {@link android.R.attr#configChanges} attribute. This is
+ * not a core resource configutation, but a higher-level value, so its
+ * constant starts at the high bits.
+ */
+ public static final int CONFIG_FONT_SCALE = 0x40000000;
+
+ /**
+ * Bit mask of kinds of configuration changes that this activity
+ * can handle itself (without being restarted by the system).
+ * Contains any combination of {@link #CONFIG_FONT_SCALE},
+ * {@link #CONFIG_MCC}, {@link #CONFIG_MNC},
+ * {@link #CONFIG_LOCALE}, {@link #CONFIG_TOUCHSCREEN},
+ * {@link #CONFIG_KEYBOARD}, {@link #CONFIG_NAVIGATION}, and
+ * {@link #CONFIG_ORIENTATION}. Set from the
+ * {@link android.R.attr#configChanges} attribute.
+ */
+ public int configChanges;
+
+ public ActivityInfo() {
+ }
+
+ public ActivityInfo(ActivityInfo orig) {
+ super(orig);
+ theme = orig.theme;
+ launchMode = orig.launchMode;
+ permission = orig.permission;
+ taskAffinity = orig.taskAffinity;
+ targetActivity = orig.targetActivity;
+ flags = orig.flags;
+ screenOrientation = orig.screenOrientation;
+ configChanges = orig.configChanges;
+ }
+
+ /**
+ * Return the theme resource identifier to use for this activity. If
+ * the activity defines a theme, that is used; else, the application
+ * theme is used.
+ *
+ * @return The theme associated with this activity.
+ */
+ public final int getThemeResource() {
+ return theme != 0 ? theme : applicationInfo.theme;
+ }
+
+ public void dump(Printer pw, String prefix) {
+ super.dumpFront(pw, prefix);
+ pw.println(prefix + "permission=" + permission);
+ pw.println(prefix + "taskAffinity=" + taskAffinity
+ + " targetActivity=" + targetActivity);
+ pw.println(prefix + "launchMode=" + launchMode
+ + " flags=0x" + Integer.toHexString(flags)
+ + " theme=0x" + Integer.toHexString(theme)
+ + " orien=" + screenOrientation
+ + " configChanges=0x" + Integer.toHexString(configChanges));
+ super.dumpBack(pw, prefix);
+ }
+
+ public String toString() {
+ return "ActivityInfo{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " " + name + "}";
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int parcelableFlags) {
+ super.writeToParcel(dest, parcelableFlags);
+ dest.writeInt(theme);
+ dest.writeInt(launchMode);
+ dest.writeString(permission);
+ dest.writeString(taskAffinity);
+ dest.writeString(targetActivity);
+ dest.writeInt(flags);
+ dest.writeInt(screenOrientation);
+ dest.writeInt(configChanges);
+ }
+
+ public static final Parcelable.Creator<ActivityInfo> CREATOR
+ = new Parcelable.Creator<ActivityInfo>() {
+ public ActivityInfo createFromParcel(Parcel source) {
+ return new ActivityInfo(source);
+ }
+ public ActivityInfo[] newArray(int size) {
+ return new ActivityInfo[size];
+ }
+ };
+
+ private ActivityInfo(Parcel source) {
+ super(source);
+ theme = source.readInt();
+ launchMode = source.readInt();
+ permission = source.readString();
+ taskAffinity = source.readString();
+ targetActivity = source.readString();
+ flags = source.readInt();
+ screenOrientation = source.readInt();
+ configChanges = source.readInt();
+ }
+}
diff --git a/core/java/android/content/pm/ApplicationInfo.aidl b/core/java/android/content/pm/ApplicationInfo.aidl
new file mode 100755
index 0000000..006d1bd
--- /dev/null
+++ b/core/java/android/content/pm/ApplicationInfo.aidl
@@ -0,0 +1,20 @@
+/* //device/java/android/android/view/WindowManager.aidl
+**
+** Copyright 2007, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.content.pm;
+
+parcelable ApplicationInfo;
diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java
new file mode 100644
index 0000000..22d01dc
--- /dev/null
+++ b/core/java/android/content/pm/ApplicationInfo.java
@@ -0,0 +1,303 @@
+package android.content.pm;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Printer;
+
+import java.text.Collator;
+import java.util.Comparator;
+
+/**
+ * Information you can retrieve about a particular application. This
+ * corresponds to information collected from the AndroidManifest.xml's
+ * &lt;application&gt; tag.
+ */
+public class ApplicationInfo extends PackageItemInfo implements Parcelable {
+
+ /**
+ * Default task affinity of all activities in this application. See
+ * {@link ActivityInfo#taskAffinity} for more information. This comes
+ * from the "taskAffinity" attribute.
+ */
+ public String taskAffinity;
+
+ /**
+ * Optional name of a permission required to be able to access this
+ * application's components. From the "permission" attribute.
+ */
+ public String permission;
+
+ /**
+ * The name of the process this application should run in. From the
+ * "process" attribute or, if not set, the same as
+ * <var>packageName</var>.
+ */
+ public String processName;
+
+ /**
+ * Class implementing the Application object. From the "class"
+ * attribute.
+ */
+ public String className;
+
+ /**
+ * A style resource identifier (in the package's resources) of the
+ * description of an application. From the "description" attribute
+ * or, if not set, 0.
+ */
+ public int descriptionRes;
+
+ /**
+ * A style resource identifier (in the package's resources) of the
+ * default visual theme of the application. From the "theme" attribute
+ * or, if not set, 0.
+ */
+ public int theme;
+
+ /**
+ * Class implementing the Application's manage space
+ * functionality. From the "manageSpaceActivity"
+ * attribute. This is an optional attribute and will be null if
+ * application's dont specify it in their manifest
+ */
+ public String manageSpaceActivityName;
+
+ /**
+ * Value for {@link #flags}: if set, this application is installed in the
+ * device's system image.
+ */
+ public static final int FLAG_SYSTEM = 1<<0;
+
+ /**
+ * Value for {@link #flags}: set to true if this application would like to
+ * allow debugging of its
+ * code, even when installed on a non-development system. Comes
+ * from {@link android.R.styleable#AndroidManifestApplication_debuggable
+ * android:debuggable} of the &lt;application&gt; tag.
+ */
+ public static final int FLAG_DEBUGGABLE = 1<<1;
+
+ /**
+ * Value for {@link #flags}: set to true if this application has code
+ * associated with it. Comes
+ * from {@link android.R.styleable#AndroidManifestApplication_hasCode
+ * android:hasCode} of the &lt;application&gt; tag.
+ */
+ public static final int FLAG_HAS_CODE = 1<<2;
+
+ /**
+ * Value for {@link #flags}: set to true if this application is persistent.
+ * Comes from {@link android.R.styleable#AndroidManifestApplication_persistent
+ * android:persistent} of the &lt;application&gt; tag.
+ */
+ public static final int FLAG_PERSISTENT = 1<<3;
+
+ /**
+ * Value for {@link #flags}: set to true iif this application holds the
+ * {@link android.Manifest.permission#FACTORY_TEST} permission and the
+ * device is running in factory test mode.
+ */
+ public static final int FLAG_FACTORY_TEST = 1<<4;
+
+ /**
+ * Value for {@link #flags}: default value for the corresponding ActivityInfo flag.
+ * Comes from {@link android.R.styleable#AndroidManifestApplication_allowTaskReparenting
+ * android:allowTaskReparenting} of the &lt;application&gt; tag.
+ */
+ public static final int FLAG_ALLOW_TASK_REPARENTING = 1<<5;
+
+ /**
+ * Value for {@link #flags}: default value for the corresponding ActivityInfo flag.
+ * Comes from {@link android.R.styleable#AndroidManifestApplication_allowClearUserData
+ * android:allowClearUserData} of the &lt;application&gt; tag.
+ */
+ public static final int FLAG_ALLOW_CLEAR_USER_DATA = 1<<6;
+
+ /**
+ * Flags associated with the application. Any combination of
+ * {@link #FLAG_SYSTEM}, {@link #FLAG_DEBUGGABLE}, {@link #FLAG_HAS_CODE},
+ * {@link #FLAG_PERSISTENT}, {@link #FLAG_FACTORY_TEST}, and
+ * {@link #FLAG_ALLOW_TASK_REPARENTING}
+ * {@link #FLAG_ALLOW_CLEAR_USER_DATA}.
+ */
+ public int flags = 0;
+
+ /**
+ * Full path to the location of this package.
+ */
+ public String sourceDir;
+
+ /**
+ * Full path to the location of the publicly available parts of this package (i.e. the resources
+ * and manifest). For non-forward-locked apps this will be the same as {@link #sourceDir).
+ */
+ public String publicSourceDir;
+
+ /**
+ * Paths to all shared libraries this application is linked against. This
+ * field is only set if the {@link PackageManager#GET_SHARED_LIBRARY_FILES
+ * PackageManager.GET_SHARED_LIBRARY_FILES} flag was used when retrieving
+ * the structure.
+ */
+ public String[] sharedLibraryFiles;
+
+ /**
+ * Full path to a directory assigned to the package for its persistent
+ * data.
+ */
+ public String dataDir;
+
+ /**
+ * The kernel user-ID that has been assigned to this application;
+ * currently this is not a unique ID (multiple applications can have
+ * the same uid).
+ */
+ public int uid;
+
+ /**
+ * When false, indicates that all components within this application are
+ * considered disabled, regardless of their individually set enabled status.
+ */
+ public boolean enabled = true;
+
+ public void dump(Printer pw, String prefix) {
+ super.dumpFront(pw, prefix);
+ pw.println(prefix + "className=" + className);
+ pw.println(prefix + "permission=" + permission
+ + " uid=" + uid);
+ pw.println(prefix + "taskAffinity=" + taskAffinity);
+ pw.println(prefix + "theme=0x" + Integer.toHexString(theme));
+ pw.println(prefix + "flags=0x" + Integer.toHexString(flags)
+ + " processName=" + processName);
+ pw.println(prefix + "sourceDir=" + sourceDir);
+ pw.println(prefix + "publicSourceDir=" + publicSourceDir);
+ pw.println(prefix + "sharedLibraryFiles=" + sharedLibraryFiles);
+ pw.println(prefix + "dataDir=" + dataDir);
+ pw.println(prefix + "enabled=" + enabled);
+ pw.println(prefix+"manageSpaceActivityName="+manageSpaceActivityName);
+ pw.println(prefix+"description=0x"+Integer.toHexString(descriptionRes));
+ super.dumpBack(pw, prefix);
+ }
+
+ public static class DisplayNameComparator
+ implements Comparator<ApplicationInfo> {
+ public DisplayNameComparator(PackageManager pm) {
+ mPM = pm;
+ }
+
+ public final int compare(ApplicationInfo aa, ApplicationInfo ab) {
+ CharSequence sa = mPM.getApplicationLabel(aa);
+ if (sa == null) {
+ sa = aa.packageName;
+ }
+ CharSequence sb = mPM.getApplicationLabel(ab);
+ if (sb == null) {
+ sb = ab.packageName;
+ }
+
+ return sCollator.compare(sa, sb);
+ }
+
+ private final Collator sCollator = Collator.getInstance();
+ private PackageManager mPM;
+ }
+
+ public ApplicationInfo() {
+ }
+
+ public ApplicationInfo(ApplicationInfo orig) {
+ super(orig);
+ taskAffinity = orig.taskAffinity;
+ permission = orig.permission;
+ processName = orig.processName;
+ className = orig.className;
+ theme = orig.theme;
+ flags = orig.flags;
+ sourceDir = orig.sourceDir;
+ publicSourceDir = orig.publicSourceDir;
+ sharedLibraryFiles = orig.sharedLibraryFiles;
+ dataDir = orig.dataDir;
+ uid = orig.uid;
+ enabled = orig.enabled;
+ manageSpaceActivityName = orig.manageSpaceActivityName;
+ descriptionRes = orig.descriptionRes;
+ }
+
+
+ public String toString() {
+ return "ApplicationInfo{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " " + packageName + "}";
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int parcelableFlags) {
+ super.writeToParcel(dest, parcelableFlags);
+ dest.writeString(taskAffinity);
+ dest.writeString(permission);
+ dest.writeString(processName);
+ dest.writeString(className);
+ dest.writeInt(theme);
+ dest.writeInt(flags);
+ dest.writeString(sourceDir);
+ dest.writeString(publicSourceDir);
+ dest.writeStringArray(sharedLibraryFiles);
+ dest.writeString(dataDir);
+ dest.writeInt(uid);
+ dest.writeInt(enabled ? 1 : 0);
+ dest.writeString(manageSpaceActivityName);
+ dest.writeInt(descriptionRes);
+ }
+
+ public static final Parcelable.Creator<ApplicationInfo> CREATOR
+ = new Parcelable.Creator<ApplicationInfo>() {
+ public ApplicationInfo createFromParcel(Parcel source) {
+ return new ApplicationInfo(source);
+ }
+ public ApplicationInfo[] newArray(int size) {
+ return new ApplicationInfo[size];
+ }
+ };
+
+ private ApplicationInfo(Parcel source) {
+ super(source);
+ taskAffinity = source.readString();
+ permission = source.readString();
+ processName = source.readString();
+ className = source.readString();
+ theme = source.readInt();
+ flags = source.readInt();
+ sourceDir = source.readString();
+ publicSourceDir = source.readString();
+ sharedLibraryFiles = source.readStringArray();
+ dataDir = source.readString();
+ uid = source.readInt();
+ enabled = source.readInt() != 0;
+ manageSpaceActivityName = source.readString();
+ descriptionRes = source.readInt();
+ }
+
+ /**
+ * Retrieve the textual description of the application. This
+ * will call back on the given PackageManager to load the description from
+ * the application.
+ *
+ * @param pm A PackageManager from which the label can be loaded; usually
+ * the PackageManager from which you originally retrieved this item.
+ *
+ * @return Returns a CharSequence containing the application's description.
+ * If there is no description, null is returned.
+ */
+ public CharSequence loadDescription(PackageManager pm) {
+ if (descriptionRes != 0) {
+ CharSequence label = pm.getText(packageName, descriptionRes, null);
+ if (label != null) {
+ return label;
+ }
+ }
+ return null;
+ }
+}
diff --git a/core/java/android/content/pm/ComponentInfo.java b/core/java/android/content/pm/ComponentInfo.java
new file mode 100644
index 0000000..73c9244
--- /dev/null
+++ b/core/java/android/content/pm/ComponentInfo.java
@@ -0,0 +1,138 @@
+package android.content.pm;
+
+import android.graphics.drawable.Drawable;
+import android.os.Parcel;
+import android.util.Printer;
+
+/**
+ * Base class containing information common to all application components
+ * ({@link ActivityInfo}, {@link ServiceInfo}). This class is not intended
+ * to be used by itself; it is simply here to share common definitions
+ * between all application components. As such, it does not itself
+ * implement Parcelable, but does provide convenience methods to assist
+ * in the implementation of Parcelable in subclasses.
+ */
+public class ComponentInfo extends PackageItemInfo {
+ /**
+ * Global information about the application/package this component is a
+ * part of.
+ */
+ public ApplicationInfo applicationInfo;
+
+ /**
+ * The name of the process this component should run in.
+ * From the "android:process" attribute or, if not set, the same
+ * as <var>applicationInfo.processName</var>.
+ */
+ public String processName;
+
+ /**
+ * Indicates whether or not this component may be instantiated. Note that this value can be
+ * overriden by the one in its parent {@link ApplicationInfo}.
+ */
+ public boolean enabled = true;
+
+ /**
+ * Set to true if this component is available for use by other applications.
+ * Comes from {@link android.R.attr#exported android:exported} of the
+ * &lt;activity&gt;, &lt;receiver&gt;, &lt;service&gt;, or
+ * &lt;provider&gt; tag.
+ */
+ public boolean exported = false;
+
+ public ComponentInfo() {
+ }
+
+ public ComponentInfo(ComponentInfo orig) {
+ super(orig);
+ applicationInfo = orig.applicationInfo;
+ processName = orig.processName;
+ enabled = orig.enabled;
+ exported = orig.exported;
+ }
+
+ @Override public CharSequence loadLabel(PackageManager pm) {
+ if (nonLocalizedLabel != null) {
+ return nonLocalizedLabel;
+ }
+ ApplicationInfo ai = applicationInfo;
+ CharSequence label;
+ if (labelRes != 0) {
+ label = pm.getText(packageName, labelRes, ai);
+ if (label != null) {
+ return label;
+ }
+ }
+ if (ai.nonLocalizedLabel != null) {
+ return ai.nonLocalizedLabel;
+ }
+ if (ai.labelRes != 0) {
+ label = pm.getText(packageName, ai.labelRes, ai);
+ if (label != null) {
+ return label;
+ }
+ }
+ return name;
+ }
+
+ @Override public Drawable loadIcon(PackageManager pm) {
+ ApplicationInfo ai = applicationInfo;
+ Drawable dr;
+ if (icon != 0) {
+ dr = pm.getDrawable(packageName, icon, ai);
+ if (dr != null) {
+ return dr;
+ }
+ }
+ if (ai.icon != 0) {
+ dr = pm.getDrawable(packageName, ai.icon, ai);
+ if (dr != null) {
+ return dr;
+ }
+ }
+ return pm.getDefaultActivityIcon();
+ }
+
+ /**
+ * Return the icon resource identifier to use for this component. If
+ * the component defines an icon, that is used; else, the application
+ * icon is used.
+ *
+ * @return The icon associated with this component.
+ */
+ public final int getIconResource() {
+ return icon != 0 ? icon : applicationInfo.icon;
+ }
+
+ protected void dumpFront(Printer pw, String prefix) {
+ super.dumpFront(pw, prefix);
+ pw.println(prefix + "enabled=" + enabled + " exported=" + exported
+ + " processName=" + processName);
+ }
+
+ protected void dumpBack(Printer pw, String prefix) {
+ if (applicationInfo != null) {
+ pw.println(prefix + "ApplicationInfo:");
+ applicationInfo.dump(pw, prefix + " ");
+ } else {
+ pw.println(prefix + "ApplicationInfo: null");
+ }
+ super.dumpBack(pw, prefix);
+ }
+
+ public void writeToParcel(Parcel dest, int parcelableFlags) {
+ super.writeToParcel(dest, parcelableFlags);
+ applicationInfo.writeToParcel(dest, parcelableFlags);
+ dest.writeString(processName);
+ dest.writeInt(enabled ? 1 : 0);
+ dest.writeInt(exported ? 1 : 0);
+ }
+
+ protected ComponentInfo(Parcel source) {
+ super(source);
+ applicationInfo = ApplicationInfo.CREATOR.createFromParcel(source);
+ processName = source.readString();
+ enabled = (source.readInt() != 0);
+ exported = (source.readInt() != 0);
+ }
+}
diff --git a/core/java/android/content/pm/IPackageDataObserver.aidl b/core/java/android/content/pm/IPackageDataObserver.aidl
new file mode 100755
index 0000000..d010ee4
--- /dev/null
+++ b/core/java/android/content/pm/IPackageDataObserver.aidl
@@ -0,0 +1,28 @@
+/*
+**
+** Copyright 2007, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.content.pm;
+
+/**
+ * API for package data change related callbacks from the Package Manager.
+ * Some usage scenarios include deletion of cache directory, generate
+ * statistics related to code, data, cache usage(TODO)
+ * {@hide}
+ */
+oneway interface IPackageDataObserver {
+ void onRemoveCompleted(in String packageName, boolean succeeded);
+}
diff --git a/core/java/android/content/pm/IPackageDeleteObserver.aidl b/core/java/android/content/pm/IPackageDeleteObserver.aidl
new file mode 100644
index 0000000..bc16b3e
--- /dev/null
+++ b/core/java/android/content/pm/IPackageDeleteObserver.aidl
@@ -0,0 +1,28 @@
+/*
+**
+** Copyright 2007, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.content.pm;
+
+/**
+ * API for deletion callbacks from the Package Manager.
+ *
+ * {@hide}
+ */
+oneway interface IPackageDeleteObserver {
+ void packageDeleted(in boolean succeeded);
+}
+
diff --git a/core/java/android/content/pm/IPackageInstallObserver.aidl b/core/java/android/content/pm/IPackageInstallObserver.aidl
new file mode 100644
index 0000000..e83bbc6
--- /dev/null
+++ b/core/java/android/content/pm/IPackageInstallObserver.aidl
@@ -0,0 +1,27 @@
+/*
+**
+** Copyright 2007, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.content.pm;
+
+/**
+ * API for installation callbacks from the Package Manager.
+ *
+ */
+oneway interface IPackageInstallObserver {
+ void packageInstalled(in String packageName, int returnCode);
+}
+
diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl
new file mode 100644
index 0000000..c79655d
--- /dev/null
+++ b/core/java/android/content/pm/IPackageManager.aidl
@@ -0,0 +1,230 @@
+/*
+**
+** Copyright 2007, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.content.pm;
+
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageInstallObserver;
+import android.content.pm.IPackageDeleteObserver;
+import android.content.pm.IPackageDataObserver;
+import android.content.pm.IPackageStatsObserver;
+import android.content.pm.InstrumentationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.ProviderInfo;
+import android.content.pm.PermissionGroupInfo;
+import android.content.pm.PermissionInfo;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.net.Uri;
+
+/**
+ * See {@link PackageManager} for documentation on most of the APIs
+ * here.
+ *
+ * {@hide}
+ */
+interface IPackageManager {
+ PackageInfo getPackageInfo(String packageName, int flags);
+ int getPackageUid(String packageName);
+ int[] getPackageGids(String packageName);
+
+ PermissionInfo getPermissionInfo(String name, int flags);
+
+ List<PermissionInfo> queryPermissionsByGroup(String group, int flags);
+
+ PermissionGroupInfo getPermissionGroupInfo(String name, int flags);
+
+ List<PermissionGroupInfo> getAllPermissionGroups(int flags);
+
+ ApplicationInfo getApplicationInfo(String packageName, int flags);
+
+ ActivityInfo getActivityInfo(in ComponentName className, int flags);
+
+ ActivityInfo getReceiverInfo(in ComponentName className, int flags);
+
+ ServiceInfo getServiceInfo(in ComponentName className, int flags);
+
+ int checkPermission(String permName, String pkgName);
+
+ int checkUidPermission(String permName, int uid);
+
+ boolean addPermission(in PermissionInfo info);
+
+ void removePermission(String name);
+
+ int checkSignatures(String pkg1, String pkg2);
+
+ String[] getPackagesForUid(int uid);
+
+ String getNameForUid(int uid);
+
+ ResolveInfo resolveIntent(in Intent intent, String resolvedType, int flags);
+
+ List<ResolveInfo> queryIntentActivities(in Intent intent,
+ String resolvedType, int flags);
+
+ List<ResolveInfo> queryIntentActivityOptions(
+ in ComponentName caller, in Intent[] specifics,
+ in String[] specificTypes, in Intent intent,
+ String resolvedType, int flags);
+
+ List<ResolveInfo> queryIntentReceivers(in Intent intent,
+ String resolvedType, int flags);
+
+ ResolveInfo resolveService(in Intent intent,
+ String resolvedType, int flags);
+
+ List<ResolveInfo> queryIntentServices(in Intent intent,
+ String resolvedType, int flags);
+
+ List<PackageInfo> getInstalledPackages(int flags);
+
+ List<ApplicationInfo> getInstalledApplications(int flags);
+
+ /**
+ * Retrieve all applications that are marked as persistent.
+ *
+ * @return A List&lt;applicationInfo> containing one entry for each persistent
+ * application.
+ */
+ List<ApplicationInfo> getPersistentApplications(int flags);
+
+ ProviderInfo resolveContentProvider(String name, int flags);
+
+ /**
+ * Retrieve sync information for all content providers.
+ *
+ * @param outNames Filled in with a list of the root names of the content
+ * providers that can sync.
+ * @param outInfo Filled in with a list of the ProviderInfo for each
+ * name in 'outNames'.
+ */
+ void querySyncProviders(inout List<String> outNames,
+ inout List<ProviderInfo> outInfo);
+
+ List<ProviderInfo> queryContentProviders(
+ String processName, int uid, int flags);
+
+ InstrumentationInfo getInstrumentationInfo(
+ in ComponentName className, int flags);
+
+ List<InstrumentationInfo> queryInstrumentation(
+ String targetPackage, int flags);
+
+ /**
+ * Install a package.
+ *
+ * @param packageURI The location of the package file to install.
+ * @param observer a callback to use to notify when the package installation in finished.
+ * @param flags - possible values: {@link #FORWARD_LOCK_PACKAGE},
+ * {@link #REPLACE_EXISITING_PACKAGE}
+ */
+ void installPackage(in Uri packageURI, IPackageInstallObserver observer, int flags);
+
+ /**
+ * Delete a package.
+ *
+ * @param packageName The fully qualified name of the package to delete.
+ * @param observer a callback to use to notify when the package deletion in finished.
+ * @param flags - possible values: {@link #DONT_DELETE_DATA}
+ */
+ void deletePackage(in String packageName, IPackageDeleteObserver observer, int flags);
+
+ void addPackageToPreferred(String packageName);
+
+ void removePackageFromPreferred(String packageName);
+
+ List<PackageInfo> getPreferredPackages(int flags);
+
+ void addPreferredActivity(in IntentFilter filter, int match,
+ in ComponentName[] set, in ComponentName activity);
+ void clearPackagePreferredActivities(String packageName);
+ int getPreferredActivities(out List<IntentFilter> outFilters,
+ out List<ComponentName> outActivities, String packageName);
+
+ /**
+ * As per {@link android.content.pm.PackageManager#setComponentEnabledSetting}.
+ */
+ void setComponentEnabledSetting(in ComponentName componentName,
+ in int newState, in int flags);
+
+ /**
+ * As per {@link android.content.pm.PackageManager#getComponentEnabledSetting}.
+ */
+ int getComponentEnabledSetting(in ComponentName componentName);
+
+ /**
+ * As per {@link android.content.pm.PackageManager#setApplicationEnabledSetting}.
+ */
+ void setApplicationEnabledSetting(in String packageName, in int newState, int flags);
+
+ /**
+ * As per {@link android.content.pm.PackageManager#getApplicationEnabledSetting}.
+ */
+ int getApplicationEnabledSetting(in String packageName);
+
+ /**
+ * Free storage by deleting LRU sorted list of cache files across all applications.
+ * If the currently available free storage on the device is greater than or equal to the
+ * requested free storage, no cache files are cleared. If the currently available storage on the
+ * device is less than the requested free storage, some or all of the cache files across
+ * all applications are deleted(based on last accessed time) to increase the free storage
+ * space on the device to the requested value. There is no gurantee that clearing all
+ * the cache files from all applications will clear up enough storage to achieve the desired
+ * value.
+ * @param freeStorageSize The number of bytes of storage to be
+ * freed by the system. Say if freeStorageSize is XX,
+ * and the current free storage is YY,
+ * if XX is less than YY, just return. if not free XX-YY number of
+ * bytes if possible.
+ * @param observer callback used to notify when the operation is completed
+ */
+ void freeApplicationCache(in long freeStorageSize, IPackageDataObserver observer);
+
+ /**
+ * Delete all the cache files in an applications cache directory
+ * @param packageName The package name of the application whose cache
+ * files need to be deleted
+ * @param observer a callback used to notify when the deletion is finished.
+ */
+ void deleteApplicationCacheFiles(in String packageName, IPackageDataObserver observer);
+
+ /**
+ * Clear the user data directory of an application.
+ * @param packageName The package name of the application whose cache
+ * files need to be deleted
+ * @param observer a callback used to notify when the operation is completed.
+ */
+ void clearApplicationUserData(in String packageName, IPackageDataObserver observer);
+
+ /**
+ * Get package statistics including the code, data and cache size for
+ * an already installed package
+ * @param packageName The package name of the application
+ * @param observer a callback to use to notify when the asynchronous
+ * retrieval of information is complete.
+ */
+ void getPackageSizeInfo(in String packageName, IPackageStatsObserver observer);
+
+ void enterSafeMode();
+ void systemReady();
+ boolean hasSystemUidErrors();
+}
diff --git a/core/java/android/content/pm/IPackageStatsObserver.aidl b/core/java/android/content/pm/IPackageStatsObserver.aidl
new file mode 100755
index 0000000..ede4d1d
--- /dev/null
+++ b/core/java/android/content/pm/IPackageStatsObserver.aidl
@@ -0,0 +1,30 @@
+/*
+**
+** Copyright 2007, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.content.pm;
+
+import android.content.pm.PackageStats;
+/**
+ * API for package data change related callbacks from the Package Manager.
+ * Some usage scenarios include deletion of cache directory, generate
+ * statistics related to code, data, cache usage(TODO)
+ * {@hide}
+ */
+oneway interface IPackageStatsObserver {
+
+ void onGetStatsCompleted(in PackageStats pStats, boolean succeeded);
+}
diff --git a/core/java/android/content/pm/InstrumentationInfo.aidl b/core/java/android/content/pm/InstrumentationInfo.aidl
new file mode 100755
index 0000000..3d847ae
--- /dev/null
+++ b/core/java/android/content/pm/InstrumentationInfo.aidl
@@ -0,0 +1,20 @@
+/* //device/java/android/android/view/WindowManager.aidl
+**
+** Copyright 2007, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.content.pm;
+
+parcelable InstrumentationInfo;
diff --git a/core/java/android/content/pm/InstrumentationInfo.java b/core/java/android/content/pm/InstrumentationInfo.java
new file mode 100644
index 0000000..e6745da
--- /dev/null
+++ b/core/java/android/content/pm/InstrumentationInfo.java
@@ -0,0 +1,96 @@
+package android.content.pm;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import java.text.Collator;
+import java.util.Comparator;
+
+/**
+ * Information you can retrieve about a particular piece of test
+ * instrumentation. This corresponds to information collected
+ * from the AndroidManifest.xml's &lt;instrumentation&gt; tag.
+ */
+public class InstrumentationInfo extends PackageItemInfo implements Parcelable {
+ /**
+ * The name of the application package being instrumented. From the
+ * "package" attribute.
+ */
+ public String targetPackage;
+
+ /**
+ * Full path to the location of this package.
+ */
+ public String sourceDir;
+
+ /**
+ * Full path to the location of the publicly available parts of this package (i.e. the resources
+ * and manifest). For non-forward-locked apps this will be the same as {@link #sourceDir).
+ */
+ public String publicSourceDir;
+ /**
+ * Full path to a directory assigned to the package for its persistent
+ * data.
+ */
+ public String dataDir;
+
+ /**
+ * Specifies whether or not this instrumentation will handle profiling.
+ */
+ public boolean handleProfiling;
+
+ /** Specifies whether or not to run this instrumentation as a functional test */
+ public boolean functionalTest;
+
+ public InstrumentationInfo() {
+ }
+
+ public InstrumentationInfo(InstrumentationInfo orig) {
+ super(orig);
+ targetPackage = orig.targetPackage;
+ sourceDir = orig.sourceDir;
+ publicSourceDir = orig.publicSourceDir;
+ dataDir = orig.dataDir;
+ handleProfiling = orig.handleProfiling;
+ functionalTest = orig.functionalTest;
+ }
+
+ public String toString() {
+ return "InstrumentationInfo{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " " + packageName + "}";
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int parcelableFlags) {
+ super.writeToParcel(dest, parcelableFlags);
+ dest.writeString(targetPackage);
+ dest.writeString(sourceDir);
+ dest.writeString(publicSourceDir);
+ dest.writeString(dataDir);
+ dest.writeInt((handleProfiling == false) ? 0 : 1);
+ }
+
+ public static final Parcelable.Creator<InstrumentationInfo> CREATOR
+ = new Parcelable.Creator<InstrumentationInfo>() {
+ public InstrumentationInfo createFromParcel(Parcel source) {
+ return new InstrumentationInfo(source);
+ }
+ public InstrumentationInfo[] newArray(int size) {
+ return new InstrumentationInfo[size];
+ }
+ };
+
+ private InstrumentationInfo(Parcel source) {
+ super(source);
+ targetPackage = source.readString();
+ sourceDir = source.readString();
+ publicSourceDir = source.readString();
+ dataDir = source.readString();
+ handleProfiling = source.readInt() != 0;
+ }
+}
diff --git a/core/java/android/content/pm/PackageInfo.aidl b/core/java/android/content/pm/PackageInfo.aidl
new file mode 100755
index 0000000..35e2322
--- /dev/null
+++ b/core/java/android/content/pm/PackageInfo.aidl
@@ -0,0 +1,20 @@
+/* //device/java/android/android/view/WindowManager.aidl
+**
+** Copyright 2007, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.content.pm;
+
+parcelable PackageInfo;
diff --git a/core/java/android/content/pm/PackageInfo.java b/core/java/android/content/pm/PackageInfo.java
new file mode 100644
index 0000000..7d694c7
--- /dev/null
+++ b/core/java/android/content/pm/PackageInfo.java
@@ -0,0 +1,170 @@
+package android.content.pm;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Overall information about the contents of a package. This corresponds
+ * to all of the information collected from AndroidManifest.xml.
+ */
+public class PackageInfo implements Parcelable {
+ /**
+ * The name of this package. From the &lt;manifest&gt; tag's "name"
+ * attribute.
+ */
+ public String packageName;
+
+ /**
+ * The version number of this package, as specified by the &lt;manifest&gt;
+ * tag's {@link android.R.styleable#AndroidManifest_versionCode versionCode}
+ * attribute.
+ */
+ public int versionCode;
+
+ /**
+ * The version name of this package, as specified by the &lt;manifest&gt;
+ * tag's {@link android.R.styleable#AndroidManifest_versionName versionName}
+ * attribute.
+ */
+ public String versionName;
+
+ /**
+ * Information collected from the &lt;application&gt; tag, or null if
+ * there was none.
+ */
+ public ApplicationInfo applicationInfo;
+
+ /**
+ * All kernel group-IDs that have been assigned to this package.
+ * This is only filled in if the flag {@link PackageManager#GET_GIDS} was set.
+ */
+ public int[] gids;
+
+ /**
+ * Array of all {@link android.R.styleable#AndroidManifestActivity
+ * &lt;activity&gt;} tags included under &lt;application&gt;,
+ * or null if there were none. This is only filled in if the flag
+ * {@link PackageManager#GET_ACTIVITIES} was set.
+ */
+ public ActivityInfo[] activities;
+
+ /**
+ * Array of all {@link android.R.styleable#AndroidManifestReceiver
+ * &lt;receiver&gt;} tags included under &lt;application&gt;,
+ * or null if there were none. This is only filled in if the flag
+ * {@link PackageManager#GET_RECEIVERS} was set.
+ */
+ public ActivityInfo[] receivers;
+
+ /**
+ * Array of all {@link android.R.styleable#AndroidManifestService
+ * &lt;service&gt;} tags included under &lt;application&gt;,
+ * or null if there were none. This is only filled in if the flag
+ * {@link PackageManager#GET_SERVICES} was set.
+ */
+ public ServiceInfo[] services;
+
+ /**
+ * Array of all {@link android.R.styleable#AndroidManifestProvider
+ * &lt;provider&gt;} tags included under &lt;application&gt;,
+ * or null if there were none. This is only filled in if the flag
+ * {@link PackageManager#GET_PROVIDERS} was set.
+ */
+ public ProviderInfo[] providers;
+
+ /**
+ * Array of all {@link android.R.styleable#AndroidManifestInstrumentation
+ * &lt;instrumentation&gt;} tags included under &lt;manifest&gt;,
+ * or null if there were none. This is only filled in if the flag
+ * {@link PackageManager#GET_INSTRUMENTATION} was set.
+ */
+ public InstrumentationInfo[] instrumentation;
+
+ /**
+ * Array of all {@link android.R.styleable#AndroidManifestPermission
+ * &lt;permission&gt;} tags included under &lt;manifest&gt;,
+ * or null if there were none. This is only filled in if the flag
+ * {@link PackageManager#GET_PERMISSIONS} was set.
+ */
+ public PermissionInfo[] permissions;
+
+ /**
+ * Array of all {@link android.R.styleable#AndroidManifestUsesPermission
+ * &lt;uses-permission&gt;} tags included under &lt;manifest&gt;,
+ * or null if there were none. This is only filled in if the flag
+ * {@link PackageManager#GET_PERMISSIONS} was set. This list includes
+ * all permissions requested, even those that were not granted or known
+ * by the system at install time.
+ */
+ public String[] requestedPermissions;
+
+ /**
+ * Array of all signatures read from the package file. This is only filled
+ * in if the flag {@link PackageManager#GET_SIGNATURES} was set.
+ */
+ public Signature[] signatures;
+
+ public PackageInfo() {
+ }
+
+ public String toString() {
+ return "PackageInfo{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " " + packageName + "}";
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int parcelableFlags) {
+ dest.writeString(packageName);
+ dest.writeInt(versionCode);
+ dest.writeString(versionName);
+ if (applicationInfo != null) {
+ dest.writeInt(1);
+ applicationInfo.writeToParcel(dest, parcelableFlags);
+ } else {
+ dest.writeInt(0);
+ }
+ dest.writeIntArray(gids);
+ dest.writeTypedArray(activities, parcelableFlags);
+ dest.writeTypedArray(receivers, parcelableFlags);
+ dest.writeTypedArray(services, parcelableFlags);
+ dest.writeTypedArray(providers, parcelableFlags);
+ dest.writeTypedArray(instrumentation, parcelableFlags);
+ dest.writeTypedArray(permissions, parcelableFlags);
+ dest.writeStringArray(requestedPermissions);
+ dest.writeTypedArray(signatures, parcelableFlags);
+ }
+
+ public static final Parcelable.Creator<PackageInfo> CREATOR
+ = new Parcelable.Creator<PackageInfo>() {
+ public PackageInfo createFromParcel(Parcel source) {
+ return new PackageInfo(source);
+ }
+
+ public PackageInfo[] newArray(int size) {
+ return new PackageInfo[size];
+ }
+ };
+
+ private PackageInfo(Parcel source) {
+ packageName = source.readString();
+ versionCode = source.readInt();
+ versionName = source.readString();
+ int hasApp = source.readInt();
+ if (hasApp != 0) {
+ applicationInfo = ApplicationInfo.CREATOR.createFromParcel(source);
+ }
+ gids = source.createIntArray();
+ activities = source.createTypedArray(ActivityInfo.CREATOR);
+ receivers = source.createTypedArray(ActivityInfo.CREATOR);
+ services = source.createTypedArray(ServiceInfo.CREATOR);
+ providers = source.createTypedArray(ProviderInfo.CREATOR);
+ instrumentation = source.createTypedArray(InstrumentationInfo.CREATOR);
+ permissions = source.createTypedArray(PermissionInfo.CREATOR);
+ requestedPermissions = source.createStringArray();
+ signatures = source.createTypedArray(Signature.CREATOR);
+ }
+}
diff --git a/core/java/android/content/pm/PackageItemInfo.java b/core/java/android/content/pm/PackageItemInfo.java
new file mode 100644
index 0000000..406a3eb
--- /dev/null
+++ b/core/java/android/content/pm/PackageItemInfo.java
@@ -0,0 +1,188 @@
+package android.content.pm;
+
+import android.content.res.XmlResourceParser;
+
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.text.TextUtils;
+import android.util.Printer;
+
+import java.text.Collator;
+import java.util.Comparator;
+
+/**
+ * Base class containing information common to all package items held by
+ * the package manager. This provides a very common basic set of attributes:
+ * a label, icon, and meta-data. This class is not intended
+ * to be used by itself; it is simply here to share common definitions
+ * between all items returned by the package manager. As such, it does not
+ * itself implement Parcelable, but does provide convenience methods to assist
+ * in the implementation of Parcelable in subclasses.
+ */
+public class PackageItemInfo {
+ /**
+ * Public name of this item. From the "android:name" attribute.
+ */
+ public String name;
+
+ /**
+ * Name of the package that this item is in.
+ */
+ public String packageName;
+
+ /**
+ * A string resource identifier (in the package's resources) of this
+ * component's label. From the "label" attribute or, if not set, 0.
+ */
+ public int labelRes;
+
+ /**
+ * The string provided in the AndroidManifest file, if any. You
+ * probably don't want to use this. You probably want
+ * {@link PackageManager#getApplicationLabel}
+ */
+ public CharSequence nonLocalizedLabel;
+
+ /**
+ * A drawable resource identifier (in the package's resources) of this
+ * component's icon. From the "icon" attribute or, if not set, 0.
+ */
+ public int icon;
+
+ /**
+ * Additional meta-data associated with this component. This field
+ * will only be filled in if you set the
+ * {@link PackageManager#GET_META_DATA} flag when requesting the info.
+ */
+ public Bundle metaData;
+
+ public PackageItemInfo() {
+ }
+
+ public PackageItemInfo(PackageItemInfo orig) {
+ name = orig.name;
+ packageName = orig.packageName;
+ labelRes = orig.labelRes;
+ nonLocalizedLabel = orig.nonLocalizedLabel;
+ icon = orig.icon;
+ metaData = orig.metaData;
+ }
+
+ /**
+ * Retrieve the current textual label associated with this item. This
+ * will call back on the given PackageManager to load the label from
+ * the application.
+ *
+ * @param pm A PackageManager from which the label can be loaded; usually
+ * the PackageManager from which you originally retrieved this item.
+ *
+ * @return Returns a CharSequence containing the item's label. If the
+ * item does not have a label, its name is returned.
+ */
+ public CharSequence loadLabel(PackageManager pm) {
+ if (nonLocalizedLabel != null) {
+ return nonLocalizedLabel;
+ }
+ if (labelRes != 0) {
+ CharSequence label = pm.getText(packageName, labelRes, null);
+ if (label != null) {
+ return label;
+ }
+ }
+ return name;
+ }
+
+ /**
+ * Retrieve the current graphical icon associated with this item. This
+ * will call back on the given PackageManager to load the icon from
+ * the application.
+ *
+ * @param pm A PackageManager from which the icon can be loaded; usually
+ * the PackageManager from which you originally retrieved this item.
+ *
+ * @return Returns a Drawable containing the item's icon. If the
+ * item does not have an icon, the default activity icon is returned.
+ */
+ public Drawable loadIcon(PackageManager pm) {
+ if (icon != 0) {
+ Drawable dr = pm.getDrawable(packageName, icon, null);
+ if (dr != null) {
+ return dr;
+ }
+ }
+ return pm.getDefaultActivityIcon();
+ }
+
+ /**
+ * Load an XML resource attached to the meta-data of this item. This will
+ * retrieved the name meta-data entry, and if defined call back on the
+ * given PackageManager to load its XML file from the application.
+ *
+ * @param pm A PackageManager from which the XML can be loaded; usually
+ * the PackageManager from which you originally retrieved this item.
+ * @param name Name of the meta-date you would like to load.
+ *
+ * @return Returns an XmlPullParser you can use to parse the XML file
+ * assigned as the given meta-data. If the meta-data name is not defined
+ * or the XML resource could not be found, null is returned.
+ */
+ public XmlResourceParser loadXmlMetaData(PackageManager pm, String name) {
+ if (metaData != null) {
+ int resid = metaData.getInt(name);
+ if (resid != 0) {
+ return pm.getXml(packageName, resid, null);
+ }
+ }
+ return null;
+ }
+
+ protected void dumpFront(Printer pw, String prefix) {
+ pw.println(prefix + "name=" + name);
+ pw.println(prefix + "packageName=" + packageName);
+ pw.println(prefix + "labelRes=0x" + Integer.toHexString(labelRes)
+ + " nonLocalizedLabel=" + nonLocalizedLabel
+ + " icon=0x" + Integer.toHexString(icon));
+ }
+
+ protected void dumpBack(Printer pw, String prefix) {
+ // no back here
+ }
+
+ public void writeToParcel(Parcel dest, int parcelableFlags) {
+ dest.writeString(name);
+ dest.writeString(packageName);
+ dest.writeInt(labelRes);
+ TextUtils.writeToParcel(nonLocalizedLabel, dest, parcelableFlags);
+ dest.writeInt(icon);
+ dest.writeBundle(metaData);
+ }
+
+ protected PackageItemInfo(Parcel source) {
+ name = source.readString();
+ packageName = source.readString();
+ labelRes = source.readInt();
+ nonLocalizedLabel
+ = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
+ icon = source.readInt();
+ metaData = source.readBundle();
+ }
+
+ public static class DisplayNameComparator
+ implements Comparator<PackageItemInfo> {
+ public DisplayNameComparator(PackageManager pm) {
+ mPM = pm;
+ }
+
+ public final int compare(PackageItemInfo aa, PackageItemInfo ab) {
+ CharSequence sa = aa.loadLabel(mPM);
+ if (sa == null) sa = aa.name;
+ CharSequence sb = ab.loadLabel(mPM);
+ if (sb == null) sb = ab.name;
+ return sCollator.compare(sa, sb);
+ }
+
+ private final Collator sCollator = Collator.getInstance();
+ private PackageManager mPM;
+ }
+}
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
new file mode 100644
index 0000000..db00a9a
--- /dev/null
+++ b/core/java/android/content/pm/PackageManager.java
@@ -0,0 +1,1453 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.pm;
+
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Resources;
+import android.content.res.XmlResourceParser;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.util.AndroidException;
+import android.util.DisplayMetrics;
+
+import java.io.File;
+import java.util.List;
+
+/**
+ * Class for retrieving various kinds of information related to the application
+ * packages that are currently installed on the device.
+ *
+ * You can find this class through {@link Context#getPackageManager}.
+ */
+public abstract class PackageManager {
+
+ /**
+ * This exception is thrown when a given package, application, or component
+ * name can not be found.
+ */
+ public static class NameNotFoundException extends AndroidException {
+ public NameNotFoundException() {
+ }
+
+ public NameNotFoundException(String name) {
+ super(name);
+ }
+ }
+
+ /**
+ * {@link PackageInfo} flag: return information about
+ * activities in the package in {@link PackageInfo#activities}.
+ */
+ public static final int GET_ACTIVITIES = 0x00000001;
+
+ /**
+ * {@link PackageInfo} flag: return information about
+ * intent receivers in the package in
+ * {@link PackageInfo#receivers}.
+ */
+ public static final int GET_RECEIVERS = 0x00000002;
+
+ /**
+ * {@link PackageInfo} flag: return information about
+ * services in the package in {@link PackageInfo#services}.
+ */
+ public static final int GET_SERVICES = 0x00000004;
+
+ /**
+ * {@link PackageInfo} flag: return information about
+ * content providers in the package in
+ * {@link PackageInfo#providers}.
+ */
+ public static final int GET_PROVIDERS = 0x00000008;
+
+ /**
+ * {@link PackageInfo} flag: return information about
+ * instrumentation in the package in
+ * {@link PackageInfo#instrumentation}.
+ */
+ public static final int GET_INSTRUMENTATION = 0x00000010;
+
+ /**
+ * {@link PackageInfo} flag: return information about the
+ * intent filters supported by the activity.
+ */
+ public static final int GET_INTENT_FILTERS = 0x00000020;
+
+ /**
+ * {@link PackageInfo} flag: return information about the
+ * signatures included in the package.
+ */
+ public static final int GET_SIGNATURES = 0x00000040;
+
+ /**
+ * {@link ResolveInfo} flag: return the IntentFilter that
+ * was matched for a particular ResolveInfo in
+ * {@link ResolveInfo#filter}.
+ */
+ public static final int GET_RESOLVED_FILTER = 0x00000040;
+
+ /**
+ * {@link ComponentInfo} flag: return the {@link ComponentInfo#metaData}
+ * data {@link android.os.Bundle}s that are associated with a component.
+ * This applies for any API returning a ComponentInfo subclass.
+ */
+ public static final int GET_META_DATA = 0x00000080;
+
+ /**
+ * {@link PackageInfo} flag: return the
+ * {@link PackageInfo#gids group ids} that are associated with an
+ * application.
+ * This applies for any API returning an PackageInfo class, either
+ * directly or nested inside of another.
+ */
+ public static final int GET_GIDS = 0x00000100;
+
+ /**
+ * {@link PackageInfo} flag: include disabled components in the returned info.
+ */
+ public static final int GET_DISABLED_COMPONENTS = 0x00000200;
+
+ /**
+ * {@link ApplicationInfo} flag: return the
+ * {@link ApplicationInfo#sharedLibraryFiles paths to the shared libraries}
+ * that are associated with an application.
+ * This applies for any API returning an ApplicationInfo class, either
+ * directly or nested inside of another.
+ */
+ public static final int GET_SHARED_LIBRARY_FILES = 0x00000400;
+
+ /**
+ * {@link ProviderInfo} flag: return the
+ * {@link ProviderInfo#uriPermissionPatterns URI permission patterns}
+ * that are associated with a content provider.
+ * This applies for any API returning an ProviderInfo class, either
+ * directly or nested inside of another.
+ */
+ public static final int GET_URI_PERMISSION_PATTERNS = 0x00000800;
+ /**
+ * {@link PackageInfo} flag: return information about
+ * permissions in the package in
+ * {@link PackageInfo#permissions}.
+ */
+ public static final int GET_PERMISSIONS = 0x00001000;
+
+ /**
+ * Permission check result: this is returned by {@link #checkPermission}
+ * if the permission has been granted to the given package.
+ */
+ public static final int PERMISSION_GRANTED = 0;
+
+ /**
+ * Permission check result: this is returned by {@link #checkPermission}
+ * if the permission has not been granted to the given package.
+ */
+ public static final int PERMISSION_DENIED = -1;
+
+ /**
+ * Signature check result: this is returned by {@link #checkSignatures}
+ * if the two packages have a matching signature.
+ */
+ public static final int SIGNATURE_MATCH = 0;
+
+ /**
+ * Signature check result: this is returned by {@link #checkSignatures}
+ * if neither of the two packages is signed.
+ */
+ public static final int SIGNATURE_NEITHER_SIGNED = 1;
+
+ /**
+ * Signature check result: this is returned by {@link #checkSignatures}
+ * if the first package is not signed, but the second is.
+ */
+ public static final int SIGNATURE_FIRST_NOT_SIGNED = -1;
+
+ /**
+ * Signature check result: this is returned by {@link #checkSignatures}
+ * if the second package is not signed, but the first is.
+ */
+ public static final int SIGNATURE_SECOND_NOT_SIGNED = -2;
+
+ /**
+ * Signature check result: this is returned by {@link #checkSignatures}
+ * if both packages are signed but there is no matching signature.
+ */
+ public static final int SIGNATURE_NO_MATCH = -3;
+
+ /**
+ * Signature check result: this is returned by {@link #checkSignatures}
+ * if either of the given package names are not valid.
+ */
+ public static final int SIGNATURE_UNKNOWN_PACKAGE = -4;
+
+ /**
+ * Resolution and querying flag: if set, only filters that support the
+ * {@link android.content.Intent#CATEGORY_DEFAULT} will be considered for
+ * matching. This is a synonym for including the CATEGORY_DEFAULT in your
+ * supplied Intent.
+ */
+ public static final int MATCH_DEFAULT_ONLY = 0x00010000;
+
+ public static final int COMPONENT_ENABLED_STATE_DEFAULT = 0;
+ public static final int COMPONENT_ENABLED_STATE_ENABLED = 1;
+ public static final int COMPONENT_ENABLED_STATE_DISABLED = 2;
+
+ /**
+ * Flag parameter for {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} to
+ * indicate that this package should be installed as forward locked, i.e. only the app itself
+ * should have access to it's code and non-resource assets.
+ */
+ public static final int FORWARD_LOCK_PACKAGE = 0x00000001;
+
+ /**
+ * Flag parameter for {@link #installPackage} to indicate that you want to replace an already
+ * installed package, if one exists
+ */
+ public static final int REPLACE_EXISTING_PACKAGE = 0x00000002;
+
+ /**
+ * Flag parameter for
+ * {@link #setComponentEnabledSetting(android.content.ComponentName, int, int)} to indicate
+ * that you don't want to kill the app containing the component. Be careful when you set this
+ * since changing component states can make the containing application's behavior unpredictable.
+ */
+ public static final int DONT_KILL_APP = 0x00000001;
+
+ /**
+ * Installation return code: this is passed to the {@link IPackageInstallObserver} by
+ * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} on success.
+ */
+ public static final int INSTALL_SUCCEEDED = 1;
+
+ /**
+ * Installation return code: this is passed to the {@link IPackageInstallObserver} by
+ * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if the package is
+ * already installed.
+ */
+ public static final int INSTALL_FAILED_ALREADY_EXISTS = -1;
+
+ /**
+ * Installation return code: this is passed to the {@link IPackageInstallObserver} by
+ * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if the package archive
+ * file is invalid.
+ */
+ public static final int INSTALL_FAILED_INVALID_APK = -2;
+
+ /**
+ * Installation return code: this is passed to the {@link IPackageInstallObserver} by
+ * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if the URI passed in
+ * is invalid.
+ */
+ public static final int INSTALL_FAILED_INVALID_URI = -3;
+
+ /**
+ * Installation return code: this is passed to the {@link IPackageInstallObserver} by
+ * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if the package manager
+ * service found that the device didn't have enough storage space to install the app
+ */
+ public static final int INSTALL_FAILED_INSUFFICIENT_STORAGE = -4;
+
+ /**
+ * Installation return code: this is passed to the {@link IPackageInstallObserver} by
+ * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if a
+ * package is already installed with the same name.
+ */
+ public static final int INSTALL_FAILED_DUPLICATE_PACKAGE = -5;
+
+ /**
+ * Installation return code: this is passed to the {@link IPackageInstallObserver} by
+ * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if
+ * the requested shared user does not exist.
+ */
+ public static final int INSTALL_FAILED_NO_SHARED_USER = -6;
+
+ /**
+ * Installation return code: this is passed to the {@link IPackageInstallObserver} by
+ * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if
+ * a previously installed package of the same name has a different signature
+ * than the new package (and the old package's data was not removed).
+ */
+ public static final int INSTALL_FAILED_UPDATE_INCOMPATIBLE = -7;
+
+ /**
+ * Installation return code: this is passed to the {@link IPackageInstallObserver} by
+ * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if
+ * the new package is requested a shared user which is already installed on the
+ * device and does not have matching signature.
+ */
+ public static final int INSTALL_FAILED_SHARED_USER_INCOMPATIBLE = -8;
+
+ /**
+ * Installation return code: this is passed to the {@link IPackageInstallObserver} by
+ * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if
+ * the new package uses a shared library that is not available.
+ */
+ public static final int INSTALL_FAILED_MISSING_SHARED_LIBRARY = -9;
+
+ /**
+ * Installation return code: this is passed to the {@link IPackageInstallObserver} by
+ * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if
+ * the new package uses a shared library that is not available.
+ */
+ public static final int INSTALL_FAILED_REPLACE_COULDNT_DELETE = -10;
+
+ /**
+ * Installation return code: this is passed to the {@link IPackageInstallObserver} by
+ * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if
+ * the new package failed while optimizing and validating its dex files,
+ * either because there was not enough storage or the validation failed.
+ */
+ public static final int INSTALL_FAILED_DEXOPT = -11;
+
+ /**
+ * Installation return code: this is passed to the {@link IPackageInstallObserver} by
+ * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if
+ * the new package failed because the current SDK version is older than
+ * that required by the package.
+ */
+ public static final int INSTALL_FAILED_OLDER_SDK = -12;
+
+ /**
+ * Installation parse return code: this is passed to the {@link IPackageInstallObserver} by
+ * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)}
+ * if the parser was given a path that is not a file, or does not end with the expected
+ * '.apk' extension.
+ */
+ public static final int INSTALL_PARSE_FAILED_NOT_APK = -100;
+
+ /**
+ * Installation parse return code: this is passed to the {@link IPackageInstallObserver} by
+ * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)}
+ * if the parser was unable to retrieve the AndroidManifest.xml file.
+ */
+ public static final int INSTALL_PARSE_FAILED_BAD_MANIFEST = -101;
+
+ /**
+ * Installation parse return code: this is passed to the {@link IPackageInstallObserver} by
+ * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)}
+ * if the parser encountered an unexpected exception.
+ */
+ public static final int INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION = -102;
+
+ /**
+ * Installation parse return code: this is passed to the {@link IPackageInstallObserver} by
+ * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)}
+ * if the parser did not find any certificates in the .apk.
+ */
+ public static final int INSTALL_PARSE_FAILED_NO_CERTIFICATES = -103;
+
+ /**
+ * Installation parse return code: this is passed to the {@link IPackageInstallObserver} by
+ * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)}
+ * if the parser found inconsistent certificates on the files in the .apk.
+ */
+ public static final int INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES = -104;
+
+ /**
+ * Installation parse return code: this is passed to the {@link IPackageInstallObserver} by
+ * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)}
+ * if the parser encountered a CertificateEncodingException in one of the
+ * files in the .apk.
+ */
+ public static final int INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING = -105;
+
+ /**
+ * Installation parse return code: this is passed to the {@link IPackageInstallObserver} by
+ * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)}
+ * if the parser encountered a bad or missing package name in the manifest.
+ */
+ public static final int INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME = -106;
+
+ /**
+ * Installation parse return code: this is passed to the {@link IPackageInstallObserver} by
+ * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)}
+ * if the parser encountered a bad shared user id name in the manifest.
+ */
+ public static final int INSTALL_PARSE_FAILED_BAD_SHARED_USER_ID = -107;
+
+ /**
+ * Installation parse return code: this is passed to the {@link IPackageInstallObserver} by
+ * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)}
+ * if the parser encountered some structural problem in the manifest.
+ */
+ public static final int INSTALL_PARSE_FAILED_MANIFEST_MALFORMED = -108;
+
+ /**
+ * Installation parse return code: this is passed to the {@link IPackageInstallObserver} by
+ * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)}
+ * if the parser did not find any actionable tags (instrumentation or application)
+ * in the manifest.
+ */
+ public static final int INSTALL_PARSE_FAILED_MANIFEST_EMPTY = -109;
+
+ /**
+ * Indicates the state of installation. Used by PackageManager to
+ * figure out incomplete installations. Say a package is being installed
+ * (the state is set to PKG_INSTALL_INCOMPLETE) and remains so till
+ * the package installation is successful or unsuccesful lin which case
+ * the PackageManager will no longer maintain state information associated
+ * with the package. If some exception(like device freeze or battery being
+ * pulled out) occurs during installation of a package, the PackageManager
+ * needs this information to clean up the previously failed installation.
+ */
+ public static final int PKG_INSTALL_INCOMPLETE = 0;
+ public static final int PKG_INSTALL_COMPLETE = 1;
+
+ /**
+ * Flag parameter for {@link #deletePackage} to indicate that you don't want to delete the
+ * package's data directory.
+ *
+ * @hide
+ */
+ public static final int DONT_DELETE_DATA = 0x00000001;
+
+ /**
+ * Retrieve overall information about an application package that is
+ * installed on the system.
+ *
+ * <p>Throws {@link NameNotFoundException} if a package with the given
+ * name can not be found on the system.
+ *
+ * @param packageName The full name (i.e. com.google.apps.contacts) of the
+ * desired package.
+ * @param flags Optional flags to control what information is returned. If
+ * 0, none of the optional information is returned.
+ *
+ * @return Returns a PackageInfo containing information about the package.
+ *
+ * @see #GET_ACTIVITIES
+ * @see #GET_RECEIVERS
+ * @see #GET_SERVICES
+ * @see #GET_INSTRUMENTATION
+ * @see #GET_SIGNATURES
+ */
+ public abstract PackageInfo getPackageInfo(String packageName, int flags)
+ throws NameNotFoundException;
+
+ /**
+ * Return an array of all of the secondary group-ids that have been
+ * assigned to a package.
+ *
+ * <p>Throws {@link NameNotFoundException} if a package with the given
+ * name can not be found on the system.
+ *
+ * @param packageName The full name (i.e. com.google.apps.contacts) of the
+ * desired package.
+ *
+ * @return Returns an int array of the assigned gids, or null if there
+ * are none.
+ */
+ public abstract int[] getPackageGids(String packageName)
+ throws NameNotFoundException;
+
+ /**
+ * Retrieve all of the information we know about a particular permission.
+ *
+ * <p>Throws {@link NameNotFoundException} if a permission with the given
+ * name can not be found on the system.
+ *
+ * @param name The fully qualified name (i.e. com.google.permission.LOGIN)
+ * of the permission you are interested in.
+ * @param flags Additional option flags. Use {@link #GET_META_DATA} to
+ * retrieve any meta-data associated with the permission.
+ *
+ * @return Returns a {@link PermissionInfo} containing information about the
+ * permission.
+ */
+ public abstract PermissionInfo getPermissionInfo(String name, int flags)
+ throws NameNotFoundException;
+
+ /**
+ * Query for all of the permissions associated with a particular group.
+ *
+ * <p>Throws {@link NameNotFoundException} if the given group does not
+ * exist.
+ *
+ * @param group The fully qualified name (i.e. com.google.permission.LOGIN)
+ * of the permission group you are interested in. Use null to
+ * find all of the permissions not associated with a group.
+ * @param flags Additional option flags. Use {@link #GET_META_DATA} to
+ * retrieve any meta-data associated with the permissions.
+ *
+ * @return Returns a list of {@link PermissionInfo} containing information
+ * about all of the permissions in the given group.
+ */
+ public abstract List<PermissionInfo> queryPermissionsByGroup(String group,
+ int flags) throws NameNotFoundException;
+
+ /**
+ * Retrieve all of the information we know about a particular group of
+ * permissions.
+ *
+ * <p>Throws {@link NameNotFoundException} if a permission group with the given
+ * name can not be found on the system.
+ *
+ * @param name The fully qualified name (i.e. com.google.permission_group.APPS)
+ * of the permission you are interested in.
+ * @param flags Additional option flags. Use {@link #GET_META_DATA} to
+ * retrieve any meta-data associated with the permission group.
+ *
+ * @return Returns a {@link PermissionGroupInfo} containing information
+ * about the permission.
+ */
+ public abstract PermissionGroupInfo getPermissionGroupInfo(String name,
+ int flags) throws NameNotFoundException;
+
+ /**
+ * Retrieve all of the known permission groups in the system.
+ *
+ * @param flags Additional option flags. Use {@link #GET_META_DATA} to
+ * retrieve any meta-data associated with the permission group.
+ *
+ * @return Returns a list of {@link PermissionGroupInfo} containing
+ * information about all of the known permission groups.
+ */
+ public abstract List<PermissionGroupInfo> getAllPermissionGroups(int flags);
+
+ /**
+ * Retrieve all of the information we know about a particular
+ * package/application.
+ *
+ * <p>Throws {@link NameNotFoundException} if an application with the given
+ * package name can not be found on the system.
+ *
+ * @param packageName The full name (i.e. com.google.apps.contacts) of an
+ * application.
+ * @param flags Additional option flags. Currently should always be 0.
+ *
+ * @return {@link ApplicationInfo} containing information about the
+ * application.
+ */
+ public abstract ApplicationInfo getApplicationInfo(String packageName,
+ int flags) throws NameNotFoundException;
+
+ /**
+ * Retrieve all of the information we know about a particular activity
+ * class.
+ *
+ * <p>Throws {@link NameNotFoundException} if an activity with the given
+ * class name can not be found on the system.
+ *
+ * @param className The full name (i.e.
+ * com.google.apps.contacts.ContactsList) of an Activity
+ * class.
+ * @param flags Additional option flags. Usually 0.
+ *
+ * @return {@link ActivityInfo} containing information about the activity.
+ *
+ * @see #GET_INTENT_FILTERS
+ */
+ public abstract ActivityInfo getActivityInfo(ComponentName className,
+ int flags) throws NameNotFoundException;
+
+ /**
+ * Retrieve all of the information we know about a particular receiver
+ * class.
+ *
+ * <p>Throws {@link NameNotFoundException} if a receiver with the given
+ * class name can not be found on the system.
+ *
+ * @param className The full name (i.e.
+ * com.google.apps.contacts.CalendarAlarm) of a Receiver
+ * class.
+ * @param flags Additional option flags. Usually 0.
+ *
+ * @return {@link ActivityInfo} containing information about the receiver.
+ *
+ * @see #GET_INTENT_FILTERS
+ */
+ public abstract ActivityInfo getReceiverInfo(ComponentName className,
+ int flags) throws NameNotFoundException;
+
+ /**
+ * Retrieve all of the information we know about a particular service
+ * class.
+ *
+ * <p>Throws {@link NameNotFoundException} if a service with the given
+ * class name can not be found on the system.
+ *
+ * @param className The full name (i.e.
+ * com.google.apps.media.BackgroundPlayback) of a Service
+ * class.
+ * @param flags Additional option flags. Currently should always be 0.
+ *
+ * @return ServiceInfo containing information about the service.
+ */
+ public abstract ServiceInfo getServiceInfo(ComponentName className,
+ int flags) throws NameNotFoundException;
+
+ /**
+ * Return a List of all packages that are installed
+ * on the device.
+ *
+ * @param flags Optional flags to control what information is returned. If
+ * 0, none of the optional information is returned.
+ *
+ * @return A List of PackageInfo objects, one for each package that is
+ * installed on the device. In the unlikely case of there being no
+ * installed packages, an empty list is returned.
+ *
+ * @see #GET_ACTIVITIES
+ * @see #GET_RECEIVERS
+ * @see #GET_SERVICES
+ * @see #GET_INSTRUMENTATION
+ * @see #GET_SIGNATURES
+ */
+ public abstract List<PackageInfo> getInstalledPackages(int flags);
+
+ /**
+ * Check whether a particular package has been granted a particular
+ * permission.
+ *
+ * @param permName The name of the permission you are checking for,
+ * @param pkgName The name of the package you are checking against.
+ *
+ * @return If the package has the permission, PERMISSION_GRANTED is
+ * returned. If it does not have the permission, PERMISSION_DENIED
+ * is returned.
+ *
+ * @see #PERMISSION_GRANTED
+ * @see #PERMISSION_DENIED
+ */
+ public abstract int checkPermission(String permName, String pkgName);
+
+ /**
+ * Add a new dynamic permission to the system. For this to work, your
+ * package must have defined a permission tree through the
+ * {@link android.R.styleable#AndroidManifestPermissionTree
+ * &lt;permission-tree&gt;} tag in its manifest. A package can only add
+ * permissions to trees that were defined by either its own package or
+ * another with the same user id; a permission is in a tree if it
+ * matches the name of the permission tree + ".": for example,
+ * "com.foo.bar" is a member of the permission tree "com.foo".
+ *
+ * <p>It is good to make your permission tree name descriptive, because you
+ * are taking possession of that entire set of permission names. Thus, it
+ * must be under a domain you control, with a suffix that will not match
+ * any normal permissions that may be declared in any applications that
+ * are part of that domain.
+ *
+ * <p>New permissions must be added before
+ * any .apks are installed that use those permissions. Permissions you
+ * add through this method are remembered across reboots of the device.
+ * If the given permission already exists, the info you supply here
+ * will be used to update it.
+ *
+ * @param info Description of the permission to be added.
+ *
+ * @return Returns true if a new permission was created, false if an
+ * existing one was updated.
+ *
+ * @throws SecurityException if you are not allowed to add the
+ * given permission name.
+ *
+ * @see #removePermission(String)
+ */
+ public abstract boolean addPermission(PermissionInfo info);
+
+ /**
+ * Removes a permission that was previously added with
+ * {@link #addPermission(PermissionInfo)}. The same ownership rules apply
+ * -- you are only allowed to remove permissions that you are allowed
+ * to add.
+ *
+ * @param name The name of the permission to remove.
+ *
+ * @throws SecurityException if you are not allowed to remove the
+ * given permission name.
+ *
+ * @see #addPermission(PermissionInfo)
+ */
+ public abstract void removePermission(String name);
+
+ /**
+ * Compare the signatures of two packages to determine if the same
+ * signature appears in both of them. If they do contain the same
+ * signature, then they are allowed special privileges when working
+ * with each other: they can share the same user-id, run instrumentation
+ * against each other, etc.
+ *
+ * @param pkg1 First package name whose signature will be compared.
+ * @param pkg2 Second package name whose signature will be compared.
+ * @return Returns an integer indicating whether there is a matching
+ * signature: the value is >= 0 if there is a match (or neither package
+ * is signed), or < 0 if there is not a match. The match result can be
+ * further distinguished with the success (>= 0) constants
+ * {@link #SIGNATURE_MATCH}, {@link #SIGNATURE_NEITHER_SIGNED}; or
+ * failure (< 0) constants {@link #SIGNATURE_FIRST_NOT_SIGNED},
+ * {@link #SIGNATURE_SECOND_NOT_SIGNED}, {@link #SIGNATURE_NO_MATCH},
+ * or {@link #SIGNATURE_UNKNOWN_PACKAGE}.
+ *
+ * @see #SIGNATURE_MATCH
+ * @see #SIGNATURE_NEITHER_SIGNED
+ * @see #SIGNATURE_FIRST_NOT_SIGNED
+ * @see #SIGNATURE_SECOND_NOT_SIGNED
+ * @see #SIGNATURE_NO_MATCH
+ * @see #SIGNATURE_UNKNOWN_PACKAGE
+ */
+ public abstract int checkSignatures(String pkg1, String pkg2);
+
+ /**
+ * Retrieve the names of all packages that are associated with a particular
+ * user id. In most cases, this will be a single package name, the package
+ * that has been assigned that user id. Where there are multiple packages
+ * sharing the same user id through the "sharedUserId" mechanism, all
+ * packages with that id will be returned.
+ *
+ * @param uid The user id for which you would like to retrieve the
+ * associated packages.
+ *
+ * @return Returns an array of one or more packages assigned to the user
+ * id, or null if there are no known packages with the given id.
+ */
+ public abstract String[] getPackagesForUid(int uid);
+
+ /**
+ * Retrieve the official name associated with a user id. This name is
+ * guaranteed to never change, though it is possibly for the underlying
+ * user id to be changed. That is, if you are storing information about
+ * user ids in persistent storage, you should use the string returned
+ * by this function instead of the raw user-id.
+ *
+ * @param uid The user id for which you would like to retrieve a name.
+ * @return Returns a unique name for the given user id, or null if the
+ * user id is not currently assigned.
+ */
+ public abstract String getNameForUid(int uid);
+
+ /**
+ * Return a List of all application packages that are installed on the
+ * device.
+ *
+ * @param flags Additional option flags. Currently should always be 0.
+ *
+ * @return A List of ApplicationInfo objects, one for each application that
+ * is installed on the device. In the unlikely case of there being
+ * no installed applications, an empty list is returned.
+ */
+ public abstract List<ApplicationInfo> getInstalledApplications(int flags);
+
+ /**
+ * Determine the best action to perform for a given Intent. This is how
+ * {@link Intent#resolveActivity} finds an activity if a class has not
+ * been explicitly specified.
+ *
+ * @param intent An intent containing all of the desired specification
+ * (action, data, type, category, and/or component).
+ * @param flags Additional option flags. The most important is
+ * MATCH_DEFAULT_ONLY, to limit the resolution to only
+ * those activities that support the CATEGORY_DEFAULT.
+ *
+ * @return Returns a ResolveInfo containing the final activity intent that
+ * was determined to be the best action. Returns null if no
+ * matching activity was found.
+ *
+ * @see #MATCH_DEFAULT_ONLY
+ * @see #GET_INTENT_FILTERS
+ * @see #GET_RESOLVED_FILTER
+ */
+ public abstract ResolveInfo resolveActivity(Intent intent, int flags);
+
+ /**
+ * Retrieve all activities that can be performed for the given intent.
+ *
+ * @param intent The desired intent as per resolveActivity().
+ * @param flags Additional option flags. The most important is
+ * MATCH_DEFAULT_ONLY, to limit the resolution to only
+ * those activities that support the CATEGORY_DEFAULT.
+ *
+ * @return A List<ResolveInfo> containing one entry for each matching
+ * Activity. These are ordered from best to worst match -- that
+ * is, the first item in the list is what is returned by
+ * resolveActivity(). If there are no matching activities, an empty
+ * list is returned.
+ *
+ * @see #MATCH_DEFAULT_ONLY
+ * @see #GET_INTENT_FILTERS
+ * @see #GET_RESOLVED_FILTER
+ */
+ public abstract List<ResolveInfo> queryIntentActivities(Intent intent,
+ int flags);
+
+ /**
+ * Retrieve a set of activities that should be presented to the user as
+ * similar options. This is like {@link #queryIntentActivities}, except it
+ * also allows you to supply a list of more explicit Intents that you would
+ * like to resolve to particular options, and takes care of returning the
+ * final ResolveInfo list in a reasonable order, with no duplicates, based
+ * on those inputs.
+ *
+ * @param caller The class name of the activity that is making the
+ * request. This activity will never appear in the output
+ * list. Can be null.
+ * @param specifics An array of Intents that should be resolved to the
+ * first specific results. Can be null.
+ * @param intent The desired intent as per resolveActivity().
+ * @param flags Additional option flags. The most important is
+ * MATCH_DEFAULT_ONLY, to limit the resolution to only
+ * those activities that support the CATEGORY_DEFAULT.
+ *
+ * @return A List<ResolveInfo> containing one entry for each matching
+ * Activity. These are ordered first by all of the intents resolved
+ * in <var>specifics</var> and then any additional activities that
+ * can handle <var>intent</var> but did not get included by one of
+ * the <var>specifics</var> intents. If there are no matching
+ * activities, an empty list is returned.
+ *
+ * @see #MATCH_DEFAULT_ONLY
+ * @see #GET_INTENT_FILTERS
+ * @see #GET_RESOLVED_FILTER
+ */
+ public abstract List<ResolveInfo> queryIntentActivityOptions(
+ ComponentName caller, Intent[] specifics, Intent intent, int flags);
+
+ /**
+ * Retrieve all receivers that can handle a broadcast of the given intent.
+ *
+ * @param intent The desired intent as per resolveActivity().
+ * @param flags Additional option flags. The most important is
+ * MATCH_DEFAULT_ONLY, to limit the resolution to only
+ * those activities that support the CATEGORY_DEFAULT.
+ *
+ * @return A List<ResolveInfo> containing one entry for each matching
+ * Receiver. These are ordered from first to last in priority. If
+ * there are no matching receivers, an empty list is returned.
+ *
+ * @see #MATCH_DEFAULT_ONLY
+ * @see #GET_INTENT_FILTERS
+ * @see #GET_RESOLVED_FILTER
+ */
+ public abstract List<ResolveInfo> queryBroadcastReceivers(Intent intent,
+ int flags);
+
+ /**
+ * Determine the best service to handle for a given Intent.
+ *
+ * @param intent An intent containing all of the desired specification
+ * (action, data, type, category, and/or component).
+ * @param flags Additional option flags.
+ *
+ * @return Returns a ResolveInfo containing the final service intent that
+ * was determined to be the best action. Returns null if no
+ * matching service was found.
+ *
+ * @see #GET_INTENT_FILTERS
+ * @see #GET_RESOLVED_FILTER
+ */
+ public abstract ResolveInfo resolveService(Intent intent, int flags);
+
+ /**
+ * Retrieve all services that can match the given intent.
+ *
+ * @param intent The desired intent as per resolveService().
+ * @param flags Additional option flags.
+ *
+ * @return A List<ResolveInfo> containing one entry for each matching
+ * ServiceInfo. These are ordered from best to worst match -- that
+ * is, the first item in the list is what is returned by
+ * resolveService(). If there are no matching services, an empty
+ * list is returned.
+ *
+ * @see #GET_INTENT_FILTERS
+ * @see #GET_RESOLVED_FILTER
+ */
+ public abstract List<ResolveInfo> queryIntentServices(Intent intent,
+ int flags);
+
+ /**
+ * Find a single content provider by its base path name.
+ *
+ * @param name The name of the provider to find.
+ * @param flags Additional option flags. Currently should always be 0.
+ *
+ * @return ContentProviderInfo Information about the provider, if found,
+ * else null.
+ */
+ public abstract ProviderInfo resolveContentProvider(String name,
+ int flags);
+
+ /**
+ * Retrieve content provider information.
+ *
+ * <p><em>Note: unlike most other methods, an empty result set is indicated
+ * by a null return instead of an empty list.</em>
+ *
+ * @param processName If non-null, limits the returned providers to only
+ * those that are hosted by the given process. If null,
+ * all content providers are returned.
+ * @param uid If <var>processName</var> is non-null, this is the required
+ * uid owning the requested content providers.
+ * @param flags Additional option flags. Currently should always be 0.
+ *
+ * @return A List<ContentProviderInfo> containing one entry for each
+ * content provider either patching <var>processName</var> or, if
+ * <var>processName</var> is null, all known content providers.
+ * <em>If there are no matching providers, null is returned.</em>
+ */
+ public abstract List<ProviderInfo> queryContentProviders(
+ String processName, int uid, int flags);
+
+ /**
+ * Retrieve all of the information we know about a particular
+ * instrumentation class.
+ *
+ * <p>Throws {@link NameNotFoundException} if instrumentation with the
+ * given class name can not be found on the system.
+ *
+ * @param className The full name (i.e.
+ * com.google.apps.contacts.InstrumentList) of an
+ * Instrumentation class.
+ * @param flags Additional option flags. Currently should always be 0.
+ *
+ * @return InstrumentationInfo containing information about the
+ * instrumentation.
+ */
+ public abstract InstrumentationInfo getInstrumentationInfo(
+ ComponentName className, int flags) throws NameNotFoundException;
+
+ /**
+ * Retrieve information about available instrumentation code. May be used
+ * to retrieve either all instrumentation code, or only the code targeting
+ * a particular package.
+ *
+ * @param targetPackage If null, all instrumentation is returned; only the
+ * instrumentation targeting this package name is
+ * returned.
+ * @param flags Additional option flags. Currently should always be 0.
+ *
+ * @return A List<InstrumentationInfo> containing one entry for each
+ * matching available Instrumentation. Returns an empty list if
+ * there is no instrumentation available for the given package.
+ */
+ public abstract List<InstrumentationInfo> queryInstrumentation(
+ String targetPackage, int flags);
+
+ /**
+ * Retrieve an image from a package. This is a low-level API used by
+ * the various package manager info structures (such as
+ * {@link ComponentInfo} to implement retrieval of their associated
+ * icon.
+ *
+ * @param packageName The name of the package that this icon is coming from.
+ * Can not be null.
+ * @param resid The resource identifier of the desired image. Can not be 0.
+ * @param appInfo Overall information about <var>packageName</var>. This
+ * may be null, in which case the application information will be retrieved
+ * for you if needed; if you already have this information around, it can
+ * be much more efficient to supply it here.
+ *
+ * @return Returns a Drawable holding the requested image. Returns null if
+ * an image could not be found for any reason.
+ */
+ public abstract Drawable getDrawable(String packageName, int resid,
+ ApplicationInfo appInfo);
+
+ /**
+ * Retrieve the icon associated with an activity. Given the full name of
+ * an activity, retrieves the information about it and calls
+ * {@link ComponentInfo#loadIcon ComponentInfo.loadIcon()} to return its icon.
+ * If the activity can not be found, NameNotFoundException is thrown.
+ *
+ * @param activityName Name of the activity whose icon is to be retrieved.
+ *
+ * @return Returns the image of the icon, or the default activity icon if
+ * it could not be found. Does not return null.
+ * @throws NameNotFoundException Thrown if the resources for the given
+ * activity could not be loaded.
+ *
+ * @see #getActivityIcon(Intent)
+ */
+ public abstract Drawable getActivityIcon(ComponentName activityName)
+ throws NameNotFoundException;
+
+ /**
+ * Retrieve the icon associated with an Intent. If intent.getClassName() is
+ * set, this simply returns the result of
+ * getActivityIcon(intent.getClassName()). Otherwise it resolves the intent's
+ * component and returns the icon associated with the resolved component.
+ * If intent.getClassName() can not be found or the Intent can not be resolved
+ * to a component, NameNotFoundException is thrown.
+ *
+ * @param intent The intent for which you would like to retrieve an icon.
+ *
+ * @return Returns the image of the icon, or the default activity icon if
+ * it could not be found. Does not return null.
+ * @throws NameNotFoundException Thrown if the resources for application
+ * matching the given intent could not be loaded.
+ *
+ * @see #getActivityIcon(ComponentName)
+ */
+ public abstract Drawable getActivityIcon(Intent intent)
+ throws NameNotFoundException;
+
+ /**
+ * Return the generic icon for an activity that is used when no specific
+ * icon is defined.
+ *
+ * @return Drawable Image of the icon.
+ */
+ public abstract Drawable getDefaultActivityIcon();
+
+ /**
+ * Retrieve the icon associated with an application. If it has not defined
+ * an icon, the default app icon is returned. Does not return null.
+ *
+ * @param info Information about application being queried.
+ *
+ * @return Returns the image of the icon, or the default application icon
+ * if it could not be found.
+ *
+ * @see #getApplicationIcon(String)
+ */
+ public abstract Drawable getApplicationIcon(ApplicationInfo info);
+
+ /**
+ * Retrieve the icon associated with an application. Given the name of the
+ * application's package, retrieves the information about it and calls
+ * getApplicationIcon() to return its icon. If the application can not be
+ * found, NameNotFoundException is thrown.
+ *
+ * @param packageName Name of the package whose application icon is to be
+ * retrieved.
+ *
+ * @return Returns the image of the icon, or the default application icon
+ * if it could not be found. Does not return null.
+ * @throws NameNotFoundException Thrown if the resources for the given
+ * application could not be loaded.
+ *
+ * @see #getApplicationIcon(ApplicationInfo)
+ */
+ public abstract Drawable getApplicationIcon(String packageName)
+ throws NameNotFoundException;
+
+ /**
+ * Retrieve text from a package. This is a low-level API used by
+ * the various package manager info structures (such as
+ * {@link ComponentInfo} to implement retrieval of their associated
+ * labels and other text.
+ *
+ * @param packageName The name of the package that this text is coming from.
+ * Can not be null.
+ * @param resid The resource identifier of the desired text. Can not be 0.
+ * @param appInfo Overall information about <var>packageName</var>. This
+ * may be null, in which case the application information will be retrieved
+ * for you if needed; if you already have this information around, it can
+ * be much more efficient to supply it here.
+ *
+ * @return Returns a CharSequence holding the requested text. Returns null
+ * if the text could not be found for any reason.
+ */
+ public abstract CharSequence getText(String packageName, int resid,
+ ApplicationInfo appInfo);
+
+ /**
+ * Retrieve an XML file from a package. This is a low-level API used to
+ * retrieve XML meta data.
+ *
+ * @param packageName The name of the package that this xml is coming from.
+ * Can not be null.
+ * @param resid The resource identifier of the desired xml. Can not be 0.
+ * @param appInfo Overall information about <var>packageName</var>. This
+ * may be null, in which case the application information will be retrieved
+ * for you if needed; if you already have this information around, it can
+ * be much more efficient to supply it here.
+ *
+ * @return Returns an XmlPullParser allowing you to parse out the XML
+ * data. Returns null if the xml resource could not be found for any
+ * reason.
+ */
+ public abstract XmlResourceParser getXml(String packageName, int resid,
+ ApplicationInfo appInfo);
+
+ /**
+ * Return the label to use for this application.
+ *
+ * @return Returns the label associated with this application, or null if
+ * it could not be found for any reason.
+ * @param info The application to get the label of
+ */
+ public abstract CharSequence getApplicationLabel(ApplicationInfo info);
+
+ /**
+ * Retrieve the resources associated with an activity. Given the full
+ * name of an activity, retrieves the information about it and calls
+ * getResources() to return its application's resources. If the activity
+ * can not be found, NameNotFoundException is thrown.
+ *
+ * @param activityName Name of the activity whose resources are to be
+ * retrieved.
+ *
+ * @return Returns the application's Resources.
+ * @throws NameNotFoundException Thrown if the resources for the given
+ * application could not be loaded.
+ *
+ * @see #getResourcesForApplication(ApplicationInfo)
+ */
+ public abstract Resources getResourcesForActivity(ComponentName activityName)
+ throws NameNotFoundException;
+
+ /**
+ * Retrieve the resources for an application. Throws NameNotFoundException
+ * if the package is no longer installed.
+ *
+ * @param app Information about the desired application.
+ *
+ * @return Returns the application's Resources.
+ * @throws NameNotFoundException Thrown if the resources for the given
+ * application could not be loaded (most likely because it was uninstalled).
+ */
+ public abstract Resources getResourcesForApplication(ApplicationInfo app)
+ throws NameNotFoundException;
+
+ /**
+ * Retrieve the resources associated with an application. Given the full
+ * package name of an application, retrieves the information about it and
+ * calls getResources() to return its application's resources. If the
+ * appPackageName can not be found, NameNotFoundException is thrown.
+ *
+ * @param appPackageName Package name of the application whose resources
+ * are to be retrieved.
+ *
+ * @return Returns the application's Resources.
+ * @throws NameNotFoundException Thrown if the resources for the given
+ * application could not be loaded.
+ *
+ * @see #getResourcesForApplication(ApplicationInfo)
+ */
+ public abstract Resources getResourcesForApplication(String appPackageName)
+ throws NameNotFoundException;
+
+ /**
+ * Retrieve overall information about an application package defined
+ * in a package archive file
+ *
+ * @param archiveFilePath The path to the archive file
+ * @param flags Optional flags to control what information is returned. If
+ * 0, none of the optional information is returned.
+ *
+ * @return Returns the information about the package. Returns
+ * null if the package could not be successfully parsed.
+ *
+ * @see #GET_ACTIVITIES
+ * @see #GET_RECEIVERS
+ * @see #GET_SERVICES
+ * @see #GET_INSTRUMENTATION
+ * @see #GET_SIGNATURES
+ */
+ public PackageInfo getPackageArchiveInfo(String archiveFilePath, int flags) {
+ PackageParser packageParser = new PackageParser(archiveFilePath);
+ DisplayMetrics metrics = new DisplayMetrics();
+ metrics.setToDefaults();
+ final File sourceFile = new File(archiveFilePath);
+ PackageParser.Package pkg = packageParser.parsePackage(
+ sourceFile, archiveFilePath, metrics, 0);
+ if (pkg == null) {
+ return null;
+ }
+ return PackageParser.generatePackageInfo(pkg, null, flags);
+ }
+
+ /**
+ * Install a package. Since this may take a little while, the result will
+ * be posted back to the given observer. An installation will fail if the calling context
+ * lacks the {@link android.Manifest.permission#INSTALL_PACKAGES} permission, if the
+ * package named in the package file's manifest is already installed, or if there's no space
+ * available on the device.
+ *
+ * @param packageURI The location of the package file to install. This can be a 'file:' or a
+ * 'content:' URI.
+ * @param observer An observer callback to get notified when the package installation is
+ * complete. {@link IPackageInstallObserver#packageInstalled(String, int)} will be
+ * called when that happens. observer may be null to indicate that no callback is desired.
+ * @param flags - possible values: {@link #FORWARD_LOCK_PACKAGE},
+ * {@link #REPLACE_EXISTING_PACKAGE}
+ *
+ * @see #installPackage(android.net.Uri)
+ */
+ public abstract void installPackage(
+ Uri packageURI, IPackageInstallObserver observer, int flags);
+
+ /**
+ * Attempts to delete a package. Since this may take a little while, the result will
+ * be posted back to the given observer. A deletion will fail if the calling context
+ * lacks the {@link android.Manifest.permission#DELETE_PACKAGES} permission, if the
+ * named package cannot be found, or if the named package is a "system package".
+ * (TODO: include pointer to documentation on "system packages")
+ *
+ * @param packageName The name of the package to delete
+ * @param observer An observer callback to get notified when the package deletion is
+ * complete. {@link android.content.pm.IPackageDeleteObserver#packageDeleted(boolean)} will be
+ * called when that happens. observer may be null to indicate that no callback is desired.
+ * @param flags - possible values: {@link #DONT_DELETE_DATA}
+ *
+ * @hide
+ */
+ public abstract void deletePackage(
+ String packageName, IPackageDeleteObserver observer, int flags);
+ /**
+ * Attempts to clear the user data directory of an application.
+ * Since this may take a little while, the result will
+ * be posted back to the given observer. A deletion will fail if the
+ * named package cannot be found, or if the named package is a "system package".
+ *
+ * @param packageName The name of the package
+ * @param observer An observer callback to get notified when the operation is finished
+ * {@link android.content.pm.IPackageDataObserver#onRemoveCompleted(String, boolean)}
+ * will be called when that happens. observer may be null to indicate that
+ * no callback is desired.
+ *
+ * @hide
+ */
+ public abstract void clearApplicationUserData(String packageName,
+ IPackageDataObserver observer);
+ /**
+ * Attempts to delete the cache files associated with an application.
+ * Since this may take a little while, the result will
+ * be posted back to the given observer. A deletion will fail if the calling context
+ * lacks the {@link android.Manifest.permission#DELETE_CACHE_FILES} permission, if the
+ * named package cannot be found, or if the named package is a "system package".
+ *
+ * @param packageName The name of the package to delete
+ * @param observer An observer callback to get notified when the cache file deletion
+ * is complete.
+ * {@link android.content.pm.IPackageDataObserver#onRemoveCompleted(String, boolean)}
+ * will be called when that happens. observer may be null to indicate that
+ * no callback is desired.
+ *
+ * @hide
+ */
+ public abstract void deleteApplicationCacheFiles(String packageName,
+ IPackageDataObserver observer);
+
+ /**
+ * Free storage by deleting LRU sorted list of cache files across all applications.
+ * If the currently available free storage on the device is greater than or equal to the
+ * requested free storage, no cache files are cleared. If the currently available storage on the
+ * device is less than the requested free storage, some or all of the cache files across
+ * all applications are deleted(based on last accessed time) to increase the free storage
+ * space on the device to the requested value. There is no gurantee that clearing all
+ * the cache files from all applications will clear up enough storage to achieve the desired
+ * value.
+ * @param freeStorageSize The number of bytes of storage to be
+ * freed by the system. Say if freeStorageSize is XX,
+ * and the current free storage is YY,
+ * if XX is less than YY, just return. if not free XX-YY number of
+ * bytes if possible.
+ * @param observer callback used to notify when the operation is completed
+ * {@link android.content.pm.IPackageDataObserver#onRemoveCompleted(String, boolean)}
+ * will be called when that happens. observer may be null to indicate that
+ * no callback is desired.
+ *
+ * @hide
+ */
+ public abstract void freeApplicationCache(long freeStorageSize,
+ IPackageDataObserver observer);
+
+ /**
+ * Retrieve the size information for a package.
+ * Since this may take a little while, the result will
+ * be posted back to the given observer. The calling context
+ * should have the {@link android.Manifest.permission#GET_PACKAGE_SIZE} permission.
+ *
+ * @param packageName The name of the package whose size information is to be retrieved
+ * @param observer An observer callback to get notified when the operation
+ * is complete.
+ * {@link android.content.pm.IPackageStatsObserver#onGetStatsCompleted(PackageStats, boolean)}
+ * The observer's callback is invoked with a PackageStats object(containing the
+ * code, data and cache sizes of the package) and a boolean value representing
+ * the status of the operation. observer may be null to indicate that
+ * no callback is desired.
+ *
+ * @hide
+ */
+ public abstract void getPackageSizeInfo(String packageName,
+ IPackageStatsObserver observer);
+
+ /**
+ * Install a package.
+ *
+ * @param packageURI The location of the package file to install
+ *
+ * @see #installPackage(android.net.Uri, IPackageInstallObserver, int)
+ */
+ public void installPackage(Uri packageURI) {
+ installPackage(packageURI, null, 0);
+ }
+
+ /**
+ * Add a new package to the list of preferred packages. This new package
+ * will be added to the front of the list (removed from its current location
+ * if already listed), meaning it will now be preferred over all other
+ * packages when resolving conflicts.
+ *
+ * @param packageName The package name of the new package to make preferred.
+ */
+ public abstract void addPackageToPreferred(String packageName);
+
+ /**
+ * Remove a package from the list of preferred packages. If it was on
+ * the list, it will no longer be preferred over other packages.
+ *
+ * @param packageName The package name to remove.
+ */
+ public abstract void removePackageFromPreferred(String packageName);
+
+ /**
+ * Retrieve the list of all currently configured preferred packages. The
+ * first package on the list is the most preferred, the last is the
+ * least preferred.
+ *
+ * @param flags Optional flags to control what information is returned. If
+ * 0, none of the optional information is returned.
+ *
+ * @return Returns a list of PackageInfo objects describing each
+ * preferred application, in order of preference.
+ *
+ * @see #GET_ACTIVITIES
+ * @see #GET_RECEIVERS
+ * @see #GET_SERVICES
+ * @see #GET_INSTRUMENTATION
+ * @see #GET_SIGNATURES
+ */
+ public abstract List<PackageInfo> getPreferredPackages(int flags);
+
+ /**
+ * Add a new preferred activity mapping to the system. This will be used
+ * to automatically select the given activity component when
+ * {@link Context#startActivity(Intent) Context.startActivity()} finds
+ * multiple matching activities and also matches the given filter.
+ *
+ * @param filter The set of intents under which this activity will be
+ * made preferred.
+ * @param match The IntentFilter match category that this preference
+ * applies to.
+ * @param set The set of activities that the user was picking from when
+ * this preference was made.
+ * @param activity The component name of the activity that is to be
+ * preferred.
+ */
+ public abstract void addPreferredActivity(IntentFilter filter, int match,
+ ComponentName[] set, ComponentName activity);
+
+ /**
+ * Remove all preferred activity mappings, previously added with
+ * {@link #addPreferredActivity}, from the
+ * system whose activities are implemented in the given package name.
+ *
+ * @param packageName The name of the package whose preferred activity
+ * mappings are to be removed.
+ */
+ public abstract void clearPackagePreferredActivities(String packageName);
+
+ /**
+ * Retrieve all preferred activities, previously added with
+ * {@link #addPreferredActivity}, that are
+ * currently registered with the system.
+ *
+ * @param outFilters A list in which to place the filters of all of the
+ * preferred activities, or null for none.
+ * @param outActivities A list in which to place the component names of
+ * all of the preferred activities, or null for none.
+ * @param packageName An option package in which you would like to limit
+ * the list. If null, all activities will be returned; if non-null, only
+ * those activities in the given package are returned.
+ *
+ * @return Returns the total number of registered preferred activities
+ * (the number of distinct IntentFilter records, not the number of unique
+ * activity components) that were found.
+ */
+ public abstract int getPreferredActivities(List<IntentFilter> outFilters,
+ List<ComponentName> outActivities, String packageName);
+
+ /**
+ * Set the enabled setting for a package component (activity, receiver, service, provider).
+ * This setting will override any enabled state which may have been set by the component in its
+ * manifest.
+ *
+ * @param componentName The component to enable
+ * @param newState The new enabled state for the component. The legal values for this state
+ * are:
+ * {@link #COMPONENT_ENABLED_STATE_ENABLED},
+ * {@link #COMPONENT_ENABLED_STATE_DISABLED}
+ * and
+ * {@link #COMPONENT_ENABLED_STATE_DEFAULT}
+ * The last one removes the setting, thereby restoring the component's state to
+ * whatever was set in it's manifest (or enabled, by default).
+ * @param flags Optional behavior flags: {@link #DONT_KILL_APP} or 0.
+ */
+ public abstract void setComponentEnabledSetting(ComponentName componentName,
+ int newState, int flags);
+
+
+ /**
+ * Return the the enabled setting for a package component (activity,
+ * receiver, service, provider). This returns the last value set by
+ * {@link #setComponentEnabledSetting(ComponentName, int, int)}; in most
+ * cases this value will be {@link #COMPONENT_ENABLED_STATE_DEFAULT} since
+ * the value originally specified in the manifest has not been modified.
+ *
+ * @param componentName The component to retrieve.
+ * @return Returns the current enabled state for the component. May
+ * be one of {@link #COMPONENT_ENABLED_STATE_ENABLED},
+ * {@link #COMPONENT_ENABLED_STATE_DISABLED}, or
+ * {@link #COMPONENT_ENABLED_STATE_DEFAULT}. The last one means the
+ * component's enabled state is based on the original information in
+ * the manifest as found in {@link ComponentInfo}.
+ */
+ public abstract int getComponentEnabledSetting(ComponentName componentName);
+
+ /**
+ * Set the enabled setting for an application
+ * This setting will override any enabled state which may have been set by the application in
+ * its manifest. It also overrides the enabled state set in the manifest for any of the
+ * application's components. It does not override any enabled state set by
+ * {@link #setComponentEnabledSetting} for any of the application's components.
+ *
+ * @param packageName The package name of the application to enable
+ * @param newState The new enabled state for the component. The legal values for this state
+ * are:
+ * {@link #COMPONENT_ENABLED_STATE_ENABLED},
+ * {@link #COMPONENT_ENABLED_STATE_DISABLED}
+ * and
+ * {@link #COMPONENT_ENABLED_STATE_DEFAULT}
+ * The last one removes the setting, thereby restoring the applications's state to
+ * whatever was set in its manifest (or enabled, by default).
+ * @param flags Optional behavior flags: {@link #DONT_KILL_APP} or 0.
+ */
+ public abstract void setApplicationEnabledSetting(String packageName,
+ int newState, int flags);
+
+ /**
+ * Return the the enabled setting for an application. This returns
+ * the last value set by
+ * {@link #setApplicationEnabledSetting(String, int, int)}; in most
+ * cases this value will be {@link #COMPONENT_ENABLED_STATE_DEFAULT} since
+ * the value originally specified in the manifest has not been modified.
+ *
+ * @param packageName The component to retrieve.
+ * @return Returns the current enabled state for the component. May
+ * be one of {@link #COMPONENT_ENABLED_STATE_ENABLED},
+ * {@link #COMPONENT_ENABLED_STATE_DISABLED}, or
+ * {@link #COMPONENT_ENABLED_STATE_DEFAULT}. The last one means the
+ * application's enabled state is based on the original information in
+ * the manifest as found in {@link ComponentInfo}.
+ */
+ public abstract int getApplicationEnabledSetting(String packageName);
+}
diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java
new file mode 100644
index 0000000..5a90261
--- /dev/null
+++ b/core/java/android/content/pm/PackageParser.java
@@ -0,0 +1,2287 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.pm;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.AssetManager;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.os.Bundle;
+import android.os.PatternMatcher;
+import android.util.AttributeSet;
+import android.util.Config;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.TypedValue;
+import com.android.internal.util.XmlUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.ref.WeakReference;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateEncodingException;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.Iterator;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+
+/**
+ * Package archive parsing
+ *
+ * {@hide}
+ */
+public class PackageParser {
+
+ private String mArchiveSourcePath;
+ private String[] mSeparateProcesses;
+ private int mSdkVersion;
+
+ private int mParseError = PackageManager.INSTALL_SUCCEEDED;
+
+ private static final Object mSync = new Object();
+ private static WeakReference<byte[]> mReadBuffer;
+
+ /** If set to true, we will only allow package files that exactly match
+ * the DTD. Otherwise, we try to get as much from the package as we
+ * can without failing. This should normally be set to false, to
+ * support extensions to the DTD in future versions. */
+ private static final boolean RIGID_PARSER = false;
+
+ private static final String TAG = "PackageParser";
+
+ public PackageParser(String archiveSourcePath) {
+ mArchiveSourcePath = archiveSourcePath;
+ }
+
+ public void setSeparateProcesses(String[] procs) {
+ mSeparateProcesses = procs;
+ }
+
+ public void setSdkVersion(int sdkVersion) {
+ mSdkVersion = sdkVersion;
+ }
+
+ private static final boolean isPackageFilename(String name) {
+ return name.endsWith(".apk");
+ }
+
+ /**
+ * Generate and return the {@link PackageInfo} for a parsed package.
+ *
+ * @param p the parsed package.
+ * @param flags indicating which optional information is included.
+ */
+ public static PackageInfo generatePackageInfo(PackageParser.Package p,
+ int gids[], int flags) {
+
+ PackageInfo pi = new PackageInfo();
+ pi.packageName = p.packageName;
+ pi.versionCode = p.mVersionCode;
+ pi.versionName = p.mVersionName;
+ pi.applicationInfo = p.applicationInfo;
+ if ((flags&PackageManager.GET_GIDS) != 0) {
+ pi.gids = gids;
+ }
+ if ((flags&PackageManager.GET_ACTIVITIES) != 0) {
+ int N = p.activities.size();
+ if (N > 0) {
+ pi.activities = new ActivityInfo[N];
+ for (int i=0; i<N; i++) {
+ final Activity activity = p.activities.get(i);
+ if (activity.info.enabled
+ || (flags&PackageManager.GET_DISABLED_COMPONENTS) != 0) {
+ pi.activities[i] = generateActivityInfo(p.activities.get(i), flags);
+ }
+ }
+ }
+ }
+ if ((flags&PackageManager.GET_RECEIVERS) != 0) {
+ int N = p.receivers.size();
+ if (N > 0) {
+ pi.receivers = new ActivityInfo[N];
+ for (int i=0; i<N; i++) {
+ final Activity activity = p.receivers.get(i);
+ if (activity.info.enabled
+ || (flags&PackageManager.GET_DISABLED_COMPONENTS) != 0) {
+ pi.receivers[i] = generateActivityInfo(p.receivers.get(i), flags);
+ }
+ }
+ }
+ }
+ if ((flags&PackageManager.GET_SERVICES) != 0) {
+ int N = p.services.size();
+ if (N > 0) {
+ pi.services = new ServiceInfo[N];
+ for (int i=0; i<N; i++) {
+ final Service service = p.services.get(i);
+ if (service.info.enabled
+ || (flags&PackageManager.GET_DISABLED_COMPONENTS) != 0) {
+ pi.services[i] = generateServiceInfo(p.services.get(i), flags);
+ }
+ }
+ }
+ }
+ if ((flags&PackageManager.GET_PROVIDERS) != 0) {
+ int N = p.providers.size();
+ if (N > 0) {
+ pi.providers = new ProviderInfo[N];
+ for (int i=0; i<N; i++) {
+ final Provider provider = p.providers.get(i);
+ if (provider.info.enabled
+ || (flags&PackageManager.GET_DISABLED_COMPONENTS) != 0) {
+ pi.providers[i] = generateProviderInfo(p.providers.get(i), flags);
+ }
+ }
+ }
+ }
+ if ((flags&PackageManager.GET_INSTRUMENTATION) != 0) {
+ int N = p.instrumentation.size();
+ if (N > 0) {
+ pi.instrumentation = new InstrumentationInfo[N];
+ for (int i=0; i<N; i++) {
+ pi.instrumentation[i] = generateInstrumentationInfo(
+ p.instrumentation.get(i), flags);
+ }
+ }
+ }
+ if ((flags&PackageManager.GET_PERMISSIONS) != 0) {
+ int N = p.permissions.size();
+ if (N > 0) {
+ pi.permissions = new PermissionInfo[N];
+ for (int i=0; i<N; i++) {
+ pi.permissions[i] = generatePermissionInfo(p.permissions.get(i), flags);
+ }
+ }
+ N = p.requestedPermissions.size();
+ if (N > 0) {
+ pi.requestedPermissions = new String[N];
+ for (int i=0; i<N; i++) {
+ pi.requestedPermissions[i] = p.requestedPermissions.get(i);
+ }
+ }
+ }
+ if ((flags&PackageManager.GET_SIGNATURES) != 0) {
+ int N = p.mSignatures.length;
+ if (N > 0) {
+ pi.signatures = new Signature[N];
+ System.arraycopy(p.mSignatures, 0, pi.signatures, 0, N);
+ }
+ }
+ return pi;
+ }
+
+ private Certificate[] loadCertificates(JarFile jarFile, JarEntry je,
+ byte[] readBuffer) {
+ try {
+ // We must read the stream for the JarEntry to retrieve
+ // its certificates.
+ InputStream is = jarFile.getInputStream(je);
+ while (is.read(readBuffer, 0, readBuffer.length) != -1) {
+ // not using
+ }
+ is.close();
+ return je != null ? je.getCertificates() : null;
+ } catch (IOException e) {
+ Log.w(TAG, "Exception reading " + je.getName() + " in "
+ + jarFile.getName(), e);
+ }
+ return null;
+ }
+
+ public final static int PARSE_IS_SYSTEM = 0x0001;
+ public final static int PARSE_CHATTY = 0x0002;
+ public final static int PARSE_MUST_BE_APK = 0x0004;
+ public final static int PARSE_IGNORE_PROCESSES = 0x0008;
+
+ public int getParseError() {
+ return mParseError;
+ }
+
+ public Package parsePackage(File sourceFile, String destFileName,
+ DisplayMetrics metrics, int flags) {
+ mParseError = PackageManager.INSTALL_SUCCEEDED;
+
+ mArchiveSourcePath = sourceFile.getPath();
+ if (!sourceFile.isFile()) {
+ Log.w(TAG, "Skipping dir: " + mArchiveSourcePath);
+ mParseError = PackageManager.INSTALL_PARSE_FAILED_NOT_APK;
+ return null;
+ }
+ if (!isPackageFilename(sourceFile.getName())
+ && (flags&PARSE_MUST_BE_APK) != 0) {
+ if ((flags&PARSE_IS_SYSTEM) == 0) {
+ // We expect to have non-.apk files in the system dir,
+ // so don't warn about them.
+ Log.w(TAG, "Skipping non-package file: " + mArchiveSourcePath);
+ }
+ mParseError = PackageManager.INSTALL_PARSE_FAILED_NOT_APK;
+ return null;
+ }
+
+ if ((flags&PARSE_CHATTY) != 0 && Config.LOGD) Log.d(
+ TAG, "Scanning package: " + mArchiveSourcePath);
+
+ XmlResourceParser parser = null;
+ AssetManager assmgr = null;
+ try {
+ assmgr = new AssetManager();
+ assmgr.addAssetPath(mArchiveSourcePath);
+ parser = assmgr.openXmlResourceParser("AndroidManifest.xml");
+ } catch (Exception e) {
+ if (assmgr != null) assmgr.close();
+ Log.w(TAG, "Unable to read AndroidManifest.xml of "
+ + mArchiveSourcePath, e);
+ mParseError = PackageManager.INSTALL_PARSE_FAILED_BAD_MANIFEST;
+ return null;
+ }
+
+ String[] errorText = new String[1];
+ Package pkg = null;
+ Exception errorException = null;
+ try {
+ // XXXX todo: need to figure out correct configuration.
+ Resources res = new Resources(assmgr, metrics, null);
+ pkg = parsePackage(res, parser, flags, errorText);
+ } catch (Exception e) {
+ errorException = e;
+ mParseError = PackageManager.INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION;
+ }
+
+
+ if (pkg == null) {
+ if (errorException != null) {
+ Log.w(TAG, mArchiveSourcePath, errorException);
+ } else {
+ Log.w(TAG, mArchiveSourcePath + " (at "
+ + parser.getPositionDescription()
+ + "): " + errorText[0]);
+ }
+ parser.close();
+ assmgr.close();
+ if (mParseError == PackageManager.INSTALL_SUCCEEDED) {
+ mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
+ }
+ return null;
+ }
+
+ parser.close();
+ assmgr.close();
+
+ pkg.applicationInfo.sourceDir = destFileName;
+ pkg.applicationInfo.publicSourceDir = destFileName;
+ pkg.mSignatures = null;
+
+ return pkg;
+ }
+
+ public boolean collectCertificates(Package pkg, int flags) {
+ pkg.mSignatures = null;
+
+ WeakReference<byte[]> readBufferRef;
+ byte[] readBuffer = null;
+ synchronized (mSync) {
+ readBufferRef = mReadBuffer;
+ if (readBufferRef != null) {
+ mReadBuffer = null;
+ readBuffer = readBufferRef.get();
+ }
+ if (readBuffer == null) {
+ readBuffer = new byte[8192];
+ readBufferRef = new WeakReference<byte[]>(readBuffer);
+ }
+ }
+
+ try {
+ JarFile jarFile = new JarFile(mArchiveSourcePath);
+
+ Certificate[] certs = null;
+
+ if ((flags&PARSE_IS_SYSTEM) != 0) {
+ // If this package comes from the system image, then we
+ // can trust it... we'll just use the AndroidManifest.xml
+ // to retrieve its signatures, not validating all of the
+ // files.
+ JarEntry jarEntry = jarFile.getJarEntry("AndroidManifest.xml");
+ certs = loadCertificates(jarFile, jarEntry, readBuffer);
+ if (certs == null) {
+ Log.e(TAG, "Package " + pkg.packageName
+ + " has no certificates at entry "
+ + jarEntry.getName() + "; ignoring!");
+ jarFile.close();
+ mParseError = PackageManager.INSTALL_PARSE_FAILED_NO_CERTIFICATES;
+ return false;
+ }
+ if (false) {
+ Log.i(TAG, "File " + mArchiveSourcePath + ": entry=" + jarEntry
+ + " certs=" + (certs != null ? certs.length : 0));
+ if (certs != null) {
+ final int N = certs.length;
+ for (int i=0; i<N; i++) {
+ Log.i(TAG, " Public key: "
+ + certs[i].getPublicKey().getEncoded()
+ + " " + certs[i].getPublicKey());
+ }
+ }
+ }
+
+ } else {
+ Enumeration entries = jarFile.entries();
+ while (entries.hasMoreElements()) {
+ JarEntry je = (JarEntry)entries.nextElement();
+ if (je.isDirectory()) continue;
+ if (je.getName().startsWith("META-INF/")) continue;
+ Certificate[] localCerts = loadCertificates(jarFile, je,
+ readBuffer);
+ if (false) {
+ Log.i(TAG, "File " + mArchiveSourcePath + " entry " + je.getName()
+ + ": certs=" + certs + " ("
+ + (certs != null ? certs.length : 0) + ")");
+ }
+ if (localCerts == null) {
+ Log.e(TAG, "Package " + pkg.packageName
+ + " has no certificates at entry "
+ + je.getName() + "; ignoring!");
+ jarFile.close();
+ mParseError = PackageManager.INSTALL_PARSE_FAILED_NO_CERTIFICATES;
+ return false;
+ } else if (certs == null) {
+ certs = localCerts;
+ } else {
+ // Ensure all certificates match.
+ for (int i=0; i<certs.length; i++) {
+ boolean found = false;
+ for (int j=0; j<localCerts.length; j++) {
+ if (certs[i] != null &&
+ certs[i].equals(localCerts[j])) {
+ found = true;
+ break;
+ }
+ }
+ if (!found || certs.length != localCerts.length) {
+ Log.e(TAG, "Package " + pkg.packageName
+ + " has mismatched certificates at entry "
+ + je.getName() + "; ignoring!");
+ jarFile.close();
+ mParseError = PackageManager.INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES;
+ return false;
+ }
+ }
+ }
+ }
+ }
+ jarFile.close();
+
+ synchronized (mSync) {
+ mReadBuffer = readBufferRef;
+ }
+
+ if (certs != null && certs.length > 0) {
+ final int N = certs.length;
+ pkg.mSignatures = new Signature[certs.length];
+ for (int i=0; i<N; i++) {
+ pkg.mSignatures[i] = new Signature(
+ certs[i].getEncoded());
+ }
+ } else {
+ Log.e(TAG, "Package " + pkg.packageName
+ + " has no certificates; ignoring!");
+ mParseError = PackageManager.INSTALL_PARSE_FAILED_NO_CERTIFICATES;
+ return false;
+ }
+ } catch (CertificateEncodingException e) {
+ Log.w(TAG, "Exception reading " + mArchiveSourcePath, e);
+ mParseError = PackageManager.INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING;
+ return false;
+ } catch (IOException e) {
+ Log.w(TAG, "Exception reading " + mArchiveSourcePath, e);
+ mParseError = PackageManager.INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING;
+ return false;
+ } catch (RuntimeException e) {
+ Log.w(TAG, "Exception reading " + mArchiveSourcePath, e);
+ mParseError = PackageManager.INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION;
+ return false;
+ }
+
+ return true;
+ }
+
+ public static String parsePackageName(String packageFilePath, int flags) {
+ XmlResourceParser parser = null;
+ AssetManager assmgr = null;
+ try {
+ assmgr = new AssetManager();
+ int cookie = assmgr.addAssetPath(packageFilePath);
+ parser = assmgr.openXmlResourceParser(cookie, "AndroidManifest.xml");
+ } catch (Exception e) {
+ if (assmgr != null) assmgr.close();
+ Log.w(TAG, "Unable to read AndroidManifest.xml of "
+ + packageFilePath, e);
+ return null;
+ }
+ AttributeSet attrs = parser;
+ String errors[] = new String[1];
+ String packageName = null;
+ try {
+ packageName = parsePackageName(parser, attrs, flags, errors);
+ } catch (IOException e) {
+ Log.w(TAG, packageFilePath, e);
+ } catch (XmlPullParserException e) {
+ Log.w(TAG, packageFilePath, e);
+ } finally {
+ if (parser != null) parser.close();
+ if (assmgr != null) assmgr.close();
+ }
+ if (packageName == null) {
+ Log.e(TAG, "parsePackageName error: " + errors[0]);
+ return null;
+ }
+ return packageName;
+ }
+
+ private static String validateName(String name, boolean requiresSeparator) {
+ final int N = name.length();
+ boolean hasSep = false;
+ boolean front = true;
+ for (int i=0; i<N; i++) {
+ final char c = name.charAt(i);
+ if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
+ front = false;
+ continue;
+ }
+ if (!front) {
+ if ((c >= '0' && c <= '9') || c == '_') {
+ continue;
+ }
+ }
+ if (c == '.') {
+ hasSep = true;
+ front = true;
+ continue;
+ }
+ return "bad character '" + c + "'";
+ }
+ return hasSep || !requiresSeparator
+ ? null : "must have at least one '.' separator";
+ }
+
+ private static String parsePackageName(XmlPullParser parser,
+ AttributeSet attrs, int flags, String[] outError)
+ throws IOException, XmlPullParserException {
+
+ int type;
+ while ((type=parser.next()) != parser.START_TAG
+ && type != parser.END_DOCUMENT) {
+ ;
+ }
+
+ if (type != parser.START_TAG) {
+ outError[0] = "No start tag found";
+ return null;
+ }
+ if ((flags&PARSE_CHATTY) != 0 && Config.LOGV) Log.v(
+ TAG, "Root element name: '" + parser.getName() + "'");
+ if (!parser.getName().equals("manifest")) {
+ outError[0] = "No <manifest> tag";
+ return null;
+ }
+ String pkgName = attrs.getAttributeValue(null, "package");
+ if (pkgName == null || pkgName.length() == 0) {
+ outError[0] = "<manifest> does not specify package";
+ return null;
+ }
+ String nameError = validateName(pkgName, true);
+ if (nameError != null && !"android".equals(pkgName)) {
+ outError[0] = "<manifest> specifies bad package name \""
+ + pkgName + "\": " + nameError;
+ return null;
+ }
+
+ return pkgName.intern();
+ }
+
+ /**
+ * Temporary.
+ */
+ static public Signature stringToSignature(String str) {
+ final int N = str.length();
+ byte[] sig = new byte[N];
+ for (int i=0; i<N; i++) {
+ sig[i] = (byte)str.charAt(i);
+ }
+ return new Signature(sig);
+ }
+
+ private Package parsePackage(
+ Resources res, XmlResourceParser parser, int flags, String[] outError)
+ throws XmlPullParserException, IOException {
+ AttributeSet attrs = parser;
+
+ String pkgName = parsePackageName(parser, attrs, flags, outError);
+ if (pkgName == null) {
+ mParseError = PackageManager.INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME;
+ return null;
+ }
+ int type;
+
+ final Package pkg = new Package(pkgName);
+ pkg.mSystem = (flags&PARSE_IS_SYSTEM) != 0;
+ boolean foundApp = false;
+
+ TypedArray sa = res.obtainAttributes(attrs,
+ com.android.internal.R.styleable.AndroidManifest);
+ pkg.mVersionCode = sa.getInteger(
+ com.android.internal.R.styleable.AndroidManifest_versionCode, 0);
+ pkg.mVersionName = sa.getNonResourceString(
+ com.android.internal.R.styleable.AndroidManifest_versionName);
+ if (pkg.mVersionName != null) {
+ pkg.mVersionName = pkg.mVersionName.intern();
+ }
+ String str = sa.getNonResourceString(
+ com.android.internal.R.styleable.AndroidManifest_sharedUserId);
+ if (str != null) {
+ String nameError = validateName(str, true);
+ if (nameError != null && !"android".equals(pkgName)) {
+ outError[0] = "<manifest> specifies bad sharedUserId name \""
+ + str + "\": " + nameError;
+ mParseError = PackageManager.INSTALL_PARSE_FAILED_BAD_SHARED_USER_ID;
+ return null;
+ }
+ pkg.mSharedUserId = str.intern();
+ }
+ sa.recycle();
+
+ final int innerDepth = parser.getDepth();
+
+ int outerDepth = parser.getDepth();
+ while ((type=parser.next()) != parser.END_DOCUMENT
+ && (type != parser.END_TAG || parser.getDepth() > outerDepth)) {
+ if (type == parser.END_TAG || type == parser.TEXT) {
+ continue;
+ }
+
+ String tagName = parser.getName();
+ if (tagName.equals("application")) {
+ if (foundApp) {
+ if (RIGID_PARSER) {
+ outError[0] = "<manifest> has more than one <application>";
+ mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
+ return null;
+ } else {
+ Log.w(TAG, "<manifest> has more than one <application>");
+ XmlUtils.skipCurrentTag(parser);
+ continue;
+ }
+ }
+
+ foundApp = true;
+ if (!parseApplication(pkg, res, parser, attrs, flags, outError)) {
+ return null;
+ }
+ } else if (tagName.equals("permission-group")) {
+ if (parsePermissionGroup(pkg, res, parser, attrs, outError) == null) {
+ return null;
+ }
+ } else if (tagName.equals("permission")) {
+ if (parsePermission(pkg, res, parser, attrs, outError) == null) {
+ return null;
+ }
+ } else if (tagName.equals("permission-tree")) {
+ if (parsePermissionTree(pkg, res, parser, attrs, outError) == null) {
+ return null;
+ }
+ } else if (tagName.equals("uses-permission")) {
+ sa = res.obtainAttributes(attrs,
+ com.android.internal.R.styleable.AndroidManifestUsesPermission);
+
+ String name = sa.getNonResourceString(
+ com.android.internal.R.styleable.AndroidManifestUsesPermission_name);
+
+ sa.recycle();
+
+ if (name != null && !pkg.requestedPermissions.contains(name)) {
+ pkg.requestedPermissions.add(name);
+ }
+
+ XmlUtils.skipCurrentTag(parser);
+
+ } else if (tagName.equals("uses-sdk")) {
+ if (mSdkVersion > 0) {
+ sa = res.obtainAttributes(attrs,
+ com.android.internal.R.styleable.AndroidManifestUsesSdk);
+
+ int vers = sa.getInt(
+ com.android.internal.R.styleable.AndroidManifestUsesSdk_minSdkVersion, 0);
+
+ sa.recycle();
+
+ if (vers > mSdkVersion) {
+ outError[0] = "Requires newer sdk version #" + vers
+ + " (current version is #" + mSdkVersion + ")";
+ mParseError = PackageManager.INSTALL_FAILED_OLDER_SDK;
+ return null;
+ }
+ }
+
+ XmlUtils.skipCurrentTag(parser);
+
+ } else if (tagName.equals("instrumentation")) {
+ if (parseInstrumentation(pkg, res, parser, attrs, outError) == null) {
+ return null;
+ }
+ } else if (RIGID_PARSER) {
+ outError[0] = "Bad element under <manifest>: "
+ + parser.getName();
+ mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
+ return null;
+ } else {
+ Log.w(TAG, "Bad element under <manifest>: "
+ + parser.getName());
+ XmlUtils.skipCurrentTag(parser);
+ continue;
+ }
+ }
+
+ if (!foundApp && pkg.instrumentation.size() == 0) {
+ outError[0] = "<manifest> does not contain an <application> or <instrumentation>";
+ mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_EMPTY;
+ }
+
+ if (pkg.usesLibraries.size() > 0) {
+ pkg.usesLibraryFiles = new String[pkg.usesLibraries.size()];
+ pkg.usesLibraries.toArray(pkg.usesLibraryFiles);
+ }
+
+ return pkg;
+ }
+
+ private static String buildClassName(String pkg, CharSequence clsSeq,
+ String[] outError) {
+ if (clsSeq == null || clsSeq.length() <= 0) {
+ outError[0] = "Empty class name in package " + pkg;
+ return null;
+ }
+ String cls = clsSeq.toString();
+ char c = cls.charAt(0);
+ if (c == '.') {
+ return (pkg + cls).intern();
+ }
+ if (cls.indexOf('.') < 0) {
+ StringBuilder b = new StringBuilder(pkg);
+ b.append('.');
+ b.append(cls);
+ return b.toString().intern();
+ }
+ if (c >= 'a' && c <= 'z') {
+ return cls.intern();
+ }
+ outError[0] = "Bad class name " + cls + " in package " + pkg;
+ return null;
+ }
+
+ private static String buildCompoundName(String pkg,
+ CharSequence procSeq, String type, String[] outError) {
+ String proc = procSeq.toString();
+ char c = proc.charAt(0);
+ if (pkg != null && c == ':') {
+ if (proc.length() < 2) {
+ outError[0] = "Bad " + type + " name " + proc + " in package " + pkg
+ + ": must be at least two characters";
+ return null;
+ }
+ String subName = proc.substring(1);
+ String nameError = validateName(subName, false);
+ if (nameError != null) {
+ outError[0] = "Invalid " + type + " name " + proc + " in package "
+ + pkg + ": " + nameError;
+ return null;
+ }
+ return (pkg + proc).intern();
+ }
+ String nameError = validateName(proc, true);
+ if (nameError != null && !"system".equals(proc)) {
+ outError[0] = "Invalid " + type + " name " + proc + " in package "
+ + pkg + ": " + nameError;
+ return null;
+ }
+ return proc.intern();
+ }
+
+ private static String buildProcessName(String pkg, String defProc,
+ CharSequence procSeq, int flags, String[] separateProcesses,
+ String[] outError) {
+ if ((flags&PARSE_IGNORE_PROCESSES) != 0 && !"system".equals(procSeq)) {
+ return defProc != null ? defProc : pkg;
+ }
+ if (separateProcesses != null) {
+ for (int i=separateProcesses.length-1; i>=0; i--) {
+ String sp = separateProcesses[i];
+ if (sp.equals(pkg) || sp.equals(defProc) || sp.equals(procSeq)) {
+ return pkg;
+ }
+ }
+ }
+ if (procSeq == null || procSeq.length() <= 0) {
+ return defProc;
+ }
+ return buildCompoundName(pkg, procSeq, "package", outError);
+ }
+
+ private static String buildTaskAffinityName(String pkg, String defProc,
+ CharSequence procSeq, String[] outError) {
+ if (procSeq == null) {
+ return defProc;
+ }
+ if (procSeq.length() <= 0) {
+ return null;
+ }
+ return buildCompoundName(pkg, procSeq, "taskAffinity", outError);
+ }
+
+ private PermissionGroup parsePermissionGroup(Package owner, Resources res,
+ XmlPullParser parser, AttributeSet attrs, String[] outError)
+ throws XmlPullParserException, IOException {
+ PermissionGroup perm = new PermissionGroup(owner);
+
+ TypedArray sa = res.obtainAttributes(attrs,
+ com.android.internal.R.styleable.AndroidManifestPermissionGroup);
+
+ if (!parsePackageItemInfo(owner, perm.info, outError,
+ "<permission-group>", sa,
+ com.android.internal.R.styleable.AndroidManifestPermissionGroup_name,
+ com.android.internal.R.styleable.AndroidManifestPermissionGroup_label,
+ com.android.internal.R.styleable.AndroidManifestPermissionGroup_icon)) {
+ sa.recycle();
+ mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
+ return null;
+ }
+
+ perm.info.descriptionRes = sa.getResourceId(
+ com.android.internal.R.styleable.AndroidManifestPermissionGroup_description,
+ 0);
+
+ sa.recycle();
+
+ if (!parseAllMetaData(res, parser, attrs, "<permission-group>", perm,
+ outError)) {
+ mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
+ return null;
+ }
+
+ owner.permissionGroups.add(perm);
+
+ return perm;
+ }
+
+ private Permission parsePermission(Package owner, Resources res,
+ XmlPullParser parser, AttributeSet attrs, String[] outError)
+ throws XmlPullParserException, IOException {
+ Permission perm = new Permission(owner);
+
+ TypedArray sa = res.obtainAttributes(attrs,
+ com.android.internal.R.styleable.AndroidManifestPermission);
+
+ if (!parsePackageItemInfo(owner, perm.info, outError,
+ "<permission>", sa,
+ com.android.internal.R.styleable.AndroidManifestPermission_name,
+ com.android.internal.R.styleable.AndroidManifestPermission_label,
+ com.android.internal.R.styleable.AndroidManifestPermission_icon)) {
+ sa.recycle();
+ mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
+ return null;
+ }
+
+ perm.info.group = sa.getNonResourceString(
+ com.android.internal.R.styleable.AndroidManifestPermission_permissionGroup);
+ if (perm.info.group != null) {
+ perm.info.group = perm.info.group.intern();
+ }
+
+ perm.info.descriptionRes = sa.getResourceId(
+ com.android.internal.R.styleable.AndroidManifestPermission_description,
+ 0);
+
+ perm.info.protectionLevel = sa.getInt(
+ com.android.internal.R.styleable.AndroidManifestPermission_protectionLevel,
+ PermissionInfo.PROTECTION_NORMAL);
+
+ sa.recycle();
+
+ if (perm.info.protectionLevel == -1) {
+ outError[0] = "<permission> does not specify protectionLevel";
+ mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
+ return null;
+ }
+
+ if (!parseAllMetaData(res, parser, attrs, "<permission>", perm,
+ outError)) {
+ mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
+ return null;
+ }
+
+ owner.permissions.add(perm);
+
+ return perm;
+ }
+
+ private Permission parsePermissionTree(Package owner, Resources res,
+ XmlPullParser parser, AttributeSet attrs, String[] outError)
+ throws XmlPullParserException, IOException {
+ Permission perm = new Permission(owner);
+
+ TypedArray sa = res.obtainAttributes(attrs,
+ com.android.internal.R.styleable.AndroidManifestPermissionTree);
+
+ if (!parsePackageItemInfo(owner, perm.info, outError,
+ "<permission-tree>", sa,
+ com.android.internal.R.styleable.AndroidManifestPermissionTree_name,
+ com.android.internal.R.styleable.AndroidManifestPermissionTree_label,
+ com.android.internal.R.styleable.AndroidManifestPermissionTree_icon)) {
+ sa.recycle();
+ mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
+ return null;
+ }
+
+ sa.recycle();
+
+ int index = perm.info.name.indexOf('.');
+ if (index > 0) {
+ index = perm.info.name.indexOf('.', index+1);
+ }
+ if (index < 0) {
+ outError[0] = "<permission-tree> name has less than three segments: "
+ + perm.info.name;
+ mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
+ return null;
+ }
+
+ perm.info.descriptionRes = 0;
+ perm.info.protectionLevel = PermissionInfo.PROTECTION_NORMAL;
+ perm.tree = true;
+
+ if (!parseAllMetaData(res, parser, attrs, "<permission-tree>", perm,
+ outError)) {
+ mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
+ return null;
+ }
+
+ owner.permissions.add(perm);
+
+ return perm;
+ }
+
+ private Instrumentation parseInstrumentation(Package owner, Resources res,
+ XmlPullParser parser, AttributeSet attrs, String[] outError)
+ throws XmlPullParserException, IOException {
+ TypedArray sa = res.obtainAttributes(attrs,
+ com.android.internal.R.styleable.AndroidManifestInstrumentation);
+
+ Instrumentation a = new Instrumentation(owner);
+
+ if (!parsePackageItemInfo(owner, a.info, outError, "<instrumentation>", sa,
+ com.android.internal.R.styleable.AndroidManifestInstrumentation_name,
+ com.android.internal.R.styleable.AndroidManifestInstrumentation_label,
+ com.android.internal.R.styleable.AndroidManifestInstrumentation_icon)) {
+ sa.recycle();
+ mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
+ return null;
+ }
+
+ a.component = new ComponentName(owner.applicationInfo.packageName,
+ a.info.name);
+
+ String str;
+ str = sa.getNonResourceString(
+ com.android.internal.R.styleable.AndroidManifestInstrumentation_targetPackage);
+ a.info.targetPackage = str != null ? str.intern() : null;
+
+ a.info.handleProfiling = sa.getBoolean(
+ com.android.internal.R.styleable.AndroidManifestInstrumentation_handleProfiling,
+ false);
+
+ a.info.functionalTest = sa.getBoolean(
+ com.android.internal.R.styleable.AndroidManifestInstrumentation_functionalTest,
+ false);
+
+ sa.recycle();
+
+ if (a.info.targetPackage == null) {
+ outError[0] = "<instrumentation> does not specify targetPackage";
+ mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
+ return null;
+ }
+
+ if (!parseAllMetaData(res, parser, attrs, "<instrumentation>", a,
+ outError)) {
+ mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
+ return null;
+ }
+
+ owner.instrumentation.add(a);
+
+ return a;
+ }
+
+ private boolean parseApplication(Package owner, Resources res,
+ XmlPullParser parser, AttributeSet attrs, int flags, String[] outError)
+ throws XmlPullParserException, IOException {
+ final ApplicationInfo ai = owner.applicationInfo;
+ final String pkgName = owner.applicationInfo.packageName;
+
+ TypedArray sa = res.obtainAttributes(attrs,
+ com.android.internal.R.styleable.AndroidManifestApplication);
+
+ String name = sa.getNonResourceString(
+ com.android.internal.R.styleable.AndroidManifestApplication_name);
+ if (name != null) {
+ ai.className = buildClassName(pkgName, name, outError);
+ if (ai.className == null) {
+ sa.recycle();
+ mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
+ return false;
+ }
+ }
+
+ String manageSpaceActivity = sa.getNonResourceString(
+ com.android.internal.R.styleable.AndroidManifestApplication_manageSpaceActivity);
+ if (manageSpaceActivity != null) {
+ ai.manageSpaceActivityName = buildClassName(pkgName, manageSpaceActivity,
+ outError);
+ }
+
+ TypedValue v = sa.peekValue(
+ com.android.internal.R.styleable.AndroidManifestApplication_label);
+ if (v != null && (ai.labelRes=v.resourceId) == 0) {
+ ai.nonLocalizedLabel = v.coerceToString();
+ }
+
+ ai.icon = sa.getResourceId(
+ com.android.internal.R.styleable.AndroidManifestApplication_icon, 0);
+ ai.theme = sa.getResourceId(
+ com.android.internal.R.styleable.AndroidManifestApplication_theme, 0);
+ ai.descriptionRes = sa.getResourceId(
+ com.android.internal.R.styleable.AndroidManifestApplication_description, 0);
+
+ if ((flags&PARSE_IS_SYSTEM) != 0) {
+ if (sa.getBoolean(
+ com.android.internal.R.styleable.AndroidManifestApplication_persistent,
+ false)) {
+ ai.flags |= ApplicationInfo.FLAG_PERSISTENT;
+ }
+ }
+
+ if (sa.getBoolean(
+ com.android.internal.R.styleable.AndroidManifestApplication_debuggable,
+ false)) {
+ ai.flags |= ApplicationInfo.FLAG_DEBUGGABLE;
+ }
+
+ if (sa.getBoolean(
+ com.android.internal.R.styleable.AndroidManifestApplication_hasCode,
+ true)) {
+ ai.flags |= ApplicationInfo.FLAG_HAS_CODE;
+ }
+
+ if (sa.getBoolean(
+ com.android.internal.R.styleable.AndroidManifestApplication_allowTaskReparenting,
+ false)) {
+ ai.flags |= ApplicationInfo.FLAG_ALLOW_TASK_REPARENTING;
+ }
+
+ if (sa.getBoolean(
+ com.android.internal.R.styleable.AndroidManifestApplication_allowClearUserData,
+ true)) {
+ ai.flags |= ApplicationInfo.FLAG_ALLOW_CLEAR_USER_DATA;
+ }
+
+ String str;
+ str = sa.getNonResourceString(
+ com.android.internal.R.styleable.AndroidManifestApplication_permission);
+ ai.permission = (str != null && str.length() > 0) ? str.intern() : null;
+
+ str = sa.getNonResourceString(
+ com.android.internal.R.styleable.AndroidManifestApplication_taskAffinity);
+ ai.taskAffinity = buildTaskAffinityName(ai.packageName, ai.packageName,
+ str, outError);
+
+ if (outError[0] == null) {
+ ai.processName = buildProcessName(ai.packageName, null, sa.getNonResourceString(
+ com.android.internal.R.styleable.AndroidManifestApplication_process),
+ flags, mSeparateProcesses, outError);
+
+ ai.enabled = sa.getBoolean(com.android.internal.R.styleable.AndroidManifestApplication_enabled, true);
+ }
+
+ sa.recycle();
+
+ if (outError[0] != null) {
+ mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
+ return false;
+ } else if (ai.processName != null && !ai.processName.equals(ai.packageName)
+ && ai.className != null) {
+ Log.w(TAG, "In package " + ai.packageName
+ + " <application> specifies both a name and a process; ignoring the process");
+ ai.processName = null;
+ }
+
+ final int innerDepth = parser.getDepth();
+
+ int type;
+ while ((type=parser.next()) != parser.END_DOCUMENT
+ && (type != parser.END_TAG || parser.getDepth() > innerDepth)) {
+ if (type == parser.END_TAG || type == parser.TEXT) {
+ continue;
+ }
+
+ String tagName = parser.getName();
+ if (tagName.equals("activity")) {
+ Activity a = parseActivity(owner, res, parser, attrs, flags, outError, false);
+ if (a == null) {
+ mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
+ return false;
+ }
+
+ owner.activities.add(a);
+
+ } else if (tagName.equals("receiver")) {
+ Activity a = parseActivity(owner, res, parser, attrs, flags, outError, true);
+ if (a == null) {
+ mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
+ return false;
+ }
+
+ owner.receivers.add(a);
+
+ } else if (tagName.equals("service")) {
+ Service s = parseService(owner, res, parser, attrs, flags, outError);
+ if (s == null) {
+ mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
+ return false;
+ }
+
+ owner.services.add(s);
+
+ } else if (tagName.equals("provider")) {
+ Provider p = parseProvider(owner, res, parser, attrs, flags, outError);
+ if (p == null) {
+ mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
+ return false;
+ }
+
+ owner.providers.add(p);
+
+ } else if (tagName.equals("activity-alias")) {
+ Activity a = parseActivityAlias(owner, res, parser, attrs, flags, outError, false);
+ if (a == null) {
+ mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
+ return false;
+ }
+
+ owner.activities.add(a);
+
+ } else if (parser.getName().equals("meta-data")) {
+ // note: application meta-data is stored off to the side, so it can
+ // remain null in the primary copy (we like to avoid extra copies because
+ // it can be large)
+ if ((owner.mAppMetaData = parseMetaData(res, parser, attrs, owner.mAppMetaData,
+ outError)) == null) {
+ mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
+ return false;
+ }
+
+ } else if (tagName.equals("uses-library")) {
+ sa = res.obtainAttributes(attrs,
+ com.android.internal.R.styleable.AndroidManifestUsesLibrary);
+
+ String lname = sa.getNonResourceString(
+ com.android.internal.R.styleable.AndroidManifestUsesLibrary_name);
+
+ sa.recycle();
+
+ if (lname != null && !owner.usesLibraries.contains(lname)) {
+ owner.usesLibraries.add(lname);
+ }
+
+ XmlUtils.skipCurrentTag(parser);
+
+ } else {
+ if (!RIGID_PARSER) {
+ Log.w(TAG, "Problem in package " + mArchiveSourcePath + ":");
+ Log.w(TAG, "Unknown element under <application>: " + tagName);
+ XmlUtils.skipCurrentTag(parser);
+ continue;
+ } else {
+ outError[0] = "Bad element under <application>: " + tagName;
+ mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ private boolean parsePackageItemInfo(Package owner, PackageItemInfo outInfo,
+ String[] outError, String tag, TypedArray sa,
+ int nameRes, int labelRes, int iconRes) {
+ String name = sa.getNonResourceString(nameRes);
+ if (name == null) {
+ outError[0] = tag + " does not specify android:name";
+ return false;
+ }
+
+ outInfo.name
+ = buildClassName(owner.applicationInfo.packageName, name, outError);
+ if (outInfo.name == null) {
+ return false;
+ }
+
+ int iconVal = sa.getResourceId(iconRes, 0);
+ if (iconVal != 0) {
+ outInfo.icon = iconVal;
+ outInfo.nonLocalizedLabel = null;
+ }
+
+ TypedValue v = sa.peekValue(labelRes);
+ if (v != null && (outInfo.labelRes=v.resourceId) == 0) {
+ outInfo.nonLocalizedLabel = v.coerceToString();
+ }
+
+ outInfo.packageName = owner.packageName;
+
+ return true;
+ }
+
+ private boolean parseComponentInfo(Package owner, int flags,
+ ComponentInfo outInfo, String[] outError, String tag, TypedArray sa,
+ int nameRes, int labelRes, int iconRes, int processRes,
+ int enabledRes) {
+ if (!parsePackageItemInfo(owner, outInfo, outError, tag, sa,
+ nameRes, labelRes, iconRes)) {
+ return false;
+ }
+
+ if (processRes != 0) {
+ outInfo.processName = buildProcessName(owner.applicationInfo.packageName,
+ owner.applicationInfo.processName, sa.getNonResourceString(processRes),
+ flags, mSeparateProcesses, outError);
+ }
+ outInfo.enabled = sa.getBoolean(enabledRes, true);
+
+ return outError[0] == null;
+ }
+
+ private Activity parseActivity(Package owner, Resources res,
+ XmlPullParser parser, AttributeSet attrs, int flags, String[] outError,
+ boolean receiver) throws XmlPullParserException, IOException {
+ TypedArray sa = res.obtainAttributes(attrs,
+ com.android.internal.R.styleable.AndroidManifestActivity);
+
+ Activity a = new Activity(owner);
+
+ if (!parseComponentInfo(owner, flags, a.info, outError,
+ receiver ? "<receiver>" : "<activity>", sa,
+ com.android.internal.R.styleable.AndroidManifestActivity_name,
+ com.android.internal.R.styleable.AndroidManifestActivity_label,
+ com.android.internal.R.styleable.AndroidManifestActivity_icon,
+ com.android.internal.R.styleable.AndroidManifestActivity_process,
+ com.android.internal.R.styleable.AndroidManifestActivity_enabled)) {
+ sa.recycle();
+ return null;
+ }
+
+ final boolean setExported = sa.hasValue(
+ com.android.internal.R.styleable.AndroidManifestActivity_exported);
+ if (setExported) {
+ a.info.exported = sa.getBoolean(
+ com.android.internal.R.styleable.AndroidManifestActivity_exported, false);
+ }
+
+ a.component = new ComponentName(owner.applicationInfo.packageName,
+ a.info.name);
+
+ a.info.theme = sa.getResourceId(
+ com.android.internal.R.styleable.AndroidManifestActivity_theme, 0);
+
+ String str;
+ str = sa.getNonResourceString(
+ com.android.internal.R.styleable.AndroidManifestActivity_permission);
+ if (str == null) {
+ a.info.permission = owner.applicationInfo.permission;
+ } else {
+ a.info.permission = str.length() > 0 ? str.toString().intern() : null;
+ }
+
+ str = sa.getNonResourceString(
+ com.android.internal.R.styleable.AndroidManifestActivity_taskAffinity);
+ a.info.taskAffinity = buildTaskAffinityName(owner.applicationInfo.packageName,
+ owner.applicationInfo.taskAffinity, str, outError);
+
+ a.info.flags = 0;
+ if (sa.getBoolean(
+ com.android.internal.R.styleable.AndroidManifestActivity_multiprocess,
+ false)) {
+ a.info.flags |= ActivityInfo.FLAG_MULTIPROCESS;
+ }
+
+ if (sa.getBoolean(
+ com.android.internal.R.styleable.AndroidManifestActivity_finishOnTaskLaunch,
+ false)) {
+ a.info.flags |= ActivityInfo.FLAG_FINISH_ON_TASK_LAUNCH;
+ }
+
+ if (sa.getBoolean(
+ com.android.internal.R.styleable.AndroidManifestActivity_clearTaskOnLaunch,
+ false)) {
+ a.info.flags |= ActivityInfo.FLAG_CLEAR_TASK_ON_LAUNCH;
+ }
+
+ if (sa.getBoolean(
+ com.android.internal.R.styleable.AndroidManifestActivity_alwaysRetainTaskState,
+ false)) {
+ a.info.flags |= ActivityInfo.FLAG_ALWAYS_RETAIN_TASK_STATE;
+ }
+
+ if (sa.getBoolean(
+ com.android.internal.R.styleable.AndroidManifestActivity_stateNotNeeded,
+ false)) {
+ a.info.flags |= ActivityInfo.FLAG_STATE_NOT_NEEDED;
+ }
+
+ if (sa.getBoolean(
+ com.android.internal.R.styleable.AndroidManifestActivity_excludeFromRecents,
+ false)) {
+ a.info.flags |= ActivityInfo.FLAG_EXCLUDE_FROM_RECENTS;
+ }
+
+ if (sa.getBoolean(
+ com.android.internal.R.styleable.AndroidManifestActivity_allowTaskReparenting,
+ (owner.applicationInfo.flags&ApplicationInfo.FLAG_ALLOW_TASK_REPARENTING) != 0)) {
+ a.info.flags |= ActivityInfo.FLAG_ALLOW_TASK_REPARENTING;
+ }
+
+ if (!receiver) {
+ a.info.launchMode = sa.getInt(
+ com.android.internal.R.styleable.AndroidManifestActivity_launchMode,
+ ActivityInfo.LAUNCH_MULTIPLE);
+ a.info.screenOrientation = sa.getInt(
+ com.android.internal.R.styleable.AndroidManifestActivity_screenOrientation,
+ ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
+ a.info.configChanges = sa.getInt(
+ com.android.internal.R.styleable.AndroidManifestActivity_configChanges,
+ 0);
+ } else {
+ a.info.launchMode = ActivityInfo.LAUNCH_MULTIPLE;
+ a.info.configChanges = 0;
+ }
+
+ sa.recycle();
+
+ if (outError[0] != null) {
+ return null;
+ }
+
+ int outerDepth = parser.getDepth();
+ int type;
+ while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
+ && (type != XmlPullParser.END_TAG
+ || parser.getDepth() > outerDepth)) {
+ if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+ continue;
+ }
+
+ if (parser.getName().equals("intent-filter")) {
+ ActivityIntentInfo intent = new ActivityIntentInfo(a);
+ if (!parseIntent(res, parser, attrs, flags, intent, outError, !receiver)) {
+ return null;
+ }
+ if (intent.countActions() == 0) {
+ Log.w(TAG, "Intent filter for activity " + intent
+ + " defines no actions");
+ } else {
+ a.intents.add(intent);
+ }
+ } else if (parser.getName().equals("meta-data")) {
+ if ((a.metaData=parseMetaData(res, parser, attrs, a.metaData,
+ outError)) == null) {
+ return null;
+ }
+ } else {
+ if (!RIGID_PARSER) {
+ Log.w(TAG, "Problem in package " + mArchiveSourcePath + ":");
+ if (receiver) {
+ Log.w(TAG, "Unknown element under <receiver>: " + parser.getName());
+ } else {
+ Log.w(TAG, "Unknown element under <activity>: " + parser.getName());
+ }
+ XmlUtils.skipCurrentTag(parser);
+ continue;
+ }
+ if (receiver) {
+ outError[0] = "Bad element under <receiver>: " + parser.getName();
+ } else {
+ outError[0] = "Bad element under <activity>: " + parser.getName();
+ }
+ return null;
+ }
+ }
+
+ if (!setExported) {
+ a.info.exported = a.intents.size() > 0;
+ }
+
+ return a;
+ }
+
+ private Activity parseActivityAlias(Package owner, Resources res,
+ XmlPullParser parser, AttributeSet attrs, int flags, String[] outError,
+ boolean receiver) throws XmlPullParserException, IOException {
+ TypedArray sa = res.obtainAttributes(attrs,
+ com.android.internal.R.styleable.AndroidManifestActivityAlias);
+
+ String targetActivity = sa.getNonResourceString(
+ com.android.internal.R.styleable.AndroidManifestActivityAlias_targetActivity);
+ if (targetActivity == null) {
+ outError[0] = "<activity-alias> does not specify android:targetActivity";
+ sa.recycle();
+ return null;
+ }
+
+ targetActivity = buildClassName(owner.applicationInfo.packageName,
+ targetActivity, outError);
+ if (targetActivity == null) {
+ sa.recycle();
+ return null;
+ }
+
+ Activity a = new Activity(owner);
+ Activity target = null;
+
+ final int NA = owner.activities.size();
+ for (int i=0; i<NA; i++) {
+ Activity t = owner.activities.get(i);
+ if (targetActivity.equals(t.info.name)) {
+ target = t;
+ break;
+ }
+ }
+
+ if (target == null) {
+ outError[0] = "<activity-alias> target activity " + targetActivity
+ + " not found in manifest";
+ sa.recycle();
+ return null;
+ }
+
+ a.info.targetActivity = targetActivity;
+
+ a.info.configChanges = target.info.configChanges;
+ a.info.flags = target.info.flags;
+ a.info.icon = target.info.icon;
+ a.info.labelRes = target.info.labelRes;
+ a.info.launchMode = target.info.launchMode;
+ a.info.nonLocalizedLabel = target.info.nonLocalizedLabel;
+ a.info.processName = target.info.processName;
+ a.info.screenOrientation = target.info.screenOrientation;
+ a.info.taskAffinity = target.info.taskAffinity;
+ a.info.theme = target.info.theme;
+
+ if (!parseComponentInfo(owner, flags, a.info, outError,
+ receiver ? "<receiver>" : "<activity>", sa,
+ com.android.internal.R.styleable.AndroidManifestActivityAlias_name,
+ com.android.internal.R.styleable.AndroidManifestActivityAlias_label,
+ com.android.internal.R.styleable.AndroidManifestActivityAlias_icon,
+ 0,
+ com.android.internal.R.styleable.AndroidManifestActivityAlias_enabled)) {
+ sa.recycle();
+ return null;
+ }
+
+ final boolean setExported = sa.hasValue(
+ com.android.internal.R.styleable.AndroidManifestActivityAlias_exported);
+ if (setExported) {
+ a.info.exported = sa.getBoolean(
+ com.android.internal.R.styleable.AndroidManifestActivityAlias_exported, false);
+ }
+
+ a.component = new ComponentName(owner.applicationInfo.packageName,
+ a.info.name);
+
+ String str;
+ str = sa.getNonResourceString(
+ com.android.internal.R.styleable.AndroidManifestActivityAlias_permission);
+ if (str != null) {
+ a.info.permission = str.length() > 0 ? str.toString().intern() : null;
+ }
+
+ sa.recycle();
+
+ if (outError[0] != null) {
+ return null;
+ }
+
+ int outerDepth = parser.getDepth();
+ int type;
+ while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
+ && (type != XmlPullParser.END_TAG
+ || parser.getDepth() > outerDepth)) {
+ if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+ continue;
+ }
+
+ if (parser.getName().equals("intent-filter")) {
+ ActivityIntentInfo intent = new ActivityIntentInfo(a);
+ if (!parseIntent(res, parser, attrs, flags, intent, outError, true)) {
+ return null;
+ }
+ if (intent.countActions() == 0) {
+ Log.w(TAG, "Intent filter for activity alias " + intent
+ + " defines no actions");
+ } else {
+ a.intents.add(intent);
+ }
+ } else if (parser.getName().equals("meta-data")) {
+ if ((a.metaData=parseMetaData(res, parser, attrs, a.metaData,
+ outError)) == null) {
+ return null;
+ }
+ } else {
+ if (!RIGID_PARSER) {
+ Log.w(TAG, "Problem in package " + mArchiveSourcePath + ":");
+ Log.w(TAG, "Unknown element under <activity-alias>: " + parser.getName());
+ XmlUtils.skipCurrentTag(parser);
+ continue;
+ }
+ outError[0] = "Bad element under <activity-alias>: " + parser.getName();
+ return null;
+ }
+ }
+
+ if (!setExported) {
+ a.info.exported = a.intents.size() > 0;
+ }
+
+ return a;
+ }
+
+ private Provider parseProvider(Package owner, Resources res,
+ XmlPullParser parser, AttributeSet attrs, int flags, String[] outError)
+ throws XmlPullParserException, IOException {
+ TypedArray sa = res.obtainAttributes(attrs,
+ com.android.internal.R.styleable.AndroidManifestProvider);
+
+ Provider p = new Provider(owner);
+
+ if (!parseComponentInfo(owner, flags, p.info, outError, "<provider>", sa,
+ com.android.internal.R.styleable.AndroidManifestProvider_name,
+ com.android.internal.R.styleable.AndroidManifestProvider_label,
+ com.android.internal.R.styleable.AndroidManifestProvider_icon,
+ com.android.internal.R.styleable.AndroidManifestProvider_process,
+ com.android.internal.R.styleable.AndroidManifestProvider_enabled)) {
+ sa.recycle();
+ return null;
+ }
+
+ p.info.exported = sa.getBoolean(
+ com.android.internal.R.styleable.AndroidManifestProvider_exported, true);
+
+ p.component = new ComponentName(owner.applicationInfo.packageName,
+ p.info.name);
+
+ String cpname = sa.getNonResourceString(
+ com.android.internal.R.styleable.AndroidManifestProvider_authorities);
+
+ p.info.isSyncable = sa.getBoolean(
+ com.android.internal.R.styleable.AndroidManifestProvider_syncable,
+ false);
+
+ String permission = sa.getNonResourceString(
+ com.android.internal.R.styleable.AndroidManifestProvider_permission);
+ String str = sa.getNonResourceString(
+ com.android.internal.R.styleable.AndroidManifestProvider_readPermission);
+ if (str == null) {
+ str = permission;
+ }
+ if (str == null) {
+ p.info.readPermission = owner.applicationInfo.permission;
+ } else {
+ p.info.readPermission =
+ str.length() > 0 ? str.toString().intern() : null;
+ }
+ str = sa.getNonResourceString(
+ com.android.internal.R.styleable.AndroidManifestProvider_writePermission);
+ if (str == null) {
+ str = permission;
+ }
+ if (str == null) {
+ p.info.writePermission = owner.applicationInfo.permission;
+ } else {
+ p.info.writePermission =
+ str.length() > 0 ? str.toString().intern() : null;
+ }
+
+ p.info.grantUriPermissions = sa.getBoolean(
+ com.android.internal.R.styleable.AndroidManifestProvider_grantUriPermissions,
+ false);
+
+ p.info.multiprocess = sa.getBoolean(
+ com.android.internal.R.styleable.AndroidManifestProvider_multiprocess,
+ false);
+
+ p.info.initOrder = sa.getInt(
+ com.android.internal.R.styleable.AndroidManifestProvider_initOrder,
+ 0);
+
+ sa.recycle();
+
+ if (cpname == null) {
+ outError[0] = "<provider> does not incude authorities attribute";
+ return null;
+ }
+ p.info.authority = cpname.intern();
+
+ if (!parseProviderTags(res, parser, attrs, p, outError)) {
+ return null;
+ }
+
+ return p;
+ }
+
+ private boolean parseProviderTags(Resources res,
+ XmlPullParser parser, AttributeSet attrs,
+ Provider outInfo, String[] outError)
+ throws XmlPullParserException, IOException {
+ int outerDepth = parser.getDepth();
+ int type;
+ while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
+ && (type != XmlPullParser.END_TAG
+ || parser.getDepth() > outerDepth)) {
+ if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+ continue;
+ }
+
+ if (parser.getName().equals("meta-data")) {
+ if ((outInfo.metaData=parseMetaData(res, parser, attrs,
+ outInfo.metaData, outError)) == null) {
+ return false;
+ }
+ } else if (parser.getName().equals("grant-uri-permission")) {
+ TypedArray sa = res.obtainAttributes(attrs,
+ com.android.internal.R.styleable.AndroidManifestGrantUriPermission);
+
+ PatternMatcher pa = null;
+
+ String str = sa.getNonResourceString(
+ com.android.internal.R.styleable.AndroidManifestGrantUriPermission_path);
+ if (str != null) {
+ pa = new PatternMatcher(str, PatternMatcher.PATTERN_LITERAL);
+ }
+
+ str = sa.getNonResourceString(
+ com.android.internal.R.styleable.AndroidManifestGrantUriPermission_pathPrefix);
+ if (str != null) {
+ pa = new PatternMatcher(str, PatternMatcher.PATTERN_PREFIX);
+ }
+
+ str = sa.getNonResourceString(
+ com.android.internal.R.styleable.AndroidManifestGrantUriPermission_pathPattern);
+ if (str != null) {
+ pa = new PatternMatcher(str, PatternMatcher.PATTERN_SIMPLE_GLOB);
+ }
+
+ sa.recycle();
+
+ if (pa != null) {
+ if (outInfo.info.uriPermissionPatterns == null) {
+ outInfo.info.uriPermissionPatterns = new PatternMatcher[1];
+ outInfo.info.uriPermissionPatterns[0] = pa;
+ } else {
+ final int N = outInfo.info.uriPermissionPatterns.length;
+ PatternMatcher[] newp = new PatternMatcher[N+1];
+ System.arraycopy(outInfo.info.uriPermissionPatterns, 0, newp, 0, N);
+ newp[N] = pa;
+ outInfo.info.uriPermissionPatterns = newp;
+ }
+ outInfo.info.grantUriPermissions = true;
+ }
+ XmlUtils.skipCurrentTag(parser);
+
+ } else {
+ if (!RIGID_PARSER) {
+ Log.w(TAG, "Problem in package " + mArchiveSourcePath + ":");
+ Log.w(TAG, "Unknown element under <provider>: "
+ + parser.getName());
+ XmlUtils.skipCurrentTag(parser);
+ continue;
+ }
+ outError[0] = "Bad element under <provider>: "
+ + parser.getName();
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private Service parseService(Package owner, Resources res,
+ XmlPullParser parser, AttributeSet attrs, int flags, String[] outError)
+ throws XmlPullParserException, IOException {
+ TypedArray sa = res.obtainAttributes(attrs,
+ com.android.internal.R.styleable.AndroidManifestService);
+
+ Service s = new Service(owner);
+
+ if (!parseComponentInfo(owner, flags, s.info, outError, "<service>", sa,
+ com.android.internal.R.styleable.AndroidManifestService_name,
+ com.android.internal.R.styleable.AndroidManifestService_label,
+ com.android.internal.R.styleable.AndroidManifestService_icon,
+ com.android.internal.R.styleable.AndroidManifestService_process,
+ com.android.internal.R.styleable.AndroidManifestService_enabled)) {
+ sa.recycle();
+ return null;
+ }
+
+ final boolean setExported = sa.hasValue(
+ com.android.internal.R.styleable.AndroidManifestService_exported);
+ if (setExported) {
+ s.info.exported = sa.getBoolean(
+ com.android.internal.R.styleable.AndroidManifestService_exported, false);
+ }
+
+ s.component = new ComponentName(owner.applicationInfo.packageName,
+ s.info.name);
+
+ String str = sa.getNonResourceString(
+ com.android.internal.R.styleable.AndroidManifestService_permission);
+ if (str == null) {
+ s.info.permission = owner.applicationInfo.permission;
+ } else {
+ s.info.permission = str.length() > 0 ? str.toString().intern() : null;
+ }
+
+ sa.recycle();
+
+ int outerDepth = parser.getDepth();
+ int type;
+ while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
+ && (type != XmlPullParser.END_TAG
+ || parser.getDepth() > outerDepth)) {
+ if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+ continue;
+ }
+
+ if (parser.getName().equals("intent-filter")) {
+ ServiceIntentInfo intent = new ServiceIntentInfo(s);
+ if (!parseIntent(res, parser, attrs, flags, intent, outError, false)) {
+ return null;
+ }
+
+ s.intents.add(intent);
+ } else if (parser.getName().equals("meta-data")) {
+ if ((s.metaData=parseMetaData(res, parser, attrs, s.metaData,
+ outError)) == null) {
+ return null;
+ }
+ } else {
+ if (!RIGID_PARSER) {
+ Log.w(TAG, "Problem in package " + mArchiveSourcePath + ":");
+ Log.w(TAG, "Unknown element under <service>: "
+ + parser.getName());
+ XmlUtils.skipCurrentTag(parser);
+ continue;
+ }
+ outError[0] = "Bad element under <service>: "
+ + parser.getName();
+ return null;
+ }
+ }
+
+ if (!setExported) {
+ s.info.exported = s.intents.size() > 0;
+ }
+
+ return s;
+ }
+
+ private boolean parseAllMetaData(Resources res,
+ XmlPullParser parser, AttributeSet attrs, String tag,
+ Component outInfo, String[] outError)
+ throws XmlPullParserException, IOException {
+ int outerDepth = parser.getDepth();
+ int type;
+ while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
+ && (type != XmlPullParser.END_TAG
+ || parser.getDepth() > outerDepth)) {
+ if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+ continue;
+ }
+
+ if (parser.getName().equals("meta-data")) {
+ if ((outInfo.metaData=parseMetaData(res, parser, attrs,
+ outInfo.metaData, outError)) == null) {
+ return false;
+ }
+ } else {
+ if (!RIGID_PARSER) {
+ Log.w(TAG, "Problem in package " + mArchiveSourcePath + ":");
+ Log.w(TAG, "Unknown element under " + tag + ": "
+ + parser.getName());
+ XmlUtils.skipCurrentTag(parser);
+ continue;
+ }
+ outError[0] = "Bad element under " + tag + ": "
+ + parser.getName();
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private Bundle parseMetaData(Resources res,
+ XmlPullParser parser, AttributeSet attrs,
+ Bundle data, String[] outError)
+ throws XmlPullParserException, IOException {
+
+ TypedArray sa = res.obtainAttributes(attrs,
+ com.android.internal.R.styleable.AndroidManifestMetaData);
+
+ if (data == null) {
+ data = new Bundle();
+ }
+
+ String name = sa.getNonResourceString(
+ com.android.internal.R.styleable.AndroidManifestMetaData_name);
+ if (name == null) {
+ outError[0] = "<meta-data> requires an android:name attribute";
+ sa.recycle();
+ return null;
+ }
+
+ boolean success = true;
+
+ TypedValue v = sa.peekValue(
+ com.android.internal.R.styleable.AndroidManifestMetaData_resource);
+ if (v != null && v.resourceId != 0) {
+ //Log.i(TAG, "Meta data ref " + name + ": " + v);
+ data.putInt(name, v.resourceId);
+ } else {
+ v = sa.peekValue(
+ com.android.internal.R.styleable.AndroidManifestMetaData_value);
+ //Log.i(TAG, "Meta data " + name + ": " + v);
+ if (v != null) {
+ if (v.type == TypedValue.TYPE_STRING) {
+ CharSequence cs = v.coerceToString();
+ data.putString(name, cs != null ? cs.toString() : null);
+ } else if (v.type == TypedValue.TYPE_INT_BOOLEAN) {
+ data.putBoolean(name, v.data != 0);
+ } else if (v.type >= TypedValue.TYPE_FIRST_INT
+ && v.type <= TypedValue.TYPE_LAST_INT) {
+ data.putInt(name, v.data);
+ } else if (v.type == TypedValue.TYPE_FLOAT) {
+ data.putFloat(name, v.getFloat());
+ } else {
+ if (!RIGID_PARSER) {
+ Log.w(TAG, "Problem in package " + mArchiveSourcePath + ":");
+ Log.w(TAG, "<meta-data> only supports string, integer, float, color, boolean, and resource reference types");
+ } else {
+ outError[0] = "<meta-data> only supports string, integer, float, color, boolean, and resource reference types";
+ data = null;
+ }
+ }
+ } else {
+ outError[0] = "<meta-data> requires an android:value or android:resource attribute";
+ data = null;
+ }
+ }
+
+ sa.recycle();
+
+ XmlUtils.skipCurrentTag(parser);
+
+ return data;
+ }
+
+ private static final String ANDROID_RESOURCES
+ = "http://schemas.android.com/apk/res/android";
+
+ private boolean parseIntent(Resources res,
+ XmlPullParser parser, AttributeSet attrs, int flags,
+ IntentInfo outInfo, String[] outError, boolean isActivity)
+ throws XmlPullParserException, IOException {
+
+ TypedArray sa = res.obtainAttributes(attrs,
+ com.android.internal.R.styleable.AndroidManifestIntentFilter);
+
+ int priority = sa.getInt(
+ com.android.internal.R.styleable.AndroidManifestIntentFilter_priority, 0);
+ if (priority > 0 && isActivity && (flags&PARSE_IS_SYSTEM) == 0) {
+ Log.w(TAG, "Activity with priority > 0, forcing to 0 at "
+ + parser.getPositionDescription());
+ priority = 0;
+ }
+ outInfo.setPriority(priority);
+
+ TypedValue v = sa.peekValue(
+ com.android.internal.R.styleable.AndroidManifestIntentFilter_label);
+ if (v != null && (outInfo.labelRes=v.resourceId) == 0) {
+ outInfo.nonLocalizedLabel = v.coerceToString();
+ }
+
+ outInfo.icon = sa.getResourceId(
+ com.android.internal.R.styleable.AndroidManifestIntentFilter_icon, 0);
+
+ sa.recycle();
+
+ int outerDepth = parser.getDepth();
+ int type;
+ while ((type=parser.next()) != parser.END_DOCUMENT
+ && (type != parser.END_TAG || parser.getDepth() > outerDepth)) {
+ if (type == parser.END_TAG || type == parser.TEXT) {
+ continue;
+ }
+
+ String nodeName = parser.getName();
+ if (nodeName.equals("action")) {
+ String value = attrs.getAttributeValue(
+ ANDROID_RESOURCES, "name");
+ if (value == null || value == "") {
+ outError[0] = "No value supplied for <android:name>";
+ return false;
+ }
+ XmlUtils.skipCurrentTag(parser);
+
+ outInfo.addAction(value);
+ } else if (nodeName.equals("category")) {
+ String value = attrs.getAttributeValue(
+ ANDROID_RESOURCES, "name");
+ if (value == null || value == "") {
+ outError[0] = "No value supplied for <android:name>";
+ return false;
+ }
+ XmlUtils.skipCurrentTag(parser);
+
+ outInfo.addCategory(value);
+
+ } else if (nodeName.equals("data")) {
+ sa = res.obtainAttributes(attrs,
+ com.android.internal.R.styleable.AndroidManifestData);
+
+ String str = sa.getNonResourceString(
+ com.android.internal.R.styleable.AndroidManifestData_mimeType);
+ if (str != null) {
+ try {
+ outInfo.addDataType(str);
+ } catch (IntentFilter.MalformedMimeTypeException e) {
+ outError[0] = e.toString();
+ sa.recycle();
+ return false;
+ }
+ }
+
+ str = sa.getNonResourceString(
+ com.android.internal.R.styleable.AndroidManifestData_scheme);
+ if (str != null) {
+ outInfo.addDataScheme(str);
+ }
+
+ String host = sa.getNonResourceString(
+ com.android.internal.R.styleable.AndroidManifestData_host);
+ String port = sa.getNonResourceString(
+ com.android.internal.R.styleable.AndroidManifestData_port);
+ if (host != null) {
+ outInfo.addDataAuthority(host, port);
+ }
+
+ str = sa.getNonResourceString(
+ com.android.internal.R.styleable.AndroidManifestData_path);
+ if (str != null) {
+ outInfo.addDataPath(str, PatternMatcher.PATTERN_LITERAL);
+ }
+
+ str = sa.getNonResourceString(
+ com.android.internal.R.styleable.AndroidManifestData_pathPrefix);
+ if (str != null) {
+ outInfo.addDataPath(str, PatternMatcher.PATTERN_PREFIX);
+ }
+
+ str = sa.getNonResourceString(
+ com.android.internal.R.styleable.AndroidManifestData_pathPattern);
+ if (str != null) {
+ outInfo.addDataPath(str, PatternMatcher.PATTERN_SIMPLE_GLOB);
+ }
+
+ sa.recycle();
+ XmlUtils.skipCurrentTag(parser);
+ } else if (!RIGID_PARSER) {
+ Log.w(TAG, "Problem in package " + mArchiveSourcePath + ":");
+ Log.w(TAG, "Unknown element under <intent-filter>: " + parser.getName());
+ XmlUtils.skipCurrentTag(parser);
+ } else {
+ outError[0] = "Bad element under <intent-filter>: " + parser.getName();
+ return false;
+ }
+ }
+
+ outInfo.hasDefault = outInfo.hasCategory(Intent.CATEGORY_DEFAULT);
+ if (false) {
+ String cats = "";
+ Iterator<String> it = outInfo.categoriesIterator();
+ while (it != null && it.hasNext()) {
+ cats += " " + it.next();
+ }
+ System.out.println("Intent d=" +
+ outInfo.hasDefault + ", cat=" + cats);
+ }
+
+ return true;
+ }
+
+ public final static class Package {
+ public final String packageName;
+
+ // For now we only support one application per package.
+ public final ApplicationInfo applicationInfo = new ApplicationInfo();
+
+ public final ArrayList<Permission> permissions = new ArrayList<Permission>(0);
+ public final ArrayList<PermissionGroup> permissionGroups = new ArrayList<PermissionGroup>(0);
+ public final ArrayList<Activity> activities = new ArrayList<Activity>(0);
+ public final ArrayList<Activity> receivers = new ArrayList<Activity>(0);
+ public final ArrayList<Provider> providers = new ArrayList<Provider>(0);
+ public final ArrayList<Service> services = new ArrayList<Service>(0);
+ public final ArrayList<Instrumentation> instrumentation = new ArrayList<Instrumentation>(0);
+
+ public final ArrayList<String> requestedPermissions = new ArrayList<String>();
+
+ public final ArrayList<String> usesLibraries = new ArrayList<String>();
+ public String[] usesLibraryFiles = null;
+
+ // We store the application meta-data independently to avoid multiple unwanted references
+ public Bundle mAppMetaData = null;
+
+ // If this is a 3rd party app, this is the path of the zip file.
+ public String mPath;
+
+ // True if this package is part of the system image.
+ public boolean mSystem;
+
+ // The version code declared for this package.
+ public int mVersionCode;
+
+ // The version name declared for this package.
+ public String mVersionName;
+
+ // The shared user id that this package wants to use.
+ public String mSharedUserId;
+
+ // Signatures that were read from the package.
+ public Signature mSignatures[];
+
+ // For use by package manager service for quick lookup of
+ // preferred up order.
+ public int mPreferredOrder = 0;
+
+ // Additional data supplied by callers.
+ public Object mExtras;
+
+ public Package(String _name) {
+ packageName = _name;
+ applicationInfo.packageName = _name;
+ applicationInfo.uid = -1;
+ }
+
+ public String toString() {
+ return "Package{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " " + packageName + "}";
+ }
+ }
+
+ public static class Component<II extends IntentInfo> {
+ public final Package owner;
+ public final ArrayList<II> intents = new ArrayList<II>(0);
+ public ComponentName component;
+ public Bundle metaData;
+
+ public Component(Package _owner) {
+ owner = _owner;
+ }
+
+ public Component(Component<II> clone) {
+ owner = clone.owner;
+ metaData = clone.metaData;
+ }
+ }
+
+ public final static class Permission extends Component<IntentInfo> {
+ public final PermissionInfo info;
+ public boolean tree;
+ public PermissionGroup group;
+
+ public Permission(Package _owner) {
+ super(_owner);
+ info = new PermissionInfo();
+ }
+
+ public Permission(Package _owner, PermissionInfo _info) {
+ super(_owner);
+ info = _info;
+ }
+
+ public String toString() {
+ return "Permission{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " " + info.name + "}";
+ }
+ }
+
+ public final static class PermissionGroup extends Component<IntentInfo> {
+ public final PermissionGroupInfo info;
+
+ public PermissionGroup(Package _owner) {
+ super(_owner);
+ info = new PermissionGroupInfo();
+ }
+
+ public PermissionGroup(Package _owner, PermissionGroupInfo _info) {
+ super(_owner);
+ info = _info;
+ }
+
+ public String toString() {
+ return "PermissionGroup{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " " + info.name + "}";
+ }
+ }
+
+ private static boolean copyNeeded(int flags, Package p, Bundle metaData) {
+ if ((flags & PackageManager.GET_META_DATA) != 0
+ && (metaData != null || p.mAppMetaData != null)) {
+ return true;
+ }
+ if ((flags & PackageManager.GET_SHARED_LIBRARY_FILES) != 0
+ && p.usesLibraryFiles != null) {
+ return true;
+ }
+ return false;
+ }
+
+ public static ApplicationInfo generateApplicationInfo(Package p, int flags) {
+ if (p == null) return null;
+ if (!copyNeeded(flags, p, null)) {
+ return p.applicationInfo;
+ }
+
+ // Make shallow copy so we can store the metadata/libraries safely
+ ApplicationInfo ai = new ApplicationInfo(p.applicationInfo);
+ if ((flags & PackageManager.GET_META_DATA) != 0) {
+ ai.metaData = p.mAppMetaData;
+ }
+ if ((flags & PackageManager.GET_SHARED_LIBRARY_FILES) != 0) {
+ ai.sharedLibraryFiles = p.usesLibraryFiles;
+ }
+ return ai;
+ }
+
+ public static final PermissionInfo generatePermissionInfo(
+ Permission p, int flags) {
+ if (p == null) return null;
+ if ((flags&PackageManager.GET_META_DATA) == 0) {
+ return p.info;
+ }
+ PermissionInfo pi = new PermissionInfo(p.info);
+ pi.metaData = p.metaData;
+ return pi;
+ }
+
+ public static final PermissionGroupInfo generatePermissionGroupInfo(
+ PermissionGroup pg, int flags) {
+ if (pg == null) return null;
+ if ((flags&PackageManager.GET_META_DATA) == 0) {
+ return pg.info;
+ }
+ PermissionGroupInfo pgi = new PermissionGroupInfo(pg.info);
+ pgi.metaData = pg.metaData;
+ return pgi;
+ }
+
+ public final static class Activity extends Component<ActivityIntentInfo> {
+ public final ActivityInfo info =
+ new ActivityInfo();
+
+ public Activity(Package _owner) {
+ super(_owner);
+ info.applicationInfo = owner.applicationInfo;
+ }
+
+ public String toString() {
+ return "Activity{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " " + component.flattenToString() + "}";
+ }
+ }
+
+ public static final ActivityInfo generateActivityInfo(Activity a,
+ int flags) {
+ if (a == null) return null;
+ if (!copyNeeded(flags, a.owner, a.metaData)) {
+ return a.info;
+ }
+ // Make shallow copies so we can store the metadata safely
+ ActivityInfo ai = new ActivityInfo(a.info);
+ ai.metaData = a.metaData;
+ ai.applicationInfo = generateApplicationInfo(a.owner, flags);
+ return ai;
+ }
+
+ public final static class Service extends Component<ServiceIntentInfo> {
+ public final ServiceInfo info =
+ new ServiceInfo();
+
+ public Service(Package _owner) {
+ super(_owner);
+ info.applicationInfo = owner.applicationInfo;
+ }
+
+ public String toString() {
+ return "Service{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " " + component.flattenToString() + "}";
+ }
+ }
+
+ public static final ServiceInfo generateServiceInfo(Service s, int flags) {
+ if (s == null) return null;
+ if (!copyNeeded(flags, s.owner, s.metaData)) {
+ return s.info;
+ }
+ // Make shallow copies so we can store the metadata safely
+ ServiceInfo si = new ServiceInfo(s.info);
+ si.metaData = s.metaData;
+ si.applicationInfo = generateApplicationInfo(s.owner, flags);
+ return si;
+ }
+
+ public final static class Provider extends Component {
+ public final ProviderInfo info;
+ public boolean syncable;
+
+ public Provider(Package _owner) {
+ super(_owner);
+ info = new ProviderInfo();
+ info.applicationInfo = owner.applicationInfo;
+ syncable = false;
+ }
+
+ public Provider(Provider existingProvider) {
+ super(existingProvider);
+ this.info = existingProvider.info;
+ this.syncable = existingProvider.syncable;
+ }
+
+ public String toString() {
+ return "Provider{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " " + info.name + "}";
+ }
+ }
+
+ public static final ProviderInfo generateProviderInfo(Provider p,
+ int flags) {
+ if (p == null) return null;
+ if (!copyNeeded(flags, p.owner, p.metaData)
+ && ((flags & PackageManager.GET_URI_PERMISSION_PATTERNS) != 0
+ || p.info.uriPermissionPatterns == null)) {
+ return p.info;
+ }
+ // Make shallow copies so we can store the metadata safely
+ ProviderInfo pi = new ProviderInfo(p.info);
+ pi.metaData = p.metaData;
+ if ((flags & PackageManager.GET_URI_PERMISSION_PATTERNS) == 0) {
+ pi.uriPermissionPatterns = null;
+ }
+ pi.applicationInfo = generateApplicationInfo(p.owner, flags);
+ return pi;
+ }
+
+ public final static class Instrumentation extends Component {
+ public final InstrumentationInfo info =
+ new InstrumentationInfo();
+
+ public Instrumentation(Package _owner) {
+ super(_owner);
+ }
+
+ public String toString() {
+ return "Instrumentation{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " " + component.flattenToString() + "}";
+ }
+ }
+
+ public static final InstrumentationInfo generateInstrumentationInfo(
+ Instrumentation i, int flags) {
+ if (i == null) return null;
+ if ((flags&PackageManager.GET_META_DATA) == 0) {
+ return i.info;
+ }
+ InstrumentationInfo ii = new InstrumentationInfo(i.info);
+ ii.metaData = i.metaData;
+ return ii;
+ }
+
+ public static class IntentInfo extends IntentFilter {
+ public boolean hasDefault;
+ public int labelRes;
+ public CharSequence nonLocalizedLabel;
+ public int icon;
+ }
+
+ public final static class ActivityIntentInfo extends IntentInfo {
+ public final Activity activity;
+
+ public ActivityIntentInfo(Activity _activity) {
+ activity = _activity;
+ }
+
+ public String toString() {
+ return "ActivityIntentInfo{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " " + activity.info.name + "}";
+ }
+ }
+
+ public final static class ServiceIntentInfo extends IntentInfo {
+ public final Service service;
+
+ public ServiceIntentInfo(Service _service) {
+ service = _service;
+ }
+
+ public String toString() {
+ return "ServiceIntentInfo{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " " + service.info.name + "}";
+ }
+ }
+}
diff --git a/core/java/android/content/pm/PackageStats.aidl b/core/java/android/content/pm/PackageStats.aidl
new file mode 100755
index 0000000..8c9786f
--- /dev/null
+++ b/core/java/android/content/pm/PackageStats.aidl
@@ -0,0 +1,20 @@
+/* //device/java/android/android/view/WindowManager.aidl
+**
+** Copyright 2007, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.content.pm;
+
+parcelable PackageStats;
diff --git a/core/java/android/content/pm/PackageStats.java b/core/java/android/content/pm/PackageStats.java
new file mode 100755
index 0000000..66c6efd
--- /dev/null
+++ b/core/java/android/content/pm/PackageStats.java
@@ -0,0 +1,63 @@
+package android.content.pm;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Arrays;
+
+/**
+ * implementation of PackageStats associated with a
+ * application package.
+ */
+public class PackageStats implements Parcelable {
+ public String packageName;
+ public long codeSize;
+ public long dataSize;
+ public long cacheSize;
+
+ public static final Parcelable.Creator<PackageStats> CREATOR
+ = new Parcelable.Creator<PackageStats>() {
+ public PackageStats createFromParcel(Parcel in) {
+ return new PackageStats(in);
+ }
+
+ public PackageStats[] newArray(int size) {
+ return new PackageStats[size];
+ }
+ };
+
+ public String toString() {
+ return "PackageStats{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " " + packageName + "}";
+ }
+
+ public PackageStats(String pkgName) {
+ packageName = pkgName;
+ }
+
+ public PackageStats(Parcel source) {
+ packageName = source.readString();
+ codeSize = source.readLong();
+ dataSize = source.readLong();
+ cacheSize = source.readLong();
+ }
+
+ public PackageStats(PackageStats pStats) {
+ packageName = pStats.packageName;
+ codeSize = pStats.codeSize;
+ dataSize = pStats.dataSize;
+ cacheSize = pStats.cacheSize;
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int parcelableFlags){
+ dest.writeString(packageName);
+ dest.writeLong(codeSize);
+ dest.writeLong(dataSize);
+ dest.writeLong(cacheSize);
+ }
+}
diff --git a/core/java/android/content/pm/PermissionGroupInfo.aidl b/core/java/android/content/pm/PermissionGroupInfo.aidl
new file mode 100755
index 0000000..9f215f1
--- /dev/null
+++ b/core/java/android/content/pm/PermissionGroupInfo.aidl
@@ -0,0 +1,20 @@
+/* //device/java/android/android/view/WindowManager.aidl
+**
+** Copyright 2008, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.content.pm;
+
+parcelable PermissionGroupInfo;
diff --git a/core/java/android/content/pm/PermissionGroupInfo.java b/core/java/android/content/pm/PermissionGroupInfo.java
new file mode 100644
index 0000000..02eb816
--- /dev/null
+++ b/core/java/android/content/pm/PermissionGroupInfo.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.pm;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+/**
+ * Information you can retrieve about a particular security permission
+ * group known to the system. This corresponds to information collected from the
+ * AndroidManifest.xml's &lt;permission-group&gt; tags.
+ */
+public class PermissionGroupInfo extends PackageItemInfo implements Parcelable {
+ /**
+ * A string resource identifier (in the package's resources) of this
+ * permission's description. From the "description" attribute or,
+ * if not set, 0.
+ */
+ public int descriptionRes;
+
+ /**
+ * The description string provided in the AndroidManifest file, if any. You
+ * probably don't want to use this, since it will be null if the description
+ * is in a resource. You probably want
+ * {@link PermissionInfo#loadDescription} instead.
+ */
+ public CharSequence nonLocalizedDescription;
+
+ public PermissionGroupInfo() {
+ }
+
+ public PermissionGroupInfo(PermissionGroupInfo orig) {
+ super(orig);
+ descriptionRes = orig.descriptionRes;
+ nonLocalizedDescription = orig.nonLocalizedDescription;
+ }
+
+ /**
+ * Retrieve the textual description of this permission. This
+ * will call back on the given PackageManager to load the description from
+ * the application.
+ *
+ * @param pm A PackageManager from which the label can be loaded; usually
+ * the PackageManager from which you originally retrieved this item.
+ *
+ * @return Returns a CharSequence containing the permission's description.
+ * If there is no description, null is returned.
+ */
+ public CharSequence loadDescription(PackageManager pm) {
+ if (nonLocalizedDescription != null) {
+ return nonLocalizedDescription;
+ }
+ if (descriptionRes != 0) {
+ CharSequence label = pm.getText(packageName, descriptionRes, null);
+ if (label != null) {
+ return label;
+ }
+ }
+ return null;
+ }
+
+ public String toString() {
+ return "PermissionGroupInfo{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " " + name + "}";
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int parcelableFlags) {
+ super.writeToParcel(dest, parcelableFlags);
+ dest.writeInt(descriptionRes);
+ TextUtils.writeToParcel(nonLocalizedDescription, dest, parcelableFlags);
+ }
+
+ public static final Creator<PermissionGroupInfo> CREATOR =
+ new Creator<PermissionGroupInfo>() {
+ public PermissionGroupInfo createFromParcel(Parcel source) {
+ return new PermissionGroupInfo(source);
+ }
+ public PermissionGroupInfo[] newArray(int size) {
+ return new PermissionGroupInfo[size];
+ }
+ };
+
+ private PermissionGroupInfo(Parcel source) {
+ super(source);
+ descriptionRes = source.readInt();
+ nonLocalizedDescription = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
+ }
+}
diff --git a/core/java/android/content/pm/PermissionInfo.aidl b/core/java/android/content/pm/PermissionInfo.aidl
new file mode 100755
index 0000000..5a7d4f4
--- /dev/null
+++ b/core/java/android/content/pm/PermissionInfo.aidl
@@ -0,0 +1,20 @@
+/* //device/java/android/android/view/WindowManager.aidl
+**
+** Copyright 2007, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.content.pm;
+
+parcelable PermissionInfo;
diff --git a/core/java/android/content/pm/PermissionInfo.java b/core/java/android/content/pm/PermissionInfo.java
new file mode 100644
index 0000000..3cc884b
--- /dev/null
+++ b/core/java/android/content/pm/PermissionInfo.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.pm;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+/**
+ * Information you can retrieve about a particular security permission
+ * known to the system. This corresponds to information collected from the
+ * AndroidManifest.xml's &lt;permission&gt; tags.
+ */
+public class PermissionInfo extends PackageItemInfo implements Parcelable {
+ /**
+ * A normal application value for {@link #protectionLevel}, corresponding
+ * to the <code>normal</code> value of
+ * {@link android.R.attr#protectionLevel}.
+ */
+ public static final int PROTECTION_NORMAL = 0;
+
+ /**
+ * Dangerous value for {@link #protectionLevel}, corresponding
+ * to the <code>dangerous</code> value of
+ * {@link android.R.attr#protectionLevel}.
+ */
+ public static final int PROTECTION_DANGEROUS = 1;
+
+ /**
+ * System-level value for {@link #protectionLevel}, corresponding
+ * to the <code>signature</code> value of
+ * {@link android.R.attr#protectionLevel}.
+ */
+ public static final int PROTECTION_SIGNATURE = 2;
+
+ /**
+ * System-level value for {@link #protectionLevel}, corresponding
+ * to the <code>signatureOrSystem</code> value of
+ * {@link android.R.attr#protectionLevel}.
+ */
+ public static final int PROTECTION_SIGNATURE_OR_SYSTEM = 3;
+
+ /**
+ * The group this permission is a part of, as per
+ * {@link android.R.attr#permissionGroup}.
+ */
+ public String group;
+
+ /**
+ * A string resource identifier (in the package's resources) of this
+ * permission's description. From the "description" attribute or,
+ * if not set, 0.
+ */
+ public int descriptionRes;
+
+ /**
+ * The description string provided in the AndroidManifest file, if any. You
+ * probably don't want to use this, since it will be null if the description
+ * is in a resource. You probably want
+ * {@link PermissionInfo#loadDescription} instead.
+ */
+ public CharSequence nonLocalizedDescription;
+
+ /**
+ * The level of access this permission is protecting, as per
+ * {@link android.R.attr#protectionLevel}. Values may be
+ * {@link #PROTECTION_NORMAL}, {@link #PROTECTION_DANGEROUS}, or
+ * {@link #PROTECTION_SIGNATURE}.
+ */
+ public int protectionLevel;
+
+ public PermissionInfo() {
+ }
+
+ public PermissionInfo(PermissionInfo orig) {
+ super(orig);
+ group = orig.group;
+ descriptionRes = orig.descriptionRes;
+ protectionLevel = orig.protectionLevel;
+ nonLocalizedDescription = orig.nonLocalizedDescription;
+ }
+
+ /**
+ * Retrieve the textual description of this permission. This
+ * will call back on the given PackageManager to load the description from
+ * the application.
+ *
+ * @param pm A PackageManager from which the label can be loaded; usually
+ * the PackageManager from which you originally retrieved this item.
+ *
+ * @return Returns a CharSequence containing the permission's description.
+ * If there is no description, null is returned.
+ */
+ public CharSequence loadDescription(PackageManager pm) {
+ if (nonLocalizedDescription != null) {
+ return nonLocalizedDescription;
+ }
+ if (descriptionRes != 0) {
+ CharSequence label = pm.getText(packageName, descriptionRes, null);
+ if (label != null) {
+ return label;
+ }
+ }
+ return null;
+ }
+
+ public String toString() {
+ return "PermissionInfo{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " " + name + "}";
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int parcelableFlags) {
+ super.writeToParcel(dest, parcelableFlags);
+ dest.writeString(group);
+ dest.writeInt(descriptionRes);
+ dest.writeInt(protectionLevel);
+ TextUtils.writeToParcel(nonLocalizedDescription, dest, parcelableFlags);
+ }
+
+ public static final Creator<PermissionInfo> CREATOR =
+ new Creator<PermissionInfo>() {
+ public PermissionInfo createFromParcel(Parcel source) {
+ return new PermissionInfo(source);
+ }
+ public PermissionInfo[] newArray(int size) {
+ return new PermissionInfo[size];
+ }
+ };
+
+ private PermissionInfo(Parcel source) {
+ super(source);
+ group = source.readString();
+ descriptionRes = source.readInt();
+ protectionLevel = source.readInt();
+ nonLocalizedDescription = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
+ }
+}
diff --git a/core/java/android/content/pm/ProviderInfo.aidl b/core/java/android/content/pm/ProviderInfo.aidl
new file mode 100755
index 0000000..18fbc8a
--- /dev/null
+++ b/core/java/android/content/pm/ProviderInfo.aidl
@@ -0,0 +1,20 @@
+/* //device/java/android/android/view/WindowManager.aidl
+**
+** Copyright 2007, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.content.pm;
+
+parcelable ProviderInfo;
diff --git a/core/java/android/content/pm/ProviderInfo.java b/core/java/android/content/pm/ProviderInfo.java
new file mode 100644
index 0000000..b67ddf6
--- /dev/null
+++ b/core/java/android/content/pm/ProviderInfo.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.pm;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.PatternMatcher;
+
+/**
+ * Holds information about a specific
+ * {@link android.content.ContentProvider content provider}. This is returned by
+ * {@link android.content.pm.PackageManager#resolveContentProvider(java.lang.String, int)
+ * PackageManager.resolveContentProvider()}.
+ */
+public final class ProviderInfo extends ComponentInfo
+ implements Parcelable {
+ /** The name provider is published under content:// */
+ public String authority = null;
+
+ /** Optional permission required for read-only access this content
+ * provider. */
+ public String readPermission = null;
+
+ /** Optional permission required for read/write access this content
+ * provider. */
+ public String writePermission = null;
+
+ /** If true, additional permissions to specific Uris in this content
+ * provider can be granted, as per the
+ * {@link android.R.styleable#AndroidManifestProvider_grantUriPermissions
+ * grantUriPermissions} attribute.
+ */
+ public boolean grantUriPermissions = false;
+
+ /**
+ * If non-null, these are the patterns that are allowed for granting URI
+ * permissions. Any URI that does not match one of these patterns will not
+ * allowed to be granted. If null, all URIs are allowed. The
+ * {@link PackageManager#GET_URI_PERMISSION_PATTERNS
+ * PackageManager.GET_URI_PERMISSION_PATTERNS} flag must be specified for
+ * this field to be filled in.
+ */
+ public PatternMatcher[] uriPermissionPatterns = null;
+
+ /** If true, this content provider allows multiple instances of itself
+ * to run in different process. If false, a single instances is always
+ * run in {@link #processName}. */
+ public boolean multiprocess = false;
+
+ /** Used to control initialization order of single-process providers
+ * running in the same process. Higher goes first. */
+ public int initOrder = 0;
+
+ /** Whether or not this provider is syncable. */
+ public boolean isSyncable = false;
+
+ public ProviderInfo() {
+ }
+
+ public ProviderInfo(ProviderInfo orig) {
+ super(orig);
+ authority = orig.authority;
+ readPermission = orig.readPermission;
+ writePermission = orig.writePermission;
+ grantUriPermissions = orig.grantUriPermissions;
+ uriPermissionPatterns = orig.uriPermissionPatterns;
+ multiprocess = orig.multiprocess;
+ initOrder = orig.initOrder;
+ isSyncable = orig.isSyncable;
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override public void writeToParcel(Parcel out, int parcelableFlags) {
+ super.writeToParcel(out, parcelableFlags);
+ out.writeString(authority);
+ out.writeString(readPermission);
+ out.writeString(writePermission);
+ out.writeInt(grantUriPermissions ? 1 : 0);
+ out.writeTypedArray(uriPermissionPatterns, parcelableFlags);
+ out.writeInt(multiprocess ? 1 : 0);
+ out.writeInt(initOrder);
+ out.writeInt(isSyncable ? 1 : 0);
+ }
+
+ public static final Parcelable.Creator<ProviderInfo> CREATOR
+ = new Parcelable.Creator<ProviderInfo>() {
+ public ProviderInfo createFromParcel(Parcel in) {
+ return new ProviderInfo(in);
+ }
+
+ public ProviderInfo[] newArray(int size) {
+ return new ProviderInfo[size];
+ }
+ };
+
+ public String toString() {
+ return "ContentProviderInfo{name=" + authority + " className=" + name
+ + " isSyncable=" + (isSyncable ? "true" : "false") + "}";
+ }
+
+ private ProviderInfo(Parcel in) {
+ super(in);
+ authority = in.readString();
+ readPermission = in.readString();
+ writePermission = in.readString();
+ grantUriPermissions = in.readInt() != 0;
+ uriPermissionPatterns = in.createTypedArray(PatternMatcher.CREATOR);
+ multiprocess = in.readInt() != 0;
+ initOrder = in.readInt();
+ isSyncable = in.readInt() != 0;
+ }
+}
diff --git a/core/java/android/content/pm/ResolveInfo.aidl b/core/java/android/content/pm/ResolveInfo.aidl
new file mode 100755
index 0000000..b4e7f8b
--- /dev/null
+++ b/core/java/android/content/pm/ResolveInfo.aidl
@@ -0,0 +1,20 @@
+/* //device/java/android/android/view/WindowManager.aidl
+**
+** Copyright 2007, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.content.pm;
+
+parcelable ResolveInfo;
diff --git a/core/java/android/content/pm/ResolveInfo.java b/core/java/android/content/pm/ResolveInfo.java
new file mode 100644
index 0000000..ee49c02
--- /dev/null
+++ b/core/java/android/content/pm/ResolveInfo.java
@@ -0,0 +1,280 @@
+package android.content.pm;
+
+import android.content.IntentFilter;
+import android.graphics.drawable.Drawable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.Printer;
+
+import java.text.Collator;
+import java.util.Comparator;
+
+/**
+ * Information that is returned from resolving an intent
+ * against an IntentFilter. This partially corresponds to
+ * information collected from the AndroidManifest.xml's
+ * &lt;intent&gt; tags.
+ */
+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
+ * serviceInfo must be non-null.
+ */
+ public ActivityInfo activityInfo;
+
+ /**
+ * The service that corresponds to this resolution match, if this
+ * resolution is for a service. One and only one of this and
+ * activityInfo must be non-null.
+ */
+ public ServiceInfo serviceInfo;
+
+ /**
+ * The IntentFilter that was matched for this ResolveInfo.
+ */
+ public IntentFilter filter;
+
+ /**
+ * The declared priority of this match. Comes from the "priority"
+ * attribute or, if not set, defaults to 0. Higher values are a higher
+ * priority.
+ */
+ public int priority;
+
+ /**
+ * Order of result according to the user's preference. If the user
+ * has not set a preference for this result, the value is 0; higher
+ * values are a higher priority.
+ */
+ public int preferredOrder;
+
+ /**
+ * The system's evaluation of how well the activity matches the
+ * IntentFilter. This is a match constant, a combination of
+ * {@link IntentFilter#MATCH_CATEGORY_MASK IntentFilter.MATCH_CATEGORY_MASK}
+ * and {@link IntentFilter#MATCH_ADJUSTMENT_MASK IntentFiler.MATCH_ADJUSTMENT_MASK}.
+ */
+ public int match;
+
+ /**
+ * Only set when returned by
+ * {@link PackageManager#queryIntentActivityOptions}, this tells you
+ * which of the given specific intents this result came from. 0 is the
+ * first in the list, < 0 means it came from the generic Intent query.
+ */
+ public int specificIndex = -1;
+
+ /**
+ * This filter has specified the Intent.CATEGORY_DEFAULT, meaning it
+ * would like to be considered a default action that the user can
+ * perform on this data.
+ */
+ public boolean isDefault;
+
+ /**
+ * A string resource identifier (in the package's resources) of this
+ * match's label. From the "label" attribute or, if not set, 0.
+ */
+ public int labelRes;
+
+ /**
+ * The actual string retrieve from <var>labelRes</var> or null if none
+ * was provided.
+ */
+ public CharSequence nonLocalizedLabel;
+
+ /**
+ * A drawable resource identifier (in the package's resources) of this
+ * match's icon. From the "icon" attribute or, if not set, 0.
+ */
+ public int icon;
+
+ /**
+ * Retrieve the current textual label associated with this resolution. This
+ * will call back on the given PackageManager to load the label from
+ * the application.
+ *
+ * @param pm A PackageManager from which the label can be loaded; usually
+ * the PackageManager from which you originally retrieved this item.
+ *
+ * @return Returns a CharSequence containing the resolutions's label. If the
+ * item does not have a label, its name is returned.
+ */
+ public CharSequence loadLabel(PackageManager pm) {
+ if (nonLocalizedLabel != null) {
+ return nonLocalizedLabel;
+ }
+ ComponentInfo ci = activityInfo != null ? activityInfo : serviceInfo;
+ ApplicationInfo ai = ci.applicationInfo;
+ CharSequence label;
+ if (labelRes != 0) {
+ label = pm.getText(ci.packageName, labelRes, ai);
+ if (label != null) {
+ return label;
+ }
+ }
+ return ci.loadLabel(pm);
+ }
+
+ /**
+ * Retrieve the current graphical icon associated with this resolution. This
+ * will call back on the given PackageManager to load the icon from
+ * the application.
+ *
+ * @param pm A PackageManager from which the icon can be loaded; usually
+ * the PackageManager from which you originally retrieved this item.
+ *
+ * @return Returns a Drawable containing the resolution's icon. If the
+ * item does not have an icon, the default activity icon is returned.
+ */
+ public Drawable loadIcon(PackageManager pm) {
+ ComponentInfo ci = activityInfo != null ? activityInfo : serviceInfo;
+ ApplicationInfo ai = ci.applicationInfo;
+ Drawable dr;
+ if (icon != 0) {
+ dr = pm.getDrawable(ci.packageName, icon, ai);
+ if (dr != null) {
+ return dr;
+ }
+ }
+ return ci.loadIcon(pm);
+ }
+
+ /**
+ * Return the icon resource identifier to use for this match. If the
+ * match defines an icon, that is used; else if the activity defines
+ * an icon, that is used; else, the application icon is used.
+ *
+ * @return The icon associated with this match.
+ */
+ public final int getIconResource() {
+ if (icon != 0) return icon;
+ if (activityInfo != null) return activityInfo.getIconResource();
+ if (serviceInfo != null) return serviceInfo.getIconResource();
+ return 0;
+ }
+
+ public void dump(Printer pw, String prefix) {
+ if (filter != null) {
+ pw.println(prefix + "Filter:");
+ filter.dump(pw, prefix + " ");
+ } else {
+ pw.println(prefix + "Filter: null");
+ }
+ pw.println(prefix + "priority=" + priority
+ + " preferredOrder=" + preferredOrder
+ + " match=0x" + Integer.toHexString(match)
+ + " specificIndex=" + specificIndex
+ + " isDefault=" + isDefault);
+ pw.println(prefix + "labelRes=0x" + Integer.toHexString(labelRes)
+ + " nonLocalizedLabel=" + nonLocalizedLabel
+ + " icon=0x" + Integer.toHexString(icon));
+ if (activityInfo != null) {
+ pw.println(prefix + "ActivityInfo:");
+ activityInfo.dump(pw, prefix + " ");
+ } else if (serviceInfo != null) {
+ pw.println(prefix + "ServiceInfo:");
+ // TODO
+ //serviceInfo.dump(pw, prefix + " ");
+ }
+ }
+
+ public ResolveInfo() {
+ }
+
+ public String toString() {
+ ComponentInfo ci = activityInfo != null ? activityInfo : serviceInfo;
+ return "ResolveInfo{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " " + ci.name + " p=" + priority + " o="
+ + preferredOrder + " m=0x" + Integer.toHexString(match) + "}";
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int parcelableFlags) {
+ if (activityInfo != null) {
+ dest.writeInt(1);
+ activityInfo.writeToParcel(dest, parcelableFlags);
+ } else if (serviceInfo != null) {
+ dest.writeInt(2);
+ serviceInfo.writeToParcel(dest, parcelableFlags);
+ } else {
+ dest.writeInt(0);
+ }
+ if (filter != null) {
+ dest.writeInt(1);
+ filter.writeToParcel(dest, parcelableFlags);
+ } else {
+ dest.writeInt(0);
+ }
+ dest.writeInt(priority);
+ dest.writeInt(preferredOrder);
+ dest.writeInt(match);
+ dest.writeInt(specificIndex);
+ dest.writeInt(labelRes);
+ TextUtils.writeToParcel(nonLocalizedLabel, dest, parcelableFlags);
+ dest.writeInt(icon);
+ }
+
+ public static final Creator<ResolveInfo> CREATOR
+ = new Creator<ResolveInfo>() {
+ public ResolveInfo createFromParcel(Parcel source) {
+ return new ResolveInfo(source);
+ }
+ public ResolveInfo[] newArray(int size) {
+ return new ResolveInfo[size];
+ }
+ };
+
+ private ResolveInfo(Parcel source) {
+ switch (source.readInt()) {
+ case 1:
+ activityInfo = ActivityInfo.CREATOR.createFromParcel(source);
+ serviceInfo = null;
+ break;
+ case 2:
+ serviceInfo = ServiceInfo.CREATOR.createFromParcel(source);
+ activityInfo = null;
+ break;
+ default:
+ activityInfo = null;
+ serviceInfo = null;
+ break;
+ }
+ if (source.readInt() != 0) {
+ filter = IntentFilter.CREATOR.createFromParcel(source);
+ }
+ priority = source.readInt();
+ preferredOrder = source.readInt();
+ match = source.readInt();
+ specificIndex = source.readInt();
+ labelRes = source.readInt();
+ nonLocalizedLabel
+ = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
+ icon = source.readInt();
+ }
+
+ public static class DisplayNameComparator
+ implements Comparator<ResolveInfo> {
+ public DisplayNameComparator(PackageManager pm) {
+ mPM = pm;
+ }
+
+ public final int compare(ResolveInfo a, ResolveInfo b) {
+ CharSequence sa = a.loadLabel(mPM);
+ if (sa == null) sa = a.activityInfo.name;
+ CharSequence sb = b.loadLabel(mPM);
+ if (sb == null) sb = b.activityInfo.name;
+
+ return sCollator.compare(sa.toString(), sb.toString());
+ }
+
+ private final Collator sCollator = Collator.getInstance();
+ private PackageManager mPM;
+ }
+}
diff --git a/core/java/android/content/pm/ServiceInfo.aidl b/core/java/android/content/pm/ServiceInfo.aidl
new file mode 100755
index 0000000..5ddae1a
--- /dev/null
+++ b/core/java/android/content/pm/ServiceInfo.aidl
@@ -0,0 +1,20 @@
+/* //device/java/android/android/view/WindowManager.aidl
+**
+** Copyright 2007, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.content.pm;
+
+parcelable ServiceInfo;
diff --git a/core/java/android/content/pm/ServiceInfo.java b/core/java/android/content/pm/ServiceInfo.java
new file mode 100644
index 0000000..b60650c
--- /dev/null
+++ b/core/java/android/content/pm/ServiceInfo.java
@@ -0,0 +1,56 @@
+package android.content.pm;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Information you can retrieve about a particular application
+ * service. This corresponds to information collected from the
+ * AndroidManifest.xml's &lt;service&gt; tags.
+ */
+public class ServiceInfo extends ComponentInfo
+ implements Parcelable {
+ /**
+ * Optional name of a permission required to be able to access this
+ * Service. From the "permission" attribute.
+ */
+ public String permission;
+
+ public ServiceInfo() {
+ }
+
+ public ServiceInfo(ServiceInfo orig) {
+ super(orig);
+ permission = orig.permission;
+ }
+
+ public String toString() {
+ return "ServiceInfo{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " " + name + "}";
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int parcelableFlags) {
+ super.writeToParcel(dest, parcelableFlags);
+ dest.writeString(permission);
+ }
+
+ public static final Creator<ServiceInfo> CREATOR =
+ new Creator<ServiceInfo>() {
+ public ServiceInfo createFromParcel(Parcel source) {
+ return new ServiceInfo(source);
+ }
+ public ServiceInfo[] newArray(int size) {
+ return new ServiceInfo[size];
+ }
+ };
+
+ private ServiceInfo(Parcel source) {
+ super(source);
+ permission = source.readString();
+ }
+}
diff --git a/core/java/android/content/pm/Signature.aidl b/core/java/android/content/pm/Signature.aidl
new file mode 100755
index 0000000..3a0d775
--- /dev/null
+++ b/core/java/android/content/pm/Signature.aidl
@@ -0,0 +1,20 @@
+/* //device/java/android/android/view/WindowManager.aidl
+**
+** Copyright 2007, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.content.pm;
+
+parcelable Signature;
diff --git a/core/java/android/content/pm/Signature.java b/core/java/android/content/pm/Signature.java
new file mode 100644
index 0000000..1bb3857
--- /dev/null
+++ b/core/java/android/content/pm/Signature.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.pm;
+
+import android.content.ComponentName;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Arrays;
+
+/**
+ * Opaque, immutable representation of a signature associated with an
+ * application package.
+ */
+public class Signature implements Parcelable {
+ private final byte[] mSignature;
+ private int mHashCode;
+ private boolean mHaveHashCode;
+ private String mString;
+
+ /**
+ * Create Signature from an existing raw byte array.
+ */
+ public Signature(byte[] signature) {
+ mSignature = signature.clone();
+ }
+
+ /**
+ * Create Signature from a text representation previously returned by
+ * {@link #toChars} or {@link #toCharsString()}.
+ */
+ public Signature(String text) {
+ final int N = text.length()/2;
+ byte[] sig = new byte[N];
+ for (int i=0; i<N; i++) {
+ char c = text.charAt(i*2);
+ byte b = (byte)(
+ (c >= 'a' ? (c - 'a' + 10) : (c - '0'))<<4);
+ c = text.charAt(i*2 + 1);
+ b |= (byte)(c >= 'a' ? (c - 'a' + 10) : (c - '0'));
+ sig[i] = b;
+ }
+ mSignature = sig;
+ }
+
+ /**
+ * Encode the Signature as ASCII text.
+ */
+ public char[] toChars() {
+ return toChars(null, null);
+ }
+
+ /**
+ * Encode the Signature as ASCII text in to an existing array.
+ *
+ * @param existingArray Existing char array or null.
+ * @param outLen Output parameter for the number of characters written in
+ * to the array.
+ * @return Returns either <var>existingArray</var> if it was large enough
+ * to hold the ASCII representation, or a newly created char[] array if
+ * needed.
+ */
+ public char[] toChars(char[] existingArray, int[] outLen) {
+ byte[] sig = mSignature;
+ final int N = sig.length;
+ final int N2 = N*2;
+ char[] text = existingArray == null || N2 > existingArray.length
+ ? new char[N2] : existingArray;
+ for (int j=0; j<N; j++) {
+ byte v = sig[j];
+ int d = (v>>4)&0xf;
+ text[j*2] = (char)(d >= 10 ? ('a' + d - 10) : ('0' + d));
+ d = v&0xf;
+ text[j*2+1] = (char)(d >= 10 ? ('a' + d - 10) : ('0' + d));
+ }
+ if (outLen != null) outLen[0] = N;
+ return text;
+ }
+
+ /**
+ * Return the result of {@link #toChars()} as a String. This result is
+ * cached so future calls will return the same String.
+ */
+ public String toCharsString() {
+ if (mString != null) return mString;
+ String str = new String(toChars());
+ mString = str;
+ return mString;
+ }
+
+ /**
+ * @return the contents of this signature as a byte array.
+ */
+ public byte[] toByteArray() {
+ byte[] bytes = new byte[mSignature.length];
+ System.arraycopy(mSignature, 0, bytes, 0, mSignature.length);
+ return bytes;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ try {
+ if (obj != null) {
+ Signature other = (Signature)obj;
+ return Arrays.equals(mSignature, other.mSignature);
+ }
+ } catch (ClassCastException e) {
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ if (mHaveHashCode) {
+ return mHashCode;
+ }
+ mHashCode = Arrays.hashCode(mSignature);
+ mHaveHashCode = true;
+ return mHashCode;
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int parcelableFlags) {
+ dest.writeByteArray(mSignature);
+ }
+
+ public static final Parcelable.Creator<Signature> CREATOR
+ = new Parcelable.Creator<Signature>() {
+ public Signature createFromParcel(Parcel source) {
+ return new Signature(source);
+ }
+
+ public Signature[] newArray(int size) {
+ return new Signature[size];
+ }
+ };
+
+ private Signature(Parcel source) {
+ mSignature = source.createByteArray();
+ }
+}
diff --git a/core/java/android/content/pm/package.html b/core/java/android/content/pm/package.html
new file mode 100644
index 0000000..766b7dd
--- /dev/null
+++ b/core/java/android/content/pm/package.html
@@ -0,0 +1,7 @@
+<HTML>
+<BODY>
+Contains classes for accessing information about an
+application package, including information about its activities,
+permissions, services, signatures, and providers.
+</BODY>
+</HTML> \ No newline at end of file
diff --git a/core/java/android/content/res/AssetFileDescriptor.java b/core/java/android/content/res/AssetFileDescriptor.java
new file mode 100644
index 0000000..4a073f7
--- /dev/null
+++ b/core/java/android/content/res/AssetFileDescriptor.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.res;
+
+import android.os.ParcelFileDescriptor;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+
+/**
+ * File descriptor of an entry in the AssetManager. This provides your own
+ * opened FileDescriptor that can be used to read the data, as well as the
+ * offset and length of that entry's data in the file.
+ */
+public class AssetFileDescriptor {
+ private final ParcelFileDescriptor mFd;
+ private final long mStartOffset;
+ private final long mLength;
+
+ /**
+ * Create a new AssetFileDescriptor from the given values.
+ */
+ public AssetFileDescriptor(ParcelFileDescriptor fd, long startOffset,
+ long length) {
+ mFd = fd;
+ mStartOffset = startOffset;
+ mLength = length;
+ }
+
+ /**
+ * The AssetFileDescriptor contains its own ParcelFileDescriptor, which
+ * in addition to the normal FileDescriptor object also allows you to close
+ * the descriptor when you are done with it.
+ */
+ public ParcelFileDescriptor getParcelFileDescriptor() {
+ return mFd;
+ }
+
+ /**
+ * Returns the FileDescriptor that can be used to read the data in the
+ * file.
+ */
+ public FileDescriptor getFileDescriptor() {
+ return mFd.getFileDescriptor();
+ }
+
+ /**
+ * Returns the byte offset where this asset entry's data starts.
+ */
+ public long getStartOffset() {
+ return mStartOffset;
+ }
+
+ /**
+ * Returns the total number of bytes of this asset entry's data.
+ */
+ public long getLength() {
+ return mLength;
+ }
+
+ /**
+ * Convenience for calling <code>getParcelFileDescriptor().close()</code>.
+ */
+ public void close() throws IOException {
+ mFd.close();
+ }
+}
diff --git a/core/java/android/content/res/AssetManager.java b/core/java/android/content/res/AssetManager.java
new file mode 100644
index 0000000..fadcb35
--- /dev/null
+++ b/core/java/android/content/res/AssetManager.java
@@ -0,0 +1,697 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.res;
+
+import android.os.ParcelFileDescriptor;
+import android.util.Config;
+import android.util.Log;
+import android.util.TypedValue;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Locale;
+
+/**
+ * Provides access to an application's raw asset files; see {@link Resources}
+ * for the way most applications will want to retrieve their resource data.
+ * This class presents a lower-level API that allows you to open and read raw
+ * files that have been bundled with the application as a simple stream of
+ * bytes.
+ */
+public final class AssetManager {
+ /* modes used when opening an asset */
+
+ /**
+ * Mode for {@link #open(String, int)}: no specific information about how
+ * data will be accessed.
+ */
+ public static final int ACCESS_UNKNOWN = 0;
+ /**
+ * Mode for {@link #open(String, int)}: Read chunks, and seek forward and
+ * backward.
+ */
+ public static final int ACCESS_RANDOM = 1;
+ /**
+ * Mode for {@link #open(String, int)}: Read sequentially, with an
+ * occasional forward seek.
+ */
+ public static final int ACCESS_STREAMING = 2;
+ /**
+ * Mode for {@link #open(String, int)}: Attempt to load contents into
+ * memory, for fast small reads.
+ */
+ public static final int ACCESS_BUFFER = 3;
+
+ private static final String TAG = "AssetManager";
+ private static final boolean localLOGV = Config.LOGV || false;
+
+ private static final Object mSync = new Object();
+ private static final TypedValue mValue = new TypedValue();
+ private static final long[] mOffsets = new long[2];
+ private static AssetManager mSystem = null;
+
+ // For communication with native code.
+ private int mObject;
+
+ private StringBlock mStringBlocks[] = null;
+
+ private int mNumRefs = 1;
+ private boolean mOpen = true;
+ private String mAssetDir;
+ private String mAppName;
+
+ /**
+ * Create a new AssetManager containing only the basic system assets.
+ * Applications will not generally use this method, instead retrieving the
+ * appropriate asset manager with {@link Resources#getAssets}. Not for
+ * use by applications.
+ * {@hide}
+ */
+ public AssetManager() {
+ synchronized (mSync) {
+ init();
+ if (localLOGV) Log.v(TAG, "New asset manager: " + this);
+ ensureSystemAssets();
+ }
+ }
+
+ private static void ensureSystemAssets() {
+ synchronized (mSync) {
+ if (mSystem == null) {
+ AssetManager system = new AssetManager(true);
+ system.makeStringBlocks(false);
+ mSystem = system;
+ }
+ }
+ }
+
+ private AssetManager(boolean isSystem) {
+ init();
+ if (localLOGV) Log.v(TAG, "New asset manager: " + this);
+ }
+
+ /**
+ * Return a global shared asset manager that provides access to only
+ * system assets (no application assets).
+ * {@hide}
+ */
+ public static AssetManager getSystem() {
+ ensureSystemAssets();
+ return mSystem;
+ }
+
+ /**
+ * Close this asset manager.
+ */
+ public void close() {
+ synchronized(mSync) {
+ //System.out.println("Release: num=" + mNumRefs
+ // + ", released=" + mReleased);
+ if (mOpen) {
+ mOpen = false;
+ decRefsLocked();
+ }
+ }
+ }
+
+ /**
+ * Retrieve the string value associated with a particular resource
+ * identifier for the current configuration / skin.
+ */
+ /*package*/ final CharSequence getResourceText(int ident) {
+ synchronized (mSync) {
+ TypedValue tmpValue = mValue;
+ int block = loadResourceValue(ident, tmpValue, true);
+ if (block >= 0) {
+ if (tmpValue.type == TypedValue.TYPE_STRING) {
+ return mStringBlocks[block].get(tmpValue.data);
+ }
+ return tmpValue.coerceToString();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Retrieve the string value associated with a particular resource
+ * identifier for the current configuration / skin.
+ */
+ /*package*/ final CharSequence getResourceBagText(int ident, int bagEntryId) {
+ synchronized (mSync) {
+ TypedValue tmpValue = mValue;
+ int block = loadResourceBagValue(ident, bagEntryId, tmpValue, true);
+ if (block >= 0) {
+ if (tmpValue.type == TypedValue.TYPE_STRING) {
+ return mStringBlocks[block].get(tmpValue.data);
+ }
+ return tmpValue.coerceToString();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Retrieve the string array associated with a particular resource
+ * identifier.
+ * @param id Resource id of the string array
+ */
+ /*package*/ final String[] getResourceStringArray(final int id) {
+ String[] retArray = getArrayStringResource(id);
+ return retArray;
+ }
+
+
+ /*package*/ final boolean getResourceValue(int ident,
+ TypedValue outValue,
+ boolean resolveRefs)
+ {
+ int block = loadResourceValue(ident, outValue, resolveRefs);
+ if (block >= 0) {
+ if (outValue.type != TypedValue.TYPE_STRING) {
+ return true;
+ }
+ outValue.string = mStringBlocks[block].get(outValue.data);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Retrieve the text array associated with a particular resource
+ * identifier.
+ * @param id Resource id of the string array
+ */
+ /*package*/ final CharSequence[] getResourceTextArray(final int id) {
+ int[] rawInfoArray = getArrayStringInfo(id);
+ int rawInfoArrayLen = rawInfoArray.length;
+ final int infoArrayLen = rawInfoArrayLen / 2;
+ int block;
+ int index;
+ CharSequence[] retArray = new CharSequence[infoArrayLen];
+ for (int i = 0, j = 0; i < rawInfoArrayLen; i = i + 2, j++) {
+ block = rawInfoArray[i];
+ index = rawInfoArray[i + 1];
+ retArray[j] = index >= 0 ? mStringBlocks[block].get(index) : null;
+ }
+ return retArray;
+ }
+
+ /*package*/ final boolean getThemeValue(int theme, int ident,
+ TypedValue outValue, boolean resolveRefs) {
+ int block = loadThemeAttributeValue(theme, ident, outValue, resolveRefs);
+ if (block >= 0) {
+ if (outValue.type != TypedValue.TYPE_STRING) {
+ return true;
+ }
+ StringBlock[] blocks = mStringBlocks;
+ if (blocks == null) {
+ ensureStringBlocks();
+ }
+ outValue.string = blocks[block].get(outValue.data);
+ return true;
+ }
+ return false;
+ }
+
+ /*package*/ final void ensureStringBlocks() {
+ if (mStringBlocks == null) {
+ synchronized (mSync) {
+ if (mStringBlocks == null) {
+ makeStringBlocks(true);
+ }
+ }
+ }
+ }
+
+ private final void makeStringBlocks(boolean copyFromSystem) {
+ final int sysNum = copyFromSystem ? mSystem.mStringBlocks.length : 0;
+ final int num = getStringBlockCount();
+ mStringBlocks = new StringBlock[num];
+ if (localLOGV) Log.v(TAG, "Making string blocks for " + this
+ + ": " + num);
+ for (int i=0; i<num; i++) {
+ if (i < sysNum) {
+ mStringBlocks[i] = mSystem.mStringBlocks[i];
+ } else {
+ mStringBlocks[i] = new StringBlock(getNativeStringBlock(i), true);
+ }
+ }
+ }
+
+ /*package*/ final CharSequence getPooledString(int block, int id) {
+ //System.out.println("Get pooled: block=" + block
+ // + ", id=#" + Integer.toHexString(id)
+ // + ", blocks=" + mStringBlocks);
+ return mStringBlocks[block-1].get(id);
+ }
+
+ /**
+ * Open an asset using ACCESS_STREAMING mode. This provides access to
+ * files that have been bundled with an application as assets -- that is,
+ * files placed in to the "assets" directory.
+ *
+ * @param fileName The name of the asset to open. This name can be
+ * hierarchical.
+ *
+ * @see #open(String, int)
+ * @see #list
+ */
+ public final InputStream open(String fileName) throws IOException {
+ return open(fileName, ACCESS_STREAMING);
+ }
+
+ /**
+ * Open an asset using an explicit access mode, returning an InputStream to
+ * read its contents. This provides access to files that have been bundled
+ * with an application as assets -- that is, files placed in to the
+ * "assets" directory.
+ *
+ * @param fileName The name of the asset to open. This name can be
+ * hierarchical.
+ * @param accessMode Desired access mode for retrieving the data.
+ *
+ * @see #ACCESS_UNKNOWN
+ * @see #ACCESS_STREAMING
+ * @see #ACCESS_RANDOM
+ * @see #ACCESS_BUFFER
+ * @see #open(String)
+ * @see #list
+ */
+ public final InputStream open(String fileName, int accessMode)
+ throws IOException {
+ synchronized (mSync) {
+ if (!mOpen) {
+ throw new RuntimeException("Assetmanager has been closed");
+ }
+ int asset = openAsset(fileName, accessMode);
+ if (asset != 0) {
+ mNumRefs++;
+ return new AssetInputStream(asset);
+ }
+ }
+ throw new FileNotFoundException("Asset file: " + fileName);
+ }
+
+ public final AssetFileDescriptor openFd(String fileName)
+ throws IOException {
+ synchronized (mSync) {
+ if (!mOpen) {
+ throw new RuntimeException("Assetmanager has been closed");
+ }
+ ParcelFileDescriptor pfd = openAssetFd(fileName, mOffsets);
+ if (pfd != null) {
+ return new AssetFileDescriptor(pfd, mOffsets[0], mOffsets[1]);
+ }
+ }
+ throw new FileNotFoundException("Asset file: " + fileName);
+ }
+
+ /**
+ * Return a String array of all the assets at the given path.
+ *
+ * @param path A relative path within the assets, i.e., "docs/home.html".
+ *
+ * @return String[] Array of strings, one for each asset. These file
+ * names are relative to 'path'. You can open the file by
+ * concatenating 'path' and a name in the returned string (via
+ * File) and passing that to open().
+ *
+ * @see #open
+ */
+ public native final String[] list(String path)
+ throws IOException;
+
+ /**
+ * {@hide}
+ * Open a non-asset file as an asset using ACCESS_STREAMING mode. This
+ * provides direct access to all of the files included in an application
+ * package (not only its assets). Applications should not normally use
+ * this.
+ *
+ * @see #open(String)
+ */
+ public final InputStream openNonAsset(String fileName) throws IOException {
+ return openNonAsset(0, fileName, ACCESS_STREAMING);
+ }
+
+ /**
+ * {@hide}
+ * Open a non-asset file as an asset using a specific access mode. This
+ * provides direct access to all of the files included in an application
+ * package (not only its assets). Applications should not normally use
+ * this.
+ *
+ * @see #open(String, int)
+ */
+ public final InputStream openNonAsset(String fileName, int accessMode)
+ throws IOException {
+ return openNonAsset(0, fileName, accessMode);
+ }
+
+ /**
+ * {@hide}
+ * Open a non-asset in a specified package. Not for use by applications.
+ *
+ * @param cookie Identifier of the package to be opened.
+ * @param fileName Name of the asset to retrieve.
+ */
+ public final InputStream openNonAsset(int cookie, String fileName)
+ throws IOException {
+ return openNonAsset(cookie, fileName, ACCESS_STREAMING);
+ }
+
+ /**
+ * {@hide}
+ * Open a non-asset in a specified package. Not for use by applications.
+ *
+ * @param cookie Identifier of the package to be opened.
+ * @param fileName Name of the asset to retrieve.
+ * @param accessMode Desired access mode for retrieving the data.
+ */
+ public final InputStream openNonAsset(int cookie, String fileName, int accessMode)
+ throws IOException {
+ synchronized (mSync) {
+ if (!mOpen) {
+ throw new RuntimeException("Assetmanager has been closed");
+ }
+ int asset = openNonAssetNative(cookie, fileName, accessMode);
+ if (asset != 0) {
+ mNumRefs++;
+ return new AssetInputStream(asset);
+ }
+ }
+ throw new FileNotFoundException("Asset absolute file: " + fileName);
+ }
+
+ public final AssetFileDescriptor openNonAssetFd(String fileName)
+ throws IOException {
+ return openNonAssetFd(0, fileName);
+ }
+
+ public final AssetFileDescriptor openNonAssetFd(int cookie,
+ String fileName) throws IOException {
+ synchronized (mSync) {
+ if (!mOpen) {
+ throw new RuntimeException("Assetmanager has been closed");
+ }
+ ParcelFileDescriptor pfd = openNonAssetFdNative(cookie,
+ fileName, mOffsets);
+ if (pfd != null) {
+ return new AssetFileDescriptor(pfd, mOffsets[0], mOffsets[1]);
+ }
+ }
+ throw new FileNotFoundException("Asset absolute file: " + fileName);
+ }
+
+ /**
+ * Retrieve a parser for a compiled XML file.
+ *
+ * @param fileName The name of the file to retrieve.
+ */
+ public final XmlResourceParser openXmlResourceParser(String fileName)
+ throws IOException {
+ return openXmlResourceParser(0, fileName);
+ }
+
+ /**
+ * Retrieve a parser for a compiled XML file.
+ *
+ * @param cookie Identifier of the package to be opened.
+ * @param fileName The name of the file to retrieve.
+ */
+ public final XmlResourceParser openXmlResourceParser(int cookie,
+ String fileName) throws IOException {
+ XmlBlock block = openXmlBlockAsset(cookie, fileName);
+ XmlResourceParser rp = block.newParser();
+ block.close();
+ return rp;
+ }
+
+ /**
+ * {@hide}
+ * Retrieve a non-asset as a compiled XML file. Not for use by
+ * applications.
+ *
+ * @param fileName The name of the file to retrieve.
+ */
+ /*package*/ final XmlBlock openXmlBlockAsset(String fileName)
+ throws IOException {
+ return openXmlBlockAsset(0, fileName);
+ }
+
+ /**
+ * {@hide}
+ * Retrieve a non-asset as a compiled XML file. Not for use by
+ * applications.
+ *
+ * @param cookie Identifier of the package to be opened.
+ * @param fileName Name of the asset to retrieve.
+ */
+ /*package*/ final XmlBlock openXmlBlockAsset(int cookie, String fileName)
+ throws IOException {
+ synchronized (mSync) {
+ if (!mOpen) {
+ throw new RuntimeException("Assetmanager has been closed");
+ }
+ int xmlBlock = openXmlAssetNative(cookie, fileName);
+ if (xmlBlock != 0) {
+ mNumRefs++;
+ return new XmlBlock(this, xmlBlock);
+ }
+ }
+ throw new FileNotFoundException("Asset XML file: " + fileName);
+ }
+
+ /*package*/ void xmlBlockGone() {
+ synchronized (mSync) {
+ decRefsLocked();
+ }
+ }
+
+ /*package*/ final int createTheme() {
+ synchronized (mSync) {
+ if (!mOpen) {
+ throw new RuntimeException("Assetmanager has been closed");
+ }
+ mNumRefs++;
+ return newTheme();
+ }
+ }
+
+ /*package*/ final void releaseTheme(int theme) {
+ synchronized (mSync) {
+ deleteTheme(theme);
+ decRefsLocked();
+ }
+ }
+
+ protected void finalize() throws Throwable {
+ destroy();
+ }
+
+ public final class AssetInputStream extends InputStream {
+ public final int getAssetInt() {
+ return mAsset;
+ }
+ private AssetInputStream(int asset)
+ {
+ mAsset = asset;
+ mLength = getAssetLength(asset);
+ }
+ public final int read() throws IOException {
+ return readAssetChar(mAsset);
+ }
+ public final boolean markSupported() {
+ return true;
+ }
+ public final int available() throws IOException {
+ long len = getAssetRemainingLength(mAsset);
+ return len > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int)len;
+ }
+ public final void close() throws IOException {
+ synchronized (AssetManager.mSync) {
+ if (mAsset != 0) {
+ destroyAsset(mAsset);
+ mAsset = 0;
+ decRefsLocked();
+ }
+ }
+ }
+ public final void mark(int readlimit) {
+ mMarkPos = seekAsset(mAsset, 0, 0);
+ }
+ public final void reset() throws IOException {
+ seekAsset(mAsset, mMarkPos, -1);
+ }
+ public final int read(byte[] b) throws IOException {
+ return readAsset(mAsset, b, 0, b.length);
+ }
+ public final int read(byte[] b, int off, int len) throws IOException {
+ return readAsset(mAsset, b, off, len);
+ }
+ public final long skip(long n) throws IOException {
+ long pos = seekAsset(mAsset, 0, 0);
+ if ((pos+n) > mLength) {
+ n = mLength-pos;
+ }
+ if (n > 0) {
+ seekAsset(mAsset, n, 0);
+ }
+ return n;
+ }
+
+ protected void finalize() throws Throwable
+ {
+ close();
+ }
+
+ private int mAsset;
+ private long mLength;
+ private long mMarkPos;
+ }
+
+ /**
+ * Add an additional set of assets to the asset manager. This can be
+ * either a directory or ZIP file. Not for use by applications. A
+ * zero return value indicates failure.
+ * {@hide}
+ */
+ public native final int addAssetPath(String path);
+
+ /**
+ * Determine whether the state in this asset manager is up-to-date with
+ * the files on the filesystem. If false is returned, you need to
+ * instantiate a new AssetManager class to see the new data.
+ * {@hide}
+ */
+ public native final boolean isUpToDate();
+
+ /**
+ * Change the locale being used by this asset manager. Not for use by
+ * applications.
+ * {@hide}
+ */
+ public native final void setLocale(String locale);
+
+ /**
+ * Get the locales that this asset manager contains data for.
+ */
+ public native final String[] getLocales();
+
+ /**
+ * Change the configuation used when retrieving resources. Not for use by
+ * applications.
+ * {@hide}
+ */
+ public native final void setConfiguration(int mcc, int mnc, String locale,
+ int orientation, int touchscreen, int density, int keyboard,
+ int keyboardHidden, int navigation, int screenWidth, int screenHeight,
+ int majorVersion);
+
+ /**
+ * Retrieve the resource identifier for the given resource name.
+ */
+ /*package*/ native final int getResourceIdentifier(String type,
+ String name,
+ String defPackage);
+
+ /*package*/ native final String getResourceName(int resid);
+ /*package*/ native final String getResourcePackageName(int resid);
+ /*package*/ native final String getResourceTypeName(int resid);
+ /*package*/ native final String getResourceEntryName(int resid);
+
+ private native final int openAsset(String fileName, int accessMode);
+ private final native ParcelFileDescriptor openAssetFd(String fileName,
+ long[] outOffsets) throws IOException;
+ private native final int openNonAssetNative(int cookie, String fileName,
+ int accessMode);
+ private native ParcelFileDescriptor openNonAssetFdNative(int cookie,
+ String fileName, long[] outOffsets) throws IOException;
+ private native final void destroyAsset(int asset);
+ private native final int readAssetChar(int asset);
+ private native final int readAsset(int asset, byte[] b, int off, int len);
+ private native final long seekAsset(int asset, long offset, int whence);
+ private native final long getAssetLength(int asset);
+ private native final long getAssetRemainingLength(int asset);
+
+ /** Returns true if the resource was found, filling in mRetStringBlock and
+ * mRetData. */
+ private native final int loadResourceValue(int ident, TypedValue outValue,
+ boolean resolve);
+ /** Returns true if the resource was found, filling in mRetStringBlock and
+ * mRetData. */
+ private native final int loadResourceBagValue(int ident, int bagEntryId, TypedValue outValue,
+ boolean resolve);
+ /*package*/ static final int STYLE_NUM_ENTRIES = 5;
+ /*package*/ static final int STYLE_TYPE = 0;
+ /*package*/ static final int STYLE_DATA = 1;
+ /*package*/ static final int STYLE_ASSET_COOKIE = 2;
+ /*package*/ static final int STYLE_RESOURCE_ID = 3;
+ /*package*/ static final int STYLE_CHANGING_CONFIGURATIONS = 4;
+ /*package*/ native static final boolean applyStyle(int theme,
+ int defStyleAttr, int defStyleRes, int xmlParser,
+ int[] inAttrs, int[] outValues, int[] outIndices);
+ /*package*/ native final boolean retrieveAttributes(
+ int xmlParser, int[] inAttrs, int[] outValues, int[] outIndices);
+ /*package*/ native final int getArraySize(int resource);
+ /*package*/ native final int retrieveArray(int resource, int[] outValues);
+ private native final int getStringBlockCount();
+ private native final int getNativeStringBlock(int block);
+
+ /**
+ * {@hide}
+ */
+ public native final String getCookieName(int cookie);
+
+ /**
+ * {@hide}
+ */
+ public native static final int getGlobalAssetCount();
+
+ /**
+ * {@hide}
+ */
+ public native static final int getGlobalAssetManagerCount();
+
+ private native final int newTheme();
+ private native final void deleteTheme(int theme);
+ /*package*/ native static final void applyThemeStyle(int theme, int styleRes, boolean force);
+ /*package*/ native static final void copyTheme(int dest, int source);
+ /*package*/ native static final int loadThemeAttributeValue(int theme, int ident,
+ TypedValue outValue,
+ boolean resolve);
+ /*package*/ native static final void dumpTheme(int theme, int priority, String tag, String prefix);
+
+ private native final int openXmlAssetNative(int cookie, String fileName);
+
+ private native final String[] getArrayStringResource(int arrayRes);
+ private native final int[] getArrayStringInfo(int arrayRes);
+ /*package*/ native final int[] getArrayIntResource(int arrayRes);
+
+ private native final void init();
+ private native final void destroy();
+
+ private final void decRefsLocked() {
+ mNumRefs--;
+ //System.out.println("Dec streams: mNumRefs=" + mNumRefs
+ // + " mReleased=" + mReleased);
+ if (mNumRefs == 0) {
+ destroy();
+ }
+ }
+}
diff --git a/core/java/android/content/res/ColorStateList.java b/core/java/android/content/res/ColorStateList.java
new file mode 100644
index 0000000..ee88c89
--- /dev/null
+++ b/core/java/android/content/res/ColorStateList.java
@@ -0,0 +1,328 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.res;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.util.AttributeSet;
+import android.util.SparseArray;
+import android.util.StateSet;
+import android.util.Xml;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.util.Arrays;
+
+import com.android.internal.util.ArrayUtils;
+
+/**
+ *
+ * Lets you map {@link android.view.View} state sets to colors.
+ *
+ * {@link android.content.res.ColorStateList}s are created from XML resource files defined in the
+ * "color" subdirectory directory of an application's resource directory. The XML file contains
+ * a single "selector" element with a number of "item" elements inside. For example:
+ *
+ * <pre>
+ * &lt;selector xmlns:android="http://schemas.android.com/apk/res/android"&gt;
+ * &lt;item android:state_focused="true" android:color="@color/testcolor1"/&gt;
+ * &lt;item android:state_pressed="true" android:state_enabled="false" android:color="@color/testcolor2" /&gt;
+ * &lt;item android:state_enabled="false" android:colore="@color/testcolor3" /&gt;
+ * &lt;item android:state_active="true" android:color="@color/testcolor4" /&gt;
+ * &lt;item android:color="@color/testcolor5"/&gt;
+ * &lt;/selector&gt;
+ * </pre>
+ *
+ * This defines a set of state spec / color pairs where each state spec specifies a set of
+ * states that a view must either be in or not be in and the color specifies the color associated
+ * with that spec. The list of state specs will be processed in order of the items in the XML file.
+ * An item with no state spec is considered to match any set of states and is generally useful as
+ * a final item to be used as a default. Note that if you have such an item before any other items
+ * in the list then any subsequent items will end up being ignored.
+ */
+public class ColorStateList implements Parcelable {
+
+ private int[][] mStateSpecs; // must be parallel to mColors
+ private int[] mColors; // must be parallel to mStateSpecs
+ private int mDefaultColor = 0xffff0000;
+
+ private static final int[][] EMPTY = new int[][] { new int[0] };
+ private static final SparseArray<WeakReference<ColorStateList>> sCache =
+ new SparseArray<WeakReference<ColorStateList>>();
+
+ private ColorStateList() { }
+
+ /**
+ * Creates a ColorStateList that returns the specified mapping from
+ * states to colors.
+ */
+ public ColorStateList(int[][] states, int[] colors) {
+ mStateSpecs = states;
+ mColors = colors;
+
+ if (states.length > 0) {
+ mDefaultColor = colors[0];
+
+ for (int i = 0; i < states.length; i++) {
+ if (states[i].length == 0) {
+ mDefaultColor = colors[i];
+ }
+ }
+ }
+ }
+
+ /**
+ * Creates or retrieves a ColorStateList that always returns a single color.
+ */
+ public static ColorStateList valueOf(int color) {
+ // TODO: should we collect these eventually?
+ synchronized (sCache) {
+ WeakReference<ColorStateList> ref = sCache.get(color);
+ ColorStateList csl = ref != null ? ref.get() : null;
+
+ if (csl != null) {
+ return csl;
+ }
+
+ csl = new ColorStateList(EMPTY, new int[] { color });
+ sCache.put(color, new WeakReference<ColorStateList>(csl));
+ return csl;
+ }
+ }
+
+ /**
+ * Create a ColorStateList from an XML document, given a set of {@link Resources}.
+ */
+ public static ColorStateList createFromXml(Resources r, XmlPullParser parser)
+ throws XmlPullParserException, IOException {
+ AttributeSet attrs = Xml.asAttributeSet(parser);
+
+ int type;
+ while ((type=parser.next()) != XmlPullParser.START_TAG
+ && type != XmlPullParser.END_DOCUMENT) {
+ }
+
+ if (type != XmlPullParser.START_TAG) {
+ throw new XmlPullParserException("No start tag found");
+ }
+
+ final ColorStateList colorStateList = createFromXmlInner(r, parser, attrs);
+
+ return colorStateList;
+ }
+
+ /* Create from inside an XML document. Called on a parser positioned at
+ * a tag in an XML document, tries to create a ColorStateList from that tag.
+ * Returns null if the tag is not a valid ColorStateList.
+ */
+ private static ColorStateList createFromXmlInner(Resources r,
+ XmlPullParser parser,
+ AttributeSet attrs)
+ throws XmlPullParserException, IOException {
+ ColorStateList colorStateList;
+
+ final String name = parser.getName();
+
+ if (name.equals("selector")) {
+ colorStateList = new ColorStateList();
+ } else {
+ throw new XmlPullParserException(
+ parser.getPositionDescription() + ": invalid drawable tag "
+ + name);
+ }
+
+ colorStateList.inflate(r, parser, attrs);
+ return colorStateList;
+ }
+
+ /**
+ * Creates a new ColorStateList that has the same states and
+ * colors as this one but where each color has the specified alpha value
+ * (0-255).
+ */
+ public ColorStateList withAlpha(int alpha) {
+ int[] colors = new int[mColors.length];
+
+ int len = colors.length;
+ for (int i = 0; i < len; i++) {
+ colors[i] = (mColors[i] & 0xFFFFFF) | (alpha << 24);
+ }
+
+ return new ColorStateList(mStateSpecs, colors);
+ }
+
+ /**
+ * Fill in this object based on the contents of an XML "selector" element.
+ */
+ private void inflate(Resources r, XmlPullParser parser, AttributeSet attrs)
+ throws XmlPullParserException, IOException {
+
+ int type;
+
+ final int innerDepth = parser.getDepth()+1;
+ int depth;
+
+ int listAllocated = 20;
+ int listSize = 0;
+ int[] colorList = new int[listAllocated];
+ int[][] stateSpecList = new int[listAllocated][];
+
+ while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
+ && ((depth=parser.getDepth()) >= innerDepth
+ || type != XmlPullParser.END_TAG)) {
+ if (type != XmlPullParser.START_TAG) {
+ continue;
+ }
+
+ if (depth > innerDepth || !parser.getName().equals("item")) {
+ continue;
+ }
+
+ int colorRes = 0;
+ int color = 0xffff0000;
+ boolean haveColor = false;
+
+ int i;
+ int j = 0;
+ final int numAttrs = attrs.getAttributeCount();
+ int[] stateSpec = new int[numAttrs];
+ for (i = 0; i < numAttrs; i++) {
+ final int stateResId = attrs.getAttributeNameResource(i);
+ if (stateResId == 0) break;
+ if (stateResId == com.android.internal.R.attr.color) {
+ colorRes = attrs.getAttributeResourceValue(i, 0);
+
+ if (colorRes == 0) {
+ color = attrs.getAttributeIntValue(i, color);
+ haveColor = true;
+ }
+ } else {
+ stateSpec[j++] = attrs.getAttributeBooleanValue(i, false)
+ ? stateResId
+ : -stateResId;
+ }
+ }
+ stateSpec = StateSet.trimStateSet(stateSpec, j);
+
+ if (colorRes != 0) {
+ color = r.getColor(colorRes);
+ } else if (!haveColor) {
+ throw new XmlPullParserException(
+ parser.getPositionDescription()
+ + ": <item> tag requires a 'android:color' attribute.");
+ }
+
+ if (listSize == 0 || stateSpec.length == 0) {
+ mDefaultColor = color;
+ }
+
+ if (listSize + 1 >= listAllocated) {
+ listAllocated = ArrayUtils.idealIntArraySize(listSize + 1);
+
+ int[] ncolor = new int[listAllocated];
+ System.arraycopy(colorList, 0, ncolor, 0, listSize);
+
+ int[][] nstate = new int[listAllocated][];
+ System.arraycopy(stateSpecList, 0, nstate, 0, listSize);
+
+ colorList = ncolor;
+ stateSpecList = nstate;
+ }
+
+ colorList[listSize] = color;
+ stateSpecList[listSize] = stateSpec;
+ listSize++;
+ }
+
+ mColors = new int[listSize];
+ mStateSpecs = new int[listSize][];
+ System.arraycopy(colorList, 0, mColors, 0, listSize);
+ System.arraycopy(stateSpecList, 0, mStateSpecs, 0, listSize);
+ }
+
+ public boolean isStateful() {
+ return mStateSpecs.length > 1;
+ }
+
+ /**
+ * Return the color associated with the given set of {@link android.view.View} states.
+ *
+ * @param stateSet an array of {@link android.view.View} states
+ * @param defaultColor the color to return if there's not state spec in this
+ * {@link ColorStateList} that matches the stateSet.
+ *
+ * @return the color associated with that set of states in this {@link ColorStateList}.
+ */
+ public int getColorForState(int[] stateSet, int defaultColor) {
+ final int setLength = mStateSpecs.length;
+ for (int i = 0; i < setLength; i++) {
+ int[] stateSpec = mStateSpecs[i];
+ if (StateSet.stateSetMatches(stateSpec, stateSet)) {
+ return mColors[i];
+ }
+ }
+ return defaultColor;
+ }
+
+ /**
+ * Return the default color in this {@link ColorStateList}.
+ *
+ * @return the default color in this {@link ColorStateList}.
+ */
+ public int getDefaultColor() {
+ return mDefaultColor;
+ }
+
+ public String toString() {
+ return "ColorStateList{" +
+ "mStateSpecs=" + Arrays.deepToString(mStateSpecs) +
+ "mColors=" + Arrays.toString(mColors) +
+ "mDefaultColor=" + mDefaultColor + '}';
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeArray(mStateSpecs);
+ dest.writeIntArray(mColors);
+ }
+
+ public static final Parcelable.Creator<ColorStateList> CREATOR =
+ new Parcelable.Creator<ColorStateList>() {
+ public ColorStateList[] newArray(int size) {
+ return new ColorStateList[size];
+ }
+
+ public ColorStateList createFromParcel(Parcel source) {
+ Object[] o = source.readArray(
+ ColorStateList.class.getClassLoader());
+ int[][] stateSpecs = new int[o.length][];
+
+ for (int i = 0; i < o.length; i++) {
+ stateSpecs[i] = (int[]) o[i];
+ }
+
+ int[] colors = source.createIntArray();
+ return new ColorStateList(stateSpecs, colors);
+ }
+ };
+}
diff --git a/core/java/android/content/res/Configuration.aidl b/core/java/android/content/res/Configuration.aidl
new file mode 100755
index 0000000..bb7f2dd
--- /dev/null
+++ b/core/java/android/content/res/Configuration.aidl
@@ -0,0 +1,21 @@
+/* //device/java/android/android/view/WindowManager.aidl
+**
+** Copyright 2007, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.content.res;
+
+parcelable Configuration;
+
diff --git a/core/java/android/content/res/Configuration.java b/core/java/android/content/res/Configuration.java
new file mode 100644
index 0000000..78a90de
--- /dev/null
+++ b/core/java/android/content/res/Configuration.java
@@ -0,0 +1,386 @@
+package android.content.res;
+
+import android.content.pm.ActivityInfo;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Locale;
+
+/**
+ * This class describes all device configuration information that can
+ * impact the resources the application retrieves. This includes both
+ * user-specified configuration options (locale and scaling) as well
+ * as dynamic device configuration (various types of input devices).
+ */
+public final class Configuration implements Parcelable, Comparable<Configuration> {
+ /**
+ * Current user preference for the scaling factor for fonts, relative
+ * to the base density scaling.
+ */
+ public float fontScale;
+
+ /**
+ * IMSI MCC (Mobile Country Code). 0 if undefined.
+ */
+ public int mcc;
+
+ /**
+ * IMSI MNC (Mobile Network Code). 0 if undefined.
+ */
+ public int mnc;
+
+ /**
+ * Current user preference for the locale.
+ */
+ public Locale locale;
+
+ public static final int TOUCHSCREEN_UNDEFINED = 0;
+ public static final int TOUCHSCREEN_NOTOUCH = 1;
+ public static final int TOUCHSCREEN_STYLUS = 2;
+ public static final int TOUCHSCREEN_FINGER = 3;
+
+ /**
+ * The kind of touch screen attached to the device.
+ * One of: {@link #TOUCHSCREEN_NOTOUCH}, {@link #TOUCHSCREEN_STYLUS},
+ * {@link #TOUCHSCREEN_FINGER}.
+ */
+ public int touchscreen;
+
+ public static final int KEYBOARD_UNDEFINED = 0;
+ public static final int KEYBOARD_NOKEYS = 1;
+ public static final int KEYBOARD_QWERTY = 2;
+ public static final int KEYBOARD_12KEY = 3;
+
+ /**
+ * The kind of keyboard attached to the device.
+ * One of: {@link #KEYBOARD_QWERTY}, {@link #KEYBOARD_12KEY}.
+ */
+ public int keyboard;
+
+ public static final int KEYBOARDHIDDEN_UNDEFINED = 0;
+ public static final int KEYBOARDHIDDEN_NO = 1;
+ public static final int KEYBOARDHIDDEN_YES = 2;
+
+ /**
+ * A flag indicating whether the keyboard has been hidden. This will
+ * be set on a device with a mechanism to hide the keyboard from the
+ * user, when that mechanism is closed.
+ */
+ public int keyboardHidden;
+
+ public static final int NAVIGATION_UNDEFINED = 0;
+ public static final int NAVIGATION_NONAV = 1;
+ public static final int NAVIGATION_DPAD = 2;
+ public static final int NAVIGATION_TRACKBALL = 3;
+ public static final int NAVIGATION_WHEEL = 4;
+
+ /**
+ * The kind of navigation method available on the device.
+ * One of: {@link #NAVIGATION_DPAD}, {@link #NAVIGATION_TRACKBALL},
+ * {@link #NAVIGATION_WHEEL}.
+ */
+ public int navigation;
+
+ public static final int ORIENTATION_UNDEFINED = 0;
+ public static final int ORIENTATION_PORTRAIT = 1;
+ public static final int ORIENTATION_LANDSCAPE = 2;
+ public static final int ORIENTATION_SQUARE = 3;
+
+ /**
+ * Overall orientation of the screen. May be one of
+ * {@link #ORIENTATION_LANDSCAPE}, {@link #ORIENTATION_PORTRAIT},
+ * or {@link #ORIENTATION_SQUARE}.
+ */
+ public int orientation;
+
+ /**
+ * Construct an invalid Configuration. You must call {@link #setToDefaults}
+ * for this object to be valid. {@more}
+ */
+ public Configuration() {
+ setToDefaults();
+ }
+
+ /**
+ * Makes a deep copy suitable for modification.
+ */
+ public Configuration(Configuration o) {
+ fontScale = o.fontScale;
+ mcc = o.mcc;
+ mnc = o.mnc;
+ if (o.locale != null) {
+ locale = (Locale) o.locale.clone();
+ }
+ touchscreen = o.touchscreen;
+ keyboard = o.keyboard;
+ keyboardHidden = o.keyboardHidden;
+ navigation = o.navigation;
+ orientation = o.orientation;
+ }
+
+ public String toString() {
+ return "{ scale=" + fontScale + " imsi=" + mcc + "/" + mnc
+ + " locale=" + locale
+ + " touch=" + touchscreen + " key=" + keyboard + "/"
+ + keyboardHidden
+ + " nav=" + navigation + " orien=" + orientation + " }";
+ }
+
+ /**
+ * Set this object to the system defaults.
+ */
+ public void setToDefaults() {
+ fontScale = 1;
+ mcc = mnc = 0;
+ locale = Locale.getDefault();
+ touchscreen = TOUCHSCREEN_UNDEFINED;
+ keyboard = KEYBOARD_UNDEFINED;
+ keyboardHidden = KEYBOARDHIDDEN_UNDEFINED;
+ navigation = NAVIGATION_UNDEFINED;
+ orientation = ORIENTATION_UNDEFINED;
+ }
+
+ /** {@hide} */
+ @Deprecated public void makeDefault() {
+ setToDefaults();
+ }
+
+ /**
+ * Copy the fields from delta into this Configuration object, keeping
+ * track of which ones have changed. Any undefined fields in
+ * <var>delta</var> are ignored and not copied in to the current
+ * Configuration.
+ * @return Returns a bit mask of the changed fields, as per
+ * {@link #diff}.
+ */
+ public int updateFrom(Configuration delta) {
+ int changed = 0;
+ if (delta.fontScale > 0 && fontScale != delta.fontScale) {
+ changed |= ActivityInfo.CONFIG_FONT_SCALE;
+ fontScale = delta.fontScale;
+ }
+ if (delta.mcc != 0 && mcc != delta.mcc) {
+ changed |= ActivityInfo.CONFIG_MCC;
+ mcc = delta.mcc;
+ }
+ if (delta.mnc != 0 && mnc != delta.mnc) {
+ changed |= ActivityInfo.CONFIG_MNC;
+ mnc = delta.mnc;
+ }
+ if (delta.locale != null
+ && (locale == null || !locale.equals(delta.locale))) {
+ changed |= ActivityInfo.CONFIG_LOCALE;
+ locale = delta.locale != null
+ ? (Locale) delta.locale.clone() : null;
+ }
+ if (delta.touchscreen != TOUCHSCREEN_UNDEFINED
+ && touchscreen != delta.touchscreen) {
+ changed |= ActivityInfo.CONFIG_TOUCHSCREEN;
+ touchscreen = delta.touchscreen;
+ }
+ if (delta.keyboard != KEYBOARD_UNDEFINED
+ && keyboard != delta.keyboard) {
+ changed |= ActivityInfo.CONFIG_KEYBOARD;
+ keyboard = delta.keyboard;
+ }
+ if (delta.keyboardHidden != KEYBOARDHIDDEN_UNDEFINED
+ && keyboardHidden != delta.keyboardHidden) {
+ changed |= ActivityInfo.CONFIG_KEYBOARD_HIDDEN;
+ keyboardHidden = delta.keyboardHidden;
+ }
+ if (delta.navigation != NAVIGATION_UNDEFINED
+ && navigation != delta.navigation) {
+ changed |= ActivityInfo.CONFIG_NAVIGATION;
+ navigation = delta.navigation;
+ }
+ if (delta.orientation != ORIENTATION_UNDEFINED
+ && orientation != delta.orientation) {
+ changed |= ActivityInfo.CONFIG_ORIENTATION;
+ orientation = delta.orientation;
+ }
+
+ return changed;
+ }
+
+ /**
+ * Return a bit mask of the differences between this Configuration
+ * object and the given one. Does not change the values of either. Any
+ * undefined fields in <var>delta</var> are ignored.
+ * @return Returns a bit mask indicating which configuration
+ * values has changed, containing any combination of
+ * {@link android.content.pm.ActivityInfo#CONFIG_FONT_SCALE
+ * PackageManager.ActivityInfo.CONFIG_FONT_SCALE},
+ * {@link android.content.pm.ActivityInfo#CONFIG_MCC
+ * PackageManager.ActivityInfo.CONFIG_MCC},
+ * {@link android.content.pm.ActivityInfo#CONFIG_MNC
+ * PackageManager.ActivityInfo.CONFIG_MNC},
+ * {@link android.content.pm.ActivityInfo#CONFIG_LOCALE
+ * PackageManager.ActivityInfo.CONFIG_LOCALE},
+ * {@link android.content.pm.ActivityInfo#CONFIG_TOUCHSCREEN
+ * PackageManager.ActivityInfo.CONFIG_TOUCHSCREEN},
+ * {@link android.content.pm.ActivityInfo#CONFIG_KEYBOARD
+ * PackageManager.ActivityInfo.CONFIG_KEYBOARD},
+ * {@link android.content.pm.ActivityInfo#CONFIG_NAVIGATION
+ * PackageManager.ActivityInfo.CONFIG_NAVIGATION}, or
+ * {@link android.content.pm.ActivityInfo#CONFIG_ORIENTATION
+ * PackageManager.ActivityInfo.CONFIG_ORIENTATION}.
+ */
+ public int diff(Configuration delta) {
+ int changed = 0;
+ if (delta.fontScale > 0 && fontScale != delta.fontScale) {
+ changed |= ActivityInfo.CONFIG_FONT_SCALE;
+ }
+ if (delta.mcc != 0 && mcc != delta.mcc) {
+ changed |= ActivityInfo.CONFIG_MCC;
+ }
+ if (delta.mnc != 0 && mnc != delta.mnc) {
+ changed |= ActivityInfo.CONFIG_MNC;
+ }
+ if (delta.locale != null
+ && (locale == null || !locale.equals(delta.locale))) {
+ changed |= ActivityInfo.CONFIG_LOCALE;
+ }
+ if (delta.touchscreen != TOUCHSCREEN_UNDEFINED
+ && touchscreen != delta.touchscreen) {
+ changed |= ActivityInfo.CONFIG_TOUCHSCREEN;
+ }
+ if (delta.keyboard != KEYBOARD_UNDEFINED
+ && keyboard != delta.keyboard) {
+ changed |= ActivityInfo.CONFIG_KEYBOARD;
+ }
+ if (delta.keyboardHidden != KEYBOARDHIDDEN_UNDEFINED
+ && keyboardHidden != delta.keyboardHidden) {
+ changed |= ActivityInfo.CONFIG_KEYBOARD_HIDDEN;
+ }
+ if (delta.navigation != NAVIGATION_UNDEFINED
+ && navigation != delta.navigation) {
+ changed |= ActivityInfo.CONFIG_NAVIGATION;
+ }
+ if (delta.orientation != ORIENTATION_UNDEFINED
+ && orientation != delta.orientation) {
+ changed |= ActivityInfo.CONFIG_ORIENTATION;
+ }
+
+ return changed;
+ }
+
+ /**
+ * Determine if a new resource needs to be loaded from the bit set of
+ * configuration changes returned by {@link #updateFrom(Configuration)}.
+ *
+ * @param configChanges The mask of changes configurations as returned by
+ * {@link #updateFrom(Configuration)}.
+ * @param interestingChanges The configuration changes that the resource
+ * can handled, as given in {@link android.util.TypedValue#changingConfigurations}.
+ *
+ * @return Return true if the resource needs to be loaded, else false.
+ */
+ public static boolean needNewResources(int configChanges, int interestingChanges) {
+ return (configChanges & (interestingChanges|ActivityInfo.CONFIG_FONT_SCALE)) != 0;
+ }
+
+ /**
+ * Parcelable methods
+ */
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeFloat(fontScale);
+ dest.writeInt(mcc);
+ dest.writeInt(mnc);
+ if (locale == null) {
+ dest.writeInt(0);
+ } else {
+ dest.writeInt(1);
+ dest.writeString(locale.getLanguage());
+ dest.writeString(locale.getCountry());
+ dest.writeString(locale.getVariant());
+ }
+ dest.writeInt(touchscreen);
+ dest.writeInt(keyboard);
+ dest.writeInt(keyboardHidden);
+ dest.writeInt(navigation);
+ dest.writeInt(orientation);
+ }
+
+ public static final Parcelable.Creator<Configuration> CREATOR
+ = new Parcelable.Creator<Configuration>() {
+ public Configuration createFromParcel(Parcel source) {
+ return new Configuration(source);
+ }
+
+ public Configuration[] newArray(int size) {
+ return new Configuration[size];
+ }
+ };
+
+ /**
+ * Construct this Configuration object, reading from the Parcel.
+ */
+ private Configuration(Parcel source) {
+ fontScale = source.readFloat();
+ mcc = source.readInt();
+ mnc = source.readInt();
+ if (source.readInt() != 0) {
+ locale = new Locale(source.readString(), source.readString(),
+ source.readString());
+ }
+ touchscreen = source.readInt();
+ keyboard = source.readInt();
+ keyboardHidden = source.readInt();
+ navigation = source.readInt();
+ orientation = source.readInt();
+ }
+
+ public int compareTo(Configuration that) {
+ int n;
+ float a = this.fontScale;
+ float b = that.fontScale;
+ if (a < b) return -1;
+ if (a > b) return 1;
+ n = this.mcc - that.mcc;
+ if (n != 0) return n;
+ n = this.mnc - that.mnc;
+ if (n != 0) return n;
+ n = this.locale.getLanguage().compareTo(that.locale.getLanguage());
+ if (n != 0) return n;
+ n = this.locale.getCountry().compareTo(that.locale.getCountry());
+ if (n != 0) return n;
+ n = this.locale.getVariant().compareTo(that.locale.getVariant());
+ if (n != 0) return n;
+ n = this.touchscreen - that.touchscreen;
+ if (n != 0) return n;
+ n = this.keyboard - that.keyboard;
+ if (n != 0) return n;
+ n = this.keyboardHidden - that.keyboardHidden;
+ if (n != 0) return n;
+ n = this.navigation - that.navigation;
+ if (n != 0) return n;
+ n = this.orientation - that.orientation;
+ //if (n != 0) return n;
+ return n;
+ }
+
+ public boolean equals(Configuration that) {
+ if (that == null) return false;
+ if (that == this) return true;
+ return this.compareTo(that) == 0;
+ }
+
+ public boolean equals(Object that) {
+ try {
+ return equals((Configuration)that);
+ } catch (ClassCastException e) {
+ }
+ return false;
+ }
+
+ public int hashCode() {
+ return ((int)this.fontScale) + this.mcc + this.mnc
+ + this.locale.hashCode() + this.touchscreen
+ + this.keyboard + this.keyboardHidden + this.navigation
+ + this.orientation;
+ }
+} \ No newline at end of file
diff --git a/core/java/android/content/res/PluralRules.java b/core/java/android/content/res/PluralRules.java
new file mode 100644
index 0000000..2d09ef0
--- /dev/null
+++ b/core/java/android/content/res/PluralRules.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.res;
+
+import java.util.Locale;
+
+/*
+ * Yuck-o. This is not the right way to implement this. When the ICU PluralRules
+ * object has been integrated to android, we should switch to that. For now, yuck-o.
+ */
+
+abstract class PluralRule {
+
+ static final int QUANTITY_OTHER = 0x0000;
+ static final int QUANTITY_ZERO = 0x0001;
+ static final int QUANTITY_ONE = 0x0002;
+ static final int QUANTITY_TWO = 0x0004;
+ static final int QUANTITY_FEW = 0x0008;
+ static final int QUANTITY_MANY = 0x0010;
+
+ static final int ID_OTHER = 0x01000004;
+
+ abstract int quantityForNumber(int n);
+
+ final int attrForNumber(int n) {
+ return PluralRule.attrForQuantity(quantityForNumber(n));
+ }
+
+ static final int attrForQuantity(int quantity) {
+ // see include/utils/ResourceTypes.h
+ switch (quantity) {
+ case QUANTITY_ZERO: return 0x01000005;
+ case QUANTITY_ONE: return 0x01000006;
+ case QUANTITY_TWO: return 0x01000007;
+ case QUANTITY_FEW: return 0x01000008;
+ case QUANTITY_MANY: return 0x01000009;
+ default: return ID_OTHER;
+ }
+ }
+
+ static final String stringForQuantity(int quantity) {
+ switch (quantity) {
+ case QUANTITY_ZERO:
+ return "zero";
+ case QUANTITY_ONE:
+ return "one";
+ case QUANTITY_TWO:
+ return "two";
+ case QUANTITY_FEW:
+ return "few";
+ case QUANTITY_MANY:
+ return "many";
+ default:
+ return "other";
+ }
+ }
+
+ static final PluralRule ruleForLocale(Locale locale) {
+ String lang = locale.getLanguage();
+ if ("cs".equals(lang)) {
+ if (cs == null) cs = new cs();
+ return cs;
+ }
+ else {
+ if (en == null) en = new en();
+ return en;
+ }
+ }
+
+ private static PluralRule cs;
+ private static class cs extends PluralRule {
+ int quantityForNumber(int n) {
+ if (n == 1) {
+ return QUANTITY_ONE;
+ }
+ else if (n >= 2 && n <= 4) {
+ return QUANTITY_FEW;
+ }
+ else {
+ return QUANTITY_OTHER;
+ }
+ }
+ }
+
+ private static PluralRule en;
+ private static class en extends PluralRule {
+ int quantityForNumber(int n) {
+ if (n == 1) {
+ return QUANTITY_ONE;
+ }
+ else {
+ return QUANTITY_OTHER;
+ }
+ }
+ }
+}
+
diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java
new file mode 100644
index 0000000..1014eee
--- /dev/null
+++ b/core/java/android/content/res/Resources.java
@@ -0,0 +1,1659 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.res;
+
+
+import android.graphics.Movie;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.ColorDrawable;
+import android.os.SystemProperties;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.SparseArray;
+import android.util.TypedValue;
+
+import java.io.InputStream;
+import java.lang.ref.WeakReference;
+
+/**
+ * Class for accessing an application's resources. This sits on top of the
+ * asset manager of the application (accessible through getAssets()) and
+ * provides a higher-level API for getting typed data from the assets.
+ */
+public class Resources {
+ static final String TAG = "Resources";
+ private static final boolean DEBUG_LOAD = false;
+ private static final boolean DEBUG_CONFIG = false;
+
+ private static final int sSdkVersion = SystemProperties.getInt(
+ "ro.build.version.sdk", 0);
+ private static final Object mSync = new Object();
+ private static Resources mSystem = null;
+
+ // Information about preloaded resources. Note that they are not
+ // protected by a lock, because while preloading in zygote we are all
+ // single-threaded, and after that these are immutable.
+ private static final SparseArray<Drawable.ConstantState> mPreloadedDrawables
+ = new SparseArray<Drawable.ConstantState>();
+ private static boolean mPreloaded;
+
+ /*package*/ final TypedValue mTmpValue = new TypedValue();
+
+ // These are protected by the mTmpValue lock.
+ private final SparseArray<WeakReference<Drawable.ConstantState> > mDrawableCache
+ = new SparseArray<WeakReference<Drawable.ConstantState> >();
+ private final SparseArray<WeakReference<ColorStateList> > mColorStateListCache
+ = new SparseArray<WeakReference<ColorStateList> >();
+ private boolean mPreloading;
+
+ /*package*/ TypedArray mCachedStyledAttributes = null;
+
+ private int mLastCachedXmlBlockIndex = -1;
+ private final int[] mCachedXmlBlockIds = { 0, 0, 0, 0 };
+ private final XmlBlock[] mCachedXmlBlocks = new XmlBlock[4];
+
+ /*package*/ final AssetManager mAssets;
+ private final Configuration mConfiguration = new Configuration();
+ /*package*/ final DisplayMetrics mMetrics = new DisplayMetrics();
+ PluralRule mPluralRule;
+
+ /**
+ * This exception is thrown by the resource APIs when a requested resource
+ * can not be found.
+ */
+ public static class NotFoundException extends RuntimeException {
+ public NotFoundException() {
+ }
+
+ public NotFoundException(String name) {
+ super(name);
+ }
+ };
+
+ /**
+ * Create a new Resources object on top of an existing set of assets in an
+ * AssetManager.
+ *
+ * @param assets Previously created AssetManager.
+ * @param metrics Current display metrics to consider when
+ * selecting/computing resource values.
+ * @param config Desired device configuration to consider when
+ * selecting/computing resource values (optional).
+ */
+ public Resources(AssetManager assets, DisplayMetrics metrics,
+ Configuration config) {
+ mAssets = assets;
+ mConfiguration.setToDefaults();
+ mMetrics.setToDefaults();
+ updateConfiguration(config, metrics);
+ assets.ensureStringBlocks();
+ }
+
+ /**
+ * Return a global shared Resources object that provides access to only
+ * system resources (no application resources), and is not configured for
+ * the current screen (can not use dimension units, does not change based
+ * on orientation, etc).
+ */
+ public static Resources getSystem() {
+ synchronized (mSync) {
+ Resources ret = mSystem;
+ if (ret == null) {
+ ret = new Resources();
+ mSystem = ret;
+ }
+
+ return ret;
+ }
+ }
+
+ /**
+ * Return the string value associated with a particular resource ID. The
+ * returned object will be a String if this is a plain string; it will be
+ * some other type of CharSequence if it is styled.
+ * {@more}
+ *
+ * @param id The desired resource identifier, as generated by the aapt
+ * tool. This integer encodes the package, type, and resource
+ * entry. The value 0 is an invalid identifier.
+ *
+ * @throws NotFoundException Throws NotFoundException if the given ID does not exist.
+ *
+ * @return CharSequence The string data associated with the resource, plus
+ * possibly styled text information.
+ */
+ public CharSequence getText(int id) throws NotFoundException {
+ CharSequence res = mAssets.getResourceText(id);
+ if (res != null) {
+ return res;
+ }
+ throw new NotFoundException("String resource ID #0x"
+ + Integer.toHexString(id));
+ }
+
+ /**
+ * @param id The desired resource identifier, as generated by the aapt
+ * tool. This integer encodes the package, type, and resource
+ * entry. The value 0 is an invalid identifier.
+ *
+ * @throws NotFoundException Throws NotFoundException if the given ID does not exist.
+ *
+ * @return CharSequence The string data associated with the resource, plus
+ * possibly styled text information.
+ */
+ public CharSequence getQuantityText(int id, int quantity) throws NotFoundException {
+ PluralRule rule = getPluralRule();
+ CharSequence res = mAssets.getResourceBagText(id, rule.attrForNumber(quantity));
+ if (res != null) {
+ return res;
+ }
+ res = mAssets.getResourceBagText(id, PluralRule.ID_OTHER);
+ if (res != null) {
+ return res;
+ }
+ throw new NotFoundException("Plural resource ID #0x" + Integer.toHexString(id)
+ + " quantity=" + quantity
+ + " item=" + PluralRule.stringForQuantity(rule.quantityForNumber(quantity)));
+ }
+
+ private PluralRule getPluralRule() {
+ synchronized (mSync) {
+ if (mPluralRule == null) {
+ mPluralRule = PluralRule.ruleForLocale(mConfiguration.locale);
+ }
+ return mPluralRule;
+ }
+ }
+
+ /**
+ * Return the string value associated with a particular resource ID. It
+ * will be stripped of any styled text information.
+ * {@more}
+ *
+ * @param id The desired resource identifier, as generated by the aapt
+ * tool. This integer encodes the package, type, and resource
+ * entry. The value 0 is an invalid identifier.
+ *
+ * @throws NotFoundException Throws NotFoundException if the given ID does not exist.
+ *
+ * @return String The string data associated with the resource,
+ * stripped of styled text information.
+ */
+ public String getString(int id) throws NotFoundException {
+ CharSequence res = getText(id);
+ if (res != null) {
+ return res.toString();
+ }
+ throw new NotFoundException("String resource ID #0x"
+ + Integer.toHexString(id));
+ }
+
+
+ /**
+ * Return the string value associated with a particular resource ID,
+ * substituting the format arguments as defined in {@link java.util.Formatter}
+ * and {@link java.lang.String#format}. It will be stripped of any styled text
+ * information.
+ * {@more}
+ *
+ * @param id The desired resource identifier, as generated by the aapt
+ * tool. This integer encodes the package, type, and resource
+ * entry. The value 0 is an invalid identifier.
+ *
+ * @param formatArgs The format arguments that will be used for substitution.
+ *
+ * @throws NotFoundException Throws NotFoundException if the given ID does not exist.
+ *
+ * @return String The string data associated with the resource,
+ * stripped of styled text information.
+ */
+ public String getString(int id, Object... formatArgs) throws NotFoundException {
+ String raw = getString(id);
+ return String.format(mConfiguration.locale, raw, formatArgs);
+ }
+
+ /**
+ * Return the string value associated with a particular resource ID for a particular
+ * numerical quantity, substituting the format arguments as defined in
+ * {@link java.util.Formatter} and {@link java.lang.String#format}. It will be
+ * stripped of any styled text information.
+ * {@more}
+ *
+ * @param id The desired resource identifier, as generated by the aapt
+ * tool. This integer encodes the package, type, and resource
+ * entry. The value 0 is an invalid identifier.
+ * @param quantity The number used to get the correct string for the current language's
+ * plural rules.
+ * @param formatArgs The format arguments that will be used for substitution.
+ *
+ * @throws NotFoundException Throws NotFoundException if the given ID does not exist.
+ *
+ * @return String The string data associated with the resource,
+ * stripped of styled text information.
+ */
+ public String getQuantityString(int id, int quantity, Object... formatArgs)
+ throws NotFoundException {
+ String raw = getQuantityText(id, quantity).toString();
+ return String.format(mConfiguration.locale, raw, formatArgs);
+ }
+
+ /**
+ * Return the string value associated with a particular resource ID for a particular
+ * numerical quantity.
+ *
+ * @param id The desired resource identifier, as generated by the aapt
+ * tool. This integer encodes the package, type, and resource
+ * entry. The value 0 is an invalid identifier.
+ * @param quantity The number used to get the correct string for the current language's
+ * plural rules.
+ *
+ * @throws NotFoundException Throws NotFoundException if the given ID does not exist.
+ *
+ * @return String The string data associated with the resource,
+ * stripped of styled text information.
+ */
+ public String getQuantityString(int id, int quantity) throws NotFoundException {
+ return getQuantityText(id, quantity).toString();
+ }
+
+ /**
+ * Return the string value associated with a particular resource ID. The
+ * returned object will be a String if this is a plain string; it will be
+ * some other type of CharSequence if it is styled.
+ *
+ * @param id The desired resource identifier, as generated by the aapt
+ * tool. This integer encodes the package, type, and resource
+ * entry. The value 0 is an invalid identifier.
+ *
+ * @param def The default CharSequence to return.
+ *
+ * @return CharSequence The string data associated with the resource, plus
+ * possibly styled text information, or def if id is 0 or not found.
+ */
+ public CharSequence getText(int id, CharSequence def) {
+ CharSequence res = id != 0 ? mAssets.getResourceText(id) : null;
+ return res != null ? res : def;
+ }
+
+ /**
+ * Return the styled text array associated with a particular resource ID.
+ *
+ * @param id The desired resource identifier, as generated by the aapt
+ * tool. This integer encodes the package, type, and resource
+ * entry. The value 0 is an invalid identifier.
+ *
+ * @throws NotFoundException Throws NotFoundException if the given ID does not exist.
+ *
+ * @return The styled text array associated with the resource.
+ */
+ public CharSequence[] getTextArray(int id) throws NotFoundException {
+ CharSequence[] res = mAssets.getResourceTextArray(id);
+ if (res != null) {
+ return res;
+ }
+ throw new NotFoundException("Text array resource ID #0x"
+ + Integer.toHexString(id));
+ }
+
+ /**
+ * Return the string array associated with a particular resource ID.
+ *
+ * @param id The desired resource identifier, as generated by the aapt
+ * tool. This integer encodes the package, type, and resource
+ * entry. The value 0 is an invalid identifier.
+ *
+ * @throws NotFoundException Throws NotFoundException if the given ID does not exist.
+ *
+ * @return The string array associated with the resource.
+ */
+ public String[] getStringArray(int id) throws NotFoundException {
+ String[] res = mAssets.getResourceStringArray(id);
+ if (res != null) {
+ return res;
+ }
+ throw new NotFoundException("String array resource ID #0x"
+ + Integer.toHexString(id));
+ }
+
+ /**
+ * Return the int array associated with a particular resource ID.
+ *
+ * @param id The desired resource identifier, as generated by the aapt
+ * tool. This integer encodes the package, type, and resource
+ * entry. The value 0 is an invalid identifier.
+ *
+ * @throws NotFoundException Throws NotFoundException if the given ID does not exist.
+ *
+ * @return The int array associated with the resource.
+ */
+ public int[] getIntArray(int id) throws NotFoundException {
+ int[] res = mAssets.getArrayIntResource(id);
+ if (res != null) {
+ return res;
+ }
+ throw new NotFoundException("Int array resource ID #0x"
+ + Integer.toHexString(id));
+ }
+
+ /**
+ * Return an array of heterogeneous values.
+ *
+ * @param id The desired resource identifier, as generated by the aapt
+ * tool. This integer encodes the package, type, and resource
+ * entry. The value 0 is an invalid identifier.
+ *
+ * @throws NotFoundException Throws NotFoundException if the given ID does not exist.
+ *
+ * @return Returns a TypedArray holding an array of the array values.
+ * Be sure to call {@link TypedArray#recycle() TypedArray.recycle()}
+ * when done with it.
+ */
+ public TypedArray obtainTypedArray(int id) throws NotFoundException {
+ int len = mAssets.getArraySize(id);
+ if (len < 0) {
+ throw new NotFoundException("Array resource ID #0x"
+ + Integer.toHexString(id));
+ }
+
+ TypedArray array = getCachedStyledAttributes(len);
+ array.mLength = mAssets.retrieveArray(id, array.mData);
+ array.mIndices[0] = 0;
+
+ return array;
+ }
+
+ /**
+ * Retrieve a dimensional for a particular resource ID. Unit
+ * conversions are based on the current {@link DisplayMetrics} associated
+ * with the resources.
+ *
+ * @param id The desired resource identifier, as generated by the aapt
+ * tool. This integer encodes the package, type, and resource
+ * entry. The value 0 is an invalid identifier.
+ *
+ * @return Resource dimension value multiplied by the appropriate
+ * metric.
+ *
+ * @throws NotFoundException Throws NotFoundException if the given ID does not exist.
+ *
+ * @return CharSequence The string data associated with the resource, plus
+ * @see #getDimensionPixelOffset
+ * @see #getDimensionPixelSize
+ */
+ public float getDimension(int id) throws NotFoundException {
+ synchronized (mTmpValue) {
+ TypedValue value = mTmpValue;
+ getValue(id, value, true);
+ if (value.type == TypedValue.TYPE_DIMENSION) {
+ return TypedValue.complexToDimension(value.data, mMetrics);
+ }
+ throw new NotFoundException(
+ "Resource ID #0x" + Integer.toHexString(id) + " type #0x"
+ + Integer.toHexString(value.type) + " is not valid");
+ }
+ }
+
+ /**
+ * Retrieve a dimensional for a particular resource ID for use
+ * as an offset in raw pixels. This is the same as
+ * {@link #getDimension}, except the returned value is converted to
+ * integer pixels for you. An offset conversion involves simply
+ * truncating the base value to an integer.
+ *
+ * @param id The desired resource identifier, as generated by the aapt
+ * tool. This integer encodes the package, type, and resource
+ * entry. The value 0 is an invalid identifier.
+ *
+ * @return Resource dimension value multiplied by the appropriate
+ * metric and truncated to integer pixels.
+ *
+ * @throws NotFoundException Throws NotFoundException if the given ID does not exist.
+ *
+ * @return CharSequence The string data associated with the resource, plus
+ * @see #getDimension
+ * @see #getDimensionPixelSize
+ */
+ public int getDimensionPixelOffset(int id) throws NotFoundException {
+ synchronized (mTmpValue) {
+ TypedValue value = mTmpValue;
+ getValue(id, value, true);
+ if (value.type == TypedValue.TYPE_DIMENSION) {
+ return TypedValue.complexToDimensionPixelOffset(
+ value.data, mMetrics);
+ }
+ throw new NotFoundException(
+ "Resource ID #0x" + Integer.toHexString(id) + " type #0x"
+ + Integer.toHexString(value.type) + " is not valid");
+ }
+ }
+
+ /**
+ * Retrieve a dimensional for a particular resource ID for use
+ * as a size in raw pixels. This is the same as
+ * {@link #getDimension}, except the returned value is converted to
+ * integer pixels for use as a size. A size conversion involves
+ * rounding the base value, and ensuring that a non-zero base value
+ * is at least one pixel in size.
+ *
+ * @param id The desired resource identifier, as generated by the aapt
+ * tool. This integer encodes the package, type, and resource
+ * entry. The value 0 is an invalid identifier.
+ *
+ * @return Resource dimension value multiplied by the appropriate
+ * metric and truncated to integer pixels.
+ *
+ * @throws NotFoundException Throws NotFoundException if the given ID does not exist.
+ *
+ * @return CharSequence The string data associated with the resource, plus
+ * @see #getDimension
+ * @see #getDimensionPixelOffset
+ */
+ public int getDimensionPixelSize(int id) throws NotFoundException {
+ synchronized (mTmpValue) {
+ TypedValue value = mTmpValue;
+ getValue(id, value, true);
+ if (value.type == TypedValue.TYPE_DIMENSION) {
+ return TypedValue.complexToDimensionPixelSize(
+ value.data, mMetrics);
+ }
+ throw new NotFoundException(
+ "Resource ID #0x" + Integer.toHexString(id) + " type #0x"
+ + Integer.toHexString(value.type) + " is not valid");
+ }
+ }
+
+ /**
+ * Return a drawable object associated with a particular resource ID.
+ * Various types of objects will be returned depending on the underlying
+ * resource -- for example, a solid color, PNG image, scalable image, etc.
+ * The Drawable API hides these implementation details.
+ *
+ * @param id The desired resource identifier, as generated by the aapt
+ * tool. This integer encodes the package, type, and resource
+ * entry. The value 0 is an invalid identifier.
+ *
+ * @throws NotFoundException Throws NotFoundException if the given ID does not exist.
+ *
+ * @return Drawable An object that can be used to draw this resource.
+ */
+ public Drawable getDrawable(int id) throws NotFoundException {
+ synchronized (mTmpValue) {
+ TypedValue value = mTmpValue;
+ getValue(id, value, true);
+ return loadDrawable(value, id);
+ }
+ }
+
+ /**
+ * Return a movie object associated with the particular resource ID.
+ * @param id The desired resource identifier, as generated by the aapt
+ * tool. This integer encodes the package, type, and resource
+ * entry. The value 0 is an invalid identifier.
+ * @throws NotFoundException Throws NotFoundException if the given ID does not exist.
+ *
+ */
+ public Movie getMovie(int id) throws NotFoundException {
+ InputStream is = openRawResource(id);
+ Movie movie = Movie.decodeStream(is);
+ try {
+ is.close();
+ }
+ catch (java.io.IOException e) {
+ // don't care, since the return value is valid
+ }
+ return movie;
+ }
+
+ /**
+ * Return a color integer associated with a particular resource ID.
+ * If the resource holds a complex
+ * {@link android.content.res.ColorStateList}, then the default color from
+ * the set is returned.
+ *
+ * @param id The desired resource identifier, as generated by the aapt
+ * tool. This integer encodes the package, type, and resource
+ * entry. The value 0 is an invalid identifier.
+ *
+ * @throws NotFoundException Throws NotFoundException if the given ID does not exist.
+ *
+ * @return Returns a single color value in the form 0xAARRGGBB.
+ */
+ public int getColor(int id) throws NotFoundException {
+ synchronized (mTmpValue) {
+ TypedValue value = mTmpValue;
+ getValue(id, value, true);
+ if (value.type >= TypedValue.TYPE_FIRST_INT
+ && value.type <= TypedValue.TYPE_LAST_INT) {
+ return value.data;
+ } else if (value.type == TypedValue.TYPE_STRING) {
+ ColorStateList csl = loadColorStateList(mTmpValue, id);
+ return csl.getDefaultColor();
+ }
+ throw new NotFoundException(
+ "Resource ID #0x" + Integer.toHexString(id) + " type #0x"
+ + Integer.toHexString(value.type) + " is not valid");
+ }
+ }
+
+ /**
+ * Return a color state list associated with a particular resource ID. The
+ * resource may contain either a single raw color value, or a complex
+ * {@link android.content.res.ColorStateList} holding multiple possible colors.
+ *
+ * @param id The desired resource identifier of a {@link ColorStateList},
+ * as generated by the aapt tool. This integer encodes the package, type, and resource
+ * entry. The value 0 is an invalid identifier.
+ *
+ * @throws NotFoundException Throws NotFoundException if the given ID does not exist.
+ *
+ * @return Returns a ColorStateList object containing either a single
+ * solid color or multiple colors that can be selected based on a state.
+ */
+ public ColorStateList getColorStateList(int id) throws NotFoundException {
+ synchronized (mTmpValue) {
+ TypedValue value = mTmpValue;
+ getValue(id, value, true);
+ return loadColorStateList(value, id);
+ }
+ }
+
+ /**
+ * Return an integer associated with a particular resource ID.
+ *
+ * @param id The desired resource identifier, as generated by the aapt
+ * tool. This integer encodes the package, type, and resource
+ * entry. The value 0 is an invalid identifier.
+ *
+ * @throws NotFoundException Throws NotFoundException if the given ID does not exist.
+ *
+ * @return Returns the integer value contained in the resource.
+ */
+ public int getInteger(int id) throws NotFoundException {
+ synchronized (mTmpValue) {
+ TypedValue value = mTmpValue;
+ getValue(id, value, true);
+ if (value.type >= TypedValue.TYPE_FIRST_INT
+ && value.type <= TypedValue.TYPE_LAST_INT) {
+ return value.data;
+ }
+ throw new NotFoundException(
+ "Resource ID #0x" + Integer.toHexString(id) + " type #0x"
+ + Integer.toHexString(value.type) + " is not valid");
+ }
+ }
+
+ /**
+ * Return an XmlResourceParser through which you can read a view layout
+ * description for the given resource ID. This parser has limited
+ * functionality -- in particular, you can't change its input, and only
+ * the high-level events are available.
+ *
+ * <p>This function is really a simple wrapper for calling
+ * {@link #getXml} with a layout resource.
+ *
+ * @param id The desired resource identifier, as generated by the aapt
+ * tool. This integer encodes the package, type, and resource
+ * entry. The value 0 is an invalid identifier.
+ *
+ * @throws NotFoundException Throws NotFoundException if the given ID does not exist.
+ *
+ * @return A new parser object through which you can read
+ * the XML data.
+ *
+ * @see #getXml
+ */
+ public XmlResourceParser getLayout(int id) throws NotFoundException {
+ return loadXmlResourceParser(id, "layout");
+ }
+
+ /**
+ * Return an XmlResourceParser through which you can read an animation
+ * description for the given resource ID. This parser has limited
+ * functionality -- in particular, you can't change its input, and only
+ * the high-level events are available.
+ *
+ * <p>This function is really a simple wrapper for calling
+ * {@link #getXml} with an animation resource.
+ *
+ * @param id The desired resource identifier, as generated by the aapt
+ * tool. This integer encodes the package, type, and resource
+ * entry. The value 0 is an invalid identifier.
+ *
+ * @throws NotFoundException Throws NotFoundException if the given ID does not exist.
+ *
+ * @return A new parser object through which you can read
+ * the XML data.
+ *
+ * @see #getXml
+ */
+ public XmlResourceParser getAnimation(int id) throws NotFoundException {
+ return loadXmlResourceParser(id, "anim");
+ }
+
+ /**
+ * Return an XmlResourceParser through which you can read a generic XML
+ * resource for the given resource ID.
+ *
+ * <p>The XmlPullParser implementation returned here has some limited
+ * functionality. In particular, you can't change its input, and only
+ * high-level parsing events are available (since the document was
+ * pre-parsed for you at build time, which involved merging text and
+ * stripping comments).
+ *
+ * @param id The desired resource identifier, as generated by the aapt
+ * tool. This integer encodes the package, type, and resource
+ * entry. The value 0 is an invalid identifier.
+ *
+ * @throws NotFoundException Throws NotFoundException if the given ID does not exist.
+ *
+ * @return A new parser object through which you can read
+ * the XML data.
+ *
+ * @see android.util.AttributeSet
+ */
+ public XmlResourceParser getXml(int id) throws NotFoundException {
+ return loadXmlResourceParser(id, "xml");
+ }
+
+ /**
+ * Open a data stream for reading a raw resource. This can only be used
+ * with resources whose value is the name of an asset files -- that is, it can be
+ * used to open drawable, sound, and raw resources; it will fail on string
+ * and color resources.
+ *
+ * @param id The resource identifier to open, as generated by the appt
+ * tool.
+ *
+ * @return InputStream Access to the resource data.
+ *
+ * @throws NotFoundException Throws NotFoundException if the given ID does not exist.
+ *
+ */
+ public InputStream openRawResource(int id) throws NotFoundException {
+ synchronized (mTmpValue) {
+ TypedValue value = mTmpValue;
+ getValue(id, value, true);
+
+ try {
+ return mAssets.openNonAsset(
+ value.assetCookie, value.string.toString(),
+ AssetManager.ACCESS_STREAMING);
+ } catch (Exception e) {
+ NotFoundException rnf = new NotFoundException(
+ "File " + value.string.toString()
+ + " from drawable resource ID #0x"
+ + Integer.toHexString(id));
+ rnf.initCause(e);
+ throw rnf;
+ }
+
+ }
+ }
+
+ /**
+ * Open a file descriptor for reading a raw resource. This can only be used
+ * with resources whose value is the name of an asset files -- that is, it can be
+ * used to open drawable, sound, and raw resources; it will fail on string
+ * and color resources.
+ *
+ * <p>This function only works for resources that are stored in the package
+ * as uncompressed data, which typically includes things like mp3 files
+ * and png images.
+ *
+ * @param id The resource identifier to open, as generated by the appt
+ * tool.
+ *
+ * @return AssetFileDescriptor A new file descriptor you can use to read
+ * the resource. This includes the file descriptor itself, as well as the
+ * offset and length of data where the resource appears in the file. A
+ * null is returned if the file exists but is compressed.
+ *
+ * @throws NotFoundException Throws NotFoundException if the given ID does not exist.
+ *
+ */
+ public AssetFileDescriptor openRawResourceFd(int id) throws NotFoundException {
+ synchronized (mTmpValue) {
+ TypedValue value = mTmpValue;
+ getValue(id, value, true);
+
+ try {
+ return mAssets.openNonAssetFd(
+ value.assetCookie, value.string.toString());
+ } catch (Exception e) {
+ NotFoundException rnf = new NotFoundException(
+ "File " + value.string.toString()
+ + " from drawable resource ID #0x"
+ + Integer.toHexString(id));
+ rnf.initCause(e);
+ throw rnf;
+ }
+
+ }
+ }
+
+ /**
+ * Return the raw data associated with a particular resource ID.
+ *
+ * @param id The desired resource identifier, as generated by the aapt
+ * tool. This integer encodes the package, type, and resource
+ * entry. The value 0 is an invalid identifier.
+ * @param outValue Object in which to place the resource data.
+ * @param resolveRefs If true, a resource that is a reference to another
+ * resource will be followed so that you receive the
+ * actual final resource data. If false, the TypedValue
+ * will be filled in with the reference itself.
+ *
+ * @throws NotFoundException Throws NotFoundException if the given ID does not exist.
+ *
+ */
+ public void getValue(int id, TypedValue outValue, boolean resolveRefs)
+ throws NotFoundException {
+ boolean found = mAssets.getResourceValue(id, outValue, resolveRefs);
+ if (found) {
+ return;
+ }
+ throw new NotFoundException("Resource ID #0x"
+ + Integer.toHexString(id));
+ }
+
+ /**
+ * Return the raw data associated with a particular resource ID.
+ * See getIdentifier() for information on how names are mapped to resource
+ * IDs, and getString(int) for information on how string resources are
+ * retrieved.
+ *
+ * <p>Note: use of this function is discouraged. It is much more
+ * efficient to retrieve resources by identifier than by name.
+ *
+ * @param name The name of the desired resource. This is passed to
+ * getIdentifier() with a default type of "string".
+ * @param outValue Object in which to place the resource data.
+ * @param resolveRefs If true, a resource that is a reference to another
+ * resource will be followed so that you receive the
+ * actual final resource data. If false, the TypedValue
+ * will be filled in with the reference itself.
+ *
+ * @throws NotFoundException Throws NotFoundException if the given ID does not exist.
+ *
+ */
+ public void getValue(String name, TypedValue outValue, boolean resolveRefs)
+ throws NotFoundException {
+ int id = getIdentifier(name, "string", null);
+ if (id != 0) {
+ getValue(id, outValue, resolveRefs);
+ return;
+ }
+ throw new NotFoundException("String resource name " + name);
+ }
+
+ /**
+ * This class holds the current attribute values for a particular theme.
+ * In other words, a Theme is a set of values for resource attributes;
+ * these are used in conjunction with {@link TypedArray}
+ * to resolve the final value for an attribute.
+ *
+ * <p>The Theme's attributes come into play in two ways: (1) a styled
+ * attribute can explicit reference a value in the theme through the
+ * "?themeAttribute" syntax; (2) if no value has been defined for a
+ * particular styled attribute, as a last resort we will try to find that
+ * attribute's value in the Theme.
+ *
+ * <p>You will normally use the {@link #obtainStyledAttributes} APIs to
+ * retrieve XML attributes with style and theme information applied.
+ */
+ public final class Theme {
+ /**
+ * Place new attribute values into the theme. The style resource
+ * specified by <var>resid</var> will be retrieved from this Theme's
+ * resources, its values placed into the Theme object.
+ *
+ * <p>The semantics of this function depends on the <var>force</var>
+ * argument: If false, only values that are not already defined in
+ * the theme will be copied from the system resource; otherwise, if
+ * any of the style's attributes are already defined in the theme, the
+ * current values in the theme will be overwritten.
+ *
+ * @param resid The resource ID of a style resource from which to
+ * obtain attribute values.
+ * @param force If true, values in the style resource will always be
+ * used in the theme; otherwise, they will only be used
+ * if not already defined in the theme.
+ */
+ public void applyStyle(int resid, boolean force) {
+ AssetManager.applyThemeStyle(mTheme, resid, force);
+ }
+
+ /**
+ * Set this theme to hold the same contents as the theme
+ * <var>other</var>. If both of these themes are from the same
+ * Resources object, they will be identical after this function
+ * returns. If they are from different Resources, only the resources
+ * they have in common will be set in this theme.
+ *
+ * @param other The existing Theme to copy from.
+ */
+ public void setTo(Theme other) {
+ AssetManager.copyTheme(mTheme, other.mTheme);
+ }
+
+ /**
+ * Return a StyledAttributes holding the values defined by
+ * <var>Theme</var> which are listed in <var>attrs</var>.
+ *
+ * <p>Be sure to call StyledAttributes.recycle() when you are done with
+ * the array.
+ *
+ * @param attrs The desired attributes.
+ *
+ * @throws NotFoundException Throws NotFoundException if the given ID does not exist.
+ *
+ * @return Returns a TypedArray holding an array of the attribute values.
+ * Be sure to call {@link TypedArray#recycle() TypedArray.recycle()}
+ * when done with it.
+ *
+ * @see Resources#obtainAttributes
+ * @see #obtainStyledAttributes(int, int[])
+ * @see #obtainStyledAttributes(AttributeSet, int[], int, int)
+ */
+ public TypedArray obtainStyledAttributes(int[] attrs) {
+ int len = attrs.length;
+ TypedArray array = getCachedStyledAttributes(len);
+ array.mRsrcs = attrs;
+ AssetManager.applyStyle(mTheme, 0, 0, 0, attrs,
+ array.mData, array.mIndices);
+ return array;
+ }
+
+ /**
+ * Return a StyledAttributes holding the values defined by the style
+ * resource <var>resid</var> which are listed in <var>attrs</var>.
+ *
+ * <p>Be sure to call StyledAttributes.recycle() when you are done with
+ * the array.
+ *
+ * @param resid The desired style resource.
+ * @param attrs The desired attributes in the style.
+ *
+ * @throws NotFoundException Throws NotFoundException if the given ID does not exist.
+ *
+ * @return Returns a TypedArray holding an array of the attribute values.
+ * Be sure to call {@link TypedArray#recycle() TypedArray.recycle()}
+ * when done with it.
+ *
+ * @see Resources#obtainAttributes
+ * @see #obtainStyledAttributes(int[])
+ * @see #obtainStyledAttributes(AttributeSet, int[], int, int)
+ */
+ public TypedArray obtainStyledAttributes(int resid, int[] attrs)
+ throws NotFoundException {
+ int len = attrs.length;
+ TypedArray array = getCachedStyledAttributes(len);
+ array.mRsrcs = attrs;
+
+ AssetManager.applyStyle(mTheme, 0, resid, 0, attrs,
+ array.mData, array.mIndices);
+ if (false) {
+ int[] data = array.mData;
+
+ System.out.println("**********************************************************");
+ System.out.println("**********************************************************");
+ System.out.println("**********************************************************");
+ System.out.println("Attributes:");
+ String s = " Attrs:";
+ int i;
+ for (i=0; i<attrs.length; i++) {
+ s = s + " 0x" + Integer.toHexString(attrs[i]);
+ }
+ System.out.println(s);
+ s = " Found:";
+ TypedValue value = new TypedValue();
+ for (i=0; i<attrs.length; i++) {
+ int d = i*AssetManager.STYLE_NUM_ENTRIES;
+ value.type = data[d+AssetManager.STYLE_TYPE];
+ value.data = data[d+AssetManager.STYLE_DATA];
+ value.assetCookie = data[d+AssetManager.STYLE_ASSET_COOKIE];
+ value.resourceId = data[d+AssetManager.STYLE_RESOURCE_ID];
+ s = s + " 0x" + Integer.toHexString(attrs[i])
+ + "=" + value;
+ }
+ System.out.println(s);
+ }
+ return array;
+ }
+
+ /**
+ * Return a StyledAttributes holding the attribute values in
+ * <var>set</var>
+ * that are listed in <var>attrs</var>. In addition, if the given
+ * AttributeSet specifies a style class (through the "style" attribute),
+ * that style will be applied on top of the base attributes it defines.
+ *
+ * <p>Be sure to call StyledAttributes.recycle() when you are done with
+ * the array.
+ *
+ * <p>When determining the final value of a particular attribute, there
+ * are four inputs that come into play:</p>
+ *
+ * <ol>
+ * <li> Any attribute values in the given AttributeSet.
+ * <li> The style resource specified in the AttributeSet (named
+ * "style").
+ * <li> The default style specified by <var>defStyleAttr</var> and
+ * <var>defStyleRes</var>
+ * <li> The base values in this theme.
+ * </ol>
+ *
+ * <p>Each of these inputs is considered in-order, with the first listed
+ * taking precedence over the following ones. In other words, if in the
+ * AttributeSet you have supplied <code>&lt;Button
+ * textColor="#ff000000"&gt;</code>, then the button's text will
+ * <em>always</em> be black, regardless of what is specified in any of
+ * the styles.
+ *
+ * @param set The base set of attribute values. May be null.
+ * @param attrs The desired attributes to be retrieved.
+ * @param defStyleAttr An attribute in the current theme that contains a
+ * reference to a style resource that supplies
+ * defaults values for the StyledAttributes. Can be
+ * 0 to not look for defaults.
+ * @param defStyleRes A resource identifier of a style resource that
+ * supplies default values for the StyledAttributes,
+ * used only if defStyleAttr is 0 or can not be found
+ * in the theme. Can be 0 to not look for defaults.
+ *
+ * @return Returns a TypedArray holding an array of the attribute values.
+ * Be sure to call {@link TypedArray#recycle() TypedArray.recycle()}
+ * when done with it.
+ *
+ * @see Resources#obtainAttributes
+ * @see #obtainStyledAttributes(int[])
+ * @see #obtainStyledAttributes(int, int[])
+ */
+ public TypedArray obtainStyledAttributes(AttributeSet set,
+ int[] attrs, int defStyleAttr, int defStyleRes) {
+ int len = attrs.length;
+ TypedArray array = getCachedStyledAttributes(len);
+
+ // XXX note that for now we only work with compiled XML files.
+ // To support generic XML files we will need to manually parse
+ // out the attributes from the XML file (applying type information
+ // contained in the resources and such).
+ XmlBlock.Parser parser = (XmlBlock.Parser)set;
+ AssetManager.applyStyle(
+ mTheme, defStyleAttr, defStyleRes,
+ parser != null ? parser.mParseState : 0, attrs,
+ array.mData, array.mIndices);
+
+ array.mRsrcs = attrs;
+ array.mXml = parser;
+
+ if (false) {
+ int[] data = array.mData;
+
+ System.out.println("Attributes:");
+ String s = " Attrs:";
+ int i;
+ for (i=0; i<set.getAttributeCount(); i++) {
+ s = s + " " + set.getAttributeName(i);
+ int id = set.getAttributeNameResource(i);
+ if (id != 0) {
+ s = s + "(0x" + Integer.toHexString(id) + ")";
+ }
+ s = s + "=" + set.getAttributeValue(i);
+ }
+ System.out.println(s);
+ s = " Found:";
+ TypedValue value = new TypedValue();
+ for (i=0; i<attrs.length; i++) {
+ int d = i*AssetManager.STYLE_NUM_ENTRIES;
+ value.type = data[d+AssetManager.STYLE_TYPE];
+ value.data = data[d+AssetManager.STYLE_DATA];
+ value.assetCookie = data[d+AssetManager.STYLE_ASSET_COOKIE];
+ value.resourceId = data[d+AssetManager.STYLE_RESOURCE_ID];
+ s = s + " 0x" + Integer.toHexString(attrs[i])
+ + "=" + value;
+ }
+ System.out.println(s);
+ }
+
+ return array;
+ }
+
+ /**
+ * Retrieve the value of an attribute in the Theme. The contents of
+ * <var>outValue</var> are ultimately filled in by
+ * {@link Resources#getValue}.
+ *
+ * @param resid The resource identifier of the desired theme
+ * attribute.
+ * @param outValue Filled in with the ultimate resource value supplied
+ * by the attribute.
+ * @param resolveRefs If true, resource references will be walked; if
+ * false, <var>outValue</var> may be a
+ * TYPE_REFERENCE. In either case, it will never
+ * be a TYPE_ATTRIBUTE.
+ *
+ * @return boolean Returns true if the attribute was found and
+ * <var>outValue</var> is valid, else false.
+ */
+ public boolean resolveAttribute(int resid, TypedValue outValue,
+ boolean resolveRefs) {
+ boolean got = mAssets.getThemeValue(mTheme, resid, outValue, resolveRefs);
+ if (false) {
+ System.out.println(
+ "resolveAttribute #" + Integer.toHexString(resid)
+ + " got=" + got + ", type=0x" + Integer.toHexString(outValue.type)
+ + ", data=0x" + Integer.toHexString(outValue.data));
+ }
+ return got;
+ }
+
+ /**
+ * Print contents of this theme out to the log. For debugging only.
+ *
+ * @param priority The log priority to use.
+ * @param tag The log tag to use.
+ * @param prefix Text to prefix each line printed.
+ */
+ public void dump(int priority, String tag, String prefix) {
+ AssetManager.dumpTheme(mTheme, priority, tag, prefix);
+ }
+
+ protected void finalize() throws Throwable {
+ super.finalize();
+ mAssets.releaseTheme(mTheme);
+ }
+
+ /*package*/ Theme() {
+ mAssets = Resources.this.mAssets;
+ mTheme = mAssets.createTheme();
+ }
+
+ private final AssetManager mAssets;
+ private final int mTheme;
+ }
+
+ /**
+ * Generate a new Theme object for this set of Resources. It initially
+ * starts out empty.
+ *
+ * @return Theme The newly created Theme container.
+ */
+ public final Theme newTheme() {
+ return new Theme();
+ }
+
+ /**
+ * Retrieve a set of basic attribute values from an AttributeSet, not
+ * performing styling of them using a theme and/or style resources.
+ *
+ * @param set The current attribute values to retrieve.
+ * @param attrs The specific attributes to be retrieved.
+ * @return Returns a TypedArray holding an array of the attribute values.
+ * Be sure to call {@link TypedArray#recycle() TypedArray.recycle()}
+ * when done with it.
+ *
+ * @see Theme#obtainStyledAttributes(AttributeSet, int[], int, int)
+ */
+ public TypedArray obtainAttributes(AttributeSet set, int[] attrs) {
+ int len = attrs.length;
+ TypedArray array = getCachedStyledAttributes(len);
+
+ // XXX note that for now we only work with compiled XML files.
+ // To support generic XML files we will need to manually parse
+ // out the attributes from the XML file (applying type information
+ // contained in the resources and such).
+ XmlBlock.Parser parser = (XmlBlock.Parser)set;
+ mAssets.retrieveAttributes(parser.mParseState, attrs,
+ array.mData, array.mIndices);
+
+ array.mRsrcs = attrs;
+ array.mXml = parser;
+
+ return array;
+ }
+
+ /**
+ * Store the newly updated configuration.
+ */
+ public void updateConfiguration(Configuration config,
+ DisplayMetrics metrics) {
+ synchronized (mTmpValue) {
+ int configChanges = 0xfffffff;
+ if (config != null) {
+ configChanges = mConfiguration.updateFrom(config);
+ }
+ if (metrics != null) {
+ mMetrics.setTo(metrics);
+ }
+ mMetrics.scaledDensity = mMetrics.density * mConfiguration.fontScale;
+ String locale = null;
+ if (mConfiguration.locale != null) {
+ locale = mConfiguration.locale.getLanguage();
+ if (mConfiguration.locale.getCountry() != null) {
+ locale += "-" + mConfiguration.locale.getCountry();
+ }
+ }
+ int width, height;
+ if (mMetrics.widthPixels >= mMetrics.heightPixels) {
+ width = mMetrics.widthPixels;
+ height = mMetrics.heightPixels;
+ } else {
+ width = mMetrics.heightPixels;
+ height = mMetrics.widthPixels;
+ }
+ mAssets.setConfiguration(mConfiguration.mcc, mConfiguration.mnc,
+ locale, mConfiguration.orientation,
+ mConfiguration.touchscreen,
+ (int)(mMetrics.density*160), mConfiguration.keyboard,
+ mConfiguration.keyboardHidden,
+ mConfiguration.navigation, width, height, sSdkVersion);
+ int N = mDrawableCache.size();
+ if (DEBUG_CONFIG) {
+ Log.d(TAG, "Cleaning up drawables config changes: 0x"
+ + Integer.toHexString(configChanges));
+ }
+ for (int i=0; i<N; i++) {
+ WeakReference<Drawable.ConstantState> ref = mDrawableCache.valueAt(i);
+ if (ref != null) {
+ Drawable.ConstantState cs = ref.get();
+ if (cs != null) {
+ if (Configuration.needNewResources(
+ configChanges, cs.getChangingConfigurations())) {
+ if (DEBUG_CONFIG) {
+ Log.d(TAG, "FLUSHING #0x"
+ + Integer.toHexString(mDrawableCache.keyAt(i))
+ + " / " + cs + " with changes: 0x"
+ + Integer.toHexString(cs.getChangingConfigurations()));
+ }
+ mDrawableCache.setValueAt(i, null);
+ } else if (DEBUG_CONFIG) {
+ Log.d(TAG, "(Keeping #0x"
+ + Integer.toHexString(mDrawableCache.keyAt(i))
+ + " / " + cs + " with changes: 0x"
+ + Integer.toHexString(cs.getChangingConfigurations())
+ + ")");
+ }
+ }
+ }
+ }
+ mDrawableCache.clear();
+ mColorStateListCache.clear();
+ flushLayoutCache();
+ }
+ synchronized (mSync) {
+ if (mPluralRule != null) {
+ mPluralRule = PluralRule.ruleForLocale(config.locale);
+ }
+ }
+ }
+
+ /**
+ * Return the current display metrics that are in effect for this resource
+ * object. The returned object should be treated as read-only.
+ *
+ * @return The resource's current display metrics.
+ */
+ public DisplayMetrics getDisplayMetrics() {
+ return mMetrics;
+ }
+
+ /**
+ * Return the current configuration that is in effect for this resource
+ * object. The returned object should be treated as read-only.
+ *
+ * @return The resource's current configuration.
+ */
+ public Configuration getConfiguration() {
+ return mConfiguration;
+ }
+
+ /**
+ * Return a resource identifier for the given resource name. A fully
+ * qualified resource name is of the form "package:type/entry". The first
+ * two components (package and type) are optional if defType and
+ * defPackage, respectively, are specified here.
+ *
+ * <p>Note: use of this function is discouraged. It is much more
+ * efficient to retrieve resources by identifier than by name.
+ *
+ * @param name The name of the desired resource.
+ * @param defType Optional default resource type to find, if "type/" is
+ * not included in the name. Can be null to require an
+ * explicit type.
+ * @param defPackage Optional default package to find, if "package:" is
+ * not included in the name. Can be null to require an
+ * explicit package.
+ *
+ * @return int The associated resource identifier. Returns 0 if no such
+ * resource was found. (0 is not a valid resource ID.)
+ */
+ public int getIdentifier(String name, String defType, String defPackage) {
+ try {
+ return Integer.parseInt(name);
+ } catch (Exception e) {
+ }
+ return mAssets.getResourceIdentifier(name, defType, defPackage);
+ }
+
+ /**
+ * Return the full name for a given resource identifier. This name is
+ * a single string of the form "package:type/entry".
+ *
+ * @param resid The resource identifier whose name is to be retrieved.
+ *
+ * @return A string holding the name of the resource.
+ *
+ * @throws NotFoundException Throws NotFoundException if the given ID does not exist.
+ *
+ * @see #getResourcePackageName
+ * @see #getResourceTypeName
+ * @see #getResourceEntryName
+ */
+ public String getResourceName(int resid) throws NotFoundException {
+ String str = mAssets.getResourceName(resid);
+ if (str != null) return str;
+ throw new NotFoundException("Unable to find resource ID #0x"
+ + Integer.toHexString(resid));
+ }
+
+ /**
+ * Return the package name for a given resource identifier.
+ *
+ * @param resid The resource identifier whose package name is to be
+ * retrieved.
+ *
+ * @return A string holding the package name of the resource.
+ *
+ * @throws NotFoundException Throws NotFoundException if the given ID does not exist.
+ *
+ * @see #getResourceName
+ */
+ public String getResourcePackageName(int resid) throws NotFoundException {
+ String str = mAssets.getResourcePackageName(resid);
+ if (str != null) return str;
+ throw new NotFoundException("Unable to find resource ID #0x"
+ + Integer.toHexString(resid));
+ }
+
+ /**
+ * Return the type name for a given resource identifier.
+ *
+ * @param resid The resource identifier whose type name is to be
+ * retrieved.
+ *
+ * @return A string holding the type name of the resource.
+ *
+ * @throws NotFoundException Throws NotFoundException if the given ID does not exist.
+ *
+ * @see #getResourceName
+ */
+ public String getResourceTypeName(int resid) throws NotFoundException {
+ String str = mAssets.getResourceTypeName(resid);
+ if (str != null) return str;
+ throw new NotFoundException("Unable to find resource ID #0x"
+ + Integer.toHexString(resid));
+ }
+
+ /**
+ * Return the entry name for a given resource identifier.
+ *
+ * @param resid The resource identifier whose entry name is to be
+ * retrieved.
+ *
+ * @return A string holding the entry name of the resource.
+ *
+ * @throws NotFoundException Throws NotFoundException if the given ID does not exist.
+ *
+ * @see #getResourceName
+ */
+ public String getResourceEntryName(int resid) throws NotFoundException {
+ String str = mAssets.getResourceEntryName(resid);
+ if (str != null) return str;
+ throw new NotFoundException("Unable to find resource ID #0x"
+ + Integer.toHexString(resid));
+ }
+
+ /**
+ * Retrieve underlying AssetManager storage for these resources.
+ */
+ public final AssetManager getAssets() {
+ return mAssets;
+ }
+
+ /**
+ * Call this to remove all cached loaded layout resources from the
+ * Resources object. Only intended for use with performance testing
+ * tools.
+ */
+ public final void flushLayoutCache() {
+ synchronized (mCachedXmlBlockIds) {
+ // First see if this block is in our cache.
+ final int num = mCachedXmlBlockIds.length;
+ for (int i=0; i<num; i++) {
+ mCachedXmlBlockIds[i] = -0;
+ XmlBlock oldBlock = mCachedXmlBlocks[i];
+ if (oldBlock != null) {
+ oldBlock.close();
+ }
+ mCachedXmlBlocks[i] = null;
+ }
+ }
+ }
+
+ /**
+ * Start preloading of resource data using this Resources object. Only
+ * for use by the zygote process for loading common system resources.
+ * {@hide}
+ */
+ public final void startPreloading() {
+ synchronized (mSync) {
+ if (mPreloaded) {
+ throw new IllegalStateException("Resources already preloaded");
+ }
+ mPreloaded = true;
+ mPreloading = true;
+ }
+ }
+
+ /**
+ * Called by zygote when it is done preloading resources, to change back
+ * to normal Resources operation.
+ */
+ public final void finishPreloading() {
+ if (mPreloading) {
+ mPreloading = false;
+ flushLayoutCache();
+ }
+ }
+
+ /*package*/ Drawable loadDrawable(TypedValue value, int id)
+ throws NotFoundException {
+ if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT
+ && value.type <= TypedValue.TYPE_LAST_COLOR_INT) {
+ // Should we be caching these? If we use constant colors much
+ // at all, most likely...
+ //System.out.println("Creating drawable for color: #" +
+ // Integer.toHexString(value.data));
+ Drawable dr = new ColorDrawable(value.data);
+ dr.setChangingConfigurations(value.changingConfigurations);
+ return dr;
+ }
+
+ final int key = (value.assetCookie<<24)|value.data;
+ Drawable dr = getCachedDrawable(key);
+ //System.out.println("Cached drawable @ #" +
+ // Integer.toHexString(key.intValue()) + ": " + dr);
+ if (dr != null) {
+ return dr;
+ }
+
+ Drawable.ConstantState cs = mPreloadedDrawables.get(key);
+ if (cs != null) {
+ dr = cs.newDrawable();
+
+ } else {
+ if (value.string == null) {
+ throw new NotFoundException(
+ "Resource is not a Drawable (color or path): " + value);
+ }
+
+ String file = value.string.toString();
+
+ if (DEBUG_LOAD) Log.v(TAG, "Loading drawable for cookie "
+ + value.assetCookie + ": " + file);
+
+ if (file.endsWith(".xml")) {
+ try {
+ XmlResourceParser rp = loadXmlResourceParser(
+ file, id, value.assetCookie, "drawable");
+ dr = Drawable.createFromXml(this, rp);
+ rp.close();
+ } catch (Exception e) {
+ NotFoundException rnf = new NotFoundException(
+ "File " + file + " from drawable resource ID #0x"
+ + Integer.toHexString(id));
+ rnf.initCause(e);
+ throw rnf;
+ }
+
+ } else {
+ try {
+ InputStream is = mAssets.openNonAsset(
+ value.assetCookie, file, AssetManager.ACCESS_BUFFER);
+ // System.out.println("Opened file " + file + ": " + is);
+ dr = Drawable.createFromStream(is, file);
+ is.close();
+ // System.out.println("Created stream: " + dr);
+ } catch (Exception e) {
+ NotFoundException rnf = new NotFoundException(
+ "File " + file + " from drawable resource ID #0x"
+ + Integer.toHexString(id));
+ rnf.initCause(e);
+ throw rnf;
+ }
+ }
+ }
+
+ if (dr != null) {
+ dr.setChangingConfigurations(value.changingConfigurations);
+ cs = dr.getConstantState();
+ if (cs != null) {
+ if (mPreloading) {
+ mPreloadedDrawables.put(key, cs);
+ }
+ synchronized (mTmpValue) {
+ //Log.i(TAG, "Saving cached drawable @ #" +
+ // Integer.toHexString(key.intValue())
+ // + " in " + this + ": " + cs);
+ mDrawableCache.put(
+ key, new WeakReference<Drawable.ConstantState>(cs));
+ }
+ }
+ }
+
+ return dr;
+ }
+
+ private final Drawable getCachedDrawable(int key) {
+ synchronized (mTmpValue) {
+ WeakReference<Drawable.ConstantState> wr = mDrawableCache.get(key);
+ if (wr != null) { // we have the key
+ Drawable.ConstantState entry = wr.get();
+ if (entry != null) {
+ //Log.i(TAG, "Returning cached drawable @ #" +
+ // Integer.toHexString(((Integer)key).intValue())
+ // + " in " + this + ": " + entry);
+ return entry.newDrawable();
+ }
+ else { // our entry has been purged
+ mDrawableCache.delete(key);
+ }
+ }
+ }
+ return null;
+ }
+
+ /*package*/ ColorStateList loadColorStateList(TypedValue value, int id)
+ throws NotFoundException {
+ if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT
+ && value.type <= TypedValue.TYPE_LAST_COLOR_INT) {
+ return ColorStateList.valueOf(value.data);
+ }
+
+ final int key = (value.assetCookie<<24)|value.data;
+ ColorStateList csl = getCachedColorStateList(key);
+ if (csl != null) {
+ return csl;
+ }
+
+ if (value.string == null) {
+ throw new NotFoundException(
+ "Resource is not a ColorStateList (color or path): " + value);
+ }
+
+ String file = value.string.toString();
+
+ if (file.endsWith(".xml")) {
+ try {
+ XmlResourceParser rp = loadXmlResourceParser(
+ file, id, value.assetCookie, "colorstatelist");
+ csl = ColorStateList.createFromXml(this, rp);
+ rp.close();
+ } catch (Exception e) {
+ NotFoundException rnf = new NotFoundException(
+ "File " + file + " from color state list resource ID #0x"
+ + Integer.toHexString(id));
+ rnf.initCause(e);
+ throw rnf;
+ }
+ } else {
+ throw new NotFoundException(
+ "File " + file + " from drawable resource ID #0x"
+ + Integer.toHexString(id) + ": .xml extension required");
+ }
+
+ if (csl != null) {
+ synchronized (mTmpValue) {
+ //Log.i(TAG, "Saving cached color state list @ #" +
+ // Integer.toHexString(key.intValue())
+ // + " in " + this + ": " + csl);
+ mColorStateListCache.put(
+ key, new WeakReference<ColorStateList>(csl));
+ }
+ }
+
+ return csl;
+ }
+
+ private ColorStateList getCachedColorStateList(int key) {
+ synchronized (mTmpValue) {
+ WeakReference<ColorStateList> wr = mColorStateListCache.get(key);
+ if (wr != null) { // we have the key
+ ColorStateList entry = wr.get();
+ if (entry != null) {
+ //Log.i(TAG, "Returning cached color state list @ #" +
+ // Integer.toHexString(((Integer)key).intValue())
+ // + " in " + this + ": " + entry);
+ return entry;
+ }
+ else { // our entry has been purged
+ mColorStateListCache.delete(key);
+ }
+ }
+ }
+ return null;
+ }
+
+ /*package*/ XmlResourceParser loadXmlResourceParser(int id, String type)
+ throws NotFoundException {
+ synchronized (mTmpValue) {
+ TypedValue value = mTmpValue;
+ getValue(id, value, true);
+ if (value.type == TypedValue.TYPE_STRING) {
+ return loadXmlResourceParser(value.string.toString(), id,
+ value.assetCookie, type);
+ }
+ throw new NotFoundException(
+ "Resource ID #0x" + Integer.toHexString(id) + " type #0x"
+ + Integer.toHexString(value.type) + " is not valid");
+ }
+ }
+
+ /*package*/ XmlResourceParser loadXmlResourceParser(String file, int id,
+ int assetCookie, String type) throws NotFoundException {
+ if (id != 0) {
+ try {
+ // These may be compiled...
+ synchronized (mCachedXmlBlockIds) {
+ // First see if this block is in our cache.
+ final int num = mCachedXmlBlockIds.length;
+ for (int i=0; i<num; i++) {
+ if (mCachedXmlBlockIds[i] == id) {
+ //System.out.println("**** REUSING XML BLOCK! id="
+ // + id + ", index=" + i);
+ return mCachedXmlBlocks[i].newParser();
+ }
+ }
+
+ // Not in the cache, create a new block and put it at
+ // the next slot in the cache.
+ XmlBlock block = mAssets.openXmlBlockAsset(
+ assetCookie, file);
+ if (block != null) {
+ int pos = mLastCachedXmlBlockIndex+1;
+ if (pos >= num) pos = 0;
+ mLastCachedXmlBlockIndex = pos;
+ XmlBlock oldBlock = mCachedXmlBlocks[pos];
+ if (oldBlock != null) {
+ oldBlock.close();
+ }
+ mCachedXmlBlockIds[pos] = id;
+ mCachedXmlBlocks[pos] = block;
+ //System.out.println("**** CACHING NEW XML BLOCK! id="
+ // + id + ", index=" + pos);
+ return block.newParser();
+ }
+ }
+ } catch (Exception e) {
+ NotFoundException rnf = new NotFoundException(
+ "File " + file + " from xml type " + type + " resource ID #0x"
+ + Integer.toHexString(id));
+ rnf.initCause(e);
+ throw rnf;
+ }
+ }
+
+ throw new NotFoundException(
+ "File " + file + " from xml type " + type + " resource ID #0x"
+ + Integer.toHexString(id));
+ }
+
+ private TypedArray getCachedStyledAttributes(int len) {
+ synchronized (mTmpValue) {
+ TypedArray attrs = mCachedStyledAttributes;
+ if (attrs != null) {
+ mCachedStyledAttributes = null;
+
+ attrs.mLength = len;
+ int fullLen = len * AssetManager.STYLE_NUM_ENTRIES;
+ if (attrs.mData.length >= fullLen) {
+ return attrs;
+ }
+ attrs.mData = new int[fullLen];
+ attrs.mIndices = new int[1+len];
+ return attrs;
+ }
+ return new TypedArray(this,
+ new int[len*AssetManager.STYLE_NUM_ENTRIES],
+ new int[1+len], len);
+ }
+ }
+
+ private Resources() {
+ mAssets = AssetManager.getSystem();
+ // NOTE: Intentionally leaving this uninitialized (all values set
+ // to zero), so that anyone who tries to do something that requires
+ // metrics will get a very wrong value.
+ mConfiguration.setToDefaults();
+ mMetrics.setToDefaults();
+ updateConfiguration(null, null);
+ mAssets.ensureStringBlocks();
+ }
+}
+
diff --git a/core/java/android/content/res/StringBlock.java b/core/java/android/content/res/StringBlock.java
new file mode 100644
index 0000000..da32cc8
--- /dev/null
+++ b/core/java/android/content/res/StringBlock.java
@@ -0,0 +1,330 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.res;
+
+import android.text.*;
+import android.text.style.*;
+import android.util.Config;
+import android.util.Log;
+import android.util.SparseArray;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import com.android.internal.util.XmlUtils;
+
+/**
+ * Conveniences for retrieving data out of a compiled string resource.
+ *
+ * {@hide}
+ */
+final class StringBlock {
+ private static final String TAG = "AssetManager";
+ private static final boolean localLOGV = Config.LOGV || false;
+
+ private final int mNative;
+ private final boolean mUseSparse;
+ private final boolean mOwnsNative;
+ private CharSequence[] mStrings;
+ private SparseArray<CharSequence> mSparseStrings;
+ StyleIDs mStyleIDs = null;
+
+ public StringBlock(byte[] data, boolean useSparse) {
+ mNative = nativeCreate(data, 0, data.length);
+ mUseSparse = useSparse;
+ mOwnsNative = true;
+ if (localLOGV) Log.v(TAG, "Created string block " + this
+ + ": " + nativeGetSize(mNative));
+ }
+
+ public StringBlock(byte[] data, int offset, int size, boolean useSparse) {
+ mNative = nativeCreate(data, offset, size);
+ mUseSparse = useSparse;
+ mOwnsNative = true;
+ if (localLOGV) Log.v(TAG, "Created string block " + this
+ + ": " + nativeGetSize(mNative));
+ }
+
+ public CharSequence get(int idx) {
+ synchronized (this) {
+ if (mStrings != null) {
+ CharSequence res = mStrings[idx];
+ if (res != null) {
+ return res;
+ }
+ } else if (mSparseStrings != null) {
+ CharSequence res = mSparseStrings.get(idx);
+ if (res != null) {
+ return res;
+ }
+ } else {
+ final int num = nativeGetSize(mNative);
+ if (mUseSparse && num > 250) {
+ mSparseStrings = new SparseArray<CharSequence>();
+ } else {
+ mStrings = new CharSequence[num];
+ }
+ }
+ String str = nativeGetString(mNative, idx);
+ CharSequence res = str;
+ int[] style = nativeGetStyle(mNative, idx);
+ if (localLOGV) Log.v(TAG, "Got string: " + str);
+ if (localLOGV) Log.v(TAG, "Got styles: " + style);
+ if (style != null) {
+ if (mStyleIDs == null) {
+ mStyleIDs = new StyleIDs();
+ mStyleIDs.boldId = nativeIndexOfString(mNative, "b");
+ mStyleIDs.italicId = nativeIndexOfString(mNative, "i");
+ mStyleIDs.underlineId = nativeIndexOfString(mNative, "u");
+ mStyleIDs.ttId = nativeIndexOfString(mNative, "tt");
+ mStyleIDs.bigId = nativeIndexOfString(mNative, "big");
+ mStyleIDs.smallId = nativeIndexOfString(mNative, "small");
+ mStyleIDs.supId = nativeIndexOfString(mNative, "sup");
+ mStyleIDs.subId = nativeIndexOfString(mNative, "sub");
+ mStyleIDs.strikeId = nativeIndexOfString(mNative, "strike");
+ mStyleIDs.listItemId = nativeIndexOfString(mNative, "li");
+
+ if (localLOGV) Log.v(TAG, "BoldId=" + mStyleIDs.boldId
+ + ", ItalicId=" + mStyleIDs.italicId
+ + ", UnderlineId=" + mStyleIDs.underlineId);
+ }
+
+ res = applyStyles(str, style, mStyleIDs);
+ }
+ if (mStrings != null) mStrings[idx] = res;
+ else mSparseStrings.put(idx, res);
+ return res;
+ }
+ }
+
+ protected void finalize() throws Throwable {
+ if (mOwnsNative) {
+ nativeDestroy(mNative);
+ }
+ }
+
+ static final class StyleIDs {
+ private int boldId;
+ private int italicId;
+ private int underlineId;
+ private int ttId;
+ private int bigId;
+ private int smallId;
+ private int subId;
+ private int supId;
+ private int strikeId;
+ private int listItemId;
+ }
+
+ private CharSequence applyStyles(String str, int[] style, StyleIDs ids) {
+ if (style.length == 0)
+ return str;
+
+ SpannableString buffer = new SpannableString(str);
+ int i=0;
+ while (i < style.length) {
+ int type = style[i];
+ if (localLOGV) Log.v(TAG, "Applying style span id=" + type
+ + ", start=" + style[i+1] + ", end=" + style[i+2]);
+ if (type == ids.boldId) {
+ buffer.setSpan(new StyleSpan(Typeface.BOLD),
+ style[i+1], style[i+2]+1,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ } else if (type == ids.italicId) {
+ buffer.setSpan(new StyleSpan(Typeface.ITALIC),
+ style[i+1], style[i+2]+1,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ } else if (type == ids.underlineId) {
+ buffer.setSpan(new UnderlineSpan(),
+ style[i+1], style[i+2]+1,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ } else if (type == ids.ttId) {
+ buffer.setSpan(new TypefaceSpan("monospace"),
+ style[i+1], style[i+2]+1,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ } else if (type == ids.bigId) {
+ buffer.setSpan(new RelativeSizeSpan(1.25f),
+ style[i+1], style[i+2]+1,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ } else if (type == ids.smallId) {
+ buffer.setSpan(new RelativeSizeSpan(0.8f),
+ style[i+1], style[i+2]+1,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ } else if (type == ids.subId) {
+ buffer.setSpan(new SubscriptSpan(),
+ style[i+1], style[i+2]+1,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ } else if (type == ids.supId) {
+ buffer.setSpan(new SuperscriptSpan(),
+ style[i+1], style[i+2]+1,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ } else if (type == ids.strikeId) {
+ buffer.setSpan(new StrikethroughSpan(),
+ style[i+1], style[i+2]+1,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ } else if (type == ids.listItemId) {
+ buffer.setSpan(new BulletSpan(10),
+ style[i+1], style[i+2]+1,
+ Spannable.SPAN_PARAGRAPH);
+ } else {
+ String tag = nativeGetString(mNative, type);
+
+ if (tag.startsWith("font;")) {
+ String sub;
+
+ sub = subtag(tag, ";height=");
+ if (sub != null) {
+ int size = Integer.parseInt(sub);
+ buffer.setSpan(new Height(size),
+ style[i+1], style[i+2]+1,
+ Spannable.SPAN_PARAGRAPH);
+ }
+
+ sub = subtag(tag, ";size=");
+ if (sub != null) {
+ int size = Integer.parseInt(sub);
+ buffer.setSpan(new AbsoluteSizeSpan(size),
+ style[i+1], style[i+2]+1,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
+ sub = subtag(tag, ";fgcolor=");
+ if (sub != null) {
+ int color = XmlUtils.convertValueToUnsignedInt(sub, -1);
+ buffer.setSpan(new ForegroundColorSpan(color),
+ style[i+1], style[i+2]+1,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
+ sub = subtag(tag, ";bgcolor=");
+ if (sub != null) {
+ int color = XmlUtils.convertValueToUnsignedInt(sub, -1);
+ buffer.setSpan(new BackgroundColorSpan(color),
+ style[i+1], style[i+2]+1,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+ }
+
+ i += 3;
+ }
+ return new SpannedString(buffer);
+ }
+
+ private static String subtag(String full, String attribute) {
+ int start = full.indexOf(attribute);
+ if (start < 0) {
+ return null;
+ }
+
+ start += attribute.length();
+ int end = full.indexOf(';', start);
+
+ if (end < 0) {
+ return full.substring(start);
+ } else {
+ return full.substring(start, end);
+ }
+ }
+
+ /**
+ * Forces the text line to be the specified height, shrinking/stretching
+ * the ascent if possible, or the descent if shrinking the ascent further
+ * will make the text unreadable.
+ */
+ private static class Height implements LineHeightSpan {
+ private int mSize;
+ private static float sProportion = 0;
+
+ public Height(int size) {
+ mSize = size;
+ }
+
+ public void chooseHeight(CharSequence text, int start, int end,
+ int spanstartv, int v,
+ Paint.FontMetricsInt fm) {
+ if (fm.bottom - fm.top < mSize) {
+ fm.top = fm.bottom - mSize;
+ fm.ascent = fm.ascent - mSize;
+ } else {
+ if (sProportion == 0) {
+ /*
+ * Calculate what fraction of the nominal ascent
+ * the height of a capital letter actually is,
+ * so that we won't reduce the ascent to less than
+ * that unless we absolutely have to.
+ */
+
+ Paint p = new Paint();
+ p.setTextSize(100);
+ Rect r = new Rect();
+ p.getTextBounds("ABCDEFG", 0, 7, r);
+
+ sProportion = (r.top) / p.ascent();
+ }
+
+ int need = (int) Math.ceil(-fm.top * sProportion);
+
+ if (mSize - fm.descent >= need) {
+ /*
+ * It is safe to shrink the ascent this much.
+ */
+
+ fm.top = fm.bottom - mSize;
+ fm.ascent = fm.descent - mSize;
+ } else if (mSize >= need) {
+ /*
+ * We can't show all the descent, but we can at least
+ * show all the ascent.
+ */
+
+ fm.top = fm.ascent = -need;
+ fm.bottom = fm.descent = fm.top + mSize;
+ } else {
+ /*
+ * Show as much of the ascent as we can, and no descent.
+ */
+
+ fm.top = fm.ascent = -mSize;
+ fm.bottom = fm.descent = 0;
+ }
+ }
+ }
+ }
+
+ /**
+ * Create from an existing string block native object. This is
+ * -extremely- dangerous -- only use it if you absolutely know what you
+ * are doing! The given native object must exist for the entire lifetime
+ * of this newly creating StringBlock.
+ */
+ StringBlock(int obj, boolean useSparse) {
+ mNative = obj;
+ mUseSparse = useSparse;
+ mOwnsNative = false;
+ if (localLOGV) Log.v(TAG, "Created string block " + this
+ + ": " + nativeGetSize(mNative));
+ }
+
+ private static final native int nativeCreate(byte[] data,
+ int offset,
+ int size);
+ private static final native int nativeGetSize(int obj);
+ private static final native String nativeGetString(int obj, int idx);
+ private static final native int[] nativeGetStyle(int obj, int idx);
+ private static final native int nativeIndexOfString(int obj, String str);
+ private static final native void nativeDestroy(int obj);
+}
diff --git a/core/java/android/content/res/TypedArray.java b/core/java/android/content/res/TypedArray.java
new file mode 100644
index 0000000..82a57dd
--- /dev/null
+++ b/core/java/android/content/res/TypedArray.java
@@ -0,0 +1,660 @@
+package android.content.res;
+
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.TypedValue;
+import com.android.internal.util.XmlUtils;
+
+import java.util.Arrays;
+
+/**
+ * Container for an array of values that were retrieved with
+ * {@link Resources.Theme#obtainStyledAttributes(AttributeSet, int[], int, int)}
+ * or {@link Resources#obtainAttributes}. Be
+ * sure to call {@link #recycle} when done with them.
+ *
+ * The indices used to retrieve values from this structure correspond to
+ * the positions of the attributes given to obtainStyledAttributes.
+ */
+public class TypedArray {
+ private final Resources mResources;
+ /*package*/ XmlBlock.Parser mXml;
+ /*package*/ int[] mRsrcs;
+ /*package*/ int[] mData;
+ /*package*/ int[] mIndices;
+ /*package*/ int mLength;
+ private TypedValue mValue = new TypedValue();
+
+ /**
+ * Return the number of values in this array.
+ */
+ public int length() {
+ return mLength;
+ }
+
+ /**
+ * Return the number of indices in the array that actually have data.
+ */
+ public int getIndexCount() {
+ return mIndices[0];
+ }
+
+ /**
+ * Return an index in the array that has data.
+ *
+ * @param at The index you would like to returned, ranging from 0 to
+ * {@link #getIndexCount()}.
+ *
+ * @return The index at the given offset, which can be used with
+ * {@link #getValue} and related APIs.
+ */
+ public int getIndex(int at) {
+ return mIndices[1+at];
+ }
+
+ /**
+ * Return the Resources object this array was loaded from.
+ */
+ public Resources getResources() {
+ return mResources;
+ }
+
+ /**
+ * Retrieve the styled string value for the attribute at <var>index</var>.
+ *
+ * @param index Index of attribute to retrieve.
+ *
+ * @return CharSequence holding string data. May be styled. Returns
+ * null if the attribute is not defined.
+ */
+ public CharSequence getText(int index) {
+ index *= AssetManager.STYLE_NUM_ENTRIES;
+ final int[] data = mData;
+ final int type = data[index+AssetManager.STYLE_TYPE];
+ if (type == TypedValue.TYPE_NULL) {
+ return null;
+ } else if (type == TypedValue.TYPE_STRING) {
+ return loadStringValueAt(index);
+ }
+
+ TypedValue v = mValue;
+ if (getValueAt(index, v)) {
+ Log.w(Resources.TAG, "Converting to string: " + v);
+ return v.coerceToString();
+ }
+ Log.w(Resources.TAG, "getString of bad type: 0x"
+ + Integer.toHexString(type));
+ return null;
+ }
+
+ /**
+ * Retrieve the string value for the attribute at <var>index</var>.
+ *
+ * @param index Index of attribute to retrieve.
+ *
+ * @return String holding string data. Any styling information is
+ * removed. Returns null if the attribute is not defined.
+ */
+ public String getString(int index) {
+ index *= AssetManager.STYLE_NUM_ENTRIES;
+ final int[] data = mData;
+ final int type = data[index+AssetManager.STYLE_TYPE];
+ if (type == TypedValue.TYPE_NULL) {
+ return null;
+ } else if (type == TypedValue.TYPE_STRING) {
+ return loadStringValueAt(index).toString();
+ }
+
+ TypedValue v = mValue;
+ if (getValueAt(index, v)) {
+ Log.w(Resources.TAG, "Converting to string: " + v);
+ CharSequence cs = v.coerceToString();
+ return cs != null ? cs.toString() : null;
+ }
+ Log.w(Resources.TAG, "getString of bad type: 0x"
+ + Integer.toHexString(type));
+ return null;
+ }
+
+ /**
+ * Retrieve the string value for the attribute at <var>index</var>, but
+ * only if that string comes from an immediate value in an XML file. That
+ * is, this does not allow references to string resources, string
+ * attributes, or conversions from other types. As such, this method
+ * will only return strings for TypedArray objects that come from
+ * attributes in an XML file.
+ *
+ * @param index Index of attribute to retrieve.
+ *
+ * @return String holding string data. Any styling information is
+ * removed. Returns null if the attribute is not defined or is not
+ * an immediate string value.
+ */
+ public String getNonResourceString(int index) {
+ index *= AssetManager.STYLE_NUM_ENTRIES;
+ final int[] data = mData;
+ final int type = data[index+AssetManager.STYLE_TYPE];
+ if (type == TypedValue.TYPE_STRING) {
+ final int cookie = data[index+AssetManager.STYLE_ASSET_COOKIE];
+ if (cookie < 0) {
+ return mXml.getPooledString(
+ data[index+AssetManager.STYLE_DATA]).toString();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Retrieve the boolean value for the attribute at <var>index</var>.
+ *
+ * @param index Index of attribute to retrieve.
+ * @param defValue Value to return if the attribute is not defined.
+ *
+ * @return Attribute boolean value, or defValue if not defined.
+ */
+ public boolean getBoolean(int index, boolean defValue) {
+ index *= AssetManager.STYLE_NUM_ENTRIES;
+ final int[] data = mData;
+ final int type = data[index+AssetManager.STYLE_TYPE];
+ if (type == TypedValue.TYPE_NULL) {
+ return defValue;
+ } else if (type >= TypedValue.TYPE_FIRST_INT
+ && type <= TypedValue.TYPE_LAST_INT) {
+ return data[index+AssetManager.STYLE_DATA] != 0;
+ }
+
+ TypedValue v = mValue;
+ if (getValueAt(index, v)) {
+ Log.w(Resources.TAG, "Converting to boolean: " + v);
+ return XmlUtils.convertValueToBoolean(
+ v.coerceToString(), defValue);
+ }
+ Log.w(Resources.TAG, "getBoolean of bad type: 0x"
+ + Integer.toHexString(type));
+ return defValue;
+ }
+
+ /**
+ * Retrieve the integer value for the attribute at <var>index</var>.
+ *
+ * @param index Index of attribute to retrieve.
+ * @param defValue Value to return if the attribute is not defined.
+ *
+ * @return Attribute int value, or defValue if not defined.
+ */
+ public int getInt(int index, int defValue) {
+ index *= AssetManager.STYLE_NUM_ENTRIES;
+ final int[] data = mData;
+ final int type = data[index+AssetManager.STYLE_TYPE];
+ if (type == TypedValue.TYPE_NULL) {
+ return defValue;
+ } else if (type >= TypedValue.TYPE_FIRST_INT
+ && type <= TypedValue.TYPE_LAST_INT) {
+ return data[index+AssetManager.STYLE_DATA];
+ }
+
+ TypedValue v = mValue;
+ if (getValueAt(index, v)) {
+ Log.w(Resources.TAG, "Converting to int: " + v);
+ return XmlUtils.convertValueToInt(
+ v.coerceToString(), defValue);
+ }
+ Log.w(Resources.TAG, "getInt of bad type: 0x"
+ + Integer.toHexString(type));
+ return defValue;
+ }
+
+ /**
+ * Retrieve the float value for the attribute at <var>index</var>.
+ *
+ * @param index Index of attribute to retrieve.
+ *
+ * @return Attribute float value, or defValue if not defined..
+ */
+ public float getFloat(int index, float defValue) {
+ index *= AssetManager.STYLE_NUM_ENTRIES;
+ final int[] data = mData;
+ final int type = data[index+AssetManager.STYLE_TYPE];
+ if (type == TypedValue.TYPE_NULL) {
+ return defValue;
+ } else if (type == TypedValue.TYPE_FLOAT) {
+ return Float.intBitsToFloat(data[index+AssetManager.STYLE_DATA]);
+ } else if (type >= TypedValue.TYPE_FIRST_INT
+ && type <= TypedValue.TYPE_LAST_INT) {
+ return data[index+AssetManager.STYLE_DATA];
+ }
+
+ TypedValue v = mValue;
+ if (getValueAt(index, v)) {
+ Log.w(Resources.TAG, "Converting to float: " + v);
+ CharSequence str = v.coerceToString();
+ if (str != null) {
+ return Float.parseFloat(str.toString());
+ }
+ }
+ Log.w(Resources.TAG, "getFloat of bad type: 0x"
+ + Integer.toHexString(type));
+ return defValue;
+ }
+
+ /**
+ * Retrieve the color value for the attribute at <var>index</var>. If
+ * the attribute references a color resource holding a complex
+ * {@link android.content.res.ColorStateList}, then the default color from
+ * the set is returned.
+ *
+ * @param index Index of attribute to retrieve.
+ * @param defValue Value to return if the attribute is not defined or
+ * not a resource.
+ *
+ * @return Attribute color value, or defValue if not defined.
+ */
+ public int getColor(int index, int defValue) {
+ index *= AssetManager.STYLE_NUM_ENTRIES;
+ final int[] data = mData;
+ final int type = data[index+AssetManager.STYLE_TYPE];
+ if (type == TypedValue.TYPE_NULL) {
+ return defValue;
+ } else if (type >= TypedValue.TYPE_FIRST_INT
+ && type <= TypedValue.TYPE_LAST_INT) {
+ return data[index+AssetManager.STYLE_DATA];
+ } else if (type == TypedValue.TYPE_STRING) {
+ final TypedValue value = mValue;
+ if (getValueAt(index, value)) {
+ ColorStateList csl = mResources.loadColorStateList(
+ value, value.resourceId);
+ return csl.getDefaultColor();
+ }
+ return defValue;
+ }
+
+ throw new UnsupportedOperationException("Can't convert to color: type=0x"
+ + Integer.toHexString(type));
+ }
+
+ /**
+ * Retrieve the ColorStateList for the attribute at <var>index</var>.
+ * The value may be either a single solid color or a reference to
+ * a color or complex {@link android.content.res.ColorStateList} description.
+ *
+ * @param index Index of attribute to retrieve.
+ *
+ * @return ColorStateList for the attribute, or null if not defined.
+ */
+ public ColorStateList getColorStateList(int index) {
+ final TypedValue value = mValue;
+ if (getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, value)) {
+ return mResources.loadColorStateList(value, value.resourceId);
+ }
+ return null;
+ }
+
+ /**
+ * Retrieve the integer value for the attribute at <var>index</var>.
+ *
+ * @param index Index of attribute to retrieve.
+ * @param defValue Value to return if the attribute is not defined or
+ * not a resource.
+ *
+ * @return Attribute integer value, or defValue if not defined.
+ */
+ public int getInteger(int index, int defValue) {
+ index *= AssetManager.STYLE_NUM_ENTRIES;
+ final int[] data = mData;
+ final int type = data[index+AssetManager.STYLE_TYPE];
+ if (type == TypedValue.TYPE_NULL) {
+ return defValue;
+ } else if (type >= TypedValue.TYPE_FIRST_INT
+ && type <= TypedValue.TYPE_LAST_INT) {
+ return data[index+AssetManager.STYLE_DATA];
+ }
+
+ throw new UnsupportedOperationException("Can't convert to integer: type=0x"
+ + Integer.toHexString(type));
+ }
+
+ /**
+ * Retrieve a dimensional unit attribute at <var>index</var>. Unit
+ * conversions are based on the current {@link DisplayMetrics}
+ * associated with the resources this {@link TypedArray} object
+ * came from.
+ *
+ * @param index Index of attribute to retrieve.
+ * @param defValue Value to return if the attribute is not defined or
+ * not a resource.
+ *
+ * @return Attribute dimension value multiplied by the appropriate
+ * metric, or defValue if not defined.
+ *
+ * @see #getDimensionPixelOffset
+ * @see #getDimensionPixelSize
+ */
+ public float getDimension(int index, float defValue) {
+ index *= AssetManager.STYLE_NUM_ENTRIES;
+ final int[] data = mData;
+ final int type = data[index+AssetManager.STYLE_TYPE];
+ if (type == TypedValue.TYPE_NULL) {
+ return defValue;
+ } else if (type == TypedValue.TYPE_DIMENSION) {
+ return TypedValue.complexToDimension(
+ data[index+AssetManager.STYLE_DATA], mResources.mMetrics);
+ }
+
+ throw new UnsupportedOperationException("Can't convert to dimension: type=0x"
+ + Integer.toHexString(type));
+ }
+
+ /**
+ * Retrieve a dimensional unit attribute at <var>index</var> for use
+ * as an offset in raw pixels. This is the same as
+ * {@link #getDimension}, except the returned value is converted to
+ * integer pixels for you. An offset conversion involves simply
+ * truncating the base value to an integer.
+ *
+ * @param index Index of attribute to retrieve.
+ * @param defValue Value to return if the attribute is not defined or
+ * not a resource.
+ *
+ * @return Attribute dimension value multiplied by the appropriate
+ * metric and truncated to integer pixels, or defValue if not defined.
+ *
+ * @see #getDimension
+ * @see #getDimensionPixelSize
+ */
+ public int getDimensionPixelOffset(int index, int defValue) {
+ index *= AssetManager.STYLE_NUM_ENTRIES;
+ final int[] data = mData;
+ final int type = data[index+AssetManager.STYLE_TYPE];
+ if (type == TypedValue.TYPE_NULL) {
+ return defValue;
+ } else if (type == TypedValue.TYPE_DIMENSION) {
+ return TypedValue.complexToDimensionPixelOffset(
+ data[index+AssetManager.STYLE_DATA], mResources.mMetrics);
+ }
+
+ throw new UnsupportedOperationException("Can't convert to dimension: type=0x"
+ + Integer.toHexString(type));
+ }
+
+ /**
+ * Retrieve a dimensional unit attribute at <var>index</var> for use
+ * as a size in raw pixels. This is the same as
+ * {@link #getDimension}, except the returned value is converted to
+ * integer pixels for use as a size. A size conversion involves
+ * rounding the base value, and ensuring that a non-zero base value
+ * is at least one pixel in size.
+ *
+ * @param index Index of attribute to retrieve.
+ * @param defValue Value to return if the attribute is not defined or
+ * not a resource.
+ *
+ * @return Attribute dimension value multiplied by the appropriate
+ * metric and truncated to integer pixels, or defValue if not defined.
+ *
+ * @see #getDimension
+ * @see #getDimensionPixelOffset
+ */
+ public int getDimensionPixelSize(int index, int defValue) {
+ index *= AssetManager.STYLE_NUM_ENTRIES;
+ final int[] data = mData;
+ final int type = data[index+AssetManager.STYLE_TYPE];
+ if (type == TypedValue.TYPE_NULL) {
+ return defValue;
+ } else if (type == TypedValue.TYPE_DIMENSION) {
+ return TypedValue.complexToDimensionPixelSize(
+ data[index+AssetManager.STYLE_DATA], mResources.mMetrics);
+ }
+
+ throw new UnsupportedOperationException("Can't convert to dimension: type=0x"
+ + Integer.toHexString(type));
+ }
+
+ /**
+ * Special version of {@link #getDimensionPixelSize} for retrieving
+ * {@link android.view.ViewGroup}'s layout_width and layout_height
+ * attributes. This is only here for performance reasons; applications
+ * should use {@link #getDimensionPixelSize}.
+ *
+ * @param index Index of the attribute to retrieve.
+ * @param name Textual name of attribute for error reporting.
+ *
+ * @return Attribute dimension value multiplied by the appropriate
+ * metric and truncated to integer pixels.
+ */
+ public int getLayoutDimension(int index, String name) {
+ index *= AssetManager.STYLE_NUM_ENTRIES;
+ final int[] data = mData;
+ final int type = data[index+AssetManager.STYLE_TYPE];
+ if (type >= TypedValue.TYPE_FIRST_INT
+ && type <= TypedValue.TYPE_LAST_INT) {
+ return data[index+AssetManager.STYLE_DATA];
+ } else if (type == TypedValue.TYPE_DIMENSION) {
+ return TypedValue.complexToDimensionPixelSize(
+ data[index+AssetManager.STYLE_DATA], mResources.mMetrics);
+ }
+
+ throw new RuntimeException(getPositionDescription()
+ + ": You must supply a " + name + " attribute.");
+ }
+
+ /**
+ * Retrieve a fractional unit attribute at <var>index</var>.
+ *
+ * @param index Index of attribute to retrieve.
+ * @param base The base value of this fraction. In other words, a
+ * standard fraction is multiplied by this value.
+ * @param pbase The parent base value of this fraction. In other
+ * words, a parent fraction (nn%p) is multiplied by this
+ * value.
+ * @param defValue Value to return if the attribute is not defined or
+ * not a resource.
+ *
+ * @return Attribute fractional value multiplied by the appropriate
+ * base value, or defValue if not defined.
+ */
+ public float getFraction(int index, int base, int pbase, float defValue) {
+ index *= AssetManager.STYLE_NUM_ENTRIES;
+ final int[] data = mData;
+ final int type = data[index+AssetManager.STYLE_TYPE];
+ if (type == TypedValue.TYPE_NULL) {
+ return defValue;
+ } else if (type == TypedValue.TYPE_FRACTION) {
+ return TypedValue.complexToFraction(
+ data[index+AssetManager.STYLE_DATA], base, pbase);
+ }
+
+ throw new UnsupportedOperationException("Can't convert to fraction: type=0x"
+ + Integer.toHexString(type));
+ }
+
+ /**
+ * Retrieve the resource identifier for the attribute at
+ * <var>index</var>. Note that attribute resource as resolved when
+ * the overall {@link TypedArray} object is retrieved. As a
+ * result, this function will return the resource identifier of the
+ * final resource value that was found, <em>not</em> necessarily the
+ * original resource that was specified by the attribute.
+ *
+ * @param index Index of attribute to retrieve.
+ * @param defValue Value to return if the attribute is not defined or
+ * not a resource.
+ *
+ * @return Attribute resource identifier, or defValue if not defined.
+ */
+ public int getResourceId(int index, int defValue) {
+ index *= AssetManager.STYLE_NUM_ENTRIES;
+ final int[] data = mData;
+ if (data[index+AssetManager.STYLE_TYPE] != TypedValue.TYPE_NULL) {
+ final int resid = data[index+AssetManager.STYLE_RESOURCE_ID];
+ if (resid != 0) {
+ return resid;
+ }
+ }
+ return defValue;
+ }
+
+ /**
+ * Retrieve the Drawable for the attribute at <var>index</var>. This
+ * gets the resource ID of the selected attribute, and uses
+ * {@link Resources#getDrawable Resources.getDrawable} of the owning
+ * Resources object to retrieve its Drawable.
+ *
+ * @param index Index of attribute to retrieve.
+ *
+ * @return Drawable for the attribute, or null if not defined.
+ */
+ public Drawable getDrawable(int index) {
+ final TypedValue value = mValue;
+ if (getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, value)) {
+ if (false) {
+ System.out.println("******************************************************************");
+ System.out.println("Got drawable resource: type="
+ + value.type
+ + " str=" + value.string
+ + " int=0x" + Integer.toHexString(value.data)
+ + " cookie=" + value.assetCookie);
+ System.out.println("******************************************************************");
+ }
+ return mResources.loadDrawable(value, value.resourceId);
+ }
+ return null;
+ }
+
+ /**
+ * Retrieve the CharSequence[] for the attribute at <var>index</var>.
+ * This gets the resource ID of the selected attribute, and uses
+ * {@link Resources#getTextArray Resources.getTextArray} of the owning
+ * Resources object to retrieve its String[].
+ *
+ * @param index Index of attribute to retrieve.
+ *
+ * @return CharSequence[] for the attribute, or null if not defined.
+ */
+ public CharSequence[] getTextArray(int index) {
+ final TypedValue value = mValue;
+ if (getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, value)) {
+ if (false) {
+ System.out.println("******************************************************************");
+ System.out.println("Got drawable resource: type="
+ + value.type
+ + " str=" + value.string
+ + " int=0x" + Integer.toHexString(value.data)
+ + " cookie=" + value.assetCookie);
+ System.out.println("******************************************************************");
+ }
+ return mResources.getTextArray(value.resourceId);
+ }
+ return null;
+ }
+
+ /**
+ * Retrieve the raw TypedValue for the attribute at <var>index</var>.
+ *
+ * @param index Index of attribute to retrieve.
+ * @param outValue TypedValue object in which to place the attribute's
+ * data.
+ *
+ * @return Returns true if the value was retrieved, else false.
+ */
+ public boolean getValue(int index, TypedValue outValue) {
+ return getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, outValue);
+ }
+
+ /**
+ * Determines whether there is an attribute at <var>index</var>.
+ *
+ * @param index Index of attribute to retrieve.
+ *
+ * @return True if the attribute has a value, false otherwise.
+ */
+ public boolean hasValue(int index) {
+ index *= AssetManager.STYLE_NUM_ENTRIES;
+ final int[] data = mData;
+ final int type = data[index+AssetManager.STYLE_TYPE];
+ return type != TypedValue.TYPE_NULL;
+ }
+
+ /**
+ * Retrieve the raw TypedValue for the attribute at <var>index</var>
+ * and return a temporary object holding its data. This object is only
+ * valid until the next call on to {@link TypedArray}.
+ *
+ * @param index Index of attribute to retrieve.
+ *
+ * @return Returns a TypedValue object if the attribute is defined,
+ * containing its data; otherwise returns null. (You will not
+ * receive a TypedValue whose type is TYPE_NULL.)
+ */
+ public TypedValue peekValue(int index) {
+ final TypedValue value = mValue;
+ if (getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, value)) {
+ return value;
+ }
+ return null;
+ }
+
+ /**
+ * Returns a message about the parser state suitable for printing error messages.
+ */
+ public String getPositionDescription() {
+ return mXml != null ? mXml.getPositionDescription() : "<internal>";
+ }
+
+ /**
+ * Give back a previously retrieved StyledAttributes, for later re-use.
+ */
+ public void recycle() {
+ synchronized (mResources.mTmpValue) {
+ TypedArray cached = mResources.mCachedStyledAttributes;
+ if (cached == null || cached.mData.length < mData.length) {
+ mXml = null;
+ mResources.mCachedStyledAttributes = this;
+ }
+ }
+ }
+
+ private boolean getValueAt(int index, TypedValue outValue) {
+ final int[] data = mData;
+ final int type = data[index+AssetManager.STYLE_TYPE];
+ if (type == TypedValue.TYPE_NULL) {
+ return false;
+ }
+ outValue.type = type;
+ outValue.data = data[index+AssetManager.STYLE_DATA];
+ outValue.assetCookie = data[index+AssetManager.STYLE_ASSET_COOKIE];
+ outValue.resourceId = data[index+AssetManager.STYLE_RESOURCE_ID];
+ outValue.changingConfigurations = data[index+AssetManager.STYLE_CHANGING_CONFIGURATIONS];
+ if (type == TypedValue.TYPE_STRING) {
+ outValue.string = loadStringValueAt(index);
+ }
+ return true;
+ }
+
+ private CharSequence loadStringValueAt(int index) {
+ final int[] data = mData;
+ final int cookie = data[index+AssetManager.STYLE_ASSET_COOKIE];
+ if (cookie < 0) {
+ if (mXml != null) {
+ return mXml.getPooledString(
+ data[index+AssetManager.STYLE_DATA]);
+ }
+ return null;
+ }
+ //System.out.println("Getting pooled from: " + v);
+ return mResources.mAssets.getPooledString(
+ cookie, data[index+AssetManager.STYLE_DATA]);
+ }
+
+ /*package*/ TypedArray(Resources resources, int[] data, int[] indices, int len) {
+ mResources = resources;
+ mData = data;
+ mIndices = indices;
+ mLength = len;
+ }
+
+ public String toString() {
+ return Arrays.toString(mData);
+ }
+} \ No newline at end of file
diff --git a/core/java/android/content/res/XmlBlock.java b/core/java/android/content/res/XmlBlock.java
new file mode 100644
index 0000000..6336678
--- /dev/null
+++ b/core/java/android/content/res/XmlBlock.java
@@ -0,0 +1,515 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.res;
+
+import android.util.TypedValue;
+import com.android.internal.util.XmlUtils;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+
+/**
+ * Wrapper around a compiled XML file.
+ *
+ * {@hide}
+ */
+final class XmlBlock {
+ private static final boolean DEBUG=false;
+
+ public XmlBlock(byte[] data) {
+ mAssets = null;
+ mNative = nativeCreate(data, 0, data.length);
+ mStrings = new StringBlock(nativeGetStringBlock(mNative), false);
+ }
+
+ public XmlBlock(byte[] data, int offset, int size) {
+ mAssets = null;
+ mNative = nativeCreate(data, offset, size);
+ mStrings = new StringBlock(nativeGetStringBlock(mNative), false);
+ }
+
+ public void close() {
+ synchronized (this) {
+ if (mOpen) {
+ mOpen = false;
+ decOpenCountLocked();
+ }
+ }
+ }
+
+ private void decOpenCountLocked() {
+ mOpenCount--;
+ if (mOpenCount == 0) {
+ nativeDestroy(mNative);
+ if (mAssets != null) {
+ mAssets.xmlBlockGone();
+ }
+ }
+ }
+
+ public XmlResourceParser newParser() {
+ synchronized (this) {
+ if (mNative != 0) {
+ return new Parser(nativeCreateParseState(mNative), this);
+ }
+ return null;
+ }
+ }
+
+ /*package*/ final class Parser implements XmlResourceParser {
+ Parser(int parseState, XmlBlock block) {
+ mParseState = parseState;
+ mBlock = block;
+ block.mOpenCount++;
+ }
+
+ public void setFeature(String name, boolean state) throws XmlPullParserException {
+ if (FEATURE_PROCESS_NAMESPACES.equals(name) && state) {
+ return;
+ }
+ if (FEATURE_REPORT_NAMESPACE_ATTRIBUTES.equals(name) && state) {
+ return;
+ }
+ throw new XmlPullParserException("Unsupported feature: " + name);
+ }
+ public boolean getFeature(String name) {
+ if (FEATURE_PROCESS_NAMESPACES.equals(name)) {
+ return true;
+ }
+ if (FEATURE_REPORT_NAMESPACE_ATTRIBUTES.equals(name)) {
+ return true;
+ }
+ return false;
+ }
+ public void setProperty(String name, Object value) throws XmlPullParserException {
+ throw new XmlPullParserException("setProperty() not supported");
+ }
+ public Object getProperty(String name) {
+ return null;
+ }
+ public void setInput(Reader in) throws XmlPullParserException {
+ throw new XmlPullParserException("setInput() not supported");
+ }
+ public void setInput(InputStream inputStream, String inputEncoding) throws XmlPullParserException {
+ throw new XmlPullParserException("setInput() not supported");
+ }
+ public void defineEntityReplacementText(String entityName, String replacementText) throws XmlPullParserException {
+ throw new XmlPullParserException("defineEntityReplacementText() not supported");
+ }
+ public String getNamespacePrefix(int pos) throws XmlPullParserException {
+ throw new XmlPullParserException("getNamespacePrefix() not supported");
+ }
+ public String getInputEncoding() {
+ return null;
+ }
+ public String getNamespace(String prefix) {
+ throw new RuntimeException("getNamespace() not supported");
+ }
+ public int getNamespaceCount(int depth) throws XmlPullParserException {
+ throw new XmlPullParserException("getNamespaceCount() not supported");
+ }
+ public String getPositionDescription() {
+ return "Binary XML file line #" + getLineNumber();
+ }
+ public String getNamespaceUri(int pos) throws XmlPullParserException {
+ throw new XmlPullParserException("getNamespaceUri() not supported");
+ }
+ public int getColumnNumber() {
+ return -1;
+ }
+ public int getDepth() {
+ return mDepth;
+ }
+ public String getText() {
+ int id = nativeGetText(mParseState);
+ return id >= 0 ? mStrings.get(id).toString() : null;
+ }
+ public int getLineNumber() {
+ return nativeGetLineNumber(mParseState);
+ }
+ public int getEventType() throws XmlPullParserException {
+ return mEventType;
+ }
+ public boolean isWhitespace() throws XmlPullParserException {
+ // whitespace was stripped by aapt.
+ return false;
+ }
+ public String getPrefix() {
+ throw new RuntimeException("getPrefix not supported");
+ }
+ public char[] getTextCharacters(int[] holderForStartAndLength) {
+ String txt = getText();
+ char[] chars = null;
+ if (txt != null) {
+ holderForStartAndLength[0] = 0;
+ holderForStartAndLength[1] = txt.length();
+ chars = new char[txt.length()];
+ txt.getChars(0, txt.length(), chars, 0);
+ }
+ return chars;
+ }
+ public String getNamespace() {
+ int id = nativeGetNamespace(mParseState);
+ return id >= 0 ? mStrings.get(id).toString() : "";
+ }
+ public String getName() {
+ int id = nativeGetName(mParseState);
+ return id >= 0 ? mStrings.get(id).toString() : null;
+ }
+ public String getAttributeNamespace(int index) {
+ int id = nativeGetAttributeNamespace(mParseState, index);
+ if (DEBUG) System.out.println("getAttributeNamespace of " + index + " = " + id);
+ if (id >= 0) return mStrings.get(id).toString();
+ else if (id == -1) return "";
+ throw new IndexOutOfBoundsException(String.valueOf(index));
+ }
+ public String getAttributeName(int index) {
+ int id = nativeGetAttributeName(mParseState, index);
+ if (DEBUG) System.out.println("getAttributeName of " + index + " = " + id);
+ if (id >= 0) return mStrings.get(id).toString();
+ throw new IndexOutOfBoundsException(String.valueOf(index));
+ }
+ public String getAttributePrefix(int index) {
+ throw new RuntimeException("getAttributePrefix not supported");
+ }
+ public boolean isEmptyElementTag() throws XmlPullParserException {
+ // XXX Need to detect this.
+ return false;
+ }
+ public int getAttributeCount() {
+ return mEventType == START_TAG ? nativeGetAttributeCount(mParseState) : -1;
+ }
+ public String getAttributeValue(int index) {
+ int id = nativeGetAttributeStringValue(mParseState, index);
+ if (DEBUG) System.out.println("getAttributeValue of " + index + " = " + id);
+ if (id >= 0) return mStrings.get(id).toString();
+
+ // May be some other type... check and try to convert if so.
+ int t = nativeGetAttributeDataType(mParseState, index);
+ if (t == TypedValue.TYPE_NULL) {
+ throw new IndexOutOfBoundsException(String.valueOf(index));
+ }
+
+ int v = nativeGetAttributeData(mParseState, index);
+ return TypedValue.coerceToString(t, v);
+ }
+ public String getAttributeType(int index) {
+ return "CDATA";
+ }
+ public boolean isAttributeDefault(int index) {
+ return false;
+ }
+ public int nextToken() throws XmlPullParserException,IOException {
+ return next();
+ }
+ public String getAttributeValue(String namespace, String name) {
+ int idx = nativeGetAttributeIndex(mParseState, namespace, name);
+ if (idx >= 0) {
+ if (DEBUG) System.out.println("getAttributeName of "
+ + namespace + ":" + name + " index = " + idx);
+ if (DEBUG) System.out.println(
+ "Namespace=" + getAttributeNamespace(idx)
+ + "Name=" + getAttributeName(idx)
+ + ", Value=" + getAttributeValue(idx));
+ return getAttributeValue(idx);
+ }
+ return null;
+ }
+ public int next() throws XmlPullParserException,IOException {
+ if (!mStarted) {
+ mStarted = true;
+ return START_DOCUMENT;
+ }
+ if (mParseState == 0) {
+ return END_DOCUMENT;
+ }
+ int ev = nativeNext(mParseState);
+ if (mDecNextDepth) {
+ mDepth--;
+ mDecNextDepth = false;
+ }
+ switch (ev) {
+ case START_TAG:
+ mDepth++;
+ break;
+ case END_TAG:
+ mDecNextDepth = true;
+ break;
+ }
+ mEventType = ev;
+ if (ev == END_DOCUMENT) {
+ // Automatically close the parse when we reach the end of
+ // a document, since the standard XmlPullParser interface
+ // doesn't have such an API so most clients will leave us
+ // dangling.
+ close();
+ }
+ return ev;
+ }
+ public void require(int type, String namespace, String name) throws XmlPullParserException,IOException {
+ if (type != getEventType()
+ || (namespace != null && !namespace.equals( getNamespace () ) )
+ || (name != null && !name.equals( getName() ) ) )
+ throw new XmlPullParserException( "expected "+ TYPES[ type ]+getPositionDescription());
+ }
+ public String nextText() throws XmlPullParserException,IOException {
+ if(getEventType() != START_TAG) {
+ throw new XmlPullParserException(
+ getPositionDescription()
+ + ": parser must be on START_TAG to read next text", this, null);
+ }
+ int eventType = next();
+ if(eventType == TEXT) {
+ String result = getText();
+ eventType = next();
+ if(eventType != END_TAG) {
+ throw new XmlPullParserException(
+ getPositionDescription()
+ + ": event TEXT it must be immediately followed by END_TAG", this, null);
+ }
+ return result;
+ } else if(eventType == END_TAG) {
+ return "";
+ } else {
+ throw new XmlPullParserException(
+ getPositionDescription()
+ + ": parser must be on START_TAG or TEXT to read text", this, null);
+ }
+ }
+ public int nextTag() throws XmlPullParserException,IOException {
+ int eventType = next();
+ if(eventType == TEXT && isWhitespace()) { // skip whitespace
+ eventType = next();
+ }
+ if (eventType != START_TAG && eventType != END_TAG) {
+ throw new XmlPullParserException(
+ getPositionDescription()
+ + ": expected start or end tag", this, null);
+ }
+ return eventType;
+ }
+
+ public int getAttributeNameResource(int index) {
+ return nativeGetAttributeResource(mParseState, index);
+ }
+
+ public int getAttributeListValue(String namespace, String attribute,
+ String[] options, int defaultValue) {
+ int idx = nativeGetAttributeIndex(mParseState, namespace, attribute);
+ if (idx >= 0) {
+ return getAttributeListValue(idx, options, defaultValue);
+ }
+ return defaultValue;
+ }
+ public boolean getAttributeBooleanValue(String namespace, String attribute,
+ boolean defaultValue) {
+ int idx = nativeGetAttributeIndex(mParseState, namespace, attribute);
+ if (idx >= 0) {
+ return getAttributeBooleanValue(idx, defaultValue);
+ }
+ return defaultValue;
+ }
+ public int getAttributeResourceValue(String namespace, String attribute,
+ int defaultValue) {
+ int idx = nativeGetAttributeIndex(mParseState, namespace, attribute);
+ if (idx >= 0) {
+ return getAttributeResourceValue(idx, defaultValue);
+ }
+ return defaultValue;
+ }
+ public int getAttributeIntValue(String namespace, String attribute,
+ int defaultValue) {
+ int idx = nativeGetAttributeIndex(mParseState, namespace, attribute);
+ if (idx >= 0) {
+ return getAttributeIntValue(idx, defaultValue);
+ }
+ return defaultValue;
+ }
+ public int getAttributeUnsignedIntValue(String namespace, String attribute,
+ int defaultValue)
+ {
+ int idx = nativeGetAttributeIndex(mParseState, namespace, attribute);
+ if (idx >= 0) {
+ return getAttributeUnsignedIntValue(idx, defaultValue);
+ }
+ return defaultValue;
+ }
+ public float getAttributeFloatValue(String namespace, String attribute,
+ float defaultValue) {
+ int idx = nativeGetAttributeIndex(mParseState, namespace, attribute);
+ if (idx >= 0) {
+ return getAttributeFloatValue(idx, defaultValue);
+ }
+ return defaultValue;
+ }
+
+ public int getAttributeListValue(int idx,
+ String[] options, int defaultValue) {
+ int t = nativeGetAttributeDataType(mParseState, idx);
+ int v = nativeGetAttributeData(mParseState, idx);
+ if (t == TypedValue.TYPE_STRING) {
+ return XmlUtils.convertValueToList(
+ mStrings.get(v), options, defaultValue);
+ }
+ return v;
+ }
+ public boolean getAttributeBooleanValue(int idx,
+ boolean defaultValue) {
+ int t = nativeGetAttributeDataType(mParseState, idx);
+ // Note: don't attempt to convert any other types, because
+ // we want to count on appt doing the conversion for us.
+ if (t >= TypedValue.TYPE_FIRST_INT &&
+ t <= TypedValue.TYPE_LAST_INT) {
+ return nativeGetAttributeData(mParseState, idx) != 0;
+ }
+ return defaultValue;
+ }
+ public int getAttributeResourceValue(int idx, int defaultValue) {
+ int t = nativeGetAttributeDataType(mParseState, idx);
+ // Note: don't attempt to convert any other types, because
+ // we want to count on appt doing the conversion for us.
+ if (t == TypedValue.TYPE_REFERENCE) {
+ return nativeGetAttributeData(mParseState, idx);
+ }
+ return defaultValue;
+ }
+ public int getAttributeIntValue(int idx, int defaultValue) {
+ int t = nativeGetAttributeDataType(mParseState, idx);
+ // Note: don't attempt to convert any other types, because
+ // we want to count on appt doing the conversion for us.
+ if (t >= TypedValue.TYPE_FIRST_INT &&
+ t <= TypedValue.TYPE_LAST_INT) {
+ return nativeGetAttributeData(mParseState, idx);
+ }
+ return defaultValue;
+ }
+ public int getAttributeUnsignedIntValue(int idx, int defaultValue) {
+ int t = nativeGetAttributeDataType(mParseState, idx);
+ // Note: don't attempt to convert any other types, because
+ // we want to count on appt doing the conversion for us.
+ if (t >= TypedValue.TYPE_FIRST_INT &&
+ t <= TypedValue.TYPE_LAST_INT) {
+ return nativeGetAttributeData(mParseState, idx);
+ }
+ return defaultValue;
+ }
+ public float getAttributeFloatValue(int idx, float defaultValue) {
+ int t = nativeGetAttributeDataType(mParseState, idx);
+ // Note: don't attempt to convert any other types, because
+ // we want to count on appt doing the conversion for us.
+ if (t == TypedValue.TYPE_FLOAT) {
+ return Float.intBitsToFloat(
+ nativeGetAttributeData(mParseState, idx));
+ }
+ throw new RuntimeException("not a float!");
+ }
+
+ public String getIdAttribute() {
+ int id = nativeGetIdAttribute(mParseState);
+ return id >= 0 ? mStrings.get(id).toString() : null;
+ }
+ public String getClassAttribute() {
+ int id = nativeGetClassAttribute(mParseState);
+ return id >= 0 ? mStrings.get(id).toString() : null;
+ }
+
+ public int getIdAttributeResourceValue(int defaultValue) {
+ //todo: create and use native method
+ return getAttributeResourceValue(null, "id", defaultValue);
+ }
+
+ public int getStyleAttribute() {
+ return nativeGetStyleAttribute(mParseState);
+ }
+
+ public void close() {
+ synchronized (mBlock) {
+ if (mParseState != 0) {
+ nativeDestroyParseState(mParseState);
+ mParseState = 0;
+ mBlock.decOpenCountLocked();
+ }
+ }
+ }
+
+ protected void finalize() throws Throwable {
+ close();
+ }
+
+ /*package*/ final CharSequence getPooledString(int id) {
+ return mStrings.get(id);
+ }
+
+ /*package*/ int mParseState;
+ private final XmlBlock mBlock;
+ private boolean mStarted = false;
+ private boolean mDecNextDepth = false;
+ private int mDepth = 0;
+ private int mEventType = START_DOCUMENT;
+ }
+
+ protected void finalize() throws Throwable {
+ close();
+ }
+
+ /**
+ * Create from an existing xml block native object. This is
+ * -extremely- dangerous -- only use it if you absolutely know what you
+ * are doing! The given native object must exist for the entire lifetime
+ * of this newly creating XmlBlock.
+ */
+ XmlBlock(AssetManager assets, int xmlBlock) {
+ mAssets = assets;
+ mNative = xmlBlock;
+ mStrings = new StringBlock(nativeGetStringBlock(xmlBlock), false);
+ }
+
+ private final AssetManager mAssets;
+ private final int mNative;
+ private final StringBlock mStrings;
+ private boolean mOpen = true;
+ private int mOpenCount = 1;
+
+ private static final native int nativeCreate(byte[] data,
+ int offset,
+ int size);
+ private static final native int nativeGetStringBlock(int obj);
+
+ private static final native int nativeCreateParseState(int obj);
+ private static final native int nativeNext(int state);
+ private static final native int nativeGetNamespace(int state);
+ private static final native int nativeGetName(int state);
+ private static final native int nativeGetText(int state);
+ private static final native int nativeGetLineNumber(int state);
+ private static final native int nativeGetAttributeCount(int state);
+ private static final native int nativeGetAttributeNamespace(int state, int idx);
+ private static final native int nativeGetAttributeName(int state, int idx);
+ private static final native int nativeGetAttributeResource(int state, int idx);
+ private static final native int nativeGetAttributeDataType(int state, int idx);
+ private static final native int nativeGetAttributeData(int state, int idx);
+ private static final native int nativeGetAttributeStringValue(int state, int idx);
+ private static final native int nativeGetIdAttribute(int state);
+ private static final native int nativeGetClassAttribute(int state);
+ private static final native int nativeGetStyleAttribute(int state);
+ private static final native int nativeGetAttributeIndex(int state, String namespace, String name);
+ private static final native void nativeDestroyParseState(int state);
+
+ private static final native void nativeDestroy(int obj);
+}
diff --git a/core/java/android/content/res/XmlResourceParser.java b/core/java/android/content/res/XmlResourceParser.java
new file mode 100644
index 0000000..c59e6d4
--- /dev/null
+++ b/core/java/android/content/res/XmlResourceParser.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.res;
+
+import org.xmlpull.v1.XmlPullParser;
+
+import android.util.AttributeSet;
+
+/**
+ * The XML parsing interface returned for an XML resource. This is a standard
+ * XmlPullParser interface, as well as an extended AttributeSet interface and
+ * an additional close() method on this interface for the client to indicate
+ * when it is done reading the resource.
+ */
+public interface XmlResourceParser extends XmlPullParser, AttributeSet {
+ /**
+ * Close this interface to the resource. Calls on the interface are no
+ * longer value after this call.
+ */
+ public void close();
+}
+
diff --git a/core/java/android/content/res/package.html b/core/java/android/content/res/package.html
new file mode 100644
index 0000000..bb09dc7
--- /dev/null
+++ b/core/java/android/content/res/package.html
@@ -0,0 +1,8 @@
+<HTML>
+<BODY>
+Contains classes for accessing application resources,
+such as raw asset files, colors, drawables, media or other other files
+in the package, plus important device configuration details
+(orientation, input types, etc.) that affect how the application may behave.
+</BODY>
+</HTML> \ No newline at end of file
diff --git a/core/java/android/database/AbstractCursor.java b/core/java/android/database/AbstractCursor.java
new file mode 100644
index 0000000..e81f7f8
--- /dev/null
+++ b/core/java/android/database/AbstractCursor.java
@@ -0,0 +1,617 @@
+/*
+ * 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.database;
+
+import android.content.ContentResolver;
+import android.net.Uri;
+import android.util.Config;
+import android.util.Log;
+import android.os.Bundle;
+
+import java.lang.ref.WeakReference;
+import java.lang.UnsupportedOperationException;
+import java.util.HashMap;
+import java.util.Map;
+
+
+/**
+ * This is an abstract cursor class that handles a lot of the common code
+ * that all cursors need to deal with and is provided for convenience reasons.
+ */
+public abstract class AbstractCursor implements CrossProcessCursor {
+ private static final String TAG = "Cursor";
+
+ DataSetObservable mDataSetObservable = new DataSetObservable();
+ ContentObservable mContentObservable = new ContentObservable();
+
+ /* -------------------------------------------------------- */
+ /* These need to be implemented by subclasses */
+ abstract public int getCount();
+
+ abstract public String[] getColumnNames();
+
+ abstract public String getString(int column);
+ abstract public short getShort(int column);
+ abstract public int getInt(int column);
+ abstract public long getLong(int column);
+ abstract public float getFloat(int column);
+ abstract public double getDouble(int column);
+ abstract public boolean isNull(int column);
+
+ // TODO implement getBlob in all cursor types
+ public byte[] getBlob(int column) {
+ throw new UnsupportedOperationException("getBlob is not supported");
+ }
+ /* -------------------------------------------------------- */
+ /* Methods that may optionally be implemented by subclasses */
+
+ /**
+ * returns a pre-filled window, return NULL if no such window
+ */
+ public CursorWindow getWindow() {
+ return null;
+ }
+
+ public int getColumnCount() {
+ return getColumnNames().length;
+ }
+
+ public void deactivate() {
+ deactivateInternal();
+ }
+
+ /**
+ * @hide
+ */
+ public void deactivateInternal() {
+ if (mSelfObserver != null) {
+ mContentResolver.unregisterContentObserver(mSelfObserver);
+ mSelfObserverRegistered = false;
+ }
+ mDataSetObservable.notifyInvalidated();
+ }
+
+ public boolean requery() {
+ if (mSelfObserver != null && mSelfObserverRegistered == false) {
+ mContentResolver.registerContentObserver(mNotifyUri, true, mSelfObserver);
+ mSelfObserverRegistered = true;
+ }
+ mDataSetObservable.notifyChanged();
+ return true;
+ }
+
+ public boolean isClosed() {
+ return mClosed;
+ }
+
+ public void close() {
+ mClosed = true;
+ mContentObservable.unregisterAll();
+ deactivateInternal();
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean commitUpdates(Map<? extends Long,? extends Map<String,Object>> values) {
+ return false;
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean deleteRow() {
+ return false;
+ }
+
+ /**
+ * This function is called every time the cursor is successfully scrolled
+ * to a new position, giving the subclass a chance to update any state it
+ * may have. If it returns false the move function will also do so and the
+ * cursor will scroll to the beforeFirst position.
+ *
+ * @param oldPosition the position that we're moving from
+ * @param newPosition the position that we're moving to
+ * @return true if the move is successful, false otherwise
+ */
+ public boolean onMove(int oldPosition, int newPosition) {
+ return true;
+ }
+
+
+ public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) {
+ // Default implementation, uses getString
+ String result = getString(columnIndex);
+ if (result != null) {
+ char[] data = buffer.data;
+ if (data == null || data.length < result.length()) {
+ buffer.data = result.toCharArray();
+ } else {
+ result.getChars(0, result.length(), data, 0);
+ }
+ buffer.sizeCopied = result.length();
+ }
+ }
+
+ /* -------------------------------------------------------- */
+ /* Implementation */
+ public AbstractCursor() {
+ mPos = -1;
+ mRowIdColumnIndex = -1;
+ mCurrentRowID = null;
+ mUpdatedRows = new HashMap<Long, Map<String, Object>>();
+ }
+
+ public final int getPosition() {
+ return mPos;
+ }
+
+ public final boolean moveToPosition(int position) {
+ // Make sure position isn't past the end of the cursor
+ final int count = getCount();
+ if (position >= count) {
+ mPos = count;
+ return false;
+ }
+
+ // Make sure position isn't before the beginning of the cursor
+ if (position < 0) {
+ mPos = -1;
+ return false;
+ }
+
+ // Check for no-op moves, and skip the rest of the work for them
+ if (position == mPos) {
+ return true;
+ }
+
+ boolean result = onMove(mPos, position);
+ if (result == false) {
+ mPos = -1;
+ } else {
+ mPos = position;
+ if (mRowIdColumnIndex != -1) {
+ mCurrentRowID = Long.valueOf(getLong(mRowIdColumnIndex));
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Copy data from cursor to CursorWindow
+ * @param position start position of data
+ * @param window
+ */
+ public void fillWindow(int position, CursorWindow window) {
+ if (position < 0 || position > getCount()) {
+ return;
+ }
+ window.acquireReference();
+ try {
+ int oldpos = mPos;
+ mPos = position - 1;
+ window.clear();
+ window.setStartPosition(position);
+ int columnNum = getColumnCount();
+ window.setNumColumns(columnNum);
+ while (moveToNext() && window.allocRow()) {
+ for (int i = 0; i < columnNum; i++) {
+ String field = getString(i);
+ if (field != null) {
+ if (!window.putString(field, mPos, i)) {
+ window.freeLastRow();
+ break;
+ }
+ } else {
+ if (!window.putNull(mPos, i)) {
+ window.freeLastRow();
+ break;
+ }
+ }
+ }
+ }
+
+ mPos = oldpos;
+ } catch (IllegalStateException e){
+ // simply ignore it
+ } finally {
+ window.releaseReference();
+ }
+ }
+
+ public final boolean move(int offset) {
+ return moveToPosition(mPos + offset);
+ }
+
+ public final boolean moveToFirst() {
+ return moveToPosition(0);
+ }
+
+ public final boolean moveToLast() {
+ return moveToPosition(getCount() - 1);
+ }
+
+ public final boolean moveToNext() {
+ return moveToPosition(mPos + 1);
+ }
+
+ public final boolean moveToPrevious() {
+ return moveToPosition(mPos - 1);
+ }
+
+ public final boolean isFirst() {
+ return mPos == 0 && getCount() != 0;
+ }
+
+ public final boolean isLast() {
+ int cnt = getCount();
+ return mPos == (cnt - 1) && cnt != 0;
+ }
+
+ public final boolean isBeforeFirst() {
+ if (getCount() == 0) {
+ return true;
+ }
+ return mPos == -1;
+ }
+
+ public final boolean isAfterLast() {
+ if (getCount() == 0) {
+ return true;
+ }
+ return mPos == getCount();
+ }
+
+ public int getColumnIndex(String columnName) {
+ // Hack according to bug 903852
+ final int periodIndex = columnName.lastIndexOf('.');
+ if (periodIndex != -1) {
+ Exception e = new Exception();
+ Log.e(TAG, "requesting column name with table name -- " + columnName, e);
+ columnName = columnName.substring(periodIndex + 1);
+ }
+
+ String columnNames[] = getColumnNames();
+ int length = columnNames.length;
+ for (int i = 0; i < length; i++) {
+ if (columnNames[i].equalsIgnoreCase(columnName)) {
+ return i;
+ }
+ }
+
+ if (Config.LOGV) {
+ if (getCount() > 0) {
+ Log.w("AbstractCursor", "Unknown column " + columnName);
+ }
+ }
+ return -1;
+ }
+
+ public int getColumnIndexOrThrow(String columnName) {
+ final int index = getColumnIndex(columnName);
+ if (index < 0) {
+ throw new IllegalArgumentException("column '" + columnName + "' does not exist");
+ }
+ return index;
+ }
+
+ public String getColumnName(int columnIndex) {
+ return getColumnNames()[columnIndex];
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean updateBlob(int columnIndex, byte[] value) {
+ return update(columnIndex, value);
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean updateString(int columnIndex, String value) {
+ return update(columnIndex, value);
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean updateShort(int columnIndex, short value) {
+ return update(columnIndex, Short.valueOf(value));
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean updateInt(int columnIndex, int value) {
+ return update(columnIndex, Integer.valueOf(value));
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean updateLong(int columnIndex, long value) {
+ return update(columnIndex, Long.valueOf(value));
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean updateFloat(int columnIndex, float value) {
+ return update(columnIndex, Float.valueOf(value));
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean updateDouble(int columnIndex, double value) {
+ return update(columnIndex, Double.valueOf(value));
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean updateToNull(int columnIndex) {
+ return update(columnIndex, null);
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean update(int columnIndex, Object obj) {
+ if (!supportsUpdates()) {
+ return false;
+ }
+
+ // Long.valueOf() returns null sometimes!
+// Long rowid = Long.valueOf(getLong(mRowIdColumnIndex));
+ Long rowid = new Long(getLong(mRowIdColumnIndex));
+ if (rowid == null) {
+ throw new IllegalStateException("null rowid. mRowIdColumnIndex = " + mRowIdColumnIndex);
+ }
+
+ synchronized(mUpdatedRows) {
+ Map<String, Object> row = mUpdatedRows.get(rowid);
+ if (row == null) {
+ row = new HashMap<String, Object>();
+ mUpdatedRows.put(rowid, row);
+ }
+ row.put(getColumnNames()[columnIndex], obj);
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns <code>true</code> if there are pending updates that have not yet been committed.
+ *
+ * @return <code>true</code> if there are pending updates that have not yet been committed.
+ * @hide
+ * @deprecated
+ */
+ public boolean hasUpdates() {
+ synchronized(mUpdatedRows) {
+ return mUpdatedRows.size() > 0;
+ }
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public void abortUpdates() {
+ synchronized(mUpdatedRows) {
+ mUpdatedRows.clear();
+ }
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean commitUpdates() {
+ return commitUpdates(null);
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean supportsUpdates() {
+ return mRowIdColumnIndex != -1;
+ }
+
+ public void registerContentObserver(ContentObserver observer) {
+ mContentObservable.registerObserver(observer);
+ }
+
+ public void unregisterContentObserver(ContentObserver observer) {
+ // cursor will unregister all observers when it close
+ if (!mClosed) {
+ mContentObservable.unregisterObserver(observer);
+ }
+ }
+
+ public void registerDataSetObserver(DataSetObserver observer) {
+ mDataSetObservable.registerObserver(observer);
+ }
+
+ public void unregisterDataSetObserver(DataSetObserver observer) {
+ mDataSetObservable.unregisterObserver(observer);
+ }
+
+ /**
+ * Subclasses must call this method when they finish committing updates to notify all
+ * observers.
+ *
+ * @param selfChange
+ */
+ protected void onChange(boolean selfChange) {
+ synchronized (mSelfObserverLock) {
+ mContentObservable.dispatchChange(selfChange);
+ if (mNotifyUri != null && selfChange) {
+ mContentResolver.notifyChange(mNotifyUri, mSelfObserver);
+ }
+ }
+ }
+
+ /**
+ * Specifies a content URI to watch for changes.
+ *
+ * @param cr The content resolver from the caller's context.
+ * @param notifyUri The URI to watch for changes. This can be a
+ * specific row URI, or a base URI for a whole class of content.
+ */
+ public void setNotificationUri(ContentResolver cr, Uri notifyUri) {
+ synchronized (mSelfObserverLock) {
+ mNotifyUri = notifyUri;
+ mContentResolver = cr;
+ if (mSelfObserver != null) {
+ mContentResolver.unregisterContentObserver(mSelfObserver);
+ }
+ mSelfObserver = new SelfContentObserver(this);
+ mContentResolver.registerContentObserver(mNotifyUri, true, mSelfObserver);
+ mSelfObserverRegistered = true;
+ }
+ }
+
+ public boolean getWantsAllOnMoveCalls() {
+ return false;
+ }
+
+ public Bundle getExtras() {
+ return Bundle.EMPTY;
+ }
+
+ public Bundle respond(Bundle extras) {
+ return Bundle.EMPTY;
+ }
+
+ /**
+ * This function returns true if the field has been updated and is
+ * used in conjunction with {@link #getUpdatedField} to allow subclasses to
+ * support reading uncommitted updates. NOTE: This function and
+ * {@link #getUpdatedField} should be called together inside of a
+ * block synchronized on mUpdatedRows.
+ *
+ * @param columnIndex the column index of the field to check
+ * @return true if the field has been updated, false otherwise
+ */
+ protected boolean isFieldUpdated(int columnIndex) {
+ if (mRowIdColumnIndex != -1 && mUpdatedRows.size() > 0) {
+ Map<String, Object> updates = mUpdatedRows.get(mCurrentRowID);
+ if (updates != null && updates.containsKey(getColumnNames()[columnIndex])) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * This function returns the uncommitted updated value for the field
+ * at columnIndex. NOTE: This function and {@link #isFieldUpdated} should
+ * be called together inside of a block synchronized on mUpdatedRows.
+ *
+ * @param columnIndex the column index of the field to retrieve
+ * @return the updated value
+ */
+ protected Object getUpdatedField(int columnIndex) {
+ Map<String, Object> updates = mUpdatedRows.get(mCurrentRowID);
+ return updates.get(getColumnNames()[columnIndex]);
+ }
+
+ /**
+ * This function throws CursorIndexOutOfBoundsException if
+ * the cursor position is out of bounds. Subclass implementations of
+ * the get functions should call this before attempting
+ * to retrieve data.
+ *
+ * @throws CursorIndexOutOfBoundsException
+ */
+ protected void checkPosition() {
+ if (-1 == mPos || getCount() == mPos) {
+ throw new CursorIndexOutOfBoundsException(mPos, getCount());
+ }
+ }
+
+ @Override
+ protected void finalize() {
+ if (mSelfObserver != null && mSelfObserverRegistered == true) {
+ mContentResolver.unregisterContentObserver(mSelfObserver);
+ }
+ }
+
+ /**
+ * Cursors use this class to track changes others make to their URI.
+ */
+ protected static class SelfContentObserver extends ContentObserver {
+ WeakReference<AbstractCursor> mCursor;
+
+ public SelfContentObserver(AbstractCursor cursor) {
+ super(null);
+ mCursor = new WeakReference<AbstractCursor>(cursor);
+ }
+
+ @Override
+ public boolean deliverSelfNotifications() {
+ return false;
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ AbstractCursor cursor = mCursor.get();
+ if (cursor != null) {
+ cursor.onChange(false);
+ }
+ }
+ }
+
+ /**
+ * This HashMap contains a mapping from Long rowIDs to another Map
+ * that maps from String column names to new values. A NULL value means to
+ * remove an existing value, and all numeric values are in their class
+ * forms, i.e. Integer, Long, Float, etc.
+ */
+ protected HashMap<Long, Map<String, Object>> mUpdatedRows;
+
+ /**
+ * This must be set to the index of the row ID column by any
+ * subclass that wishes to support updates.
+ */
+ protected int mRowIdColumnIndex;
+
+ protected int mPos;
+ protected Long mCurrentRowID;
+ protected ContentResolver mContentResolver;
+ protected boolean mClosed = false;
+ private Uri mNotifyUri;
+ private ContentObserver mSelfObserver;
+ final private Object mSelfObserverLock = new Object();
+ private boolean mSelfObserverRegistered;
+}
diff --git a/core/java/android/database/AbstractWindowedCursor.java b/core/java/android/database/AbstractWindowedCursor.java
new file mode 100644
index 0000000..1ec4312
--- /dev/null
+++ b/core/java/android/database/AbstractWindowedCursor.java
@@ -0,0 +1,204 @@
+/*
+ * 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.database;
+
+/**
+ * A base class for Cursors that store their data in {@link CursorWindow}s.
+ */
+public abstract class AbstractWindowedCursor extends AbstractCursor
+{
+ @Override
+ public byte[] getBlob(int columnIndex)
+ {
+ checkPosition();
+
+ synchronized(mUpdatedRows) {
+ if (isFieldUpdated(columnIndex)) {
+ return (byte[])getUpdatedField(columnIndex);
+ }
+ }
+
+ return mWindow.getBlob(mPos, columnIndex);
+ }
+
+ @Override
+ public String getString(int columnIndex)
+ {
+ checkPosition();
+
+ synchronized(mUpdatedRows) {
+ if (isFieldUpdated(columnIndex)) {
+ return (String)getUpdatedField(columnIndex);
+ }
+ }
+
+ return mWindow.getString(mPos, columnIndex);
+ }
+
+ @Override
+ public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer)
+ {
+ checkPosition();
+
+ synchronized(mUpdatedRows) {
+ if (isFieldUpdated(columnIndex)) {
+ super.copyStringToBuffer(columnIndex, buffer);
+ }
+ }
+
+ mWindow.copyStringToBuffer(mPos, columnIndex, buffer);
+ }
+
+ @Override
+ public short getShort(int columnIndex)
+ {
+ checkPosition();
+
+ synchronized(mUpdatedRows) {
+ if (isFieldUpdated(columnIndex)) {
+ Number value = (Number)getUpdatedField(columnIndex);
+ return value.shortValue();
+ }
+ }
+
+ return mWindow.getShort(mPos, columnIndex);
+ }
+
+ @Override
+ public int getInt(int columnIndex)
+ {
+ checkPosition();
+
+ synchronized(mUpdatedRows) {
+ if (isFieldUpdated(columnIndex)) {
+ Number value = (Number)getUpdatedField(columnIndex);
+ return value.intValue();
+ }
+ }
+
+ return mWindow.getInt(mPos, columnIndex);
+ }
+
+ @Override
+ public long getLong(int columnIndex)
+ {
+ checkPosition();
+
+ synchronized(mUpdatedRows) {
+ if (isFieldUpdated(columnIndex)) {
+ Number value = (Number)getUpdatedField(columnIndex);
+ return value.longValue();
+ }
+ }
+
+ return mWindow.getLong(mPos, columnIndex);
+ }
+
+ @Override
+ public float getFloat(int columnIndex)
+ {
+ checkPosition();
+
+ synchronized(mUpdatedRows) {
+ if (isFieldUpdated(columnIndex)) {
+ Number value = (Number)getUpdatedField(columnIndex);
+ return value.floatValue();
+ }
+ }
+
+ return mWindow.getFloat(mPos, columnIndex);
+ }
+
+ @Override
+ public double getDouble(int columnIndex)
+ {
+ checkPosition();
+
+ synchronized(mUpdatedRows) {
+ if (isFieldUpdated(columnIndex)) {
+ Number value = (Number)getUpdatedField(columnIndex);
+ return value.doubleValue();
+ }
+ }
+
+ return mWindow.getDouble(mPos, columnIndex);
+ }
+
+ @Override
+ public boolean isNull(int columnIndex)
+ {
+ checkPosition();
+
+ synchronized(mUpdatedRows) {
+ if (isFieldUpdated(columnIndex)) {
+ return getUpdatedField(columnIndex) == null;
+ }
+ }
+
+ return mWindow.isNull(mPos, columnIndex);
+ }
+
+ public boolean isBlob(int columnIndex)
+ {
+ checkPosition();
+
+ synchronized(mUpdatedRows) {
+ if (isFieldUpdated(columnIndex)) {
+ Object object = getUpdatedField(columnIndex);
+ return object == null || object instanceof byte[];
+ }
+ }
+
+ return mWindow.isBlob(mPos, columnIndex);
+ }
+
+ @Override
+ protected void checkPosition()
+ {
+ super.checkPosition();
+
+ if (mWindow == null) {
+ throw new StaleDataException("This cursor has changed, you must call requery()");
+ }
+ }
+
+ @Override
+ public CursorWindow getWindow() {
+ return mWindow;
+ }
+
+ /**
+ * Set a new cursor window to cursor, usually set a remote cursor window
+ * @param window cursor window
+ */
+ public void setWindow(CursorWindow window) {
+ if (mWindow != null) {
+ mWindow.close();
+ }
+ mWindow = window;
+ }
+
+ public boolean hasWindow() {
+ return mWindow != null;
+ }
+
+ /**
+ * This needs be updated in {@link #onMove} by subclasses, and
+ * needs to be set to NULL when the contents of the cursor change.
+ */
+ protected CursorWindow mWindow;
+}
diff --git a/core/java/android/database/BulkCursorNative.java b/core/java/android/database/BulkCursorNative.java
new file mode 100644
index 0000000..baa94d8
--- /dev/null
+++ b/core/java/android/database/BulkCursorNative.java
@@ -0,0 +1,440 @@
+/*
+ * 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.database;
+
+import android.os.Binder;
+import android.os.RemoteException;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Bundle;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Native implementation of the bulk cursor. This is only for use in implementing
+ * IPC, application code should use the Cursor interface.
+ *
+ * {@hide}
+ */
+public abstract class BulkCursorNative extends Binder implements IBulkCursor
+{
+ public BulkCursorNative()
+ {
+ attachInterface(this, descriptor);
+ }
+
+ /**
+ * Cast a Binder object into a content resolver interface, generating
+ * a proxy if needed.
+ */
+ static public IBulkCursor asInterface(IBinder obj)
+ {
+ if (obj == null) {
+ return null;
+ }
+ IBulkCursor in = (IBulkCursor)obj.queryLocalInterface(descriptor);
+ if (in != null) {
+ return in;
+ }
+
+ return new BulkCursorProxy(obj);
+ }
+
+ @Override
+ public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
+ throws RemoteException {
+ try {
+ switch (code) {
+ case GET_CURSOR_WINDOW_TRANSACTION: {
+ data.enforceInterface(IBulkCursor.descriptor);
+ int startPos = data.readInt();
+ CursorWindow window = getWindow(startPos);
+ if (window == null) {
+ reply.writeInt(0);
+ return true;
+ }
+ reply.writeNoException();
+ reply.writeInt(1);
+ window.writeToParcel(reply, 0);
+ return true;
+ }
+
+ case COUNT_TRANSACTION: {
+ data.enforceInterface(IBulkCursor.descriptor);
+ int count = count();
+ reply.writeNoException();
+ reply.writeInt(count);
+ return true;
+ }
+
+ case GET_COLUMN_NAMES_TRANSACTION: {
+ data.enforceInterface(IBulkCursor.descriptor);
+ String[] columnNames = getColumnNames();
+ reply.writeNoException();
+ reply.writeInt(columnNames.length);
+ int length = columnNames.length;
+ for (int i = 0; i < length; i++) {
+ reply.writeString(columnNames[i]);
+ }
+ return true;
+ }
+
+ case DEACTIVATE_TRANSACTION: {
+ data.enforceInterface(IBulkCursor.descriptor);
+ deactivate();
+ reply.writeNoException();
+ return true;
+ }
+
+ case CLOSE_TRANSACTION: {
+ data.enforceInterface(IBulkCursor.descriptor);
+ close();
+ reply.writeNoException();
+ return true;
+ }
+
+ case REQUERY_TRANSACTION: {
+ data.enforceInterface(IBulkCursor.descriptor);
+ IContentObserver observer =
+ IContentObserver.Stub.asInterface(data.readStrongBinder());
+ CursorWindow window = CursorWindow.CREATOR.createFromParcel(data);
+ int count = requery(observer, window);
+ reply.writeNoException();
+ reply.writeInt(count);
+ reply.writeBundle(getExtras());
+ return true;
+ }
+
+ case UPDATE_ROWS_TRANSACTION: {
+ data.enforceInterface(IBulkCursor.descriptor);
+ // TODO - what ClassLoader should be passed to readHashMap?
+ // TODO - switch to Bundle
+ HashMap<Long, Map<String, Object>> values = data.readHashMap(null);
+ boolean result = updateRows(values);
+ reply.writeNoException();
+ reply.writeInt((result == true ? 1 : 0));
+ return true;
+ }
+
+ case DELETE_ROW_TRANSACTION: {
+ data.enforceInterface(IBulkCursor.descriptor);
+ int position = data.readInt();
+ boolean result = deleteRow(position);
+ reply.writeNoException();
+ reply.writeInt((result == true ? 1 : 0));
+ return true;
+ }
+
+ case ON_MOVE_TRANSACTION: {
+ data.enforceInterface(IBulkCursor.descriptor);
+ int position = data.readInt();
+ onMove(position);
+ reply.writeNoException();
+ return true;
+ }
+
+ case WANTS_ON_MOVE_TRANSACTION: {
+ data.enforceInterface(IBulkCursor.descriptor);
+ boolean result = getWantsAllOnMoveCalls();
+ reply.writeNoException();
+ reply.writeInt(result ? 1 : 0);
+ return true;
+ }
+
+ case GET_EXTRAS_TRANSACTION: {
+ data.enforceInterface(IBulkCursor.descriptor);
+ Bundle extras = getExtras();
+ reply.writeNoException();
+ reply.writeBundle(extras);
+ return true;
+ }
+
+ case RESPOND_TRANSACTION: {
+ data.enforceInterface(IBulkCursor.descriptor);
+ Bundle extras = data.readBundle();
+ Bundle returnExtras = respond(extras);
+ reply.writeNoException();
+ reply.writeBundle(returnExtras);
+ return true;
+ }
+ }
+ } catch (Exception e) {
+ DatabaseUtils.writeExceptionToParcel(reply, e);
+ return true;
+ }
+
+ return super.onTransact(code, data, reply, flags);
+ }
+
+ public IBinder asBinder()
+ {
+ return this;
+ }
+}
+
+
+final class BulkCursorProxy implements IBulkCursor {
+ private IBinder mRemote;
+ private Bundle mExtras;
+
+ public BulkCursorProxy(IBinder remote)
+ {
+ mRemote = remote;
+ mExtras = null;
+ }
+
+ public IBinder asBinder()
+ {
+ return mRemote;
+ }
+
+ public CursorWindow getWindow(int startPos) throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+
+ data.writeInterfaceToken(IBulkCursor.descriptor);
+
+ data.writeInt(startPos);
+
+ mRemote.transact(GET_CURSOR_WINDOW_TRANSACTION, data, reply, 0);
+
+ DatabaseUtils.readExceptionFromParcel(reply);
+
+ CursorWindow window = null;
+ if (reply.readInt() == 1) {
+ window = CursorWindow.newFromParcel(reply);
+ }
+
+ data.recycle();
+ reply.recycle();
+
+ return window;
+ }
+
+ public void onMove(int position) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+
+ data.writeInterfaceToken(IBulkCursor.descriptor);
+
+ data.writeInt(position);
+
+ mRemote.transact(ON_MOVE_TRANSACTION, data, reply, 0);
+
+ DatabaseUtils.readExceptionFromParcel(reply);
+
+ data.recycle();
+ reply.recycle();
+ }
+
+ public int count() throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+
+ data.writeInterfaceToken(IBulkCursor.descriptor);
+
+ boolean result = mRemote.transact(COUNT_TRANSACTION, data, reply, 0);
+
+ DatabaseUtils.readExceptionFromParcel(reply);
+
+ int count;
+ if (result == false) {
+ count = -1;
+ } else {
+ count = reply.readInt();
+ }
+ data.recycle();
+ reply.recycle();
+ return count;
+ }
+
+ public String[] getColumnNames() throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+
+ data.writeInterfaceToken(IBulkCursor.descriptor);
+
+ mRemote.transact(GET_COLUMN_NAMES_TRANSACTION, data, reply, 0);
+
+ DatabaseUtils.readExceptionFromParcel(reply);
+
+ String[] columnNames = null;
+ int numColumns = reply.readInt();
+ columnNames = new String[numColumns];
+ for (int i = 0; i < numColumns; i++) {
+ columnNames[i] = reply.readString();
+ }
+
+ data.recycle();
+ reply.recycle();
+ return columnNames;
+ }
+
+ public void deactivate() throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+
+ data.writeInterfaceToken(IBulkCursor.descriptor);
+
+ mRemote.transact(DEACTIVATE_TRANSACTION, data, reply, 0);
+ DatabaseUtils.readExceptionFromParcel(reply);
+
+ data.recycle();
+ reply.recycle();
+ }
+
+ public void close() throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+
+ data.writeInterfaceToken(IBulkCursor.descriptor);
+
+ mRemote.transact(CLOSE_TRANSACTION, data, reply, 0);
+ DatabaseUtils.readExceptionFromParcel(reply);
+
+ data.recycle();
+ reply.recycle();
+ }
+
+ public int requery(IContentObserver observer, CursorWindow window) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+
+ data.writeInterfaceToken(IBulkCursor.descriptor);
+
+ data.writeStrongInterface(observer);
+ window.writeToParcel(data, 0);
+
+ boolean result = mRemote.transact(REQUERY_TRANSACTION, data, reply, 0);
+
+ DatabaseUtils.readExceptionFromParcel(reply);
+
+ int count;
+ if (!result) {
+ count = -1;
+ } else {
+ count = reply.readInt();
+ mExtras = reply.readBundle();
+ }
+
+ data.recycle();
+ reply.recycle();
+
+ return count;
+ }
+
+ public boolean updateRows(Map values) throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+
+ data.writeInterfaceToken(IBulkCursor.descriptor);
+
+ data.writeMap(values);
+
+ mRemote.transact(UPDATE_ROWS_TRANSACTION, data, reply, 0);
+
+ DatabaseUtils.readExceptionFromParcel(reply);
+
+ boolean result = (reply.readInt() == 1 ? true : false);
+
+ data.recycle();
+ reply.recycle();
+
+ return result;
+ }
+
+ public boolean deleteRow(int position) throws RemoteException
+ {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+
+ data.writeInterfaceToken(IBulkCursor.descriptor);
+
+ data.writeInt(position);
+
+ mRemote.transact(DELETE_ROW_TRANSACTION, data, reply, 0);
+
+ DatabaseUtils.readExceptionFromParcel(reply);
+
+ boolean result = (reply.readInt() == 1 ? true : false);
+
+ data.recycle();
+ reply.recycle();
+
+ return result;
+ }
+
+ public boolean getWantsAllOnMoveCalls() throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+
+ data.writeInterfaceToken(IBulkCursor.descriptor);
+
+ mRemote.transact(WANTS_ON_MOVE_TRANSACTION, data, reply, 0);
+
+ DatabaseUtils.readExceptionFromParcel(reply);
+
+ int result = reply.readInt();
+ data.recycle();
+ reply.recycle();
+ return result != 0;
+ }
+
+ public Bundle getExtras() throws RemoteException {
+ if (mExtras == null) {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+
+ data.writeInterfaceToken(IBulkCursor.descriptor);
+
+ mRemote.transact(GET_EXTRAS_TRANSACTION, data, reply, 0);
+
+ DatabaseUtils.readExceptionFromParcel(reply);
+
+ mExtras = reply.readBundle();
+ data.recycle();
+ reply.recycle();
+ }
+ return mExtras;
+ }
+
+ public Bundle respond(Bundle extras) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+
+ data.writeInterfaceToken(IBulkCursor.descriptor);
+
+ data.writeBundle(extras);
+
+ mRemote.transact(RESPOND_TRANSACTION, data, reply, 0);
+
+ DatabaseUtils.readExceptionFromParcel(reply);
+
+ Bundle returnExtras = reply.readBundle();
+ data.recycle();
+ reply.recycle();
+ return returnExtras;
+ }
+}
+
diff --git a/core/java/android/database/BulkCursorToCursorAdaptor.java b/core/java/android/database/BulkCursorToCursorAdaptor.java
new file mode 100644
index 0000000..c26810a
--- /dev/null
+++ b/core/java/android/database/BulkCursorToCursorAdaptor.java
@@ -0,0 +1,256 @@
+/*
+ * 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.database;
+
+import android.os.RemoteException;
+import android.os.Bundle;
+import android.util.Log;
+
+import java.util.Map;
+
+/**
+ * Adapts an {@link IBulkCursor} to a {@link Cursor} for use in the local
+ * process.
+ *
+ * {@hide}
+ */
+public final class BulkCursorToCursorAdaptor extends AbstractWindowedCursor {
+ private static final String TAG = "BulkCursor";
+
+ private SelfContentObserver mObserverBridge;
+ private IBulkCursor mBulkCursor;
+ private int mCount;
+ private String[] mColumns;
+ private boolean mWantsAllOnMoveCalls;
+
+ public void set(IBulkCursor bulkCursor) {
+ mBulkCursor = bulkCursor;
+
+ try {
+ mCount = mBulkCursor.count();
+ mWantsAllOnMoveCalls = mBulkCursor.getWantsAllOnMoveCalls();
+
+ // Search for the rowID column index and set it for our parent
+ mColumns = mBulkCursor.getColumnNames();
+ int length = mColumns.length;
+ for (int i = 0; i < length; i++) {
+ if (mColumns[i].equals("_id")) {
+ mRowIdColumnIndex = i;
+ break;
+ }
+ }
+ } catch (RemoteException ex) {
+ Log.e(TAG, "Setup failed because the remote process is dead");
+ }
+ }
+
+ /**
+ * Gets a SelfDataChangeOberserver that can be sent to a remote
+ * process to receive change notifications over IPC.
+ *
+ * @return A SelfContentObserver hooked up to this Cursor
+ */
+ public synchronized IContentObserver getObserver() {
+ if (mObserverBridge == null) {
+ mObserverBridge = new SelfContentObserver(this);
+ }
+ return mObserverBridge.getContentObserver();
+ }
+
+ @Override
+ public int getCount() {
+ return mCount;
+ }
+
+ @Override
+ public boolean onMove(int oldPosition, int newPosition) {
+ try {
+ // Make sure we have the proper window
+ if (mWindow != null) {
+ if (newPosition < mWindow.getStartPosition() ||
+ newPosition >= (mWindow.getStartPosition() + mWindow.getNumRows())) {
+ mWindow = mBulkCursor.getWindow(newPosition);
+ } else if (mWantsAllOnMoveCalls) {
+ mBulkCursor.onMove(newPosition);
+ }
+ } else {
+ mWindow = mBulkCursor.getWindow(newPosition);
+ }
+ } catch (RemoteException ex) {
+ // We tried to get a window and failed
+ Log.e(TAG, "Unable to get window because the remote process is dead");
+ return false;
+ }
+
+ // Couldn't obtain a window, something is wrong
+ if (mWindow == null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public void deactivate() {
+ // This will call onInvalidated(), so make sure to do it before calling release,
+ // which is what actually makes the data set invalid.
+ super.deactivate();
+
+ try {
+ mBulkCursor.deactivate();
+ } catch (RemoteException ex) {
+ Log.w(TAG, "Remote process exception when deactivating");
+ }
+ mWindow = null;
+ }
+
+ @Override
+ public void close() {
+ super.close();
+ try {
+ mBulkCursor.close();
+ } catch (RemoteException ex) {
+ Log.w(TAG, "Remote process exception when closing");
+ }
+ mWindow = null;
+ }
+
+ @Override
+ public boolean requery() {
+ try {
+ int oldCount = mCount;
+ //TODO get the window from a pool somewhere to avoid creating the memory dealer
+ mCount = mBulkCursor.requery(getObserver(), new CursorWindow(
+ false /* the window will be accessed across processes */));
+ if (mCount != -1) {
+ mPos = -1;
+ mWindow = null;
+
+ // super.requery() will call onChanged. Do it here instead of relying on the
+ // observer from the far side so that observers can see a correct value for mCount
+ // when responding to onChanged.
+ super.requery();
+ return true;
+ } else {
+ deactivate();
+ return false;
+ }
+ } catch (Exception ex) {
+ Log.e(TAG, "Unable to requery because the remote process exception " + ex.getMessage());
+ deactivate();
+ return false;
+ }
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ @Override
+ public boolean deleteRow() {
+ try {
+ boolean result = mBulkCursor.deleteRow(mPos);
+ if (result != false) {
+ // The window contains the old value, discard it
+ mWindow = null;
+
+ // Fix up the position
+ mCount = mBulkCursor.count();
+ if (mPos < mCount) {
+ int oldPos = mPos;
+ mPos = -1;
+ moveToPosition(oldPos);
+ } else {
+ mPos = mCount;
+ }
+
+ // Send the change notification
+ onChange(true);
+ }
+ return result;
+ } catch (RemoteException ex) {
+ Log.e(TAG, "Unable to delete row because the remote process is dead");
+ return false;
+ }
+ }
+
+ @Override
+ public String[] getColumnNames() {
+ return mColumns;
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ @Override
+ public boolean commitUpdates(Map<? extends Long,
+ ? extends Map<String,Object>> additionalValues) {
+ if (!supportsUpdates()) {
+ Log.e(TAG, "commitUpdates not supported on this cursor, did you include the _id column?");
+ return false;
+ }
+
+ synchronized(mUpdatedRows) {
+ if (additionalValues != null) {
+ mUpdatedRows.putAll(additionalValues);
+ }
+
+ if (mUpdatedRows.size() <= 0) {
+ return false;
+ }
+
+ try {
+ boolean result = mBulkCursor.updateRows(mUpdatedRows);
+
+ if (result == true) {
+ mUpdatedRows.clear();
+
+ // Send the change notification
+ onChange(true);
+ }
+ return result;
+ } catch (RemoteException ex) {
+ Log.e(TAG, "Unable to commit updates because the remote process is dead");
+ return false;
+ }
+ }
+ }
+
+ @Override
+ public Bundle getExtras() {
+ try {
+ return mBulkCursor.getExtras();
+ } catch (RemoteException e) {
+ // This should never happen because the system kills processes that are using remote
+ // cursors when the provider process is killed.
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public Bundle respond(Bundle extras) {
+ try {
+ return mBulkCursor.respond(extras);
+ } catch (RemoteException e) {
+ // This should never happen because the system kills processes that are using remote
+ // cursors when the provider process is killed.
+ throw new RuntimeException(e);
+ }
+ }
+}
+
diff --git a/core/java/android/database/CharArrayBuffer.java b/core/java/android/database/CharArrayBuffer.java
new file mode 100644
index 0000000..73781b7
--- /dev/null
+++ b/core/java/android/database/CharArrayBuffer.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.database;
+
+/**
+ * This is used for {@link Cursor#copyStringToBuffer}
+ */
+public final class CharArrayBuffer {
+ public CharArrayBuffer(int size) {
+ data = new char[size];
+ }
+
+ public CharArrayBuffer(char[] buf) {
+ data = buf;
+ }
+
+ public char[] data; // In and out parameter
+ public int sizeCopied; // Out parameter
+}
diff --git a/core/java/android/database/ContentObservable.java b/core/java/android/database/ContentObservable.java
new file mode 100644
index 0000000..8d7b7c5
--- /dev/null
+++ b/core/java/android/database/ContentObservable.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.database;
+
+/**
+ * A specialization of Observable for ContentObserver that provides methods for
+ * invoking the various callback methods of ContentObserver.
+ */
+public class ContentObservable extends Observable<ContentObserver> {
+
+ @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
+ */
+ public void dispatchChange(boolean selfChange) {
+ synchronized(mObservers) {
+ for (ContentObserver observer : mObservers) {
+ if (!selfChange || observer.deliverSelfNotifications()) {
+ observer.dispatchChange(selfChange);
+ }
+ }
+ }
+ }
+
+ /**
+ * invokes onChange on each observer
+ * @param selfChange
+ */
+ public void notifyChange(boolean selfChange) {
+ synchronized(mObservers) {
+ for (ContentObserver observer : mObservers) {
+ observer.onChange(selfChange);
+ }
+ }
+ }
+}
diff --git a/core/java/android/database/ContentObserver.java b/core/java/android/database/ContentObserver.java
new file mode 100644
index 0000000..3b829a3
--- /dev/null
+++ b/core/java/android/database/ContentObserver.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.database;
+
+import android.os.Handler;
+
+/**
+ * Receives call backs for changes to content. Must be implemented by objects which are added
+ * to a {@link ContentObservable}.
+ */
+public abstract class ContentObserver {
+
+ 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;
+ }
+ }
+
+ /**
+ * onChange() will happen on the provider Handler.
+ *
+ * @param handler The handler to run {@link #onChange} on.
+ */
+ public ContentObserver(Handler handler) {
+ mHandler = handler;
+ }
+
+ /**
+ * Gets access to the binder transport object. Not for public consumption.
+ *
+ * {@hide}
+ */
+ public IContentObserver getContentObserver() {
+ synchronized(lock) {
+ if (mTransport == null) {
+ mTransport = new Transport(this);
+ }
+ return mTransport;
+ }
+ }
+
+ /**
+ * Gets access to the binder transport object, and unlinks the transport object
+ * from the ContentObserver. Not for public consumption.
+ *
+ * {@hide}
+ */
+ public IContentObserver releaseContentObserver() {
+ synchronized(lock) {
+ Transport oldTransport = mTransport;
+ if (oldTransport != null) {
+ oldTransport.releaseContentObserver();
+ mTransport = null;
+ }
+ return oldTransport;
+ }
+ }
+
+ /**
+ * Returns true if this observer is interested in notifications for changes
+ * made through the cursor the observer is registered with.
+ */
+ 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.
+ */
+ public void onChange(boolean selfChange) {}
+
+ public final void dispatchChange(boolean selfChange) {
+ if (mHandler == null) {
+ onChange(selfChange);
+ } else {
+ mHandler.post(new NotificationRunnable(selfChange));
+ }
+ }
+}
diff --git a/core/java/android/database/CrossProcessCursor.java b/core/java/android/database/CrossProcessCursor.java
new file mode 100644
index 0000000..77ba3a5
--- /dev/null
+++ b/core/java/android/database/CrossProcessCursor.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.database;
+
+public interface CrossProcessCursor extends Cursor{
+ /**
+ * returns a pre-filled window, return NULL if no such window
+ */
+ CursorWindow getWindow();
+
+ /**
+ * copies cursor data into the window start at pos
+ */
+ void fillWindow(int pos, CursorWindow winow);
+
+ /**
+ * This function is called every time the cursor is successfully scrolled
+ * to a new position, giving the subclass a chance to update any state it
+ * may have. If it returns false the move function will also do so and the
+ * cursor will scroll to the beforeFirst position.
+ *
+ * @param oldPosition the position that we're moving from
+ * @param newPosition the position that we're moving to
+ * @return true if the move is successful, false otherwise
+ */
+ boolean onMove(int oldPosition, int newPosition);
+
+}
diff --git a/core/java/android/database/Cursor.java b/core/java/android/database/Cursor.java
new file mode 100644
index 0000000..79178f4
--- /dev/null
+++ b/core/java/android/database/Cursor.java
@@ -0,0 +1,587 @@
+/*
+ * 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.database;
+
+import android.content.ContentResolver;
+import android.net.Uri;
+import android.os.Bundle;
+
+import java.util.Map;
+
+/**
+ * This interface provides random read-write access to the result set returned
+ * by a database query.
+ */
+public interface Cursor {
+ /**
+ * Returns the numbers of rows in the cursor.
+ *
+ * @return the number of rows in the cursor.
+ */
+ int getCount();
+
+ /**
+ * Returns the current position of the cursor in the row set.
+ * The value is zero-based. When the row set is first returned the cursor
+ * will be at positon -1, which is before the first row. After the
+ * last row is returned another call to next() will leave the cursor past
+ * the last entry, at a position of count().
+ *
+ * @return the current cursor position.
+ */
+ int getPosition();
+
+ /**
+ * Move the cursor by a relative amount, forward or backward, from the
+ * current position. Positive offsets move forwards, negative offsets move
+ * backwards. If the final position is outside of the bounds of the result
+ * set then the resultant position will be pinned to -1 or count() depending
+ * on whether the value is off the front or end of the set, respectively.
+ *
+ * <p>This method will return true if the requested destination was
+ * reachable, otherwise, it returns false. For example, if the cursor is at
+ * currently on the second entry in the result set and move(-5) is called,
+ * the position will be pinned at -1, and false will be returned.
+ *
+ * @param offset the offset to be applied from the current position.
+ * @return whether the requested move fully succeeded.
+ */
+ boolean move(int offset);
+
+ /**
+ * Move the cursor to an absolute position. The valid
+ * range of values is -1 &lt;= position &lt;= count.
+ *
+ * <p>This method will return true if the request destination was reachable,
+ * otherwise, it returns false.
+ *
+ * @param position the zero-based position to move to.
+ * @return whether the requested move fully succeeded.
+ */
+ boolean moveToPosition(int position);
+
+ /**
+ * Move the cursor to the first row.
+ *
+ * <p>This method will return false if the cursor is empty.
+ *
+ * @return whether the move succeeded.
+ */
+ boolean moveToFirst();
+
+ /**
+ * Move the cursor to the last row.
+ *
+ * <p>This method will return false if the cursor is empty.
+ *
+ * @return whether the move succeeded.
+ */
+ boolean moveToLast();
+
+ /**
+ * Move the cursor to the next row.
+ *
+ * <p>This method will return false if the cursor is already past the
+ * last entry in the result set.
+ *
+ * @return whether the move succeeded.
+ */
+ boolean moveToNext();
+
+ /**
+ * Move the cursor to the previous row.
+ *
+ * <p>This method will return false if the cursor is already before the
+ * first entry in the result set.
+ *
+ * @return whether the move succeeded.
+ */
+ boolean moveToPrevious();
+
+ /**
+ * Returns whether the cursor is pointing to the first row.
+ *
+ * @return whether the cursor is pointing at the first entry.
+ */
+ boolean isFirst();
+
+ /**
+ * Returns whether the cursor is pointing to the last row.
+ *
+ * @return whether the cursor is pointing at the last entry.
+ */
+ boolean isLast();
+
+ /**
+ * Returns whether the cursor is pointing to the position before the first
+ * row.
+ *
+ * @return whether the cursor is before the first result.
+ */
+ boolean isBeforeFirst();
+
+ /**
+ * Returns whether the cursor is pointing to the position after the last
+ * row.
+ *
+ * @return whether the cursor is after the last result.
+ */
+ boolean isAfterLast();
+
+ /**
+ * Removes the row at the current cursor position from the underlying data
+ * store. After this method returns the cursor will be pointing to the row
+ * after the row that is deleted. This has the side effect of decrementing
+ * the result of count() by one.
+ * <p>
+ * The query must have the row ID column in its selection, otherwise this
+ * call will fail.
+ *
+ * @hide
+ * @return whether the record was successfully deleted.
+ * @deprecated use {@link ContentResolver#delete(Uri, String, String[])}
+ */
+ @Deprecated
+ boolean deleteRow();
+
+ /**
+ * Returns the zero-based index for the given column name, or -1 if the column doesn't exist.
+ * If you expect the column to exist use {@link #getColumnIndexOrThrow(String)} instead, which
+ * will make the error more clear.
+ *
+ * @param columnName the name of the target column.
+ * @return the zero-based column index for the given column name, or -1 if
+ * the column name does not exist.
+ * @see #getColumnIndexOrThrow(String)
+ */
+ int getColumnIndex(String columnName);
+
+ /**
+ * Returns the zero-based index for the given column name, or throws
+ * {@link IllegalArgumentException} if the column doesn't exist. If you're not sure if
+ * a column will exist or not use {@link #getColumnIndex(String)} and check for -1, which
+ * is more efficient than catching the exceptions.
+ *
+ * @param columnName the name of the target column.
+ * @return the zero-based column index for the given column name
+ * @see #getColumnIndex(String)
+ * @throws IllegalArgumentException if the column does not exist
+ */
+ int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException;
+
+ /**
+ * Returns the column name at the given zero-based column index.
+ *
+ * @param columnIndex the zero-based index of the target column.
+ * @return the column name for the given column index.
+ */
+ String getColumnName(int columnIndex);
+
+ /**
+ * Returns a string array holding the names of all of the columns in the
+ * result set in the order in which they were listed in the result.
+ *
+ * @return the names of the columns returned in this query.
+ */
+ String[] getColumnNames();
+
+ /**
+ * Return total number of columns
+ * @return number of columns
+ */
+ int getColumnCount();
+
+ /**
+ * Returns the value of the requested column as a byte array.
+ *
+ * <p>If the native content of that column is not blob exception may throw
+ *
+ * @param columnIndex the zero-based index of the target column.
+ * @return the value of that column as a byte array.
+ */
+ byte[] getBlob(int columnIndex);
+
+ /**
+ * Returns the value of the requested column as a String.
+ *
+ * <p>If the native content of that column is not text the result will be
+ * the result of passing the column value to String.valueOf(x).
+ *
+ * @param columnIndex the zero-based index of the target column.
+ * @return the value of that column as a String.
+ */
+ String getString(int columnIndex);
+
+ /**
+ * Retrieves the requested column text and stores it in the buffer provided.
+ * If the buffer size is not sufficient, a new char buffer will be allocated
+ * and assigned to CharArrayBuffer.data
+ * @param columnIndex the zero-based index of the target column.
+ * if the target column is null, return buffer
+ * @param buffer the buffer to copy the text into.
+ */
+ void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer);
+
+ /**
+ * Returns the value of the requested column as a short.
+ *
+ * <p>If the native content of that column is not numeric the result will be
+ * the result of passing the column value to Short.valueOf(x).
+ *
+ * @param columnIndex the zero-based index of the target column.
+ * @return the value of that column as a short.
+ */
+ short getShort(int columnIndex);
+
+ /**
+ * Returns the value of the requested column as an int.
+ *
+ * <p>If the native content of that column is not numeric the result will be
+ * the result of passing the column value to Integer.valueOf(x).
+ *
+ * @param columnIndex the zero-based index of the target column.
+ * @return the value of that column as an int.
+ */
+ int getInt(int columnIndex);
+
+ /**
+ * Returns the value of the requested column as a long.
+ *
+ * <p>If the native content of that column is not numeric the result will be
+ * the result of passing the column value to Long.valueOf(x).
+ *
+ * @param columnIndex the zero-based index of the target column.
+ * @return the value of that column as a long.
+ */
+ long getLong(int columnIndex);
+
+ /**
+ * Returns the value of the requested column as a float.
+ *
+ * <p>If the native content of that column is not numeric the result will be
+ * the result of passing the column value to Float.valueOf(x).
+ *
+ * @param columnIndex the zero-based index of the target column.
+ * @return the value of that column as a float.
+ */
+ float getFloat(int columnIndex);
+
+ /**
+ * Returns the value of the requested column as a double.
+ *
+ * <p>If the native content of that column is not numeric the result will be
+ * the result of passing the column value to Double.valueOf(x).
+ *
+ * @param columnIndex the zero-based index of the target column.
+ * @return the value of that column as a double.
+ */
+ double getDouble(int columnIndex);
+
+ /**
+ * Returns <code>true</code> if the value in the indicated column is null.
+ *
+ * @param columnIndex the zero-based index of the target column.
+ * @return whether the column value is null.
+ */
+ boolean isNull(int columnIndex);
+
+ /**
+ * Returns <code>true</code> if the cursor supports updates.
+ *
+ * @return whether the cursor supports updates.
+ * @hide
+ * @deprecated use the {@link ContentResolver} update methods instead of the Cursor
+ * update methods
+ */
+ @Deprecated
+ boolean supportsUpdates();
+
+ /**
+ * Returns <code>true</code> if there are pending updates that have not yet been committed.
+ *
+ * @return <code>true</code> if there are pending updates that have not yet been committed.
+ * @hide
+ * @deprecated use the {@link ContentResolver} update methods instead of the Cursor
+ * update methods
+ */
+ @Deprecated
+ boolean hasUpdates();
+
+ /**
+ * Updates the value for the given column in the row the cursor is
+ * currently pointing at. Updates are not committed to the backing store
+ * until {@link #commitUpdates()} is called.
+ *
+ * @param columnIndex the zero-based index of the target column.
+ * @param value the new value.
+ * @return whether the operation succeeded.
+ * @hide
+ * @deprecated use the {@link ContentResolver} update methods instead of the Cursor
+ * update methods
+ */
+ @Deprecated
+ boolean updateBlob(int columnIndex, byte[] value);
+
+ /**
+ * Updates the value for the given column in the row the cursor is
+ * currently pointing at. Updates are not committed to the backing store
+ * until {@link #commitUpdates()} is called.
+ *
+ * @param columnIndex the zero-based index of the target column.
+ * @param value the new value.
+ * @return whether the operation succeeded.
+ * @hide
+ * @deprecated use the {@link ContentResolver} update methods instead of the Cursor
+ * update methods
+ */
+ @Deprecated
+ boolean updateString(int columnIndex, String value);
+
+ /**
+ * Updates the value for the given column in the row the cursor is
+ * currently pointing at. Updates are not committed to the backing store
+ * until {@link #commitUpdates()} is called.
+ *
+ * @param columnIndex the zero-based index of the target column.
+ * @param value the new value.
+ * @return whether the operation succeeded.
+ * @hide
+ * @deprecated use the {@link ContentResolver} update methods instead of the Cursor
+ * update methods
+ */
+ @Deprecated
+ boolean updateShort(int columnIndex, short value);
+
+ /**
+ * Updates the value for the given column in the row the cursor is
+ * currently pointing at. Updates are not committed to the backing store
+ * until {@link #commitUpdates()} is called.
+ *
+ * @param columnIndex the zero-based index of the target column.
+ * @param value the new value.
+ * @return whether the operation succeeded.
+ * @hide
+ * @deprecated use the {@link ContentResolver} update methods instead of the Cursor
+ * update methods
+ */
+ @Deprecated
+ boolean updateInt(int columnIndex, int value);
+
+ /**
+ * Updates the value for the given column in the row the cursor is
+ * currently pointing at. Updates are not committed to the backing store
+ * until {@link #commitUpdates()} is called.
+ *
+ * @param columnIndex the zero-based index of the target column.
+ * @param value the new value.
+ * @return whether the operation succeeded.
+ * @hide
+ * @deprecated use the {@link ContentResolver} update methods instead of the Cursor
+ * update methods
+ */
+ @Deprecated
+ boolean updateLong(int columnIndex, long value);
+
+ /**
+ * Updates the value for the given column in the row the cursor is
+ * currently pointing at. Updates are not committed to the backing store
+ * until {@link #commitUpdates()} is called.
+ *
+ * @param columnIndex the zero-based index of the target column.
+ * @param value the new value.
+ * @return whether the operation succeeded.
+ * @hide
+ * @deprecated use the {@link ContentResolver} update methods instead of the Cursor
+ * update methods
+ */
+ @Deprecated
+ boolean updateFloat(int columnIndex, float value);
+
+ /**
+ * Updates the value for the given column in the row the cursor is
+ * currently pointing at. Updates are not committed to the backing store
+ * until {@link #commitUpdates()} is called.
+ *
+ * @param columnIndex the zero-based index of the target column.
+ * @param value the new value.
+ * @return whether the operation succeeded.
+ * @hide
+ * @deprecated use the {@link ContentResolver} update methods instead of the Cursor
+ * update methods
+ */
+ @Deprecated
+ boolean updateDouble(int columnIndex, double value);
+
+ /**
+ * Removes the value for the given column in the row the cursor is
+ * currently pointing at. Updates are not committed to the backing store
+ * until {@link #commitUpdates()} is called.
+ *
+ * @param columnIndex the zero-based index of the target column.
+ * @return whether the operation succeeded.
+ * @hide
+ * @deprecated use the {@link ContentResolver} update methods instead of the Cursor
+ * update methods
+ */
+ @Deprecated
+ boolean updateToNull(int columnIndex);
+
+ /**
+ * Atomically commits all updates to the backing store. After completion,
+ * this method leaves the data in an inconsistent state and you should call
+ * {@link #requery} before reading data from the cursor again.
+ *
+ * @return whether the operation succeeded.
+ * @hide
+ * @deprecated use the {@link ContentResolver} update methods instead of the Cursor
+ * update methods
+ */
+ @Deprecated
+ boolean commitUpdates();
+
+ /**
+ * Atomically commits all updates to the backing store, as well as the
+ * updates included in values. After completion,
+ * this method leaves the data in an inconsistent state and you should call
+ * {@link #requery} before reading data from the cursor again.
+ *
+ * @param values A map from row IDs to Maps associating column names with
+ * updated values. A null value indicates the field should be
+ removed.
+ * @return whether the operation succeeded.
+ * @hide
+ * @deprecated use the {@link ContentResolver} update methods instead of the Cursor
+ * update methods
+ */
+ @Deprecated
+ boolean commitUpdates(Map<? extends Long,
+ ? extends Map<String,Object>> values);
+
+ /**
+ * Reverts all updates made to the cursor since the last call to
+ * commitUpdates.
+ * @hide
+ * @deprecated use the {@link ContentResolver} update methods instead of the Cursor
+ * update methods
+ */
+ @Deprecated
+ void abortUpdates();
+
+ /**
+ * 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.
+ */
+ void deactivate();
+
+ /**
+ * Performs the query that created the cursor again, refreshing its
+ * contents. This may be done at any time, including after a call to {@link
+ * #deactivate}.
+ *
+ * @return true if the requery succeeded, false if not, in which case the
+ * cursor becomes invalid.
+ */
+ boolean requery();
+
+ /**
+ * Closes the Cursor, releasing all of its resources and making it completely invalid.
+ * Unlike {@link #deactivate()} a call to {@link #requery()} will not make the Cursor valid
+ * again.
+ */
+ void close();
+
+ /**
+ * return true if the cursor is closed
+ * @return true if the cursor is closed.
+ */
+ boolean isClosed();
+
+ /**
+ * Register an observer that is called when changes happen to the content backing this cursor.
+ * Typically the data set won't change until {@link #requery()} is called.
+ *
+ * @param observer the object that gets notified when the content backing the cursor changes.
+ * @see #unregisterContentObserver(ContentObserver)
+ */
+ void registerContentObserver(ContentObserver observer);
+
+ /**
+ * Unregister an observer that has previously been registered with this
+ * cursor via {@link #registerContentObserver}.
+ *
+ * @param observer the object to unregister.
+ * @see #registerContentObserver(ContentObserver)
+ */
+ void unregisterContentObserver(ContentObserver observer);
+
+ /**
+ * Register an observer that is called when changes happen to the contents
+ * of the this cursors data set, for example, when the data set is changed via
+ * {@link #requery()}, {@link #deactivate()}, or {@link #close()}.
+ *
+ * @param observer the object that gets notified when the cursors data set changes.
+ * @see #unregisterDataSetObserver(DataSetObserver)
+ */
+ void registerDataSetObserver(DataSetObserver observer);
+
+ /**
+ * Unregister an observer that has previously been registered with this
+ * cursor via {@link #registerContentObserver}.
+ *
+ * @param observer the object to unregister.
+ * @see #registerDataSetObserver(DataSetObserver)
+ */
+ void unregisterDataSetObserver(DataSetObserver observer);
+
+ /**
+ * Register to watch a content URI for changes. This can be the URI of a specific data row (for
+ * example, "content://my_provider_type/23"), or a a generic URI for a content type.
+ *
+ * @param cr The content resolver from the caller's context. The listener attached to
+ * this resolver will be notified.
+ * @param uri The content URI to watch.
+ */
+ void setNotificationUri(ContentResolver cr, Uri uri);
+
+ /**
+ * onMove() will only be called across processes if this method returns true.
+ * @return whether all cursor movement should result in a call to onMove().
+ */
+ boolean getWantsAllOnMoveCalls();
+
+ /**
+ * Returns a bundle of extra values. This is an optional way for cursors to provide out-of-band
+ * metadata to their users. One use of this is for reporting on the progress of network requests
+ * that are required to fetch data for the cursor.
+ *
+ * <p>These values may only change when requery is called.
+ * @return cursor-defined values, or Bundle.EMTPY if there are no values. Never null.
+ */
+ Bundle getExtras();
+
+ /**
+ * This is an out-of-band way for the the user of a cursor to communicate with the cursor. The
+ * structure of each bundle is entirely defined by the cursor.
+ *
+ * <p>One use of this is to tell a cursor that it should retry its network request after it
+ * reported an error.
+ * @param extras extra values, or Bundle.EMTPY. Never null.
+ * @return extra values, or Bundle.EMTPY. Never null.
+ */
+ Bundle respond(Bundle extras);
+}
diff --git a/core/java/android/database/CursorIndexOutOfBoundsException.java b/core/java/android/database/CursorIndexOutOfBoundsException.java
new file mode 100644
index 0000000..1f77d00
--- /dev/null
+++ b/core/java/android/database/CursorIndexOutOfBoundsException.java
@@ -0,0 +1,31 @@
+/*
+ * 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.database;
+
+/**
+ * An exception indicating that a cursor is out of bounds.
+ */
+public class CursorIndexOutOfBoundsException extends IndexOutOfBoundsException {
+
+ public CursorIndexOutOfBoundsException(int index, int size) {
+ super("Index " + index + " requested, with a size of " + size);
+ }
+
+ public CursorIndexOutOfBoundsException(String message) {
+ super(message);
+ }
+}
diff --git a/core/java/android/database/CursorJoiner.java b/core/java/android/database/CursorJoiner.java
new file mode 100644
index 0000000..e3c2988
--- /dev/null
+++ b/core/java/android/database/CursorJoiner.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.database;
+
+import java.util.Iterator;
+
+/**
+ * Does a join on two cursors using the specified columns. The cursors must already
+ * be sorted on each of the specified columns in ascending order. This joiner only
+ * supports the case where the tuple of key column values is unique.
+ * <p>
+ * Typical usage:
+ *
+ * <pre>
+ * CursorJoiner joiner = new CursorJoiner(cursorA, keyColumnsofA, cursorB, keyColumnsofB);
+ * for (CursorJointer.Result joinerResult : joiner) {
+ * switch (joinerResult) {
+ * case LEFT:
+ * // handle case where a row in cursorA is unique
+ * break;
+ * case RIGHT:
+ * // handle case where a row in cursorB is unique
+ * break;
+ * case BOTH:
+ * // handle case where a row with the same key is in both cursors
+ * break;
+ * }
+ * }
+ * </pre>
+ */
+public final class CursorJoiner
+ implements Iterator<CursorJoiner.Result>, Iterable<CursorJoiner.Result> {
+ private Cursor mCursorLeft;
+ private Cursor mCursorRight;
+ private boolean mCompareResultIsValid;
+ private Result mCompareResult;
+ private int[] mColumnsLeft;
+ private int[] mColumnsRight;
+ private String[] mValues;
+
+ /**
+ * The result of a call to next().
+ */
+ public enum Result {
+ /** The row currently pointed to by the left cursor is unique */
+ RIGHT,
+ /** The row currently pointed to by the right cursor is unique */
+ LEFT,
+ /** The rows pointed to by both cursors are the same */
+ BOTH
+ }
+
+ /**
+ * Initializes the CursorJoiner and resets the cursors to the first row. The left and right
+ * column name arrays must have the same number of columns.
+ * @param cursorLeft The left cursor to compare
+ * @param columnNamesLeft The column names to compare from the left cursor
+ * @param cursorRight The right cursor to compare
+ * @param columnNamesRight The column names to compare from the right cursor
+ */
+ public CursorJoiner(
+ Cursor cursorLeft, String[] columnNamesLeft,
+ Cursor cursorRight, String[] columnNamesRight) {
+ if (columnNamesLeft.length != columnNamesRight.length) {
+ throw new IllegalArgumentException(
+ "you must have the same number of columns on the left and right, "
+ + columnNamesLeft.length + " != " + columnNamesRight.length);
+ }
+
+ mCursorLeft = cursorLeft;
+ mCursorRight = cursorRight;
+
+ mCursorLeft.moveToFirst();
+ mCursorRight.moveToFirst();
+
+ mCompareResultIsValid = false;
+
+ mColumnsLeft = buildColumnIndiciesArray(cursorLeft, columnNamesLeft);
+ mColumnsRight = buildColumnIndiciesArray(cursorRight, columnNamesRight);
+
+ mValues = new String[mColumnsLeft.length * 2];
+ }
+
+ public Iterator<Result> iterator() {
+ return this;
+ }
+
+ /**
+ * Lookup the indicies of the each column name and return them in an array.
+ * @param cursor the cursor that contains the columns
+ * @param columnNames the array of names to lookup
+ * @return an array of column indices
+ */
+ private int[] buildColumnIndiciesArray(Cursor cursor, String[] columnNames) {
+ int[] columns = new int[columnNames.length];
+ for (int i = 0; i < columnNames.length; i++) {
+ columns[i] = cursor.getColumnIndexOrThrow(columnNames[i]);
+ }
+ return columns;
+ }
+
+ /**
+ * Returns whether or not there are more rows to compare using next().
+ * @return true if there are more rows to compare
+ */
+ public boolean hasNext() {
+ if (mCompareResultIsValid) {
+ switch (mCompareResult) {
+ case BOTH:
+ return !mCursorLeft.isLast() || !mCursorRight.isLast();
+
+ case LEFT:
+ return !mCursorLeft.isLast() || !mCursorRight.isAfterLast();
+
+ case RIGHT:
+ return !mCursorLeft.isAfterLast() || !mCursorRight.isLast();
+
+ default:
+ throw new IllegalStateException("bad value for mCompareResult, "
+ + mCompareResult);
+ }
+ } else {
+ return !mCursorLeft.isAfterLast() || !mCursorRight.isAfterLast();
+ }
+ }
+
+ /**
+ * Returns the comparison result of the next row from each cursor. If one cursor
+ * has no more rows but the other does then subsequent calls to this will indicate that
+ * the remaining rows are unique.
+ * <p>
+ * The caller must check that hasNext() returns true before calling this.
+ * <p>
+ * Once next() has been called the cursors specified in the result of the call to
+ * next() are guaranteed to point to the row that was indicated. Reading values
+ * from the cursor that was not indicated in the call to next() will result in
+ * undefined behavior.
+ * @return LEFT, if the row pointed to by the left cursor is unique, RIGHT
+ * if the row pointed to by the right cursor is unique, BOTH if the rows in both
+ * cursors are the same.
+ */
+ public Result next() {
+ if (!hasNext()) {
+ throw new IllegalStateException("you must only call next() when hasNext() is true");
+ }
+ incrementCursors();
+ assert hasNext();
+
+ boolean hasLeft = !mCursorLeft.isAfterLast();
+ boolean hasRight = !mCursorRight.isAfterLast();
+
+ if (hasLeft && hasRight) {
+ populateValues(mValues, mCursorLeft, mColumnsLeft, 0 /* start filling at index 0 */);
+ populateValues(mValues, mCursorRight, mColumnsRight, 1 /* start filling at index 1 */);
+ switch (compareStrings(mValues)) {
+ case -1:
+ mCompareResult = Result.LEFT;
+ break;
+ case 0:
+ mCompareResult = Result.BOTH;
+ break;
+ case 1:
+ mCompareResult = Result.RIGHT;
+ break;
+ }
+ } else if (hasLeft) {
+ mCompareResult = Result.LEFT;
+ } else {
+ assert hasRight;
+ mCompareResult = Result.RIGHT;
+ }
+ mCompareResultIsValid = true;
+ return mCompareResult;
+ }
+
+ public void remove() {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
+ /**
+ * Reads the strings from the cursor that are specifed in the columnIndicies
+ * array and saves them in values beginning at startingIndex, skipping a slot
+ * for each value. If columnIndicies has length 3 and startingIndex is 1, the
+ * values will be stored in slots 1, 3, and 5.
+ * @param values the String[] to populate
+ * @param cursor the cursor from which to read
+ * @param columnIndicies the indicies of the values to read from the cursor
+ * @param startingIndex the slot in which to start storing values, and must be either 0 or 1.
+ */
+ private static void populateValues(String[] values, Cursor cursor, int[] columnIndicies,
+ int startingIndex) {
+ assert startingIndex == 0 || startingIndex == 1;
+ for (int i = 0; i < columnIndicies.length; i++) {
+ values[startingIndex + i*2] = cursor.getString(columnIndicies[i]);
+ }
+ }
+
+ /**
+ * Increment the cursors past the rows indicated in the most recent call to next().
+ * This will only have an affect once per call to next().
+ */
+ private void incrementCursors() {
+ if (mCompareResultIsValid) {
+ switch (mCompareResult) {
+ case LEFT:
+ mCursorLeft.moveToNext();
+ break;
+ case RIGHT:
+ mCursorRight.moveToNext();
+ break;
+ case BOTH:
+ mCursorLeft.moveToNext();
+ mCursorRight.moveToNext();
+ break;
+ }
+ mCompareResultIsValid = false;
+ }
+ }
+
+ /**
+ * Compare the values. Values contains n pairs of strings. If all the pairs of strings match
+ * then returns 0. Otherwise returns the comparison result of the first non-matching pair
+ * of values, -1 if the first of the pair is less than the second of the pair or 1 if it
+ * is greater.
+ * @param values the n pairs of values to compare
+ * @return -1, 0, or 1 as described above.
+ */
+ private static int compareStrings(String... values) {
+ if ((values.length % 2) != 0) {
+ throw new IllegalArgumentException("you must specify an even number of values");
+ }
+
+ for (int index = 0; index < values.length; index+=2) {
+ if (values[index] == null) {
+ if (values[index+1] == null) continue;
+ return -1;
+ }
+
+ if (values[index+1] == null) {
+ return 1;
+ }
+
+ int comp = values[index].compareTo(values[index+1]);
+ if (comp != 0) {
+ return comp < 0 ? -1 : 1;
+ }
+ }
+
+ return 0;
+ }
+}
diff --git a/core/java/android/database/CursorToBulkCursorAdaptor.java b/core/java/android/database/CursorToBulkCursorAdaptor.java
new file mode 100644
index 0000000..19ad946
--- /dev/null
+++ b/core/java/android/database/CursorToBulkCursorAdaptor.java
@@ -0,0 +1,233 @@
+/*
+ * 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.database;
+
+import android.database.sqlite.SQLiteMisuseException;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Config;
+import android.util.Log;
+
+import java.util.Map;
+
+
+/**
+ * Wraps a BulkCursor around an existing Cursor making it remotable.
+ *
+ * {@hide}
+ */
+public final class CursorToBulkCursorAdaptor extends BulkCursorNative
+ implements IBinder.DeathRecipient {
+ private static final String TAG = "Cursor";
+ private final CrossProcessCursor mCursor;
+ private CursorWindow mWindow;
+ private final String mProviderName;
+ private final boolean mReadOnly;
+ private ContentObserverProxy mObserver;
+
+ private static final class ContentObserverProxy extends ContentObserver
+ {
+ protected IContentObserver mRemote;
+
+ public ContentObserverProxy(IContentObserver remoteObserver, DeathRecipient recipient) {
+ super(null);
+ mRemote = remoteObserver;
+ try {
+ remoteObserver.asBinder().linkToDeath(recipient, 0);
+ } catch (RemoteException e) {
+ // Do nothing, the far side is dead
+ }
+ }
+
+ public boolean unlinkToDeath(DeathRecipient recipient) {
+ return mRemote.asBinder().unlinkToDeath(recipient, 0);
+ }
+
+ @Override
+ public boolean deliverSelfNotifications() {
+ // The far side handles the self notifications.
+ return false;
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ try {
+ mRemote.onChange(selfChange);
+ } catch (RemoteException ex) {
+ // Do nothing, the far side is dead
+ }
+ }
+ }
+
+ public CursorToBulkCursorAdaptor(Cursor cursor, IContentObserver observer, String providerName,
+ boolean allowWrite, CursorWindow window) {
+ try {
+ mCursor = (CrossProcessCursor) cursor;
+ if (mCursor instanceof AbstractWindowedCursor) {
+ AbstractWindowedCursor windowedCursor = (AbstractWindowedCursor) cursor;
+ if (windowedCursor.hasWindow()) {
+ if (Log.isLoggable(TAG, Log.VERBOSE) || Config.LOGV) {
+ Log.v(TAG, "Cross process cursor has a local window before setWindow in "
+ + providerName, new RuntimeException());
+ }
+ }
+ windowedCursor.setWindow(window);
+ } else {
+ mWindow = window;
+ mCursor.fillWindow(0, window);
+ }
+ } catch (ClassCastException e) {
+ // TODO Implement this case.
+ throw new UnsupportedOperationException(
+ "Only CrossProcessCursor cursors are supported across process for now", e);
+ }
+ mProviderName = providerName;
+ mReadOnly = !allowWrite;
+
+ createAndRegisterObserverProxy(observer);
+ }
+
+ public void binderDied() {
+ mCursor.close();
+ if (mWindow != null) {
+ mWindow.close();
+ }
+ }
+
+ public CursorWindow getWindow(int startPos) {
+ mCursor.moveToPosition(startPos);
+
+ if (mWindow != null) {
+ if (startPos < mWindow.getStartPosition() ||
+ startPos >= (mWindow.getStartPosition() + mWindow.getNumRows())) {
+ mCursor.fillWindow(startPos, mWindow);
+ }
+ return mWindow;
+ } else {
+ return ((AbstractWindowedCursor)mCursor).getWindow();
+ }
+ }
+
+ public void onMove(int position) {
+ mCursor.onMove(mCursor.getPosition(), position);
+ }
+
+ public int count() {
+ return mCursor.getCount();
+ }
+
+ public String[] getColumnNames() {
+ return mCursor.getColumnNames();
+ }
+
+ public void deactivate() {
+ maybeUnregisterObserverProxy();
+ mCursor.deactivate();
+ }
+
+ public void close() {
+ maybeUnregisterObserverProxy();
+ mCursor.deactivate();
+
+ }
+
+ public int requery(IContentObserver observer, CursorWindow window) {
+ if (mWindow == null) {
+ ((AbstractWindowedCursor)mCursor).setWindow(window);
+ }
+ try {
+ if (!mCursor.requery()) {
+ return -1;
+ }
+ } catch (IllegalStateException e) {
+ IllegalStateException leakProgram = new IllegalStateException(
+ mProviderName + " Requery misuse db, mCursor isClosed:" +
+ mCursor.isClosed(), e);
+ throw leakProgram;
+ }
+
+ if (mWindow != null) {
+ mCursor.fillWindow(0, window);
+ mWindow = window;
+ }
+ maybeUnregisterObserverProxy();
+ createAndRegisterObserverProxy(observer);
+ return mCursor.getCount();
+ }
+
+ public boolean getWantsAllOnMoveCalls() {
+ return mCursor.getWantsAllOnMoveCalls();
+ }
+
+ /**
+ * Create a ContentObserver from the observer and register it as an observer on the
+ * underlying cursor.
+ * @param observer the IContentObserver that wants to monitor the cursor
+ * @throws IllegalStateException if an observer is already registered
+ */
+ private void createAndRegisterObserverProxy(IContentObserver observer) {
+ if (mObserver != null) {
+ throw new IllegalStateException("an observer is already registered");
+ }
+ mObserver = new ContentObserverProxy(observer, this);
+ mCursor.registerContentObserver(mObserver);
+ }
+
+ /** Unregister the observer if it is already registered. */
+ private void maybeUnregisterObserverProxy() {
+ if (mObserver != null) {
+ mCursor.unregisterContentObserver(mObserver);
+ mObserver.unlinkToDeath(this);
+ mObserver = null;
+ }
+ }
+
+ public boolean updateRows(Map<? extends Long, ? extends Map<String, Object>> values) {
+ if (mReadOnly) {
+ Log.w("ContentProvider", "Permission Denial: modifying "
+ + mProviderName
+ + " from pid=" + Binder.getCallingPid()
+ + ", uid=" + Binder.getCallingUid());
+ return false;
+ }
+ return mCursor.commitUpdates(values);
+ }
+
+ public boolean deleteRow(int position) {
+ if (mReadOnly) {
+ Log.w("ContentProvider", "Permission Denial: modifying "
+ + mProviderName
+ + " from pid=" + Binder.getCallingPid()
+ + ", uid=" + Binder.getCallingUid());
+ return false;
+ }
+ if (mCursor.moveToPosition(position) == false) {
+ return false;
+ }
+ return mCursor.deleteRow();
+ }
+
+ public Bundle getExtras() {
+ return mCursor.getExtras();
+ }
+
+ public Bundle respond(Bundle extras) {
+ return mCursor.respond(extras);
+ }
+}
diff --git a/core/java/android/database/CursorWindow.java b/core/java/android/database/CursorWindow.java
new file mode 100644
index 0000000..72dc3a9
--- /dev/null
+++ b/core/java/android/database/CursorWindow.java
@@ -0,0 +1,478 @@
+/*
+ * 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.database;
+
+import android.database.sqlite.SQLiteClosable;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * A buffer containing multiple cursor rows.
+ */
+public class CursorWindow extends SQLiteClosable implements Parcelable {
+ /** The pointer to the native window class */
+ @SuppressWarnings("unused")
+ private int nWindow;
+
+ private int mStartPos;
+
+ /**
+ * Creates a new empty window.
+ *
+ * @param localWindow true if this window will be used in this process only
+ */
+ public CursorWindow(boolean localWindow) {
+ mStartPos = 0;
+ native_init(localWindow);
+ }
+
+ /**
+ * Returns the starting position of this window within the entire
+ * Cursor's result set.
+ *
+ * @return the starting position of this window within the entire
+ * Cursor's result set.
+ */
+ public int getStartPosition() {
+ return mStartPos;
+ }
+
+ /**
+ * Set the start position of cursor window
+ * @param pos
+ */
+ public void setStartPosition(int pos) {
+ mStartPos = pos;
+ }
+
+ /**
+ * Returns the number of rows in this window.
+ *
+ * @return the number of rows in this window.
+ */
+ public int getNumRows() {
+ acquireReference();
+ try {
+ return getNumRows_native();
+ } finally {
+ releaseReference();
+ }
+ }
+
+ private native int getNumRows_native();
+ /**
+ * Set number of Columns
+ * @param columnNum
+ * @return true if success
+ */
+ public boolean setNumColumns(int columnNum) {
+ acquireReference();
+ try {
+ return setNumColumns_native(columnNum);
+ } finally {
+ releaseReference();
+ }
+ }
+
+ private native boolean setNumColumns_native(int columnNum);
+
+ /**
+ * Allocate a row in cursor window
+ * @return false if cursor window is out of memory
+ */
+ public boolean allocRow(){
+ acquireReference();
+ try {
+ return allocRow_native();
+ } finally {
+ releaseReference();
+ }
+ }
+
+ private native boolean allocRow_native();
+
+ /**
+ * Free the last row
+ */
+ public void freeLastRow(){
+ acquireReference();
+ try {
+ freeLastRow_native();
+ } finally {
+ releaseReference();
+ }
+ }
+
+ private native void freeLastRow_native();
+
+ /**
+ * copy byte array to cursor window
+ * @param value
+ * @param row
+ * @param col
+ * @return false if fail to copy
+ */
+ public boolean putBlob(byte[] value, int row, int col) {
+ acquireReference();
+ try {
+ return putBlob_native(value, row - mStartPos, col);
+ } finally {
+ releaseReference();
+ }
+ }
+
+ private native boolean putBlob_native(byte[] value, int row, int col);
+
+ /**
+ * Copy String to cursor window
+ * @param value
+ * @param row
+ * @param col
+ * @return false if fail to copy
+ */
+ public boolean putString(String value, int row, int col) {
+ acquireReference();
+ try {
+ return putString_native(value, row - mStartPos, col);
+ } finally {
+ releaseReference();
+ }
+ }
+
+ private native boolean putString_native(String value, int row, int col);
+
+ /**
+ * Copy integer to cursor window
+ * @param value
+ * @param row
+ * @param col
+ * @return false if fail to copy
+ */
+ public boolean putLong(long value, int row, int col) {
+ acquireReference();
+ try {
+ return putLong_native(value, row - mStartPos, col);
+ } finally {
+ releaseReference();
+ }
+ }
+
+ private native boolean putLong_native(long value, int row, int col);
+
+
+ /**
+ * Copy double to cursor window
+ * @param value
+ * @param row
+ * @param col
+ * @return false if fail to copy
+ */
+ public boolean putDouble(double value, int row, int col) {
+ acquireReference();
+ try {
+ return putDouble_native(value, row - mStartPos, col);
+ } finally {
+ releaseReference();
+ }
+ }
+
+ private native boolean putDouble_native(double value, int row, int col);
+
+ /**
+ * Set the [row, col] value to NULL
+ * @param row
+ * @param col
+ * @return false if fail to copy
+ */
+ public boolean putNull(int row, int col) {
+ acquireReference();
+ try {
+ return putNull_native(row - mStartPos, col);
+ } finally {
+ releaseReference();
+ }
+ }
+
+ private native boolean putNull_native(int row, int col);
+
+
+ /**
+ * Returns {@code true} if given field is {@code NULL}.
+ *
+ * @param row the row to read from, row - getStartPosition() being the actual row in the window
+ * @param col the column to read from
+ * @return {@code true} if given field is {@code NULL}
+ */
+ public boolean isNull(int row, int col) {
+ acquireReference();
+ try {
+ return isNull_native(row - mStartPos, col);
+ } finally {
+ releaseReference();
+ }
+ }
+
+ private native boolean isNull_native(int row, int col);
+
+ /**
+ * Returns a byte array for the given field.
+ *
+ * @param row the row to read from, row - getStartPosition() being the actual row in the window
+ * @param col the column to read from
+ * @return a String value for the given field
+ */
+ public byte[] getBlob(int row, int col) {
+ acquireReference();
+ try {
+ return getBlob_native(row - mStartPos, col);
+ } finally {
+ releaseReference();
+ }
+ }
+
+ private native byte[] getBlob_native(int row, int col);
+
+ /**
+ * Checks if a field contains either a blob or is null.
+ *
+ * @param row the row to read from, row - getStartPosition() being the actual row in the window
+ * @param col the column to read from
+ * @return {@code true} if given field is {@code NULL} or a blob
+ */
+ public boolean isBlob(int row, int col) {
+ acquireReference();
+ try {
+ return isBlob_native(row - mStartPos, col);
+ } finally {
+ releaseReference();
+ }
+ }
+
+ private native boolean isBlob_native(int row, int col);
+
+ /**
+ * Returns a String for the given field.
+ *
+ * @param row the row to read from, row - getStartPosition() being the actual row in the window
+ * @param col the column to read from
+ * @return a String value for the given field
+ */
+ public String getString(int row, int col) {
+ acquireReference();
+ try {
+ return getString_native(row - mStartPos, col);
+ } finally {
+ releaseReference();
+ }
+ }
+
+ private native String getString_native(int row, int col);
+
+ /**
+ * copy the text for the given field in the provided char array.
+ *
+ * @param row the row to read from, row - getStartPosition() being the actual row in the window
+ * @param col the column to read from
+ * @param buffer the CharArrayBuffer to copy the text into,
+ * If the requested string is larger than the buffer
+ * a new char buffer will be created to hold the string. and assigne to
+ * CharArrayBuffer.data
+ */
+ public void copyStringToBuffer(int row, int col, CharArrayBuffer buffer) {
+ if (buffer == null) {
+ throw new IllegalArgumentException("CharArrayBuffer should not be null");
+ }
+ if (buffer.data == null) {
+ buffer.data = new char[64];
+ }
+ acquireReference();
+ try {
+ char[] newbuf = copyStringToBuffer_native(
+ row - mStartPos, col, buffer.data.length, buffer);
+ if (newbuf != null) {
+ buffer.data = newbuf;
+ }
+ } finally {
+ releaseReference();
+ }
+ }
+
+ private native char[] copyStringToBuffer_native(
+ int row, int col, int bufferSize, CharArrayBuffer buffer);
+
+ /**
+ * Returns a long for the given field.
+ * row is 0 based
+ *
+ * @param row the row to read from, row - getStartPosition() being the actual row in the window
+ * @param col the column to read from
+ * @return a long value for the given field
+ */
+ public long getLong(int row, int col) {
+ acquireReference();
+ try {
+ return getLong_native(row - mStartPos, col);
+ } finally {
+ releaseReference();
+ }
+ }
+
+ private native long getLong_native(int row, int col);
+
+ /**
+ * Returns a double for the given field.
+ * row is 0 based
+ *
+ * @param row the row to read from, row - getStartPosition() being the actual row in the window
+ * @param col the column to read from
+ * @return a double value for the given field
+ */
+ public double getDouble(int row, int col) {
+ acquireReference();
+ try {
+ return getDouble_native(row - mStartPos, col);
+ } finally {
+ releaseReference();
+ }
+ }
+
+ private native double getDouble_native(int row, int col);
+
+ /**
+ * Returns a short for the given field.
+ * row is 0 based
+ *
+ * @param row the row to read from, row - getStartPosition() being the actual row in the window
+ * @param col the column to read from
+ * @return a short value for the given field
+ */
+ public short getShort(int row, int col) {
+ acquireReference();
+ try {
+ return (short) getLong_native(row - mStartPos, col);
+ } finally {
+ releaseReference();
+ }
+ }
+
+ /**
+ * Returns an int for the given field.
+ *
+ * @param row the row to read from, row - getStartPosition() being the actual row in the window
+ * @param col the column to read from
+ * @return an int value for the given field
+ */
+ public int getInt(int row, int col) {
+ acquireReference();
+ try {
+ return (int) getLong_native(row - mStartPos, col);
+ } finally {
+ releaseReference();
+ }
+ }
+
+ /**
+ * Returns a float for the given field.
+ * row is 0 based
+ *
+ * @param row the row to read from, row - getStartPosition() being the actual row in the window
+ * @param col the column to read from
+ * @return a float value for the given field
+ */
+ public float getFloat(int row, int col) {
+ acquireReference();
+ try {
+ return (float) getDouble_native(row - mStartPos, col);
+ } finally {
+ releaseReference();
+ }
+ }
+
+ /**
+ * Clears out the existing contents of the window, making it safe to reuse
+ * for new data. Note that the number of columns in the window may NOT
+ * change across a call to clear().
+ */
+ public void clear() {
+ mStartPos = 0;
+ native_clear();
+ }
+
+ /** Clears out the native side of things */
+ private native void native_clear();
+
+ /**
+ * Cleans up the native resources associated with the window.
+ */
+ public void close() {
+ releaseReference();
+ }
+
+ private native void close_native();
+
+ @Override
+ protected void finalize() {
+ // Just in case someone forgot to call close...
+ close_native();
+ }
+
+ public static final Parcelable.Creator<CursorWindow> CREATOR
+ = new Parcelable.Creator<CursorWindow>() {
+ public CursorWindow createFromParcel(Parcel source) {
+ return new CursorWindow(source);
+ }
+
+ public CursorWindow[] newArray(int size) {
+ return new CursorWindow[size];
+ }
+ };
+
+ public static CursorWindow newFromParcel(Parcel p) {
+ return CREATOR.createFromParcel(p);
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeStrongBinder(native_getBinder());
+ dest.writeInt(mStartPos);
+ }
+
+ private CursorWindow(Parcel source) {
+ IBinder nativeBinder = source.readStrongBinder();
+ mStartPos = source.readInt();
+
+ native_init(nativeBinder);
+ }
+
+ /** Get the binder for the native side of the window */
+ private native IBinder native_getBinder();
+
+ /** Does the native side initialization for an empty window */
+ private native void native_init(boolean localOnly);
+
+ /** Does the native side initialization with an existing binder from another process */
+ private native void native_init(IBinder nativeBinder);
+
+ @Override
+ protected void onAllReferencesReleased() {
+ close_native();
+ }
+}
diff --git a/core/java/android/database/CursorWrapper.java b/core/java/android/database/CursorWrapper.java
new file mode 100644
index 0000000..f0aa7d7
--- /dev/null
+++ b/core/java/android/database/CursorWrapper.java
@@ -0,0 +1,305 @@
+/*
+ * 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.database;
+
+import android.content.ContentResolver;
+import android.database.CharArrayBuffer;
+import android.net.Uri;
+import android.os.Bundle;
+
+import java.util.Map;
+
+/**
+ * Wrapper class for Cursor that delegates all calls to the actual cursor object
+ */
+
+public class CursorWrapper implements Cursor {
+
+ public CursorWrapper(Cursor cursor) {
+ mCursor = cursor;
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public void abortUpdates() {
+ mCursor.abortUpdates();
+ }
+
+ public void close() {
+ mCursor.close();
+ }
+
+ public boolean isClosed() {
+ return mCursor.isClosed();
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean commitUpdates() {
+ return mCursor.commitUpdates();
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean commitUpdates(
+ Map<? extends Long, ? extends Map<String, Object>> values) {
+ return mCursor.commitUpdates(values);
+ }
+
+ public int getCount() {
+ return mCursor.getCount();
+ }
+
+ public void deactivate() {
+ mCursor.deactivate();
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean deleteRow() {
+ return mCursor.deleteRow();
+ }
+
+ public boolean moveToFirst() {
+ return mCursor.moveToFirst();
+ }
+
+ public int getColumnCount() {
+ return mCursor.getColumnCount();
+ }
+
+ public int getColumnIndex(String columnName) {
+ return mCursor.getColumnIndex(columnName);
+ }
+
+ public int getColumnIndexOrThrow(String columnName)
+ throws IllegalArgumentException {
+ return mCursor.getColumnIndexOrThrow(columnName);
+ }
+
+ public String getColumnName(int columnIndex) {
+ return mCursor.getColumnName(columnIndex);
+ }
+
+ public String[] getColumnNames() {
+ return mCursor.getColumnNames();
+ }
+
+ public double getDouble(int columnIndex) {
+ return mCursor.getDouble(columnIndex);
+ }
+
+ public Bundle getExtras() {
+ return mCursor.getExtras();
+ }
+
+ public float getFloat(int columnIndex) {
+ return mCursor.getFloat(columnIndex);
+ }
+
+ public int getInt(int columnIndex) {
+ return mCursor.getInt(columnIndex);
+ }
+
+ public long getLong(int columnIndex) {
+ return mCursor.getLong(columnIndex);
+ }
+
+ public short getShort(int columnIndex) {
+ return mCursor.getShort(columnIndex);
+ }
+
+ public String getString(int columnIndex) {
+ return mCursor.getString(columnIndex);
+ }
+
+ public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) {
+ mCursor.copyStringToBuffer(columnIndex, buffer);
+ }
+
+ public byte[] getBlob(int columnIndex) {
+ return mCursor.getBlob(columnIndex);
+ }
+
+ public boolean getWantsAllOnMoveCalls() {
+ return mCursor.getWantsAllOnMoveCalls();
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean hasUpdates() {
+ return mCursor.hasUpdates();
+ }
+
+ public boolean isAfterLast() {
+ return mCursor.isAfterLast();
+ }
+
+ public boolean isBeforeFirst() {
+ return mCursor.isBeforeFirst();
+ }
+
+ public boolean isFirst() {
+ return mCursor.isFirst();
+ }
+
+ public boolean isLast() {
+ return mCursor.isLast();
+ }
+
+ public boolean isNull(int columnIndex) {
+ return mCursor.isNull(columnIndex);
+ }
+
+ public boolean moveToLast() {
+ return mCursor.moveToLast();
+ }
+
+ public boolean move(int offset) {
+ return mCursor.move(offset);
+ }
+
+ public boolean moveToPosition(int position) {
+ return mCursor.moveToPosition(position);
+ }
+
+ public boolean moveToNext() {
+ return mCursor.moveToNext();
+ }
+
+ public int getPosition() {
+ return mCursor.getPosition();
+ }
+
+ public boolean moveToPrevious() {
+ return mCursor.moveToPrevious();
+ }
+
+ public void registerContentObserver(ContentObserver observer) {
+ mCursor.registerContentObserver(observer);
+ }
+
+ public void registerDataSetObserver(DataSetObserver observer) {
+ mCursor.registerDataSetObserver(observer);
+ }
+
+ public boolean requery() {
+ return mCursor.requery();
+ }
+
+ public Bundle respond(Bundle extras) {
+ return mCursor.respond(extras);
+ }
+
+ public void setNotificationUri(ContentResolver cr, Uri uri) {
+ mCursor.setNotificationUri(cr, uri);
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean supportsUpdates() {
+ return mCursor.supportsUpdates();
+ }
+
+ public void unregisterContentObserver(ContentObserver observer) {
+ mCursor.unregisterContentObserver(observer);
+ }
+
+ public void unregisterDataSetObserver(DataSetObserver observer) {
+ mCursor.unregisterDataSetObserver(observer);
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean updateDouble(int columnIndex, double value) {
+ return mCursor.updateDouble(columnIndex, value);
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean updateFloat(int columnIndex, float value) {
+ return mCursor.updateFloat(columnIndex, value);
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean updateInt(int columnIndex, int value) {
+ return mCursor.updateInt(columnIndex, value);
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean updateLong(int columnIndex, long value) {
+ return mCursor.updateLong(columnIndex, value);
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean updateShort(int columnIndex, short value) {
+ return mCursor.updateShort(columnIndex, value);
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean updateString(int columnIndex, String value) {
+ return mCursor.updateString(columnIndex, value);
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean updateBlob(int columnIndex, byte[] value) {
+ return mCursor.updateBlob(columnIndex, value);
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ public boolean updateToNull(int columnIndex) {
+ return mCursor.updateToNull(columnIndex);
+ }
+
+ private Cursor mCursor;
+
+}
+
diff --git a/core/java/android/database/DataSetObservable.java b/core/java/android/database/DataSetObservable.java
new file mode 100644
index 0000000..9200e81
--- /dev/null
+++ b/core/java/android/database/DataSetObservable.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.database;
+
+/**
+ * A specialization of Observable for DataSetObserver that provides methods for
+ * invoking the various callback methods of DataSetObserver.
+ */
+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.
+ */
+ public void notifyChanged() {
+ synchronized(mObservers) {
+ for (DataSetObserver observer : mObservers) {
+ observer.onChanged();
+ }
+ }
+ }
+
+ /**
+ * Invokes onInvalidated on each observer. Called when the data set being monitored
+ * has changed such that it is no longer valid.
+ */
+ public void notifyInvalidated() {
+ synchronized (mObservers) {
+ for (DataSetObserver observer : mObservers) {
+ observer.onInvalidated();
+ }
+ }
+ }
+}
diff --git a/core/java/android/database/DataSetObserver.java b/core/java/android/database/DataSetObserver.java
new file mode 100644
index 0000000..28616c8
--- /dev/null
+++ b/core/java/android/database/DataSetObserver.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.database;
+
+/**
+ * Receives call backs when a data set has been changed, or made invalid. The typically data sets
+ * that are observed are {@link Cursor}s or {@link android.widget.Adapter}s.
+ * DataSetObserver must be implemented by objects which are added to a DataSetObservable.
+ */
+public abstract class DataSetObserver {
+ /**
+ * This method is called when the entire data set has changed,
+ * most likely through a call to {@link Cursor#requery()} on a {@link Cursor}.
+ */
+ public void onChanged() {
+ // Do nothing
+ }
+
+ /**
+ * This method is called when the entire data becomes invalid,
+ * most likely through a call to {@link Cursor#deactivate()} or {@link Cursor#close()} on a
+ * {@link Cursor}.
+ */
+ public void onInvalidated() {
+ // Do nothing
+ }
+}
diff --git a/core/java/android/database/DatabaseUtils.java b/core/java/android/database/DatabaseUtils.java
new file mode 100644
index 0000000..ab0dc3f
--- /dev/null
+++ b/core/java/android/database/DatabaseUtils.java
@@ -0,0 +1,1002 @@
+/*
+ * 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.database;
+
+import org.apache.commons.codec.binary.Hex;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.sqlite.SQLiteAbortException;
+import android.database.sqlite.SQLiteConstraintException;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteDatabaseCorruptException;
+import android.database.sqlite.SQLiteDiskIOException;
+import android.database.sqlite.SQLiteException;
+import android.database.sqlite.SQLiteFullException;
+import android.database.sqlite.SQLiteProgram;
+import android.database.sqlite.SQLiteStatement;
+import android.os.Parcel;
+import android.text.TextUtils;
+import android.util.Config;
+import android.util.Log;
+
+import java.io.FileNotFoundException;
+import java.io.PrintStream;
+import java.text.Collator;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Static utility methods for dealing with databases and {@link Cursor}s.
+ */
+public class DatabaseUtils {
+ private static final String TAG = "DatabaseUtils";
+
+ private static final boolean DEBUG = false;
+ private static final boolean LOCAL_LOGV = DEBUG ? Config.LOGD : Config.LOGV;
+
+ private static final String[] countProjection = new String[]{"count(*)"};
+
+ /**
+ * Special function for writing an exception result at the header of
+ * a parcel, to be used when returning an exception from a transaction.
+ * exception will be re-thrown by the function in another process
+ * @param reply Parcel to write to
+ * @param e The Exception to be written.
+ * @see Parcel#writeNoException
+ * @see Parcel#writeException
+ */
+ public static final void writeExceptionToParcel(Parcel reply, Exception e) {
+ int code = 0;
+ boolean logException = true;
+ if (e instanceof FileNotFoundException) {
+ code = 1;
+ logException = false;
+ } else if (e instanceof IllegalArgumentException) {
+ code = 2;
+ } else if (e instanceof UnsupportedOperationException) {
+ code = 3;
+ } else if (e instanceof SQLiteAbortException) {
+ code = 4;
+ } else if (e instanceof SQLiteConstraintException) {
+ code = 5;
+ } else if (e instanceof SQLiteDatabaseCorruptException) {
+ code = 6;
+ } else if (e instanceof SQLiteFullException) {
+ code = 7;
+ } else if (e instanceof SQLiteDiskIOException) {
+ code = 8;
+ } else if (e instanceof SQLiteException) {
+ code = 9;
+ } else {
+ reply.writeException(e);
+ return;
+ }
+ reply.writeInt(code);
+ reply.writeString(e.getMessage());
+
+ if (logException) {
+ Log.e(TAG, "Writing exception to parcel", e);
+ }
+ }
+
+ /**
+ * Special function for reading an exception result from the header of
+ * a parcel, to be used after receiving the result of a transaction. This
+ * will throw the exception for you if it had been written to the Parcel,
+ * otherwise return and let you read the normal result data from the Parcel.
+ * @param reply Parcel to read from
+ * @see Parcel#writeNoException
+ * @see Parcel#readException
+ */
+ public static final void readExceptionFromParcel(Parcel reply) {
+ int code = reply.readInt();
+ if (code == 0) return;
+ String msg = reply.readString();
+ DatabaseUtils.readExceptionFromParcel(reply, msg, code);
+ }
+
+ public static void readExceptionWithFileNotFoundExceptionFromParcel(
+ Parcel reply) throws FileNotFoundException {
+ int code = reply.readInt();
+ if (code == 0) return;
+ String msg = reply.readString();
+ if (code == 1) {
+ throw new FileNotFoundException(msg);
+ } else {
+ DatabaseUtils.readExceptionFromParcel(reply, msg, code);
+ }
+ }
+
+ private static final void readExceptionFromParcel(Parcel reply, String msg, int code) {
+ switch (code) {
+ case 2:
+ throw new IllegalArgumentException(msg);
+ case 3:
+ throw new UnsupportedOperationException(msg);
+ case 4:
+ throw new SQLiteAbortException(msg);
+ case 5:
+ throw new SQLiteConstraintException(msg);
+ case 6:
+ throw new SQLiteDatabaseCorruptException(msg);
+ case 7:
+ throw new SQLiteFullException(msg);
+ case 8:
+ throw new SQLiteDiskIOException(msg);
+ case 9:
+ throw new SQLiteException(msg);
+ default:
+ reply.readException(code, msg);
+ }
+ }
+
+ /**
+ * Binds the given Object to the given SQLiteProgram using the proper
+ * typing. For example, bind numbers as longs/doubles, and everything else
+ * as a string by call toString() on it.
+ *
+ * @param prog the program to bind the object to
+ * @param index the 1-based index to bind at
+ * @param value the value to bind
+ */
+ public static void bindObjectToProgram(SQLiteProgram prog, int index,
+ Object value) {
+ if (value == null) {
+ prog.bindNull(index);
+ } else if (value instanceof Double || value instanceof Float) {
+ prog.bindDouble(index, ((Number)value).doubleValue());
+ } else if (value instanceof Number) {
+ prog.bindLong(index, ((Number)value).longValue());
+ } else if (value instanceof Boolean) {
+ Boolean bool = (Boolean)value;
+ if (bool) {
+ prog.bindLong(index, 1);
+ } else {
+ prog.bindLong(index, 0);
+ }
+ } else if (value instanceof byte[]){
+ prog.bindBlob(index, (byte[]) value);
+ } else {
+ prog.bindString(index, value.toString());
+ }
+ }
+
+ /**
+ * Appends an SQL string to the given StringBuilder, including the opening
+ * and closing single quotes. Any single quotes internal to sqlString will
+ * be escaped.
+ *
+ * This method is deprecated because we want to encourage everyone
+ * to use the "?" binding form. However, when implementing a
+ * ContentProvider, one may want to add WHERE clauses that were
+ * not provided by the caller. Since "?" is a positional form,
+ * using it in this case could break the caller because the
+ * indexes would be shifted to accomodate the ContentProvider's
+ * internal bindings. In that case, it may be necessary to
+ * construct a WHERE clause manually. This method is useful for
+ * those cases.
+ *
+ * @param sb the StringBuilder that the SQL string will be appended to
+ * @param sqlString the raw string to be appended, which may contain single
+ * quotes
+ */
+ public static void appendEscapedSQLString(StringBuilder sb, String sqlString) {
+ sb.append('\'');
+ if (sqlString.indexOf('\'') != -1) {
+ int length = sqlString.length();
+ for (int i = 0; i < length; i++) {
+ char c = sqlString.charAt(i);
+ if (c == '\'') {
+ sb.append('\'');
+ }
+ sb.append(c);
+ }
+ } else
+ sb.append(sqlString);
+ sb.append('\'');
+ }
+
+ /**
+ * SQL-escape a string.
+ */
+ public static String sqlEscapeString(String value) {
+ StringBuilder escaper = new StringBuilder();
+
+ DatabaseUtils.appendEscapedSQLString(escaper, value);
+
+ return escaper.toString();
+ }
+
+ /**
+ * Appends an Object to an SQL string with the proper escaping, etc.
+ */
+ public static final void appendValueToSql(StringBuilder sql, Object value) {
+ if (value == null) {
+ sql.append("NULL");
+ } else if (value instanceof Boolean) {
+ Boolean bool = (Boolean)value;
+ if (bool) {
+ sql.append('1');
+ } else {
+ sql.append('0');
+ }
+ } else {
+ appendEscapedSQLString(sql, value.toString());
+ }
+ }
+
+ /**
+ * return the collation key
+ * @param name
+ * @return the collation key
+ */
+ public static String getCollationKey(String name) {
+ byte [] arr = getCollationKeyInBytes(name);
+ try {
+ return new String(arr, 0, getKeyLen(arr), "ISO8859_1");
+ } catch (Exception ex) {
+ return "";
+ }
+ }
+
+ /**
+ * return the collation key in hex format
+ * @param name
+ * @return the collation key in hex format
+ */
+ public static String getHexCollationKey(String name) {
+ byte [] arr = getCollationKeyInBytes(name);
+ char[] keys = Hex.encodeHex(arr);
+ return new String(keys, 0, getKeyLen(arr) * 2);
+ }
+
+ private static int getKeyLen(byte[] arr) {
+ if (arr[arr.length - 1] != 0) {
+ return arr.length;
+ } else {
+ // remove zero "termination"
+ return arr.length-1;
+ }
+ }
+
+ private static byte[] getCollationKeyInBytes(String name) {
+ if (mColl == null) {
+ mColl = Collator.getInstance();
+ mColl.setStrength(Collator.PRIMARY);
+ }
+ return mColl.getCollationKey(name).toByteArray();
+ }
+
+ private static Collator mColl = null;
+ /**
+ * Prints the contents of a Cursor to System.out. The position is restored
+ * after printing.
+ *
+ * @param cursor the cursor to print
+ */
+ public static void dumpCursor(Cursor cursor) {
+ dumpCursor(cursor, System.out);
+ }
+
+ /**
+ * Prints the contents of a Cursor to a PrintSteam. The position is restored
+ * after printing.
+ *
+ * @param cursor the cursor to print
+ * @param stream the stream to print to
+ */
+ public static void dumpCursor(Cursor cursor, PrintStream stream) {
+ stream.println(">>>>> Dumping cursor " + cursor);
+ if (cursor != null) {
+ int startPos = cursor.getPosition();
+
+ cursor.moveToPosition(-1);
+ while (cursor.moveToNext()) {
+ dumpCurrentRow(cursor, stream);
+ }
+ cursor.moveToPosition(startPos);
+ }
+ stream.println("<<<<<");
+ }
+
+ /**
+ * Prints the contents of a Cursor to a StringBuilder. The position
+ * is restored after printing.
+ *
+ * @param cursor the cursor to print
+ * @param sb the StringBuilder to print to
+ */
+ public static void dumpCursor(Cursor cursor, StringBuilder sb) {
+ sb.append(">>>>> Dumping cursor " + cursor + "\n");
+ if (cursor != null) {
+ int startPos = cursor.getPosition();
+
+ cursor.moveToPosition(-1);
+ while (cursor.moveToNext()) {
+ dumpCurrentRow(cursor, sb);
+ }
+ cursor.moveToPosition(startPos);
+ }
+ sb.append("<<<<<\n");
+ }
+
+ /**
+ * Prints the contents of a Cursor to a String. The position is restored
+ * after printing.
+ *
+ * @param cursor the cursor to print
+ * @return a String that contains the dumped cursor
+ */
+ public static String dumpCursorToString(Cursor cursor) {
+ StringBuilder sb = new StringBuilder();
+ dumpCursor(cursor, sb);
+ return sb.toString();
+ }
+
+ /**
+ * Prints the contents of a Cursor's current row to System.out.
+ *
+ * @param cursor the cursor to print from
+ */
+ public static void dumpCurrentRow(Cursor cursor) {
+ dumpCurrentRow(cursor, System.out);
+ }
+
+ /**
+ * Prints the contents of a Cursor's current row to a PrintSteam.
+ *
+ * @param cursor the cursor to print
+ * @param stream the stream to print to
+ */
+ public static void dumpCurrentRow(Cursor cursor, PrintStream stream) {
+ String[] cols = cursor.getColumnNames();
+ stream.println("" + cursor.getPosition() + " {");
+ int length = cols.length;
+ for (int i = 0; i< length; i++) {
+ String value;
+ try {
+ value = cursor.getString(i);
+ } catch (SQLiteException e) {
+ // assume that if the getString threw this exception then the column is not
+ // representable by a string, e.g. it is a BLOB.
+ value = "<unprintable>";
+ }
+ stream.println(" " + cols[i] + '=' + value);
+ }
+ stream.println("}");
+ }
+
+ /**
+ * Prints the contents of a Cursor's current row to a StringBuilder.
+ *
+ * @param cursor the cursor to print
+ * @param sb the StringBuilder to print to
+ */
+ public static void dumpCurrentRow(Cursor cursor, StringBuilder sb) {
+ String[] cols = cursor.getColumnNames();
+ sb.append("" + cursor.getPosition() + " {\n");
+ int length = cols.length;
+ for (int i = 0; i < length; i++) {
+ String value;
+ try {
+ value = cursor.getString(i);
+ } catch (SQLiteException e) {
+ // assume that if the getString threw this exception then the column is not
+ // representable by a string, e.g. it is a BLOB.
+ value = "<unprintable>";
+ }
+ sb.append(" " + cols[i] + '=' + value + "\n");
+ }
+ sb.append("}\n");
+ }
+
+ /**
+ * Dump the contents of a Cursor's current row to a String.
+ *
+ * @param cursor the cursor to print
+ * @return a String that contains the dumped cursor row
+ */
+ public static String dumpCurrentRowToString(Cursor cursor) {
+ StringBuilder sb = new StringBuilder();
+ dumpCurrentRow(cursor, sb);
+ return sb.toString();
+ }
+
+ /**
+ * Reads a String out of a field in a Cursor and writes it to a Map.
+ *
+ * @param cursor The cursor to read from
+ * @param field The TEXT field to read
+ * @param values The {@link ContentValues} to put the value into, with the field as the key
+ */
+ public static void cursorStringToContentValues(Cursor cursor, String field,
+ ContentValues values) {
+ cursorStringToContentValues(cursor, field, values, field);
+ }
+
+ /**
+ * Reads a String out of a field in a Cursor and writes it to an InsertHelper.
+ *
+ * @param cursor The cursor to read from
+ * @param field The TEXT field to read
+ * @param inserter The InsertHelper to bind into
+ * @param index the index of the bind entry in the InsertHelper
+ */
+ public static void cursorStringToInsertHelper(Cursor cursor, String field,
+ InsertHelper inserter, int index) {
+ inserter.bind(index, cursor.getString(cursor.getColumnIndexOrThrow(field)));
+ }
+
+ /**
+ * Reads a String out of a field in a Cursor and writes it to a Map.
+ *
+ * @param cursor The cursor to read from
+ * @param field The TEXT field to read
+ * @param values The {@link ContentValues} to put the value into, with the field as the key
+ * @param key The key to store the value with in the map
+ */
+ public static void cursorStringToContentValues(Cursor cursor, String field,
+ ContentValues values, String key) {
+ values.put(key, cursor.getString(cursor.getColumnIndexOrThrow(field)));
+ }
+
+ /**
+ * Reads an Integer out of a field in a Cursor and writes it to a Map.
+ *
+ * @param cursor The cursor to read from
+ * @param field The INTEGER field to read
+ * @param values The {@link ContentValues} to put the value into, with the field as the key
+ */
+ public static void cursorIntToContentValues(Cursor cursor, String field, ContentValues values) {
+ cursorIntToContentValues(cursor, field, values, field);
+ }
+
+ /**
+ * Reads a Integer out of a field in a Cursor and writes it to a Map.
+ *
+ * @param cursor The cursor to read from
+ * @param field The INTEGER field to read
+ * @param values The {@link ContentValues} to put the value into, with the field as the key
+ * @param key The key to store the value with in the map
+ */
+ public static void cursorIntToContentValues(Cursor cursor, String field, ContentValues values,
+ String key) {
+ int colIndex = cursor.getColumnIndex(field);
+ if (!cursor.isNull(colIndex)) {
+ values.put(key, cursor.getInt(colIndex));
+ } else {
+ values.put(key, (Integer) null);
+ }
+ }
+
+ /**
+ * Reads a Long out of a field in a Cursor and writes it to a Map.
+ *
+ * @param cursor The cursor to read from
+ * @param field The INTEGER field to read
+ * @param values The {@link ContentValues} to put the value into, with the field as the key
+ */
+ public static void cursorLongToContentValues(Cursor cursor, String field, ContentValues values)
+ {
+ cursorLongToContentValues(cursor, field, values, field);
+ }
+
+ /**
+ * Reads a Long out of a field in a Cursor and writes it to a Map.
+ *
+ * @param cursor The cursor to read from
+ * @param field The INTEGER field to read
+ * @param values The {@link ContentValues} to put the value into
+ * @param key The key to store the value with in the map
+ */
+ public static void cursorLongToContentValues(Cursor cursor, String field, ContentValues values,
+ String key) {
+ int colIndex = cursor.getColumnIndex(field);
+ if (!cursor.isNull(colIndex)) {
+ Long value = Long.valueOf(cursor.getLong(colIndex));
+ values.put(key, value);
+ } else {
+ values.put(key, (Long) null);
+ }
+ }
+
+ /**
+ * Reads a Double out of a field in a Cursor and writes it to a Map.
+ *
+ * @param cursor The cursor to read from
+ * @param field The REAL field to read
+ * @param values The {@link ContentValues} to put the value into
+ */
+ public static void cursorDoubleToCursorValues(Cursor cursor, String field, ContentValues values)
+ {
+ cursorDoubleToContentValues(cursor, field, values, field);
+ }
+
+ /**
+ * Reads a Double out of a field in a Cursor and writes it to a Map.
+ *
+ * @param cursor The cursor to read from
+ * @param field The REAL field to read
+ * @param values The {@link ContentValues} to put the value into
+ * @param key The key to store the value with in the map
+ */
+ public static void cursorDoubleToContentValues(Cursor cursor, String field,
+ ContentValues values, String key) {
+ int colIndex = cursor.getColumnIndex(field);
+ if (!cursor.isNull(colIndex)) {
+ values.put(key, cursor.getDouble(colIndex));
+ } else {
+ values.put(key, (Double) null);
+ }
+ }
+
+ /**
+ * Read the entire contents of a cursor row and store them in a ContentValues.
+ *
+ * @param cursor the cursor to read from.
+ * @param values the {@link ContentValues} to put the row into.
+ */
+ public static void cursorRowToContentValues(Cursor cursor, ContentValues values) {
+ AbstractWindowedCursor awc =
+ (cursor instanceof AbstractWindowedCursor) ? (AbstractWindowedCursor) cursor : null;
+
+ String[] columns = cursor.getColumnNames();
+ int length = columns.length;
+ for (int i = 0; i < length; i++) {
+ if (awc != null && awc.isBlob(i)) {
+ values.put(columns[i], cursor.getBlob(i));
+ } else {
+ values.put(columns[i], cursor.getString(i));
+ }
+ }
+ }
+
+ /**
+ * 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
+ * @return the number of rows in the table
+ */
+ public static long queryNumEntries(SQLiteDatabase db, String table) {
+ Cursor cursor = db.query(table, countProjection,
+ null, null, null, null, null);
+ cursor.moveToFirst();
+ long count = cursor.getLong(0);
+ cursor.deactivate();
+ return count;
+ }
+
+ /**
+ * Utility method to run the query on the db and return the value in the
+ * first column of the first row.
+ */
+ public static long longForQuery(SQLiteDatabase db, String query, String[] selectionArgs) {
+ SQLiteStatement prog = db.compileStatement(query);
+ try {
+ return longForQuery(prog, selectionArgs);
+ } finally {
+ prog.close();
+ }
+ }
+
+ /**
+ * Utility method to run the pre-compiled query and return the value in the
+ * first column of the first row.
+ */
+ public static long longForQuery(SQLiteStatement prog, String[] selectionArgs) {
+ if (selectionArgs != null) {
+ int size = selectionArgs.length;
+ for (int i = 0; i < size; i++) {
+ bindObjectToProgram(prog, i + 1, selectionArgs[i]);
+ }
+ }
+ long value = prog.simpleQueryForLong();
+ return value;
+ }
+
+ /**
+ * Utility method to run the query on the db and return the value in the
+ * first column of the first row.
+ */
+ public static String stringForQuery(SQLiteDatabase db, String query, String[] selectionArgs) {
+ SQLiteStatement prog = db.compileStatement(query);
+ try {
+ return stringForQuery(prog, selectionArgs);
+ } finally {
+ prog.close();
+ }
+ }
+
+ /**
+ * Utility method to run the pre-compiled query and return the value in the
+ * first column of the first row.
+ */
+ public static String stringForQuery(SQLiteStatement prog, String[] selectionArgs) {
+ if (selectionArgs != null) {
+ int size = selectionArgs.length;
+ for (int i = 0; i < size; i++) {
+ bindObjectToProgram(prog, i + 1, selectionArgs[i]);
+ }
+ }
+ String value = prog.simpleQueryForString();
+ return value;
+ }
+
+ /**
+ * This class allows users to do multiple inserts into a table but
+ * compile the SQL insert statement only once, which may increase
+ * performance.
+ */
+ public static class InsertHelper {
+ private final SQLiteDatabase mDb;
+ private final String mTableName;
+ private HashMap<String, Integer> mColumns;
+ private String mInsertSQL = null;
+ private SQLiteStatement mInsertStatement = null;
+ private SQLiteStatement mReplaceStatement = null;
+ private SQLiteStatement mPreparedStatement = null;
+
+ /**
+ * {@hide}
+ *
+ * These are the columns returned by sqlite's "PRAGMA
+ * table_info(...)" command that we depend on.
+ */
+ public static final int TABLE_INFO_PRAGMA_COLUMNNAME_INDEX = 1;
+ public static final int TABLE_INFO_PRAGMA_DEFAULT_INDEX = 4;
+
+ /**
+ * @param db the SQLiteDatabase to insert into
+ * @param tableName the name of the table to insert into
+ */
+ public InsertHelper(SQLiteDatabase db, String tableName) {
+ mDb = db;
+ mTableName = tableName;
+ }
+
+ private void buildSQL() throws SQLException {
+ StringBuilder sb = new StringBuilder(128);
+ sb.append("INSERT INTO ");
+ sb.append(mTableName);
+ sb.append(" (");
+
+ StringBuilder sbv = new StringBuilder(128);
+ sbv.append("VALUES (");
+
+ int i = 1;
+ Cursor cur = null;
+ try {
+ cur = mDb.rawQuery("PRAGMA table_info(" + mTableName + ")", null);
+ mColumns = new HashMap<String, Integer>(cur.getCount());
+ while (cur.moveToNext()) {
+ String columnName = cur.getString(TABLE_INFO_PRAGMA_COLUMNNAME_INDEX);
+ String defaultValue = cur.getString(TABLE_INFO_PRAGMA_DEFAULT_INDEX);
+
+ mColumns.put(columnName, i);
+ sb.append("'");
+ sb.append(columnName);
+ sb.append("'");
+
+ if (defaultValue == null) {
+ sbv.append("?");
+ } else {
+ sbv.append("COALESCE(?, ");
+ sbv.append(defaultValue);
+ sbv.append(")");
+ }
+
+ sb.append(i == cur.getCount() ? ") " : ", ");
+ sbv.append(i == cur.getCount() ? ");" : ", ");
+ ++i;
+ }
+ } finally {
+ if (cur != null) cur.close();
+ }
+
+ sb.append(sbv);
+
+ mInsertSQL = sb.toString();
+ if (LOCAL_LOGV) Log.v(TAG, "insert statement is " + mInsertSQL);
+ }
+
+ private SQLiteStatement getStatement(boolean allowReplace) throws SQLException {
+ if (allowReplace) {
+ if (mReplaceStatement == null) {
+ if (mInsertSQL == null) buildSQL();
+ // chop "INSERT" off the front and prepend "INSERT OR REPLACE" instead.
+ String replaceSQL = "INSERT OR REPLACE" + mInsertSQL.substring(6);
+ mReplaceStatement = mDb.compileStatement(replaceSQL);
+ }
+ return mReplaceStatement;
+ } else {
+ if (mInsertStatement == null) {
+ if (mInsertSQL == null) buildSQL();
+ mInsertStatement = mDb.compileStatement(mInsertSQL);
+ }
+ return mInsertStatement;
+ }
+ }
+
+ /**
+ * Performs an insert, adding a new row with the given values.
+ *
+ * @param values the set of values with which to populate the
+ * new row
+ * @param allowReplace if true, the statement does "INSERT OR
+ * REPLACE" instead of "INSERT", silently deleting any
+ * previously existing rows that would cause a conflict
+ *
+ * @return the row ID of the newly inserted row, or -1 if an
+ * error occurred
+ */
+ private synchronized long insertInternal(ContentValues values, boolean allowReplace) {
+ try {
+ SQLiteStatement stmt = getStatement(allowReplace);
+ stmt.clearBindings();
+ if (LOCAL_LOGV) Log.v(TAG, "--- inserting in table " + mTableName);
+ for (Map.Entry<String, Object> e: values.valueSet()) {
+ final String key = e.getKey();
+ int i = getColumnIndex(key);
+ DatabaseUtils.bindObjectToProgram(stmt, i, e.getValue());
+ if (LOCAL_LOGV) {
+ Log.v(TAG, "binding " + e.getValue() + " to column " +
+ i + " (" + key + ")");
+ }
+ }
+ return stmt.executeInsert();
+ } catch (SQLException e) {
+ Log.e(TAG, "Error inserting " + values + " into table " + mTableName, e);
+ return -1;
+ }
+ }
+
+ /**
+ * Returns the index of the specified column. This is index is suitagble for use
+ * in calls to bind().
+ * @param key the column name
+ * @return the index of the column
+ */
+ public int getColumnIndex(String key) {
+ getStatement(false);
+ final Integer index = mColumns.get(key);
+ if (index == null) {
+ throw new IllegalArgumentException("column '" + key + "' is invalid");
+ }
+ return index;
+ }
+
+ /**
+ * Bind the value to an index. A prepareForInsert() or prepareForReplace()
+ * without a matching execute() must have already have been called.
+ * @param index the index of the slot to which to bind
+ * @param value the value to bind
+ */
+ public void bind(int index, double value) {
+ mPreparedStatement.bindDouble(index, value);
+ }
+
+ /**
+ * Bind the value to an index. A prepareForInsert() or prepareForReplace()
+ * without a matching execute() must have already have been called.
+ * @param index the index of the slot to which to bind
+ * @param value the value to bind
+ */
+ public void bind(int index, float value) {
+ mPreparedStatement.bindDouble(index, value);
+ }
+
+ /**
+ * Bind the value to an index. A prepareForInsert() or prepareForReplace()
+ * without a matching execute() must have already have been called.
+ * @param index the index of the slot to which to bind
+ * @param value the value to bind
+ */
+ public void bind(int index, long value) {
+ mPreparedStatement.bindLong(index, value);
+ }
+
+ /**
+ * Bind the value to an index. A prepareForInsert() or prepareForReplace()
+ * without a matching execute() must have already have been called.
+ * @param index the index of the slot to which to bind
+ * @param value the value to bind
+ */
+ public void bind(int index, int value) {
+ mPreparedStatement.bindLong(index, value);
+ }
+
+ /**
+ * Bind the value to an index. A prepareForInsert() or prepareForReplace()
+ * without a matching execute() must have already have been called.
+ * @param index the index of the slot to which to bind
+ * @param value the value to bind
+ */
+ public void bind(int index, boolean value) {
+ mPreparedStatement.bindLong(index, value ? 1 : 0);
+ }
+
+ /**
+ * Bind null to an index. A prepareForInsert() or prepareForReplace()
+ * without a matching execute() must have already have been called.
+ * @param index the index of the slot to which to bind
+ */
+ public void bindNull(int index) {
+ mPreparedStatement.bindNull(index);
+ }
+
+ /**
+ * Bind the value to an index. A prepareForInsert() or prepareForReplace()
+ * without a matching execute() must have already have been called.
+ * @param index the index of the slot to which to bind
+ * @param value the value to bind
+ */
+ public void bind(int index, byte[] value) {
+ if (value == null) {
+ mPreparedStatement.bindNull(index);
+ } else {
+ mPreparedStatement.bindBlob(index, value);
+ }
+ }
+
+ /**
+ * Bind the value to an index. A prepareForInsert() or prepareForReplace()
+ * without a matching execute() must have already have been called.
+ * @param index the index of the slot to which to bind
+ * @param value the value to bind
+ */
+ public void bind(int index, String value) {
+ if (value == null) {
+ mPreparedStatement.bindNull(index);
+ } else {
+ mPreparedStatement.bindString(index, value);
+ }
+ }
+
+ /**
+ * Performs an insert, adding a new row with the given values.
+ * If the table contains conflicting rows, an error is
+ * returned.
+ *
+ * @param values the set of values with which to populate the
+ * new row
+ *
+ * @return the row ID of the newly inserted row, or -1 if an
+ * error occurred
+ */
+ public long insert(ContentValues values) {
+ return insertInternal(values, false);
+ }
+
+ /**
+ * Execute the previously prepared insert or replace using the bound values
+ * since the last call to prepareForInsert or prepareForReplace.
+ *
+ * <p>Note that calling bind() and then execute() is not thread-safe. The only thread-safe
+ * way to use this class is to call insert() or replace().
+ *
+ * @return the row ID of the newly inserted row, or -1 if an
+ * error occurred
+ */
+ public long execute() {
+ if (mPreparedStatement == null) {
+ throw new IllegalStateException("you must prepare this inserter before calling "
+ + "execute");
+ }
+ try {
+ if (LOCAL_LOGV) Log.v(TAG, "--- doing insert or replace in table " + mTableName);
+ return mPreparedStatement.executeInsert();
+ } catch (SQLException e) {
+ Log.e(TAG, "Error executing InsertHelper with table " + mTableName, e);
+ return -1;
+ } finally {
+ // you can only call this once per prepare
+ mPreparedStatement = null;
+ }
+ }
+
+ /**
+ * Prepare the InsertHelper for an insert. The pattern for this is:
+ * <ul>
+ * <li>prepareForInsert()
+ * <li>bind(index, value);
+ * <li>bind(index, value);
+ * <li>...
+ * <li>bind(index, value);
+ * <li>execute();
+ * </ul>
+ */
+ public void prepareForInsert() {
+ mPreparedStatement = getStatement(false);
+ mPreparedStatement.clearBindings();
+ }
+
+ /**
+ * Prepare the InsertHelper for a replace. The pattern for this is:
+ * <ul>
+ * <li>prepareForReplace()
+ * <li>bind(index, value);
+ * <li>bind(index, value);
+ * <li>...
+ * <li>bind(index, value);
+ * <li>execute();
+ * </ul>
+ */
+ public void prepareForReplace() {
+ mPreparedStatement = getStatement(true);
+ mPreparedStatement.clearBindings();
+ }
+
+ /**
+ * Performs an insert, adding a new row with the given values.
+ * If the table contains conflicting rows, they are deleted
+ * and replaced with the new row.
+ *
+ * @param values the set of values with which to populate the
+ * new row
+ *
+ * @return the row ID of the newly inserted row, or -1 if an
+ * error occurred
+ */
+ public long replace(ContentValues values) {
+ return insertInternal(values, true);
+ }
+
+ /**
+ * Close this object and release any resources associated with
+ * it. The behavior of calling <code>insert()</code> after
+ * calling this method is undefined.
+ */
+ public void close() {
+ if (mInsertStatement != null) {
+ mInsertStatement.close();
+ mInsertStatement = null;
+ }
+ if (mReplaceStatement != null) {
+ mReplaceStatement.close();
+ mReplaceStatement = null;
+ }
+ mInsertSQL = null;
+ mColumns = null;
+ }
+ }
+
+ /**
+ * Creates a db and populates it with the sql statements in sqlStatements.
+ *
+ * @param context the context to use to create the db
+ * @param dbName the name of the db to create
+ * @param dbVersion the version to set on the db
+ * @param sqlStatements the statements to use to populate the db. This should be a single string
+ * of the form returned by sqlite3's <tt>.dump</tt> command (statements separated by
+ * semicolons)
+ */
+ static public void createDbFromSqlStatements(
+ Context context, String dbName, int dbVersion, String sqlStatements) {
+ SQLiteDatabase db = context.openOrCreateDatabase(dbName, 0, null);
+ // TODO: this is not quite safe since it assumes that all semicolons at the end of a line
+ // terminate statements. It is possible that a text field contains ;\n. We will have to fix
+ // this if that turns out to be a problem.
+ String[] statements = TextUtils.split(sqlStatements, ";\n");
+ for (String statement : statements) {
+ if (TextUtils.isEmpty(statement)) continue;
+ db.execSQL(statement);
+ }
+ db.setVersion(dbVersion);
+ db.close();
+ }
+}
diff --git a/core/java/android/database/IBulkCursor.java b/core/java/android/database/IBulkCursor.java
new file mode 100644
index 0000000..24354fd
--- /dev/null
+++ b/core/java/android/database/IBulkCursor.java
@@ -0,0 +1,90 @@
+/*
+ * 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.database;
+
+import android.os.RemoteException;
+import android.os.IBinder;
+import android.os.IInterface;
+import android.os.Bundle;
+
+import java.util.Map;
+
+/**
+ * This interface provides a low-level way to pass bulk cursor data across
+ * both process and language boundries. Application code should use the Cursor
+ * interface directly.
+ *
+ * {@hide}
+ */
+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.
+ */
+ public CursorWindow getWindow(int startPos) throws RemoteException;
+
+ public void onMove(int position) throws RemoteException;
+
+ /**
+ * Returns the number of rows in the cursor.
+ *
+ * @return the number of rows in the cursor.
+ */
+ public int count() throws RemoteException;
+
+ /**
+ * Returns a string array holding the names of all of the columns in the
+ * cursor in the order in which they were listed in the result.
+ *
+ * @return the names of the columns returned in this query.
+ */
+ public String[] getColumnNames() throws RemoteException;
+
+ public boolean updateRows(Map<? extends Long, ? extends Map<String, Object>> values) throws RemoteException;
+
+ public boolean deleteRow(int position) throws RemoteException;
+
+ public void deactivate() throws RemoteException;
+
+ public void close() throws RemoteException;
+
+ public int requery(IContentObserver observer, CursorWindow window) throws RemoteException;
+
+ boolean getWantsAllOnMoveCalls() throws RemoteException;
+
+ Bundle getExtras() throws RemoteException;
+
+ Bundle respond(Bundle extras) throws RemoteException;
+
+ /* IPC constants */
+ static final String descriptor = "android.content.IBulkCursor";
+
+ static final int GET_CURSOR_WINDOW_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION;
+ static final int COUNT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 1;
+ static final int GET_COLUMN_NAMES_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 2;
+ static final int UPDATE_ROWS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 3;
+ static final int DELETE_ROW_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 4;
+ static final int DEACTIVATE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 5;
+ static final int REQUERY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 6;
+ static final int ON_MOVE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 7;
+ static final int WANTS_ON_MOVE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 8;
+ static final int GET_EXTRAS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 9;
+ static final int RESPOND_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 10;
+ static final int CLOSE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 11;
+}
+
diff --git a/core/java/android/database/IContentObserver.aidl b/core/java/android/database/IContentObserver.aidl
new file mode 100755
index 0000000..ac2f975
--- /dev/null
+++ b/core/java/android/database/IContentObserver.aidl
@@ -0,0 +1,31 @@
+/*
+**
+** Copyright 2007, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.database;
+
+/**
+ * @hide
+ */
+interface IContentObserver
+{
+ /**
+ * This method is called when an update occurs to the cursor that is being
+ * 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);
+}
diff --git a/core/java/android/database/MatrixCursor.java b/core/java/android/database/MatrixCursor.java
new file mode 100644
index 0000000..cf5a573
--- /dev/null
+++ b/core/java/android/database/MatrixCursor.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.database;
+
+import java.util.ArrayList;
+
+/**
+ * A mutable cursor implementation backed by an array of {@code Object}s. Use
+ * {@link #newRow()} to add rows. Automatically expands internal capacity
+ * as needed.
+ */
+public class MatrixCursor extends AbstractCursor {
+
+ private final String[] columnNames;
+ private Object[] data;
+ private int rowCount = 0;
+ private final int columnCount;
+
+ /**
+ * Constructs a new cursor with the given initial capacity.
+ *
+ * @param columnNames names of the columns, the ordering of which
+ * determines column ordering elsewhere in this cursor
+ * @param initialCapacity in rows
+ */
+ public MatrixCursor(String[] columnNames, int initialCapacity) {
+ this.columnNames = columnNames;
+ this.columnCount = columnNames.length;
+
+ if (initialCapacity < 1) {
+ initialCapacity = 1;
+ }
+
+ this.data = new Object[columnCount * initialCapacity];
+ }
+
+ /**
+ * Constructs a new cursor.
+ *
+ * @param columnNames names of the columns, the ordering of which
+ * determines column ordering elsewhere in this cursor
+ */
+ public MatrixCursor(String[] columnNames) {
+ this(columnNames, 16);
+ }
+
+ /**
+ * Gets value at the given column for the current row.
+ */
+ private Object get(int column) {
+ if (column < 0 || column >= columnCount) {
+ throw new CursorIndexOutOfBoundsException("Requested column: "
+ + column + ", # of columns: " + columnCount);
+ }
+ if (mPos < 0) {
+ throw new CursorIndexOutOfBoundsException("Before first row.");
+ }
+ if (mPos >= rowCount) {
+ throw new CursorIndexOutOfBoundsException("After last row.");
+ }
+ return data[mPos * columnCount + column];
+ }
+
+ /**
+ * Adds a new row to the end and returns a builder for that row. Not safe
+ * for concurrent use.
+ *
+ * @return builder which can be used to set the column values for the new
+ * row
+ */
+ public RowBuilder newRow() {
+ rowCount++;
+ int endIndex = rowCount * columnCount;
+ ensureCapacity(endIndex);
+ int start = endIndex - columnCount;
+ return new RowBuilder(start, endIndex);
+ }
+
+ /**
+ * Adds a new row to the end with the given column values. Not safe
+ * for concurrent use.
+ *
+ * @throws IllegalArgumentException if {@code columnValues.length !=
+ * columnNames.length}
+ * @param columnValues in the same order as the the column names specified
+ * at cursor construction time
+ */
+ public void addRow(Object[] columnValues) {
+ if (columnValues.length != columnCount) {
+ throw new IllegalArgumentException("columnNames.length = "
+ + columnCount + ", columnValues.length = "
+ + columnValues.length);
+ }
+
+ int start = rowCount++ * columnCount;
+ ensureCapacity(start + columnCount);
+ System.arraycopy(columnValues, 0, data, start, columnCount);
+ }
+
+ /**
+ * Adds a new row to the end with the given column values. Not safe
+ * for concurrent use.
+ *
+ * @throws IllegalArgumentException if {@code columnValues.size() !=
+ * columnNames.length}
+ * @param columnValues in the same order as the the column names specified
+ * at cursor construction time
+ */
+ public void addRow(Iterable<?> columnValues) {
+ int start = rowCount * columnCount;
+ int end = start + columnCount;
+ ensureCapacity(end);
+
+ if (columnValues instanceof ArrayList<?>) {
+ addRow((ArrayList<?>) columnValues, start);
+ return;
+ }
+
+ int current = start;
+ Object[] localData = data;
+ for (Object columnValue : columnValues) {
+ if (current == end) {
+ // TODO: null out row?
+ throw new IllegalArgumentException(
+ "columnValues.size() > columnNames.length");
+ }
+ localData[current++] = columnValue;
+ }
+
+ if (current != end) {
+ // TODO: null out row?
+ throw new IllegalArgumentException(
+ "columnValues.size() < columnNames.length");
+ }
+
+ // Increase row count here in case we encounter an exception.
+ rowCount++;
+ }
+
+ /** Optimization for {@link ArrayList}. */
+ private void addRow(ArrayList<?> columnValues, int start) {
+ int size = columnValues.size();
+ if (size != columnCount) {
+ throw new IllegalArgumentException("columnNames.length = "
+ + columnCount + ", columnValues.size() = " + size);
+ }
+
+ rowCount++;
+ Object[] localData = data;
+ for (int i = 0; i < size; i++) {
+ localData[start + i] = columnValues.get(i);
+ }
+ }
+
+ /** Ensures that this cursor has enough capacity. */
+ private void ensureCapacity(int size) {
+ if (size > data.length) {
+ Object[] oldData = this.data;
+ int newSize = data.length * 2;
+ if (newSize < size) {
+ newSize = size;
+ }
+ this.data = new Object[newSize];
+ System.arraycopy(oldData, 0, this.data, 0, oldData.length);
+ }
+ }
+
+ /**
+ * Builds a row, starting from the left-most column and adding one column
+ * value at a time. Follows the same ordering as the column names specified
+ * at cursor construction time.
+ */
+ public class RowBuilder {
+
+ private int index;
+ private final int endIndex;
+
+ RowBuilder(int index, int endIndex) {
+ this.index = index;
+ this.endIndex = endIndex;
+ }
+
+ /**
+ * Sets the next column value in this row.
+ *
+ * @throws CursorIndexOutOfBoundsException if you try to add too many
+ * values
+ * @return this builder to support chaining
+ */
+ public RowBuilder add(Object columnValue) {
+ if (index == endIndex) {
+ throw new CursorIndexOutOfBoundsException(
+ "No more columns left.");
+ }
+
+ data[index++] = columnValue;
+ return this;
+ }
+ }
+
+ // AbstractCursor implementation.
+
+ public int getCount() {
+ return rowCount;
+ }
+
+ public String[] getColumnNames() {
+ return columnNames;
+ }
+
+ public String getString(int column) {
+ return String.valueOf(get(column));
+ }
+
+ public short getShort(int column) {
+ Object value = get(column);
+ return (value instanceof String)
+ ? Short.valueOf((String) value)
+ : ((Number) value).shortValue();
+ }
+
+ public int getInt(int column) {
+ Object value = get(column);
+ return (value instanceof String)
+ ? Integer.valueOf((String) value)
+ : ((Number) value).intValue();
+ }
+
+ public long getLong(int column) {
+ Object value = get(column);
+ return (value instanceof String)
+ ? Long.valueOf((String) value)
+ : ((Number) value).longValue();
+ }
+
+ public float getFloat(int column) {
+ Object value = get(column);
+ return (value instanceof String)
+ ? Float.valueOf((String) value)
+ : ((Number) value).floatValue();
+ }
+
+ public double getDouble(int column) {
+ Object value = get(column);
+ return (value instanceof String)
+ ? Double.valueOf((String) value)
+ : ((Number) value).doubleValue();
+ }
+
+ public boolean isNull(int column) {
+ return get(column) == null;
+ }
+}
diff --git a/core/java/android/database/MergeCursor.java b/core/java/android/database/MergeCursor.java
new file mode 100644
index 0000000..7e91159
--- /dev/null
+++ b/core/java/android/database/MergeCursor.java
@@ -0,0 +1,257 @@
+/*
+ * 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.database;
+
+/**
+ * A convience class that lets you present an array of Cursors as a single linear Cursor.
+ * The schema of the cursors presented is entirely up to the creator of the MergeCursor, and
+ * may be different if that is desired. Calls to getColumns, getColumnIndex, etc will return the
+ * value for the row that the MergeCursor is currently pointing at.
+ */
+public class MergeCursor extends AbstractCursor
+{
+ private DataSetObserver mObserver = new DataSetObserver() {
+
+ @Override
+ public void onChanged() {
+ // Reset our position so the optimizations in move-related code
+ // don't screw us over
+ mPos = -1;
+ }
+
+ @Override
+ public void onInvalidated() {
+ mPos = -1;
+ }
+ };
+
+ public MergeCursor(Cursor[] cursors)
+ {
+ mCursors = cursors;
+ mCursor = cursors[0];
+
+ for (int i = 0; i < mCursors.length; i++) {
+ if (mCursors[i] == null) continue;
+
+ mCursors[i].registerDataSetObserver(mObserver);
+ }
+ }
+
+ @Override
+ public int getCount()
+ {
+ int count = 0;
+ int length = mCursors.length;
+ for (int i = 0 ; i < length ; i++) {
+ if (mCursors[i] != null) {
+ count += mCursors[i].getCount();
+ }
+ }
+ return count;
+ }
+
+ @Override
+ public boolean onMove(int oldPosition, int newPosition)
+ {
+ /* Find the right cursor */
+ mCursor = null;
+ int cursorStartPos = 0;
+ int length = mCursors.length;
+ for (int i = 0 ; i < length; i++) {
+ if (mCursors[i] == null) {
+ continue;
+ }
+
+ if (newPosition < (cursorStartPos + mCursors[i].getCount())) {
+ mCursor = mCursors[i];
+ break;
+ }
+
+ cursorStartPos += mCursors[i].getCount();
+ }
+
+ /* Move it to the right position */
+ if (mCursor != null) {
+ boolean ret = mCursor.moveToPosition(newPosition - cursorStartPos);
+ return ret;
+ }
+ return false;
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ @Override
+ public boolean deleteRow()
+ {
+ return mCursor.deleteRow();
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ @Override
+ public boolean commitUpdates() {
+ int length = mCursors.length;
+ for (int i = 0 ; i < length ; i++) {
+ if (mCursors[i] != null) {
+ mCursors[i].commitUpdates();
+ }
+ }
+ onChange(true);
+ return true;
+ }
+
+ @Override
+ public String getString(int column)
+ {
+ return mCursor.getString(column);
+ }
+
+ @Override
+ public short getShort(int column)
+ {
+ return mCursor.getShort(column);
+ }
+
+ @Override
+ public int getInt(int column)
+ {
+ return mCursor.getInt(column);
+ }
+
+ @Override
+ public long getLong(int column)
+ {
+ return mCursor.getLong(column);
+ }
+
+ @Override
+ public float getFloat(int column)
+ {
+ return mCursor.getFloat(column);
+ }
+
+ @Override
+ public double getDouble(int column)
+ {
+ return mCursor.getDouble(column);
+ }
+
+ @Override
+ public boolean isNull(int column)
+ {
+ return mCursor.isNull(column);
+ }
+
+ @Override
+ public byte[] getBlob(int column)
+ {
+ return mCursor.getBlob(column);
+ }
+
+ @Override
+ public String[] getColumnNames()
+ {
+ if (mCursor != null) {
+ return mCursor.getColumnNames();
+ } else {
+ return new String[0];
+ }
+ }
+
+ @Override
+ public void deactivate()
+ {
+ int length = mCursors.length;
+ for (int i = 0 ; i < length ; i++) {
+ if (mCursors[i] != null) {
+ mCursors[i].deactivate();
+ }
+ }
+ }
+
+ @Override
+ public void close() {
+ int length = mCursors.length;
+ for (int i = 0 ; i < length ; i++) {
+ if (mCursors[i] == null) continue;
+ mCursors[i].close();
+ }
+ }
+
+ @Override
+ public void registerContentObserver(ContentObserver observer) {
+ int length = mCursors.length;
+ for (int i = 0 ; i < length ; i++) {
+ if (mCursors[i] != null) {
+ mCursors[i].registerContentObserver(observer);
+ }
+ }
+ }
+ @Override
+ public void unregisterContentObserver(ContentObserver observer) {
+ int length = mCursors.length;
+ for (int i = 0 ; i < length ; i++) {
+ if (mCursors[i] != null) {
+ mCursors[i].unregisterContentObserver(observer);
+ }
+ }
+ }
+
+ @Override
+ public void registerDataSetObserver(DataSetObserver observer) {
+ int length = mCursors.length;
+ for (int i = 0 ; i < length ; i++) {
+ if (mCursors[i] != null) {
+ mCursors[i].registerDataSetObserver(observer);
+ }
+ }
+ }
+
+ @Override
+ public void unregisterDataSetObserver(DataSetObserver observer) {
+ int length = mCursors.length;
+ for (int i = 0 ; i < length ; i++) {
+ if (mCursors[i] != null) {
+ mCursors[i].unregisterDataSetObserver(observer);
+ }
+ }
+ }
+
+ @Override
+ public boolean requery()
+ {
+ int length = mCursors.length;
+ for (int i = 0 ; i < length ; i++) {
+ if (mCursors[i] == null) {
+ continue;
+ }
+
+ if (mCursors[i].requery() == false) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private Cursor mCursor; // updated in onMove
+ private Cursor[] mCursors;
+}
diff --git a/core/java/android/database/Observable.java b/core/java/android/database/Observable.java
new file mode 100644
index 0000000..b6fecab
--- /dev/null
+++ b/core/java/android/database/Observable.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.database;
+
+import java.util.ArrayList;
+
+/**
+ * Provides methods for (un)registering arbitrary observers in an ArrayList.
+ */
+public abstract class Observable<T> {
+ /**
+ * The list of observers. An observer can be in the list at most
+ * once and will never be null.
+ */
+ protected final ArrayList<T> mObservers = new ArrayList<T>();
+
+ /**
+ * Adds an observer to the list. The observer cannot be null and it must not already
+ * be registered.
+ * @param observer the observer to register
+ * @throws IllegalArgumentException the observer is null
+ * @throws IllegalStateException the observer is already registered
+ */
+ public void registerObserver(T observer) {
+ if (observer == null) {
+ throw new IllegalArgumentException("The observer is null.");
+ }
+ synchronized(mObservers) {
+ if (mObservers.contains(observer)) {
+ throw new IllegalStateException("Observer " + observer + " is already registered.");
+ }
+ mObservers.add(observer);
+ }
+ }
+
+ /**
+ * Removes a previously registered observer. The observer must not be null and it
+ * must already have been registered.
+ * @param observer the observer to unregister
+ * @throws IllegalArgumentException the observer is null
+ * @throws IllegalStateException the observer is not yet registered
+ */
+ public void unregisterObserver(T observer) {
+ if (observer == null) {
+ throw new IllegalArgumentException("The observer is null.");
+ }
+ synchronized(mObservers) {
+ int index = mObservers.indexOf(observer);
+ if (index == -1) {
+ throw new IllegalStateException("Observer " + observer + " was not registered.");
+ }
+ mObservers.remove(index);
+ }
+ }
+
+ /**
+ * Remove all registered observer
+ */
+ public void unregisterAll() {
+ synchronized(mObservers) {
+ mObservers.clear();
+ }
+ }
+}
diff --git a/core/java/android/database/SQLException.java b/core/java/android/database/SQLException.java
new file mode 100644
index 0000000..0386af0
--- /dev/null
+++ b/core/java/android/database/SQLException.java
@@ -0,0 +1,30 @@
+/*
+ * 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.database;
+
+/**
+ * An exception that indicates there was an error with SQL parsing or execution.
+ */
+public class SQLException extends RuntimeException
+{
+ public SQLException() {}
+
+ public SQLException(String error)
+ {
+ super(error);
+ }
+}
diff --git a/core/java/android/database/StaleDataException.java b/core/java/android/database/StaleDataException.java
new file mode 100644
index 0000000..ee70beb
--- /dev/null
+++ b/core/java/android/database/StaleDataException.java
@@ -0,0 +1,34 @@
+/*
+ * 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.database;
+
+/**
+ * This exception is thrown when a Cursor contains stale data and must be
+ * requeried before being used again.
+ */
+public class StaleDataException extends java.lang.RuntimeException
+{
+ public StaleDataException()
+ {
+ super();
+ }
+
+ public StaleDataException(String description)
+ {
+ super(description);
+ }
+}
diff --git a/core/java/android/database/package.html b/core/java/android/database/package.html
new file mode 100644
index 0000000..1f76d9f
--- /dev/null
+++ b/core/java/android/database/package.html
@@ -0,0 +1,14 @@
+<HTML>
+<BODY>
+Contains classes to explore data returned through a content provider.
+<p>
+If you need to manage data in a private database, use the {@link
+android.database.sqlite} classes. These classes are used to manage the {@link
+android.database.Cursor} object returned from a content provider query. Databases
+are usually created and opened with {@link android.content.Context#openOrCreateDatabase}
+To make requests through
+content providers, you can use the {@link android.content.ContentResolver
+content.ContentResolver} class.
+<p>All databases are stored on the device in <code>/data/data/&lt;package_name&gt;/databases</code>
+</BODY>
+</HTML>
diff --git a/core/java/android/database/sqlite/SQLiteAbortException.java b/core/java/android/database/sqlite/SQLiteAbortException.java
new file mode 100644
index 0000000..64dc4b7
--- /dev/null
+++ b/core/java/android/database/sqlite/SQLiteAbortException.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.database.sqlite;
+
+/**
+ * An exception that indicates that the SQLite program was aborted.
+ * This can happen either through a call to ABORT in a trigger,
+ * or as the result of using the ABORT conflict clause.
+ */
+public class SQLiteAbortException extends SQLiteException {
+ public SQLiteAbortException() {}
+
+ public SQLiteAbortException(String error) {
+ super(error);
+ }
+}
diff --git a/core/java/android/database/sqlite/SQLiteClosable.java b/core/java/android/database/sqlite/SQLiteClosable.java
new file mode 100644
index 0000000..f64261c
--- /dev/null
+++ b/core/java/android/database/sqlite/SQLiteClosable.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.database.sqlite;
+
+/**
+ * An object create from a SQLiteDatabase that can be closed.
+ */
+public abstract class SQLiteClosable {
+ private int mReferenceCount = 1;
+ private Object mLock = new Object();
+ protected abstract void onAllReferencesReleased();
+ protected void onAllReferencesReleasedFromContainer(){}
+
+ public void acquireReference() {
+ synchronized(mLock) {
+ if (mReferenceCount <= 0) {
+ throw new IllegalStateException(
+ "attempt to acquire a reference on a close SQLiteClosable");
+ }
+ mReferenceCount++;
+ }
+ }
+
+ public void releaseReference() {
+ synchronized(mLock) {
+ mReferenceCount--;
+ if (mReferenceCount == 0) {
+ onAllReferencesReleased();
+ }
+ }
+ }
+
+ public void releaseReferenceFromContainer() {
+ synchronized(mLock) {
+ mReferenceCount--;
+ if (mReferenceCount == 0) {
+ onAllReferencesReleasedFromContainer();
+ }
+ }
+ }
+}
diff --git a/core/java/android/database/sqlite/SQLiteConstraintException.java b/core/java/android/database/sqlite/SQLiteConstraintException.java
new file mode 100644
index 0000000..e3119eb
--- /dev/null
+++ b/core/java/android/database/sqlite/SQLiteConstraintException.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.database.sqlite;
+
+/**
+ * An exception that indicates that an integrity constraint was violated.
+ */
+public class SQLiteConstraintException extends SQLiteException {
+ public SQLiteConstraintException() {}
+
+ public SQLiteConstraintException(String error) {
+ super(error);
+ }
+}
diff --git a/core/java/android/database/sqlite/SQLiteCursor.java b/core/java/android/database/sqlite/SQLiteCursor.java
new file mode 100644
index 0000000..ae2fc95
--- /dev/null
+++ b/core/java/android/database/sqlite/SQLiteCursor.java
@@ -0,0 +1,450 @@
+/*
+ * 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.database.sqlite;
+
+import android.database.AbstractWindowedCursor;
+import android.database.CursorWindow;
+import android.database.SQLException;
+import android.text.TextUtils;
+import android.util.Config;
+import android.util.Log;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+/**
+ * A Cursor implementation that exposes results from a query on a
+ * {@link SQLiteDatabase}.
+ */
+public class SQLiteCursor extends AbstractWindowedCursor {
+ static final String TAG = "Cursor";
+ static final int NO_COUNT = -1;
+
+ /** The name of the table to edit */
+ private String mEditTable;
+
+ /** The names of the columns in the rows */
+ private String[] mColumns;
+
+ /** The query object for the cursor */
+ private SQLiteQuery mQuery;
+
+ /** The database the cursor was created from */
+ private SQLiteDatabase mDatabase;
+
+ /** The compiled query this cursor came from */
+ private SQLiteCursorDriver mDriver;
+
+ /** The number of rows in the cursor */
+ private int mCount = NO_COUNT;
+
+ /** A mapping of column names to column indices, to speed up lookups */
+ private Map<String, Integer> mColumnNameMap;
+
+ /** Used to find out where a cursor was allocated in case it never got
+ * released. */
+ private StackTraceElement[] mStackTraceElements;
+
+ /**
+ * Execute a query and provide access to its result set through a Cursor
+ * 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.
+ *
+ * @param db a reference to a Database object that is already constructed
+ * and opened
+ * @param editTable the name of the table used for this query
+ * @param query the rest of the query terms
+ * cursor is finalized
+ */
+ public SQLiteCursor(SQLiteDatabase db, SQLiteCursorDriver driver,
+ String editTable, SQLiteQuery query) {
+ // The AbstractCursor constructor needs to do some setup.
+ super();
+
+ if (SQLiteDebug.DEBUG_ACTIVE_CURSOR_FINALIZATION) {
+ mStackTraceElements = new Exception().getStackTrace();
+ }
+
+ mDatabase = db;
+ mDriver = driver;
+ mEditTable = editTable;
+ mColumnNameMap = null;
+ mQuery = query;
+
+ try {
+ db.lock();
+
+ // 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 (Config.LOGV) {
+ 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;
+ }
+ }
+ } finally {
+ db.unlock();
+ }
+ }
+
+ /**
+ * @return the SQLiteDatabase that this cursor is associated with.
+ */
+ public SQLiteDatabase getDatabase() {
+ return mDatabase;
+ }
+
+ @Override
+ public boolean onMove(int oldPosition, int newPosition) {
+ // Make sure the row at newPosition is present in the window
+ if (mWindow == null || newPosition < mWindow.getStartPosition() ||
+ newPosition >= (mWindow.getStartPosition() + mWindow.getNumRows())) {
+ fillWindow(newPosition);
+ }
+
+ return true;
+ }
+
+ @Override
+ public int getCount() {
+ if (mCount == NO_COUNT) {
+ fillWindow(0);
+ }
+ return mCount;
+ }
+
+ private void fillWindow (int startPos) {
+ if (mWindow == null) {
+ // If there isn't a window set already it will only be accessed locally
+ mWindow = new CursorWindow(true /* the window is local only */);
+ } else {
+ mWindow.clear();
+ }
+
+ // mWindow must be cleared
+ mCount = mQuery.fillWindow(mWindow, startPos);
+ }
+
+ @Override
+ public int getColumnIndex(String columnName) {
+ // Create mColumnNameMap on demand
+ if (mColumnNameMap == null) {
+ String[] columns = mColumns;
+ int columnCount = columns.length;
+ HashMap<String, Integer> map = new HashMap<String, Integer>(columnCount, 1);
+ for (int i = 0; i < columnCount; i++) {
+ map.put(columns[i], i);
+ }
+ mColumnNameMap = map;
+ }
+
+ // Hack according to bug 903852
+ final int periodIndex = columnName.lastIndexOf('.');
+ if (periodIndex != -1) {
+ Exception e = new Exception();
+ Log.e(TAG, "requesting column name with table name -- " + columnName, e);
+ columnName = columnName.substring(periodIndex + 1);
+ }
+
+ Integer i = mColumnNameMap.get(columnName);
+ if (i != null) {
+ return i.intValue();
+ } else {
+ return -1;
+ }
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ @Override
+ public boolean deleteRow() {
+ checkPosition();
+
+ // Only allow deletes if there is an ID column, and the ID has been read from it
+ if (mRowIdColumnIndex == -1 || mCurrentRowID == null) {
+ Log.e(TAG,
+ "Could not delete row because either the row ID column is not available or it" +
+ "has not been read.");
+ return false;
+ }
+
+ boolean success;
+
+ /*
+ * Ensure we don't change the state of the database when another
+ * thread is holding the database lock. requery() and moveTo() are also
+ * synchronized here to make sure they get the state of the database
+ * immediately following the DELETE.
+ */
+ mDatabase.lock();
+ try {
+ try {
+ mDatabase.delete(mEditTable, mColumns[mRowIdColumnIndex] + "=?",
+ new String[] {mCurrentRowID.toString()});
+ success = true;
+ } catch (SQLException e) {
+ success = false;
+ }
+
+ int pos = mPos;
+ requery();
+
+ /*
+ * Ensure proper cursor state. Note that mCurrentRowID changes
+ * in this call.
+ */
+ moveToPosition(pos);
+ } finally {
+ mDatabase.unlock();
+ }
+
+ if (success) {
+ onChange(true);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public String[] getColumnNames() {
+ return mColumns;
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ @Override
+ public boolean supportsUpdates() {
+ return super.supportsUpdates() && !TextUtils.isEmpty(mEditTable);
+ }
+
+ /**
+ * @hide
+ * @deprecated
+ */
+ @Override
+ public boolean commitUpdates(Map<? extends Long,
+ ? extends Map<String, Object>> additionalValues) {
+ if (!supportsUpdates()) {
+ Log.e(TAG, "commitUpdates not supported on this cursor, did you "
+ + "include the _id column?");
+ return false;
+ }
+
+ /*
+ * Prevent other threads from changing the updated rows while they're
+ * being processed here.
+ */
+ synchronized (mUpdatedRows) {
+ if (additionalValues != null) {
+ mUpdatedRows.putAll(additionalValues);
+ }
+
+ if (mUpdatedRows.size() == 0) {
+ return true;
+ }
+
+ /*
+ * Prevent other threads from changing the database state while
+ * we process the updated rows, and prevents us from changing the
+ * database behind the back of another thread.
+ */
+ mDatabase.beginTransaction();
+ try {
+ StringBuilder sql = new StringBuilder(128);
+
+ // For each row that has been updated
+ for (Map.Entry<Long, Map<String, Object>> rowEntry :
+ mUpdatedRows.entrySet()) {
+ Map<String, Object> values = rowEntry.getValue();
+ Long rowIdObj = rowEntry.getKey();
+
+ if (rowIdObj == null || values == null) {
+ throw new IllegalStateException("null rowId or values found! rowId = "
+ + rowIdObj + ", values = " + values);
+ }
+
+ if (values.size() == 0) {
+ continue;
+ }
+
+ long rowId = rowIdObj.longValue();
+
+ Iterator<Map.Entry<String, Object>> valuesIter =
+ values.entrySet().iterator();
+
+ sql.setLength(0);
+ sql.append("UPDATE " + mEditTable + " SET ");
+
+ // For each column value that has been updated
+ Object[] bindings = new Object[values.size()];
+ int i = 0;
+ while (valuesIter.hasNext()) {
+ Map.Entry<String, Object> entry = valuesIter.next();
+ sql.append(entry.getKey());
+ sql.append("=?");
+ bindings[i] = entry.getValue();
+ if (valuesIter.hasNext()) {
+ sql.append(", ");
+ }
+ i++;
+ }
+
+ sql.append(" WHERE " + mColumns[mRowIdColumnIndex]
+ + '=' + rowId);
+ sql.append(';');
+ mDatabase.execSQL(sql.toString(), bindings);
+ mDatabase.rowUpdated(mEditTable, rowId);
+ }
+ mDatabase.setTransactionSuccessful();
+ } finally {
+ mDatabase.endTransaction();
+ }
+
+ mUpdatedRows.clear();
+ }
+
+ // Let any change observers know about the update
+ onChange(true);
+
+ return true;
+ }
+
+ private void deactivateCommon() {
+ if (Config.LOGV) Log.v(TAG, "<<< Releasing cursor " + this);
+ if (mWindow != null) {
+ mWindow.close();
+ mWindow = null;
+ }
+ if (Config.LOGV) Log.v("DatabaseWindow", "closing window in release()");
+ }
+
+ @Override
+ public void deactivate() {
+ super.deactivate();
+ deactivateCommon();
+ mDriver.cursorDeactivated();
+ }
+
+ @Override
+ public void close() {
+ super.close();
+ deactivateCommon();
+ mQuery.close();
+ mDriver.cursorClosed();
+ }
+
+ @Override
+ public boolean requery() {
+ long timeStart = 0;
+ if (Config.LOGV) {
+ timeStart = System.currentTimeMillis();
+ }
+ /*
+ * Synchronize on the database lock to ensure that mCount matches the
+ * results of mQuery.requery().
+ */
+ mDatabase.lock();
+ try {
+ if (mWindow != null) {
+ mWindow.clear();
+ }
+ mPos = -1;
+ // This one will recreate the temp table, and get its count
+ mDriver.cursorRequeried(this);
+ mCount = NO_COUNT;
+ // Requery the program that runs over the temp table
+ mQuery.requery();
+ } finally {
+ mDatabase.unlock();
+ }
+
+ if (Config.LOGV) {
+ Log.v("DatabaseWindow", "closing window in requery()");
+ Log.v(TAG, "--- Requery()ed cursor " + this + ": " + mQuery);
+ }
+
+ boolean result = super.requery();
+ if (Config.LOGV) {
+ long timeEnd = System.currentTimeMillis();
+ Log.v(TAG, "requery (" + (timeEnd - timeStart) + " ms): " + mDriver.toString());
+ }
+ return result;
+ }
+
+ @Override
+ public void setWindow(CursorWindow window) {
+ if (mWindow != null) {
+ mWindow.close();
+ mCount = NO_COUNT;
+ }
+ mWindow = window;
+ }
+
+ /**
+ * Changes the selection arguments. The new values take effect after a call to requery().
+ */
+ public void setSelectionArguments(String[] selectionArgs) {
+ mDriver.setBindArguments(selectionArgs);
+ }
+
+ /**
+ * Release the native resources, if they haven't been released yet.
+ */
+ @Override
+ protected void finalize() {
+ try {
+ if (mWindow != null) {
+ close();
+ String message = "Finalizing cursor " + this + " on " + mEditTable
+ + " that has not been deactivated or closed";
+ if (SQLiteDebug.DEBUG_ACTIVE_CURSOR_FINALIZATION) {
+ Log.d(TAG, message + "\nThis cursor was created in:");
+ for (StackTraceElement ste : mStackTraceElements) {
+ Log.d(TAG, " " + ste);
+ }
+ }
+ SQLiteDebug.notifyActiveCursorFinalized();
+ throw new IllegalStateException(message);
+ } else {
+ if (Config.LOGV) {
+ Log.v(TAG, "Finalizing cursor " + this + " on " + mEditTable);
+ }
+ }
+ } finally {
+ super.finalize();
+ }
+ }
+}
diff --git a/core/java/android/database/sqlite/SQLiteCursorDriver.java b/core/java/android/database/sqlite/SQLiteCursorDriver.java
new file mode 100644
index 0000000..eda1b78
--- /dev/null
+++ b/core/java/android/database/sqlite/SQLiteCursorDriver.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.database.sqlite;
+
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase.CursorFactory;
+
+/**
+ * A driver for SQLiteCursors that is used to create them and gets notified
+ * by the cursors it creates on significant events in their lifetimes.
+ */
+public interface SQLiteCursorDriver {
+ /**
+ * Executes the query returning a Cursor over the result set.
+ *
+ * @param factory The CursorFactory to use when creating the Cursors, or
+ * null if standard SQLiteCursors should be returned.
+ * @return a Cursor over the result set
+ */
+ Cursor query(CursorFactory factory, String[] bindArgs);
+
+ /**
+ * Called by a SQLiteCursor when it is released.
+ */
+ void cursorDeactivated();
+
+ /**
+ * Called by a SQLiteCursor when it is requeryed.
+ *
+ * @return The new count value.
+ */
+ void cursorRequeried(Cursor cursor);
+
+ /**
+ * Called by a SQLiteCursor when it it closed to destroy this object as well.
+ */
+ void cursorClosed();
+
+ /**
+ * Set new bind arguments. These will take effect in cursorRequeried().
+ * @param bindArgs the new arguments
+ */
+ public void setBindArguments(String[] bindArgs);
+}
diff --git a/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java
new file mode 100644
index 0000000..e497190
--- /dev/null
+++ b/core/java/android/database/sqlite/SQLiteDatabase.java
@@ -0,0 +1,1512 @@
+/*
+ * 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.database.sqlite;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.SQLException;
+import android.os.Debug;
+import android.os.SystemClock;
+import android.text.TextUtils;
+import android.util.Config;
+import android.util.Log;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.WeakHashMap;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * Exposes methods to manage a SQLite database.
+ * <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
+ * and managing a database.
+ * <p> Database names must be unique within an application, not across all
+ * applications.
+ *
+ * <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.
+ */
+public class SQLiteDatabase extends SQLiteClosable {
+ private final static String TAG = "Database";
+
+ /**
+ * Maximum Length Of A LIKE Or GLOB Pattern
+ * The pattern matching algorithm used in the default LIKE and GLOB implementation
+ * of SQLite can exhibit O(N^2) performance (where N is the number of characters in
+ * the pattern) for certain pathological cases. To avoid denial-of-service attacks
+ * the length of the LIKE or GLOB pattern is limited to SQLITE_MAX_LIKE_PATTERN_LENGTH bytes.
+ * The default value of this limit is 50000. A modern workstation can evaluate
+ * even a pathological LIKE or GLOB pattern of 50000 bytes relatively quickly.
+ * The denial of service problem only comes into play when the pattern length gets
+ * into millions of bytes. Nevertheless, since most useful LIKE or GLOB patterns
+ * are at most a few dozen bytes in length, paranoid application developers may
+ * want to reduce this parameter to something in the range of a few hundred
+ * if they know that external users are able to generate arbitrary patterns.
+ */
+ public static final int SQLITE_MAX_LIKE_PATTERN_LENGTH = 50000;
+
+ /**
+ * 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.
+ */
+ public static final int OPEN_READWRITE = 0x00000000; // update native code if changing
+
+ /**
+ * 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
+
+ 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.
+ *
+ * {@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
+ * created with. If this is set, {@link #setLocale} will do nothing.
+ */
+ 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.
+ */
+ 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;
+
+ /** Synchronize on this when accessing the database */
+ private final ReentrantLock mLock = new ReentrantLock(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 long mLastLockMessageTime = 0L;
+
+ /** Used by native code, do not rename */
+ /* package */ int mNativeHandle = 0;
+
+ /** Used to make temp table names unique */
+ /* package */ int mTempTableSequence = 0;
+
+ /** The path for the database file */
+ private String mPath;
+
+ /** The flags passed to open/create */
+ private int mFlags;
+
+ /** The optional factory to use when creating new Cursors */
+ private CursorFactory mFactory;
+
+ private WeakHashMap<SQLiteClosable, Object> mPrograms;
+
+ private final RuntimeException mLeakedException;
+ /**
+ * @param closable
+ */
+ void addSQLiteClosable(SQLiteClosable closable) {
+ lock();
+ try {
+ mPrograms.put(closable, null);
+ } finally {
+ unlock();
+ }
+ }
+
+ void removeSQLiteClosable(SQLiteClosable closable) {
+ lock();
+ try {
+ mPrograms.remove(closable);
+ } finally {
+ unlock();
+ }
+ }
+
+ @Override
+ protected void onAllReferencesReleased() {
+ if (isOpen()) {
+ dbclose();
+ }
+ }
+
+ /**
+ * Attempts to release memory that SQLite holds but does not require to
+ * operate properly. Typically this memory will come from the page cache.
+ *
+ * @return the number of bytes actually released
+ */
+ static public native int releaseMemory();
+
+ /**
+ * Control whether or not the SQLiteDatabase is made thread-safe by using locks
+ * around critical sections. This is pretty expensive, so if you know that your
+ * 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
+ */
+ public void setLockingEnabled(boolean lockingEnabled) {
+ mLockingEnabled = lockingEnabled;
+ }
+
+ /**
+ * If set then the SQLiteDatabase is made thread-safe by using locks
+ * around critical sections
+ */
+ private boolean mLockingEnabled = true;
+
+ /* package */ void onCorruption() {
+ try {
+ // Close the database (if we can), which will cause subsequent operations to fail.
+ close();
+ } finally {
+ Log.e(TAG, "Removing corrupt database: " + mPath);
+ // Delete the corrupt file. Don't re-create it now -- that would just confuse people
+ // -- but the next time someone tries to open it, they can set it up from scratch.
+ new File(mPath).delete();
+ }
+ }
+
+ /**
+ * 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()
+ */
+ /* package */ void lock() {
+ if (!mLockingEnabled) return;
+ mLock.lock();
+ 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();
+ }
+ }
+ }
+
+ /**
+ * 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.
+ *
+ * @see #unlockForced()
+ */
+ private void lockForced() {
+ mLock.lock();
+ 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();
+ }
+ }
+ }
+
+ /**
+ * 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();
+ }
+ }
+ mLock.unlock();
+ }
+
+ /**
+ * Releases the database lock.
+ *
+ * @see #unlockForced()
+ */
+ private void unlockForced() {
+ if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING) {
+ if (mLock.getHoldCount() == 1) {
+ checkLockHoldTime();
+ }
+ }
+ mLock.unlock();
+ }
+
+ 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);
+ }
+ }
+ }
+ }
+
+ /**
+ * Begins a transaction. Transactions can be nested. When the outer transaction is ended all of
+ * the work done in that transaction and all of the nested transactions will be committed or
+ * rolled back. The changes will be rolled back if any transaction is ended without being
+ * marked as clean (by calling setTransactionSuccessful). Otherwise they will be committed.
+ *
+ * <p>Here is the standard idiom for transactions:
+ *
+ * <pre>
+ * db.beginTransaction();
+ * try {
+ * ...
+ * db.setTransactionSuccessful();
+ * } finally {
+ * db.endTransaction();
+ * }
+ * </pre>
+ */
+ public void beginTransaction() {
+ lockForced();
+ 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.
+ execSQL("BEGIN EXCLUSIVE;");
+ mTransactionIsSuccessful = true;
+ mInnerTransactionIsSuccessful = false;
+ ok = true;
+ } finally {
+ if (!ok) {
+ // beginTransaction is called before the try block so we must release the lock in
+ // the case of failure.
+ unlockForced();
+ }
+ }
+ }
+
+ /**
+ * End a transaction. See beginTransaction for notes about how to use this and when transactions
+ * are committed and rolled back.
+ */
+ public void endTransaction() {
+ if (!mLock.isHeldByCurrentThread()) {
+ throw new IllegalStateException("no transaction pending");
+ }
+ try {
+ if (mInnerTransactionIsSuccessful) {
+ mInnerTransactionIsSuccessful = false;
+ } else {
+ mTransactionIsSuccessful = false;
+ }
+ if (mLock.getHoldCount() != 1) {
+ return;
+ }
+ if (mTransactionIsSuccessful) {
+ execSQL("COMMIT;");
+ } else {
+ execSQL("ROLLBACK;");
+ }
+ } finally {
+ unlockForced();
+ if (Config.LOGV) {
+ Log.v(TAG, "unlocked " + Thread.currentThread()
+ + ", holdCount is " + mLock.getHoldCount());
+ }
+ }
+ }
+
+ /**
+ * Marks the current transaction as successful. Do not do any more database work between
+ * calling this and calling endTransaction. Do as little non-database work as possible in that
+ * situation too. If any errors are encountered between this and endTransaction the transaction
+ * will still be committed.
+ *
+ * @throws IllegalStateException if the current thread is not in a transaction or the
+ * transaction is already marked as successful.
+ */
+ public void setTransactionSuccessful() {
+ 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;
+ }
+
+ /**
+ * return true if there is a transaction pending
+ */
+ public boolean inTransaction() {
+ return mLock.getHoldCount() > 0;
+ }
+
+ /**
+ * Checks if the database lock is held by this thread.
+ *
+ * @return true, if this thread is holding the database lock.
+ */
+ public boolean isDbLockedByCurrentThread() {
+ return mLock.isHeldByCurrentThread();
+ }
+
+ /**
+ * 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.
+ *
+ * @return true, if the database is locked by another thread
+ */
+ public boolean isDbLockedByOtherThreads() {
+ return !mLock.isHeldByCurrentThread() && mLock.isLocked();
+ }
+
+ /**
+ * Temporarily end the transaction to let other threads run. The transaction is assumed to be
+ * successful so far. Do not call setTransactionSuccessful before calling this. When this
+ * returns a new transaction will have been created but not marked as successful.
+ * @return true if the transaction was yielded
+ */
+ public boolean yieldIfContended() {
+ 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();
+ endTransaction();
+ beginTransaction();
+ return true;
+ }
+
+ /** Maps table names to info about what to which _sync_time column to set
+ * to NULL on an update. This is used to support syncing. */
+ private final Map<String, SyncUpdateInfo> mSyncUpdateInfo =
+ new HashMap<String, SyncUpdateInfo>();
+
+ public Map<String, String> getSyncedTables() {
+ synchronized(mSyncUpdateInfo) {
+ HashMap<String, String> tables = new HashMap<String, String>();
+ for (String table : mSyncUpdateInfo.keySet()) {
+ SyncUpdateInfo info = mSyncUpdateInfo.get(table);
+ if (info.deletedTable != null) {
+ tables.put(table, info.deletedTable);
+ }
+ }
+ return tables;
+ }
+ }
+
+ /**
+ * Internal class used to keep track what needs to be marked as changed
+ * when an update occurs. This is used for syncing, so the sync engine
+ * knows what data has been updated locally.
+ */
+ static private class SyncUpdateInfo {
+ /**
+ * Creates the SyncUpdateInfo class.
+ *
+ * @param masterTable The table to set _sync_time to NULL in
+ * @param deletedTable The deleted table that corresponds to the
+ * master table
+ * @param foreignKey The key that refers to the primary key in table
+ */
+ SyncUpdateInfo(String masterTable, String deletedTable,
+ String foreignKey) {
+ this.masterTable = masterTable;
+ this.deletedTable = deletedTable;
+ this.foreignKey = foreignKey;
+ }
+
+ /** The table containing the _sync_time column */
+ String masterTable;
+
+ /** The deleted table that corresponds to the master table */
+ String deletedTable;
+
+ /** The key in the local table the row in table. It may be _id, if table
+ * is the local table. */
+ String foreignKey;
+ }
+
+ /**
+ * Used to allow returning sub-classes of {@link Cursor} when calling query.
+ */
+ public interface CursorFactory {
+ /**
+ * See
+ * {@link SQLiteCursor#SQLiteCursor(SQLiteDatabase, 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}.
+ *
+ * <p>Sets the locale of the database to the the system's current locale.
+ * Call {@link #setLocale} if you would like something else.</p>
+ *
+ * @param path to database file to open and/or create
+ * @param factory an optional factory class that is called to instantiate a
+ * cursor when query is called, or null for default
+ * @param flags to control database access mode
+ * @return the newly opened database
+ * @throws SQLiteException if the database cannot be opened
+ */
+ public static SQLiteDatabase openDatabase(String path, CursorFactory factory, int flags) {
+ SQLiteDatabase db = null;
+ try {
+ // Open the database.
+ return new SQLiteDatabase(path, factory, flags);
+ } catch (SQLiteDatabaseCorruptException e) {
+ // Try to recover from this, if we can.
+ // TODO: should we do this for other open failures?
+ Log.e(TAG, "Deleting and re-creating corrupt database " + path, e);
+ new File(path).delete();
+ return new SQLiteDatabase(path, factory, flags);
+ }
+ }
+
+ /**
+ * Equivalent to openDatabase(file.getPath(), factory, CREATE_IF_NECESSARY).
+ */
+ public static SQLiteDatabase openOrCreateDatabase(File file, CursorFactory factory) {
+ return openOrCreateDatabase(file.getPath(), factory);
+ }
+
+ /**
+ * Equivalent to openDatabase(path, factory, CREATE_IF_NECESSARY).
+ */
+ public static SQLiteDatabase openOrCreateDatabase(String path, CursorFactory factory) {
+ return openDatabase(path, factory, CREATE_IF_NECESSARY);
+ }
+
+ /**
+ * Create a memory backed SQLite database. Its contents will be destroyed
+ * when the database is closed.
+ *
+ * <p>Sets the locale of the database to the the system's current locale.
+ * Call {@link #setLocale} if you would like something else.</p>
+ *
+ * @param factory an optional factory class that is called to instantiate a
+ * cursor when query is called
+ * @return a SQLiteDatabase object, or null if the database can't be created
+ */
+ public static SQLiteDatabase create(CursorFactory factory) {
+ // This is a magic string with special meaning for SQLite.
+ return openDatabase(":memory:", factory, CREATE_IF_NECESSARY);
+ }
+
+ /**
+ * Close the database.
+ */
+ public void close() {
+ lock();
+ try {
+ closeClosable();
+ releaseReference();
+ } finally {
+ unlock();
+ }
+ }
+
+ private void closeClosable() {
+ 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();
+ }
+ }
+ }
+
+ /**
+ * Native call to close the database.
+ */
+ private native void dbclose();
+
+ /**
+ * Gets the database version.
+ *
+ * @return the database version
+ */
+ public int getVersion() {
+ SQLiteStatement prog = null;
+ lock();
+ try {
+ prog = new SQLiteStatement(this, "PRAGMA user_version;");
+ long version = prog.simpleQueryForLong();
+ return (int) version;
+ } finally {
+ if (prog != null) prog.close();
+ unlock();
+ }
+ }
+
+ /**
+ * Sets the database version.
+ *
+ * @param version the new database version
+ */
+ public void setVersion(int version) {
+ execSQL("PRAGMA user_version = " + version);
+ }
+
+ /**
+ * Returns the maximum size the database may grow to.
+ *
+ * @return the new maximum database size
+ */
+ public long getMaximumSize() {
+ SQLiteStatement prog = null;
+ lock();
+ try {
+ prog = new SQLiteStatement(this,
+ "PRAGMA max_page_count;");
+ long pageCount = prog.simpleQueryForLong();
+ return pageCount * getPageSize();
+ } finally {
+ if (prog != null) prog.close();
+ unlock();
+ }
+ }
+
+ /**
+ * Sets the maximum size the database will grow to. The maximum size cannot
+ * be set below the current size.
+ *
+ * @param numBytes the maximum database size, in bytes
+ * @return the new maximum database size
+ */
+ public long setMaximumSize(long numBytes) {
+ SQLiteStatement prog = null;
+ lock();
+ try {
+ long pageSize = getPageSize();
+ long numPages = numBytes / pageSize;
+ // If numBytes isn't a multiple of pageSize, bump up a page
+ if ((numBytes % pageSize) != 0) {
+ numPages++;
+ }
+ prog = new SQLiteStatement(this,
+ "PRAGMA max_page_count = " + numPages);
+ long newPageCount = prog.simpleQueryForLong();
+ return newPageCount * pageSize;
+ } finally {
+ if (prog != null) prog.close();
+ unlock();
+ }
+ }
+
+ /**
+ * Returns the maximum size the database may grow to.
+ *
+ * @return the new maximum database size
+ */
+ public long getPageSize() {
+ SQLiteStatement prog = null;
+ lock();
+ try {
+ prog = new SQLiteStatement(this,
+ "PRAGMA page_size;");
+ long size = prog.simpleQueryForLong();
+ return size;
+ } finally {
+ if (prog != null) prog.close();
+ unlock();
+ }
+ }
+
+ /**
+ * Sets the database page size. The page size must be a power of two. This
+ * method does not work if any data has been written to the database file,
+ * and must be called right after the database has been created.
+ *
+ * @param numBytes the database page size, in bytes
+ */
+ public void setPageSize(long numBytes) {
+ execSQL("PRAGMA page_size = " + numBytes);
+ }
+
+ /**
+ * Mark this table as syncable. When an update occurs in this table the
+ * _sync_dirty field will be set to ensure proper syncing operation.
+ *
+ * @param table the table to mark as syncable
+ * @param deletedTable The deleted table that corresponds to the
+ * syncable table
+ */
+ public void markTableSyncable(String table, String deletedTable) {
+ markTableSyncable(table, "_id", table, deletedTable);
+ }
+
+ /**
+ * Mark this table as syncable, with the _sync_dirty residing in another
+ * table. When an update occurs in this table the _sync_dirty field of the
+ * row in updateTable with the _id in foreignKey will be set to
+ * ensure proper syncing operation.
+ *
+ * @param table an update on this table will trigger a sync time removal
+ * @param foreignKey this is the column in table whose value is an _id in
+ * updateTable
+ * @param updateTable this is the table that will have its _sync_dirty
+ */
+ public void markTableSyncable(String table, String foreignKey,
+ String updateTable) {
+ markTableSyncable(table, foreignKey, updateTable, null);
+ }
+
+ /**
+ * Mark this table as syncable, with the _sync_dirty residing in another
+ * table. When an update occurs in this table the _sync_dirty field of the
+ * row in updateTable with the _id in foreignKey will be set to
+ * ensure proper syncing operation.
+ *
+ * @param table an update on this table will trigger a sync time removal
+ * @param foreignKey this is the column in table whose value is an _id in
+ * updateTable
+ * @param updateTable this is the table that will have its _sync_dirty
+ * @param deletedTable The deleted table that corresponds to the
+ * updateTable
+ */
+ private void markTableSyncable(String table, String foreignKey,
+ String updateTable, String deletedTable) {
+ lock();
+ try {
+ native_execSQL("SELECT _sync_dirty FROM " + updateTable
+ + " LIMIT 0");
+ native_execSQL("SELECT " + foreignKey + " FROM " + table
+ + " LIMIT 0");
+ } finally {
+ unlock();
+ }
+
+ SyncUpdateInfo info = new SyncUpdateInfo(updateTable, deletedTable,
+ foreignKey);
+ synchronized (mSyncUpdateInfo) {
+ mSyncUpdateInfo.put(table, info);
+ }
+ }
+
+ /**
+ * Call for each row that is updated in a cursor.
+ *
+ * @param table the table the row is in
+ * @param rowId the row ID of the updated row
+ */
+ /* package */ void rowUpdated(String table, long rowId) {
+ SyncUpdateInfo info;
+ synchronized (mSyncUpdateInfo) {
+ info = mSyncUpdateInfo.get(table);
+ }
+ if (info != null) {
+ execSQL("UPDATE " + info.masterTable
+ + " SET _sync_dirty=1 WHERE _id=(SELECT " + info.foreignKey
+ + " FROM " + table + " WHERE _id=" + rowId + ")");
+ }
+ }
+
+ /**
+ * Finds the name of the first table, which is editable.
+ *
+ * @param tables a list of tables
+ * @return the first table listed
+ */
+ public static String findEditTable(String tables) {
+ if (!TextUtils.isEmpty(tables)) {
+ // find the first word terminated by either a space or a comma
+ int spacepos = tables.indexOf(' ');
+ int commapos = tables.indexOf(',');
+
+ if (spacepos > 0 && (spacepos < commapos || commapos < 0)) {
+ return tables.substring(0, spacepos);
+ } else if (commapos > 0 && (commapos < spacepos || spacepos < 0) ) {
+ return tables.substring(0, commapos);
+ }
+ return tables;
+ } else {
+ throw new IllegalStateException("Invalid tables");
+ }
+ }
+
+ /**
+ * Compiles an SQL statement into a reusable pre-compiled statement object.
+ * The parameters are identical to {@link #execSQL(String)}. You may put ?s in the
+ * statement and fill in those values with {@link SQLiteProgram#bindString}
+ * and {@link SQLiteProgram#bindLong} each time you want to run the
+ * statement. Statements may not return result sets larger than 1x1.
+ *
+ * @param sql The raw SQL statement, may contain ? for unknown values to be
+ * bound later.
+ * @return a pre-compiled statement object.
+ */
+ public SQLiteStatement compileStatement(String sql) throws SQLException {
+ lock();
+ try {
+ return new SQLiteStatement(this, sql);
+ } finally {
+ unlock();
+ }
+ }
+
+ /**
+ * 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.
+ * @return A Cursor object, which is positioned before the first entry
+ * @see Cursor
+ */
+ public Cursor query(boolean distinct, String table, String[] columns,
+ String selection, String[] selectionArgs, String groupBy,
+ String having, String orderBy, String limit) {
+ return queryWithFactory(null, distinct, table, columns, selection, selectionArgs,
+ groupBy, having, orderBy, limit);
+ }
+
+ /**
+ * 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.
+ * @return A Cursor object, which is positioned before the first entry
+ * @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) {
+ String sql = SQLiteQueryBuilder.buildQueryString(
+ distinct, table, columns, selection, groupBy, having, orderBy, limit);
+
+ return rawQueryWithFactory(
+ cursorFactory, sql, selectionArgs, findEditTable(table));
+ }
+
+ /**
+ * Query the given table, returning a {@link Cursor} over the result set.
+ *
+ * @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.
+ * @return A {@link Cursor} object, which is positioned before the first entry
+ * @see Cursor
+ */
+ public Cursor query(String table, String[] columns, String selection,
+ String[] selectionArgs, String groupBy, String having,
+ String orderBy) {
+
+ return query(false, table, columns, selection, selectionArgs, groupBy,
+ having, orderBy, null /* limit */);
+ }
+
+ /**
+ * Query the given table, returning a {@link Cursor} over the result set.
+ *
+ * @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.
+ * @return A {@link Cursor} object, which is positioned before the first entry
+ * @see Cursor
+ */
+ public Cursor query(String table, String[] columns, String selection,
+ String[] selectionArgs, String groupBy, String having,
+ String orderBy, String limit) {
+
+ return query(false, table, columns, selection, selectionArgs, groupBy,
+ having, orderBy, limit);
+ }
+
+ /**
+ * 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.
+ * @return A {@link Cursor} object, which is positioned before the first entry
+ */
+ public Cursor rawQuery(String sql, String[] selectionArgs) {
+ return rawQueryWithFactory(null, sql, selectionArgs, null);
+ }
+
+ /**
+ * 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
+ * @return A {@link Cursor} object, which is positioned before the first entry
+ */
+ public Cursor rawQueryWithFactory(
+ CursorFactory cursorFactory, String sql, String[] selectionArgs,
+ String editTable) {
+ long timeStart = 0;
+
+ if (Config.LOGV) {
+ timeStart = System.currentTimeMillis();
+ }
+
+ SQLiteCursorDriver driver = new SQLiteDirectCursorDriver(this, sql, editTable);
+
+ try {
+ return driver.query(
+ cursorFactory != null ? cursorFactory : mFactory,
+ selectionArgs);
+ } finally {
+ if (Config.LOGV) {
+ long duration = System.currentTimeMillis() - timeStart;
+
+ Log.v(SQLiteCursor.TAG,
+ "query (" + duration + " ms): " + driver.toString() + ", args are "
+ + (selectionArgs != null
+ ? TextUtils.join(",", selectionArgs)
+ : "<null>"));
+ }
+ }
+ }
+
+ /**
+ * Convenience method for inserting a row into the database.
+ *
+ * @param table the table to insert the row into
+ * @param nullColumnHack SQL doesn't allow inserting a completely empty row,
+ * so if initialValues is empty this column will explicitly be
+ * assigned a NULL value
+ * @param values this map contains the initial column values for the
+ * row. The keys should be the column names and the values the
+ * column values
+ * @return the row ID of the newly inserted row, or -1 if an error occurred
+ */
+ public long insert(String table, String nullColumnHack, ContentValues values) {
+ try {
+ return insertOrReplace(table, nullColumnHack, values, false);
+ } catch (SQLException e) {
+ Log.e(TAG, "Error inserting " + values, e);
+ return -1;
+ }
+ }
+
+ /**
+ * Convenience method for inserting a row into the database.
+ *
+ * @param table the table to insert the row into
+ * @param nullColumnHack SQL doesn't allow inserting a completely empty row,
+ * so if initialValues is empty this column will explicitly be
+ * assigned a NULL value
+ * @param values this map contains the initial column values for the
+ * row. The keys should be the column names and the values the
+ * column values
+ * @throws SQLException
+ * @return the row ID of the newly inserted row, or -1 if an error occurred
+ */
+ public long insertOrThrow(String table, String nullColumnHack, ContentValues values)
+ throws SQLException {
+ return insertOrReplace(table, nullColumnHack, values, false) ;
+ }
+
+ /**
+ * Convenience method for replacing a row in the database.
+ *
+ * @param table the table in which to replace the row
+ * @param nullColumnHack SQL doesn't allow inserting a completely empty row,
+ * so if initialValues is empty this row will explicitly be
+ * assigned a NULL value
+ * @param initialValues this map contains the initial column values for
+ * the row. The key
+ * @return the row ID of the newly inserted row, or -1 if an error occurred
+ */
+ public long replace(String table, String nullColumnHack, ContentValues initialValues) {
+ try {
+ return insertOrReplace(table, nullColumnHack, initialValues, true);
+ } catch (SQLException e) {
+ Log.e(TAG, "Error inserting " + initialValues, e);
+ return -1;
+ }
+ }
+
+ /**
+ * Convenience method for replacing a row in the database.
+ *
+ * @param table the table in which to replace the row
+ * @param nullColumnHack SQL doesn't allow inserting a completely empty row,
+ * so if initialValues is empty this row will explicitly be
+ * assigned a NULL value
+ * @param initialValues this map contains the initial column values for
+ * the row. The key
+ * @throws SQLException
+ * @return the row ID of the newly inserted row, or -1 if an error occurred
+ */
+ public long replaceOrThrow(String table, String nullColumnHack,
+ ContentValues initialValues) throws SQLException {
+ return insertOrReplace(table, nullColumnHack, initialValues, true);
+ }
+
+ private long insertOrReplace(String table, String nullColumnHack,
+ ContentValues initialValues, boolean allowReplace) {
+ if (!isOpen()) {
+ throw new IllegalStateException("database not open");
+ }
+
+ // Measurements show most sql lengths <= 152
+ StringBuilder sql = new StringBuilder(152);
+ sql.append("INSERT ");
+ if (allowReplace) {
+ sql.append("OR REPLACE ");
+ }
+ sql.append("INTO ");
+ sql.append(table);
+ // Measurements show most values lengths < 40
+ StringBuilder values = new StringBuilder(40);
+
+ Set<Map.Entry<String, Object>> entrySet = null;
+ if (initialValues != null && initialValues.size() > 0) {
+ entrySet = initialValues.valueSet();
+ Iterator<Map.Entry<String, Object>> entriesIter = entrySet.iterator();
+ sql.append('(');
+
+ boolean needSeparator = false;
+ while (entriesIter.hasNext()) {
+ if (needSeparator) {
+ sql.append(", ");
+ values.append(", ");
+ }
+ needSeparator = true;
+ Map.Entry<String, Object> entry = entriesIter.next();
+ sql.append(entry.getKey());
+ values.append('?');
+ }
+
+ sql.append(')');
+ } else {
+ sql.append("(" + nullColumnHack + ") ");
+ values.append("NULL");
+ }
+
+ sql.append(" VALUES(");
+ sql.append(values);
+ sql.append(");");
+
+ lock();
+ SQLiteStatement statement = null;
+ try {
+ statement = compileStatement(sql.toString());
+
+ // Bind the values
+ if (entrySet != null) {
+ int size = entrySet.size();
+ Iterator<Map.Entry<String, Object>> entriesIter = entrySet.iterator();
+ for (int i = 0; i < size; i++) {
+ Map.Entry<String, Object> entry = entriesIter.next();
+ DatabaseUtils.bindObjectToProgram(statement, i + 1, entry.getValue());
+ }
+ }
+
+ // Run the program and then cleanup
+ statement.execute();
+
+ long insertedRowId = lastInsertRow();
+ if (insertedRowId == -1) {
+ Log.e(TAG, "Error inserting " + initialValues + " using " + sql);
+ } else {
+ if (Config.LOGD && Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "Inserting row " + insertedRowId + " from "
+ + initialValues + " using " + sql);
+ }
+ }
+ return insertedRowId;
+ } catch (SQLiteDatabaseCorruptException e) {
+ onCorruption();
+ throw e;
+ } finally {
+ if (statement != null) {
+ statement.close();
+ }
+ unlock();
+ }
+ }
+
+ /**
+ * Convenience method for deleting rows in the database.
+ *
+ * @param table the table to delete from
+ * @param whereClause the optional WHERE clause to apply when deleting.
+ * Passing null will delete all rows.
+ * @return the number of rows affected if a whereClause is passed in, 0
+ * otherwise. To remove all rows and get a count pass "1" as the
+ * whereClause.
+ */
+ public int delete(String table, String whereClause, String[] whereArgs) {
+ if (!isOpen()) {
+ throw new IllegalStateException("database not open");
+ }
+ lock();
+ SQLiteStatement statement = null;
+ try {
+ statement = compileStatement("DELETE FROM " + table
+ + (!TextUtils.isEmpty(whereClause)
+ ? " WHERE " + whereClause : ""));
+ if (whereArgs != null) {
+ int numArgs = whereArgs.length;
+ for (int i = 0; i < numArgs; i++) {
+ DatabaseUtils.bindObjectToProgram(statement, i + 1, whereArgs[i]);
+ }
+ }
+ statement.execute();
+ statement.close();
+ return lastChangeCount();
+ } catch (SQLiteDatabaseCorruptException e) {
+ onCorruption();
+ throw e;
+ } finally {
+ if (statement != null) {
+ statement.close();
+ }
+ unlock();
+ }
+ }
+
+ /**
+ * Convenience method for updating rows in the database.
+ *
+ * @param table the table to update in
+ * @param values a map from column names to new column values. null is a
+ * valid value that will be translated to NULL.
+ * @param whereClause the optional WHERE clause to apply when updating.
+ * Passing null will update all rows.
+ * @return the number of rows affected
+ */
+ public int update(String table, ContentValues values, String whereClause, String[] whereArgs) {
+ if (!isOpen()) {
+ throw new IllegalStateException("database not open");
+ }
+
+ if (values == null || values.size() == 0) {
+ throw new IllegalArgumentException("Empty values");
+ }
+
+ StringBuilder sql = new StringBuilder(120);
+ sql.append("UPDATE ");
+ sql.append(table);
+ sql.append(" SET ");
+
+ Set<Map.Entry<String, Object>> entrySet = values.valueSet();
+ Iterator<Map.Entry<String, Object>> entriesIter = entrySet.iterator();
+
+ while (entriesIter.hasNext()) {
+ Map.Entry<String, Object> entry = entriesIter.next();
+ sql.append(entry.getKey());
+ sql.append("=?");
+ if (entriesIter.hasNext()) {
+ sql.append(", ");
+ }
+ }
+
+ if (!TextUtils.isEmpty(whereClause)) {
+ sql.append(" WHERE ");
+ sql.append(whereClause);
+ }
+
+ lock();
+ SQLiteStatement statement = null;
+ try {
+ statement = compileStatement(sql.toString());
+
+ // Bind the values
+ int size = entrySet.size();
+ entriesIter = entrySet.iterator();
+ int bindArg = 1;
+ for (int i = 0; i < size; i++) {
+ Map.Entry<String, Object> entry = entriesIter.next();
+ DatabaseUtils.bindObjectToProgram(statement, bindArg, entry.getValue());
+ bindArg++;
+ }
+
+ if (whereArgs != null) {
+ size = whereArgs.length;
+ for (int i = 0; i < size; i++) {
+ statement.bindString(bindArg, whereArgs[i]);
+ bindArg++;
+ }
+ }
+
+ // Run the program and then cleanup
+ statement.execute();
+ statement.close();
+ int numChangedRows = lastChangeCount();
+ if (Config.LOGD && Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "Updated " + numChangedRows + " using " + values + " and " + sql);
+ }
+ return numChangedRows;
+ } catch (SQLiteDatabaseCorruptException e) {
+ onCorruption();
+ throw e;
+ } catch (SQLException e) {
+ Log.e(TAG, "Error updating " + values + " using " + sql);
+ throw e;
+ } finally {
+ if (statement != null) {
+ statement.close();
+ }
+ unlock();
+ }
+ }
+
+ /**
+ * Execute a single SQL statement that is not a query. For example, CREATE
+ * TABLE, DELETE, INSERT, etc. Multiple statements separated by ;s are not
+ * supported. it takes a write lock
+ *
+ * @throws SQLException If the SQL string is invalid for some reason
+ */
+ public void execSQL(String sql) throws SQLException {
+ long timeStart = 0;
+ if (Config.LOGV) {
+ timeStart = System.currentTimeMillis();
+ }
+ lock();
+ try {
+ native_execSQL(sql);
+ } catch (SQLiteDatabaseCorruptException e) {
+ onCorruption();
+ throw e;
+ } finally {
+ unlock();
+ }
+ if (Config.LOGV) {
+ long timeEnd = System.currentTimeMillis();
+ Log.v(TAG, "Executed (" + (timeEnd - timeStart) + " ms):" + sql);
+ }
+ }
+
+ /**
+ * Execute a single SQL statement that is not a query. For example, CREATE
+ * TABLE, DELETE, INSERT, etc. Multiple statements separated by ;s are not
+ * supported. it takes a write lock,
+ *
+ * @param sql
+ * @param bindArgs only byte[], String, Long and Double are supported in bindArgs.
+ * @throws SQLException If the SQL string is invalid for some reason
+ */
+ public void execSQL(String sql, Object[] bindArgs) throws SQLException {
+ if (bindArgs == null) {
+ throw new IllegalArgumentException("Empty bindArgs");
+ }
+ long timeStart = 0;
+ if (Config.LOGV) {
+ timeStart = System.currentTimeMillis();
+ }
+ lock();
+ SQLiteStatement statement = null;
+ try {
+ statement = compileStatement(sql);
+ if (bindArgs != null) {
+ int numArgs = bindArgs.length;
+ for (int i = 0; i < numArgs; i++) {
+ DatabaseUtils.bindObjectToProgram(statement, i + 1, bindArgs[i]);
+ }
+ }
+ statement.execute();
+ } catch (SQLiteDatabaseCorruptException e) {
+ onCorruption();
+ throw e;
+ } finally {
+ if (statement != null) {
+ statement.close();
+ }
+ unlock();
+ }
+ if (Config.LOGV) {
+ long timeEnd = System.currentTimeMillis();
+ Log.v(TAG, "Executed (" + (timeEnd - timeStart) + " ms):" + sql);
+ }
+ }
+
+ @Override
+ protected void finalize() {
+ if (isOpen()) {
+ if (mPrograms.isEmpty()) {
+ Log.e(TAG, "Leak found", mLeakedException);
+ } else {
+ IllegalStateException leakProgram = new IllegalStateException(
+ "mPrograms size " + mPrograms.size(), mLeakedException);
+ Log.e(TAG, "Leak found", leakProgram);
+ }
+ closeClosable();
+ onAllReferencesReleased();
+ }
+ }
+
+ /**
+ * Private constructor. See {@link createDatabase} and {@link openDatabase}.
+ *
+ * @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.
+ */
+ private SQLiteDatabase(String path, CursorFactory factory, int flags) {
+ if (path == null) {
+ throw new IllegalArgumentException("path should not be null");
+ }
+ mFlags = flags;
+ mPath = path;
+ mLeakedException = new IllegalStateException(path +
+ " SQLiteDatabase created and never closed");
+ mFactory = factory;
+ dbopen(mPath, mFlags);
+ mPrograms = new WeakHashMap<SQLiteClosable,Object>();
+ try {
+ setLocale(Locale.getDefault());
+ } catch (RuntimeException e) {
+ Log.e(TAG, "Failed to setLocale() when constructing, closing the database", e);
+ dbclose();
+ throw e;
+ }
+ }
+
+ /**
+ * return whether the DB is opened as read only.
+ * @return true if DB is opened as read only
+ */
+ public boolean isReadOnly() {
+ return (mFlags & OPEN_READ_MASK) == OPEN_READONLY;
+ }
+
+ /**
+ * @return true if the DB is currently open (has not been closed)
+ */
+ public boolean isOpen() {
+ return mNativeHandle != 0;
+ }
+
+ public boolean needUpgrade(int newVersion) {
+ return newVersion > getVersion();
+ }
+
+ /**
+ * Getter for the path to the database file.
+ *
+ * @return the path to our database file.
+ */
+ public final String getPath() {
+ return mPath;
+ }
+
+ /**
+ * Sets the locale for this database. Does nothing if this database has
+ * the NO_LOCALIZED_COLLATORS flag set or was opened read only.
+ * @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();
+ }
+ }
+
+ /**
+ * Native call to open the database.
+ *
+ * @param path The full path to the database
+ */
+ private native void dbopen(String path, int flags);
+
+ /**
+ * Native call to execute a raw SQL statement. {@link mLock} must be held
+ * when calling this method.
+ *
+ * @param sql The raw SQL string
+ * @throws SQLException
+ */
+ /* package */ native void native_execSQL(String sql) throws SQLException;
+
+ /**
+ * Native call to set the locale. {@link mLock} must be held when calling
+ * this method.
+ * @throws SQLException
+ */
+ /* package */ native void native_setLocale(String loc, int flags);
+
+ /**
+ * Returns the row ID of the last row inserted into the database.
+ *
+ * @return the row ID of the last row inserted into the database.
+ */
+ /* package */ native long lastInsertRow();
+
+ /**
+ * Returns the number of changes made in the last statement executed.
+ *
+ * @return the number of changes made in the last statement executed.
+ */
+ /* package */ native int lastChangeCount();
+}
diff --git a/core/java/android/database/sqlite/SQLiteDatabaseCorruptException.java b/core/java/android/database/sqlite/SQLiteDatabaseCorruptException.java
new file mode 100644
index 0000000..73b6c0c
--- /dev/null
+++ b/core/java/android/database/sqlite/SQLiteDatabaseCorruptException.java
@@ -0,0 +1,28 @@
+/*
+ * 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.database.sqlite;
+
+/**
+ * An exception that indicates that the SQLite database file is corrupt.
+ */
+public class SQLiteDatabaseCorruptException extends SQLiteException {
+ public SQLiteDatabaseCorruptException() {}
+
+ public SQLiteDatabaseCorruptException(String error) {
+ super(error);
+ }
+}
diff --git a/core/java/android/database/sqlite/SQLiteDebug.java b/core/java/android/database/sqlite/SQLiteDebug.java
new file mode 100644
index 0000000..d04afb0
--- /dev/null
+++ b/core/java/android/database/sqlite/SQLiteDebug.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.database.sqlite;
+
+import android.util.Config;
+
+/**
+ * Provides debugging info about all SQLite databases running in the current process.
+ *
+ * {@hide}
+ */
+public final class SQLiteDebug {
+ /**
+ * Controls the printing of SQL statements as they are executed.
+ */
+ public static final boolean DEBUG_SQL_STATEMENTS = Config.LOGV;
+
+ /**
+ * Controls the stack trace reporting of active cursors being
+ * finalized.
+ */
+ public static final boolean DEBUG_ACTIVE_CURSOR_FINALIZATION = Config.LOGV;
+
+ /**
+ * Controls the tracking of time spent holding the database lock.
+ */
+ public static final boolean DEBUG_LOCK_TIME_TRACKING = false;
+
+ /**
+ * 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 = false;
+
+ /**
+ * Contains statistics about the active pagers in the current process.
+ *
+ * @see #getPagerStats(PagerStats)
+ */
+ public static class PagerStats {
+ /** The total number of bytes in all pagers in the current process */
+ public long totalBytes;
+ /** The number of bytes in referenced pages in all pagers in the current process */
+ public long referencedBytes;
+ /** The number of bytes in all database files opened in the current process */
+ public long databaseBytes;
+ /** The number of pagers opened in the current process */
+ public int numPagers;
+ }
+
+ /**
+ * 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.
+ */
+ 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;
+ }
+
+ static synchronized void notifyActiveCursorFinalized() {
+ sNumActiveCursorsFinalized++;
+ }
+}
diff --git a/core/java/android/database/sqlite/SQLiteDirectCursorDriver.java b/core/java/android/database/sqlite/SQLiteDirectCursorDriver.java
new file mode 100644
index 0000000..ca64aca
--- /dev/null
+++ b/core/java/android/database/sqlite/SQLiteDirectCursorDriver.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.database.sqlite;
+
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase.CursorFactory;
+import android.util.Log;
+
+/**
+ * A cursor driver that uses the given query directly.
+ *
+ * @hide
+ */
+public class SQLiteDirectCursorDriver implements SQLiteCursorDriver {
+ private static String TAG = "SQLiteDirectCursorDriver";
+ private String mEditTable;
+ private SQLiteDatabase mDatabase;
+ private Cursor mCursor;
+ private String mSql;
+ private SQLiteQuery mQuery;
+
+ public SQLiteDirectCursorDriver(SQLiteDatabase db, String sql, String editTable) {
+ mDatabase = db;
+ mEditTable = editTable;
+ //TODO remove all callers that end in ; and remove this check
+ if (sql.charAt(sql.length() - 1) == ';') {
+ Log.w(TAG, "Found SQL string that ends in ; -- " + sql);
+ sql = sql.substring(0, sql.length() - 1);
+ }
+ mSql = sql;
+ }
+
+ public Cursor query(CursorFactory factory, String[] selectionArgs) {
+ // Compile the query
+ SQLiteQuery query = new SQLiteQuery(mDatabase, mSql, 0, selectionArgs);
+
+ try {
+ // Arg binding
+ int numArgs = selectionArgs == null ? 0 : selectionArgs.length;
+ for (int i = 0; i < numArgs; i++) {
+ query.bindString(i + 1, selectionArgs[i]);
+ }
+
+ // Create the cursor
+ if (factory == null) {
+ mCursor = new SQLiteCursor(mDatabase, this, mEditTable, query);
+ } else {
+ mCursor = 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();
+ }
+ }
+
+ public void cursorClosed() {
+ mCursor = null;
+ }
+
+ public void setBindArguments(String[] bindArgs) {
+ final int numArgs = bindArgs.length;
+ for (int i = 0; i < numArgs; i++) {
+ mQuery.bindString(i + 1, bindArgs[i]);
+ }
+ }
+
+ public void cursorDeactivated() {
+ // Do nothing
+ }
+
+ public void cursorRequeried(Cursor cursor) {
+ // Do nothing
+ }
+
+ @Override
+ public String toString() {
+ return "SQLiteDirectCursorDriver: " + mSql;
+ }
+}
diff --git a/core/java/android/database/sqlite/SQLiteDiskIOException.java b/core/java/android/database/sqlite/SQLiteDiskIOException.java
new file mode 100644
index 0000000..01b2069
--- /dev/null
+++ b/core/java/android/database/sqlite/SQLiteDiskIOException.java
@@ -0,0 +1,29 @@
+/*
+ * 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.database.sqlite;
+
+/**
+ * An exception that indicates that an IO error occured while accessing the
+ * SQLite database file.
+ */
+public class SQLiteDiskIOException extends SQLiteException {
+ public SQLiteDiskIOException() {}
+
+ public SQLiteDiskIOException(String error) {
+ super(error);
+ }
+}
diff --git a/core/java/android/database/sqlite/SQLiteDoneException.java b/core/java/android/database/sqlite/SQLiteDoneException.java
new file mode 100644
index 0000000..d6d3f66
--- /dev/null
+++ b/core/java/android/database/sqlite/SQLiteDoneException.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.database.sqlite;
+
+/**
+ * An exception that indicates that the SQLite program is done.
+ * Thrown when an operation that expects a row (such as {@link
+ * SQLiteStatement#simpleQueryForString} or {@link
+ * SQLiteStatement#simpleQueryForLong}) does not get one.
+ */
+public class SQLiteDoneException extends SQLiteException {
+ public SQLiteDoneException() {}
+
+ public SQLiteDoneException(String error) {
+ super(error);
+ }
+}
diff --git a/core/java/android/database/sqlite/SQLiteException.java b/core/java/android/database/sqlite/SQLiteException.java
new file mode 100644
index 0000000..3a97bfb
--- /dev/null
+++ b/core/java/android/database/sqlite/SQLiteException.java
@@ -0,0 +1,30 @@
+/*
+ * 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.database.sqlite;
+
+import android.database.SQLException;
+
+/**
+ * A SQLite exception that indicates there was an error with SQL parsing or execution.
+ */
+public class SQLiteException extends SQLException {
+ public SQLiteException() {}
+
+ public SQLiteException(String error) {
+ super(error);
+ }
+}
diff --git a/core/java/android/database/sqlite/SQLiteFullException.java b/core/java/android/database/sqlite/SQLiteFullException.java
new file mode 100644
index 0000000..582d930
--- /dev/null
+++ b/core/java/android/database/sqlite/SQLiteFullException.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.database.sqlite;
+
+/**
+ * An exception that indicates that the SQLite database is full.
+ */
+public class SQLiteFullException extends SQLiteException {
+ public SQLiteFullException() {}
+
+ public SQLiteFullException(String error) {
+ super(error);
+ }
+}
diff --git a/core/java/android/database/sqlite/SQLiteMisuseException.java b/core/java/android/database/sqlite/SQLiteMisuseException.java
new file mode 100644
index 0000000..685f3ea
--- /dev/null
+++ b/core/java/android/database/sqlite/SQLiteMisuseException.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.database.sqlite;
+
+public class SQLiteMisuseException extends SQLiteException {
+ public SQLiteMisuseException() {}
+
+ public SQLiteMisuseException(String error) {
+ super(error);
+ }
+}
diff --git a/core/java/android/database/sqlite/SQLiteOpenHelper.java b/core/java/android/database/sqlite/SQLiteOpenHelper.java
new file mode 100644
index 0000000..f6872ac
--- /dev/null
+++ b/core/java/android/database/sqlite/SQLiteOpenHelper.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.database.sqlite;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase.CursorFactory;
+import android.util.Log;
+
+/**
+ * A helper class to manage database creation and version management.
+ * You create a subclass implementing {@link #onCreate}, {@link #onUpgrade} and
+ * optionally {@link #onOpen}, and this class takes care of opening the database
+ * if it exists, creating it if it does not, and upgrading it as necessary.
+ * Transactions are used to make sure the database is always in a sensible state.
+ *
+ * @see com.google.provider.NotePad.NotePadProvider
+ */
+public abstract class SQLiteOpenHelper {
+ private static final String TAG = SQLiteOpenHelper.class.getSimpleName();
+
+ private final Context mContext;
+ private final String mName;
+ private final CursorFactory mFactory;
+ private final int mNewVersion;
+
+ private SQLiteDatabase mDatabase = null;
+ private boolean mIsInitializing = false;
+
+ /**
+ * Create a helper object to create, open, and/or manage a database.
+ * The database is not actually created or opened until one of
+ * {@link #getWritableDatabase} or {@link #getReadableDatabase} is called.
+ *
+ * @param context to use to open or create the database
+ * @param name of the database file, or null for an in-memory database
+ * @param factory to use for creating cursor objects, or null for the default
+ * @param version number of the database (starting at 1); if the database is older,
+ * {@link #onUpgrade} will be used to upgrade the database
+ */
+ public SQLiteOpenHelper(Context context, String name, CursorFactory factory, int version) {
+ if (version < 1) throw new IllegalArgumentException("Version must be >= 1, was " + version);
+
+ mContext = context;
+ mName = name;
+ mFactory = factory;
+ mNewVersion = version;
+ }
+
+ /**
+ * Create and/or open a database that will be used for reading and writing.
+ * Once opened successfully, the database is cached, so you can call this
+ * method every time you need to write to the database. Make sure to call
+ * {@link #close} when you no longer need it.
+ *
+ * <p>Errors such as bad permissions or a full disk may cause this operation
+ * to fail, but future attempts may succeed if the problem is fixed.</p>
+ *
+ * @throws SQLiteException if the database cannot be opened for writing
+ * @return a read/write database object valid until {@link #close} is called
+ */
+ public synchronized SQLiteDatabase getWritableDatabase() {
+ if (mDatabase != null && mDatabase.isOpen() && !mDatabase.isReadOnly()) {
+ return mDatabase; // The database is already open for business
+ }
+
+ if (mIsInitializing) {
+ throw new IllegalStateException("getWritableDatabase called recursively");
+ }
+
+ // 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.
+
+ boolean success = false;
+ SQLiteDatabase db = null;
+ if (mDatabase != null) mDatabase.lock();
+ try {
+ mIsInitializing = true;
+ if (mName == null) {
+ db = SQLiteDatabase.create(null);
+ } else {
+ db = mContext.openOrCreateDatabase(mName, 0, mFactory);
+ }
+
+ int version = db.getVersion();
+ if (version != mNewVersion) {
+ db.beginTransaction();
+ try {
+ if (version == 0) {
+ onCreate(db);
+ } else {
+ onUpgrade(db, version, mNewVersion);
+ }
+ db.setVersion(mNewVersion);
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ onOpen(db);
+ success = true;
+ return db;
+ } finally {
+ mIsInitializing = false;
+ if (success) {
+ if (mDatabase != null) {
+ try { mDatabase.close(); } catch (Exception e) { }
+ mDatabase.unlock();
+ }
+ mDatabase = db;
+ } else {
+ if (mDatabase != null) mDatabase.unlock();
+ if (db != null) db.close();
+ }
+ }
+ }
+
+ /**
+ * Create and/or open a database. This will be the same object returned by
+ * {@link #getWritableDatabase} unless some problem, such as a full disk,
+ * requires the database to be opened read-only. In that case, a read-only
+ * database object will be returned. If the problem is fixed, a future call
+ * to {@link #getWritableDatabase} may succeed, in which case the read-only
+ * database object will be closed and the read/write object will be returned
+ * in the future.
+ *
+ * @throws SQLiteException if the database cannot be opened
+ * @return a database object valid until {@link #getWritableDatabase}
+ * or {@link #close} is called.
+ */
+ public synchronized SQLiteDatabase getReadableDatabase() {
+ if (mDatabase != null && mDatabase.isOpen()) {
+ return mDatabase; // The database is already open for business
+ }
+
+ if (mIsInitializing) {
+ throw new IllegalStateException("getReadableDatabase called recursively");
+ }
+
+ try {
+ return getWritableDatabase();
+ } catch (SQLiteException e) {
+ if (mName == null) throw e; // Can't open a temp database read-only!
+ Log.e(TAG, "Couldn't open " + mName + " for writing (will try read-only):", e);
+ }
+
+ SQLiteDatabase db = null;
+ try {
+ mIsInitializing = true;
+ String path = mContext.getDatabasePath(mName).getPath();
+ db = SQLiteDatabase.openDatabase(path, mFactory, SQLiteDatabase.OPEN_READONLY);
+ if (db.getVersion() != mNewVersion) {
+ throw new SQLiteException("Can't upgrade read-only database from version " +
+ db.getVersion() + " to " + mNewVersion + ": " + path);
+ }
+
+ onOpen(db);
+ Log.w(TAG, "Opened " + mName + " in read-only mode");
+ mDatabase = db;
+ return mDatabase;
+ } finally {
+ mIsInitializing = false;
+ if (db != null && db != mDatabase) db.close();
+ }
+ }
+
+ /**
+ * Close any open database object.
+ */
+ public synchronized void close() {
+ if (mIsInitializing) throw new IllegalStateException("Closed during initialization");
+
+ if (mDatabase != null && mDatabase.isOpen()) {
+ mDatabase.close();
+ mDatabase = null;
+ }
+ }
+
+ /**
+ * Called when the database is created for the first time. This is where the
+ * creation of tables and the initial population of the tables should happen.
+ *
+ * @param db The database.
+ */
+ public abstract void onCreate(SQLiteDatabase db);
+
+ /**
+ * Called when the database needs to be upgraded. The implementation
+ * should use this method to drop tables, add tables, or do anything else it
+ * needs to upgrade to the new schema version.
+ *
+ * <p>The SQLite ALTER TABLE documentation can be found
+ * <a href="http://sqlite.org/lang_altertable.html">here</a>. If you add new columns
+ * you can use ALTER TABLE to insert them into a live table. If you rename or remove columns
+ * you can use ALTER TABLE to rename the old table, then create the new table and then
+ * populate the new table with the contents of the old table.
+ *
+ * @param db The database.
+ * @param oldVersion The old database version.
+ * @param newVersion The new database version.
+ */
+ public abstract void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion);
+
+ /**
+ * Called when the database has been opened.
+ * Override method should check {@link SQLiteDatabase#isReadOnly} before
+ * updating the database.
+ *
+ * @param db The database.
+ */
+ public void onOpen(SQLiteDatabase db) {}
+}
diff --git a/core/java/android/database/sqlite/SQLiteProgram.java b/core/java/android/database/sqlite/SQLiteProgram.java
new file mode 100644
index 0000000..e0341a2
--- /dev/null
+++ b/core/java/android/database/sqlite/SQLiteProgram.java
@@ -0,0 +1,262 @@
+/*
+ * 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.database.sqlite;
+
+import android.util.Log;
+
+/**
+ * A base class for compiled SQLite programs.
+ */
+public abstract class SQLiteProgram extends SQLiteClosable {
+ static final String TAG = "SQLiteProgram";
+
+ /** The database this program is compiled against. */
+ protected SQLiteDatabase mDatabase;
+
+ /**
+ * Native linkage, do not modify. This comes from the database and should not be modified
+ * in here or in the native code.
+ */
+ protected int nHandle = 0;
+
+ /**
+ * 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.
+ */
+ protected int nStatement = 0;
+
+ /**
+ * Used to find out where a cursor was allocated in case it never got
+ * released.
+ */
+ private StackTraceElement[] mStackTraceElements;
+
+ /* package */ SQLiteProgram(SQLiteDatabase db, String sql) {
+ if (SQLiteDebug.DEBUG_SQL_STATEMENTS) {
+ mStackTraceElements = new Exception().getStackTrace();
+ }
+
+ mDatabase = db;
+ db.acquireReference();
+ db.addSQLiteClosable(this);
+ this.nHandle = db.mNativeHandle;
+ compile(sql, false);
+ }
+
+ @Override
+ protected void onAllReferencesReleased() {
+ // Note that native_finalize() checks to make sure that nStatement is
+ // non-null before destroying it.
+ native_finalize();
+ mDatabase.releaseReference();
+ mDatabase.removeSQLiteClosable(this);
+ }
+
+ @Override
+ protected void onAllReferencesReleasedFromContainer(){
+ // Note that native_finalize() checks to make sure that nStatement is
+ // non-null before destroying it.
+ native_finalize();
+ mDatabase.releaseReference();
+ }
+
+ /**
+ * Returns a unique identifier for this program.
+ *
+ * @return a unique identifier for this program
+ */
+ public final int getUniqueId() {
+ return nStatement;
+ }
+
+ /**
+ * Compiles the given SQL into a SQLite byte code program using sqlite3_prepare_v2(). If
+ * this method has been called previously without a call to close and forCompilation is set
+ * to false the previous compilation will be used. Setting forceCompilation to true will
+ * always re-compile the program and should be done if you pass differing SQL strings to this
+ * method.
+ *
+ * <P>Note: this method acquires the database lock.</P>
+ *
+ * @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
+ */
+ protected void compile(String sql, boolean forceCompilation) {
+ // Only compile if we don't have a valid statement already or the caller has
+ // explicitly requested a recompile.
+ if (nStatement == 0 || forceCompilation) {
+ mDatabase.lock();
+ try {
+ // Note that the native_compile() takes care of destroying any previously
+ // existing programs before it compiles.
+ acquireReference();
+ native_compile(sql);
+ } finally {
+ releaseReference();
+ mDatabase.unlock();
+ }
+ }
+ }
+
+ /**
+ * Bind a NULL value to this statement. The value remains bound until
+ * {@link #clearBindings} is called.
+ *
+ * @param index The 1-based index to the parameter to bind null to
+ */
+ public void bindNull(int index) {
+ acquireReference();
+ try {
+ native_bind_null(index);
+ } finally {
+ releaseReference();
+ }
+ }
+
+ /**
+ * Bind a long value to this statement. The value remains bound until
+ * {@link #clearBindings} is called.
+ *
+ * @param index The 1-based index to the parameter to bind
+ * @param value The value to bind
+ */
+ public void bindLong(int index, long value) {
+ acquireReference();
+ try {
+ native_bind_long(index, value);
+ } finally {
+ releaseReference();
+ }
+ }
+
+ /**
+ * Bind a double value to this statement. The value remains bound until
+ * {@link #clearBindings} is called.
+ *
+ * @param index The 1-based index to the parameter to bind
+ * @param value The value to bind
+ */
+ public void bindDouble(int index, double value) {
+ acquireReference();
+ try {
+ native_bind_double(index, value);
+ } finally {
+ releaseReference();
+ }
+ }
+
+ /**
+ * Bind a String value to this statement. The value remains bound until
+ * {@link #clearBindings} is called.
+ *
+ * @param index The 1-based index to the parameter to bind
+ * @param value The value to bind
+ */
+ public void bindString(int index, String value) {
+ if (value == null) {
+ throw new IllegalArgumentException("the bind value at index " + index + " is null");
+ }
+ acquireReference();
+ try {
+ native_bind_string(index, value);
+ } finally {
+ releaseReference();
+ }
+ }
+
+ /**
+ * Bind a byte array value to this statement. The value remains bound until
+ * {@link #clearBindings} is called.
+ *
+ * @param index The 1-based index to the parameter to bind
+ * @param value The value to bind
+ */
+ public void bindBlob(int index, byte[] value) {
+ if (value == null) {
+ throw new IllegalArgumentException("the bind value at index " + index + " is null");
+ }
+ acquireReference();
+ try {
+ native_bind_blob(index, value);
+ } finally {
+ releaseReference();
+ }
+ }
+
+ /**
+ * Clears all existing bindings. Unset bindings are treated as NULL.
+ */
+ public void clearBindings() {
+ acquireReference();
+ try {
+ native_clear_bindings();
+ } finally {
+ releaseReference();
+ }
+ }
+
+ /**
+ * Release this program's resources, making it invalid.
+ */
+ public void close() {
+ mDatabase.lock();
+ try {
+ releaseReference();
+ } finally {
+ mDatabase.unlock();
+ }
+ }
+
+ /**
+ * Make sure that the native resource is cleaned up.
+ */
+ @Override
+ protected void finalize() {
+ if (nStatement != 0) {
+ if (SQLiteDebug.DEBUG_SQL_STATEMENTS) {
+ String message = "Finalizing " + this +
+ " that has not been closed";
+
+ Log.d(TAG, message + "\nThis cursor was created in:");
+ for (StackTraceElement ste : mStackTraceElements) {
+ Log.d(TAG, " " + ste);
+ }
+ }
+ onAllReferencesReleased();
+ }
+ }
+
+ /**
+ * Compiles SQL into a SQLite program.
+ *
+ * <P>The database lock must be held when calling this method.
+ * @param sql The SQL to compile.
+ */
+ protected final native void native_compile(String sql);
+ 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();
+}
+
diff --git a/core/java/android/database/sqlite/SQLiteQuery.java b/core/java/android/database/sqlite/SQLiteQuery.java
new file mode 100644
index 0000000..40855b6
--- /dev/null
+++ b/core/java/android/database/sqlite/SQLiteQuery.java
@@ -0,0 +1,182 @@
+/*
+ * 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.database.sqlite;
+
+import android.database.CursorWindow;
+
+/**
+ * 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.
+ */
+public class SQLiteQuery extends SQLiteProgram {
+ //private static final String TAG = "Cursor";
+
+ /** The index of the unbound OFFSET parameter */
+ private int mOffsetIndex;
+
+ /** The SQL used to create this query */
+ private String mQuery;
+
+ /** Args to bind on requery */
+ private String[] mBindArgs;
+
+ 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. It must include "INDEX -1
+ * OFFSET ?" at the end
+ * @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;
+ mQuery = query;
+ mBindArgs = bindArgs;
+ }
+
+ /**
+ * Reads rows into a buffer. This method acquires the database lock.
+ *
+ * @param window The window to fill into
+ * @param startPos The position to start reading rows from
+ * @return number of total rows in the query
+ */
+ /* package */ int fillWindow(CursorWindow window, int startPos) {
+ if (startPos < 0) {
+ throw new IllegalArgumentException("startPos should > 0");
+ }
+ window.setStartPosition(startPos);
+ mDatabase.lock();
+ try {
+ acquireReference();
+ window.acquireReference();
+ return native_fill_window(window, startPos, mOffsetIndex);
+ } catch (IllegalStateException e){
+ // simply ignore it
+ return 0;
+ } catch (SQLiteDatabaseCorruptException e) {
+ mDatabase.onCorruption();
+ throw e;
+ } finally {
+ window.releaseReference();
+ 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 native_column_count();
+ } 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 native_column_name(columnIndex);
+ } finally {
+ releaseReference();
+ }
+ }
+
+ @Override
+ public void close() {
+ super.close();
+ mClosed = true;
+ }
+
+ /**
+ * Called by SQLiteCursor when it is requeried.
+ */
+ /* package */ void requery() {
+ boolean oldMClosed = mClosed;
+ if (mClosed) {
+ mClosed = false;
+ compile(mQuery, false);
+ }
+ if (mBindArgs != null) {
+ int len = mBindArgs.length;
+ try {
+ for (int i = 0; i < len; i++) {
+ super.bindString(i + 1, mBindArgs[i]);
+ }
+ } catch (SQLiteMisuseException e) {
+ StringBuilder errMsg = new StringBuilder
+ ("old mClosed " + oldMClosed + " mQuery " + mQuery);
+ for (int i = 0; i < len; i++) {
+ errMsg.append(" ");
+ errMsg.append(mBindArgs[i]);
+ }
+ errMsg.append(" ");
+ IllegalStateException leakProgram = new IllegalStateException(
+ errMsg.toString(), e);
+ throw leakProgram;
+ }
+ }
+ }
+
+ @Override
+ public void bindNull(int index) {
+ mBindArgs[index - 1] = null;
+ if (!mClosed) super.bindNull(index);
+ }
+
+ @Override
+ public void bindLong(int index, long value) {
+ mBindArgs[index - 1] = Long.toString(value);
+ if (!mClosed) super.bindLong(index, value);
+ }
+
+ @Override
+ public void bindDouble(int index, double value) {
+ mBindArgs[index - 1] = Double.toString(value);
+ if (!mClosed) super.bindDouble(index, value);
+ }
+
+ @Override
+ public void bindString(int index, String value) {
+ mBindArgs[index - 1] = value;
+ if (!mClosed) super.bindString(index, value);
+ }
+
+ private final native int native_fill_window(CursorWindow window, int startPos, int offsetParam);
+
+ private final native int native_column_count();
+
+ private final native String native_column_name(int columnIndex);
+}
diff --git a/core/java/android/database/sqlite/SQLiteQueryBuilder.java b/core/java/android/database/sqlite/SQLiteQueryBuilder.java
new file mode 100644
index 0000000..519a81c
--- /dev/null
+++ b/core/java/android/database/sqlite/SQLiteQueryBuilder.java
@@ -0,0 +1,520 @@
+/*
+ * 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.database.sqlite;
+
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.provider.BaseColumns;
+import android.text.TextUtils;
+import android.util.Config;
+import android.util.Log;
+
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.Map.Entry;
+
+/**
+ * This is a convience class that helps build SQL queries to be sent to
+ * {@link SQLiteDatabase} objects.
+ */
+public class SQLiteQueryBuilder
+{
+ private static final String TAG = "SQLiteQueryBuilder";
+
+ private Map<String, String> mProjectionMap = null;
+ private String mTables = "";
+ private StringBuilder mWhereClause = new StringBuilder(64);
+ private boolean mDistinct;
+ private SQLiteDatabase.CursorFactory mFactory;
+
+ public SQLiteQueryBuilder() {
+ mDistinct = false;
+ mFactory = null;
+ }
+
+ /**
+ * Mark the query as DISTINCT.
+ *
+ * @param distinct if true the query is DISTINCT, otherwise it isn't
+ */
+ public void setDistinct(boolean distinct) {
+ mDistinct = distinct;
+ }
+
+ /**
+ * Returns the list of tables being queried
+ *
+ * @return the list of tables being queried
+ */
+ public String getTables() {
+ return mTables;
+ }
+
+ /**
+ * Sets the list of tables to query. Multiple tables can be specified to perform a join.
+ * For example:
+ * setTables("foo, bar")
+ * setTables("foo LEFT OUTER JOIN bar ON (foo.id = bar.foo_id)")
+ *
+ * @param inTables the list of tables to query on
+ */
+ public void setTables(String inTables) {
+ mTables = inTables;
+ }
+
+ /**
+ * Append a chunk to the WHERE clause of the query. All chunks appended are surrounded
+ * by parenthesis and ANDed with the selection passed to {@link #query}. The final
+ * WHERE clause looks like:
+ *
+ * WHERE (&lt;append chunk 1>&lt;append chunk2>) AND (&lt;query() selection parameter>)
+ *
+ * @param inWhere the chunk of text to append to the WHERE clause.
+ */
+ public void appendWhere(CharSequence inWhere) {
+ if (mWhereClause.length() == 0) {
+ mWhereClause.append('(');
+ }
+ mWhereClause.append(inWhere);
+ }
+
+ /**
+ * Append a chunk to the WHERE clause of the query. All chunks appended are surrounded
+ * by parenthesis and ANDed with the selection passed to {@link #query}. The final
+ * WHERE clause looks like:
+ *
+ * WHERE (&lt;append chunk 1>&lt;append chunk2>) AND (&lt;query() selection parameter>)
+ *
+ * @param inWhere the chunk of text to append to the WHERE clause. it will be escaped
+ * to avoid SQL injection attacks
+ */
+ public void appendWhereEscapeString(String inWhere) {
+ if (mWhereClause.length() == 0) {
+ mWhereClause.append('(');
+ }
+ DatabaseUtils.appendEscapedSQLString(mWhereClause, inWhere);
+ }
+
+ /**
+ * Sets the projection map for the query. The projection map maps
+ * from column names that the caller passes into query to database
+ * column names. This is useful for renaming columns as well as
+ * disambiguating column names when doing joins. For example you
+ * could map "name" to "people.name". If a projection map is set
+ * it must contain all column names the user may request, even if
+ * the key and value are the same.
+ *
+ * @param columnMap maps from the user column names to the database column names
+ */
+ public void setProjectionMap(Map<String, String> columnMap) {
+ mProjectionMap = columnMap;
+ }
+
+ /**
+ * 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
+ */
+ public void setCursorFactory(SQLiteDatabase.CursorFactory factory) {
+ mFactory = factory;
+ }
+
+ /**
+ * Build an SQL query string from the given clauses.
+ *
+ * @param distinct true if you want each row to be unique, false otherwise.
+ * @param tables The table names 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 where 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 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.
+ * @return the SQL query string
+ */
+ public static String buildQueryString(
+ boolean distinct, String tables, String[] columns, String where,
+ String groupBy, String having, String orderBy, String limit) {
+ if (TextUtils.isEmpty(groupBy) && !TextUtils.isEmpty(having)) {
+ throw new IllegalArgumentException(
+ "HAVING clauses are only permitted when using a groupBy clause");
+ }
+
+ StringBuilder query = new StringBuilder(120);
+
+ query.append("SELECT ");
+ if (distinct) {
+ query.append("DISTINCT ");
+ }
+ if (columns != null && columns.length != 0) {
+ appendColumns(query, columns);
+ } else {
+ query.append("* ");
+ }
+ query.append("FROM ");
+ query.append(tables);
+ appendClause(query, " WHERE ", where);
+ appendClause(query, " GROUP BY ", groupBy);
+ appendClause(query, " HAVING ", having);
+ appendClause(query, " ORDER BY ", orderBy);
+ appendClauseEscapeClause(query, " LIMIT ", limit);
+
+ return query.toString();
+ }
+
+ private static void appendClause(StringBuilder s, String name, String clause) {
+ if (!TextUtils.isEmpty(clause)) {
+ s.append(name);
+ s.append(clause);
+ }
+ }
+
+ private static void appendClauseEscapeClause(StringBuilder s, String name, String clause) {
+ if (!TextUtils.isEmpty(clause)) {
+ s.append(name);
+ DatabaseUtils.appendEscapedSQLString(s, clause);
+ }
+ }
+
+ /**
+ * Add the names that are non-null in columns to s, separating
+ * them with commas.
+ */
+ public static void appendColumns(StringBuilder s, String[] columns) {
+ int n = columns.length;
+
+ for (int i = 0; i < n; i++) {
+ String column = columns[i];
+
+ if (column != null) {
+ if (i > 0) {
+ s.append(", ");
+ }
+ s.append(column);
+ }
+ }
+ s.append(' ');
+ }
+
+ /**
+ * 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.
+ * @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) {
+ return query(db, projectionIn, selection, selectionArgs, groupBy, having, sortOrder,
+ null /* limit */);
+ }
+
+ /**
+ * 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.
+ * @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) {
+ if (mTables == null) {
+ return null;
+ }
+
+ String sql = buildQuery(
+ projectionIn, selection, selectionArgs, groupBy, having,
+ sortOrder, limit);
+
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Performing query: " + sql);
+ }
+ return db.rawQueryWithFactory(
+ mFactory, sql, selectionArgs,
+ SQLiteDatabase.findEditTable(mTables));
+ }
+
+ /**
+ * Construct a SELECT statement suitable for use in a group of
+ * SELECT statements that will be joined through UNION operators
+ * in buildUnionQuery.
+ *
+ * @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.
+ * @return the resulting SQL SELECT statement
+ */
+ public String buildQuery(
+ String[] projectionIn, String selection, String[] selectionArgs,
+ String groupBy, String having, String sortOrder, String limit) {
+ String[] projection = computeProjection(projectionIn);
+
+ if (mWhereClause.length() > 0) {
+ mWhereClause.append(')');
+ }
+
+ // Tack on the user's selection, if present.
+ if (selection != null && selection.length() > 0) {
+ if (mWhereClause.length() > 0) {
+ mWhereClause.append(" AND ");
+ }
+
+ mWhereClause.append('(');
+ mWhereClause.append(selection);
+ mWhereClause.append(')');
+ }
+
+ return buildQueryString(
+ mDistinct, mTables, projection, mWhereClause.toString(),
+ groupBy, having, sortOrder, limit);
+ }
+
+ /**
+ * Construct a SELECT statement suitable for use in a group of
+ * SELECT statements that will be joined through UNION operators
+ * in buildUnionQuery.
+ *
+ * @param typeDiscriminatorColumn the name of the result column
+ * whose cells will contain the name of the table from which
+ * each row was drawn.
+ * @param unionColumns the names of the columns to appear in the
+ * result. This may include columns that do not appear in the
+ * table this SELECT is querying (i.e. mTables), but that do
+ * appear in one of the other tables in the UNION query that we
+ * are constructing.
+ * @param columnsPresentInTable a Set of the names of the columns
+ * that appear in this table (i.e. in the table whose name is
+ * mTables). Since columns in unionColumns include columns that
+ * appear only in other tables, we use this array to distinguish
+ * which ones actually are present. Other columns will have
+ * NULL values for results from this subquery.
+ * @param computedColumnsOffset all columns in unionColumns before
+ * this index are included under the assumption that they're
+ * computed and therefore won't appear in columnsPresentInTable,
+ * e.g. "date * 1000 as normalized_date"
+ * @param typeDiscriminatorValue the value used for the
+ * type-discriminator column in this subquery
+ * @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.
+ * @return the resulting SQL SELECT statement
+ */
+ public String buildUnionSubQuery(
+ String typeDiscriminatorColumn,
+ String[] unionColumns,
+ Set<String> columnsPresentInTable,
+ int computedColumnsOffset,
+ String typeDiscriminatorValue,
+ String selection,
+ String[] selectionArgs,
+ String groupBy,
+ String having) {
+ int unionColumnsCount = unionColumns.length;
+ String[] projectionIn = new String[unionColumnsCount];
+
+ for (int i = 0; i < unionColumnsCount; i++) {
+ String unionColumn = unionColumns[i];
+
+ if (unionColumn.equals(typeDiscriminatorColumn)) {
+ projectionIn[i] = "'" + typeDiscriminatorValue + "' AS "
+ + typeDiscriminatorColumn;
+ } else if (i <= computedColumnsOffset
+ || columnsPresentInTable.contains(unionColumn)) {
+ projectionIn[i] = unionColumn;
+ } else {
+ projectionIn[i] = "NULL AS " + unionColumn;
+ }
+ }
+ return buildQuery(
+ projectionIn, selection, selectionArgs, groupBy, having,
+ null /* sortOrder */,
+ null /* limit */);
+ }
+
+ /**
+ * Given a set of subqueries, all of which are SELECT statements,
+ * construct a query that returns the union of what those
+ * subqueries return.
+ * @param subQueries an array of SQL SELECT statements, all of
+ * which must have the same columns as the same positions in
+ * their results
+ * @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 The limit clause, which applies to the entire union result set
+ *
+ * @return the resulting SQL SELECT statement
+ */
+ public String buildUnionQuery(String[] subQueries, String sortOrder, String limit) {
+ StringBuilder query = new StringBuilder(128);
+ int subQueryCount = subQueries.length;
+ String unionOperator = mDistinct ? " UNION " : " UNION ALL ";
+
+ for (int i = 0; i < subQueryCount; i++) {
+ if (i > 0) {
+ query.append(unionOperator);
+ }
+ query.append(subQueries[i]);
+ }
+ appendClause(query, " ORDER BY ", sortOrder);
+ appendClause(query, " LIMIT ", limit);
+ return query.toString();
+ }
+
+ private String[] computeProjection(String[] projectionIn) {
+ if (projectionIn != null && projectionIn.length > 0) {
+ if (mProjectionMap != null) {
+ String[] projection = new String[projectionIn.length];
+ int length = projectionIn.length;
+
+ for (int i = 0; i < length; i++) {
+ String userColumn = projectionIn[i];
+ String column = mProjectionMap.get(userColumn);
+
+ if (column == null) {
+ throw new IllegalArgumentException(
+ "Invalid column " + projectionIn[i]);
+ } else {
+ projection[i] = column;
+ }
+ }
+ return projection;
+ } else {
+ return projectionIn;
+ }
+ } else if (mProjectionMap != null) {
+ // Return all columns in projection map.
+ Set<Entry<String, String>> entrySet = mProjectionMap.entrySet();
+ String[] projection = new String[entrySet.size()];
+ Iterator<Entry<String, String>> entryIter = entrySet.iterator();
+ int i = 0;
+
+ while (entryIter.hasNext()) {
+ Entry<String, String> entry = entryIter.next();
+
+ // Don't include the _count column when people ask for no projection.
+ if (entry.getKey().equals(BaseColumns._COUNT)) {
+ continue;
+ }
+ projection[i++] = entry.getValue();
+ }
+ return projection;
+ }
+ return null;
+ }
+}
diff --git a/core/java/android/database/sqlite/SQLiteStatement.java b/core/java/android/database/sqlite/SQLiteStatement.java
new file mode 100644
index 0000000..bf9361d
--- /dev/null
+++ b/core/java/android/database/sqlite/SQLiteStatement.java
@@ -0,0 +1,118 @@
+/*
+ * 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.database.sqlite;
+
+/**
+ * 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)}
+ */
+public class SQLiteStatement extends SQLiteProgram
+{
+ /**
+ * Don't use SQLiteStatement constructor directly, please use
+ * {@link SQLiteDatabase#compileStatement(String)}
+ * @param db
+ * @param sql
+ */
+ /* package */ SQLiteStatement(SQLiteDatabase db, String sql) {
+ super(db, sql);
+ }
+
+ /**
+ * Execute this SQL statement, if it is not a query. For example,
+ * CREATE TABLE, DELTE, INSERT, etc.
+ *
+ * @throws android.database.SQLException If the SQL string is invalid for
+ * some reason
+ */
+ public void execute() {
+ mDatabase.lock();
+ acquireReference();
+ try {
+ native_execute();
+ } finally {
+ releaseReference();
+ mDatabase.unlock();
+ }
+ }
+
+ /**
+ * Execute this SQL statement and return the ID of the most
+ * recently inserted row. The SQL statement should probably be an
+ * INSERT for this to be a useful call.
+ *
+ * @return the row ID of the last row inserted.
+ *
+ * @throws android.database.SQLException If the SQL string is invalid for
+ * some reason
+ */
+ public long executeInsert() {
+ mDatabase.lock();
+ acquireReference();
+ try {
+ native_execute();
+ return mDatabase.lastInsertRow();
+ } finally {
+ releaseReference();
+ mDatabase.unlock();
+ }
+ }
+
+ /**
+ * Execute a statement that returns a 1 by 1 table with a numeric value.
+ * For example, SELECT COUNT(*) FROM table;
+ *
+ * @return The result of the query.
+ *
+ * @throws android.database.sqlite.SQLiteDoneException if the query returns zero rows
+ */
+ public long simpleQueryForLong() {
+ mDatabase.lock();
+ acquireReference();
+ try {
+ return native_1x1_long();
+ } finally {
+ releaseReference();
+ mDatabase.unlock();
+ }
+ }
+
+ /**
+ * Execute a statement that returns a 1 by 1 table with a text value.
+ * For example, SELECT COUNT(*) FROM table;
+ *
+ * @return The result of the query.
+ *
+ * @throws android.database.sqlite.SQLiteDoneException if the query returns zero rows
+ */
+ public String simpleQueryForString() {
+ mDatabase.lock();
+ acquireReference();
+ try {
+ return native_1x1_string();
+ } finally {
+ releaseReference();
+ mDatabase.unlock();
+ }
+ }
+
+ private final native void native_execute();
+ private final native long native_1x1_long();
+ private final native String native_1x1_string();
+}
diff --git a/core/java/android/database/sqlite/package.html b/core/java/android/database/sqlite/package.html
new file mode 100644
index 0000000..c03a8dc
--- /dev/null
+++ b/core/java/android/database/sqlite/package.html
@@ -0,0 +1,20 @@
+<HTML>
+<BODY>
+Contains the SQLite database management
+classes that an application would use to manage its own private database.
+<p>
+Applications use these classes to maange private databases. If creating a
+content provider, you will probably have to use these classes to create and
+manage your own database to store content. See <a
+href="{@docRoot}devel/data.html">Storing, Retrieving and Exposing Data</a> to learn
+the conventions for implementing a content provider. See the
+NotePadProvider class in the NotePad sample application in the SDK for an
+example of a content provider. Android ships with SQLite version 3.4.0
+<p>If you are working with data sent to you by a provider, you will not use
+these SQLite classes, but instead use the generic {@link android.database}
+classes.
+<p>Android ships with the sqlite3 database tool in the <code>tools/</code>
+folder. You can use this tool to browse or run SQL commands on the device. Run by
+typing <code>sqlite3</code> in a shell window.
+</BODY>
+</HTML>
diff --git a/core/java/android/ddm/DdmHandleAppName.java b/core/java/android/ddm/DdmHandleAppName.java
new file mode 100644
index 0000000..4a57d12
--- /dev/null
+++ b/core/java/android/ddm/DdmHandleAppName.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.ddm;
+
+import org.apache.harmony.dalvik.ddmc.Chunk;
+import org.apache.harmony.dalvik.ddmc.ChunkHandler;
+import org.apache.harmony.dalvik.ddmc.DdmServer;
+import android.util.Config;
+import android.util.Log;
+import java.nio.ByteBuffer;
+
+
+/**
+ * Track our app name. We don't (currently) handle any inbound packets.
+ */
+public class DdmHandleAppName extends ChunkHandler {
+
+ public static final int CHUNK_APNM = type("APNM");
+
+ private volatile static String mAppName = "";
+
+ private static DdmHandleAppName mInstance = new DdmHandleAppName();
+
+
+ /* singleton, do not instantiate */
+ private DdmHandleAppName() {}
+
+ /**
+ * Register for the messages we're interested in.
+ */
+ public static void register() {}
+
+ /**
+ * Called when the DDM server connects. The handler is allowed to
+ * send messages to the server.
+ */
+ public void connected() {}
+
+ /**
+ * Called when the DDM server disconnects. Can be used to disable
+ * periodic transmissions or clean up saved state.
+ */
+ public void disconnected() {}
+
+ /**
+ * Handle a chunk of data.
+ */
+ public Chunk handleChunk(Chunk request) {
+ return null;
+ }
+
+
+
+ /**
+ * Set the application name. Called when we get named, which may be
+ * before or after DDMS connects. For the latter we need to send up
+ * an APNM message.
+ */
+ public static void setAppName(String name) {
+ if (name == null || name.length() == 0)
+ return;
+
+ mAppName = name;
+
+ // if DDMS is already connected, send the app name up
+ sendAPNM(name);
+ }
+
+ public static String getAppName() {
+ return mAppName;
+ }
+
+ /*
+ * Send an APNM (APplication NaMe) chunk.
+ */
+ private static void sendAPNM(String appName) {
+ if (Config.LOGV)
+ Log.v("ddm", "Sending app name");
+
+ ByteBuffer out = ByteBuffer.allocate(4 + appName.length()*2);
+ out.order(ChunkHandler.CHUNK_ORDER);
+ out.putInt(appName.length());
+ putString(out, appName);
+
+ Chunk chunk = new Chunk(CHUNK_APNM, out);
+ DdmServer.sendChunk(chunk);
+ }
+
+}
+
diff --git a/core/java/android/ddm/DdmHandleExit.java b/core/java/android/ddm/DdmHandleExit.java
new file mode 100644
index 0000000..8a0b9a4
--- /dev/null
+++ b/core/java/android/ddm/DdmHandleExit.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.ddm;
+
+import org.apache.harmony.dalvik.ddmc.Chunk;
+import org.apache.harmony.dalvik.ddmc.ChunkHandler;
+import org.apache.harmony.dalvik.ddmc.DdmServer;
+import android.util.Config;
+import android.util.Log;
+import java.nio.ByteBuffer;
+
+/**
+ * Handle an EXIT chunk.
+ */
+public class DdmHandleExit extends ChunkHandler {
+
+ public static final int CHUNK_EXIT = type("EXIT");
+
+ private static DdmHandleExit mInstance = new DdmHandleExit();
+
+
+ /* singleton, do not instantiate */
+ private DdmHandleExit() {}
+
+ /**
+ * Register for the messages we're interested in.
+ */
+ public static void register() {
+ DdmServer.registerHandler(CHUNK_EXIT, mInstance);
+ }
+
+ /**
+ * Called when the DDM server connects. The handler is allowed to
+ * send messages to the server.
+ */
+ public void connected() {}
+
+ /**
+ * Called when the DDM server disconnects. Can be used to disable
+ * periodic transmissions or clean up saved state.
+ */
+ public void disconnected() {}
+
+ /**
+ * Handle a chunk of data. We're only registered for "EXIT".
+ */
+ public Chunk handleChunk(Chunk request) {
+ if (Config.LOGV)
+ Log.v("ddm-exit", "Handling " + name(request.type) + " chunk");
+
+ /*
+ * Process the request.
+ */
+ ByteBuffer in = wrapChunk(request);
+
+ int statusCode = in.getInt();
+
+ Runtime.getRuntime().halt(statusCode);
+
+ // if that doesn't work, return an empty message
+ return null;
+ }
+}
+
diff --git a/core/java/android/ddm/DdmHandleHeap.java b/core/java/android/ddm/DdmHandleHeap.java
new file mode 100644
index 0000000..54457c2
--- /dev/null
+++ b/core/java/android/ddm/DdmHandleHeap.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.ddm;
+
+import org.apache.harmony.dalvik.ddmc.Chunk;
+import org.apache.harmony.dalvik.ddmc.ChunkHandler;
+import org.apache.harmony.dalvik.ddmc.DdmServer;
+import org.apache.harmony.dalvik.ddmc.DdmVmInternal;
+import android.util.Config;
+import android.util.Log;
+import java.nio.ByteBuffer;
+
+/**
+ * Handle thread-related traffic.
+ */
+public class DdmHandleHeap extends ChunkHandler {
+
+ public static final int CHUNK_HPIF = type("HPIF");
+ public static final int CHUNK_HPSG = type("HPSG");
+ public static final int CHUNK_NHSG = type("NHSG");
+ public static final int CHUNK_HPGC = type("HPGC");
+ public static final int CHUNK_REAE = type("REAE");
+ public static final int CHUNK_REAQ = type("REAQ");
+ public static final int CHUNK_REAL = type("REAL");
+
+ private static DdmHandleHeap mInstance = new DdmHandleHeap();
+
+
+ /* singleton, do not instantiate */
+ private DdmHandleHeap() {}
+
+ /**
+ * Register for the messages we're interested in.
+ */
+ public static void register() {
+ DdmServer.registerHandler(CHUNK_HPIF, mInstance);
+ DdmServer.registerHandler(CHUNK_HPSG, mInstance);
+ DdmServer.registerHandler(CHUNK_NHSG, mInstance);
+ DdmServer.registerHandler(CHUNK_HPGC, mInstance);
+ DdmServer.registerHandler(CHUNK_REAE, mInstance);
+ DdmServer.registerHandler(CHUNK_REAQ, mInstance);
+ DdmServer.registerHandler(CHUNK_REAL, mInstance);
+ }
+
+ /**
+ * Called when the DDM server connects. The handler is allowed to
+ * send messages to the server.
+ */
+ public void connected() {}
+
+ /**
+ * Called when the DDM server disconnects. Can be used to disable
+ * periodic transmissions or clean up saved state.
+ */
+ public void disconnected() {}
+
+ /**
+ * Handle a chunk of data.
+ */
+ public Chunk handleChunk(Chunk request) {
+ if (Config.LOGV)
+ Log.v("ddm-heap", "Handling " + name(request.type) + " chunk");
+ int type = request.type;
+
+ if (type == CHUNK_HPIF) {
+ return handleHPIF(request);
+ } else if (type == CHUNK_HPSG) {
+ return handleHPSGNHSG(request, false);
+ } else if (type == CHUNK_NHSG) {
+ return handleHPSGNHSG(request, true);
+ } else if (type == CHUNK_HPGC) {
+ return handleHPGC(request);
+ } else if (type == CHUNK_REAE) {
+ return handleREAE(request);
+ } else if (type == CHUNK_REAQ) {
+ return handleREAQ(request);
+ } else if (type == CHUNK_REAL) {
+ return handleREAL(request);
+ } else {
+ throw new RuntimeException("Unknown packet "
+ + ChunkHandler.name(type));
+ }
+ }
+
+ /*
+ * Handle a "HeaP InFo request".
+ */
+ private Chunk handleHPIF(Chunk request) {
+ ByteBuffer in = wrapChunk(request);
+
+ int when = in.get();
+ if (Config.LOGV)
+ Log.v("ddm-heap", "Heap segment enable: when=" + when);
+
+ boolean ok = DdmVmInternal.heapInfoNotify(when);
+ if (!ok) {
+ return createFailChunk(1, "Unsupported HPIF what");
+ } else {
+ return null; // empty response
+ }
+ }
+
+ /*
+ * Handle a "HeaP SeGment" or "Native Heap SeGment" request.
+ */
+ private Chunk handleHPSGNHSG(Chunk request, boolean isNative) {
+ ByteBuffer in = wrapChunk(request);
+
+ int when = in.get();
+ int what = in.get();
+ if (Config.LOGV)
+ Log.v("ddm-heap", "Heap segment enable: when=" + when
+ + ", what=" + what + ", isNative=" + isNative);
+
+ boolean ok = DdmVmInternal.heapSegmentNotify(when, what, isNative);
+ if (!ok) {
+ return createFailChunk(1, "Unsupported HPSG what/when");
+ } else {
+ // TODO: if "when" is non-zero and we want to see a dump
+ // right away, initiate a GC.
+ return null; // empty response
+ }
+ }
+
+ /*
+ * Handle a "HeaP Garbage Collection" request.
+ */
+ private Chunk handleHPGC(Chunk request) {
+ //ByteBuffer in = wrapChunk(request);
+
+ if (Config.LOGD)
+ Log.d("ddm-heap", "Heap GC request");
+ System.gc();
+
+ return null; // empty response
+ }
+
+ /*
+ * Handle a "REcent Allocation Enable" request.
+ */
+ private Chunk handleREAE(Chunk request) {
+ ByteBuffer in = wrapChunk(request);
+ boolean enable;
+
+ enable = (in.get() != 0);
+
+ if (Config.LOGD)
+ Log.d("ddm-heap", "Recent allocation enable request: " + enable);
+
+ DdmVmInternal.enableRecentAllocations(enable);
+
+ return null; // empty response
+ }
+
+ /*
+ * Handle a "REcent Allocation Query" request.
+ */
+ private Chunk handleREAQ(Chunk request) {
+ //ByteBuffer in = wrapChunk(request);
+
+ byte[] reply = new byte[1];
+ reply[0] = DdmVmInternal.getRecentAllocationStatus() ? (byte)1 :(byte)0;
+ return new Chunk(CHUNK_REAQ, reply, 0, reply.length);
+ }
+
+ /*
+ * Handle a "REcent ALlocations" request.
+ */
+ private Chunk handleREAL(Chunk request) {
+ //ByteBuffer in = wrapChunk(request);
+
+ if (Config.LOGD)
+ Log.d("ddm-heap", "Recent allocations request");
+
+ /* generate the reply in a ready-to-go format */
+ byte[] reply = DdmVmInternal.getRecentAllocations();
+ return new Chunk(CHUNK_REAL, reply, 0, reply.length);
+ }
+}
+
diff --git a/core/java/android/ddm/DdmHandleHello.java b/core/java/android/ddm/DdmHandleHello.java
new file mode 100644
index 0000000..e4d630e
--- /dev/null
+++ b/core/java/android/ddm/DdmHandleHello.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.ddm;
+
+import org.apache.harmony.dalvik.ddmc.Chunk;
+import org.apache.harmony.dalvik.ddmc.ChunkHandler;
+import org.apache.harmony.dalvik.ddmc.DdmServer;
+import android.util.Config;
+import android.util.Log;
+import android.os.Debug;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Handle a HELO chunk.
+ */
+public class DdmHandleHello extends ChunkHandler {
+
+ public static final int CHUNK_HELO = type("HELO");
+ public static final int CHUNK_WAIT = type("WAIT");
+
+ private static DdmHandleHello mInstance = new DdmHandleHello();
+
+
+ /* singleton, do not instantiate */
+ private DdmHandleHello() {}
+
+ /**
+ * Register for the messages we're interested in.
+ */
+ public static void register() {
+ DdmServer.registerHandler(CHUNK_HELO, mInstance);
+ }
+
+ /**
+ * Called when the DDM server connects. The handler is allowed to
+ * send messages to the server.
+ */
+ public void connected() {
+ if (Config.LOGV)
+ Log.v("ddm-hello", "Connected!");
+
+ if (true) {
+ /* test spontaneous transmission */
+ byte[] data = new byte[] { 0, 1, 2, 3, 4, -4, -3, -2, -1, 127 };
+ Chunk testChunk =
+ new Chunk(ChunkHandler.type("TEST"), data, 1, data.length-2);
+ DdmServer.sendChunk(testChunk);
+ }
+ }
+
+ /**
+ * Called when the DDM server disconnects. Can be used to disable
+ * periodic transmissions or clean up saved state.
+ */
+ public void disconnected() {
+ if (Config.LOGV)
+ Log.v("ddm-hello", "Disconnected!");
+ }
+
+ /**
+ * Handle a chunk of data. We're only registered for "HELO".
+ */
+ public Chunk handleChunk(Chunk request) {
+ if (Config.LOGV)
+ Log.v("ddm-hello", "Handling " + name(request.type) + " chunk");
+
+ if (false)
+ return createFailChunk(123, "This is a test");
+
+ /*
+ * Process the request.
+ */
+ ByteBuffer in = wrapChunk(request);
+
+ int serverProtoVers = in.getInt();
+ if (Config.LOGV)
+ Log.v("ddm-hello", "Server version is " + serverProtoVers);
+
+ /*
+ * Create a response.
+ */
+ String vmName = System.getProperty("java.vm.name", "?");
+ String vmVersion = System.getProperty("java.vm.version", "?");
+ String vmIdent = vmName + " v" + vmVersion;
+
+ //String appName = android.app.ActivityThread.currentPackageName();
+ //if (appName == null)
+ // appName = "unknown";
+ String appName = DdmHandleAppName.getAppName();
+
+ ByteBuffer out = ByteBuffer.allocate(16
+ + vmIdent.length()*2 + appName.length()*2);
+ out.order(ChunkHandler.CHUNK_ORDER);
+ out.putInt(DdmServer.CLIENT_PROTOCOL_VERSION);
+ out.putInt(android.os.Process.myPid());
+ out.putInt(vmIdent.length());
+ out.putInt(appName.length());
+ putString(out, vmIdent);
+ putString(out, appName);
+
+ Chunk reply = new Chunk(CHUNK_HELO, out);
+
+ /*
+ * Take the opportunity to inform DDMS if we are waiting for a
+ * debugger to attach.
+ */
+ if (Debug.waitingForDebugger())
+ sendWAIT(0);
+
+ return reply;
+ }
+
+ /**
+ * Send up a WAIT chunk. The only currently defined value for "reason"
+ * is zero, which means "waiting for a debugger".
+ */
+ public static void sendWAIT(int reason) {
+ byte[] data = new byte[] { (byte) reason };
+ Chunk waitChunk = new Chunk(CHUNK_WAIT, data, 0, 1);
+ DdmServer.sendChunk(waitChunk);
+ }
+}
+
diff --git a/core/java/android/ddm/DdmHandleNativeHeap.java b/core/java/android/ddm/DdmHandleNativeHeap.java
new file mode 100644
index 0000000..6bd65aa
--- /dev/null
+++ b/core/java/android/ddm/DdmHandleNativeHeap.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.ddm;
+
+import org.apache.harmony.dalvik.ddmc.Chunk;
+import org.apache.harmony.dalvik.ddmc.ChunkHandler;
+import org.apache.harmony.dalvik.ddmc.DdmServer;
+import android.util.Log;
+import java.nio.ByteBuffer;
+
+/**
+ * Handle thread-related traffic.
+ */
+public class DdmHandleNativeHeap extends ChunkHandler {
+
+ public static final int CHUNK_NHGT = type("NHGT");
+
+ private static DdmHandleNativeHeap mInstance = new DdmHandleNativeHeap();
+
+
+ /* singleton, do not instantiate */
+ private DdmHandleNativeHeap() {}
+
+ /**
+ * Register for the messages we're interested in.
+ */
+ public static void register() {
+ DdmServer.registerHandler(CHUNK_NHGT, mInstance);
+ }
+
+ /**
+ * Called when the DDM server connects. The handler is allowed to
+ * send messages to the server.
+ */
+ public void connected() {}
+
+ /**
+ * Called when the DDM server disconnects. Can be used to disable
+ * periodic transmissions or clean up saved state.
+ */
+ public void disconnected() {}
+
+ /**
+ * Handle a chunk of data.
+ */
+ public Chunk handleChunk(Chunk request) {
+ Log.i("ddm-nativeheap", "Handling " + name(request.type) + " chunk");
+ int type = request.type;
+
+ if (type == CHUNK_NHGT) {
+ return handleNHGT(request);
+ } else {
+ throw new RuntimeException("Unknown packet "
+ + ChunkHandler.name(type));
+ }
+ }
+
+ /*
+ * Handle a "Native Heap GeT" request.
+ */
+ private Chunk handleNHGT(Chunk request) {
+ //ByteBuffer in = wrapChunk(request);
+
+ byte[] data = getLeakInfo();
+
+ if (data != null) {
+ // wrap & return
+ Log.i("ddm-nativeheap", "Sending " + data.length + " bytes");
+ return new Chunk(ChunkHandler.type("NHGT"), data, 0, data.length);
+ } else {
+ // failed, return a failure error code and message
+ return createFailChunk(1, "Something went wrong");
+ }
+ }
+
+ private native byte[] getLeakInfo();
+}
+
diff --git a/core/java/android/ddm/DdmHandleThread.java b/core/java/android/ddm/DdmHandleThread.java
new file mode 100644
index 0000000..c307988
--- /dev/null
+++ b/core/java/android/ddm/DdmHandleThread.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.ddm;
+
+import org.apache.harmony.dalvik.ddmc.Chunk;
+import org.apache.harmony.dalvik.ddmc.ChunkHandler;
+import org.apache.harmony.dalvik.ddmc.DdmServer;
+import org.apache.harmony.dalvik.ddmc.DdmVmInternal;
+import android.util.Config;
+import android.util.Log;
+import java.nio.ByteBuffer;
+
+/**
+ * Handle thread-related traffic.
+ */
+public class DdmHandleThread extends ChunkHandler {
+
+ public static final int CHUNK_THEN = type("THEN");
+ public static final int CHUNK_THCR = type("THCR");
+ public static final int CHUNK_THDE = type("THDE");
+ public static final int CHUNK_THST = type("THST");
+ public static final int CHUNK_STKL = type("STKL");
+
+ private static DdmHandleThread mInstance = new DdmHandleThread();
+
+
+ /* singleton, do not instantiate */
+ private DdmHandleThread() {}
+
+ /**
+ * Register for the messages we're interested in.
+ */
+ public static void register() {
+ DdmServer.registerHandler(CHUNK_THEN, mInstance);
+ DdmServer.registerHandler(CHUNK_THST, mInstance);
+ DdmServer.registerHandler(CHUNK_STKL, mInstance);
+ }
+
+ /**
+ * Called when the DDM server connects. The handler is allowed to
+ * send messages to the server.
+ */
+ public void connected() {}
+
+ /**
+ * Called when the DDM server disconnects. Can be used to disable
+ * periodic transmissions or clean up saved state.
+ */
+ public void disconnected() {}
+
+ /**
+ * Handle a chunk of data.
+ */
+ public Chunk handleChunk(Chunk request) {
+ if (Config.LOGV)
+ Log.v("ddm-thread", "Handling " + name(request.type) + " chunk");
+ int type = request.type;
+
+ if (type == CHUNK_THEN) {
+ return handleTHEN(request);
+ } else if (type == CHUNK_THST) {
+ return handleTHST(request);
+ } else if (type == CHUNK_STKL) {
+ return handleSTKL(request);
+ } else {
+ throw new RuntimeException("Unknown packet "
+ + ChunkHandler.name(type));
+ }
+ }
+
+ /*
+ * Handle a "THread notification ENable" request.
+ */
+ private Chunk handleTHEN(Chunk request) {
+ ByteBuffer in = wrapChunk(request);
+
+ boolean enable = (in.get() != 0);
+ //Log.i("ddm-thread", "Thread notify enable: " + enable);
+
+ DdmVmInternal.threadNotify(enable);
+ return null; // empty response
+ }
+
+ /*
+ * Handle a "THread STatus" request. This is constructed by the VM.
+ */
+ private Chunk handleTHST(Chunk request) {
+ ByteBuffer in = wrapChunk(request);
+ // currently nothing to read from "in"
+
+ //Log.d("ddm-thread", "Thread status request");
+
+ byte[] status = DdmVmInternal.getThreadStats();
+ if (status != null)
+ return new Chunk(CHUNK_THST, status, 0, status.length);
+ else
+ return createFailChunk(1, "Can't build THST chunk");
+ }
+
+ /*
+ * Handle a STacK List request.
+ *
+ * This is done by threadId, which isn't great since those are
+ * recycled. We need a thread serial ID. The Linux tid is an okay
+ * answer as it's unlikely to recycle at the exact wrong moment.
+ * However, we're using the short threadId in THST messages, so we
+ * use them here for consistency. (One thought is to keep the current
+ * thread ID in the low 16 bits and somehow serialize the top 16 bits.)
+ */
+ private Chunk handleSTKL(Chunk request) {
+ ByteBuffer in = wrapChunk(request);
+ int threadId;
+
+ threadId = in.getInt();
+
+ //Log.d("ddm-thread", "Stack list request " + threadId);
+
+ StackTraceElement[] trace = DdmVmInternal.getStackTraceById(threadId);
+ if (trace == null) {
+ return createFailChunk(1, "Stack trace unavailable");
+ } else {
+ return createStackChunk(trace, threadId);
+ }
+ }
+
+ /*
+ * Serialize a StackTraceElement[] into an STKL chunk.
+ *
+ * We include the threadId in the response so the other side doesn't have
+ * to match up requests and responses as carefully.
+ */
+ private Chunk createStackChunk(StackTraceElement[] trace, int threadId) {
+ int bufferSize = 0;
+
+ bufferSize += 4; // version, flags, whatever
+ bufferSize += 4; // thread ID
+ bufferSize += 4; // frame count
+ for (StackTraceElement elem : trace) {
+ bufferSize += 4 + elem.getClassName().length() * 2;
+ bufferSize += 4 + elem.getMethodName().length() * 2;
+ bufferSize += 4;
+ if (elem.getFileName() != null)
+ bufferSize += elem.getFileName().length() * 2;
+ bufferSize += 4; // line number
+ }
+
+ ByteBuffer out = ByteBuffer.allocate(bufferSize);
+ out.putInt(0);
+ out.putInt(threadId);
+ out.putInt(trace.length);
+ for (StackTraceElement elem : trace) {
+ out.putInt(elem.getClassName().length());
+ putString(out, elem.getClassName());
+ out.putInt(elem.getMethodName().length());
+ putString(out, elem.getMethodName());
+ if (elem.getFileName() != null) {
+ out.putInt(elem.getFileName().length());
+ putString(out, elem.getFileName());
+ } else {
+ out.putInt(0);
+ }
+ out.putInt(elem.getLineNumber());
+ }
+
+ return new Chunk(CHUNK_STKL, out);
+ }
+}
+
diff --git a/core/java/android/ddm/DdmRegister.java b/core/java/android/ddm/DdmRegister.java
new file mode 100644
index 0000000..b7f1ab8
--- /dev/null
+++ b/core/java/android/ddm/DdmRegister.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.ddm;
+
+import org.apache.harmony.dalvik.ddmc.DdmServer;
+import android.util.Config;
+import android.util.Log;
+
+/**
+ * Just a place to stick handler registrations, instead of scattering
+ * them around.
+ */
+public class DdmRegister {
+
+ private DdmRegister() {}
+
+ /**
+ * Register handlers for all known chunk types.
+ *
+ * If you write a handler, add a registration call here.
+ *
+ * Note that this is invoked by the application (usually through a
+ * static initializer in the main class), not the VM. It's done this
+ * way so that the handlers can use Android classes with native calls
+ * that aren't registered until after the VM is initialized (e.g.
+ * logging). It also allows debugging of DDM handler initialization.
+ *
+ * The chunk dispatcher will pause until we call registrationComplete(),
+ * so that we don't have a race that causes us to drop packets before
+ * we finish here.
+ */
+ public static void registerHandlers() {
+ if (Config.LOGV)
+ Log.v("ddm", "Registering DDM message handlers");
+ DdmHandleHello.register();
+ DdmHandleThread.register();
+ DdmHandleHeap.register();
+ DdmHandleNativeHeap.register();
+ DdmHandleExit.register();
+
+ DdmServer.registrationComplete();
+ }
+}
+
diff --git a/core/java/android/ddm/README.txt b/core/java/android/ddm/README.txt
new file mode 100644
index 0000000..a8e645d
--- /dev/null
+++ b/core/java/android/ddm/README.txt
@@ -0,0 +1,6 @@
+Some classes that handle DDM traffic.
+
+It's not necessary to put all DDM-related code in this package; this just
+has the essentials. Subclass org.apache.harmony.dalvik.ddmc.ChunkHandler and add a new
+registration call in DdmRegister.java.
+
diff --git a/core/java/android/ddm/package.html b/core/java/android/ddm/package.html
new file mode 100755
index 0000000..1c9bf9d
--- /dev/null
+++ b/core/java/android/ddm/package.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+ {@hide}
+</body>
+</html>
diff --git a/core/java/android/debug/JNITest.java b/core/java/android/debug/JNITest.java
new file mode 100644
index 0000000..2ce374a
--- /dev/null
+++ b/core/java/android/debug/JNITest.java
@@ -0,0 +1,48 @@
+/*
+ * 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.debug;
+
+/**
+ * Simple JNI verification test.
+ */
+public class JNITest {
+
+ public JNITest() {
+ }
+
+ public int test(int intArg, double doubleArg, String stringArg) {
+ int[] intArray = { 42, 53, 65, 127 };
+
+ return part1(intArg, doubleArg, stringArg, intArray);
+ }
+
+ private native int part1(int intArg, double doubleArg, String stringArg,
+ int[] arrayArg);
+
+ private int part2(double doubleArg, int fromArray, String stringArg) {
+ int result;
+
+ System.out.println(stringArg + " : " + (float) doubleArg + " : " +
+ fromArray);
+ result = part3(stringArg);
+
+ return result + 6;
+ }
+
+ private static native int part3(String stringArg);
+}
+
diff --git a/core/java/android/debug/package.html b/core/java/android/debug/package.html
new file mode 100755
index 0000000..c9f96a6
--- /dev/null
+++ b/core/java/android/debug/package.html
@@ -0,0 +1,5 @@
+<body>
+
+{@hide}
+
+</body>
diff --git a/core/java/android/hardware/Camera.java b/core/java/android/hardware/Camera.java
new file mode 100644
index 0000000..8330750
--- /dev/null
+++ b/core/java/android/hardware/Camera.java
@@ -0,0 +1,633 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware;
+
+import java.lang.ref.WeakReference;
+import java.util.HashMap;
+
+import android.util.Log;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.graphics.PixelFormat;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+
+/**
+ * The Camera class is used to connect/disconnect with the camera service,
+ * set capture settings, start/stop preview, snap a picture, and retrieve
+ * frames for encoding for video.
+ * <p>There is no default constructor for this class. Use {@link #open()} to
+ * get a Camera object.</p>
+ */
+public class Camera {
+ private static final String TAG = "Camera";
+
+ // These match the enum in libs/android_runtime/android_hardware_Camera.cpp
+ private static final int SHUTTER_CALLBACK = 0;
+ private static final int RAW_PICTURE_CALLBACK = 1;
+ private static final int JPEG_PICTURE_CALLBACK = 2;
+ private static final int PREVIEW_CALLBACK = 3;
+ private static final int AUTOFOCUS_CALLBACK = 4;
+ private static final int ERROR_CALLBACK = 5;
+
+ private int mNativeContext; // accessed by native methods
+ private int mListenerContext;
+ private EventHandler mEventHandler;
+ private ShutterCallback mShutterCallback;
+ private PictureCallback mRawImageCallback;
+ private PictureCallback mJpegCallback;
+ private PreviewCallback mPreviewCallback;
+ private AutoFocusCallback mAutoFocusCallback;
+ private ErrorCallback mErrorCallback;
+
+ /**
+ * Returns a new Camera object.
+ */
+ public static Camera open() {
+ return new Camera();
+ }
+
+ Camera() {
+ mShutterCallback = null;
+ mRawImageCallback = null;
+ mJpegCallback = null;
+ mPreviewCallback = null;
+
+ Looper looper;
+ if ((looper = Looper.myLooper()) != null) {
+ mEventHandler = new EventHandler(this, looper);
+ } else if ((looper = Looper.getMainLooper()) != null) {
+ mEventHandler = new EventHandler(this, looper);
+ } else {
+ mEventHandler = null;
+ }
+
+ native_setup(new WeakReference<Camera>(this));
+ }
+
+ protected void finalize() {
+ native_release();
+ }
+
+ private native final void native_setup(Object camera_this);
+ private native final void native_release();
+
+
+ /**
+ * Disconnects and releases the Camera object resources.
+ * <p>It is recommended that you call this as soon as you're done with the
+ * Camera object.</p>
+ */
+ public final void release() {
+ native_release();
+ }
+
+ /**
+ * Sets the SurfaceHolder to be used for a picture preview. If the surface
+ * changed since the last call, the screen will blank. Nothing happens
+ * if the same surface is re-set.
+ *
+ * @param holder the SurfaceHolder upon which to place the picture preview
+ */
+ public final void setPreviewDisplay(SurfaceHolder holder) {
+ setPreviewDisplay(holder.getSurface());
+ }
+
+ private native final void setPreviewDisplay(Surface surface);
+
+ /**
+ * Used to get a copy of each preview frame.
+ */
+ public interface PreviewCallback
+ {
+ /**
+ * The callback that delivers the preview frames.
+ *
+ * @param data The contents of the preview frame in getPreviewFormat()
+ * format.
+ * @param camera The Camera service object.
+ */
+ void onPreviewFrame(byte[] data, Camera camera);
+ };
+
+ /**
+ * Start drawing preview frames to the surface.
+ */
+ public native final void startPreview();
+
+ /**
+ * Stop drawing preview frames to the surface.
+ */
+ public native final void stopPreview();
+
+ /**
+ * Can be called at any time to instruct the camera to use a callback for
+ * each preview frame in addition to displaying it.
+ *
+ * @param cb A callback object that receives a copy of each preview frame.
+ * Pass null to stop receiving callbacks at any time.
+ */
+ public final void setPreviewCallback(PreviewCallback cb) {
+ mPreviewCallback = cb;
+ setHasPreviewCallback(cb != null);
+ }
+ private native final void setHasPreviewCallback(boolean installed);
+
+ private class EventHandler extends Handler
+ {
+ private Camera mCamera;
+
+ public EventHandler(Camera c, Looper looper) {
+ super(looper);
+ mCamera = c;
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch(msg.what) {
+ case SHUTTER_CALLBACK:
+ if (mShutterCallback != null) {
+ mShutterCallback.onShutter();
+ }
+ return;
+ case RAW_PICTURE_CALLBACK:
+ if (mRawImageCallback != null)
+ mRawImageCallback.onPictureTaken((byte[])msg.obj, mCamera);
+ return;
+
+ case JPEG_PICTURE_CALLBACK:
+ if (mJpegCallback != null)
+ mJpegCallback.onPictureTaken((byte[])msg.obj, mCamera);
+ return;
+
+ case PREVIEW_CALLBACK:
+ if (mPreviewCallback != null)
+ mPreviewCallback.onPreviewFrame((byte[])msg.obj, mCamera);
+ return;
+
+ case AUTOFOCUS_CALLBACK:
+ if (mAutoFocusCallback != null)
+ mAutoFocusCallback.onAutoFocus(msg.arg1 == 0 ? false : true, mCamera);
+ return;
+
+ case ERROR_CALLBACK:
+ Log.e(TAG, "Error " + msg.arg1);
+ if (mErrorCallback != null)
+ mErrorCallback.onError(msg.arg1, mCamera);
+ return;
+
+ default:
+ Log.e(TAG, "Unknown message type " + msg.what);
+ return;
+ }
+ }
+ }
+
+ private static void postEventFromNative(Object camera_ref,
+ int what, int arg1, int arg2, Object obj)
+ {
+ Camera c = (Camera)((WeakReference)camera_ref).get();
+ if (c == null)
+ return;
+
+ if (c.mEventHandler != null) {
+ Message m = c.mEventHandler.obtainMessage(what, arg1, arg2, obj);
+ c.mEventHandler.sendMessage(m);
+ }
+ }
+
+ /**
+ * Handles the callback for the camera auto focus.
+ */
+ public interface AutoFocusCallback
+ {
+ /**
+ * Callback for the camera auto focus.
+ *
+ * @param success true if focus was successful, false if otherwise
+ * @param camera the Camera service object
+ */
+ void onAutoFocus(boolean success, Camera camera);
+ };
+
+ /**
+ * Registers a callback to be invoked when the auto focus responds.
+ *
+ * @param cb the callback to run
+ */
+ public final void autoFocus(AutoFocusCallback cb)
+ {
+ mAutoFocusCallback = cb;
+ native_autoFocus();
+ }
+ private native final void native_autoFocus();
+
+ /**
+ * An interface which contains a callback for the shutter closing after taking a picture.
+ */
+ public interface ShutterCallback
+ {
+ /**
+ * Can be used to play a shutter sound as soon as the image has been captured, but before
+ * the data is available.
+ */
+ void onShutter();
+ }
+
+ /**
+ * Handles the callback for when a picture is taken.
+ */
+ public interface PictureCallback {
+ /**
+ * Callback for when a picture is taken.
+ *
+ * @param data a byte array of the picture data
+ * @param camera the Camera service object
+ */
+ void onPictureTaken(byte[] data, Camera camera);
+ };
+
+ /**
+ * Registers a callback to be invoked when a picture is taken.
+ *
+ * @param raw the callback to run for raw images, may be null
+ * @param jpeg the callback to run for jpeg images, may be null
+ */
+ public final void takePicture(ShutterCallback shutter, PictureCallback raw,
+ PictureCallback jpeg) {
+ mShutterCallback = shutter;
+ mRawImageCallback = raw;
+ mJpegCallback = jpeg;
+ native_takePicture();
+ }
+ private native final void native_takePicture();
+
+ // These match the enum in libs/android_runtime/android_hardware_Camera.cpp
+ /** Unspecified camerar error. @see #ErrorCallback */
+ public static final int CAMERA_ERROR_UNKNOWN = 1;
+ /** Media server died. In this case, the application must release the
+ * Camera object and instantiate a new one. @see #ErrorCallback */
+ public static final int CAMERA_ERROR_SERVER_DIED = 100;
+
+ /**
+ * Handles the camera error callback.
+ */
+ public interface ErrorCallback
+ {
+ /**
+ * Callback for camera errors.
+ * @param error error code:
+ * <ul>
+ * <li>{@link #CAMERA_ERROR_UNKNOWN}
+ * <li>{@link #CAMERA_ERROR_SERVER_DIED}
+ * </ul>
+ * @param camera the Camera service object
+ */
+ void onError(int error, Camera camera);
+ };
+
+ /**
+ * Registers a callback to be invoked when an error occurs.
+ * @param cb the callback to run
+ */
+ public final void setErrorCallback(ErrorCallback cb)
+ {
+ mErrorCallback = cb;
+ }
+
+ private native final void native_setParameters(String params);
+ private native final String native_getParameters();
+
+ /**
+ * Sets the Parameters for pictures from this Camera service.
+ *
+ * @param params the Parameters to use for this Camera service
+ */
+ public void setParameters(Parameters params) {
+ Log.e(TAG, "setParameters()");
+ //params.dump();
+ native_setParameters(params.flatten());
+ }
+
+ /**
+ * Returns the picture Parameters for this Camera service.
+ */
+ public Parameters getParameters() {
+ Parameters p = new Parameters();
+ String s = native_getParameters();
+ Log.e(TAG, "_getParameters: " + s);
+ p.unflatten(s);
+ return p;
+ }
+
+ /**
+ * Handles the picture size (dimensions).
+ */
+ public class Size {
+ /**
+ * Sets the dimensions for pictures.
+ *
+ * @param w the photo width (pixels)
+ * @param h the photo height (pixels)
+ */
+ public Size(int w, int h) {
+ width = w;
+ height = h;
+ }
+ /** width of the picture */
+ public int width;
+ /** height of the picture */
+ public int height;
+ };
+
+ /**
+ * Handles the parameters for pictures created by a Camera service.
+ */
+ public class Parameters {
+ private HashMap<String, String> mMap;
+
+ private Parameters() {
+ mMap = new HashMap<String, String>();
+ }
+
+ /**
+ * Writes the current Parameters to the log.
+ * @hide
+ * @deprecated
+ */
+ public void dump() {
+ Log.e(TAG, "dump: size=" + mMap.size());
+ for (String k : mMap.keySet()) {
+ Log.e(TAG, "dump: " + k + "=" + mMap.get(k));
+ }
+ }
+
+ /**
+ * Creates a single string with all the parameters set in
+ * this Parameters object.
+ * <p>The {@link #unflatten(String)} method does the reverse.</p>
+ *
+ * @return a String with all values from this Parameters object, in
+ * semi-colon delimited key-value pairs
+ */
+ public String flatten() {
+ StringBuilder flattened = new StringBuilder();
+ for (String k : mMap.keySet()) {
+ flattened.append(k);
+ flattened.append("=");
+ flattened.append(mMap.get(k));
+ flattened.append(";");
+ }
+ // chop off the extra semicolon at the end
+ flattened.deleteCharAt(flattened.length()-1);
+ return flattened.toString();
+ }
+
+ /**
+ * Takes a flattened string of parameters and adds each one to
+ * this Parameters object.
+ * <p>The {@link #flatten()} method does the reverse.</p>
+ *
+ * @param flattened a String of parameters (key-value paired) that
+ * are semi-colon delimited
+ */
+ public void unflatten(String flattened) {
+ mMap.clear();
+ String[] pairs = flattened.split(";");
+ for (String p : pairs) {
+ String[] kv = p.split("=");
+ if (kv.length == 2)
+ mMap.put(kv[0], kv[1]);
+ }
+ }
+
+ public void remove(String key) {
+ mMap.remove(key);
+ }
+
+ /**
+ * Sets a String parameter.
+ *
+ * @param key the key name for the parameter
+ * @param value the String value of the parameter
+ */
+ public void set(String key, String value) {
+ if (key.indexOf('=') != -1 || key.indexOf(';') != -1) {
+ Log.e(TAG, "Key \"" + key + "\" contains invalid character (= or ;)");
+ return;
+ }
+ if (value.indexOf('=') != -1 || value.indexOf(';') != -1) {
+ Log.e(TAG, "Value \"" + value + "\" contains invalid character (= or ;)");
+ return;
+ }
+
+ mMap.put(key, value);
+ }
+
+ /**
+ * Sets an integer parameter.
+ *
+ * @param key the key name for the parameter
+ * @param value the int value of the parameter
+ */
+ public void set(String key, int value) {
+ mMap.put(key, Integer.toString(value));
+ }
+
+ /**
+ * Returns the value of a String parameter.
+ *
+ * @param key the key name for the parameter
+ * @return the String value of the parameter
+ */
+ public String get(String key) {
+ return mMap.get(key);
+ }
+
+ /**
+ * Returns the value of an integer parameter.
+ *
+ * @param key the key name for the parameter
+ * @return the int value of the parameter
+ */
+ public int getInt(String key) {
+ return Integer.parseInt(mMap.get(key));
+ }
+
+ /**
+ * Sets the dimensions for preview pictures.
+ *
+ * @param width the width of the pictures, in pixels
+ * @param height the height of the pictures, in pixels
+ */
+ public void setPreviewSize(int width, int height) {
+ String v = Integer.toString(width) + "x" + Integer.toString(height);
+ set("preview-size", v);
+ }
+
+ /**
+ * Returns the dimensions setting for preview pictures.
+ *
+ * @return a Size object with the height and width setting
+ * for the preview picture
+ */
+ public Size getPreviewSize() {
+ String pair = get("preview-size");
+ if (pair == null)
+ return null;
+ String[] dims = pair.split("x");
+ if (dims.length != 2)
+ return null;
+
+ return new Size(Integer.parseInt(dims[0]),
+ Integer.parseInt(dims[1]));
+
+ }
+
+ /**
+ * Sets the rate at which preview frames are received.
+ *
+ * @param fps the frame rate (frames per second)
+ */
+ public void setPreviewFrameRate(int fps) {
+ set("preview-frame-rate", fps);
+ }
+
+ /**
+ * Returns the setting for the rate at which preview frames
+ * are received.
+ *
+ * @return the frame rate setting (frames per second)
+ */
+ public int getPreviewFrameRate() {
+ return getInt("preview-frame-rate");
+ }
+
+ /**
+ * Sets the image format for preview pictures.
+ *
+ * @param pixel_format the desired preview picture format
+ * (<var>PixelFormat.YCbCr_422_SP</var>,
+ * <var>PixelFormat.RGB_565</var>, or
+ * <var>PixelFormat.JPEG</var>)
+ * @see android.graphics.PixelFormat
+ */
+ public void setPreviewFormat(int pixel_format) {
+ String s = cameraFormatForPixelFormat(pixel_format);
+ if (s == null) {
+ throw new IllegalArgumentException();
+ }
+
+ set("preview-format", s);
+ }
+
+ /**
+ * Returns the image format for preview pictures.
+ *
+ * @return the PixelFormat int representing the preview picture format
+ */
+ public int getPreviewFormat() {
+ return pixelFormatForCameraFormat(get("preview-format"));
+ }
+
+ /**
+ * Sets the dimensions for pictures.
+ *
+ * @param width the width for pictures, in pixels
+ * @param height the height for pictures, in pixels
+ */
+ public void setPictureSize(int width, int height) {
+ String v = Integer.toString(width) + "x" + Integer.toString(height);
+ set("picture-size", v);
+ }
+
+ /**
+ * Returns the dimension setting for pictures.
+ *
+ * @return a Size object with the height and width setting
+ * for pictures
+ */
+ public Size getPictureSize() {
+ String pair = get("picture-size");
+ if (pair == null)
+ return null;
+ String[] dims = pair.split("x");
+ if (dims.length != 2)
+ return null;
+
+ return new Size(Integer.parseInt(dims[0]),
+ Integer.parseInt(dims[1]));
+
+ }
+
+ /**
+ * Sets the image format for pictures.
+ *
+ * @param pixel_format the desired picture format
+ * (<var>PixelFormat.YCbCr_422_SP</var>,
+ * <var>PixelFormat.RGB_565</var>, or
+ * <var>PixelFormat.JPEG</var>)
+ * @see android.graphics.PixelFormat
+ */
+ public void setPictureFormat(int pixel_format) {
+ String s = cameraFormatForPixelFormat(pixel_format);
+ if (s == null) {
+ throw new IllegalArgumentException();
+ }
+
+ set("picture-format", s);
+ }
+
+ /**
+ * Returns the image format for pictures.
+ *
+ * @return the PixelFormat int representing the picture format
+ */
+ public int getPictureFormat() {
+ return pixelFormatForCameraFormat(get("picture-format"));
+ }
+
+ private String cameraFormatForPixelFormat(int pixel_format) {
+ switch(pixel_format) {
+ case PixelFormat.YCbCr_422_SP: return "yuv422sp";
+ case PixelFormat.RGB_565: return "rgb565";
+ case PixelFormat.JPEG: return "jpeg";
+ default: return null;
+ }
+ }
+
+ private int pixelFormatForCameraFormat(String format) {
+ if (format == null)
+ return PixelFormat.UNKNOWN;
+
+ if (format.equals("yuv422sp"))
+ return PixelFormat.YCbCr_422_SP;
+
+ if (format.equals("rgb565"))
+ return PixelFormat.RGB_565;
+
+ if (format.equals("jpeg"))
+ return PixelFormat.JPEG;
+
+ return PixelFormat.UNKNOWN;
+ }
+
+ };
+}
+
+
diff --git a/core/java/android/hardware/ISensorService.aidl b/core/java/android/hardware/ISensorService.aidl
new file mode 100644
index 0000000..b6ac3ab
--- /dev/null
+++ b/core/java/android/hardware/ISensorService.aidl
@@ -0,0 +1,30 @@
+/* //device/java/android/android/hardware/ISensorService.aidl
+**
+** Copyright 2008, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.hardware;
+
+import android.os.ParcelFileDescriptor;
+
+/**
+ * {@hide}
+ */
+interface ISensorService
+{
+ ParcelFileDescriptor getDataChanel();
+ boolean enableSensor(IBinder listener, int sensor, int enable);
+ oneway void reportAccuracy(int sensor, int value);
+}
diff --git a/core/java/android/hardware/SensorListener.java b/core/java/android/hardware/SensorListener.java
new file mode 100644
index 0000000..d676a5e
--- /dev/null
+++ b/core/java/android/hardware/SensorListener.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware;
+
+/**
+ * Used for receiving notifications from the SensorManager when
+ * sensor values have changed.
+ */
+public interface SensorListener {
+
+ /**
+ * Called when sensor values have changed.
+ * The length and contents of the values array vary
+ * depending on which sensor is being monitored.
+ * See {@link android.hardware.SensorManager SensorManager}
+ * for details on possible sensor types and values.
+ *
+ * @param sensor The ID of the sensor being monitored
+ * @param values The new values for the sensor
+ */
+ public void onSensorChanged(int sensor, float[] values);
+
+ /**
+ * Called when the accuracy of a sensor has changed.
+ * See {@link android.hardware.SensorManager SensorManager}
+ * for details.
+ *
+ * @param sensor The ID of the sensor being monitored
+ * @param accuracy The new accuracy of this sensor
+ */
+ public void onAccuracyChanged(int sensor, int accuracy);
+}
diff --git a/core/java/android/hardware/SensorManager.java b/core/java/android/hardware/SensorManager.java
new file mode 100644
index 0000000..9b88fff
--- /dev/null
+++ b/core/java/android/hardware/SensorManager.java
@@ -0,0 +1,619 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware;
+
+import android.content.Context;
+import android.os.Binder;
+import android.os.Looper;
+import android.os.ParcelFileDescriptor;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.Handler;
+import android.os.Message;
+import android.os.ServiceManager;
+import android.util.Log;
+import android.view.IRotationWatcher;
+import android.view.IWindowManager;
+import android.view.Surface;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * Class that lets you access the device's sensors. Get an instance of this
+ * class by calling {@link android.content.Context#getSystemService(java.lang.String)
+ * Context.getSystemService()} with an argument of {@link android.content.Context#SENSOR_SERVICE}.
+ */
+public class SensorManager extends IRotationWatcher.Stub
+{
+ private static final String TAG = "SensorManager";
+
+ /** NOTE: sensor IDs must be a power of 2 */
+
+ /** A constant describing an orientation sensor.
+ * Sensor values are yaw, pitch and roll
+ *
+ * Yaw is the compass heading in degrees, range [0, 360[
+ * 0 = North, 90 = East, 180 = South, 270 = West
+ *
+ * Pitch indicates the tilt of the top of the device,
+ * with range -90 to 90.
+ * Positive values indicate that the bottom of the device is tilted up
+ * and negative values indicate the top of the device is tilted up.
+ *
+ * Roll indicates the side to side tilt of the device,
+ * with range -90 to 90.
+ * Positive values indicate that the left side of the device is tilted up
+ * and negative values indicate the right side of the device is tilted up.
+ */
+ public static final int SENSOR_ORIENTATION = 1 << 0;
+
+ /** A constant describing an accelerometer.
+ * Sensor values are acceleration in the X, Y and Z axis,
+ * where the X axis has positive direction toward the right side of the device,
+ * the Y axis has positive direction toward the top of the device
+ * and the Z axis has positive direction toward the front of the device.
+ *
+ * The direction of the force of gravity is indicated by acceleration values in the
+ * X, Y and Z axes. The typical case where the device is flat relative to the surface
+ * of the Earth appears as -STANDARD_GRAVITY in the Z axis
+ * and X and Z values close to zero.
+ *
+ * Acceleration values are given in SI units (m/s^2)
+ *
+ */
+ public static final int SENSOR_ACCELEROMETER = 1 << 1;
+
+ /** A constant describing a temperature sensor
+ * Only the first value is defined for this sensor and it
+ * contains the ambient temperature in degree C.
+ */
+ public static final int SENSOR_TEMPERATURE = 1 << 2;
+
+ /** A constant describing a magnetic sensor
+ * Sensor values are the magnetic vector in the X, Y and Z axis,
+ * where the X axis has positive direction toward the right side of the device,
+ * the Y axis has positive direction toward the top of the device
+ * and the Z axis has positive direction toward the front of the device.
+ *
+ * Magnetic values are given in micro-Tesla (uT)
+ *
+ */
+ public static final int SENSOR_MAGNETIC_FIELD = 1 << 3;
+
+ /** A constant describing an ambient light sensor
+ * Only the first value is defined for this sensor and it contains
+ * the ambient light measure in lux.
+ *
+ */
+ public static final int SENSOR_LIGHT = 1 << 4;
+
+ /** A constant describing a proximity sensor
+ * Only the first value is defined for this sensor and it contains
+ * the distance between the sensor and the object in meters (m)
+ */
+ public static final int SENSOR_PROXIMITY = 1 << 5;
+
+ /** A constant describing a Tricorder
+ * When this sensor is available and enabled, the device can be
+ * used as a fully functional Tricorder. All values are returned in
+ * SI units.
+ */
+ public static final int SENSOR_TRICORDER = 1 << 6;
+
+ /** A constant describing an orientation sensor.
+ * Sensor values are yaw, pitch and roll
+ *
+ * Yaw is the compass heading in degrees, 0 <= range < 360
+ * 0 = North, 90 = East, 180 = South, 270 = West
+ *
+ * This is similar to SENSOR_ORIENTATION except the data is not
+ * smoothed or filtered in any way.
+ */
+ public static final int SENSOR_ORIENTATION_RAW = 1 << 7;
+
+ /** A constant that includes all sensors */
+ public static final int SENSOR_ALL = 0x7F;
+
+ /** Smallest sensor ID */
+ public static final int SENSOR_MIN = SENSOR_ORIENTATION;
+
+ /** Largest sensor ID */
+ public static final int SENSOR_MAX = ((SENSOR_ALL + 1)>>1);
+
+
+ /** Index of the X value in the array returned by
+ * {@link android.hardware.SensorListener#onSensorChanged} */
+ public static final int DATA_X = 0;
+ /** Index of the Y value in the array returned by
+ * {@link android.hardware.SensorListener#onSensorChanged} */
+ public static final int DATA_Y = 1;
+ /** Index of the Z value in the array returned by
+ * {@link android.hardware.SensorListener#onSensorChanged} */
+ public static final int DATA_Z = 2;
+
+ /** Offset to the raw values in the array returned by
+ * {@link android.hardware.SensorListener#onSensorChanged} */
+ public static final int RAW_DATA_INDEX = 3;
+
+ /** Index of the raw X value in the array returned by
+ * {@link android.hardware.SensorListener#onSensorChanged} */
+ public static final int RAW_DATA_X = 3;
+ /** Index of the raw X value in the array returned by
+ * {@link android.hardware.SensorListener#onSensorChanged} */
+ public static final int RAW_DATA_Y = 4;
+ /** Index of the raw X value in the array returned by
+ * {@link android.hardware.SensorListener#onSensorChanged} */
+ public static final int RAW_DATA_Z = 5;
+
+
+ /** Standard gravity (g) on Earth. This value is equivalent to 1G */
+ public static final float STANDARD_GRAVITY = 9.80665f;
+
+ /** values returned by the accelerometer in various locations in the universe.
+ * all values are in SI units (m/s^2) */
+ public static final float GRAVITY_SUN = 275.0f;
+ public static final float GRAVITY_MERCURY = 3.70f;
+ public static final float GRAVITY_VENUS = 8.87f;
+ public static final float GRAVITY_EARTH = 9.80665f;
+ public static final float GRAVITY_MOON = 1.6f;
+ public static final float GRAVITY_MARS = 3.71f;
+ public static final float GRAVITY_JUPITER = 23.12f;
+ public static final float GRAVITY_SATURN = 8.96f;
+ public static final float GRAVITY_URANUS = 8.69f;
+ public static final float GRAVITY_NEPTUN = 11.0f;
+ public static final float GRAVITY_PLUTO = 0.6f;
+ public static final float GRAVITY_DEATH_STAR_I = 0.000000353036145f;
+ public static final float GRAVITY_THE_ISLAND = 4.815162342f;
+
+
+ /** Maximum magnetic field on Earth's surface */
+ public static final float MAGNETIC_FIELD_EARTH_MAX = 60.0f;
+
+ /** Minimum magnetic field on Earth's surface */
+ public static final float MAGNETIC_FIELD_EARTH_MIN = 30.0f;
+
+
+ /** Various luminance values during the day (lux) */
+ public static final float LIGHT_SUNLIGHT_MAX = 120000.0f;
+ public static final float LIGHT_SUNLIGHT = 110000.0f;
+ public static final float LIGHT_SHADE = 20000.0f;
+ public static final float LIGHT_OVERCAST = 10000.0f;
+ public static final float LIGHT_SUNRISE = 400.0f;
+ public static final float LIGHT_CLOUDY = 100.0f;
+ /** Various luminance values during the night (lux) */
+ public static final float LIGHT_FULLMOON = 0.25f;
+ public static final float LIGHT_NO_MOON = 0.001f;
+
+ /** get sensor data as fast as possible */
+ public static final int SENSOR_DELAY_FASTEST = 0;
+ /** rate suitable for games */
+ public static final int SENSOR_DELAY_GAME = 1;
+ /** rate suitable for the user interface */
+ public static final int SENSOR_DELAY_UI = 2;
+ /** rate (default) suitable for screen orientation changes */
+ public static final int SENSOR_DELAY_NORMAL = 3;
+
+
+ /** The values returned by this sensor cannot be trusted, calibration
+ * is needed or the environment doesn't allow readings */
+ public static final int SENSOR_STATUS_UNRELIABLE = 0;
+
+ /** This sensor is reporting data with low accuracy, calibration with the
+ * environment is needed */
+ public static final int SENSOR_STATUS_ACCURACY_LOW = 1;
+
+ /** This sensor is reporting data with an average level of accuracy,
+ * calibration with the environment may improve the readings */
+ public static final int SENSOR_STATUS_ACCURACY_MEDIUM = 2;
+
+ /** This sensor is reporting data with maximum accuracy */
+ public static final int SENSOR_STATUS_ACCURACY_HIGH = 3;
+
+
+
+ private static final int SENSOR_DISABLE = -1;
+ private static final int SENSOR_ORDER_MASK = 0x1F;
+ private static final int SENSOR_STATUS_SHIFT = 28;
+ private ISensorService mSensorService;
+ private Looper mLooper;
+
+ private static IWindowManager sWindowManager;
+ private static int sRotation = 0;
+
+ /* The thread and the sensor list are global to the process
+ * but the actual thread is spawned on demand */
+ static final private SensorThread sSensorThread = new SensorThread();
+ static final private ArrayList<ListenerDelegate> sListeners =
+ new ArrayList<ListenerDelegate>();
+
+
+ static private class SensorThread {
+
+ private Thread mThread;
+
+ // must be called with sListeners lock
+ void startLocked(ISensorService service) {
+ try {
+ if (mThread == null) {
+ ParcelFileDescriptor fd = service.getDataChanel();
+ mThread = new Thread(new SensorThreadRunnable(fd, service),
+ SensorThread.class.getName());
+ mThread.start();
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException in startLocked: ", e);
+ }
+ }
+
+ private class SensorThreadRunnable implements Runnable {
+ private ISensorService mSensorService;
+ private ParcelFileDescriptor mSensorDataFd;
+ private final byte mAccuracies[] = new byte[32];
+ SensorThreadRunnable(ParcelFileDescriptor fd, ISensorService service) {
+ mSensorDataFd = fd;
+ mSensorService = service;
+ Arrays.fill(mAccuracies, (byte)-1);
+ }
+ public void run() {
+ int sensors_of_interest;
+ float[] values = new float[6];
+ Process.setThreadPriority(Process.THREAD_PRIORITY_DISPLAY);
+
+ synchronized (sListeners) {
+ _sensors_data_open(mSensorDataFd.getFileDescriptor());
+ try {
+ mSensorDataFd.close();
+ } catch (IOException e) {
+ // *shrug*
+ Log.e(TAG, "IOException: ", e);
+ }
+ mSensorDataFd = null;
+ //mSensorDataFd.
+ // the first time, compute the sensors we need. this is not
+ // a big deal if it changes by the time we call
+ // _sensors_data_poll, it'll get recomputed for the next
+ // round.
+ sensors_of_interest = 0;
+ final int size = sListeners.size();
+ for (int i=0 ; i<size ; i++) {
+ sensors_of_interest |= sListeners.get(i).mSensors;
+ if ((sensors_of_interest & SENSOR_ALL) == SENSOR_ALL)
+ break;
+ }
+ }
+
+ while (true) {
+ // wait for an event
+ final int sensor_result = _sensors_data_poll(values, sensors_of_interest);
+ final int sensor_order = sensor_result & SENSOR_ORDER_MASK;
+ final int sensor = 1 << sensor_result;
+ int accuracy = sensor_result>>>SENSOR_STATUS_SHIFT;
+
+ if ((sensors_of_interest & sensor)!=0) {
+ // show the notification only if someone is listening for
+ // this sensor
+ if (accuracy != mAccuracies[sensor_order]) {
+ try {
+ mSensorService.reportAccuracy(sensor, accuracy);
+ mAccuracies[sensor_order] = (byte)accuracy;
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException in reportAccuracy: ", e);
+ }
+ } else {
+ accuracy = -1;
+ }
+ }
+
+ synchronized (sListeners) {
+ if (sListeners.isEmpty()) {
+ // we have no more listeners, terminate the thread
+ _sensors_data_close();
+ mThread = null;
+ break;
+ }
+ // convert for the current screen orientation
+ mapSensorDataToWindow(sensor, values, SensorManager.getRotation());
+ // report the sensor event to all listeners that
+ // care about it.
+ sensors_of_interest = 0;
+ final int size = sListeners.size();
+ for (int i=0 ; i<size ; i++) {
+ ListenerDelegate listener = sListeners.get(i);
+ sensors_of_interest |= listener.mSensors;
+ if (listener.hasSensor(sensor)) {
+ // this is asynchronous (okay to call
+ // with sListeners lock held.
+ listener.onSensorChanged(sensor, values, accuracy);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private class ListenerDelegate extends Binder {
+
+ private SensorListener mListener;
+ private int mSensors;
+ private float[] mValuesPool;
+
+ ListenerDelegate(SensorListener listener, int sensors) {
+ mListener = listener;
+ mSensors = sensors;
+ mValuesPool = new float[6];
+ }
+
+ int addSensors(int sensors) {
+ mSensors |= sensors;
+ return mSensors;
+ }
+ int removeSensors(int sensors) {
+ mSensors &= ~sensors;
+ return mSensors;
+ }
+ boolean hasSensor(int sensor) {
+ return ((mSensors & sensor) != 0);
+ }
+
+ void onSensorChanged(int sensor, float[] values, int accuracy) {
+ float[] v;
+ synchronized (this) {
+ // remove the array from the pool
+ v = mValuesPool;
+ mValuesPool = null;
+ }
+
+ if (v != null) {
+ v[0] = values[0];
+ v[1] = values[1];
+ v[2] = values[2];
+ v[3] = values[3];
+ v[4] = values[4];
+ v[5] = values[5];
+ } else {
+ // the pool was empty, we need to dup the array
+ v = values.clone();
+ }
+
+ Message msg = Message.obtain();
+ msg.what = sensor;
+ msg.obj = v;
+ msg.arg1 = accuracy;
+ mHandler.sendMessage(msg);
+ }
+
+ private final Handler mHandler = new Handler(mLooper) {
+ @Override public void handleMessage(Message msg) {
+ if (msg.arg1 >= 0) {
+ try {
+ mListener.onAccuracyChanged(msg.what, msg.arg1);
+ } catch (AbstractMethodError e) {
+ // old app that doesn't implement this method
+ // just ignore it.
+ }
+ }
+ mListener.onSensorChanged(msg.what, (float[])msg.obj);
+ synchronized (this) {
+ // put back the array into the pool
+ if (mValuesPool == null) {
+ mValuesPool = (float[])msg.obj;
+ }
+ }
+ }
+ };
+ }
+
+ /**
+ * {@hide}
+ */
+ public SensorManager(Looper mainLooper) {
+ mSensorService = ISensorService.Stub.asInterface(
+ ServiceManager.getService(Context.SENSOR_SERVICE));
+
+ sWindowManager = IWindowManager.Stub.asInterface(
+ ServiceManager.getService("window"));
+
+ if (sWindowManager != null) {
+ // if it's null we're running in the system process
+ // which won't get the rotated values
+ try {
+ sWindowManager.watchRotation(this);
+ } catch (RemoteException e) {
+ }
+ }
+
+ mLooper = mainLooper;
+ }
+
+ /** @return available sensors */
+ public int getSensors() {
+ return _sensors_data_get_sensors();
+ }
+
+ /**
+ * Registers a listener for given sensors.
+ *
+ * @param listener sensor listener object
+ * @param sensors a bit masks of the sensors to register to
+ *
+ * @return true if the sensor is supported and successfully enabled
+ */
+ public boolean registerListener(SensorListener listener, int sensors) {
+ return registerListener(listener, sensors, SENSOR_DELAY_NORMAL);
+ }
+
+ /**
+ * Registers a listener for given sensors.
+ *
+ * @param listener sensor listener object
+ * @param sensors a bit masks of the sensors to register to
+ * @param rate rate of events. This is only a hint to the system. events
+ * may be received faster or slower than the specified rate. Usually events
+ * are received faster.
+ *
+ * @return true if the sensor is supported and successfully enabled
+ */
+ public boolean registerListener(SensorListener listener, int sensors, int rate) {
+ boolean result;
+
+ int delay = -1;
+ switch (rate) {
+ case SENSOR_DELAY_FASTEST:
+ delay = 0;
+ break;
+ case SENSOR_DELAY_GAME:
+ delay = 20;
+ break;
+ case SENSOR_DELAY_UI:
+ delay = 60;
+ break;
+ case SENSOR_DELAY_NORMAL:
+ delay = 200;
+ break;
+ default:
+ return false;
+ }
+
+ try {
+ synchronized (sListeners) {
+ ListenerDelegate l = null;
+ for (ListenerDelegate i : sListeners) {
+ if (i.mListener == listener) {
+ l = i;
+ break;
+ }
+ }
+
+ if (l == null) {
+ l = new ListenerDelegate(listener, sensors);
+ result = mSensorService.enableSensor(l, sensors, delay);
+ if (result) {
+ sListeners.add(l);
+ sListeners.notify();
+ }
+ if (!sListeners.isEmpty()) {
+ sSensorThread.startLocked(mSensorService);
+ }
+ } else {
+ result = mSensorService.enableSensor(l, sensors, delay);
+ if (result) {
+ l.addSensors(sensors);
+ }
+ }
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException in registerListener: ", e);
+ result = false;
+ }
+ return result;
+ }
+
+ /**
+ * Unregisters a listener for the sensors with which it is registered.
+ *
+ * @param listener a SensorListener object
+ * @param sensors a bit masks of the sensors to unregister from
+ */
+ public void unregisterListener(SensorListener listener, int sensors) {
+ try {
+ synchronized (sListeners) {
+ final int size = sListeners.size();
+ for (int i=0 ; i<size ; i++) {
+ ListenerDelegate l = sListeners.get(i);
+ if (l.mListener == listener) {
+ // disable these sensors
+ mSensorService.enableSensor(l, sensors, SENSOR_DISABLE);
+ // if we have no more sensors enabled on this listener,
+ // take it off the list.
+ if (l.removeSensors(sensors) == 0)
+ sListeners.remove(i);
+ break;
+ }
+ }
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException in unregisterListener: ", e);
+ }
+ }
+
+ /**
+ * Unregisters a listener for all sensors.
+ *
+ * @param listener a SensorListener object
+ */
+ public void unregisterListener(SensorListener listener) {
+ unregisterListener(listener, SENSOR_ALL);
+ }
+
+
+ /**
+ * Helper function to convert the specified sensor's data to the windows's
+ * coordinate space from the device's coordinate space.
+ */
+
+ private static void mapSensorDataToWindow(int sensor, float[] values, int orientation) {
+ final float x = values[DATA_X];
+ final float y = values[DATA_Y];
+ final float z = values[DATA_Z];
+ // copy the raw raw values...
+ values[RAW_DATA_X] = x;
+ values[RAW_DATA_Y] = y;
+ values[RAW_DATA_Z] = z;
+ // TODO: add support for 180 and 270 orientations
+ if (orientation == Surface.ROTATION_90) {
+ switch (sensor) {
+ case SENSOR_ACCELEROMETER:
+ case SENSOR_MAGNETIC_FIELD:
+ values[DATA_X] =-y;
+ values[DATA_Y] = x;
+ values[DATA_Z] = z;
+ break;
+ case SENSOR_ORIENTATION:
+ case SENSOR_ORIENTATION_RAW:
+ values[DATA_X] = x + ((x < 270) ? 90 : -270);
+ values[DATA_Y] = z;
+ values[DATA_Z] = y;
+ break;
+ }
+ }
+ }
+
+
+ private static native int _sensors_data_open(FileDescriptor fd);
+ private static native int _sensors_data_close();
+ // returns the sensor's status in the top 4 bits of "res".
+ private static native int _sensors_data_poll(float[] values, int sensors);
+ private static native int _sensors_data_get_sensors();
+
+ /** {@hide} */
+ public void onRotationChanged(int rotation) {
+ synchronized(sListeners) {
+ sRotation = rotation;
+ }
+ }
+
+ private static int getRotation() {
+ synchronized(sListeners) {
+ return sRotation;
+ }
+ }
+}
+
diff --git a/core/java/android/hardware/package.html b/core/java/android/hardware/package.html
new file mode 100644
index 0000000..06788a6
--- /dev/null
+++ b/core/java/android/hardware/package.html
@@ -0,0 +1,5 @@
+<HTML>
+<BODY>
+Provides support for hardware devices that may not be present on every Android device.
+</BODY>
+</HTML>
diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java
new file mode 100644
index 0000000..213813a
--- /dev/null
+++ b/core/java/android/net/ConnectivityManager.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.os.RemoteException;
+
+/**
+ * Class that answers queries about the state of network connectivity. It also
+ * notifies applications when network connectivity changes. Get an instance
+ * of this class by calling
+ * {@link android.content.Context#getSystemService(String) Context.getSystemService(Context.CONNECTIVITY_SERVICE)}.
+ * <p>
+ * The primary responsibilities of this class are to:
+ * <ol>
+ * <li>Monitor network connections (Wi-Fi, GPRS, UMTS, etc.)</li>
+ * <li>Send broadcast intents when network connectivity changes</li>
+ * <li>Attempt to "fail over" to another network when connectivity to a network
+ * is lost</li>
+ * <li>Provide an API that allows applications to query the coarse-grained or fine-grained
+ * state of the available networks</li>
+ * </ol>
+ */
+public class ConnectivityManager
+{
+ /**
+ * A change in network connectivity has occurred. A connection has either
+ * been established or lost. The NetworkInfo for the affected network is
+ * sent as an extra; it should be consulted to see what kind of
+ * connectivity event occurred.
+ * <p/>
+ * If this is a connection that was the result of failing over from a
+ * disconnected network, then the FAILOVER_CONNECTION boolean extra is
+ * set to true.
+ * <p/>
+ * For a loss of connectivity, if the connectivity manager is attempting
+ * to connect (or has already connected) to another network, the
+ * NetworkInfo for the new network is also passed as an extra. This lets
+ * any receivers of the broadcast know that they should not necessarily
+ * tell the user that no data traffic will be possible. Instead, the
+ * reciever should expect another broadcast soon, indicating either that
+ * the failover attempt succeeded (and so there is still overall data
+ * connectivity), or that the failover attempt failed, meaning that all
+ * connectivity has been lost.
+ * <p/>
+ * For a disconnect event, the boolean extra EXTRA_NO_CONNECTIVITY
+ * is set to {@code true} if there are no connected networks at all.
+ */
+ public static final String CONNECTIVITY_ACTION = "android.net.conn.CONNECTIVITY_CHANGE";
+ /**
+ * The lookup key for a {@link NetworkInfo} object. Retrieve with
+ * {@link android.content.Intent#getParcelableExtra(String)}.
+ */
+ public static final String EXTRA_NETWORK_INFO = "networkInfo";
+ /**
+ * The lookup key for a boolean that indicates whether a connect event
+ * is for a network to which the connectivity manager was failing over
+ * following a disconnect on another network.
+ * Retrieve it with {@link android.content.Intent#getBooleanExtra(String,boolean)}.
+ */
+ public static final String EXTRA_IS_FAILOVER = "isFailover";
+ /**
+ * The lookup key for a {@link NetworkInfo} object. This is supplied when
+ * there is another network that it may be possible to connect to. Retrieve with
+ * {@link android.content.Intent#getParcelableExtra(String)}.
+ */
+ public static final String EXTRA_OTHER_NETWORK_INFO = "otherNetwork";
+ /**
+ * The lookup key for a boolean that indicates whether there is a
+ * complete lack of connectivity, i.e., no network is available.
+ * Retrieve it with {@link android.content.Intent#getBooleanExtra(String,boolean)}.
+ */
+ public static final String EXTRA_NO_CONNECTIVITY = "noConnectivity";
+ /**
+ * The lookup key for a string that indicates why an attempt to connect
+ * to a network failed. The string has no particular structure. It is
+ * intended to be used in notifications presented to users. Retrieve
+ * it with {@link android.content.Intent#getStringExtra(String)}.
+ */
+ public static final String EXTRA_REASON = "reason";
+ /**
+ * The lookup key for a string that provides optionally supplied
+ * extra information about the network state. The information
+ * may be passed up from the lower networking layers, and its
+ * meaning may be specific to a particular network type. Retrieve
+ * it with {@link android.content.Intent#getStringExtra(String)}.
+ */
+ public static final String EXTRA_EXTRA_INFO = "extraInfo";
+
+ public static final int TYPE_MOBILE = 0;
+ public static final int TYPE_WIFI = 1;
+
+ public static final int DEFAULT_NETWORK_PREFERENCE = TYPE_WIFI;
+
+ private IConnectivityManager mService;
+
+ static public boolean isNetworkTypeValid(int networkType) {
+ return networkType == TYPE_WIFI || networkType == TYPE_MOBILE;
+ }
+
+ public void setNetworkPreference(int preference) {
+ try {
+ mService.setNetworkPreference(preference);
+ } catch (RemoteException e) {
+ }
+ }
+
+ public int getNetworkPreference() {
+ try {
+ return mService.getNetworkPreference();
+ } catch (RemoteException e) {
+ return -1;
+ }
+ }
+
+ public NetworkInfo getActiveNetworkInfo() {
+ try {
+ return mService.getActiveNetworkInfo();
+ } catch (RemoteException e) {
+ return null;
+ }
+ }
+
+ public NetworkInfo getNetworkInfo(int networkType) {
+ try {
+ return mService.getNetworkInfo(networkType);
+ } catch (RemoteException e) {
+ return null;
+ }
+ }
+
+ public NetworkInfo[] getAllNetworkInfo() {
+ try {
+ return mService.getAllNetworkInfo();
+ } catch (RemoteException e) {
+ return null;
+ }
+ }
+
+ /** {@hide} */
+ public boolean setRadios(boolean turnOn) {
+ try {
+ return mService.setRadios(turnOn);
+ } catch (RemoteException e) {
+ return false;
+ }
+ }
+
+ /** {@hide} */
+ public boolean setRadio(int networkType, boolean turnOn) {
+ try {
+ return mService.setRadio(networkType, turnOn);
+ } catch (RemoteException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Tells the underlying networking system that the caller wants to
+ * begin using the named feature. The interpretation of {@code feature}
+ * is completely up to each networking implementation.
+ * @param networkType specifies which network the request pertains to
+ * @param feature the name of the feature to be used
+ * @return an integer value representing the outcome of the request.
+ * The interpretation of this value is specific to each networking
+ * implementation+feature combination, except that the value {@code -1}
+ * always indicates failure.
+ */
+ public int startUsingNetworkFeature(int networkType, String feature) {
+ try {
+ return mService.startUsingNetworkFeature(networkType, feature);
+ } catch (RemoteException e) {
+ return -1;
+ }
+ }
+
+ /**
+ * Tells the underlying networking system that the caller is finished
+ * using the named feature. The interpretation of {@code feature}
+ * is completely up to each networking implementation.
+ * @param networkType specifies which network the request pertains to
+ * @param feature the name of the feature that is no longer needed
+ * @return an integer value representing the outcome of the request.
+ * The interpretation of this value is specific to each networking
+ * implementation+feature combination, except that the value {@code -1}
+ * always indicates failure.
+ */
+ public int stopUsingNetworkFeature(int networkType, String feature) {
+ try {
+ return mService.stopUsingNetworkFeature(networkType, feature);
+ } catch (RemoteException e) {
+ return -1;
+ }
+ }
+
+ /**
+ * Ensure that a network route exists to deliver traffic to the specified
+ * host via the specified network interface. An attempt to add a route that
+ * already exists is ignored, but treated as successful.
+ * @param networkType the type of the network over which traffic to the specified
+ * host is to be routed
+ * @param hostAddress the IP address of the host to which the route is desired
+ * @return {@code true} on success, {@code false} on failure
+ */
+ public boolean requestRouteToHost(int networkType, int hostAddress) {
+ try {
+ return mService.requestRouteToHost(networkType, hostAddress);
+ } catch (RemoteException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Don't allow use of default constructor.
+ */
+ @SuppressWarnings({"UnusedDeclaration"})
+ private ConnectivityManager() {
+ }
+
+ /**
+ * {@hide}
+ */
+ public ConnectivityManager(IConnectivityManager service) {
+ if (service == null) {
+ throw new IllegalArgumentException(
+ "ConnectivityManager() cannot be constructed with null service");
+ }
+ mService = service;
+ }
+}
diff --git a/core/java/android/net/Credentials.java b/core/java/android/net/Credentials.java
new file mode 100644
index 0000000..7f6cf9d
--- /dev/null
+++ b/core/java/android/net/Credentials.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+/**
+ * A class for representing UNIX credentials passed via ancillary data
+ * on UNIX domain sockets. See "man 7 unix" on a desktop linux distro.
+ */
+public class Credentials {
+ /** pid of process. root peers may lie. */
+ private final int pid;
+ /** uid of process. root peers may lie. */
+ private final int uid;
+ /** gid of process. root peers may lie. */
+ private final int gid;
+
+ public Credentials (int pid, int uid, int gid) {
+ this.pid = pid;
+ this.uid = uid;
+ this.gid = gid;
+ }
+
+ public int getPid() {
+ return pid;
+ }
+
+ public int getUid() {
+ return uid;
+ }
+
+ public int getGid() {
+ return gid;
+ }
+}
diff --git a/core/java/android/net/DhcpInfo.aidl b/core/java/android/net/DhcpInfo.aidl
new file mode 100644
index 0000000..29cd21f
--- /dev/null
+++ b/core/java/android/net/DhcpInfo.aidl
@@ -0,0 +1,19 @@
+/**
+ * Copyright (c) 2008, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+parcelable DhcpInfo;
diff --git a/core/java/android/net/DhcpInfo.java b/core/java/android/net/DhcpInfo.java
new file mode 100644
index 0000000..1178bec
--- /dev/null
+++ b/core/java/android/net/DhcpInfo.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.os.Parcelable;
+import android.os.Parcel;
+
+/**
+ * A simple object for retrieving the results of a DHCP request.
+ */
+public class DhcpInfo implements Parcelable {
+ public int ipAddress;
+ public int gateway;
+ public int netmask;
+
+ public int dns1;
+ public int dns2;
+
+ public int serverAddress;
+ public int leaseDuration;
+
+ public DhcpInfo() {
+ super();
+ }
+
+ public String toString() {
+ StringBuffer str = new StringBuffer();
+
+ str.append("ipaddr "); putAddress(str, ipAddress);
+ str.append(" gateway "); putAddress(str, gateway);
+ str.append(" netmask "); putAddress(str, netmask);
+ str.append(" dns1 "); putAddress(str, dns1);
+ str.append(" dns2 "); putAddress(str, dns2);
+ str.append(" DHCP server "); putAddress(str, serverAddress);
+ str.append(" lease ").append(leaseDuration).append(" seconds");
+
+ return str.toString();
+ }
+
+ private static void putAddress(StringBuffer buf, int addr) {
+ buf.append(addr & 0xff).append('.').
+ append((addr >>>= 8) & 0xff).append('.').
+ append((addr >>>= 8) & 0xff).append('.').
+ append((addr >>>= 8) & 0xff);
+ }
+
+ /** Implement the Parcelable interface {@hide} */
+ public int describeContents() {
+ return 0;
+ }
+
+ /** Implement the Parcelable interface {@hide} */
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(ipAddress);
+ dest.writeInt(gateway);
+ dest.writeInt(netmask);
+ dest.writeInt(dns1);
+ dest.writeInt(dns2);
+ dest.writeInt(serverAddress);
+ dest.writeInt(leaseDuration);
+ }
+
+ /** Implement the Parcelable interface {@hide} */
+ public static final Creator<DhcpInfo> CREATOR =
+ new Creator<DhcpInfo>() {
+ public DhcpInfo createFromParcel(Parcel in) {
+ DhcpInfo info = new DhcpInfo();
+ info.ipAddress = in.readInt();
+ info.gateway = in.readInt();
+ info.netmask = in.readInt();
+ info.dns1 = in.readInt();
+ info.dns2 = in.readInt();
+ info.serverAddress = in.readInt();
+ info.leaseDuration = in.readInt();
+ return info;
+ }
+
+ public DhcpInfo[] newArray(int size) {
+ return new DhcpInfo[size];
+ }
+ };
+}
diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl
new file mode 100644
index 0000000..e1d921f
--- /dev/null
+++ b/core/java/android/net/IConnectivityManager.aidl
@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) 2008, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.net.NetworkInfo;
+
+/**
+ * Interface that answers queries about, and allows changing, the
+ * state of network connectivity.
+ */
+/** {@hide} */
+interface IConnectivityManager
+{
+ void setNetworkPreference(int pref);
+
+ int getNetworkPreference();
+
+ NetworkInfo getActiveNetworkInfo();
+
+ NetworkInfo getNetworkInfo(int networkType);
+
+ NetworkInfo[] getAllNetworkInfo();
+
+ boolean setRadios(boolean onOff);
+
+ boolean setRadio(int networkType, boolean turnOn);
+
+ int startUsingNetworkFeature(int networkType, in String feature);
+
+ int stopUsingNetworkFeature(int networkType, in String feature);
+
+ boolean requestRouteToHost(int networkType, int hostAddress);
+}
diff --git a/core/java/android/net/LocalServerSocket.java b/core/java/android/net/LocalServerSocket.java
new file mode 100644
index 0000000..2b93fc2
--- /dev/null
+++ b/core/java/android/net/LocalServerSocket.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import java.io.IOException;
+import java.io.FileDescriptor;
+
+/**
+ * non-standard class for creating inbound UNIX-domain socket
+ * on the Android platform, this is created in the Linux non-filesystem
+ * namespace.
+ *
+ * On simulator platforms, this may be created in a temporary directory on
+ * the filesystem
+ */
+public class LocalServerSocket {
+ private final LocalSocketImpl impl;
+ private final LocalSocketAddress localAddress;
+
+ /** 50 seems a bit much, but it's what was here */
+ private static final int LISTEN_BACKLOG = 50;
+
+ /**
+ * Crewates a new server socket listening at specified name.
+ * On the Android platform, the name is created in the Linux
+ * abstract namespace (instead of on the filesystem).
+ *
+ * @param name address for socket
+ * @throws IOException
+ */
+ public LocalServerSocket(String name) throws IOException
+ {
+ impl = new LocalSocketImpl();
+
+ impl.create(true);
+
+ localAddress = new LocalSocketAddress(name);
+ impl.bind(localAddress);
+
+ impl.listen(LISTEN_BACKLOG);
+ }
+
+ /**
+ * Create a LocalServerSocket from a file descriptor that's already
+ * been created and bound. listen() will be called immediately on it.
+ * Used for cases where file descriptors are passed in via environment
+ * variables
+ *
+ * @param fd bound file descriptor
+ * @throws IOException
+ */
+ public LocalServerSocket(FileDescriptor fd) throws IOException
+ {
+ impl = new LocalSocketImpl(fd);
+ impl.listen(LISTEN_BACKLOG);
+ localAddress = impl.getSockAddress();
+ }
+
+ /**
+ * Obtains the socket's local address
+ *
+ * @return local address
+ */
+ public LocalSocketAddress getLocalSocketAddress()
+ {
+ return localAddress;
+ }
+
+ /**
+ * Accepts a new connection to the socket. Blocks until a new
+ * connection arrives.
+ *
+ * @return a socket representing the new connection.
+ * @throws IOException
+ */
+ public LocalSocket accept() throws IOException
+ {
+ LocalSocketImpl acceptedImpl = new LocalSocketImpl();
+
+ impl.accept (acceptedImpl);
+
+ return new LocalSocket(acceptedImpl);
+ }
+
+ /**
+ * Returns file descriptor or null if not yet open/already closed
+ *
+ * @return fd or null
+ */
+ public FileDescriptor getFileDescriptor() {
+ return impl.getFileDescriptor();
+ }
+
+ /**
+ * Closes server socket.
+ *
+ * @throws IOException
+ */
+ public void close() throws IOException
+ {
+ impl.close();
+ }
+}
diff --git a/core/java/android/net/LocalSocket.java b/core/java/android/net/LocalSocket.java
new file mode 100644
index 0000000..4039a69
--- /dev/null
+++ b/core/java/android/net/LocalSocket.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.SocketOptions;
+
+/**
+ * Creates a (non-server) socket in the UNIX-domain namespace. The interface
+ * here is not entirely unlike that of java.net.Socket
+ */
+public class LocalSocket {
+
+ private LocalSocketImpl impl;
+ private volatile boolean implCreated;
+ private LocalSocketAddress localAddress;
+ private boolean isBound;
+ private boolean isConnected;
+
+ /**
+ * Creates a AF_LOCAL/UNIX domain stream socket.
+ */
+ public LocalSocket() {
+ this(new LocalSocketImpl());
+ isBound = false;
+ isConnected = false;
+ }
+
+ /**
+ * for use with AndroidServerSocket
+ * @param impl a SocketImpl
+ */
+ /*package*/ LocalSocket(LocalSocketImpl impl) {
+ this.impl = impl;
+ this.isConnected = false;
+ this.isBound = false;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ return super.toString() + " impl:" + impl;
+ }
+
+ /**
+ * It's difficult to discern from the spec when impl.create() should be
+ * called, but it seems like a reasonable rule is "as soon as possible,
+ * but not in a context where IOException cannot be thrown"
+ *
+ * @throws IOException from SocketImpl.create()
+ */
+ private void implCreateIfNeeded() throws IOException {
+ if (!implCreated) {
+ synchronized (this) {
+ if (!implCreated) {
+ implCreated = true;
+ impl.create(true);
+ }
+ }
+ }
+ }
+
+ /**
+ * Connects this socket to an endpoint. May only be called on an instance
+ * that has not yet been connected.
+ *
+ * @param endpoint endpoint address
+ * @throws IOException if socket is in invalid state or the address does
+ * not exist.
+ */
+ public void connect(LocalSocketAddress endpoint) throws IOException {
+ synchronized (this) {
+ if (isConnected) {
+ throw new IOException("already connected");
+ }
+
+ implCreateIfNeeded();
+ impl.connect(endpoint, 0);
+ isConnected = true;
+ isBound = true;
+ }
+ }
+
+ /**
+ * Binds this socket to an endpoint name. May only be called on an instance
+ * that has not yet been bound.
+ *
+ * @param bindpoint endpoint address
+ * @throws IOException
+ */
+ public void bind(LocalSocketAddress bindpoint) throws IOException {
+ implCreateIfNeeded();
+
+ synchronized (this) {
+ if (isBound) {
+ throw new IOException("already bound");
+ }
+
+ localAddress = bindpoint;
+ impl.bind(localAddress);
+ isBound = true;
+ }
+ }
+
+ /**
+ * Retrieves the name that this socket is bound to, if any.
+ *
+ * @return Local address or null if anonymous
+ */
+ public LocalSocketAddress getLocalSocketAddress() {
+ return localAddress;
+ }
+
+ /**
+ * Retrieves the input stream for this instance.
+ *
+ * @return input stream
+ * @throws IOException if socket has been closed or cannot be created.
+ */
+ public InputStream getInputStream() throws IOException {
+ implCreateIfNeeded();
+ return impl.getInputStream();
+ }
+
+ /**
+ * Retrieves the output stream for this instance.
+ *
+ * @return output stream
+ * @throws IOException if socket has been closed or cannot be created.
+ */
+ public OutputStream getOutputStream() throws IOException {
+ implCreateIfNeeded();
+ return impl.getOutputStream();
+ }
+
+ /**
+ * Closes the socket.
+ *
+ * @throws IOException
+ */
+ public void close() throws IOException {
+ implCreateIfNeeded();
+ impl.close();
+ }
+
+ /**
+ * Shuts down the input side of the socket.
+ *
+ * @throws IOException
+ */
+ public void shutdownInput() throws IOException {
+ implCreateIfNeeded();
+ impl.shutdownInput();
+ }
+
+ /**
+ * Shuts down the output side of the socket.
+ *
+ * @throws IOException
+ */
+ public void shutdownOutput() throws IOException {
+ implCreateIfNeeded();
+ impl.shutdownOutput();
+ }
+
+ public void setReceiveBufferSize(int size) throws IOException {
+ impl.setOption(SocketOptions.SO_RCVBUF, Integer.valueOf(size));
+ }
+
+ public int getReceiveBufferSize() throws IOException {
+ return ((Integer) impl.getOption(SocketOptions.SO_RCVBUF)).intValue();
+ }
+
+ public void setSoTimeout(int n) throws IOException {
+ impl.setOption(SocketOptions.SO_TIMEOUT, Integer.valueOf(n));
+ }
+
+ public int getSoTimeout() throws IOException {
+ return ((Integer) impl.getOption(SocketOptions.SO_TIMEOUT)).intValue();
+ }
+
+ public void setSendBufferSize(int n) throws IOException {
+ impl.setOption(SocketOptions.SO_SNDBUF, Integer.valueOf(n));
+ }
+
+ public int getSendBufferSize() throws IOException {
+ return ((Integer) impl.getOption(SocketOptions.SO_SNDBUF)).intValue();
+ }
+
+ //???SEC
+ public LocalSocketAddress getRemoteSocketAddress() {
+ throw new UnsupportedOperationException();
+ }
+
+ //???SEC
+ public synchronized boolean isConnected() {
+ return isConnected;
+ }
+
+ //???SEC
+ public boolean isClosed() {
+ throw new UnsupportedOperationException();
+ }
+
+ //???SEC
+ public synchronized boolean isBound() {
+ return isBound;
+ }
+
+ //???SEC
+ public boolean isOutputShutdown() {
+ throw new UnsupportedOperationException();
+ }
+
+ //???SEC
+ public boolean isInputShutdown() {
+ throw new UnsupportedOperationException();
+ }
+
+ //???SEC
+ public void connect(LocalSocketAddress endpoint, int timeout)
+ throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Enqueues a set of file descriptors to send to the peer. The queue
+ * is one deep. The file descriptors will be sent with the next write
+ * of normal data, and will be delivered in a single ancillary message.
+ * See "man 7 unix" SCM_RIGHTS on a desktop Linux machine.
+ *
+ * @param fds non-null; file descriptors to send.
+ */
+ public void setFileDescriptorsForSend(FileDescriptor[] fds) {
+ impl.setFileDescriptorsForSend(fds);
+ }
+
+ /**
+ * Retrieves a set of file descriptors that a peer has sent through
+ * an ancillary message. This method retrieves the most recent set sent,
+ * and then returns null until a new set arrives.
+ * File descriptors may only be passed along with regular data, so this
+ * method can only return a non-null after a read operation.
+ *
+ * @return null or file descriptor array
+ * @throws IOException
+ */
+ public FileDescriptor[] getAncillaryFileDescriptors() throws IOException {
+ return impl.getAncillaryFileDescriptors();
+ }
+
+ /**
+ * Retrieves the credentials of this socket's peer. Only valid on
+ * connected sockets.
+ *
+ * @return non-null; peer credentials
+ * @throws IOException
+ */
+ public Credentials getPeerCredentials() throws IOException {
+ return impl.getPeerCredentials();
+ }
+
+ /**
+ * Returns file descriptor or null if not yet open/already closed
+ *
+ * @return fd or null
+ */
+ public FileDescriptor getFileDescriptor() {
+ return impl.getFileDescriptor();
+ }
+}
diff --git a/core/java/android/net/LocalSocketAddress.java b/core/java/android/net/LocalSocketAddress.java
new file mode 100644
index 0000000..8265b85
--- /dev/null
+++ b/core/java/android/net/LocalSocketAddress.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+/**
+ * A UNIX-domain (AF_LOCAL) socket address. For use with
+ * android.net.LocalSocket and android.net.LocalServerSocket.
+ *
+ * On the Android system, these names refer to names in the Linux
+ * abstract (non-filesystem) UNIX domain namespace.
+ */
+public class LocalSocketAddress
+{
+ /**
+ * The namespace that this address exists in. See also
+ * include/cutils/sockets.h ANDROID_SOCKET_NAMESPACE_*
+ */
+ public enum Namespace {
+ /** A socket in the Linux abstract namespace */
+ ABSTRACT(0),
+ /**
+ * A socket in the Android reserved namespace in /dev/socket.
+ * Only the init process may create a socket here.
+ */
+ RESERVED(1),
+ /**
+ * A socket named with a normal filesystem path.
+ */
+ FILESYSTEM(2);
+
+ /** The id matches with a #define in include/cutils/sockets.h */
+ private int id;
+ Namespace (int id) {
+ this.id = id;
+ }
+
+ /**
+ * @return int constant shared with native code
+ */
+ /*package*/ int getId() {
+ return id;
+ }
+ }
+
+ private final String name;
+ private final Namespace namespace;
+
+ /**
+ * Creates an instance with a given name.
+ *
+ * @param name non-null name
+ * @param namespace namespace the name should be created in.
+ */
+ public LocalSocketAddress(String name, Namespace namespace) {
+ this.name = name;
+ this.namespace = namespace;
+ }
+
+ /**
+ * Creates an instance with a given name in the {@link Namespace#ABSTRACT}
+ * namespace
+ *
+ * @param name non-null name
+ */
+ public LocalSocketAddress(String name) {
+ this(name,Namespace.ABSTRACT);
+ }
+
+ /**
+ * Retrieves the string name of this address
+ * @return string name
+ */
+ public String getName()
+ {
+ return name;
+ }
+
+ /**
+ * Returns the namespace used by this address.
+ *
+ * @return non-null a namespace
+ */
+ public Namespace getNamespace() {
+ return namespace;
+ }
+}
diff --git a/core/java/android/net/LocalSocketImpl.java b/core/java/android/net/LocalSocketImpl.java
new file mode 100644
index 0000000..6c36a7d
--- /dev/null
+++ b/core/java/android/net/LocalSocketImpl.java
@@ -0,0 +1,490 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.InputStream;
+import java.io.FileDescriptor;
+import java.net.SocketOptions;
+
+/**
+ * Socket implementation used for android.net.LocalSocket and
+ * android.net.LocalServerSocket. Supports only AF_LOCAL sockets.
+ */
+class LocalSocketImpl
+{
+ private SocketInputStream fis;
+ private SocketOutputStream fos;
+ private Object readMonitor = new Object();
+ private Object writeMonitor = new Object();
+
+ /** null if closed or not yet created */
+ private FileDescriptor fd;
+
+ // These fields are accessed by native code;
+ /** file descriptor array received during a previous read */
+ FileDescriptor[] inboundFileDescriptors;
+ /** file descriptor array that should be written during next write */
+ FileDescriptor[] outboundFileDescriptors;
+
+ /**
+ * An input stream for local sockets. Needed because we may
+ * need to read ancillary data.
+ */
+ class SocketInputStream extends InputStream {
+ /** {@inheritDoc} */
+ @Override
+ public int available() throws IOException {
+ return available_native(fd);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void close() throws IOException {
+ LocalSocketImpl.this.close();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int read() throws IOException {
+ int ret;
+ synchronized (readMonitor) {
+ FileDescriptor myFd = fd;
+ if (myFd == null) throw new IOException("socket closed");
+
+ ret = read_native(myFd);
+ return ret;
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int read(byte[] b) throws IOException {
+ return read(b, 0, b.length);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException {
+ synchronized (readMonitor) {
+ FileDescriptor myFd = fd;
+ if (myFd == null) throw new IOException("socket closed");
+
+ if (off < 0 || len < 0 || (off + len) > b.length ) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+
+ int ret = readba_native(b, off, len, myFd);
+
+ return ret;
+ }
+ }
+ }
+
+ /**
+ * An output stream for local sockets. Needed because we may
+ * need to read ancillary data.
+ */
+ class SocketOutputStream extends OutputStream {
+ /** {@inheritDoc} */
+ @Override
+ public void close() throws IOException {
+ LocalSocketImpl.this.close();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void write (byte[] b) throws IOException {
+ write(b, 0, b.length);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void write (byte[] b, int off, int len) throws IOException {
+ synchronized (writeMonitor) {
+ FileDescriptor myFd = fd;
+ if (myFd == null) throw new IOException("socket closed");
+
+ if (off < 0 || len < 0 || (off + len) > b.length ) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+ writeba_native(b, off, len, myFd);
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void write (int b) throws IOException {
+ synchronized (writeMonitor) {
+ FileDescriptor myFd = fd;
+ if (myFd == null) throw new IOException("socket closed");
+ write_native(b, myFd);
+ }
+ }
+ }
+
+ private native int available_native(FileDescriptor fd) throws IOException;
+ private native void close_native(FileDescriptor fd) throws IOException;
+ private native int read_native(FileDescriptor fd) throws IOException;
+ private native int readba_native(byte[] b, int off, int len,
+ FileDescriptor fd) throws IOException;
+ private native void writeba_native(byte[] b, int off, int len,
+ FileDescriptor fd) throws IOException;
+ private native void write_native(int b, FileDescriptor fd)
+ throws IOException;
+ private native void connectLocal(FileDescriptor fd, String name,
+ int namespace) throws IOException;
+ private native void bindLocal(FileDescriptor fd, String name, int namespace)
+ throws IOException;
+ private native FileDescriptor create_native(boolean stream)
+ throws IOException;
+ private native void listen_native(FileDescriptor fd, int backlog)
+ throws IOException;
+ private native void shutdown(FileDescriptor fd, boolean shutdownInput);
+ private native Credentials getPeerCredentials_native(
+ FileDescriptor fd) throws IOException;
+ private native int getOption_native(FileDescriptor fd, int optID)
+ throws IOException;
+ private native void setOption_native(FileDescriptor fd, int optID,
+ int b, int value) throws IOException;
+
+// private native LocalSocketAddress getSockName_native
+// (FileDescriptor fd) throws IOException;
+
+ /**
+ * Accepts a connection on a server socket.
+ *
+ * @param fd file descriptor of server socket
+ * @param s socket implementation that will become the new socket
+ * @return file descriptor of new socket
+ */
+ private native FileDescriptor accept
+ (FileDescriptor fd, LocalSocketImpl s) throws IOException;
+
+ /**
+ * Create a new instance.
+ */
+ /*package*/ LocalSocketImpl()
+ {
+ }
+
+ /**
+ * Create a new instance from a file descriptor representing
+ * a bound socket. The state of the file descriptor is not checked here
+ * but the caller can verify socket state by calling listen().
+ *
+ * @param fd non-null; bound file descriptor
+ */
+ /*package*/ LocalSocketImpl(FileDescriptor fd) throws IOException
+ {
+ this.fd = fd;
+ }
+
+ public String toString() {
+ return super.toString() + " fd:" + fd;
+ }
+
+ /**
+ * Creates a socket in the underlying OS.
+ *
+ * @param stream true if this should be a stream socket, false for
+ * datagram.
+ * @throws IOException
+ */
+ public void create (boolean stream) throws IOException {
+ // no error if socket already created
+ // need this for LocalServerSocket.accept()
+ if (fd == null) {
+ fd = create_native(stream);
+ }
+ }
+
+ /**
+ * Closes the socket.
+ *
+ * @throws IOException
+ */
+ public void close() throws IOException {
+ synchronized (LocalSocketImpl.this) {
+ if (fd == null) return;
+ close_native(fd);
+ fd = null;
+ }
+ }
+
+ /** note timeout presently ignored */
+ protected void connect(LocalSocketAddress address, int timeout)
+ throws IOException
+ {
+ if (fd == null) {
+ throw new IOException("socket not created");
+ }
+
+ connectLocal(fd, address.getName(), address.getNamespace().getId());
+ }
+
+ /**
+ * Binds this socket to an endpoint name. May only be called on an instance
+ * that has not yet been bound.
+ *
+ * @param endpoint endpoint address
+ * @throws IOException
+ */
+ public void bind(LocalSocketAddress endpoint) throws IOException
+ {
+ if (fd == null) {
+ throw new IOException("socket not created");
+ }
+
+ bindLocal(fd, endpoint.getName(), endpoint.getNamespace().getId());
+ }
+
+ protected void listen(int backlog) throws IOException
+ {
+ if (fd == null) {
+ throw new IOException("socket not created");
+ }
+
+ listen_native(fd, backlog);
+ }
+
+ /**
+ * Accepts a new connection to the socket. Blocks until a new
+ * connection arrives.
+ *
+ * @param s a socket that will be used to represent the new connection.
+ * @throws IOException
+ */
+ protected void accept(LocalSocketImpl s) throws IOException
+ {
+ if (fd == null) {
+ throw new IOException("socket not created");
+ }
+
+ s.fd = accept(fd, s);
+ }
+
+ /**
+ * Retrieves the input stream for this instance.
+ *
+ * @return input stream
+ * @throws IOException if socket has been closed or cannot be created.
+ */
+ protected InputStream getInputStream() throws IOException
+ {
+ if (fd == null) {
+ throw new IOException("socket not created");
+ }
+
+ synchronized (this) {
+ if (fis == null) {
+ fis = new SocketInputStream();
+ }
+
+ return fis;
+ }
+ }
+
+ /**
+ * Retrieves the output stream for this instance.
+ *
+ * @return output stream
+ * @throws IOException if socket has been closed or cannot be created.
+ */
+ protected OutputStream getOutputStream() throws IOException
+ {
+ if (fd == null) {
+ throw new IOException("socket not created");
+ }
+
+ synchronized (this) {
+ if (fos == null) {
+ fos = new SocketOutputStream();
+ }
+
+ return fos;
+ }
+ }
+
+ /**
+ * Returns the number of bytes available for reading without blocking.
+ *
+ * @return >= 0 count bytes available
+ * @throws IOException
+ */
+ protected int available() throws IOException
+ {
+ return getInputStream().available();
+ }
+
+ /**
+ * Shuts down the input side of the socket.
+ *
+ * @throws IOException
+ */
+ protected void shutdownInput() throws IOException
+ {
+ if (fd == null) {
+ throw new IOException("socket not created");
+ }
+
+ shutdown(fd, true);
+ }
+
+ /**
+ * Shuts down the output side of the socket.
+ *
+ * @throws IOException
+ */
+ protected void shutdownOutput() throws IOException
+ {
+ if (fd == null) {
+ throw new IOException("socket not created");
+ }
+
+ shutdown(fd, false);
+ }
+
+ protected FileDescriptor getFileDescriptor()
+ {
+ return fd;
+ }
+
+ protected boolean supportsUrgentData()
+ {
+ return false;
+ }
+
+ protected void sendUrgentData(int data) throws IOException
+ {
+ throw new RuntimeException ("not impled");
+ }
+
+ public Object getOption(int optID) throws IOException
+ {
+ if (fd == null) {
+ throw new IOException("socket not created");
+ }
+
+ if (optID == SocketOptions.SO_TIMEOUT) {
+ return 0;
+ }
+
+ int value = getOption_native(fd, optID);
+ switch (optID)
+ {
+ case SocketOptions.SO_RCVBUF:
+ case SocketOptions.SO_SNDBUF:
+ return value;
+ case SocketOptions.SO_REUSEADDR:
+ default:
+ return value;
+ }
+ }
+
+ public void setOption(int optID, Object value)
+ throws IOException {
+ /*
+ * Boolean.FALSE is used to disable some options, so it
+ * is important to distinguish between FALSE and unset.
+ * We define it here that -1 is unset, 0 is FALSE, and 1
+ * is TRUE.
+ */
+ int boolValue = -1;
+ int intValue = 0;
+
+ if (fd == null) {
+ throw new IOException("socket not created");
+ }
+
+ if (value instanceof Integer) {
+ intValue = (Integer)value;
+ } else if (value instanceof Boolean) {
+ boolValue = ((Boolean) value)? 1 : 0;
+ } else {
+ throw new IOException("bad value: " + value);
+ }
+
+ setOption_native(fd, optID, boolValue, intValue);
+ }
+
+ /**
+ * Enqueues a set of file descriptors to send to the peer. The queue
+ * is one deep. The file descriptors will be sent with the next write
+ * of normal data, and will be delivered in a single ancillary message.
+ * See "man 7 unix" SCM_RIGHTS on a desktop Linux machine.
+ *
+ * @param fds non-null; file descriptors to send.
+ * @throws IOException
+ */
+ public void setFileDescriptorsForSend(FileDescriptor[] fds) {
+ synchronized(writeMonitor) {
+ outboundFileDescriptors = fds;
+ }
+ }
+
+ /**
+ * Retrieves a set of file descriptors that a peer has sent through
+ * an ancillary message. This method retrieves the most recent set sent,
+ * and then returns null until a new set arrives.
+ * File descriptors may only be passed along with regular data, so this
+ * method can only return a non-null after a read operation.
+ *
+ * @return null or file descriptor array
+ * @throws IOException
+ */
+ public FileDescriptor[] getAncillaryFileDescriptors() throws IOException {
+ synchronized(readMonitor) {
+ FileDescriptor[] result = inboundFileDescriptors;
+
+ inboundFileDescriptors = null;
+ return result;
+ }
+ }
+
+ /**
+ * Retrieves the credentials of this socket's peer. Only valid on
+ * connected sockets.
+ *
+ * @return non-null; peer credentials
+ * @throws IOException
+ */
+ public Credentials getPeerCredentials() throws IOException
+ {
+ return getPeerCredentials_native(fd);
+ }
+
+ /**
+ * Retrieves the socket name from the OS.
+ *
+ * @return non-null; socket name
+ * @throws IOException on failure
+ */
+ public LocalSocketAddress getSockAddress() throws IOException
+ {
+ return null;
+ //TODO implement this
+ //return getSockName_native(fd);
+ }
+
+ @Override
+ protected void finalize() throws IOException {
+ close();
+ }
+}
+
diff --git a/core/java/android/net/MailTo.java b/core/java/android/net/MailTo.java
new file mode 100644
index 0000000..ca28f86
--- /dev/null
+++ b/core/java/android/net/MailTo.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ *
+ * MailTo URL parser
+ *
+ * This class parses a mailto scheme URL and then can be queried for
+ * the parsed parameters. This implements RFC 2368.
+ *
+ */
+public class MailTo {
+
+ static public final String MAILTO_SCHEME = "mailto:";
+
+ // All the parsed content is added to the headers.
+ private HashMap<String, String> mHeaders;
+
+ // Well known headers
+ static private final String TO = "to";
+ static private final String BODY = "body";
+ static private final String CC = "cc";
+ static private final String SUBJECT = "subject";
+
+
+ /**
+ * Test to see if the given string is a mailto URL
+ * @param url string to be tested
+ * @return true if the string is a mailto URL
+ */
+ public static boolean isMailTo(String url) {
+ if (url != null && url.startsWith(MAILTO_SCHEME)) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Parse and decode a mailto scheme string. This parser implements
+ * RFC 2368. The returned object can be queried for the parsed parameters.
+ * @param url String containing a mailto URL
+ * @return MailTo object
+ * @exception ParseException if the scheme is not a mailto URL
+ */
+ public static MailTo parse(String url) throws ParseException {
+ if (url == null) {
+ throw new NullPointerException();
+ }
+ if (!isMailTo(url)) {
+ throw new ParseException("Not a mailto scheme");
+ }
+ // Strip the scheme as the Uri parser can't cope with it.
+ String noScheme = url.substring(MAILTO_SCHEME.length());
+ Uri email = Uri.parse(noScheme);
+ MailTo m = new MailTo();
+
+ // Parse out the query parameters
+ String query = email.getQuery();
+ if (query != null ) {
+ String[] queries = query.split("&");
+ for (String q : queries) {
+ String[] nameval = q.split("=");
+ if (nameval.length == 0) {
+ continue;
+ }
+ // insert the headers with the name in lowercase so that
+ // we can easily find common headers
+ m.mHeaders.put(Uri.decode(nameval[0]).toLowerCase(),
+ nameval.length > 1 ? Uri.decode(nameval[1]) : null);
+ }
+ }
+
+ // Address can be specified in both the headers and just after the
+ // mailto line. Join the two together.
+ String address = email.getPath();
+ if (address != null) {
+ String addr = m.getTo();
+ if (addr != null) {
+ address += ", " + addr;
+ }
+ m.mHeaders.put(TO, address);
+ }
+
+ return m;
+ }
+
+ /**
+ * Retrieve the To address line from the parsed mailto URL. This could be
+ * several email address that are comma-space delimited.
+ * If no To line was specified, then null is return
+ * @return comma delimited email addresses or null
+ */
+ public String getTo() {
+ return mHeaders.get(TO);
+ }
+
+ /**
+ * Retrieve the CC address line from the parsed mailto URL. This could be
+ * several email address that are comma-space delimited.
+ * If no CC line was specified, then null is return
+ * @return comma delimited email addresses or null
+ */
+ public String getCc() {
+ return mHeaders.get(CC);
+ }
+
+ /**
+ * Retrieve the subject line from the parsed mailto URL.
+ * If no subject line was specified, then null is return
+ * @return subject or null
+ */
+ public String getSubject() {
+ return mHeaders.get(SUBJECT);
+ }
+
+ /**
+ * Retrieve the body line from the parsed mailto URL.
+ * If no body line was specified, then null is return
+ * @return body or null
+ */
+ public String getBody() {
+ return mHeaders.get(BODY);
+ }
+
+ /**
+ * Retrieve all the parsed email headers from the mailto URL
+ * @return map containing all parsed values
+ */
+ public Map<String, String> getHeaders() {
+ return mHeaders;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder(MAILTO_SCHEME);
+ sb.append('?');
+ for (Map.Entry<String,String> header : mHeaders.entrySet()) {
+ sb.append(Uri.encode(header.getKey()));
+ sb.append('=');
+ sb.append(Uri.encode(header.getValue()));
+ sb.append('&');
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Private constructor. The only way to build a Mailto object is through
+ * the parse() method.
+ */
+ private MailTo() {
+ mHeaders = new HashMap<String, String>();
+ }
+}
diff --git a/core/java/android/net/MobileDataStateTracker.java b/core/java/android/net/MobileDataStateTracker.java
new file mode 100644
index 0000000..ae74e6f
--- /dev/null
+++ b/core/java/android/net/MobileDataStateTracker.java
@@ -0,0 +1,479 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.RemoteException;
+import android.os.Handler;
+import android.os.ServiceManager;
+import android.os.SystemProperties;
+import com.android.internal.telephony.ITelephony;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.TelephonyIntents;
+import android.net.NetworkInfo.DetailedState;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+import android.util.Config;
+import android.text.TextUtils;
+
+import java.util.List;
+import java.util.ArrayList;
+
+/**
+ * Track the state of mobile data connectivity. This is done by
+ * receiving broadcast intents from the Phone process whenever
+ * the state of data connectivity changes.
+ *
+ * {@hide}
+ */
+public class MobileDataStateTracker extends NetworkStateTracker {
+
+ private static final String TAG = "MobileDataStateTracker";
+ private static final boolean DBG = false;
+
+ private Phone.DataState mMobileDataState;
+ private ITelephony mPhoneService;
+ private static final String[] sDnsPropNames = {
+ "net.rmnet0.dns1",
+ "net.rmnet0.dns2",
+ "net.eth0.dns1",
+ "net.eth0.dns2",
+ "net.eth0.dns3",
+ "net.eth0.dns4",
+ "net.gprs.dns1",
+ "net.gprs.dns2"
+ };
+ private List<String> mDnsServers;
+ private String mInterfaceName;
+ private int mDefaultGatewayAddr;
+ private int mLastCallingPid = -1;
+
+ /**
+ * Create a new MobileDataStateTracker
+ * @param context the application context of the caller
+ * @param target a message handler for getting callbacks about state changes
+ */
+ public MobileDataStateTracker(Context context, Handler target) {
+ super(context, target, ConnectivityManager.TYPE_MOBILE);
+ mPhoneService = null;
+ mDnsServers = new ArrayList<String>();
+ }
+
+ /**
+ * Begin monitoring mobile data connectivity.
+ */
+ public void startMonitoring() {
+
+ IntentFilter filter = new IntentFilter(TelephonyIntents.ACTION_ANY_DATA_CONNECTION_STATE_CHANGED);
+ filter.addAction(TelephonyIntents.ACTION_DATA_CONNECTION_FAILED);
+
+ Intent intent = mContext.registerReceiver(new MobileDataStateReceiver(), filter);
+ if (intent != null)
+ mMobileDataState = getMobileDataState(intent);
+ else
+ mMobileDataState = Phone.DataState.DISCONNECTED;
+ }
+
+ private static Phone.DataState getMobileDataState(Intent intent) {
+ String str = intent.getStringExtra(Phone.STATE_KEY);
+ if (str != null)
+ return Enum.valueOf(Phone.DataState.class, str);
+ else
+ return Phone.DataState.DISCONNECTED;
+ }
+
+ private class MobileDataStateReceiver extends BroadcastReceiver {
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction().equals(TelephonyIntents.ACTION_ANY_DATA_CONNECTION_STATE_CHANGED)) {
+ Phone.DataState state = getMobileDataState(intent);
+ String reason = intent.getStringExtra(Phone.STATE_CHANGE_REASON_KEY);
+ String apnName = intent.getStringExtra(Phone.DATA_APN_KEY);
+ boolean unavailable = intent.getBooleanExtra(Phone.NETWORK_UNAVAILABLE_KEY, false);
+ if (DBG) Log.d(TAG, "Received " + intent.getAction() +
+ " broadcast - state = " + state
+ + ", unavailable = " + unavailable
+ + ", reason = " + (reason == null ? "(unspecified)" : reason));
+ mNetworkInfo.setIsAvailable(!unavailable);
+ if (mMobileDataState != state) {
+ mMobileDataState = state;
+
+ switch (state) {
+ case DISCONNECTED:
+ setDetailedState(DetailedState.DISCONNECTED, reason, apnName);
+ if (mInterfaceName != null) {
+ NetworkUtils.resetConnections(mInterfaceName);
+ }
+ mInterfaceName = null;
+ mDefaultGatewayAddr = 0;
+ break;
+ case CONNECTING:
+ setDetailedState(DetailedState.CONNECTING, reason, apnName);
+ break;
+ case SUSPENDED:
+ setDetailedState(DetailedState.SUSPENDED, reason, apnName);
+ break;
+ case CONNECTED:
+ mInterfaceName = intent.getStringExtra(Phone.DATA_IFACE_NAME_KEY);
+ if (mInterfaceName == null) {
+ Log.d(TAG, "CONNECTED event did not supply interface name.");
+ }
+ setupDnsProperties();
+ setDetailedState(DetailedState.CONNECTED, reason, apnName);
+ break;
+ }
+ }
+ } else if (intent.getAction().equals(TelephonyIntents.ACTION_DATA_CONNECTION_FAILED)) {
+ String reason = intent.getStringExtra(Phone.FAILURE_REASON_KEY);
+ String apnName = intent.getStringExtra(Phone.DATA_APN_KEY);
+ if (DBG) Log.d(TAG, "Received " + intent.getAction() + " broadcast" +
+ reason == null ? "" : "(" + reason + ")");
+ setDetailedState(DetailedState.FAILED, reason, apnName);
+ }
+ }
+ }
+
+ /**
+ * Make sure that route(s) exist to the carrier DNS server(s).
+ */
+ public void addPrivateRoutes() {
+ if (mInterfaceName != null) {
+ for (String addrString : mDnsServers) {
+ int addr = NetworkUtils.lookupHost(addrString);
+ if (addr != -1) {
+ NetworkUtils.addHostRoute(mInterfaceName, addr);
+ }
+ }
+ }
+ }
+
+ public void removePrivateRoutes() {
+ if(mInterfaceName != null) {
+ NetworkUtils.removeHostRoutes(mInterfaceName);
+ }
+ }
+
+ public void removeDefaultRoute() {
+ if(mInterfaceName != null) {
+ mDefaultGatewayAddr = NetworkUtils.getDefaultRoute(mInterfaceName);
+ NetworkUtils.removeDefaultRoute(mInterfaceName);
+ }
+ }
+
+ public void restoreDefaultRoute() {
+ // 0 is not a valid address for a gateway
+ if (mInterfaceName != null && mDefaultGatewayAddr != 0) {
+ NetworkUtils.setDefaultRoute(mInterfaceName, mDefaultGatewayAddr);
+ }
+ }
+
+ private void getPhoneService(boolean forceRefresh) {
+ if ((mPhoneService == null) || forceRefresh) {
+ mPhoneService = ITelephony.Stub.asInterface(ServiceManager.getService("phone"));
+ }
+ }
+
+ /**
+ * Report whether data connectivity is possible.
+ */
+ public boolean isAvailable() {
+ getPhoneService(false);
+
+ /*
+ * If the phone process has crashed in the past, we'll get a
+ * RemoteException and need to re-reference the service.
+ */
+ for (int retry = 0; retry < 2; retry++) {
+ if (mPhoneService == null) break;
+
+ try {
+ return mPhoneService.isDataConnectivityPossible();
+ } catch (RemoteException e) {
+ // First-time failed, get the phone service again
+ if (retry == 0) getPhoneService(true);
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Return the IP addresses of the DNS servers available for the mobile data
+ * network interface.
+ * @return a list of DNS addresses, with no holes.
+ */
+ public String[] getNameServers() {
+ return getNameServerList(sDnsPropNames);
+ }
+
+ /**
+ * Return the system properties name associated with the tcp buffer sizes
+ * for this network.
+ */
+ public String getTcpBufferSizesPropName() {
+ String networkTypeStr = "unknown";
+ TelephonyManager tm = new TelephonyManager(mContext);
+ switch(tm.getNetworkType()) {
+ case TelephonyManager.NETWORK_TYPE_GPRS:
+ networkTypeStr = "gprs";
+ break;
+ case TelephonyManager.NETWORK_TYPE_EDGE:
+ networkTypeStr = "edge";
+ break;
+ case TelephonyManager.NETWORK_TYPE_UMTS:
+ networkTypeStr = "umts";
+ break;
+ }
+ return "net.tcp.buffersize." + networkTypeStr;
+ }
+
+ /**
+ * Tear down mobile data connectivity, i.e., disable the ability to create
+ * mobile data connections.
+ */
+ @Override
+ public boolean teardown() {
+ getPhoneService(false);
+ /*
+ * If the phone process has crashed in the past, we'll get a
+ * RemoteException and need to re-reference the service.
+ */
+ for (int retry = 0; retry < 2; retry++) {
+ if (mPhoneService == null) {
+ Log.w(TAG,
+ "Ignoring mobile data teardown request because could not acquire PhoneService");
+ break;
+ }
+
+ try {
+ return mPhoneService.disableDataConnectivity();
+ } catch (RemoteException e) {
+ if (retry == 0) getPhoneService(true);
+ }
+ }
+
+ Log.w(TAG, "Failed to tear down mobile data connectivity");
+ return false;
+ }
+
+ /**
+ * Re-enable mobile data connectivity after a {@link #teardown()}.
+ */
+ public boolean reconnect() {
+ getPhoneService(false);
+ /*
+ * If the phone process has crashed in the past, we'll get a
+ * RemoteException and need to re-reference the service.
+ */
+ for (int retry = 0; retry < 2; retry++) {
+ if (mPhoneService == null) {
+ Log.w(TAG,
+ "Ignoring mobile data connect request because could not acquire PhoneService");
+ break;
+ }
+
+ try {
+ return mPhoneService.enableDataConnectivity();
+ } catch (RemoteException e) {
+ if (retry == 0) getPhoneService(true);
+ }
+ }
+
+ Log.w(TAG, "Failed to set up mobile data connectivity");
+ return false;
+ }
+
+ /**
+ * Turn on or off the mobile radio. No connectivity will be possible while the
+ * radio is off. The operation is a no-op if the radio is already in the desired state.
+ * @param turnOn {@code true} if the radio should be turned on, {@code false} if
+ */
+ public boolean setRadio(boolean turnOn) {
+ getPhoneService(false);
+ /*
+ * If the phone process has crashed in the past, we'll get a
+ * RemoteException and need to re-reference the service.
+ */
+ for (int retry = 0; retry < 2; retry++) {
+ if (mPhoneService == null) {
+ Log.w(TAG,
+ "Ignoring mobile radio request because could not acquire PhoneService");
+ break;
+ }
+
+ try {
+ return mPhoneService.setRadio(turnOn);
+ } catch (RemoteException e) {
+ if (retry == 0) getPhoneService(true);
+ }
+ }
+
+ Log.w(TAG, "Could not set radio power to " + (turnOn ? "on" : "off"));
+ return false;
+ }
+
+ /**
+ * Tells the phone sub-system that the caller wants to
+ * begin using the named feature. The only supported feature at
+ * this time is {@code Phone.FEATURE_ENABLE_MMS}, which allows an application
+ * to specify that it wants to send and/or receive MMS data.
+ * @param feature the name of the feature to be used
+ * @param callingPid the process ID of the process that is issuing this request
+ * @param callingUid the user ID of the process that is issuing this request
+ * @return an integer value representing the outcome of the request.
+ * The interpretation of this value is feature-specific.
+ * specific, except that the value {@code -1}
+ * always indicates failure. For {@code Phone.FEATURE_ENABLE_MMS},
+ * the other possible return values are
+ * <ul>
+ * <li>{@code Phone.APN_ALREADY_ACTIVE}</li>
+ * <li>{@code Phone.APN_REQUEST_STARTED}</li>
+ * <li>{@code Phone.APN_TYPE_NOT_AVAILABLE}</li>
+ * <li>{@code Phone.APN_REQUEST_FAILED}</li>
+ * </ul>
+ */
+ public int startUsingNetworkFeature(String feature, int callingPid, int callingUid) {
+ if (TextUtils.equals(feature, Phone.FEATURE_ENABLE_MMS)) {
+ mLastCallingPid = callingPid;
+ return setEnableApn(Phone.APN_TYPE_MMS, true);
+ } else {
+ return -1;
+ }
+ }
+
+ /**
+ * Tells the phone sub-system that the caller is finished is
+ * finished using the named feature. The only supported feature at
+ * this time is {@code Phone.FEATURE_ENABLE_MMS}, which allows an application
+ * to specify that it wants to send and/or receive MMS data.
+ * @param feature the name of the feature that is no longer needed
+ * @param callingPid the process ID of the process that is issuing this request
+ * @param callingUid the user ID of the process that is issuing this request
+ * @return an integer value representing the outcome of the request.
+ * The interpretation of this value is feature-specific, except that
+ * the value {@code -1} always indicates failure.
+ */
+ public int stopUsingNetworkFeature(String feature, int callingPid, int callingUid) {
+ if (TextUtils.equals(feature, Phone.FEATURE_ENABLE_MMS)) {
+ return setEnableApn(Phone.APN_TYPE_MMS, false);
+ } else {
+ return -1;
+ }
+ }
+
+ /**
+ * Ensure that a network route exists to deliver traffic to the specified
+ * host via the mobile data network.
+ * @param hostAddress the IP address of the host to which the route is desired,
+ * in network byte order.
+ * @return {@code true} on success, {@code false} on failure
+ */
+ @Override
+ public boolean requestRouteToHost(int hostAddress) {
+ if (mInterfaceName != null && hostAddress != -1) {
+ if (DBG) {
+ Log.d(TAG, "Requested host route to " + Integer.toHexString(hostAddress));
+ }
+ return NetworkUtils.addHostRoute(mInterfaceName, hostAddress) == 0;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuffer sb = new StringBuffer("Mobile data state: ");
+
+ sb.append(mMobileDataState);
+ return sb.toString();
+ }
+
+ private void setupDnsProperties() {
+ mDnsServers.clear();
+ // Set up per-process DNS server list on behalf of the MMS process
+ int i = 1;
+ if (mInterfaceName != null) {
+ for (String propName : sDnsPropNames) {
+ if (propName.indexOf(mInterfaceName) != -1) {
+ String propVal = SystemProperties.get(propName);
+ if (propVal != null && propVal.length() != 0 && !propVal.equals("0.0.0.0")) {
+ mDnsServers.add(propVal);
+ if (mLastCallingPid != -1) {
+ SystemProperties.set("net.dns" + i + "." + mLastCallingPid, propVal);
+ }
+ ++i;
+ }
+ }
+ }
+ }
+ if (i == 1) {
+ Log.d(TAG, "DNS server addresses are not known.");
+ } else if (mLastCallingPid != -1) {
+ /*
+ * Bump the property that tells the name resolver library
+ * to reread the DNS server list from the properties.
+ */
+ String propVal = SystemProperties.get("net.dnschange");
+ if (propVal.length() != 0) {
+ try {
+ int n = Integer.parseInt(propVal);
+ SystemProperties.set("net.dnschange", "" + (n+1));
+ } catch (NumberFormatException e) {
+ }
+ }
+ }
+ mLastCallingPid = -1;
+ }
+
+ /**
+ * Internal method supporting the ENABLE_MMS feature.
+ * @param apnType the type of APN to be enabled or disabled (e.g., mms)
+ * @param enable {@code true} to enable the specified APN type,
+ * {@code false} to disable it.
+ * @return an integer value representing the outcome of the request.
+ */
+ private int setEnableApn(String apnType, boolean enable) {
+ getPhoneService(false);
+ /*
+ * If the phone process has crashed in the past, we'll get a
+ * RemoteException and need to re-reference the service.
+ */
+ for (int retry = 0; retry < 2; retry++) {
+ if (mPhoneService == null) {
+ Log.w(TAG,
+ "Ignoring feature request because could not acquire PhoneService");
+ break;
+ }
+
+ try {
+ if (enable) {
+ return mPhoneService.enableApnType(apnType);
+ } else {
+ return mPhoneService.disableApnType(apnType);
+ }
+ } catch (RemoteException e) {
+ if (retry == 0) getPhoneService(true);
+ }
+ }
+
+ Log.w(TAG, "Could not " + (enable ? "enable" : "disable")
+ + " APN type \"" + apnType + "\"");
+ return Phone.APN_REQUEST_FAILED;
+ }
+}
diff --git a/core/java/android/net/NetworkConnectivityListener.java b/core/java/android/net/NetworkConnectivityListener.java
new file mode 100644
index 0000000..858fc77
--- /dev/null
+++ b/core/java/android/net/NetworkConnectivityListener.java
@@ -0,0 +1,220 @@
+/*
+ * 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.net;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+
+import java.util.HashMap;
+import java.util.Iterator;
+
+/**
+ * A wrapper for a broadcast receiver that provides network connectivity
+ * state information, independent of network type (mobile, Wi-Fi, etc.).
+ * {@hide}
+ */
+public class NetworkConnectivityListener {
+ private static final String TAG = "NetworkConnectivityListener";
+ private static final boolean DBG = false;
+
+ private Context mContext;
+ private HashMap<Handler, Integer> mHandlers = new HashMap<Handler, Integer>();
+ private State mState;
+ private boolean mListening;
+ private String mReason;
+ private boolean mIsFailover;
+
+ /** Network connectivity information */
+ private NetworkInfo mNetworkInfo;
+
+ /**
+ * In case of a Disconnect, the connectivity manager may have
+ * already established, or may be attempting to establish, connectivity
+ * with another network. If so, {@code mOtherNetworkInfo} will be non-null.
+ */
+ private NetworkInfo mOtherNetworkInfo;
+
+ private ConnectivityBroadcastReceiver mReceiver;
+
+ private class ConnectivityBroadcastReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+
+ if (!action.equals(ConnectivityManager.CONNECTIVITY_ACTION) ||
+ mListening == false) {
+ Log.w(TAG, "onReceived() called with " + mState.toString() + " and " + intent);
+ return;
+ }
+
+ boolean noConnectivity =
+ intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false);
+
+ if (noConnectivity) {
+ mState = State.NOT_CONNECTED;
+ } else {
+ mState = State.CONNECTED;
+ }
+
+ mNetworkInfo = (NetworkInfo)
+ intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO);
+ mOtherNetworkInfo = (NetworkInfo)
+ intent.getParcelableExtra(ConnectivityManager.EXTRA_OTHER_NETWORK_INFO);
+
+ mReason = intent.getStringExtra(ConnectivityManager.EXTRA_REASON);
+ mIsFailover =
+ intent.getBooleanExtra(ConnectivityManager.EXTRA_IS_FAILOVER, false);
+
+ if (DBG) {
+ Log.d(TAG, "onReceive(): mNetworkInfo=" + mNetworkInfo + " mOtherNetworkInfo = "
+ + (mOtherNetworkInfo == null ? "[none]" : mOtherNetworkInfo +
+ " noConn=" + noConnectivity) + " mState=" + mState.toString());
+ }
+
+ // Notifiy any handlers.
+ Iterator<Handler> it = mHandlers.keySet().iterator();
+ while (it.hasNext()) {
+ Handler target = it.next();
+ Message message = Message.obtain(target, mHandlers.get(target));
+ target.sendMessage(message);
+ }
+ }
+ };
+
+ public enum State {
+ UNKNOWN,
+
+ /** This state is returned if there is connectivity to any network **/
+ CONNECTED,
+ /**
+ * This state is returned if there is no connectivity to any network. This is set
+ * to true under two circumstances:
+ * <ul>
+ * <li>When connectivity is lost to one network, and there is no other available
+ * network to attempt to switch to.</li>
+ * <li>When connectivity is lost to one network, and the attempt to switch to
+ * another network fails.</li>
+ */
+ NOT_CONNECTED
+ }
+
+ /**
+ * Create a new NetworkConnectivityListener.
+ */
+ public NetworkConnectivityListener() {
+ mState = State.UNKNOWN;
+ mReceiver = new ConnectivityBroadcastReceiver();
+ }
+
+ /**
+ * This method starts listening for network connectivity state changes.
+ * @param context
+ */
+ public synchronized void startListening(Context context) {
+ if (!mListening) {
+ mContext = context;
+
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
+ context.registerReceiver(mReceiver, filter);
+ mListening = true;
+ }
+ }
+
+ /**
+ * This method stops this class from listening for network changes.
+ */
+ public synchronized void stopListening() {
+ if (mListening) {
+ mContext.unregisterReceiver(mReceiver);
+ mContext = null;
+ mNetworkInfo = null;
+ mOtherNetworkInfo = null;
+ mIsFailover = false;
+ mReason = null;
+ mListening = false;
+ }
+ }
+
+ /**
+ * This methods registers a Handler to be called back onto with the specified what code when
+ * the network connectivity state changes.
+ *
+ * @param target The target handler.
+ * @param what The what code to be used when posting a message to the handler.
+ */
+ public void registerHandler(Handler target, int what) {
+ mHandlers.put(target, what);
+ }
+
+ /**
+ * This methods unregisters the specified Handler.
+ * @param target
+ */
+ public void unregisterHandler(Handler target) {
+ mHandlers.remove(target);
+ }
+
+ public State getState() {
+ return mState;
+ }
+
+ /**
+ * Return the NetworkInfo associated with the most recent connectivity event.
+ * @return {@code NetworkInfo} for the network that had the most recent connectivity event.
+ */
+ public NetworkInfo getNetworkInfo() {
+ return mNetworkInfo;
+ }
+
+ /**
+ * If the most recent connectivity event was a DISCONNECT, return
+ * any information supplied in the broadcast about an alternate
+ * network that might be available. If this returns a non-null
+ * value, then another broadcast should follow shortly indicating
+ * whether connection to the other network succeeded.
+ *
+ * @return NetworkInfo
+ */
+ public NetworkInfo getOtherNetworkInfo() {
+ return mOtherNetworkInfo;
+ }
+
+ /**
+ * Returns true if the most recent event was for an attempt to switch over to
+ * a new network following loss of connectivity on another network.
+ * @return {@code true} if this was a failover attempt, {@code false} otherwise.
+ */
+ public boolean isFailover() {
+ return mIsFailover;
+ }
+
+ /**
+ * An optional reason for the connectivity state change may have been supplied.
+ * This returns it.
+ * @return the reason for the state change, if available, or {@code null}
+ * otherwise.
+ */
+ public String getReason() {
+ return mReason;
+ }
+}
diff --git a/core/java/android/net/NetworkInfo.aidl b/core/java/android/net/NetworkInfo.aidl
new file mode 100644
index 0000000..f501873
--- /dev/null
+++ b/core/java/android/net/NetworkInfo.aidl
@@ -0,0 +1,19 @@
+/**
+ * Copyright (c) 2007, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+parcelable NetworkInfo;
diff --git a/core/java/android/net/NetworkInfo.java b/core/java/android/net/NetworkInfo.java
new file mode 100644
index 0000000..f776abf
--- /dev/null
+++ b/core/java/android/net/NetworkInfo.java
@@ -0,0 +1,305 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.os.Parcelable;
+import android.os.Parcel;
+
+import java.util.EnumMap;
+
+/**
+ * Describes the status of a network interface of a given type
+ * (currently either Mobile or Wifi).
+ */
+public class NetworkInfo implements Parcelable {
+
+ /**
+ * Coarse-grained network state. This is probably what most applications should
+ * use, rather than {@link android.net.NetworkInfo.DetailedState DetailedState}.
+ * The mapping between the two is as follows:
+ * <br/><br/>
+ * <table>
+ * <tr><td><b>Detailed state</b></td><td><b>Coarse-grained state</b></td></tr>
+ * <tr><td><code>IDLE</code></td><td><code>DISCONNECTED</code></td></tr>
+ * <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>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>
+ * <tr><td><code>FAILED</code></td><td><code>DISCONNECTED</code></td></tr>
+ * </table>
+ */
+ public enum State {
+ CONNECTING, CONNECTED, SUSPENDED, DISCONNECTING, DISCONNECTED, UNKNOWN
+ }
+
+ /**
+ * The fine-grained state of a network connection. This level of detail
+ * is probably of interest to few applications. Most should use
+ * {@link android.net.NetworkInfo.State State} instead.
+ */
+ public enum DetailedState {
+ /** Ready to start data connection setup. */
+ IDLE,
+ /** Searching for an available access point. */
+ SCANNING,
+ /** Currently setting up data connection. */
+ CONNECTING,
+ /** Network link established, performing authentication. */
+ AUTHENTICATING,
+ /** Awaiting response from DHCP server in order to assign IP address information. */
+ OBTAINING_IPADDR,
+ /** IP traffic should be available. */
+ CONNECTED,
+ /** IP traffic is suspended */
+ SUSPENDED,
+ /** Currently tearing down data connection. */
+ DISCONNECTING,
+ /** IP traffic not available. */
+ DISCONNECTED,
+ /** Attempt to connect failed. */
+ FAILED
+ }
+
+ /**
+ * This is the map described in the Javadoc comment above. The positions
+ * of the elements of the array must correspond to the ordinal values
+ * of <code>DetailedState</code>.
+ */
+ private static final EnumMap<DetailedState, State> stateMap =
+ new EnumMap<DetailedState, State>(DetailedState.class);
+
+ static {
+ stateMap.put(DetailedState.IDLE, State.DISCONNECTED);
+ stateMap.put(DetailedState.SCANNING, State.DISCONNECTED);
+ stateMap.put(DetailedState.CONNECTING, State.CONNECTING);
+ stateMap.put(DetailedState.AUTHENTICATING, State.CONNECTING);
+ stateMap.put(DetailedState.OBTAINING_IPADDR, State.CONNECTING);
+ stateMap.put(DetailedState.CONNECTED, State.CONNECTED);
+ stateMap.put(DetailedState.SUSPENDED, State.SUSPENDED);
+ stateMap.put(DetailedState.DISCONNECTING, State.DISCONNECTING);
+ stateMap.put(DetailedState.DISCONNECTED, State.DISCONNECTED);
+ stateMap.put(DetailedState.FAILED, State.DISCONNECTED);
+ }
+
+ private int mNetworkType;
+ private State mState;
+ private DetailedState mDetailedState;
+ private String mReason;
+ private String mExtraInfo;
+ private boolean mIsFailover;
+ /**
+ * Indicates whether network connectivity is possible:
+ */
+ private boolean mIsAvailable;
+
+ public NetworkInfo(int type) {
+ if (!ConnectivityManager.isNetworkTypeValid(type)) {
+ throw new IllegalArgumentException("Invalid network type: " + type);
+ }
+ this.mNetworkType = type;
+ setDetailedState(DetailedState.IDLE, null, null);
+ mState = State.UNKNOWN;
+ mIsAvailable = true;
+ }
+
+ /**
+ * Reports the type of network (currently mobile or Wi-Fi) to which the
+ * info in this object pertains.
+ * @return the network type
+ */
+ public int getType() {
+ return mNetworkType;
+ }
+
+ /**
+ * Indicates whether network connectivity exists or is in the process
+ * of being established. This is good for applications that need to
+ * do anything related to the network other than read or write data.
+ * For the latter, call {@link #isConnected()} instead, which guarantees
+ * that the network is fully usable.
+ * @return {@code true} if network connectivity exists or is in the process
+ * of being established, {@code false} otherwise.
+ */
+ public boolean isConnectedOrConnecting() {
+ return mState == State.CONNECTED || mState == State.CONNECTING;
+ }
+
+ /**
+ * Indicates whether network connectivity exists and it is possible to establish
+ * connections and pass data.
+ * @return {@code true} if network connectivity exists, {@code false} otherwise.
+ */
+ public boolean isConnected() {
+ return mState == State.CONNECTED;
+ }
+
+ /**
+ * Indicates whether network connectivity is possible. A network is unavailable
+ * when a persistent or semi-persistent condition prevents the possibility
+ * of connecting to that network. Examples include
+ * <ul>
+ * <li>The device is out of the coverage area for any network of this type.</li>
+ * <li>The device is on a network other than the home network (i.e., roaming), and
+ * data roaming has been disabled.</li>
+ * <li>The device's radio is turned off, e.g., because airplane mode is enabled.</li>
+ * </ul>
+ * @return {@code true} if the network is available, {@code false} otherwise
+ */
+ public boolean isAvailable() {
+ return mIsAvailable;
+ }
+
+ /**
+ * Sets if the network is available, ie, if the connectivity is possible.
+ * @param isAvailable the new availability value.
+ *
+ * {@hide}
+ */
+ public void setIsAvailable(boolean isAvailable) {
+ mIsAvailable = isAvailable;
+ }
+
+ /**
+ * Indicates whether the current attempt to connect to the network
+ * resulted from the ConnectivityManager trying to fail over to this
+ * network following a disconnect from another network.
+ * @return {@code true} if this is a failover attempt, {@code false}
+ * otherwise.
+ */
+ public boolean isFailover() {
+ return mIsFailover;
+ }
+
+ /** {@hide} */
+ public void setFailover(boolean isFailover) {
+ mIsFailover = isFailover;
+ }
+
+ /**
+ * Reports the current coarse-grained state of the network.
+ * @return the coarse-grained state
+ */
+ public State getState() {
+ return mState;
+ }
+
+ /**
+ * Reports the current fine-grained state of the network.
+ * @return the fine-grained state
+ */
+ public DetailedState getDetailedState() {
+ return mDetailedState;
+ }
+
+ /**
+ * Sets the fine-grained state of the network.
+ * @param detailedState the {@link DetailedState}.
+ * @param reason a {@code String} indicating the reason for the state change,
+ * if one was supplied. May be {@code null}.
+ * @param extraInfo an optional {@code String} providing addditional network state
+ * information passed up from the lower networking layers.
+ *
+ * {@hide}
+ */
+ void setDetailedState(DetailedState detailedState, String reason, String extraInfo) {
+ this.mDetailedState = detailedState;
+ this.mState = stateMap.get(detailedState);
+ this.mReason = reason;
+ this.mExtraInfo = extraInfo;
+ }
+
+ /**
+ * Report the reason an attempt to establish connectivity failed,
+ * if one is available.
+ * @return the reason for failure, or null if not available
+ */
+ public String getReason() {
+ return mReason;
+ }
+
+ /**
+ * Report the extra information about the network state, if any was
+ * provided by the lower networking layers.,
+ * if one is available.
+ * @return the extra information, or null if not available
+ */
+ public String getExtraInfo() {
+ return mExtraInfo;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder("NetworkInfo: ");
+ builder.append("type: ").append(getTypeName()).append(", state: ").append(mState).
+ append("/").append(mDetailedState).
+ append(", reason: ").append(mReason == null ? "(unspecified)" : mReason).
+ append(", extra: ").append(mExtraInfo == null ? "(none)" : mExtraInfo).
+ append(", failover: ").append(mIsFailover).
+ append(", isAvailable: ").append(mIsAvailable);
+ return builder.toString();
+ }
+
+ public String getTypeName() {
+ switch (mNetworkType) {
+ case ConnectivityManager.TYPE_WIFI:
+ return "WIFI";
+ case ConnectivityManager.TYPE_MOBILE:
+ return "MOBILE";
+ default:
+ return "<invalid>";
+ }
+ }
+
+ /** Implement the Parcelable interface {@hide} */
+ public int describeContents() {
+ return 0;
+ }
+
+ /** Implement the Parcelable interface {@hide} */
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mNetworkType);
+ dest.writeString(mState.name());
+ dest.writeString(mDetailedState.name());
+ dest.writeInt(mIsFailover ? 1 : 0);
+ dest.writeInt(mIsAvailable ? 1 : 0);
+ dest.writeString(mReason);
+ dest.writeString(mExtraInfo);
+ }
+
+ /** Implement the Parcelable interface {@hide} */
+ public static final Creator<NetworkInfo> CREATOR =
+ new Creator<NetworkInfo>() {
+ public NetworkInfo createFromParcel(Parcel in) {
+ int netType = in.readInt();
+ NetworkInfo netInfo = new NetworkInfo(netType);
+ netInfo.mState = State.valueOf(in.readString());
+ netInfo.mDetailedState = DetailedState.valueOf(in.readString());
+ netInfo.mIsFailover = in.readInt() != 0;
+ netInfo.mIsAvailable = in.readInt() != 0;
+ netInfo.mReason = in.readString();
+ netInfo.mExtraInfo = in.readString();
+ return netInfo;
+ }
+
+ public NetworkInfo[] newArray(int size) {
+ return new NetworkInfo[size];
+ }
+ };
+}
diff --git a/core/java/android/net/NetworkStateTracker.java b/core/java/android/net/NetworkStateTracker.java
new file mode 100644
index 0000000..4e1efa6
--- /dev/null
+++ b/core/java/android/net/NetworkStateTracker.java
@@ -0,0 +1,306 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import java.io.FileWriter;
+import java.io.IOException;
+
+import android.os.Handler;
+import android.os.Message;
+import android.os.SystemProperties;
+import android.os.PowerManager;
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.Config;
+import android.util.Log;
+
+/**
+ * Each subclass of this class keeps track of the state of connectivity
+ * of a network interface. All state information for a network should
+ * be kept in a Tracker class. This superclass manages the
+ * network-type-independent aspects of network state.
+ *
+ * {@hide}
+ */
+public abstract class NetworkStateTracker extends Handler {
+
+ protected NetworkInfo mNetworkInfo;
+ protected Context mContext;
+ protected Handler mTarget;
+
+ private static boolean DBG = Config.LOGV;
+ private static final String TAG = "NetworkStateTracker";
+
+ public static final int EVENT_STATE_CHANGED = 1;
+ public static final int EVENT_SCAN_RESULTS_AVAILABLE = 2;
+ /**
+ * arg1: 1 to show, 0 to hide
+ * arg2: ID of the notification
+ * obj: Notification (if showing)
+ */
+ public static final int EVENT_NOTIFICATION_CHANGED = 3;
+ public static final int EVENT_CONFIGURATION_CHANGED = 4;
+
+ public NetworkStateTracker(Context context, Handler target, int networkType) {
+ super();
+ mContext = context;
+ mTarget = target;
+ this.mNetworkInfo = new NetworkInfo(networkType);
+ }
+
+ public NetworkInfo getNetworkInfo() {
+ return mNetworkInfo;
+ }
+
+ /**
+ * Return the list of DNS servers associated with this network.
+ * @return a list of the IP addresses of the DNS servers available
+ * for the network.
+ */
+ public abstract String[] getNameServers();
+
+ /**
+ * Return the system properties name associated with the tcp buffer sizes
+ * for this network.
+ */
+ public abstract String getTcpBufferSizesPropName();
+
+ /**
+ * Return the IP addresses of the DNS servers available for this
+ * network interface.
+ * @param propertyNames the names of the system properties whose values
+ * give the IP addresses. Properties with no values are skipped.
+ * @return an array of {@code String}s containing the IP addresses
+ * of the DNS servers, in dot-notation. This may have fewer
+ * non-null entries than the list of names passed in, since
+ * some of the passed-in names may have empty values.
+ */
+ static protected String[] getNameServerList(String[] propertyNames) {
+ String[] dnsAddresses = new String[propertyNames.length];
+ int i, j;
+
+ for (i = 0, j = 0; i < propertyNames.length; i++) {
+ String value = SystemProperties.get(propertyNames[i]);
+ // The GSM layer sometimes sets a bogus DNS server address of
+ // 0.0.0.0
+ if (!TextUtils.isEmpty(value) && !TextUtils.equals(value, "0.0.0.0")) {
+ dnsAddresses[j++] = value;
+ }
+ }
+ return dnsAddresses;
+ }
+
+ /**
+ * Reads the network specific TCP buffer sizes from SystemProperties
+ * net.tcp.buffersize.[default|wifi|umts|edge|gprs] and set them for system
+ * wide use
+ */
+ public void updateNetworkSettings() {
+ String key = getTcpBufferSizesPropName();
+ String bufferSizes = SystemProperties.get(key);
+
+ if (bufferSizes.length() == 0) {
+ Log.e(TAG, key + " not found in system properties. Using defaults");
+
+ // Setting to default values so we won't be stuck to previous values
+ key = "net.tcp.buffersize.default";
+ bufferSizes = SystemProperties.get(key);
+ }
+
+ // Set values in kernel
+ if (bufferSizes.length() != 0) {
+ if (DBG) {
+ Log.v(TAG, "Setting TCP values: [" + bufferSizes
+ + "] which comes from [" + key + "]");
+ }
+ setBufferSize(bufferSizes);
+ }
+ }
+
+ /**
+ * Release the wakelock, if any, that may be held while handling a
+ * disconnect operation.
+ */
+ public void releaseWakeLock() {
+ }
+
+ /**
+ * Writes TCP buffer sizes to /sys/kernel/ipv4/tcp_[r/w]mem_[min/def/max]
+ * which maps to /proc/sys/net/ipv4/tcp_rmem and tcpwmem
+ *
+ * @param bufferSizes in the format of "readMin, readInitial, readMax,
+ * writeMin, writeInitial, writeMax"
+ */
+ private void setBufferSize(String bufferSizes) {
+ try {
+ String[] values = bufferSizes.split(",");
+
+ if (values.length == 6) {
+ final String prefix = "/sys/kernel/ipv4/tcp_";
+ stringToFile(prefix + "rmem_min", values[0]);
+ stringToFile(prefix + "rmem_def", values[1]);
+ stringToFile(prefix + "rmem_max", values[2]);
+ stringToFile(prefix + "wmem_min", values[3]);
+ stringToFile(prefix + "wmem_def", values[4]);
+ stringToFile(prefix + "wmem_max", values[5]);
+ } else {
+ Log.e(TAG, "Invalid buffersize string: " + bufferSizes);
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Can't set tcp buffer sizes:" + e);
+ }
+ }
+
+ /**
+ * Writes string to file. Basically same as "echo -n $string > $filename"
+ *
+ * @param filename
+ * @param string
+ * @throws IOException
+ */
+ private void stringToFile(String filename, String string) throws IOException {
+ FileWriter out = new FileWriter(filename);
+ try {
+ out.write(string);
+ } finally {
+ out.close();
+ }
+ }
+
+ /**
+ * Record the detailed state of a network, and if it is a
+ * change from the previous state, send a notification to
+ * any listeners.
+ * @param state the new @{code DetailedState}
+ */
+ public void setDetailedState(NetworkInfo.DetailedState state) {
+ setDetailedState(state, null, null);
+ }
+
+ /**
+ * Record the detailed state of a network, and if it is a
+ * change from the previous state, send a notification to
+ * any listeners.
+ * @param state the new @{code DetailedState}
+ * @param reason a {@code String} indicating a reason for the state change,
+ * if one was supplied. May be {@code null}.
+ * @param extraInfo optional {@code String} providing extra information about the state change
+ */
+ public void setDetailedState(NetworkInfo.DetailedState state, String reason, String extraInfo) {
+ if (state != mNetworkInfo.getDetailedState()) {
+ boolean wasConnecting = (mNetworkInfo.getState() == NetworkInfo.State.CONNECTING);
+ String lastReason = mNetworkInfo.getReason();
+ /*
+ * If a reason was supplied when the CONNECTING state was entered, and no
+ * reason was supplied for entering the CONNECTED state, then retain the
+ * reason that was supplied when going to CONNECTING.
+ */
+ if (wasConnecting && state == NetworkInfo.DetailedState.CONNECTED && reason == null
+ && lastReason != null)
+ reason = lastReason;
+ mNetworkInfo.setDetailedState(state, reason, extraInfo);
+ Message msg = mTarget.obtainMessage(EVENT_STATE_CHANGED, mNetworkInfo);
+ msg.sendToTarget();
+ }
+ }
+
+ protected void setDetailedStateInternal(NetworkInfo.DetailedState state) {
+ mNetworkInfo.setDetailedState(state, null, null);
+ }
+
+ /**
+ * Send a notification that the results of a scan for network access
+ * points has completed, and results are available.
+ */
+ protected void sendScanResultsAvailable() {
+ Message msg = mTarget.obtainMessage(EVENT_SCAN_RESULTS_AVAILABLE, mNetworkInfo);
+ msg.sendToTarget();
+ }
+
+ public abstract void startMonitoring();
+
+ /**
+ * Disable connectivity to a network
+ * @return {@code true} if a teardown occurred, {@code false} if the
+ * teardown did not occur.
+ */
+ public abstract boolean teardown();
+
+ /**
+ * Reenable connectivity to a network after a {@link #teardown()}.
+ */
+ public abstract boolean reconnect();
+
+ /**
+ * Turn the wireless radio off for a network.
+ * @param turnOn {@code true} to turn the radio on, {@code false}
+ */
+ public abstract boolean setRadio(boolean turnOn);
+
+ /**
+ * Returns an indication of whether this network is available for
+ * connections. A value of {@code false} means that some quasi-permanent
+ * condition prevents connectivity to this network.
+ */
+ public abstract boolean isAvailable();
+
+ /**
+ * Tells the underlying networking system that the caller wants to
+ * begin using the named feature. The interpretation of {@code feature}
+ * is completely up to each networking implementation.
+ * @param feature the name of the feature to be used
+ * @param callingPid the process ID of the process that is issuing this request
+ * @param callingUid the user ID of the process that is issuing this request
+ * @return an integer value representing the outcome of the request.
+ * The interpretation of this value is specific to each networking
+ * implementation+feature combination, except that the value {@code -1}
+ * always indicates failure.
+ */
+ public abstract int startUsingNetworkFeature(String feature, int callingPid, int callingUid);
+
+ /**
+ * Tells the underlying networking system that the caller is finished
+ * using the named feature. The interpretation of {@code feature}
+ * is completely up to each networking implementation.
+ * @param feature the name of the feature that is no longer needed.
+ * @param callingPid the process ID of the process that is issuing this request
+ * @param callingUid the user ID of the process that is issuing this request
+ * @return an integer value representing the outcome of the request.
+ * The interpretation of this value is specific to each networking
+ * implementation+feature combination, except that the value {@code -1}
+ * always indicates failure.
+ */
+ public abstract int stopUsingNetworkFeature(String feature, int callingPid, int callingUid);
+
+ /**
+ * Ensure that a network route exists to deliver traffic to the specified
+ * host via this network interface.
+ * @param hostAddress the IP address of the host to which the route is desired
+ * @return {@code true} on success, {@code false} on failure
+ */
+ public boolean requestRouteToHost(int hostAddress) {
+ return false;
+ }
+
+ /**
+ * Interprets scan results. This will be called at a safe time for
+ * processing, and from a safe thread.
+ */
+ public void interpretScanResultsAvailable() {
+ }
+
+}
diff --git a/core/java/android/net/NetworkUtils.java b/core/java/android/net/NetworkUtils.java
new file mode 100644
index 0000000..129248a
--- /dev/null
+++ b/core/java/android/net/NetworkUtils.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+/**
+ * Native methods for managing network interfaces.
+ *
+ * {@hide}
+ */
+public class NetworkUtils {
+ /** Bring the named network interface down. */
+ public native static int disableInterface(String interfaceName);
+
+ /** Add a route to the specified host via the named interface. */
+ public native static int addHostRoute(String interfaceName, int hostaddr);
+
+ /** Add a default route for the named interface. */
+ public native static int setDefaultRoute(String interfaceName, int gwayAddr);
+
+ /** Return the gateway address for the default route for the named interface. */
+ public native static int getDefaultRoute(String interfaceName);
+
+ /** Remove host routes that uses the named interface. */
+ public native static int removeHostRoutes(String interfaceName);
+
+ /** Remove the default route for the named interface. */
+ public native static int removeDefaultRoute(String interfaceName);
+
+ /** Reset any sockets that are connected via the named interface. */
+ public native static int resetConnections(String interfaceName);
+
+ /**
+ * Start the DHCP client daemon, in order to have it request addresses
+ * for the named interface, and then configure the interface with those
+ * addresses. This call blocks until it obtains a result (either success
+ * or failure) from the daemon.
+ * @param interfaceName the name of the interface to configure
+ * @param ipInfo if the request succeeds, this object is filled in with
+ * the IP address information.
+ * @return {@code true} for success, {@code false} for failure
+ */
+ public native static boolean runDhcp(String interfaceName, DhcpInfo ipInfo);
+
+ /**
+ * Shut down the DHCP client daemon.
+ * @param interfaceName the name of the interface for which the daemon
+ * should be stopped
+ * @return {@code true} for success, {@code false} for failure
+ */
+ public native static boolean stopDhcp(String interfaceName);
+
+ /**
+ * Return the last DHCP-related error message that was recorded.
+ * <p/>NOTE: This string is not localized, but currently it is only
+ * used in logging.
+ * @return the most recent error message, if any
+ */
+ public native static String getDhcpError();
+
+ /**
+ * When static IP configuration has been specified, configure the network
+ * interface according to the values supplied.
+ * @param interfaceName the name of the interface to configure
+ * @param ipInfo the IP address, default gateway, and DNS server addresses
+ * with which to configure the interface.
+ * @return {@code true} for success, {@code false} for failure
+ */
+ public static boolean configureInterface(String interfaceName, DhcpInfo ipInfo) {
+ return configureNative(interfaceName,
+ ipInfo.ipAddress,
+ ipInfo.netmask,
+ ipInfo.gateway,
+ ipInfo.dns1,
+ ipInfo.dns2);
+ }
+
+ private native static boolean configureNative(
+ String interfaceName, int ipAddress, int netmask, int gateway, int dns1, int dns2);
+
+ /**
+ * Look up a host name and return the result as an int. Works if the argument
+ * is an IP address in dot notation. Obviously, this can only be used for IPv4
+ * addresses.
+ * @param hostname the name of the host (or the IP address)
+ * @return the IP address as an {@code int} in network byte order
+ */
+ public static int lookupHost(String hostname) {
+ InetAddress inetAddress;
+ try {
+ inetAddress = InetAddress.getByName(hostname);
+ } catch (UnknownHostException e) {
+ return -1;
+ }
+ byte[] addrBytes;
+ int addr;
+ addrBytes = inetAddress.getAddress();
+ addr = ((addrBytes[3] & 0xff) << 24)
+ | ((addrBytes[2] & 0xff) << 16)
+ | ((addrBytes[1] & 0xff) << 8)
+ | (addrBytes[0] & 0xff);
+ return addr;
+ }
+}
diff --git a/core/java/android/net/ParseException.java b/core/java/android/net/ParseException.java
new file mode 100644
index 0000000..000fa68
--- /dev/null
+++ b/core/java/android/net/ParseException.java
@@ -0,0 +1,30 @@
+/*
+ * 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.net;
+
+/**
+ *
+ *
+ * When WebAddress Parser Fails, this exception is thrown
+ */
+public class ParseException extends RuntimeException {
+ public String response;
+
+ ParseException(String response) {
+ this.response = response;
+ }
+}
diff --git a/core/java/android/net/Proxy.java b/core/java/android/net/Proxy.java
new file mode 100644
index 0000000..86e1d5b
--- /dev/null
+++ b/core/java/android/net/Proxy.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.os.SystemProperties;
+import android.provider.Settings;
+import android.util.Log;
+
+import junit.framework.Assert;
+
+/**
+ * A convenience class for accessing the user and default proxy
+ * settings.
+ */
+final public class Proxy {
+
+ static final public String PROXY_CHANGE_ACTION =
+ "android.intent.action.PROXY_CHANGE";
+
+ /**
+ * Return the proxy host set by the user.
+ * @param ctx A Context used to get the settings for the proxy host.
+ * @return String containing the host name. If the user did not set a host
+ * name it returns the default host. A null value means that no
+ * host is to be used.
+ */
+ static final public String getHost(Context ctx) {
+ ContentResolver contentResolver = ctx.getContentResolver();
+ Assert.assertNotNull(contentResolver);
+ String host = Settings.System.getString(
+ contentResolver,
+ Settings.System.HTTP_PROXY);
+ if (host != null) {
+ int i = host.indexOf(':');
+ if (i == -1) {
+ if (android.util.Config.DEBUG) {
+ Assert.assertTrue(host.length() == 0);
+ }
+ return null;
+ }
+ return host.substring(0, i);
+ }
+ return getDefaultHost();
+ }
+
+ /**
+ * Return the proxy port set by the user.
+ * @param ctx A Context used to get the settings for the proxy port.
+ * @return The port number to use or -1 if no proxy is to be used.
+ */
+ static final public int getPort(Context ctx) {
+ ContentResolver contentResolver = ctx.getContentResolver();
+ Assert.assertNotNull(contentResolver);
+ String host = Settings.System.getString(
+ contentResolver,
+ Settings.System.HTTP_PROXY);
+ if (host != null) {
+ int i = host.indexOf(':');
+ if (i == -1) {
+ if (android.util.Config.DEBUG) {
+ Assert.assertTrue(host.length() == 0);
+ }
+ return -1;
+ }
+ if (android.util.Config.DEBUG) {
+ Assert.assertTrue(i < host.length());
+ }
+ return Integer.parseInt(host.substring(i+1));
+ }
+ return getDefaultPort();
+ }
+
+ /**
+ * Return the default proxy host specified by the carrier.
+ * @return String containing the host name or null if there is no proxy for
+ * this carrier.
+ */
+ static final public String getDefaultHost() {
+ String host = SystemProperties.get("net.gprs.http-proxy");
+ if (host != null) {
+ Uri u = Uri.parse(host);
+ host = u.getHost();
+ return host;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Return the default proxy port specified by the carrier.
+ * @return The port number to be used with the proxy host or -1 if there is
+ * no proxy for this carrier.
+ */
+ static final public int getDefaultPort() {
+ String host = SystemProperties.get("net.gprs.http-proxy");
+ if (host != null) {
+ Uri u = Uri.parse(host);
+ return u.getPort();
+ } else {
+ return -1;
+ }
+ }
+
+};
diff --git a/core/java/android/net/SSLCertificateSocketFactory.java b/core/java/android/net/SSLCertificateSocketFactory.java
new file mode 100644
index 0000000..f816caa
--- /dev/null
+++ b/core/java/android/net/SSLCertificateSocketFactory.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.util.Log;
+import android.util.Config;
+import android.net.http.DomainNameChecker;
+import android.os.SystemProperties;
+
+import javax.net.SocketFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509TrustManager;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.security.NoSuchAlgorithmException;
+import java.security.KeyManagementException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.GeneralSecurityException;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+
+public class SSLCertificateSocketFactory extends SSLSocketFactory {
+
+ private static final boolean DBG = true;
+ private static final String LOG_TAG = "SSLCertificateSocketFactory";
+
+ private static X509TrustManager sDefaultTrustManager;
+
+ private final int socketReadTimeoutForSslHandshake;
+
+ static {
+ try {
+ TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
+ tmf.init((KeyStore)null);
+ TrustManager[] tms = tmf.getTrustManagers();
+ if (tms != null) {
+ for (TrustManager tm : tms) {
+ if (tm instanceof X509TrustManager) {
+ sDefaultTrustManager = (X509TrustManager)tm;
+ break;
+ }
+ }
+ }
+ } catch (NoSuchAlgorithmException e) {
+ Log.e(LOG_TAG, "Unable to get X509 Trust Manager ", e);
+ } catch (KeyStoreException e) {
+ Log.e(LOG_TAG, "Key Store exception while initializing TrustManagerFactory ", e);
+ }
+ }
+
+ private static final TrustManager[] TRUST_MANAGER = new TrustManager[] {
+ new X509TrustManager() {
+ public X509Certificate[] getAcceptedIssuers() {
+ return null;
+ }
+
+ public void checkClientTrusted(X509Certificate[] certs,
+ String authType) { }
+
+ public void checkServerTrusted(X509Certificate[] certs,
+ String authType) { }
+ }
+ };
+
+ private SSLSocketFactory factory;
+
+ public SSLCertificateSocketFactory(int socketReadTimeoutForSslHandshake)
+ throws NoSuchAlgorithmException, KeyManagementException {
+ SSLContext context = SSLContext.getInstance("TLS");
+ context.init(null, TRUST_MANAGER, new java.security.SecureRandom());
+ factory = (SSLSocketFactory) context.getSocketFactory();
+ this.socketReadTimeoutForSslHandshake = socketReadTimeoutForSslHandshake;
+ }
+
+ /**
+ * Returns a default instantiation of a new socket factory which
+ * only allows SSL connections with valid certificates.
+ *
+ * @param socketReadTimeoutForSslHandshake the socket read timeout used for performing
+ * ssl handshake. The socket read timeout is set back to 0 after the handshake.
+ * @return a new SocketFactory, or null on error
+ */
+ public static SocketFactory getDefault(int socketReadTimeoutForSslHandshake) {
+ try {
+ return new SSLCertificateSocketFactory(socketReadTimeoutForSslHandshake);
+ } catch (NoSuchAlgorithmException e) {
+ Log.e(LOG_TAG,
+ "SSLCertifcateSocketFactory.getDefault" +
+ " NoSuchAlgorithmException " , e);
+ return null;
+ } catch (KeyManagementException e) {
+ Log.e(LOG_TAG,
+ "SSLCertifcateSocketFactory.getDefault" +
+ " KeyManagementException " , e);
+ return null;
+ }
+ }
+
+ private boolean hasValidCertificateChain(Certificate[] certs)
+ throws IOException {
+ if (sDefaultTrustManager == null) {
+ if (Config.LOGD) {
+ Log.d(LOG_TAG,"hasValidCertificateChain():" +
+ " null default trust manager!");
+ }
+ throw new IOException("null default trust manager");
+ }
+
+ boolean trusted = (certs != null && (certs.length > 0));
+
+ if (trusted) {
+ try {
+ // the authtype we pass in doesn't actually matter
+ sDefaultTrustManager.checkServerTrusted((X509Certificate[]) certs, "RSA");
+ } catch (GeneralSecurityException e) {
+ String exceptionMessage = e != null ? e.getMessage() : "none";
+ if (Config.LOGD) {
+ Log.d(LOG_TAG,"hasValidCertificateChain(): sec. exception: "
+ + exceptionMessage);
+ }
+ trusted = false;
+ }
+ }
+
+ return trusted;
+ }
+
+ private void validateSocket(SSLSocket sslSock, String destHost)
+ throws IOException
+ {
+ if (Config.LOGV) {
+ Log.v(LOG_TAG,"validateSocket() to host "+destHost);
+ }
+
+ String relaxSslCheck = SystemProperties.get("socket.relaxsslcheck");
+ String secure = SystemProperties.get("ro.secure");
+
+ // only allow relaxing the ssl check on non-secure builds where the relaxation is
+ // specifically requested.
+ if ("0".equals(secure) && "yes".equals(relaxSslCheck)) {
+ if (Config.LOGD) {
+ Log.d(LOG_TAG,"sys prop socket.relaxsslcheck is set," +
+ " ignoring invalid certs");
+ }
+ return;
+ }
+
+ Certificate[] certs = null;
+ sslSock.setUseClientMode(true);
+ sslSock.startHandshake();
+ certs = sslSock.getSession().getPeerCertificates();
+
+ // check that the root certificate in the chain belongs to
+ // a CA we trust
+ if (certs == null) {
+ Log.e(LOG_TAG,
+ "[SSLCertificateSocketFactory] no trusted root CA");
+ throw new IOException("no trusted root CA");
+ }
+
+ if (Config.LOGV) {
+ Log.v(LOG_TAG,"validateSocket # certs = " +certs.length);
+ }
+
+ if (!hasValidCertificateChain(certs)) {
+ if (Config.LOGD) {
+ Log.d(LOG_TAG,"validateSocket(): certificate untrusted!");
+ }
+ throw new IOException("Certificate untrusted");
+ }
+
+ X509Certificate lastChainCert = (X509Certificate) certs[0];
+
+ if (!DomainNameChecker.match(lastChainCert, destHost)) {
+ if (Config.LOGD) {
+ Log.d(LOG_TAG,"validateSocket(): domain name check failed");
+ }
+ throw new IOException("Domain Name check failed");
+ }
+ }
+
+ public Socket createSocket(Socket socket, String s, int i, boolean flag)
+ throws IOException
+ {
+ throw new IOException("Cannot validate certification without a hostname");
+ }
+
+ public Socket createSocket(InetAddress inaddr, int i, InetAddress inaddr2, int j)
+ throws IOException
+ {
+ throw new IOException("Cannot validate certification without a hostname");
+ }
+
+ public Socket createSocket(InetAddress inaddr, int i) throws IOException {
+ throw new IOException("Cannot validate certification without a hostname");
+ }
+
+ public Socket createSocket(String s, int i, InetAddress inaddr, int j) throws IOException {
+ SSLSocket sslSock = (SSLSocket) factory.createSocket(s, i, inaddr, j);
+
+ if (socketReadTimeoutForSslHandshake >= 0) {
+ sslSock.setSoTimeout(socketReadTimeoutForSslHandshake);
+ }
+
+ validateSocket(sslSock,s);
+ sslSock.setSoTimeout(0);
+
+ return sslSock;
+ }
+
+ public Socket createSocket(String s, int i) throws IOException {
+ SSLSocket sslSock = (SSLSocket) factory.createSocket(s, i);
+
+ if (socketReadTimeoutForSslHandshake >= 0) {
+ sslSock.setSoTimeout(socketReadTimeoutForSslHandshake);
+ }
+
+ validateSocket(sslSock,s);
+ sslSock.setSoTimeout(0);
+
+ return sslSock;
+ }
+
+ public String[] getDefaultCipherSuites() {
+ return factory.getSupportedCipherSuites();
+ }
+
+ public String[] getSupportedCipherSuites() {
+ return factory.getSupportedCipherSuites();
+ }
+}
+
+
diff --git a/core/java/android/net/SntpClient.java b/core/java/android/net/SntpClient.java
new file mode 100644
index 0000000..28134b2
--- /dev/null
+++ b/core/java/android/net/SntpClient.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.os.SystemClock;
+import android.util.Config;
+import android.util.Log;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+
+/**
+ * {@hide}
+ *
+ * Simple SNTP client class for retrieving network time.
+ *
+ * Sample usage:
+ * <pre>SntpClient client = new SntpClient();
+ * if (client.requestTime("time.foo.com")) {
+ * long now = client.getNtpTime() + SystemClock.elapsedRealtime() - client.getNtpTimeReference();
+ * }
+ * </pre>
+ */
+public class SntpClient
+{
+ private static final String TAG = "SntpClient";
+
+ private static final int REFERENCE_TIME_OFFSET = 16;
+ private static final int ORIGINATE_TIME_OFFSET = 24;
+ private static final int RECEIVE_TIME_OFFSET = 32;
+ private static final int TRANSMIT_TIME_OFFSET = 40;
+ private static final int NTP_PACKET_SIZE = 48;
+
+ private static final int NTP_PORT = 123;
+ private static final int NTP_MODE_CLIENT = 3;
+ private static final int NTP_VERSION = 3;
+
+ // Number of seconds between Jan 1, 1900 and Jan 1, 1970
+ // 70 years plus 17 leap days
+ private static final long OFFSET_1900_TO_1970 = ((365L * 70L) + 17L) * 24L * 60L * 60L;
+
+ // system time computed from NTP server response
+ private long mNtpTime;
+
+ // value of SystemClock.elapsedRealtime() corresponding to mNtpTime
+ private long mNtpTimeReference;
+
+ // round trip time in milliseconds
+ private long mRoundTripTime;
+
+ /**
+ * Sends an SNTP request to the given host and processes the response.
+ *
+ * @param host host name of the server.
+ * @param timeout network timeout in milliseconds.
+ * @return true if the transaction was successful.
+ */
+ public boolean requestTime(String host, int timeout) {
+ try {
+ DatagramSocket socket = new DatagramSocket();
+ socket.setSoTimeout(timeout);
+ InetAddress address = InetAddress.getByName(host);
+ byte[] buffer = new byte[NTP_PACKET_SIZE];
+ DatagramPacket request = new DatagramPacket(buffer, buffer.length, address, NTP_PORT);
+
+ // set mode = 3 (client) and version = 3
+ // mode is in low 3 bits of first byte
+ // version is in bits 3-5 of first byte
+ buffer[0] = NTP_MODE_CLIENT | (NTP_VERSION << 3);
+
+ // get current time and write it to the request packet
+ long requestTime = System.currentTimeMillis();
+ long requestTicks = SystemClock.elapsedRealtime();
+ writeTimeStamp(buffer, TRANSMIT_TIME_OFFSET, requestTime);
+
+ socket.send(request);
+
+ // read the response
+ DatagramPacket response = new DatagramPacket(buffer, buffer.length);
+ socket.receive(response);
+ long responseTicks = SystemClock.elapsedRealtime();
+ long responseTime = requestTime + (responseTicks - requestTicks);
+ socket.close();
+
+ // extract the results
+ long originateTime = readTimeStamp(buffer, ORIGINATE_TIME_OFFSET);
+ long receiveTime = readTimeStamp(buffer, RECEIVE_TIME_OFFSET);
+ long transmitTime = readTimeStamp(buffer, TRANSMIT_TIME_OFFSET);
+ long roundTripTime = responseTicks - requestTicks - (transmitTime - receiveTime);
+ long clockOffset = (receiveTime - originateTime) + (transmitTime - responseTime);
+ if (Config.LOGD) Log.d(TAG, "round trip: " + roundTripTime + " ms");
+ if (Config.LOGD) Log.d(TAG, "clock offset: " + clockOffset + " ms");
+
+ // save our results
+ mNtpTime = requestTime + clockOffset;
+ mNtpTimeReference = requestTicks;
+ mRoundTripTime = roundTripTime;
+ } catch (Exception e) {
+ if (Config.LOGD) Log.d(TAG, "request time failed: " + e);
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns the time computed from the NTP transaction.
+ *
+ * @return time value computed from NTP server response.
+ */
+ public long getNtpTime() {
+ return mNtpTime;
+ }
+
+ /**
+ * Returns the reference clock value (value of SystemClock.elapsedRealtime())
+ * corresponding to the NTP time.
+ *
+ * @return reference clock corresponding to the NTP time.
+ */
+ public long getNtpTimeReference() {
+ return mNtpTimeReference;
+ }
+
+ /**
+ * Returns the round trip time of the NTP transaction
+ *
+ * @return round trip time in milliseconds.
+ */
+ public long getRoundTripTime() {
+ return mRoundTripTime;
+ }
+
+ /**
+ * Reads an unsigned 32 bit big endian number from the given offset in the buffer.
+ */
+ private long read32(byte[] buffer, int offset) {
+ byte b0 = buffer[offset];
+ byte b1 = buffer[offset+1];
+ byte b2 = buffer[offset+2];
+ byte b3 = buffer[offset+3];
+
+ // convert signed bytes to unsigned values
+ int i0 = ((b0 & 0x80) == 0x80 ? (b0 & 0x7F) + 0x80 : b0);
+ int i1 = ((b1 & 0x80) == 0x80 ? (b1 & 0x7F) + 0x80 : b1);
+ int i2 = ((b2 & 0x80) == 0x80 ? (b2 & 0x7F) + 0x80 : b2);
+ int i3 = ((b3 & 0x80) == 0x80 ? (b3 & 0x7F) + 0x80 : b3);
+
+ return ((long)i0 << 24) + ((long)i1 << 16) + ((long)i2 << 8) + (long)i3;
+ }
+
+ /**
+ * Reads the NTP time stamp at the given offset in the buffer and returns
+ * it as a system time (milliseconds since January 1, 1970).
+ */
+ private long readTimeStamp(byte[] buffer, int offset) {
+ long seconds = read32(buffer, offset);
+ long fraction = read32(buffer, offset + 4);
+ return ((seconds - OFFSET_1900_TO_1970) * 1000) + ((fraction * 1000L) / 0x100000000L);
+ }
+
+ /**
+ * Writes system time (milliseconds since January 1, 1970) as an NTP time stamp
+ * at the given offset in the buffer.
+ */
+ private void writeTimeStamp(byte[] buffer, int offset, long time) {
+ long seconds = time / 1000L;
+ long milliseconds = time - seconds * 1000L;
+ seconds += OFFSET_1900_TO_1970;
+
+ // write seconds in big endian format
+ buffer[offset++] = (byte)(seconds >> 24);
+ buffer[offset++] = (byte)(seconds >> 16);
+ buffer[offset++] = (byte)(seconds >> 8);
+ buffer[offset++] = (byte)(seconds >> 0);
+
+ long fraction = milliseconds * 0x100000000L / 1000L;
+ // write fraction in big endian format
+ buffer[offset++] = (byte)(fraction >> 24);
+ buffer[offset++] = (byte)(fraction >> 16);
+ buffer[offset++] = (byte)(fraction >> 8);
+ // low order bits should be random data
+ buffer[offset++] = (byte)(Math.random() * 255.0);
+ }
+}
diff --git a/core/java/android/net/Uri.aidl b/core/java/android/net/Uri.aidl
new file mode 100755
index 0000000..6bd3be5
--- /dev/null
+++ b/core/java/android/net/Uri.aidl
@@ -0,0 +1,19 @@
+/**
+ * Copyright (c) 2007, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+parcelable Uri;
diff --git a/core/java/android/net/Uri.java b/core/java/android/net/Uri.java
new file mode 100644
index 0000000..32a26e4
--- /dev/null
+++ b/core/java/android/net/Uri.java
@@ -0,0 +1,2251 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import 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.util.AbstractList;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.RandomAccess;
+
+/**
+ * Immutable URI reference. A URI reference includes a URI and a fragment, the
+ * component of the URI following a '#'. Builds and parses URI references
+ * which conform to
+ * <a href="http://www.faqs.org/rfcs/rfc2396.html">RFC 2396</a>.
+ *
+ * <p>In the interest of performance, this class performs little to no
+ * validation. Behavior is undefined for invalid input. This class is very
+ * forgiving--in the face of invalid input, it will return garbage
+ * rather than throw an exception unless otherwise specified.
+ */
+public abstract class Uri implements Parcelable, Comparable<Uri> {
+
+ /*
+
+ This class aims to do as little up front work as possible. To accomplish
+ that, we vary the implementation dependending on what the user passes in.
+ For example, we have one implementation if the user passes in a
+ URI string (StringUri) and another if the user passes in the
+ individual components (OpaqueUri).
+
+ *Concurrency notes*: Like any truly immutable object, this class is safe
+ for concurrent use. This class uses a caching pattern in some places where
+ it doesn't use volatile or synchronized. This is safe to do with ints
+ because getting or setting an int is atomic. It's safe to do with a String
+ because the internal fields are final and the memory model guarantees other
+ threads won't see a partially initialized instance. We are not guaranteed
+ that some threads will immediately see changes from other threads on
+ certain platforms, but we don't mind if those threads reconstruct the
+ cached result. As a result, we get thread safe caching with no concurrency
+ overhead, which means the most common case, access from a single thread,
+ is as fast as possible.
+
+ From the Java Language spec.:
+
+ "17.5 Final Field Semantics
+
+ ... when the object is seen by another thread, that thread will always
+ see the correctly constructed version of that object's final fields.
+ It will also see versions of any object or array referenced by
+ those final fields that are at least as up-to-date as the final fields
+ are."
+
+ In that same vein, all non-transient fields within Uri
+ implementations should be final and immutable so as to ensure true
+ immutability for clients even when they don't use proper concurrency
+ control.
+
+ For reference, from RFC 2396:
+
+ "4.3. Parsing a URI Reference
+
+ A URI reference is typically parsed according to the four main
+ components and fragment identifier in order to determine what
+ components are present and whether the reference is relative or
+ absolute. The individual components are then parsed for their
+ subparts and, if not opaque, to verify their validity.
+
+ Although the BNF defines what is allowed in each component, it is
+ ambiguous in terms of differentiating between an authority component
+ and a path component that begins with two slash characters. The
+ greedy algorithm is used for disambiguation: the left-most matching
+ rule soaks up as much of the URI reference string as it is capable of
+ matching. In other words, the authority component wins."
+
+ The "four main components" of a hierarchical URI consist of
+ <scheme>://<authority><path>?<query>
+
+ */
+
+ /** Log tag. */
+ private static final String LOG = Uri.class.getSimpleName();
+
+ /**
+ * The empty URI, equivalent to "".
+ */
+ public static final Uri EMPTY = new HierarchicalUri(null, Part.NULL,
+ PathPart.EMPTY, Part.NULL, Part.NULL);
+
+ /**
+ * Prevents external subclassing.
+ */
+ private Uri() {}
+
+ /**
+ * Returns true if this URI is hierarchical like "http://google.com".
+ * Absolute URIs are hierarchical if the scheme-specific part starts with
+ * a '/'. Relative URIs are always hierarchical.
+ */
+ public abstract boolean isHierarchical();
+
+ /**
+ * Returns true if this URI is opaque like "mailto:nobody@google.com". The
+ * scheme-specific part of an opaque URI cannot start with a '/'.
+ */
+ public boolean isOpaque() {
+ return !isHierarchical();
+ }
+
+ /**
+ * Returns true if this URI is relative, i.e. if it doesn't contain an
+ * explicit scheme.
+ *
+ * @return true if this URI is relative, false if it's absolute
+ */
+ public abstract boolean isRelative();
+
+ /**
+ * Returns true if this URI is absolute, i.e. if it contains an
+ * explicit scheme.
+ *
+ * @return true if this URI is absolute, false if it's relative
+ */
+ public boolean isAbsolute() {
+ return !isRelative();
+ }
+
+ /**
+ * Gets the scheme of this URI. Example: "http"
+ *
+ * @return the scheme or null if this is a relative URI
+ */
+ public abstract String getScheme();
+
+ /**
+ * Gets the scheme-specific part of this URI, i.e. everything between the
+ * scheme separator ':' and the fragment separator '#'. If this is a
+ * relative URI, this method returns the entire URI. Decodes escaped octets.
+ *
+ * <p>Example: "//www.google.com/search?q=android"
+ *
+ * @return the decoded scheme-specific-part
+ */
+ public abstract String getSchemeSpecificPart();
+
+ /**
+ * Gets the scheme-specific part of this URI, i.e. everything between the
+ * scheme separator ':' and the fragment separator '#'. If this is a
+ * relative URI, this method returns the entire URI. Leaves escaped octets
+ * intact.
+ *
+ * <p>Example: "//www.google.com/search?q=android"
+ *
+ * @return the decoded scheme-specific-part
+ */
+ public abstract String getEncodedSchemeSpecificPart();
+
+ /**
+ * Gets the decoded authority part of this URI. For
+ * server addresses, the authority is structured as follows:
+ * {@code [ userinfo '@' ] host [ ':' port ]}
+ *
+ * <p>Examples: "google.com", "bob@google.com:80"
+ *
+ * @return the authority for this URI or null if not present
+ */
+ public abstract String getAuthority();
+
+ /**
+ * Gets the encoded authority part of this URI. For
+ * server addresses, the authority is structured as follows:
+ * {@code [ userinfo '@' ] host [ ':' port ]}
+ *
+ * <p>Examples: "google.com", "bob@google.com:80"
+ *
+ * @return the authority for this URI or null if not present
+ */
+ public abstract String getEncodedAuthority();
+
+ /**
+ * Gets the decoded user information from the authority.
+ * For example, if the authority is "nobody@google.com", this method will
+ * return "nobody".
+ *
+ * @return the user info for this URI or null if not present
+ */
+ public abstract String getUserInfo();
+
+ /**
+ * Gets the encoded user information from the authority.
+ * For example, if the authority is "nobody@google.com", this method will
+ * return "nobody".
+ *
+ * @return the user info for this URI or null if not present
+ */
+ public abstract String getEncodedUserInfo();
+
+ /**
+ * Gets the encoded host from the authority for this URI. For example,
+ * if the authority is "bob@google.com", this method will return
+ * "google.com".
+ *
+ * @return the host for this URI or null if not present
+ */
+ public abstract String getHost();
+
+ /**
+ * Gets the port from the authority for this URI. For example,
+ * if the authority is "google.com:80", this method will return 80.
+ *
+ * @return the port for this URI or -1 if invalid or not present
+ */
+ public abstract int getPort();
+
+ /**
+ * Gets the decoded path.
+ *
+ * @return the decoded path, or null if this is not a hierarchical URI
+ * (like "mailto:nobody@google.com") or the URI is invalid
+ */
+ public abstract String getPath();
+
+ /**
+ * Gets the encoded path.
+ *
+ * @return the encoded path, or null if this is not a hierarchical URI
+ * (like "mailto:nobody@google.com") or the URI is invalid
+ */
+ public abstract String getEncodedPath();
+
+ /**
+ * Gets the decoded query component from this URI. The query comes after
+ * the query separator ('?') and before the fragment separator ('#'). This
+ * method would return "q=android" for
+ * "http://www.google.com/search?q=android".
+ *
+ * @return the decoded query or null if there isn't one
+ */
+ public abstract String getQuery();
+
+ /**
+ * Gets the encoded query component from this URI. The query comes after
+ * the query separator ('?') and before the fragment separator ('#'). This
+ * method would return "q=android" for
+ * "http://www.google.com/search?q=android".
+ *
+ * @return the encoded query or null if there isn't one
+ */
+ public abstract String getEncodedQuery();
+
+ /**
+ * Gets the decoded fragment part of this URI, everything after the '#'.
+ *
+ * @return the decoded fragment or null if there isn't one
+ */
+ public abstract String getFragment();
+
+ /**
+ * Gets the encoded fragment part of this URI, everything after the '#'.
+ *
+ * @return the encoded fragment or null if there isn't one
+ */
+ public abstract String getEncodedFragment();
+
+ /**
+ * Gets the decoded path segments.
+ *
+ * @return decoded path segments, each without a leading or trailing '/'
+ */
+ public abstract List<String> getPathSegments();
+
+ /**
+ * Gets the decoded last segment in the path.
+ *
+ * @return the decoded last segment or null if the path is empty
+ */
+ public abstract String getLastPathSegment();
+
+ /**
+ * Compares this Uri to another object for equality. Returns true if the
+ * encoded string representations of this Uri and the given Uri are
+ * equal. Case counts. Paths are not normalized. If one Uri specifies a
+ * default port explicitly and the other leaves it implicit, they will not
+ * be considered equal.
+ */
+ public boolean equals(Object o) {
+ if (!(o instanceof Uri)) {
+ return false;
+ }
+
+ Uri other = (Uri) o;
+
+ return toString().equals(other.toString());
+ }
+
+ /**
+ * Hashes the encoded string represention of this Uri consistently with
+ * {@link #equals(Object)}.
+ */
+ public int hashCode() {
+ return toString().hashCode();
+ }
+
+ /**
+ * Compares the string representation of this Uri with that of
+ * another.
+ */
+ public int compareTo(Uri other) {
+ return toString().compareTo(other.toString());
+ }
+
+ /**
+ * Returns the encoded string representation of this URI.
+ * Example: "http://google.com/"
+ */
+ public abstract String toString();
+
+ /**
+ * Constructs a new builder, copying the attributes from this Uri.
+ */
+ public abstract Builder buildUpon();
+
+ /** Index of a component which was not found. */
+ private final static int NOT_FOUND = -1;
+
+ /** Placeholder value for an index which hasn't been calculated yet. */
+ private final static int NOT_CALCULATED = -2;
+
+ /**
+ * Placeholder for strings which haven't been cached. This enables us
+ * to cache null. We intentionally create a new String instance so we can
+ * compare its identity and there is no chance we will confuse it with
+ * user data.
+ */
+ @SuppressWarnings("RedundantStringConstructorCall")
+ private static final String NOT_CACHED = new String("NOT CACHED");
+
+ /**
+ * Error message presented when a user tries to treat an opaque URI as
+ * hierarchical.
+ */
+ private static final String NOT_HIERARCHICAL
+ = "This isn't a hierarchical URI.";
+
+ /** Default encoding. */
+ private static final String DEFAULT_ENCODING = "UTF-8";
+
+ /**
+ * Creates a Uri which parses the given encoded URI string.
+ *
+ * @param uriString an RFC 3296-compliant, encoded URI
+ * @throws NullPointerException if uriString is null
+ * @return Uri for this given uri string
+ */
+ public static Uri parse(String uriString) {
+ return new StringUri(uriString);
+ }
+
+ /**
+ * Creates a Uri from a file. The URI has the form
+ * "file://<absolute path>". Encodes path characters with the exception of
+ * '/'.
+ *
+ * <p>Example: "file:///tmp/android.txt"
+ *
+ * @throws NullPointerException if file is null
+ * @return a Uri for the given file
+ */
+ public static Uri fromFile(File file) {
+ if (file == null) {
+ throw new NullPointerException("file");
+ }
+
+ PathPart path = PathPart.fromDecoded(file.getAbsolutePath());
+ return new HierarchicalUri(
+ "file", Part.EMPTY, path, Part.NULL, Part.NULL);
+ }
+
+ /**
+ * An implementation which wraps a String URI. This URI can be opaque or
+ * hierarchical, but we extend AbstractHierarchicalUri in case we need
+ * the hierarchical functionality.
+ */
+ private static class StringUri extends AbstractHierarchicalUri {
+
+ /** Used in parcelling. */
+ static final int TYPE_ID = 1;
+
+ /** URI string representation. */
+ private final String uriString;
+
+ private StringUri(String uriString) {
+ if (uriString == null) {
+ throw new NullPointerException("uriString");
+ }
+
+ this.uriString = uriString;
+ }
+
+ static Uri readFrom(Parcel parcel) {
+ return new StringUri(parcel.readString());
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeInt(TYPE_ID);
+ parcel.writeString(uriString);
+ }
+
+ /** Cached scheme separator index. */
+ private volatile int cachedSsi = NOT_CALCULATED;
+
+ /** Finds the first ':'. Returns -1 if none found. */
+ private int findSchemeSeparator() {
+ return cachedSsi == NOT_CALCULATED
+ ? cachedSsi = uriString.indexOf(':')
+ : cachedSsi;
+ }
+
+ /** Cached fragment separator index. */
+ private volatile int cachedFsi = NOT_CALCULATED;
+
+ /** Finds the first '#'. Returns -1 if none found. */
+ private int findFragmentSeparator() {
+ return cachedFsi == NOT_CALCULATED
+ ? cachedFsi = uriString.indexOf('#', findSchemeSeparator())
+ : cachedFsi;
+ }
+
+ public boolean isHierarchical() {
+ int ssi = findSchemeSeparator();
+
+ if (ssi == NOT_FOUND) {
+ // All relative URIs are hierarchical.
+ return true;
+ }
+
+ if (uriString.length() == ssi + 1) {
+ // No ssp.
+ return false;
+ }
+
+ // If the ssp starts with a '/', this is hierarchical.
+ return uriString.charAt(ssi + 1) == '/';
+ }
+
+ public boolean isRelative() {
+ // Note: We return true if the index is 0
+ return findSchemeSeparator() == NOT_FOUND;
+ }
+
+ private volatile String scheme = NOT_CACHED;
+
+ public String getScheme() {
+ @SuppressWarnings("StringEquality")
+ boolean cached = (scheme != NOT_CACHED);
+ return cached ? scheme : (scheme = parseScheme());
+ }
+
+ private String parseScheme() {
+ int ssi = findSchemeSeparator();
+ return ssi == NOT_FOUND ? null : uriString.substring(0, ssi);
+ }
+
+ private Part ssp;
+
+ private Part getSsp() {
+ return ssp == null ? ssp = Part.fromEncoded(parseSsp()) : ssp;
+ }
+
+ public String getEncodedSchemeSpecificPart() {
+ return getSsp().getEncoded();
+ }
+
+ public String getSchemeSpecificPart() {
+ return getSsp().getDecoded();
+ }
+
+ private String parseSsp() {
+ int ssi = findSchemeSeparator();
+ int fsi = findFragmentSeparator();
+
+ // Return everything between ssi and fsi.
+ return fsi == NOT_FOUND
+ ? uriString.substring(ssi + 1)
+ : uriString.substring(ssi + 1, fsi);
+ }
+
+ private Part authority;
+
+ private Part getAuthorityPart() {
+ if (authority == null) {
+ String encodedAuthority
+ = parseAuthority(this.uriString, findSchemeSeparator());
+ return authority = Part.fromEncoded(encodedAuthority);
+ }
+
+ return authority;
+ }
+
+ public String getEncodedAuthority() {
+ return getAuthorityPart().getEncoded();
+ }
+
+ public String getAuthority() {
+ return getAuthorityPart().getDecoded();
+ }
+
+ private PathPart path;
+
+ private PathPart getPathPart() {
+ return path == null
+ ? path = PathPart.fromEncoded(parsePath())
+ : path;
+ }
+
+ public String getPath() {
+ return getPathPart().getDecoded();
+ }
+
+ public String getEncodedPath() {
+ return getPathPart().getEncoded();
+ }
+
+ public List<String> getPathSegments() {
+ return getPathPart().getPathSegments();
+ }
+
+ private String parsePath() {
+ String uriString = this.uriString;
+ int ssi = findSchemeSeparator();
+
+ // If the URI is absolute.
+ if (ssi > -1) {
+ // Is there anything after the ':'?
+ boolean schemeOnly = ssi + 1 == uriString.length();
+ if (schemeOnly) {
+ // Opaque URI.
+ return null;
+ }
+
+ // A '/' after the ':' means this is hierarchical.
+ if (uriString.charAt(ssi + 1) != '/') {
+ // Opaque URI.
+ return null;
+ }
+ } else {
+ // All relative URIs are hierarchical.
+ }
+
+ return parsePath(uriString, ssi);
+ }
+
+ private Part query;
+
+ private Part getQueryPart() {
+ return query == null
+ ? query = Part.fromEncoded(parseQuery()) : query;
+ }
+
+ public String getEncodedQuery() {
+ return getQueryPart().getEncoded();
+ }
+
+ private String parseQuery() {
+ // It doesn't make sense to cache this index. We only ever
+ // calculate it once.
+ int qsi = uriString.indexOf('?', findSchemeSeparator());
+ if (qsi == NOT_FOUND) {
+ return null;
+ }
+
+ int fsi = findFragmentSeparator();
+
+ if (fsi == NOT_FOUND) {
+ return uriString.substring(qsi + 1);
+ }
+
+ if (fsi < qsi) {
+ // Invalid.
+ return null;
+ }
+
+ return uriString.substring(qsi + 1, fsi);
+ }
+
+ public String getQuery() {
+ return getQueryPart().getDecoded();
+ }
+
+ private Part fragment;
+
+ private Part getFragmentPart() {
+ return fragment == null
+ ? fragment = Part.fromEncoded(parseFragment()) : fragment;
+ }
+
+ public String getEncodedFragment() {
+ return getFragmentPart().getEncoded();
+ }
+
+ private String parseFragment() {
+ int fsi = findFragmentSeparator();
+ return fsi == NOT_FOUND ? null : uriString.substring(fsi + 1);
+ }
+
+ public String getFragment() {
+ return getFragmentPart().getDecoded();
+ }
+
+ public String toString() {
+ return uriString;
+ }
+
+ /**
+ * Parses an authority out of the given URI string.
+ *
+ * @param uriString URI string
+ * @param ssi scheme separator index, -1 for a relative URI
+ *
+ * @return the authority or null if none is found
+ */
+ static String parseAuthority(String uriString, int ssi) {
+ int length = uriString.length();
+
+ // If "//" follows the scheme separator, we have an authority.
+ if (length > ssi + 2
+ && uriString.charAt(ssi + 1) == '/'
+ && uriString.charAt(ssi + 2) == '/') {
+ // We have an authority.
+
+ // Look for the start of the path, query, or fragment, or the
+ // end of the string.
+ int end = ssi + 3;
+ LOOP: while (end < length) {
+ switch (uriString.charAt(end)) {
+ case '/': // Start of path
+ case '?': // Start of query
+ case '#': // Start of fragment
+ break LOOP;
+ }
+ end++;
+ }
+
+ return uriString.substring(ssi + 3, end);
+ } else {
+ return null;
+ }
+
+ }
+
+ /**
+ * Parses a path out of this given URI string.
+ *
+ * @param uriString URI string
+ * @param ssi scheme separator index, -1 for a relative URI
+ *
+ * @return the path
+ */
+ static String parsePath(String uriString, int ssi) {
+ int length = uriString.length();
+
+ // Find start of path.
+ int pathStart;
+ if (length > ssi + 2
+ && uriString.charAt(ssi + 1) == '/'
+ && uriString.charAt(ssi + 2) == '/') {
+ // Skip over authority to path.
+ pathStart = ssi + 3;
+ LOOP: while (pathStart < length) {
+ switch (uriString.charAt(pathStart)) {
+ case '?': // Start of query
+ case '#': // Start of fragment
+ return ""; // Empty path.
+ case '/': // Start of path!
+ break LOOP;
+ }
+ pathStart++;
+ }
+ } else {
+ // Path starts immediately after scheme separator.
+ pathStart = ssi + 1;
+ }
+
+ // Find end of path.
+ int pathEnd = pathStart;
+ LOOP: while (pathEnd < length) {
+ switch (uriString.charAt(pathEnd)) {
+ case '?': // Start of query
+ case '#': // Start of fragment
+ break LOOP;
+ }
+ pathEnd++;
+ }
+
+ return uriString.substring(pathStart, pathEnd);
+ }
+
+ public Builder buildUpon() {
+ if (isHierarchical()) {
+ return new Builder()
+ .scheme(getScheme())
+ .authority(getAuthorityPart())
+ .path(getPathPart())
+ .query(getQueryPart())
+ .fragment(getFragmentPart());
+ } else {
+ return new Builder()
+ .scheme(getScheme())
+ .opaquePart(getSsp())
+ .fragment(getFragmentPart());
+ }
+ }
+ }
+
+ /**
+ * Creates an opaque Uri from the given components. Encodes the ssp
+ * which means this method cannot be used to create hierarchical URIs.
+ *
+ * @param scheme of the URI
+ * @param ssp scheme-specific-part, everything between the
+ * scheme separator (':') and the fragment separator ('#'), which will
+ * get encoded
+ * @param fragment fragment, everything after the '#', null if undefined,
+ * will get encoded
+ *
+ * @throws NullPointerException if scheme or ssp is null
+ * @return Uri composed of the given scheme, ssp, and fragment
+ *
+ * @see Builder if you don't want the ssp and fragment to be encoded
+ */
+ public static Uri fromParts(String scheme, String ssp,
+ String fragment) {
+ if (scheme == null) {
+ throw new NullPointerException("scheme");
+ }
+ if (ssp == null) {
+ throw new NullPointerException("ssp");
+ }
+
+ return new OpaqueUri(scheme, Part.fromDecoded(ssp),
+ Part.fromDecoded(fragment));
+ }
+
+ /**
+ * Opaque URI.
+ */
+ private static class OpaqueUri extends Uri {
+
+ /** Used in parcelling. */
+ static final int TYPE_ID = 2;
+
+ private final String scheme;
+ private final Part ssp;
+ private final Part fragment;
+
+ private OpaqueUri(String scheme, Part ssp, Part fragment) {
+ this.scheme = scheme;
+ this.ssp = ssp;
+ this.fragment = fragment == null ? Part.NULL : fragment;
+ }
+
+ static Uri readFrom(Parcel parcel) {
+ return new OpaqueUri(
+ parcel.readString(),
+ Part.readFrom(parcel),
+ Part.readFrom(parcel)
+ );
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeInt(TYPE_ID);
+ parcel.writeString(scheme);
+ ssp.writeTo(parcel);
+ fragment.writeTo(parcel);
+ }
+
+ public boolean isHierarchical() {
+ return false;
+ }
+
+ public boolean isRelative() {
+ return scheme == null;
+ }
+
+ public String getScheme() {
+ return this.scheme;
+ }
+
+ public String getEncodedSchemeSpecificPart() {
+ return ssp.getEncoded();
+ }
+
+ public String getSchemeSpecificPart() {
+ return ssp.getDecoded();
+ }
+
+ public String getAuthority() {
+ return null;
+ }
+
+ public String getEncodedAuthority() {
+ return null;
+ }
+
+ public String getPath() {
+ return null;
+ }
+
+ public String getEncodedPath() {
+ return null;
+ }
+
+ public String getQuery() {
+ return null;
+ }
+
+ public String getEncodedQuery() {
+ return null;
+ }
+
+ public String getFragment() {
+ return fragment.getDecoded();
+ }
+
+ public String getEncodedFragment() {
+ return fragment.getEncoded();
+ }
+
+ public List<String> getPathSegments() {
+ return Collections.emptyList();
+ }
+
+ public String getLastPathSegment() {
+ return null;
+ }
+
+ public String getUserInfo() {
+ return null;
+ }
+
+ public String getEncodedUserInfo() {
+ return null;
+ }
+
+ public String getHost() {
+ return null;
+ }
+
+ public int getPort() {
+ return -1;
+ }
+
+ private volatile String cachedString = NOT_CACHED;
+
+ public String toString() {
+ @SuppressWarnings("StringEquality")
+ boolean cached = cachedString != NOT_CACHED;
+ if (cached) {
+ return cachedString;
+ }
+
+ StringBuilder sb = new StringBuilder();
+
+ sb.append(scheme).append(':');
+ sb.append(getEncodedSchemeSpecificPart());
+
+ if (!fragment.isEmpty()) {
+ sb.append('#').append(fragment.getEncoded());
+ }
+
+ return cachedString = sb.toString();
+ }
+
+ public Builder buildUpon() {
+ return new Builder()
+ .scheme(this.scheme)
+ .opaquePart(this.ssp)
+ .fragment(this.fragment);
+ }
+ }
+
+ /**
+ * Wrapper for path segment array.
+ */
+ static class PathSegments extends AbstractList<String>
+ implements RandomAccess {
+
+ static final PathSegments EMPTY = new PathSegments(null, 0);
+
+ final String[] segments;
+ final int size;
+
+ PathSegments(String[] segments, int size) {
+ this.segments = segments;
+ this.size = size;
+ }
+
+ public String get(int index) {
+ if (index >= size) {
+ throw new IndexOutOfBoundsException();
+ }
+
+ return segments[index];
+ }
+
+ public int size() {
+ return this.size;
+ }
+ }
+
+ /**
+ * Builds PathSegments.
+ */
+ static class PathSegmentsBuilder {
+
+ String[] segments;
+ int size = 0;
+
+ void add(String segment) {
+ if (segments == null) {
+ segments = new String[4];
+ } else if (size + 1 == segments.length) {
+ String[] expanded = new String[segments.length * 2];
+ System.arraycopy(segments, 0, expanded, 0, segments.length);
+ segments = expanded;
+ }
+
+ segments[size++] = segment;
+ }
+
+ PathSegments build() {
+ if (segments == null) {
+ return PathSegments.EMPTY;
+ }
+
+ try {
+ return new PathSegments(segments, size);
+ } finally {
+ // Makes sure this doesn't get reused.
+ segments = null;
+ }
+ }
+ }
+
+ /**
+ * Support for hierarchical URIs.
+ */
+ private abstract static class AbstractHierarchicalUri extends Uri {
+
+ public String getLastPathSegment() {
+ // TODO: If we haven't parsed all of the segments already, just
+ // grab the last one directly so we only allocate one string.
+
+ List<String> segments = getPathSegments();
+ int size = segments.size();
+ if (size == 0) {
+ return null;
+ }
+ return segments.get(size - 1);
+ }
+
+ private Part userInfo;
+
+ private Part getUserInfoPart() {
+ return userInfo == null
+ ? userInfo = Part.fromEncoded(parseUserInfo()) : userInfo;
+ }
+
+ public final String getEncodedUserInfo() {
+ return getUserInfoPart().getEncoded();
+ }
+
+ private String parseUserInfo() {
+ String authority = getEncodedAuthority();
+ if (authority == null) {
+ return null;
+ }
+
+ int end = authority.indexOf('@');
+ return end == NOT_FOUND ? null : authority.substring(0, end);
+ }
+
+ public String getUserInfo() {
+ return getUserInfoPart().getDecoded();
+ }
+
+ private volatile String host = NOT_CACHED;
+
+ public String getHost() {
+ @SuppressWarnings("StringEquality")
+ boolean cached = (host != NOT_CACHED);
+ return cached ? host
+ : (host = parseHost());
+ }
+
+ private String parseHost() {
+ String authority = getAuthority();
+ if (authority == null) {
+ return null;
+ }
+
+ // Parse out user info and then port.
+ int userInfoSeparator = authority.indexOf('@');
+ int portSeparator = authority.indexOf(':', userInfoSeparator);
+
+ return portSeparator == NOT_FOUND
+ ? authority.substring(userInfoSeparator + 1)
+ : authority.substring(userInfoSeparator + 1, portSeparator);
+ }
+
+ private volatile int port = NOT_CALCULATED;
+
+ public int getPort() {
+ return port == NOT_CALCULATED
+ ? port = parsePort()
+ : port;
+ }
+
+ private int parsePort() {
+ String authority = getAuthority();
+ if (authority == null) {
+ return -1;
+ }
+
+ // Make sure we look for the port separtor *after* the user info
+ // separator. We have URLs with a ':' in the user info.
+ int userInfoSeparator = authority.indexOf('@');
+ int portSeparator = authority.indexOf(':', userInfoSeparator);
+
+ if (portSeparator == NOT_FOUND) {
+ return -1;
+ }
+
+ String portString = authority.substring(portSeparator + 1);
+ try {
+ return Integer.parseInt(portString);
+ } catch (NumberFormatException e) {
+ Log.w(LOG, "Error parsing port string.", e);
+ return -1;
+ }
+ }
+ }
+
+ /**
+ * Hierarchical Uri.
+ */
+ private static class HierarchicalUri extends AbstractHierarchicalUri {
+
+ /** Used in parcelling. */
+ static final int TYPE_ID = 3;
+
+ private final String scheme;
+ private final Part authority;
+ private final PathPart path;
+ private final Part query;
+ private final Part fragment;
+
+ private HierarchicalUri(String scheme, Part authority, PathPart path,
+ Part query, Part fragment) {
+ this.scheme = scheme;
+ this.authority = authority;
+ this.path = path;
+ this.query = query;
+ this.fragment = fragment;
+ }
+
+ static Uri readFrom(Parcel parcel) {
+ return new HierarchicalUri(
+ parcel.readString(),
+ Part.readFrom(parcel),
+ PathPart.readFrom(parcel),
+ Part.readFrom(parcel),
+ Part.readFrom(parcel)
+ );
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeInt(TYPE_ID);
+ parcel.writeString(scheme);
+ authority.writeTo(parcel);
+ path.writeTo(parcel);
+ query.writeTo(parcel);
+ fragment.writeTo(parcel);
+ }
+
+ public boolean isHierarchical() {
+ return true;
+ }
+
+ public boolean isRelative() {
+ return scheme == null;
+ }
+
+ public String getScheme() {
+ return scheme;
+ }
+
+ private Part ssp;
+
+ private Part getSsp() {
+ return ssp == null
+ ? ssp = Part.fromEncoded(makeSchemeSpecificPart()) : ssp;
+ }
+
+ public String getEncodedSchemeSpecificPart() {
+ return getSsp().getEncoded();
+ }
+
+ public String getSchemeSpecificPart() {
+ return getSsp().getDecoded();
+ }
+
+ /**
+ * Creates the encoded scheme-specific part from its sub parts.
+ */
+ private String makeSchemeSpecificPart() {
+ StringBuilder builder = new StringBuilder();
+ appendSspTo(builder);
+ return builder.toString();
+ }
+
+ private void appendSspTo(StringBuilder builder) {
+ if (authority != null) {
+ String encodedAuthority = authority.getEncoded();
+ if (encodedAuthority != null) {
+ // Even if the authority is "", we still want to append "//".
+ builder.append("//").append(encodedAuthority);
+ }
+ }
+
+ // path is never null.
+ String encodedPath = path.getEncoded();
+ if (encodedPath != null) {
+ builder.append(encodedPath);
+ }
+
+ if (query != null && !query.isEmpty()) {
+ builder.append('?').append(query.getEncoded());
+ }
+ }
+
+ public String getAuthority() {
+ return this.authority.getDecoded();
+ }
+
+ public String getEncodedAuthority() {
+ return this.authority.getEncoded();
+ }
+
+ public String getEncodedPath() {
+ return this.path.getEncoded();
+ }
+
+ public String getPath() {
+ return this.path.getDecoded();
+ }
+
+ public String getQuery() {
+ return this.query.getDecoded();
+ }
+
+ public String getEncodedQuery() {
+ return this.query.getEncoded();
+ }
+
+ public String getFragment() {
+ return this.fragment.getDecoded();
+ }
+
+ public String getEncodedFragment() {
+ return this.fragment.getEncoded();
+ }
+
+ public List<String> getPathSegments() {
+ return this.path.getPathSegments();
+ }
+
+ private volatile String uriString = NOT_CACHED;
+
+ @Override
+ public String toString() {
+ @SuppressWarnings("StringEquality")
+ boolean cached = (uriString != NOT_CACHED);
+ return cached ? uriString
+ : (uriString = makeUriString());
+ }
+
+ private String makeUriString() {
+ StringBuilder builder = new StringBuilder();
+
+ if (scheme != null) {
+ builder.append(scheme).append(':');
+ }
+
+ appendSspTo(builder);
+
+ if (fragment != null && !fragment.isEmpty()) {
+ builder.append('#').append(fragment.getEncoded());
+ }
+
+ return builder.toString();
+ }
+
+ public Builder buildUpon() {
+ return new Builder()
+ .scheme(scheme)
+ .authority(authority)
+ .path(path)
+ .query(query)
+ .fragment(fragment);
+ }
+ }
+
+ /**
+ * Helper class for building or manipulating URI references. Not safe for
+ * concurrent use.
+ *
+ * <p>An absolute hierarchical URI reference follows the pattern:
+ * {@code &lt;scheme&gt;://&lt;authority&gt;&lt;absolute path&gt;?&lt;query&gt;#&lt;fragment&gt;}
+ *
+ * <p>Relative URI references (which are always hierarchical) follow one
+ * of two patterns: {@code &lt;relative or absolute path&gt;?&lt;query&gt;#&lt;fragment&gt;}
+ * or {@code //&lt;authority&gt;&lt;absolute path&gt;?&lt;query&gt;#&lt;fragment&gt;}
+ *
+ * <p>An opaque URI follows this pattern:
+ * {@code &lt;scheme&gt;:&lt;opaque part&gt;#&lt;fragment&gt;}
+ */
+ public static final class Builder {
+
+ private String scheme;
+ private Part opaquePart;
+ private Part authority;
+ private PathPart path;
+ private Part query;
+ private Part fragment;
+
+ /**
+ * Constructs a new Builder.
+ */
+ public Builder() {}
+
+ /**
+ * Sets the scheme.
+ *
+ * @param scheme name or {@code null} if this is a relative Uri
+ */
+ public Builder scheme(String scheme) {
+ this.scheme = scheme;
+ return this;
+ }
+
+ Builder opaquePart(Part opaquePart) {
+ this.opaquePart = opaquePart;
+ return this;
+ }
+
+ /**
+ * Encodes and sets the given opaque scheme-specific-part.
+ *
+ * @param opaquePart decoded opaque part
+ */
+ public Builder opaquePart(String opaquePart) {
+ return opaquePart(Part.fromDecoded(opaquePart));
+ }
+
+ /**
+ * Sets the previously encoded opaque scheme-specific-part.
+ *
+ * @param opaquePart encoded opaque part
+ */
+ public Builder encodedOpaquePart(String opaquePart) {
+ return opaquePart(Part.fromEncoded(opaquePart));
+ }
+
+ Builder authority(Part authority) {
+ // This URI will be hierarchical.
+ this.opaquePart = null;
+
+ this.authority = authority;
+ return this;
+ }
+
+ /**
+ * Encodes and sets the authority.
+ */
+ public Builder authority(String authority) {
+ return authority(Part.fromDecoded(authority));
+ }
+
+ /**
+ * Sets the previously encoded authority.
+ */
+ public Builder encodedAuthority(String authority) {
+ return authority(Part.fromEncoded(authority));
+ }
+
+ Builder path(PathPart path) {
+ // This URI will be hierarchical.
+ this.opaquePart = null;
+
+ this.path = path;
+ return this;
+ }
+
+ /**
+ * Sets the path. Leaves '/' characters intact but encodes others as
+ * necessary.
+ *
+ * <p>If the path is not null and doesn't start with a '/', and if
+ * you specify a scheme and/or authority, the builder will prepend the
+ * given path with a '/'.
+ */
+ public Builder path(String path) {
+ return path(PathPart.fromDecoded(path));
+ }
+
+ /**
+ * Sets the previously encoded path.
+ *
+ * <p>If the path is not null and doesn't start with a '/', and if
+ * you specify a scheme and/or authority, the builder will prepend the
+ * given path with a '/'.
+ */
+ public Builder encodedPath(String path) {
+ return path(PathPart.fromEncoded(path));
+ }
+
+ /**
+ * Encodes the given segment and appends it to the path.
+ */
+ public Builder appendPath(String newSegment) {
+ return path(PathPart.appendDecodedSegment(path, newSegment));
+ }
+
+ /**
+ * Appends the given segment to the path.
+ */
+ public Builder appendEncodedPath(String newSegment) {
+ return path(PathPart.appendEncodedSegment(path, newSegment));
+ }
+
+ Builder query(Part query) {
+ // This URI will be hierarchical.
+ this.opaquePart = null;
+
+ this.query = query;
+ return this;
+ }
+
+ /**
+ * Encodes and sets the query.
+ */
+ public Builder query(String query) {
+ return query(Part.fromDecoded(query));
+ }
+
+ /**
+ * Sets the previously encoded query.
+ */
+ public Builder encodedQuery(String query) {
+ return query(Part.fromEncoded(query));
+ }
+
+ Builder fragment(Part fragment) {
+ this.fragment = fragment;
+ return this;
+ }
+
+ /**
+ * Encodes and sets the fragment.
+ */
+ public Builder fragment(String fragment) {
+ return fragment(Part.fromDecoded(fragment));
+ }
+
+ /**
+ * Sets the previously encoded fragment.
+ */
+ public Builder encodedFragment(String fragment) {
+ return fragment(Part.fromEncoded(fragment));
+ }
+
+ /**
+ * Encodes the key and value and then appends the parameter to the
+ * query string.
+ *
+ * @param key which will be encoded
+ * @param value which will be encoded
+ */
+ public Builder appendQueryParameter(String key, String value) {
+ // This URI will be hierarchical.
+ this.opaquePart = null;
+
+ String encodedParameter = encode(key, null) + "="
+ + encode(value, null);
+
+ if (query == null) {
+ query = Part.fromEncoded(encodedParameter);
+ return this;
+ }
+
+ String oldQuery = query.getEncoded();
+ if (oldQuery == null || oldQuery.length() == 0) {
+ query = Part.fromEncoded(encodedParameter);
+ } else {
+ query = Part.fromEncoded(oldQuery + "&" + encodedParameter);
+ }
+
+ return this;
+ }
+
+ /**
+ * Constructs a Uri with the current attributes.
+ *
+ * @throws UnsupportedOperationException if the URI is opaque and the
+ * scheme is null
+ */
+ public Uri build() {
+ if (opaquePart != null) {
+ if (this.scheme == null) {
+ throw new UnsupportedOperationException(
+ "An opaque URI must have a scheme.");
+ }
+
+ return new OpaqueUri(scheme, opaquePart, fragment);
+ } else {
+ // Hierarchical URIs should not return null for getPath().
+ PathPart path = this.path;
+ if (path == null || path == PathPart.NULL) {
+ path = PathPart.EMPTY;
+ } else {
+ // If we have a scheme and/or authority, the path must
+ // be absolute. Prepend it with a '/' if necessary.
+ if (hasSchemeOrAuthority()) {
+ path = PathPart.makeAbsolute(path);
+ }
+ }
+
+ return new HierarchicalUri(
+ scheme, authority, path, query, fragment);
+ }
+ }
+
+ private boolean hasSchemeOrAuthority() {
+ return scheme != null
+ || (authority != null && authority != Part.NULL);
+
+ }
+
+ @Override
+ public String toString() {
+ return build().toString();
+ }
+ }
+
+ /**
+ * Searches the query string for parameter values with the given key.
+ *
+ * @param key which will be encoded
+ *
+ * @throws UnsupportedOperationException if this isn't a hierarchical URI
+ * @throws NullPointerException if key is null
+ *
+ * @return a list of decoded values
+ */
+ public List<String> getQueryParameters(String key) {
+ if (isOpaque()) {
+ throw new UnsupportedOperationException(NOT_HIERARCHICAL);
+ }
+
+ String query = getQuery();
+ if (query == null) {
+ return Collections.emptyList();
+ }
+
+ String encodedKey;
+ try {
+ encodedKey = URLEncoder.encode(key, DEFAULT_ENCODING);
+ } catch (UnsupportedEncodingException e) {
+ throw new AssertionError(e);
+ }
+
+ // Prepend query with "&" making the first parameter the same as the
+ // rest.
+ query = "&" + query;
+
+ // Parameter prefix.
+ String prefix = "&" + encodedKey + "=";
+
+ ArrayList<String> values = new ArrayList<String>();
+
+ int start = 0;
+ int length = query.length();
+ while (start < length) {
+ start = query.indexOf(prefix, start);
+
+ if (start == -1) {
+ // No more values.
+ break;
+ }
+
+ // Move start to start of value.
+ start += prefix.length();
+
+ // Find end of value.
+ int end = query.indexOf('&', start);
+ if (end == -1) {
+ end = query.length();
+ }
+
+ String value = query.substring(start, end);
+ values.add(decode(value));
+
+ start = end;
+ }
+
+ return Collections.unmodifiableList(values);
+ }
+
+ /**
+ * Searches the query string for the first value with the given key.
+ *
+ * @param key which will be encoded
+ * @throws UnsupportedOperationException if this isn't a hierarchical URI
+ * @throws NullPointerException if key is null
+ *
+ * @return the decoded value or null if no parameter is found
+ */
+ public String getQueryParameter(String key) {
+ if (isOpaque()) {
+ throw new UnsupportedOperationException(NOT_HIERARCHICAL);
+ }
+
+ String query = getQuery();
+
+ if (query == null) {
+ return null;
+ }
+
+ String encodedKey;
+ try {
+ encodedKey = URLEncoder.encode(key, DEFAULT_ENCODING);
+ } catch (UnsupportedEncodingException e) {
+ throw new AssertionError(e);
+ }
+
+ String prefix = encodedKey + "=";
+
+ if (query.length() < prefix.length()) {
+ return null;
+ }
+
+ int start;
+ if (query.startsWith(prefix)) {
+ // It's the first parameter.
+ start = prefix.length();
+ } else {
+ // It must be later in the query string.
+ prefix = "&" + prefix;
+ start = query.indexOf(prefix);
+
+ if (start == -1) {
+ // Not found.
+ return null;
+ }
+
+ start += prefix.length();
+ }
+
+ // Find end of value.
+ int end = query.indexOf('&', start);
+ if (end == -1) {
+ end = query.length();
+ }
+
+ String value = query.substring(start, end);
+ return decode(value);
+ }
+
+ /** Identifies a null parcelled Uri. */
+ private static final int NULL_TYPE_ID = 0;
+
+ /**
+ * Reads Uris from Parcels.
+ */
+ public static final Parcelable.Creator<Uri> CREATOR
+ = new Parcelable.Creator<Uri>() {
+ public Uri createFromParcel(Parcel in) {
+ int type = in.readInt();
+ switch (type) {
+ case NULL_TYPE_ID: return null;
+ case StringUri.TYPE_ID: return StringUri.readFrom(in);
+ case OpaqueUri.TYPE_ID: return OpaqueUri.readFrom(in);
+ case HierarchicalUri.TYPE_ID:
+ return HierarchicalUri.readFrom(in);
+ }
+
+ throw new AssertionError("Unknown URI type: " + type);
+ }
+
+ public Uri[] newArray(int size) {
+ return new Uri[size];
+ }
+ };
+
+ /**
+ * Writes a Uri to a Parcel.
+ *
+ * @param out parcel to write to
+ * @param uri to write, can be null
+ */
+ public static void writeToParcel(Parcel out, Uri uri) {
+ if (uri == null) {
+ out.writeInt(NULL_TYPE_ID);
+ } else {
+ uri.writeToParcel(out, 0);
+ }
+ }
+
+ private static final char[] HEX_DIGITS = "0123456789ABCDEF".toCharArray();
+
+ /**
+ * Encodes characters in the given string as '%'-escaped octets
+ * using the UTF-8 scheme. Leaves letters ("A-Z", "a-z"), numbers
+ * ("0-9"), and unreserved characters ("_-!.~'()*") intact. Encodes
+ * all other characters.
+ *
+ * @param s string to encode
+ * @return an encoded version of s suitable for use as a URI component,
+ * or null if s is null
+ */
+ public static String encode(String s) {
+ return encode(s, null);
+ }
+
+ /**
+ * Encodes characters in the given string as '%'-escaped octets
+ * using the UTF-8 scheme. Leaves letters ("A-Z", "a-z"), numbers
+ * ("0-9"), and unreserved characters ("_-!.~'()*") intact. Encodes
+ * all other characters with the exception of those specified in the
+ * allow argument.
+ *
+ * @param s string to encode
+ * @param allow set of additional characters to allow in the encoded form,
+ * null if no characters should be skipped
+ * @return an encoded version of s suitable for use as a URI component,
+ * or null if s is null
+ */
+ public static String encode(String s, String allow) {
+ if (s == null) {
+ return null;
+ }
+
+ // Lazily-initialized buffers.
+ StringBuilder encoded = null;
+
+ int oldLength = s.length();
+
+ // This loop alternates between copying over allowed characters and
+ // encoding in chunks. This results in fewer method calls and
+ // allocations than encoding one character at a time.
+ int current = 0;
+ while (current < oldLength) {
+ // Start in "copying" mode where we copy over allowed chars.
+
+ // Find the next character which needs to be encoded.
+ int nextToEncode = current;
+ while (nextToEncode < oldLength
+ && isAllowed(s.charAt(nextToEncode), allow)) {
+ nextToEncode++;
+ }
+
+ // If there's nothing more to encode...
+ if (nextToEncode == oldLength) {
+ if (current == 0) {
+ // We didn't need to encode anything!
+ return s;
+ } else {
+ // Presumably, we've already done some encoding.
+ encoded.append(s, current, oldLength);
+ return encoded.toString();
+ }
+ }
+
+ if (encoded == null) {
+ encoded = new StringBuilder();
+ }
+
+ if (nextToEncode > current) {
+ // Append allowed characters leading up to this point.
+ encoded.append(s, current, nextToEncode);
+ } else {
+ // assert nextToEncode == current
+ }
+
+ // Switch to "encoding" mode.
+
+ // Find the next allowed character.
+ current = nextToEncode;
+ int nextAllowed = current + 1;
+ while (nextAllowed < oldLength
+ && !isAllowed(s.charAt(nextAllowed), allow)) {
+ nextAllowed++;
+ }
+
+ // Convert the substring to bytes and encode the bytes as
+ // '%'-escaped octets.
+ String toEncode = s.substring(current, nextAllowed);
+ try {
+ byte[] bytes = toEncode.getBytes(DEFAULT_ENCODING);
+ int bytesLength = bytes.length;
+ for (int i = 0; i < bytesLength; i++) {
+ encoded.append('%');
+ encoded.append(HEX_DIGITS[(bytes[i] & 0xf0) >> 4]);
+ encoded.append(HEX_DIGITS[bytes[i] & 0xf]);
+ }
+ } catch (UnsupportedEncodingException e) {
+ throw new AssertionError(e);
+ }
+
+ current = nextAllowed;
+ }
+
+ // Encoded could still be null at this point if s is empty.
+ return encoded == null ? s : encoded.toString();
+ }
+
+ /**
+ * Returns true if the given character is allowed.
+ *
+ * @param c character to check
+ * @param allow characters to allow
+ * @return true if the character is allowed or false if it should be
+ * encoded
+ */
+ private static boolean isAllowed(char c, String allow) {
+ return (c >= 'A' && c <= 'Z')
+ || (c >= 'a' && c <= 'z')
+ || (c >= '0' && c <= '9')
+ || "_-!.~'()*".indexOf(c) != NOT_FOUND
+ || (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
+ * ("\\uFFFD").
+ *
+ * @param s encoded string to decode
+ * @return the given string with escaped octets decoded, or null if
+ * 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();
+ }
+
+ /**
+ * Support for part implementations.
+ */
+ static abstract class AbstractPart {
+
+ /**
+ * Enum which indicates which representation of a given part we have.
+ */
+ static class Representation {
+ static final int BOTH = 0;
+ static final int ENCODED = 1;
+ static final int DECODED = 2;
+ }
+
+ volatile String encoded;
+ volatile String decoded;
+
+ AbstractPart(String encoded, String decoded) {
+ this.encoded = encoded;
+ this.decoded = decoded;
+ }
+
+ abstract String getEncoded();
+
+ final String getDecoded() {
+ @SuppressWarnings("StringEquality")
+ boolean hasDecoded = decoded != NOT_CACHED;
+ return hasDecoded ? decoded : (decoded = decode(encoded));
+ }
+
+ final void writeTo(Parcel parcel) {
+ @SuppressWarnings("StringEquality")
+ boolean hasEncoded = encoded != NOT_CACHED;
+
+ @SuppressWarnings("StringEquality")
+ boolean hasDecoded = decoded != NOT_CACHED;
+
+ if (hasEncoded && hasDecoded) {
+ parcel.writeInt(Representation.BOTH);
+ parcel.writeString(encoded);
+ parcel.writeString(decoded);
+ } else if (hasEncoded) {
+ parcel.writeInt(Representation.ENCODED);
+ parcel.writeString(encoded);
+ } else if (hasDecoded) {
+ parcel.writeInt(Representation.DECODED);
+ parcel.writeString(decoded);
+ } else {
+ throw new AssertionError();
+ }
+ }
+ }
+
+ /**
+ * Immutable wrapper of encoded and decoded versions of a URI part. Lazily
+ * creates the encoded or decoded version from the other.
+ */
+ static class Part extends AbstractPart {
+
+ /** A part with null values. */
+ static final Part NULL = new EmptyPart(null);
+
+ /** A part with empty strings for values. */
+ static final Part EMPTY = new EmptyPart("");
+
+ private Part(String encoded, String decoded) {
+ super(encoded, decoded);
+ }
+
+ boolean isEmpty() {
+ return false;
+ }
+
+ String getEncoded() {
+ @SuppressWarnings("StringEquality")
+ boolean hasEncoded = encoded != NOT_CACHED;
+ return hasEncoded ? encoded : (encoded = encode(decoded));
+ }
+
+ static Part readFrom(Parcel parcel) {
+ int representation = parcel.readInt();
+ switch (representation) {
+ case Representation.BOTH:
+ return from(parcel.readString(), parcel.readString());
+ case Representation.ENCODED:
+ return fromEncoded(parcel.readString());
+ case Representation.DECODED:
+ return fromDecoded(parcel.readString());
+ default:
+ throw new AssertionError();
+ }
+ }
+
+ /**
+ * Returns given part or {@link #NULL} if the given part is null.
+ */
+ static Part nonNull(Part part) {
+ return part == null ? NULL : part;
+ }
+
+ /**
+ * Creates a part from the encoded string.
+ *
+ * @param encoded part string
+ */
+ static Part fromEncoded(String encoded) {
+ return from(encoded, NOT_CACHED);
+ }
+
+ /**
+ * Creates a part from the decoded string.
+ *
+ * @param decoded part string
+ */
+ static Part fromDecoded(String decoded) {
+ return from(NOT_CACHED, decoded);
+ }
+
+ /**
+ * Creates a part from the encoded and decoded strings.
+ *
+ * @param encoded part string
+ * @param decoded part string
+ */
+ static Part from(String encoded, String decoded) {
+ // We have to check both encoded and decoded in case one is
+ // NOT_CACHED.
+
+ if (encoded == null) {
+ return NULL;
+ }
+ if (encoded.length() == 0) {
+ return EMPTY;
+ }
+
+ if (decoded == null) {
+ return NULL;
+ }
+ if (decoded .length() == 0) {
+ return EMPTY;
+ }
+
+ return new Part(encoded, decoded);
+ }
+
+ private static class EmptyPart extends Part {
+ public EmptyPart(String value) {
+ super(value, value);
+ }
+
+ @Override
+ boolean isEmpty() {
+ return true;
+ }
+ }
+ }
+
+ /**
+ * Immutable wrapper of encoded and decoded versions of a path part. Lazily
+ * creates the encoded or decoded version from the other.
+ */
+ static class PathPart extends AbstractPart {
+
+ /** A part with null values. */
+ static final PathPart NULL = new PathPart(null, null);
+
+ /** A part with empty strings for values. */
+ static final PathPart EMPTY = new PathPart("", "");
+
+ private PathPart(String encoded, String decoded) {
+ super(encoded, decoded);
+ }
+
+ String getEncoded() {
+ @SuppressWarnings("StringEquality")
+ boolean hasEncoded = encoded != NOT_CACHED;
+
+ // Don't encode '/'.
+ return hasEncoded ? encoded : (encoded = encode(decoded, "/"));
+ }
+
+ /**
+ * Cached path segments. This doesn't need to be volatile--we don't
+ * care if other threads see the result.
+ */
+ private PathSegments pathSegments;
+
+ /**
+ * Gets the individual path segments. Parses them if necessary.
+ *
+ * @return parsed path segments or null if this isn't a hierarchical
+ * URI
+ */
+ PathSegments getPathSegments() {
+ if (pathSegments != null) {
+ return pathSegments;
+ }
+
+ String path = getEncoded();
+ if (path == null) {
+ return pathSegments = PathSegments.EMPTY;
+ }
+
+ PathSegmentsBuilder segmentBuilder = new PathSegmentsBuilder();
+
+ int previous = 0;
+ int current;
+ while ((current = path.indexOf('/', previous)) > -1) {
+ // This check keeps us from adding a segment if the path starts
+ // '/' and an empty segment for "//".
+ if (previous < current) {
+ String decodedSegment
+ = decode(path.substring(previous, current));
+ segmentBuilder.add(decodedSegment);
+ }
+ previous = current + 1;
+ }
+
+ // Add in the final path segment.
+ if (previous < path.length()) {
+ segmentBuilder.add(decode(path.substring(previous)));
+ }
+
+ return pathSegments = segmentBuilder.build();
+ }
+
+ static PathPart appendEncodedSegment(PathPart oldPart,
+ String newSegment) {
+ // If there is no old path, should we make the new path relative
+ // or absolute? I pick absolute.
+
+ if (oldPart == null) {
+ // No old path.
+ return fromEncoded("/" + newSegment);
+ }
+
+ String oldPath = oldPart.getEncoded();
+
+ if (oldPath == null) {
+ oldPath = "";
+ }
+
+ int oldPathLength = oldPath.length();
+ String newPath;
+ if (oldPathLength == 0) {
+ // No old path.
+ newPath = "/" + newSegment;
+ } else if (oldPath.charAt(oldPathLength - 1) == '/') {
+ newPath = oldPath + newSegment;
+ } else {
+ newPath = oldPath + "/" + newSegment;
+ }
+
+ return fromEncoded(newPath);
+ }
+
+ static PathPart appendDecodedSegment(PathPart oldPart, String decoded) {
+ String encoded = encode(decoded);
+
+ // TODO: Should we reuse old PathSegments? Probably not.
+ return appendEncodedSegment(oldPart, encoded);
+ }
+
+ static PathPart readFrom(Parcel parcel) {
+ int representation = parcel.readInt();
+ switch (representation) {
+ case Representation.BOTH:
+ return from(parcel.readString(), parcel.readString());
+ case Representation.ENCODED:
+ return fromEncoded(parcel.readString());
+ case Representation.DECODED:
+ return fromDecoded(parcel.readString());
+ default:
+ throw new AssertionError();
+ }
+ }
+
+ /**
+ * Creates a path from the encoded string.
+ *
+ * @param encoded part string
+ */
+ static PathPart fromEncoded(String encoded) {
+ return from(encoded, NOT_CACHED);
+ }
+
+ /**
+ * Creates a path from the decoded string.
+ *
+ * @param decoded part string
+ */
+ static PathPart fromDecoded(String decoded) {
+ return from(NOT_CACHED, decoded);
+ }
+
+ /**
+ * Creates a path from the encoded and decoded strings.
+ *
+ * @param encoded part string
+ * @param decoded part string
+ */
+ static PathPart from(String encoded, String decoded) {
+ if (encoded == null) {
+ return NULL;
+ }
+
+ if (encoded.length() == 0) {
+ return EMPTY;
+ }
+
+ return new PathPart(encoded, decoded);
+ }
+
+ /**
+ * Prepends path values with "/" if they're present, not empty, and
+ * they don't already start with "/".
+ */
+ static PathPart makeAbsolute(PathPart oldPart) {
+ @SuppressWarnings("StringEquality")
+ boolean encodedCached = oldPart.encoded != NOT_CACHED;
+
+ // We don't care which version we use, and we don't want to force
+ // unneccessary encoding/decoding.
+ String oldPath = encodedCached ? oldPart.encoded : oldPart.decoded;
+
+ if (oldPath == null || oldPath.length() == 0
+ || oldPath.startsWith("/")) {
+ return oldPart;
+ }
+
+ // Prepend encoded string if present.
+ String newEncoded = encodedCached
+ ? "/" + oldPart.encoded : NOT_CACHED;
+
+ // Prepend decoded string if present.
+ @SuppressWarnings("StringEquality")
+ boolean decodedCached = oldPart.decoded != NOT_CACHED;
+ String newDecoded = decodedCached
+ ? "/" + oldPart.decoded
+ : NOT_CACHED;
+
+ return new PathPart(newEncoded, newDecoded);
+ }
+ }
+
+ /**
+ * Creates a new Uri by encoding and appending a path segment to a base Uri.
+ *
+ * @param baseUri Uri to append path segment to
+ * @param pathSegment to encode and append
+ * @return a new Uri based on baseUri with the given segment encoded and
+ * appended to the path
+ * @throws NullPointerException if baseUri is null
+ */
+ public static Uri withAppendedPath(Uri baseUri, String pathSegment) {
+ Builder builder = baseUri.buildUpon();
+ builder = builder.appendEncodedPath(pathSegment);
+ return builder.build();
+ }
+}
diff --git a/core/java/android/net/UrlQuerySanitizer.java b/core/java/android/net/UrlQuerySanitizer.java
new file mode 100644
index 0000000..70e50b7
--- /dev/null
+++ b/core/java/android/net/UrlQuerySanitizer.java
@@ -0,0 +1,913 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Set;
+import java.util.StringTokenizer;
+
+/**
+ *
+ * Sanitizes the Query portion of a URL. Simple example:
+ * <code>
+ * UrlQuerySanitizer sanitizer = new UrlQuerySanitizer();
+ * sanitizer.setAllowUnregisteredParamaters(true);
+ * sanitizer.parseUrl("http://example.com/?name=Joe+User");
+ * String name = sanitizer.getValue("name"));
+ * // name now contains "Joe_User"
+ * </code>
+ *
+ * Register ValueSanitizers to customize the way individual
+ * parameters are sanitized:
+ * <code>
+ * UrlQuerySanitizer sanitizer = new UrlQuerySanitizer();
+ * sanitizer.registerParamater("name", UrlQuerySanitizer.createSpaceLegal());
+ * sanitizer.parseUrl("http://example.com/?name=Joe+User");
+ * String name = sanitizer.getValue("name"));
+ * // name now contains "Joe User". (The string is first decoded, which
+ * // converts the '+' to a ' '. Then the string is sanitized, which
+ * // converts the ' ' to an '_'. (The ' ' is converted because the default
+ * unregistered parameter sanitizer does not allow any special characters,
+ * and ' ' is a special character.)
+ * </code>
+ *
+ * There are several ways to create ValueSanitizers. In order of increasing
+ * sophistication:
+ * <ol>
+ * <li>Call one of the UrlQuerySanitizer.createXXX() methods.
+ * <li>Construct your own instance of
+ * UrlQuerySanitizer.IllegalCharacterValueSanitizer.
+ * <li>Subclass UrlQuerySanitizer.ValueSanitizer to define your own value
+ * sanitizer.
+ * </ol>
+ *
+ */
+public class UrlQuerySanitizer {
+
+ /**
+ * A simple tuple that holds parameter-value pairs.
+ *
+ */
+ public class ParameterValuePair {
+ /**
+ * Construct a parameter-value tuple.
+ * @param parameter an unencoded parameter
+ * @param value an unencoded value
+ */
+ public ParameterValuePair(String parameter,
+ String value) {
+ mParameter = parameter;
+ mValue = value;
+ }
+ /**
+ * The unencoded parameter
+ */
+ public String mParameter;
+ /**
+ * The unencoded value
+ */
+ public String mValue;
+ }
+
+ final private HashMap<String, ValueSanitizer> mSanitizers =
+ new HashMap<String, ValueSanitizer>();
+ final private HashMap<String, String> mEntries =
+ new HashMap<String, String>();
+ final private ArrayList<ParameterValuePair> mEntriesList =
+ new ArrayList<ParameterValuePair>();
+ private boolean mAllowUnregisteredParamaters;
+ private boolean mPreferFirstRepeatedParameter;
+ private ValueSanitizer mUnregisteredParameterValueSanitizer =
+ getAllIllegal();
+
+ /**
+ * A functor used to sanitize a single query value.
+ *
+ */
+ public static interface ValueSanitizer {
+ /**
+ * Sanitize an unencoded value.
+ * @param value
+ * @return the sanitized unencoded value
+ */
+ public String sanitize(String value);
+ }
+
+ /**
+ * Sanitize values based on which characters they contain. Illegal
+ * characters are replaced with either space or '_', depending upon
+ * whether space is a legal character or not.
+ */
+ public static class IllegalCharacterValueSanitizer implements
+ ValueSanitizer {
+ private int mFlags;
+
+ /**
+ * Allow space (' ') characters.
+ */
+ public final static int SPACE_OK = 1 << 0;
+ /**
+ * Allow whitespace characters other than space. The
+ * other whitespace characters are
+ * '\t' '\f' '\n' '\r' and '\0x000b' (vertical tab)
+ */
+ public final static int OTHER_WHITESPACE_OK = 1 << 1;
+ /**
+ * Allow characters with character codes 128 to 255.
+ */
+ public final static int NON_7_BIT_ASCII_OK = 1 << 2;
+ /**
+ * Allow double quote characters. ('"')
+ */
+ public final static int DQUOTE_OK = 1 << 3;
+ /**
+ * Allow single quote characters. ('\'')
+ */
+ public final static int SQUOTE_OK = 1 << 4;
+ /**
+ * Allow less-than characters. ('<')
+ */
+ public final static int LT_OK = 1 << 5;
+ /**
+ * Allow greater-than characters. ('>')
+ */
+ public final static int GT_OK = 1 << 6;
+ /**
+ * Allow ampersand characters ('&')
+ */
+ public final static int AMP_OK = 1 << 7;
+ /**
+ * Allow percent-sign characters ('%')
+ */
+ public final static int PCT_OK = 1 << 8;
+ /**
+ * Allow nul characters ('\0')
+ */
+ public final static int NUL_OK = 1 << 9;
+ /**
+ * Allow text to start with a script URL
+ * such as "javascript:" or "vbscript:"
+ */
+ public final static int SCRIPT_URL_OK = 1 << 10;
+
+ /**
+ * Mask with all fields set to OK
+ */
+ public final static int ALL_OK = 0x7ff;
+
+ /**
+ * Mask with both regular space and other whitespace OK
+ */
+ public final static int ALL_WHITESPACE_OK =
+ SPACE_OK | OTHER_WHITESPACE_OK;
+
+
+ // Common flag combinations:
+
+ /**
+ * <ul>
+ * <li>Deny all special characters.
+ * <li>Deny script URLs.
+ * </ul>
+ */
+ public final static int ALL_ILLEGAL =
+ 0;
+ /**
+ * <ul>
+ * <li>Allow all special characters except Nul. ('\0').
+ * <li>Allow script URLs.
+ * </ul>
+ */
+ public final static int ALL_BUT_NUL_LEGAL =
+ ALL_OK & ~NUL_OK;
+ /**
+ * <ul>
+ * <li>Allow all special characters except for:
+ * <ul>
+ * <li>whitespace characters
+ * <li>Nul ('\0')
+ * </ul>
+ * <li>Allow script URLs.
+ * </ul>
+ */
+ public final static int ALL_BUT_WHITESPACE_LEGAL =
+ ALL_OK & ~(ALL_WHITESPACE_OK | NUL_OK);
+ /**
+ * <ul>
+ * <li>Allow characters used by encoded URLs.
+ * <li>Deny script URLs.
+ * </ul>
+ */
+ public final static int URL_LEGAL =
+ NON_7_BIT_ASCII_OK | SQUOTE_OK | AMP_OK | PCT_OK;
+ /**
+ * <ul>
+ * <li>Allow characters used by encoded URLs.
+ * <li>Allow spaces.
+ * <li>Deny script URLs.
+ * </ul>
+ */
+ public final static int URL_AND_SPACE_LEGAL =
+ URL_LEGAL | SPACE_OK;
+ /**
+ * <ul>
+ * <li>Allow ampersand.
+ * <li>Deny script URLs.
+ * </ul>
+ */
+ public final static int AMP_LEGAL =
+ AMP_OK;
+ /**
+ * <ul>
+ * <li>Allow ampersand.
+ * <li>Allow space.
+ * <li>Deny script URLs.
+ * </ul>
+ */
+ public final static int AMP_AND_SPACE_LEGAL =
+ AMP_OK | SPACE_OK;
+ /**
+ * <ul>
+ * <li>Allow space.
+ * <li>Deny script URLs.
+ * </ul>
+ */
+ public final static int SPACE_LEGAL =
+ SPACE_OK;
+ /**
+ * <ul>
+ * <li>Allow all but.
+ * <ul>
+ * <li>Nul ('\0')
+ * <li>Angle brackets ('<', '>')
+ * </ul>
+ * <li>Deny script URLs.
+ * </ul>
+ */
+ public final static int ALL_BUT_NUL_AND_ANGLE_BRACKETS_LEGAL =
+ ALL_OK & ~(NUL_OK | LT_OK | GT_OK);
+
+ /**
+ * Script URL definitions
+ */
+
+ private final static String JAVASCRIPT_PREFIX = "javascript:";
+
+ private final static String VBSCRIPT_PREFIX = "vbscript:";
+
+ private final static int MIN_SCRIPT_PREFIX_LENGTH = Math.min(
+ JAVASCRIPT_PREFIX.length(), VBSCRIPT_PREFIX.length());
+
+ /**
+ * Construct a sanitizer. The parameters set the behavior of the
+ * sanitizer.
+ * @param flags some combination of the XXX_OK flags.
+ */
+ public IllegalCharacterValueSanitizer(
+ int flags) {
+ mFlags = flags;
+ }
+ /**
+ * Sanitize a value.
+ * <ol>
+ * <li>If script URLs are not OK, the will be removed.
+ * <li>If neither spaces nor other white space is OK, then
+ * white space will be trimmed from the beginning and end of
+ * the URL. (Just the actual white space characters are trimmed, not
+ * other control codes.)
+ * <li> Illegal characters will be replaced with
+ * either ' ' or '_', depending on whether a space is itself a
+ * legal character.
+ * </ol>
+ * @param value
+ * @return the sanitized value
+ */
+ public String sanitize(String value) {
+ if (value == null) {
+ return null;
+ }
+ int length = value.length();
+ if ((mFlags & SCRIPT_URL_OK) != 0) {
+ if (length >= MIN_SCRIPT_PREFIX_LENGTH) {
+ String asLower = value.toLowerCase();
+ if (asLower.startsWith(JAVASCRIPT_PREFIX) ||
+ asLower.startsWith(VBSCRIPT_PREFIX)) {
+ return "";
+ }
+ }
+ }
+
+ // If whitespace isn't OK, get rid of whitespace at beginning
+ // and end of value.
+ if ( (mFlags & ALL_WHITESPACE_OK) == 0) {
+ value = trimWhitespace(value);
+ // The length could have changed, so we need to correct
+ // the length variable.
+ length = value.length();
+ }
+
+ StringBuilder stringBuilder = new StringBuilder(length);
+ for(int i = 0; i < length; i++) {
+ char c = value.charAt(i);
+ if (!characterIsLegal(c)) {
+ if ((mFlags & SPACE_OK) != 0) {
+ c = ' ';
+ }
+ else {
+ c = '_';
+ }
+ }
+ stringBuilder.append(c);
+ }
+ return stringBuilder.toString();
+ }
+
+ /**
+ * Trim whitespace from the beginning and end of a string.
+ * <p>
+ * Note: can't use {@link String#trim} because {@link String#trim} has a
+ * different definition of whitespace than we want.
+ * @param value the string to trim
+ * @return the trimmed string
+ */
+ private String trimWhitespace(String value) {
+ int start = 0;
+ int last = value.length() - 1;
+ int end = last;
+ while (start <= end && isWhitespace(value.charAt(start))) {
+ start++;
+ }
+ while (end >= start && isWhitespace(value.charAt(end))) {
+ end--;
+ }
+ if (start == 0 && end == last) {
+ return value;
+ }
+ return value.substring(start, end + 1);
+ }
+
+ /**
+ * Check if c is whitespace.
+ * @param c character to test
+ * @return true if c is a whitespace character
+ */
+ private boolean isWhitespace(char c) {
+ switch(c) {
+ case ' ':
+ case '\t':
+ case '\f':
+ case '\n':
+ case '\r':
+ case 11: /* VT */
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Check whether an individual character is legal. Uses the
+ * flag bit-set passed into the constructor.
+ * @param c
+ * @return true if c is a legal character
+ */
+ private boolean characterIsLegal(char c) {
+ switch(c) {
+ case ' ' : return (mFlags & SPACE_OK) != 0;
+ case '\t': case '\f': case '\n': case '\r': case 11: /* VT */
+ return (mFlags & OTHER_WHITESPACE_OK) != 0;
+ case '\"': return (mFlags & DQUOTE_OK) != 0;
+ case '\'': return (mFlags & SQUOTE_OK) != 0;
+ case '<' : return (mFlags & LT_OK) != 0;
+ case '>' : return (mFlags & GT_OK) != 0;
+ case '&' : return (mFlags & AMP_OK) != 0;
+ case '%' : return (mFlags & PCT_OK) != 0;
+ case '\0': return (mFlags & NUL_OK) != 0;
+ default : return (c >= 32 && c < 127) ||
+ (c >= 128 && c <= 255 && ((mFlags & NON_7_BIT_ASCII_OK) != 0));
+ }
+ }
+ }
+
+ /**
+ * Get the current value sanitizer used when processing
+ * unregistered parameter values.
+ * <p>
+ * <b>Note:</b> The default unregistered parameter value sanitizer is
+ * one that doesn't allow any special characters, similar to what
+ * is returned by calling createAllIllegal.
+ *
+ * @return the current ValueSanitizer used to sanitize unregistered
+ * parameter values.
+ */
+ public ValueSanitizer getUnregisteredParameterValueSanitizer() {
+ return mUnregisteredParameterValueSanitizer;
+ }
+
+ /**
+ * Set the value sanitizer used when processing unregistered
+ * parameter values.
+ * @param sanitizer set the ValueSanitizer used to sanitize unregistered
+ * parameter values.
+ */
+ public void setUnregisteredParameterValueSanitizer(
+ ValueSanitizer sanitizer) {
+ mUnregisteredParameterValueSanitizer = sanitizer;
+ }
+
+
+ // Private fields for singleton sanitizers:
+
+ private static final ValueSanitizer sAllIllegal =
+ new IllegalCharacterValueSanitizer(
+ IllegalCharacterValueSanitizer.ALL_ILLEGAL);
+
+ private static final ValueSanitizer sAllButNulLegal =
+ new IllegalCharacterValueSanitizer(
+ IllegalCharacterValueSanitizer.ALL_BUT_NUL_LEGAL);
+
+ private static final ValueSanitizer sAllButWhitespaceLegal =
+ new IllegalCharacterValueSanitizer(
+ IllegalCharacterValueSanitizer.ALL_BUT_WHITESPACE_LEGAL);
+
+ private static final ValueSanitizer sURLLegal =
+ new IllegalCharacterValueSanitizer(
+ IllegalCharacterValueSanitizer.URL_LEGAL);
+
+ private static final ValueSanitizer sUrlAndSpaceLegal =
+ new IllegalCharacterValueSanitizer(
+ IllegalCharacterValueSanitizer.URL_AND_SPACE_LEGAL);
+
+ private static final ValueSanitizer sAmpLegal =
+ new IllegalCharacterValueSanitizer(
+ IllegalCharacterValueSanitizer.AMP_LEGAL);
+
+ private static final ValueSanitizer sAmpAndSpaceLegal =
+ new IllegalCharacterValueSanitizer(
+ IllegalCharacterValueSanitizer.AMP_AND_SPACE_LEGAL);
+
+ private static final ValueSanitizer sSpaceLegal =
+ new IllegalCharacterValueSanitizer(
+ IllegalCharacterValueSanitizer.SPACE_LEGAL);
+
+ private static final ValueSanitizer sAllButNulAndAngleBracketsLegal =
+ new IllegalCharacterValueSanitizer(
+ IllegalCharacterValueSanitizer.ALL_BUT_NUL_AND_ANGLE_BRACKETS_LEGAL);
+
+ /**
+ * Return a value sanitizer that does not allow any special characters,
+ * and also does not allow script URLs.
+ * @return a value sanitizer
+ */
+ public static final ValueSanitizer getAllIllegal() {
+ return sAllIllegal;
+ }
+
+ /**
+ * Return a value sanitizer that allows everything except Nul ('\0')
+ * characters. Script URLs are allowed.
+ * @return a value sanitizer
+ */
+ public static final ValueSanitizer getAllButNulLegal() {
+ return sAllButNulLegal;
+ }
+ /**
+ * Return a value sanitizer that allows everything except Nul ('\0')
+ * characters, space (' '), and other whitespace characters.
+ * Script URLs are allowed.
+ * @return a value sanitizer
+ */
+ public static final ValueSanitizer getAllButWhitespaceLegal() {
+ return sAllButWhitespaceLegal;
+ }
+ /**
+ * Return a value sanitizer that allows all the characters used by
+ * encoded URLs. Does not allow script URLs.
+ * @return a value sanitizer
+ */
+ public static final ValueSanitizer getUrlLegal() {
+ return sURLLegal;
+ }
+ /**
+ * Return a value sanitizer that allows all the characters used by
+ * encoded URLs and allows spaces, which are not technically legal
+ * in encoded URLs, but commonly appear anyway.
+ * Does not allow script URLs.
+ * @return a value sanitizer
+ */
+ public static final ValueSanitizer getUrlAndSpaceLegal() {
+ return sUrlAndSpaceLegal;
+ }
+ /**
+ * Return a value sanitizer that does not allow any special characters
+ * except ampersand ('&'). Does not allow script URLs.
+ * @return a value sanitizer
+ */
+ public static final ValueSanitizer getAmpLegal() {
+ return sAmpLegal;
+ }
+ /**
+ * Return a value sanitizer that does not allow any special characters
+ * except ampersand ('&') and space (' '). Does not allow script URLs.
+ * @return a value sanitizer
+ */
+ public static final ValueSanitizer getAmpAndSpaceLegal() {
+ return sAmpAndSpaceLegal;
+ }
+ /**
+ * Return a value sanitizer that does not allow any special characters
+ * except space (' '). Does not allow script URLs.
+ * @return a value sanitizer
+ */
+ public static final ValueSanitizer getSpaceLegal() {
+ return sSpaceLegal;
+ }
+ /**
+ * Return a value sanitizer that allows any special characters
+ * except angle brackets ('<' and '>') and Nul ('\0').
+ * Allows script URLs.
+ * @return a value sanitizer
+ */
+ public static final ValueSanitizer getAllButNulAndAngleBracketsLegal() {
+ return sAllButNulAndAngleBracketsLegal;
+ }
+
+ /**
+ * Constructs a UrlQuerySanitizer.
+ * <p>
+ * Defaults:
+ * <ul>
+ * <li>unregistered parameters are not allowed.
+ * <li>the last instance of a repeated parameter is preferred.
+ * <li>The default value sanitizer is an AllIllegal value sanitizer.
+ * <ul>
+ */
+ public UrlQuerySanitizer() {
+ }
+
+ /**
+ * Constructs a UrlQuerySanitizer and parse a URL.
+ * This constructor is provided for convenience when the
+ * default parsing behavior is acceptable.
+ * <p>
+ * Because the URL is parsed before the constructor returns, there isn't
+ * a chance to configure the sanitizer to change the parsing behavior.
+ * <p>
+ * <code>
+ * UrlQuerySanitizer sanitizer = new UrlQuerySanitizer(myUrl);
+ * String name = sanitizer.getValue("name");
+ * </code>
+ * <p>
+ * Defaults:
+ * <ul>
+ * <li>unregistered parameters <em>are</em> allowed.
+ * <li>the last instance of a repeated parameter is preferred.
+ * <li>The default value sanitizer is an AllIllegal value sanitizer.
+ * <ul>
+ */
+ public UrlQuerySanitizer(String url) {
+ setAllowUnregisteredParamaters(true);
+ parseUrl(url);
+ }
+
+ /**
+ * Parse the query parameters out of an encoded URL.
+ * Works by extracting the query portion from the URL and then
+ * calling parseQuery(). If there is no query portion it is
+ * treated as if the query portion is an empty string.
+ * @param url the encoded URL to parse.
+ */
+ public void parseUrl(String url) {
+ int queryIndex = url.indexOf('?');
+ String query;
+ if (queryIndex >= 0) {
+ query = url.substring(queryIndex + 1);
+ }
+ else {
+ query = "";
+ }
+ parseQuery(query);
+ }
+
+ /**
+ * Parse a query. A query string is any number of parameter-value clauses
+ * separated by any non-zero number of ampersands. A parameter-value clause
+ * is a parameter followed by an equal sign, followed by a value. If the
+ * equal sign is missing, the value is assumed to be the empty string.
+ * @param query the query to parse.
+ */
+ public void parseQuery(String query) {
+ clear();
+ // Split by '&'
+ StringTokenizer tokenizer = new StringTokenizer(query, "&");
+ while(tokenizer.hasMoreElements()) {
+ String attributeValuePair = tokenizer.nextToken();
+ if (attributeValuePair.length() > 0) {
+ int assignmentIndex = attributeValuePair.indexOf('=');
+ if (assignmentIndex < 0) {
+ // No assignment found, treat as if empty value
+ parseEntry(attributeValuePair, "");
+ }
+ else {
+ parseEntry(attributeValuePair.substring(0, assignmentIndex),
+ attributeValuePair.substring(assignmentIndex + 1));
+ }
+ }
+ }
+ }
+
+ /**
+ * Get a set of all of the parameters found in the sanitized query.
+ * <p>
+ * Note: Do not modify this set. Treat it as a read-only set.
+ * @return all the parameters found in the current query.
+ */
+ public Set<String> getParameterSet() {
+ return mEntries.keySet();
+ }
+
+ /**
+ * An array list of all of the parameter value pairs in the sanitized
+ * query, in the order they appeared in the query. May contain duplicate
+ * parameters.
+ * <p class="note"><b>Note:</b> Do not modify this list. Treat it as a read-only list.</p>
+ */
+ public List<ParameterValuePair> getParameterList() {
+ return mEntriesList;
+ }
+
+ /**
+ * Check if a parameter exists in the current sanitized query.
+ * @param parameter the unencoded name of a parameter.
+ * @return true if the paramater exists in the current sanitized queary.
+ */
+ public boolean hasParameter(String parameter) {
+ return mEntries.containsKey(parameter);
+ }
+
+ /**
+ * Get the value for a parameter in the current sanitized query.
+ * Returns null if the parameter does not
+ * exit.
+ * @param parameter the unencoded name of a parameter.
+ * @return the sanitized unencoded value of the parameter,
+ * or null if the parameter does not exist.
+ */
+ public String getValue(String parameter) {
+ return mEntries.get(parameter);
+ }
+
+ /**
+ * Register a value sanitizer for a particular parameter. Can also be used
+ * to replace or remove an already-set value sanitizer.
+ * <p>
+ * Registering a non-null value sanitizer for a particular parameter
+ * makes that parameter a registered parameter.
+ * @param parameter an unencoded parameter name
+ * @param valueSanitizer the value sanitizer to use for a particular
+ * parameter. May be null in order to unregister that parameter.
+ * @see #getAllowUnregisteredParamaters()
+ */
+ public void registerParameter(String parameter,
+ ValueSanitizer valueSanitizer) {
+ if (valueSanitizer == null) {
+ mSanitizers.remove(parameter);
+ }
+ mSanitizers.put(parameter, valueSanitizer);
+ }
+
+ /**
+ * Register a value sanitizer for an array of parameters.
+ * @param parameters An array of unencoded parameter names.
+ * @param valueSanitizer
+ * @see #registerParameter
+ */
+ public void registerParameters(String[] parameters,
+ ValueSanitizer valueSanitizer) {
+ int length = parameters.length;
+ for(int i = 0; i < length; i++) {
+ mSanitizers.put(parameters[i], valueSanitizer);
+ }
+ }
+
+ /**
+ * Set whether or not unregistered parameters are allowed. If they
+ * are not allowed, then they will be dropped when a query is sanitized.
+ * <p>
+ * Defaults to false.
+ * @param allowUnregisteredParamaters true to allow unregistered parameters.
+ * @see #getAllowUnregisteredParamaters()
+ */
+ public void setAllowUnregisteredParamaters(
+ boolean allowUnregisteredParamaters) {
+ mAllowUnregisteredParamaters = allowUnregisteredParamaters;
+ }
+
+ /**
+ * Get whether or not unregistered parameters are allowed. If not
+ * allowed, they will be dropped when a query is parsed.
+ * @return true if unregistered parameters are allowed.
+ * @see #setAllowUnregisteredParamaters(boolean)
+ */
+ public boolean getAllowUnregisteredParamaters() {
+ return mAllowUnregisteredParamaters;
+ }
+
+ /**
+ * Set whether or not the first occurrence of a repeated parameter is
+ * preferred. True means the first repeated parameter is preferred.
+ * False means that the last repeated parameter is preferred.
+ * <p>
+ * The preferred parameter is the one that is returned when getParameter
+ * is called.
+ * <p>
+ * defaults to false.
+ * @param preferFirstRepeatedParameter True if the first repeated
+ * parameter is preferred.
+ * @see #getPreferFirstRepeatedParameter()
+ */
+ public void setPreferFirstRepeatedParameter(
+ boolean preferFirstRepeatedParameter) {
+ mPreferFirstRepeatedParameter = preferFirstRepeatedParameter;
+ }
+
+ /**
+ * Get whether or not the first occurrence of a repeated parameter is
+ * preferred.
+ * @return true if the first occurrence of a repeated parameter is
+ * preferred.
+ * @see #setPreferFirstRepeatedParameter(boolean)
+ */
+ public boolean getPreferFirstRepeatedParameter() {
+ return mPreferFirstRepeatedParameter;
+ }
+
+ /**
+ * Parse an escaped parameter-value pair. The default implementation
+ * unescapes both the parameter and the value, then looks up the
+ * effective value sanitizer for the parameter and uses it to sanitize
+ * the value. If all goes well then addSanitizedValue is called with
+ * the unescaped parameter and the sanitized unescaped value.
+ * @param parameter an escaped parameter
+ * @param value an unsanitzied escaped value
+ */
+ protected void parseEntry(String parameter, String value) {
+ String unescapedParameter = unescape(parameter);
+ ValueSanitizer valueSanitizer =
+ getEffectiveValueSanitizer(unescapedParameter);
+
+ if (valueSanitizer == null) {
+ return;
+ }
+ String unescapedValue = unescape(value);
+ String sanitizedValue = valueSanitizer.sanitize(unescapedValue);
+ addSanitizedEntry(unescapedParameter, sanitizedValue);
+ }
+
+ /**
+ * Record a sanitized parameter-value pair. Override if you want to
+ * do additional filtering or validation.
+ * @param parameter an unescaped parameter
+ * @param value a sanitized unescaped value
+ */
+ protected void addSanitizedEntry(String parameter, String value) {
+ mEntriesList.add(
+ new ParameterValuePair(parameter, value));
+ if (mPreferFirstRepeatedParameter) {
+ if (mEntries.containsKey(parameter)) {
+ return;
+ }
+ }
+ mEntries.put(parameter, value);
+ }
+
+ /**
+ * Get the value sanitizer for a parameter. Returns null if there
+ * is no value sanitizer registered for the parameter.
+ * @param parameter the unescaped parameter
+ * @return the currently registered value sanitizer for this parameter.
+ * @see #registerParameter(String, android.net.UrlQuerySanitizer.ValueSanitizer)
+ */
+ public ValueSanitizer getValueSanitizer(String parameter) {
+ return mSanitizers.get(parameter);
+ }
+
+ /**
+ * Get the effective value sanitizer for a parameter. Like getValueSanitizer,
+ * except if there is no value sanitizer registered for a parameter, and
+ * unregistered paramaters are allowed, then the default value sanitizer is
+ * returned.
+ * @param parameter an unescaped parameter
+ * @return the effective value sanitizer for a parameter.
+ */
+ public ValueSanitizer getEffectiveValueSanitizer(String parameter) {
+ ValueSanitizer sanitizer = getValueSanitizer(parameter);
+ if (sanitizer == null && mAllowUnregisteredParamaters) {
+ sanitizer = getUnregisteredParameterValueSanitizer();
+ }
+ return sanitizer;
+ }
+
+ /**
+ * Unescape an escaped string.
+ * <ul>
+ * <li>'+' characters are replaced by
+ * ' ' characters.
+ * <li>Valid "%xx" escape sequences are replaced by the
+ * corresponding unescaped character.
+ * <li>Invalid escape sequences such as %1z", are passed through unchanged.
+ * <ol>
+ * @param string the escaped string
+ * @return the unescaped string.
+ */
+ public String unescape(String string) {
+ // Early exit if no escaped characters.
+ int firstEscape = string.indexOf('%');
+ if ( firstEscape < 0) {
+ firstEscape = string.indexOf('+');
+ if (firstEscape < 0) {
+ return string;
+ }
+ }
+
+ int length = string.length();
+
+ StringBuilder stringBuilder = new StringBuilder(length);
+ stringBuilder.append(string.substring(0, firstEscape));
+ for (int i = firstEscape; i < length; i++) {
+ char c = string.charAt(i);
+ if (c == '+') {
+ c = ' ';
+ }
+ else if ( c == '%' && i + 2 < length) {
+ char c1 = string.charAt(i + 1);
+ char c2 = string.charAt(i + 2);
+ if (isHexDigit(c1) && isHexDigit(c2)) {
+ c = (char) (decodeHexDigit(c1) * 16 + decodeHexDigit(c2));
+ i += 2;
+ }
+ }
+ stringBuilder.append(c);
+ }
+ return stringBuilder.toString();
+ }
+
+ /**
+ * Test if a character is a hexidecimal digit. Both upper case and lower
+ * case hex digits are allowed.
+ * @param c the character to test
+ * @return true if c is a hex digit.
+ */
+ protected boolean isHexDigit(char c) {
+ return decodeHexDigit(c) >= 0;
+ }
+
+ /**
+ * Convert a character that represents a hexidecimal digit into an integer.
+ * If the character is not a hexidecimal digit, then -1 is returned.
+ * Both upper case and lower case hex digits are allowed.
+ * @param c the hexidecimal digit.
+ * @return the integer value of the hexidecimal digit.
+ */
+
+ protected int decodeHexDigit(char c) {
+ if (c >= '0' && c <= '9') {
+ return c - '0';
+ }
+ else if (c >= 'A' && c <= 'F') {
+ return c - 'A' + 10;
+ }
+ else if (c >= 'a' && c <= 'f') {
+ return c - 'a' + 10;
+ }
+ else {
+ return -1;
+ }
+ }
+
+ /**
+ * Clear the existing entries. Called to get ready to parse a new
+ * query string.
+ */
+ protected void clear() {
+ mEntries.clear();
+ mEntriesList.clear();
+ }
+}
+
diff --git a/core/java/android/net/WebAddress.java b/core/java/android/net/WebAddress.java
new file mode 100644
index 0000000..f4a2a6a
--- /dev/null
+++ b/core/java/android/net/WebAddress.java
@@ -0,0 +1,134 @@
+/*
+ * 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.net;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * {@hide}
+ *
+ * Web Address Parser
+ *
+ * This is called WebAddress, rather than URL or URI, because it
+ * attempts to parse the stuff that a user will actually type into a
+ * browser address widget.
+ *
+ * Unlike java.net.uri, this parser will not choke on URIs missing
+ * schemes. It will only throw a ParseException if the input is
+ * really hosed.
+ *
+ * If given an https scheme but no port, fills in port
+ *
+ */
+public class WebAddress {
+
+ private final static String LOGTAG = "http";
+
+ public String mScheme;
+ public String mHost;
+ public int mPort;
+ public String mPath;
+ public String mAuthInfo;
+
+ static final int MATCH_GROUP_SCHEME = 1;
+ static final int MATCH_GROUP_AUTHORITY = 2;
+ static final int MATCH_GROUP_HOST = 3;
+ static final int MATCH_GROUP_PORT = 4;
+ static final int MATCH_GROUP_PATH = 5;
+
+ static Pattern sAddressPattern = Pattern.compile(
+ /* scheme */ "(?:(http|HTTP|https|HTTPS|file|FILE)\\:\\/\\/)?" +
+ /* authority */ "(?:([-A-Za-z0-9$_.+!*'(),;?&=]+(?:\\:[-A-Za-z0-9$_.+!*'(),;?&=]+)?)@)?" +
+ /* host */ "([-A-Za-z0-9%]+(?:\\.[-A-Za-z0-9%]+)*)?" +
+ /* port */ "(?:\\:([0-9]+))?" +
+ /* path */ "(\\/?.*)?");
+
+ /** parses given uriString. */
+ public WebAddress(String address) throws ParseException {
+ if (address == null) {
+ throw new NullPointerException();
+ }
+
+ // android.util.Log.d(LOGTAG, "WebAddress: " + address);
+
+ mScheme = "";
+ mHost = "";
+ mPort = -1;
+ mPath = "/";
+ mAuthInfo = "";
+
+ Matcher m = sAddressPattern.matcher(address);
+ String t;
+ if (m.matches()) {
+ t = m.group(MATCH_GROUP_SCHEME);
+ if (t != null) mScheme = t;
+ t = m.group(MATCH_GROUP_AUTHORITY);
+ if (t != null) mAuthInfo = t;
+ t = m.group(MATCH_GROUP_HOST);
+ if (t != null) mHost = t;
+ t = m.group(MATCH_GROUP_PORT);
+ if (t != null) {
+ try {
+ mPort = Integer.parseInt(t);
+ } catch (NumberFormatException ex) {
+ throw new ParseException("Bad port");
+ }
+ }
+ t = m.group(MATCH_GROUP_PATH);
+ if (t != null && t.length() > 0) {
+ /* handle busted myspace frontpage redirect with
+ missing initial "/" */
+ if (t.charAt(0) == '/') {
+ mPath = t;
+ } else {
+ mPath = "/" + t;
+ }
+ }
+
+ } else {
+ // nothing found... outa here
+ throw new ParseException("Bad address");
+ }
+
+ /* Get port from scheme or scheme from port, if necessary and
+ possible */
+ if (mPort == 443 && mScheme.equals("")) {
+ mScheme = "https";
+ } else if (mPort == -1) {
+ if (mScheme.equals("https"))
+ mPort = 443;
+ else
+ mPort = 80; // default
+ }
+ if (mScheme.equals("")) mScheme = "http";
+ }
+
+ public String toString() {
+ String port = "";
+ if ((mPort != 443 && mScheme.equals("https")) ||
+ (mPort != 80 && mScheme.equals("http"))) {
+ port = ":" + Integer.toString(mPort);
+ }
+ String authInfo = "";
+ if (mAuthInfo.length() > 0) {
+ authInfo = mAuthInfo + "@";
+ }
+
+ return mScheme + "://" + authInfo + mHost + port + mPath;
+ }
+}
diff --git a/core/java/android/net/http/AndroidHttpClient.java b/core/java/android/net/http/AndroidHttpClient.java
new file mode 100644
index 0000000..01442ae
--- /dev/null
+++ b/core/java/android/net/http/AndroidHttpClient.java
@@ -0,0 +1,452 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.http;
+
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpEntityEnclosingRequest;
+import org.apache.http.HttpException;
+import org.apache.http.HttpHost;
+import org.apache.http.HttpRequest;
+import org.apache.http.HttpRequestInterceptor;
+import org.apache.http.HttpResponse;
+import org.apache.http.entity.AbstractHttpEntity;
+import org.apache.http.entity.ByteArrayEntity;
+import org.apache.http.client.CookieStore;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.ResponseHandler;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.protocol.ClientContext;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.params.HttpClientParams;
+import org.apache.http.conn.ClientConnectionManager;
+import org.apache.http.conn.scheme.PlainSocketFactory;
+import org.apache.http.conn.scheme.Scheme;
+import org.apache.http.conn.scheme.SchemeRegistry;
+import org.apache.http.conn.ssl.SSLSocketFactory;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.impl.client.RequestWrapper;
+import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
+import org.apache.http.params.BasicHttpParams;
+import org.apache.http.params.HttpConnectionParams;
+import org.apache.http.params.HttpParams;
+import org.apache.http.params.HttpProtocolParams;
+import org.apache.http.protocol.BasicHttpProcessor;
+import org.apache.http.protocol.HttpContext;
+import org.apache.http.protocol.BasicHttpContext;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.OutputStream;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.GZIPOutputStream;
+import java.net.URI;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import android.util.Log;
+import android.content.ContentResolver;
+import android.provider.Settings;
+import android.text.TextUtils;
+
+/**
+ * Subclass of the Apache {@link DefaultHttpClient} that is configured with
+ * reasonable default settings and registered schemes for Android, and
+ * also lets the user add {@link HttpRequestInterceptor} classes.
+ * Don't create this directly, use the {@link #newInstance} factory method.
+ *
+ * <p>This client processes cookies but does not retain them by default.
+ * To retain cookies, simply add a cookie store to the HttpContext:</p>
+ *
+ * <pre>context.setAttribute(ClientContext.COOKIE_STORE, cookieStore);</pre>
+ *
+ * {@hide}
+ */
+public final class AndroidHttpClient implements HttpClient {
+
+ // Gzip of data shorter than this probably won't be worthwhile
+ public static long DEFAULT_SYNC_MIN_GZIP_BYTES = 256;
+
+ private static final String TAG = "AndroidHttpClient";
+
+
+ /** Set if HTTP requests are blocked from being executed on this thread */
+ private static final ThreadLocal<Boolean> sThreadBlocked =
+ new ThreadLocal<Boolean>();
+
+ /** Interceptor throws an exception if the executing thread is blocked */
+ private static final HttpRequestInterceptor sThreadCheckInterceptor =
+ new HttpRequestInterceptor() {
+ public void process(HttpRequest request, HttpContext context) {
+ if (sThreadBlocked.get() != null && sThreadBlocked.get()) {
+ throw new RuntimeException("This thread forbids HTTP requests");
+ }
+ }
+ };
+
+ /**
+ * Create a new HttpClient with reasonable defaults (which you can update).
+ * @param userAgent to report in your HTTP requests.
+ * @return AndroidHttpClient for you to use for all your requests.
+ */
+ public static AndroidHttpClient newInstance(String userAgent) {
+ HttpParams params = new BasicHttpParams();
+
+ // Turn off stale checking. Our connections break all the time anyway,
+ // and it's not worth it to pay the penalty of checking every time.
+ HttpConnectionParams.setStaleCheckingEnabled(params, false);
+
+ // Default connection and socket timeout of 20 seconds. Tweak to taste.
+ HttpConnectionParams.setConnectionTimeout(params, 20 * 1000);
+ HttpConnectionParams.setSoTimeout(params, 20 * 1000);
+ HttpConnectionParams.setSocketBufferSize(params, 8192);
+
+ // Don't handle redirects -- return them to the caller. Our code
+ // often wants to re-POST after a redirect, which we must do ourselves.
+ HttpClientParams.setRedirecting(params, false);
+
+ // Set the specified user agent and register standard protocols.
+ HttpProtocolParams.setUserAgent(params, userAgent);
+ SchemeRegistry schemeRegistry = new SchemeRegistry();
+ schemeRegistry.register(new Scheme("http",
+ PlainSocketFactory.getSocketFactory(), 80));
+ schemeRegistry.register(new Scheme("https",
+ SSLSocketFactory.getSocketFactory(), 443));
+ ClientConnectionManager manager =
+ new ThreadSafeClientConnManager(params, schemeRegistry);
+
+ // We use a factory method to modify superclass initialization
+ // parameters without the funny call-a-static-method dance.
+ return new AndroidHttpClient(manager, params);
+ }
+
+ private final HttpClient delegate;
+
+ private RuntimeException mLeakedException = new IllegalStateException(
+ "AndroidHttpClient created and never closed");
+
+ private AndroidHttpClient(ClientConnectionManager ccm, HttpParams params) {
+ this.delegate = new DefaultHttpClient(ccm, params) {
+ @Override
+ protected BasicHttpProcessor createHttpProcessor() {
+ // Add interceptor to prevent making requests from main thread.
+ BasicHttpProcessor processor = super.createHttpProcessor();
+ processor.addRequestInterceptor(sThreadCheckInterceptor);
+ processor.addRequestInterceptor(new CurlLogger());
+
+ return processor;
+ }
+
+ @Override
+ protected HttpContext createHttpContext() {
+ // Same as DefaultHttpClient.createHttpContext() minus the
+ // cookie store.
+ HttpContext context = new BasicHttpContext();
+ context.setAttribute(
+ ClientContext.AUTHSCHEME_REGISTRY,
+ getAuthSchemes());
+ context.setAttribute(
+ ClientContext.COOKIESPEC_REGISTRY,
+ getCookieSpecs());
+ context.setAttribute(
+ ClientContext.CREDS_PROVIDER,
+ getCredentialsProvider());
+ return context;
+ }
+ };
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ super.finalize();
+ if (mLeakedException != null) {
+ Log.e(TAG, "Leak found", mLeakedException);
+ mLeakedException = null;
+ }
+ }
+
+ /**
+ * Block this thread from executing HTTP requests.
+ * Used to guard against HTTP requests blocking the main application thread.
+ * @param blocked if HTTP requests run on this thread should be denied
+ */
+ public static void setThreadBlocked(boolean blocked) {
+ sThreadBlocked.set(blocked);
+ }
+
+ /**
+ * Modifies a request to indicate to the server that we would like a
+ * gzipped response. (Uses the "Accept-Encoding" HTTP header.)
+ * @param request the request to modify
+ * @see #getUngzippedContent
+ */
+ public static void modifyRequestToAcceptGzipResponse(HttpRequest request) {
+ request.addHeader("Accept-Encoding", "gzip");
+ }
+
+ /**
+ * Gets the input stream from a response entity. If the entity is gzipped
+ * then this will get a stream over the uncompressed data.
+ *
+ * @param entity the entity whose content should be read
+ * @return the input stream to read from
+ * @throws IOException
+ */
+ public static InputStream getUngzippedContent(HttpEntity entity)
+ throws IOException {
+ InputStream responseStream = entity.getContent();
+ if (responseStream == null) return responseStream;
+ Header header = entity.getContentEncoding();
+ if (header == null) return responseStream;
+ String contentEncoding = header.getValue();
+ if (contentEncoding == null) return responseStream;
+ if (contentEncoding.contains("gzip")) responseStream
+ = new GZIPInputStream(responseStream);
+ return responseStream;
+ }
+
+ /**
+ * Release resources associated with this client. You must call this,
+ * or significant resources (sockets and memory) may be leaked.
+ */
+ public void close() {
+ if (mLeakedException != null) {
+ getConnectionManager().shutdown();
+ mLeakedException = null;
+ }
+ }
+
+ public HttpParams getParams() {
+ return delegate.getParams();
+ }
+
+ public ClientConnectionManager getConnectionManager() {
+ return delegate.getConnectionManager();
+ }
+
+ public HttpResponse execute(HttpUriRequest request) throws IOException {
+ return delegate.execute(request);
+ }
+
+ public HttpResponse execute(HttpUriRequest request, HttpContext context)
+ throws IOException {
+ return delegate.execute(request, context);
+ }
+
+ public HttpResponse execute(HttpHost target, HttpRequest request)
+ throws IOException {
+ return delegate.execute(target, request);
+ }
+
+ public HttpResponse execute(HttpHost target, HttpRequest request,
+ HttpContext context) throws IOException {
+ return delegate.execute(target, request, context);
+ }
+
+ public <T> T execute(HttpUriRequest request,
+ ResponseHandler<? extends T> responseHandler)
+ throws IOException, ClientProtocolException {
+ return delegate.execute(request, responseHandler);
+ }
+
+ public <T> T execute(HttpUriRequest request,
+ ResponseHandler<? extends T> responseHandler, HttpContext context)
+ throws IOException, ClientProtocolException {
+ return delegate.execute(request, responseHandler, context);
+ }
+
+ public <T> T execute(HttpHost target, HttpRequest request,
+ ResponseHandler<? extends T> responseHandler) throws IOException,
+ ClientProtocolException {
+ return delegate.execute(target, request, responseHandler);
+ }
+
+ public <T> T execute(HttpHost target, HttpRequest request,
+ ResponseHandler<? extends T> responseHandler, HttpContext context)
+ throws IOException, ClientProtocolException {
+ return delegate.execute(target, request, responseHandler, context);
+ }
+
+ /**
+ * Compress data to send to server.
+ * Creates a Http Entity holding the gzipped data.
+ * The data will not be compressed if it is too short.
+ * @param data The bytes to compress
+ * @return Entity holding the data
+ */
+ public static AbstractHttpEntity getCompressedEntity(byte data[], ContentResolver resolver)
+ throws IOException {
+ AbstractHttpEntity entity;
+ if (data.length < getMinGzipSize(resolver)) {
+ entity = new ByteArrayEntity(data);
+ } else {
+ ByteArrayOutputStream arr = new ByteArrayOutputStream();
+ OutputStream zipper = new GZIPOutputStream(arr);
+ zipper.write(data);
+ zipper.close();
+ entity = new ByteArrayEntity(arr.toByteArray());
+ entity.setContentEncoding("gzip");
+ }
+ return entity;
+ }
+
+ /**
+ * Retrieves the minimum size for compressing data.
+ * Shorter data will not be compressed.
+ */
+ public static long getMinGzipSize(ContentResolver resolver) {
+ String sMinGzipBytes = Settings.Gservices.getString(resolver,
+ Settings.Gservices.SYNC_MIN_GZIP_BYTES);
+
+ if (!TextUtils.isEmpty(sMinGzipBytes)) {
+ try {
+ return Long.parseLong(sMinGzipBytes);
+ } catch (NumberFormatException nfe) {
+ Log.w(TAG, "Unable to parse " +
+ Settings.Gservices.SYNC_MIN_GZIP_BYTES + " " +
+ sMinGzipBytes, nfe);
+ }
+ }
+ return DEFAULT_SYNC_MIN_GZIP_BYTES;
+ }
+
+ /* cURL logging support. */
+
+ /**
+ * Logging tag and level.
+ */
+ private static class LoggingConfiguration {
+
+ private final String tag;
+ private final int level;
+
+ private LoggingConfiguration(String tag, int level) {
+ this.tag = tag;
+ this.level = level;
+ }
+
+ /**
+ * Returns true if logging is turned on for this configuration.
+ */
+ private boolean isLoggable() {
+ return Log.isLoggable(tag, level);
+ }
+
+ /**
+ * Prints a message using this configuration.
+ */
+ private void println(String message) {
+ Log.println(level, tag, message);
+ }
+ }
+
+ /** cURL logging configuration. */
+ private volatile LoggingConfiguration curlConfiguration;
+
+ /**
+ * Enables cURL request logging for this client.
+ *
+ * @param name to log messages with
+ * @param level at which to log messages (see {@link android.util.Log})
+ */
+ public void enableCurlLogging(String name, int level) {
+ if (name == null) {
+ throw new NullPointerException("name");
+ }
+ if (level < Log.VERBOSE || level > Log.ASSERT) {
+ throw new IllegalArgumentException("Level is out of range ["
+ + Log.VERBOSE + ".." + Log.ASSERT + "]");
+ }
+
+ curlConfiguration = new LoggingConfiguration(name, level);
+ }
+
+ /**
+ * Disables cURL logging for this client.
+ */
+ public void disableCurlLogging() {
+ curlConfiguration = null;
+ }
+
+ /**
+ * Logs cURL commands equivalent to requests.
+ */
+ private class CurlLogger implements HttpRequestInterceptor {
+ public void process(HttpRequest request, HttpContext context)
+ throws HttpException, IOException {
+ LoggingConfiguration configuration = curlConfiguration;
+ if (configuration != null
+ && configuration.isLoggable()
+ && request instanceof HttpUriRequest) {
+ configuration.println(toCurl((HttpUriRequest) request));
+ }
+ }
+ }
+
+ /**
+ * Generates a cURL command equivalent to the given request.
+ */
+ private static String toCurl(HttpUriRequest request) throws IOException {
+ StringBuilder builder = new StringBuilder();
+
+ builder.append("curl ");
+
+ for (Header header: request.getAllHeaders()) {
+ builder.append("--header \"");
+ builder.append(header.toString().trim());
+ builder.append("\" ");
+ }
+
+ URI uri = request.getURI();
+
+ // If this is a wrapped request, use the URI from the original
+ // request instead. getURI() on the wrapper seems to return a
+ // relative URI. We want an absolute URI.
+ if (request instanceof RequestWrapper) {
+ HttpRequest original = ((RequestWrapper) request).getOriginal();
+ if (original instanceof HttpUriRequest) {
+ uri = ((HttpUriRequest) original).getURI();
+ }
+ }
+
+ builder.append("\"");
+ builder.append(uri);
+ builder.append("\"");
+
+ if (request instanceof HttpEntityEnclosingRequest) {
+ HttpEntityEnclosingRequest entityRequest =
+ (HttpEntityEnclosingRequest) request;
+ HttpEntity entity = entityRequest.getEntity();
+ if (entity != null && entity.isRepeatable()) {
+ if (entity.getContentLength() < 1024) {
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ entity.writeTo(stream);
+ String entityString = stream.toString();
+
+ // TODO: Check the content type, too.
+ builder.append(" --data-ascii \"")
+ .append(entityString)
+ .append("\"");
+ } else {
+ builder.append(" [TOO MUCH DATA TO INCLUDE]");
+ }
+ }
+ }
+
+ return builder.toString();
+ }
+}
diff --git a/core/java/android/net/http/AndroidHttpClientConnection.java b/core/java/android/net/http/AndroidHttpClientConnection.java
new file mode 100644
index 0000000..eb96679
--- /dev/null
+++ b/core/java/android/net/http/AndroidHttpClientConnection.java
@@ -0,0 +1,464 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.http;
+
+import org.apache.http.Header;
+
+import org.apache.http.HttpConnection;
+import org.apache.http.HttpClientConnection;
+import org.apache.http.HttpConnectionMetrics;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpEntityEnclosingRequest;
+import org.apache.http.HttpException;
+import org.apache.http.HttpInetConnection;
+import org.apache.http.HttpRequest;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpResponseFactory;
+import org.apache.http.NoHttpResponseException;
+import org.apache.http.StatusLine;
+import org.apache.http.entity.BasicHttpEntity;
+import org.apache.http.entity.ContentLengthStrategy;
+import org.apache.http.impl.DefaultHttpResponseFactory;
+import org.apache.http.impl.HttpConnectionMetricsImpl;
+import org.apache.http.impl.entity.EntitySerializer;
+import org.apache.http.impl.entity.StrictContentLengthStrategy;
+import org.apache.http.impl.io.ChunkedInputStream;
+import org.apache.http.impl.io.ContentLengthInputStream;
+import org.apache.http.impl.io.HttpRequestWriter;
+import org.apache.http.impl.io.IdentityInputStream;
+import org.apache.http.impl.io.SocketInputBuffer;
+import org.apache.http.impl.io.SocketOutputBuffer;
+import org.apache.http.io.HttpMessageWriter;
+import org.apache.http.io.SessionInputBuffer;
+import org.apache.http.io.SessionOutputBuffer;
+import org.apache.http.message.BasicLineParser;
+import org.apache.http.message.ParserCursor;
+import org.apache.http.params.CoreConnectionPNames;
+import org.apache.http.params.HttpConnectionParams;
+import org.apache.http.params.HttpParams;
+import org.apache.http.ParseException;
+import org.apache.http.util.CharArrayBuffer;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.SocketException;
+
+/**
+ * A alternate class for (@link DefaultHttpClientConnection).
+ * It has better performance than DefaultHttpClientConnection
+ *
+ * {@hide}
+ */
+public class AndroidHttpClientConnection
+ implements HttpInetConnection, HttpConnection {
+
+ private SessionInputBuffer inbuffer = null;
+ private SessionOutputBuffer outbuffer = null;
+ private int maxHeaderCount;
+ // store CoreConnectionPNames.MAX_LINE_LENGTH for performance
+ private int maxLineLength;
+
+ private final EntitySerializer entityserializer;
+
+ private HttpMessageWriter requestWriter = null;
+ private HttpConnectionMetricsImpl metrics = null;
+ private volatile boolean open;
+ private Socket socket = null;
+
+ public AndroidHttpClientConnection() {
+ this.entityserializer = new EntitySerializer(
+ new StrictContentLengthStrategy());
+ }
+
+ /**
+ * Bind socket and set HttpParams to AndroidHttpClientConnection
+ * @param socket outgoing socket
+ * @param params HttpParams
+ * @throws IOException
+ */
+ public void bind(
+ final Socket socket,
+ final HttpParams params) throws IOException {
+ if (socket == null) {
+ throw new IllegalArgumentException("Socket may not be null");
+ }
+ if (params == null) {
+ throw new IllegalArgumentException("HTTP parameters may not be null");
+ }
+ assertNotOpen();
+ socket.setTcpNoDelay(HttpConnectionParams.getTcpNoDelay(params));
+ socket.setSoTimeout(HttpConnectionParams.getSoTimeout(params));
+
+ int linger = HttpConnectionParams.getLinger(params);
+ if (linger >= 0) {
+ socket.setSoLinger(linger > 0, linger);
+ }
+ this.socket = socket;
+
+ int buffersize = HttpConnectionParams.getSocketBufferSize(params);
+ this.inbuffer = new SocketInputBuffer(socket, buffersize, params);
+ this.outbuffer = new SocketOutputBuffer(socket, buffersize, params);
+
+ maxHeaderCount = params.getIntParameter(
+ CoreConnectionPNames.MAX_HEADER_COUNT, -1);
+ maxLineLength = params.getIntParameter(
+ CoreConnectionPNames.MAX_LINE_LENGTH, -1);
+
+ this.requestWriter = new HttpRequestWriter(outbuffer, null, params);
+
+ this.metrics = new HttpConnectionMetricsImpl(
+ inbuffer.getMetrics(),
+ outbuffer.getMetrics());
+
+ this.open = true;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder buffer = new StringBuilder();
+ buffer.append(getClass().getSimpleName()).append("[");
+ if (isOpen()) {
+ buffer.append(getRemotePort());
+ } else {
+ buffer.append("closed");
+ }
+ buffer.append("]");
+ return buffer.toString();
+ }
+
+
+ private void assertNotOpen() {
+ if (this.open) {
+ throw new IllegalStateException("Connection is already open");
+ }
+ }
+
+ private void assertOpen() {
+ if (!this.open) {
+ throw new IllegalStateException("Connection is not open");
+ }
+ }
+
+ public boolean isOpen() {
+ // to make this method useful, we want to check if the socket is connected
+ return (this.open && this.socket != null && this.socket.isConnected());
+ }
+
+ public InetAddress getLocalAddress() {
+ if (this.socket != null) {
+ return this.socket.getLocalAddress();
+ } else {
+ return null;
+ }
+ }
+
+ public int getLocalPort() {
+ if (this.socket != null) {
+ return this.socket.getLocalPort();
+ } else {
+ return -1;
+ }
+ }
+
+ public InetAddress getRemoteAddress() {
+ if (this.socket != null) {
+ return this.socket.getInetAddress();
+ } else {
+ return null;
+ }
+ }
+
+ public int getRemotePort() {
+ if (this.socket != null) {
+ return this.socket.getPort();
+ } else {
+ return -1;
+ }
+ }
+
+ public void setSocketTimeout(int timeout) {
+ assertOpen();
+ if (this.socket != null) {
+ try {
+ this.socket.setSoTimeout(timeout);
+ } catch (SocketException ignore) {
+ // It is not quite clear from the original documentation if there are any
+ // other legitimate cases for a socket exception to be thrown when setting
+ // SO_TIMEOUT besides the socket being already closed
+ }
+ }
+ }
+
+ public int getSocketTimeout() {
+ if (this.socket != null) {
+ try {
+ return this.socket.getSoTimeout();
+ } catch (SocketException ignore) {
+ return -1;
+ }
+ } else {
+ return -1;
+ }
+ }
+
+ public void shutdown() throws IOException {
+ this.open = false;
+ Socket tmpsocket = this.socket;
+ if (tmpsocket != null) {
+ tmpsocket.close();
+ }
+ }
+
+ public void close() throws IOException {
+ if (!this.open) {
+ return;
+ }
+ this.open = false;
+ doFlush();
+ try {
+ try {
+ this.socket.shutdownOutput();
+ } catch (IOException ignore) {
+ }
+ try {
+ this.socket.shutdownInput();
+ } catch (IOException ignore) {
+ }
+ } catch (UnsupportedOperationException ignore) {
+ // if one isn't supported, the other one isn't either
+ }
+ this.socket.close();
+ }
+
+ /**
+ * Sends the request line and all headers over the connection.
+ * @param request the request whose headers to send.
+ * @throws HttpException
+ * @throws IOException
+ */
+ public void sendRequestHeader(final HttpRequest request)
+ throws HttpException, IOException {
+ if (request == null) {
+ throw new IllegalArgumentException("HTTP request may not be null");
+ }
+ assertOpen();
+ this.requestWriter.write(request);
+ this.metrics.incrementRequestCount();
+ }
+
+ /**
+ * Sends the request entity over the connection.
+ * @param request the request whose entity to send.
+ * @throws HttpException
+ * @throws IOException
+ */
+ public void sendRequestEntity(final HttpEntityEnclosingRequest request)
+ throws HttpException, IOException {
+ if (request == null) {
+ throw new IllegalArgumentException("HTTP request may not be null");
+ }
+ assertOpen();
+ if (request.getEntity() == null) {
+ return;
+ }
+ this.entityserializer.serialize(
+ this.outbuffer,
+ request,
+ request.getEntity());
+ }
+
+ protected void doFlush() throws IOException {
+ this.outbuffer.flush();
+ }
+
+ public void flush() throws IOException {
+ assertOpen();
+ doFlush();
+ }
+
+ /**
+ * Parses the response headers and adds them to the
+ * given {@code headers} object, and returns the response StatusLine
+ * @param headers store parsed header to headers.
+ * @throws IOException
+ * @return StatusLine
+ * @see HttpClientConnection#receiveResponseHeader()
+ */
+ public StatusLine parseResponseHeader(Headers headers)
+ throws IOException, ParseException {
+ assertOpen();
+
+ CharArrayBuffer current = new CharArrayBuffer(64);
+
+ if (inbuffer.readLine(current) == -1) {
+ throw new NoHttpResponseException("The target server failed to respond");
+ }
+
+ // Create the status line from the status string
+ StatusLine statusline = BasicLineParser.DEFAULT.parseStatusLine(
+ current, new ParserCursor(0, current.length()));
+
+ if (HttpLog.LOGV) HttpLog.v("read: " + statusline);
+ int statusCode = statusline.getStatusCode();
+
+ // Parse header body
+ CharArrayBuffer previous = null;
+ int headerNumber = 0;
+ while(true) {
+ if (current == null) {
+ current = new CharArrayBuffer(64);
+ } else {
+ // This must be he buffer used to parse the status
+ current.clear();
+ }
+ int l = inbuffer.readLine(current);
+ if (l == -1 || current.length() < 1) {
+ break;
+ }
+ // Parse the header name and value
+ // Check for folded headers first
+ // Detect LWS-char see HTTP/1.0 or HTTP/1.1 Section 2.2
+ // discussion on folded headers
+ char first = current.charAt(0);
+ if ((first == ' ' || first == '\t') && previous != null) {
+ // we have continuation folded header
+ // so append value
+ int start = 0;
+ int length = current.length();
+ while (start < length) {
+ char ch = current.charAt(start);
+ if (ch != ' ' && ch != '\t') {
+ break;
+ }
+ start++;
+ }
+ if (maxLineLength > 0 &&
+ previous.length() + 1 + current.length() - start >
+ maxLineLength) {
+ throw new IOException("Maximum line length limit exceeded");
+ }
+ previous.append(' ');
+ previous.append(current, start, current.length() - start);
+ } else {
+ if (previous != null) {
+ headers.parseHeader(previous);
+ }
+ headerNumber++;
+ previous = current;
+ current = null;
+ }
+ if (maxHeaderCount > 0 && headerNumber >= maxHeaderCount) {
+ throw new IOException("Maximum header count exceeded");
+ }
+ }
+
+ if (previous != null) {
+ headers.parseHeader(previous);
+ }
+
+ if (statusCode >= 200) {
+ this.metrics.incrementResponseCount();
+ }
+ return statusline;
+ }
+
+ /**
+ * Return the next response entity.
+ * @param headers contains values for parsing entity
+ * @see HttpClientConnection#receiveResponseEntity(HttpResponse response)
+ */
+ public HttpEntity receiveResponseEntity(final Headers headers) {
+ assertOpen();
+ BasicHttpEntity entity = new BasicHttpEntity();
+
+ long len = determineLength(headers);
+ if (len == ContentLengthStrategy.CHUNKED) {
+ entity.setChunked(true);
+ entity.setContentLength(-1);
+ entity.setContent(new ChunkedInputStream(inbuffer));
+ } else if (len == ContentLengthStrategy.IDENTITY) {
+ entity.setChunked(false);
+ entity.setContentLength(-1);
+ entity.setContent(new IdentityInputStream(inbuffer));
+ } else {
+ entity.setChunked(false);
+ entity.setContentLength(len);
+ entity.setContent(new ContentLengthInputStream(inbuffer, len));
+ }
+
+ String contentTypeHeader = headers.getContentType();
+ if (contentTypeHeader != null) {
+ entity.setContentType(contentTypeHeader);
+ }
+ String contentEncodingHeader = headers.getContentEncoding();
+ if (contentEncodingHeader != null) {
+ entity.setContentEncoding(contentEncodingHeader);
+ }
+
+ return entity;
+ }
+
+ private long determineLength(final Headers headers) {
+ long transferEncoding = headers.getTransferEncoding();
+ // We use Transfer-Encoding if present and ignore Content-Length.
+ // RFC2616, 4.4 item number 3
+ if (transferEncoding < Headers.NO_TRANSFER_ENCODING) {
+ return transferEncoding;
+ } else {
+ long contentlen = headers.getContentLength();
+ if (contentlen > Headers.NO_CONTENT_LENGTH) {
+ return contentlen;
+ } else {
+ return ContentLengthStrategy.IDENTITY;
+ }
+ }
+ }
+
+ /**
+ * Checks whether this connection has gone down.
+ * Network connections may get closed during some time of inactivity
+ * for several reasons. The next time a read is attempted on such a
+ * connection it will throw an IOException.
+ * This method tries to alleviate this inconvenience by trying to
+ * find out if a connection is still usable. Implementations may do
+ * that by attempting a read with a very small timeout. Thus this
+ * method may block for a small amount of time before returning a result.
+ * It is therefore an <i>expensive</i> operation.
+ *
+ * @return <code>true</code> if attempts to use this connection are
+ * likely to succeed, or <code>false</code> if they are likely
+ * to fail and this connection should be closed
+ */
+ public boolean isStale() {
+ assertOpen();
+ try {
+ this.inbuffer.isDataAvailable(1);
+ return false;
+ } catch (IOException ex) {
+ return true;
+ }
+ }
+
+ /**
+ * Returns a collection of connection metrcis
+ * @return HttpConnectionMetrics
+ */
+ public HttpConnectionMetrics getMetrics() {
+ return this.metrics;
+ }
+}
diff --git a/core/java/android/net/http/CertificateChainValidator.java b/core/java/android/net/http/CertificateChainValidator.java
new file mode 100644
index 0000000..b7f7368
--- /dev/null
+++ b/core/java/android/net/http/CertificateChainValidator.java
@@ -0,0 +1,444 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.http;
+
+import android.os.SystemClock;
+
+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.Arrays;
+import java.util.Date;
+import java.util.Enumeration;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLPeerUnverifiedException;
+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.http.HttpHost;
+
+import org.bouncycastle.asn1.x509.X509Name;
+
+/**
+ * Class responsible for all server certificate validation functionality
+ *
+ * {@hide}
+ */
+class CertificateChainValidator {
+
+ private static long sTotal = 0;
+ private static long sTotalReused = 0;
+
+ /**
+ * The singleton instance of the certificate chain validator
+ */
+ private static CertificateChainValidator sInstance;
+
+ /**
+ * Default trust manager (used to perform CA certificate validation)
+ */
+ private X509TrustManager mDefaultTrustManager;
+
+ /**
+ * @return The singleton instance of the certificator chain validator
+ */
+ public static CertificateChainValidator getInstance() {
+ if (sInstance == null) {
+ sInstance = new CertificateChainValidator();
+ }
+
+ return sInstance;
+ }
+
+ /**
+ * Creates a new certificate chain validator. This is a pivate constructor.
+ * If you need a Certificate chain validator, call getInstance().
+ */
+ private CertificateChainValidator() {
+ try {
+ TrustManagerFactory trustManagerFactory
+ = TrustManagerFactory.getInstance("X509");
+ trustManagerFactory.init((KeyStore)null);
+ TrustManager[] trustManagers =
+ trustManagerFactory.getTrustManagers();
+ if (trustManagers != null && trustManagers.length > 0) {
+ for (TrustManager trustManager : trustManagers) {
+ if (trustManager instanceof X509TrustManager) {
+ mDefaultTrustManager = (X509TrustManager)(trustManager);
+ break;
+ }
+ }
+ }
+ } catch (Exception exc) {
+ if (HttpLog.LOGV) {
+ HttpLog.v("CertificateChainValidator():" +
+ " failed to initialize the trust manager");
+ }
+ }
+ }
+
+ /**
+ * Performs the handshake and server certificates validation
+ * @param sslSocket The secure connection socket
+ * @param domain The website domain
+ * @return An SSL error object if there is an error and null otherwise
+ */
+ public SslError doHandshakeAndValidateServerCertificates(
+ HttpsConnection connection, SSLSocket sslSocket, String domain)
+ throws SSLHandshakeException, IOException {
+
+ ++sTotal;
+
+ SSLContext sslContext = HttpsConnection.getContext();
+ if (sslContext == null) {
+ closeSocketThrowException(sslSocket, "SSL context is null");
+ }
+
+ X509Certificate[] serverCertificates = null;
+
+ long sessionBeforeHandshakeLastAccessedTime = 0;
+ byte[] sessionBeforeHandshakeId = null;
+
+ SSLSession sessionAfterHandshake = null;
+
+ synchronized(sslContext) {
+ // get SSL session before the handshake
+ SSLSession sessionBeforeHandshake =
+ getSSLSession(sslContext, connection.getHost());
+ if (sessionBeforeHandshake != null) {
+ sessionBeforeHandshakeLastAccessedTime =
+ sessionBeforeHandshake.getLastAccessedTime();
+
+ sessionBeforeHandshakeId =
+ sessionBeforeHandshake.getId();
+ }
+
+ // start handshake, close the socket if we fail
+ try {
+ sslSocket.setUseClientMode(true);
+ sslSocket.startHandshake();
+ } catch (IOException e) {
+ closeSocketThrowException(
+ sslSocket, e.getMessage(),
+ "failed to perform SSL handshake");
+ }
+
+ // retrieve the chain of the server peer certificates
+ Certificate[] peerCertificates =
+ sslSocket.getSession().getPeerCertificates();
+
+ if (peerCertificates == null || peerCertificates.length <= 0) {
+ closeSocketThrowException(
+ sslSocket, "failed to retrieve peer certificates");
+ } else {
+ serverCertificates =
+ new X509Certificate[peerCertificates.length];
+ for (int i = 0; i < peerCertificates.length; ++i) {
+ serverCertificates[i] =
+ (X509Certificate)(peerCertificates[i]);
+ }
+
+ // update the SSL certificate associated with the connection
+ if (connection != null) {
+ if (serverCertificates[0] != null) {
+ connection.setCertificate(
+ new SslCertificate(serverCertificates[0]));
+ }
+ }
+ }
+
+ // get SSL session after the handshake
+ sessionAfterHandshake =
+ getSSLSession(sslContext, connection.getHost());
+ }
+
+ if (sessionBeforeHandshakeLastAccessedTime != 0 &&
+ sessionAfterHandshake != null &&
+ Arrays.equals(
+ sessionBeforeHandshakeId, sessionAfterHandshake.getId()) &&
+ sessionBeforeHandshakeLastAccessedTime <
+ sessionAfterHandshake.getLastAccessedTime()) {
+
+ if (HttpLog.LOGV) {
+ HttpLog.v("SSL session was reused: total reused: "
+ + sTotalReused
+ + " out of total of: " + sTotal);
+
+ ++sTotalReused;
+ }
+
+ // no errors!!!
+ return null;
+ }
+
+ // check if the first certificate in the chain is for this site
+ X509Certificate currCertificate = serverCertificates[0];
+ if (currCertificate == null) {
+ closeSocketThrowException(
+ sslSocket, "certificate for this site is null");
+ } else {
+ if (!DomainNameChecker.match(currCertificate, domain)) {
+ String errorMessage = "certificate not for this host: " + domain;
+
+ if (HttpLog.LOGV) {
+ HttpLog.v(errorMessage);
+ }
+
+ sslSocket.getSession().invalidate();
+ return new SslError(
+ SslError.SSL_IDMISMATCH, currCertificate);
+ }
+ }
+
+ //
+ // first, we validate the chain using the standard validation
+ // solution; if we do not find any errors, we are done; if we
+ // fail the standard validation, we re-validate again below,
+ // this time trying to retrieve any individual errors we can
+ // report back to the user.
+ //
+ try {
+ synchronized (mDefaultTrustManager) {
+ mDefaultTrustManager.checkServerTrusted(
+ serverCertificates, "RSA");
+
+ // no errors!!!
+ return null;
+ }
+ } catch (CertificateException e) {
+ if (HttpLog.LOGV) {
+ HttpLog.v(
+ "failed to pre-validate the certificate chain, error: " +
+ e.getMessage());
+ }
+ }
+
+ sslSocket.getSession().invalidate();
+
+ SslError error = null;
+
+ // we check the root certificate separately from the rest of the
+ // chain; this is because we need to know what certificate in
+ // the chain resulted in an error if any
+ currCertificate =
+ serverCertificates[serverCertificates.length - 1];
+ if (currCertificate == null) {
+ closeSocketThrowException(
+ sslSocket, "root certificate is null");
+ }
+
+ // check if the last certificate in the chain (root) is trusted
+ X509Certificate[] rootCertificateChain = { currCertificate };
+ try {
+ synchronized (mDefaultTrustManager) {
+ mDefaultTrustManager.checkServerTrusted(
+ rootCertificateChain, "RSA");
+ }
+ } catch (CertificateExpiredException e) {
+ String errorMessage = e.getMessage();
+ if (errorMessage == null) {
+ errorMessage = "root certificate has expired";
+ }
+
+ if (HttpLog.LOGV) {
+ HttpLog.v(errorMessage);
+ }
+
+ error = new SslError(
+ SslError.SSL_EXPIRED, currCertificate);
+ } catch (CertificateNotYetValidException e) {
+ String errorMessage = e.getMessage();
+ if (errorMessage == null) {
+ errorMessage = "root certificate not valid yet";
+ }
+
+ if (HttpLog.LOGV) {
+ HttpLog.v(errorMessage);
+ }
+
+ error = new SslError(
+ SslError.SSL_NOTYETVALID, currCertificate);
+ } catch (CertificateException e) {
+ String errorMessage = e.getMessage();
+ if (errorMessage == null) {
+ errorMessage = "root certificate not trusted";
+ }
+
+ if (HttpLog.LOGV) {
+ HttpLog.v(errorMessage);
+ }
+
+ return new SslError(
+ SslError.SSL_UNTRUSTED, currCertificate);
+ }
+
+ // Then go through the certificate chain checking that each
+ // certificate trusts the next and that each certificate is
+ // within its valid date range. Walk the chain in the order
+ // from the CA to the end-user
+ X509Certificate prevCertificate =
+ serverCertificates[serverCertificates.length - 1];
+
+ for (int i = serverCertificates.length - 2; i >= 0; --i) {
+ currCertificate = serverCertificates[i];
+
+ // if a certificate is null, we cannot verify the chain
+ if (currCertificate == null) {
+ closeSocketThrowException(
+ sslSocket, "null certificate in the chain");
+ }
+
+ // verify if trusted by chain
+ if (!prevCertificate.getSubjectDN().equals(
+ currCertificate.getIssuerDN())) {
+ String errorMessage = "not trusted by chain";
+
+ if (HttpLog.LOGV) {
+ HttpLog.v(errorMessage);
+ }
+
+ return new SslError(
+ SslError.SSL_UNTRUSTED, currCertificate);
+ }
+
+ try {
+ currCertificate.verify(prevCertificate.getPublicKey());
+ } catch (GeneralSecurityException e) {
+ String errorMessage = e.getMessage();
+ if (errorMessage == null) {
+ errorMessage = "not trusted by chain";
+ }
+
+ if (HttpLog.LOGV) {
+ HttpLog.v(errorMessage);
+ }
+
+ return new SslError(
+ SslError.SSL_UNTRUSTED, currCertificate);
+ }
+
+ // verify if the dates are valid
+ try {
+ currCertificate.checkValidity();
+ } catch (CertificateExpiredException e) {
+ String errorMessage = e.getMessage();
+ if (errorMessage == null) {
+ errorMessage = "certificate expired";
+ }
+
+ if (HttpLog.LOGV) {
+ HttpLog.v(errorMessage);
+ }
+
+ if (error == null ||
+ error.getPrimaryError() < SslError.SSL_EXPIRED) {
+ error = new SslError(
+ SslError.SSL_EXPIRED, currCertificate);
+ }
+ } catch (CertificateNotYetValidException e) {
+ String errorMessage = e.getMessage();
+ if (errorMessage == null) {
+ errorMessage = "certificate not valid yet";
+ }
+
+ if (HttpLog.LOGV) {
+ HttpLog.v(errorMessage);
+ }
+
+ if (error == null ||
+ error.getPrimaryError() < SslError.SSL_NOTYETVALID) {
+ error = new SslError(
+ SslError.SSL_NOTYETVALID, currCertificate);
+ }
+ }
+
+ prevCertificate = currCertificate;
+ }
+
+ // if we do not have an error to report back to the user, throw
+ // an exception (a generic error will be reported instead)
+ if (error == null) {
+ closeSocketThrowException(
+ sslSocket,
+ "failed to pre-validate the certificate chain due to a non-standard error");
+ }
+
+ return error;
+ }
+
+ private void closeSocketThrowException(
+ SSLSocket socket, String errorMessage, String defaultErrorMessage)
+ throws SSLHandshakeException, IOException {
+ closeSocketThrowException(
+ socket, errorMessage != null ? errorMessage : defaultErrorMessage);
+ }
+
+ private void closeSocketThrowException(SSLSocket socket, String errorMessage)
+ throws SSLHandshakeException, IOException {
+ if (HttpLog.LOGV) {
+ HttpLog.v("validation error: " + errorMessage);
+ }
+
+ if (socket != null) {
+ SSLSession session = socket.getSession();
+ if (session != null) {
+ session.invalidate();
+ }
+
+ socket.close();
+ }
+
+ throw new SSLHandshakeException(errorMessage);
+ }
+
+ /**
+ * @param sslContext The SSL context shared accross all the SSL sessions
+ * @param host The host associated with the session
+ * @return A suitable SSL session from the SSL context
+ */
+ private SSLSession getSSLSession(SSLContext sslContext, HttpHost host) {
+ if (sslContext != null && host != null) {
+ Enumeration en = sslContext.getClientSessionContext().getIds();
+ while (en.hasMoreElements()) {
+ byte[] id = (byte[]) en.nextElement();
+ if (id != null) {
+ SSLSession session =
+ sslContext.getClientSessionContext().getSession(id);
+ if (session.isValid() &&
+ host.getHostName().equals(session.getPeerHost()) &&
+ host.getPort() == session.getPeerPort()) {
+ return session;
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/core/java/android/net/http/CertificateValidatorCache.java b/core/java/android/net/http/CertificateValidatorCache.java
new file mode 100644
index 0000000..54a1dbe
--- /dev/null
+++ b/core/java/android/net/http/CertificateValidatorCache.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.http;
+
+import android.os.SystemClock;
+
+import android.security.Sha1MessageDigest;
+
+import java.security.cert.Certificate;
+import java.security.cert.CertificateFactory;
+import java.security.cert.CertPath;
+import java.security.GeneralSecurityException;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Random;
+
+
+/**
+ * Validator cache used to speed-up certificate chain validation. The idea is
+ * to keep each secure domain name associated with a cryptographically secure
+ * hash of the certificate chain successfully used to validate the domain. If
+ * we establish connection with the domain more than once and each time receive
+ * the same list of certificates, we do not have to re-validate.
+ *
+ * {@hide}
+ */
+class CertificateValidatorCache {
+
+ // TODO: debug only!
+ public static long mSave = 0;
+ public static long mCost = 0;
+ // TODO: debug only!
+
+ /**
+ * The cache-entry lifetime in milliseconds (here, 10 minutes)
+ */
+ private static final long CACHE_ENTRY_LIFETIME = 10 * 60 * 1000;
+
+ /**
+ * The certificate factory
+ */
+ private static CertificateFactory sCertificateFactory;
+
+ /**
+ * The certificate validator cache map (domain to a cache entry)
+ */
+ private HashMap<Integer, CacheEntry> mCacheMap;
+
+ /**
+ * Random salt
+ */
+ private int mBigScrew;
+
+ /**
+ * @param certificate The array of server certificates to compute a
+ * secure hash from
+ * @return The secure hash computed from server certificates
+ */
+ public static byte[] secureHash(Certificate[] certificates) {
+ byte[] secureHash = null;
+
+ // TODO: debug only!
+ long beg = SystemClock.uptimeMillis();
+ // TODO: debug only!
+
+ if (certificates != null && certificates.length != 0) {
+ byte[] encodedCertPath = null;
+ try {
+ synchronized (CertificateValidatorCache.class) {
+ if (sCertificateFactory == null) {
+ try {
+ sCertificateFactory =
+ CertificateFactory.getInstance("X.509");
+ } catch(GeneralSecurityException e) {
+ if (HttpLog.LOGV) {
+ HttpLog.v("CertificateValidatorCache:" +
+ " failed to create the certificate factory");
+ }
+ }
+ }
+ }
+
+ CertPath certPath =
+ sCertificateFactory.generateCertPath(Arrays.asList(certificates));
+ if (certPath != null) {
+ encodedCertPath = certPath.getEncoded();
+ if (encodedCertPath != null) {
+ Sha1MessageDigest messageDigest =
+ new Sha1MessageDigest();
+ secureHash = messageDigest.digest(encodedCertPath);
+ }
+ }
+ } catch (GeneralSecurityException e) {}
+ }
+
+ // TODO: debug only!
+ long end = SystemClock.uptimeMillis();
+ mCost += (end - beg);
+ // TODO: debug only!
+
+ return secureHash;
+ }
+
+ /**
+ * Creates a new certificate-validator cache
+ */
+ public CertificateValidatorCache() {
+ Random random = new Random();
+ mBigScrew = random.nextInt();
+
+ mCacheMap = new HashMap<Integer, CacheEntry>();
+ }
+
+ /**
+ * @param domain The domain to check against
+ * @param secureHash The secure hash to check against
+ * @return True iff there is a valid (not expired) cache entry
+ * associated with the domain and the secure hash
+ */
+ public boolean has(String domain, byte[] secureHash) {
+ boolean rval = false;
+
+ if (domain != null && domain.length() != 0) {
+ if (secureHash != null && secureHash.length != 0) {
+ CacheEntry cacheEntry = (CacheEntry)mCacheMap.get(
+ new Integer(mBigScrew ^ domain.hashCode()));
+ if (cacheEntry != null) {
+ if (!cacheEntry.expired()) {
+ rval = cacheEntry.has(domain, secureHash);
+ // TODO: debug only!
+ if (rval) {
+ mSave += cacheEntry.mSave;
+ }
+ // TODO: debug only!
+ } else {
+ mCacheMap.remove(cacheEntry);
+ }
+ }
+ }
+ }
+
+ return rval;
+ }
+
+ /**
+ * Adds the (domain, secureHash) tuple to the cache
+ * @param domain The domain to be added to the cache
+ * @param secureHash The secure hash to be added to the cache
+ * @return True iff succeeds
+ */
+ public boolean put(String domain, byte[] secureHash, long save) {
+ if (domain != null && domain.length() != 0) {
+ if (secureHash != null && secureHash.length != 0) {
+ mCacheMap.put(
+ new Integer(mBigScrew ^ domain.hashCode()),
+ new CacheEntry(domain, secureHash, save));
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Certificate-validator cache entry. We have one per domain
+ */
+ private class CacheEntry {
+
+ /**
+ * The hash associated with this cache entry
+ */
+ private byte[] mHash;
+
+ /**
+ * The time associated with this cache entry
+ */
+ private long mTime;
+
+ // TODO: debug only!
+ public long mSave;
+ // TODO: debug only!
+
+ /**
+ * The host associated with this cache entry
+ */
+ private String mDomain;
+
+ /**
+ * Creates a new certificate-validator cache entry
+ * @param domain The domain to be associated with this cache entry
+ * @param secureHash The secure hash to be associated with this cache
+ * entry
+ */
+ public CacheEntry(String domain, byte[] secureHash, long save) {
+ mDomain = domain;
+ mHash = secureHash;
+ // TODO: debug only!
+ mSave = save;
+ // TODO: debug only!
+ mTime = SystemClock.uptimeMillis();
+ }
+
+ /**
+ * @return True iff the cache item has expired
+ */
+ public boolean expired() {
+ return CACHE_ENTRY_LIFETIME < SystemClock.uptimeMillis() - mTime;
+ }
+
+ /**
+ * @param domain The domain to check
+ * @param secureHash The secure hash to check
+ * @return True iff the given domain and hash match those associated
+ * with this entry
+ */
+ public boolean has(String domain, byte[] secureHash) {
+ if (domain != null && 0 < domain.length()) {
+ if (!mDomain.equals(domain)) {
+ return false;
+ }
+ }
+
+ int hashLength = secureHash.length;
+ if (secureHash != null && 0 < hashLength) {
+ if (hashLength == mHash.length) {
+ for (int i = 0; i < hashLength; ++i) {
+ if (secureHash[i] != mHash[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+};
diff --git a/core/java/android/net/http/CharArrayBuffers.java b/core/java/android/net/http/CharArrayBuffers.java
new file mode 100644
index 0000000..77d45f6
--- /dev/null
+++ b/core/java/android/net/http/CharArrayBuffers.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.http;
+
+import org.apache.http.util.CharArrayBuffer;
+import org.apache.http.protocol.HTTP;
+
+/**
+ * Utility methods for working on CharArrayBuffers.
+ *
+ * {@hide}
+ */
+class CharArrayBuffers {
+
+ static final char uppercaseAddon = 'a' - 'A';
+
+ /**
+ * Returns true if the buffer contains the given string. Ignores leading
+ * whitespace and case.
+ *
+ * @param buffer to search
+ * @param beginIndex index at which we should start
+ * @param str to search for
+ */
+ static boolean containsIgnoreCaseTrimmed(CharArrayBuffer buffer,
+ int beginIndex, final String str) {
+ int len = buffer.length();
+ char[] chars = buffer.buffer();
+ while (beginIndex < len && HTTP.isWhitespace(chars[beginIndex])) {
+ beginIndex++;
+ }
+ int size = str.length();
+ boolean ok = len >= beginIndex + size;
+ for (int j=0; ok && (j<size); j++) {
+ char a = chars[beginIndex+j];
+ char b = str.charAt(j);
+ if (a != b) {
+ a = toLower(a);
+ b = toLower(b);
+ ok = a == b;
+ }
+ }
+ return ok;
+ }
+
+ /**
+ * Returns index of first occurence ch. Lower cases characters leading up
+ * to first occurrence of ch.
+ */
+ static int setLowercaseIndexOf(CharArrayBuffer buffer, final int ch) {
+
+ int beginIndex = 0;
+ int endIndex = buffer.length();
+ char[] chars = buffer.buffer();
+
+ for (int i = beginIndex; i < endIndex; i++) {
+ char current = chars[i];
+ if (current == ch) {
+ return i;
+ } else if (current >= 'A' && current <= 'Z'){
+ // make lower case
+ current += uppercaseAddon;
+ chars[i] = current;
+ }
+ }
+ return -1;
+ }
+
+ private static char toLower(char c) {
+ if (c >= 'A' && c <= 'Z'){
+ c += uppercaseAddon;
+ }
+ return c;
+ }
+}
diff --git a/core/java/android/net/http/Connection.java b/core/java/android/net/http/Connection.java
new file mode 100644
index 0000000..2c82582
--- /dev/null
+++ b/core/java/android/net/http/Connection.java
@@ -0,0 +1,523 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.http;
+
+import android.content.Context;
+import android.os.SystemClock;
+
+import java.io.IOException;
+import java.net.UnknownHostException;
+import java.util.ListIterator;
+import java.util.LinkedList;
+
+import javax.net.ssl.SSLHandshakeException;
+
+import org.apache.http.ConnectionReuseStrategy;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpException;
+import org.apache.http.HttpHost;
+import org.apache.http.HttpVersion;
+import org.apache.http.ParseException;
+import org.apache.http.ProtocolVersion;
+import org.apache.http.protocol.ExecutionContext;
+import org.apache.http.protocol.HttpContext;
+import org.apache.http.protocol.BasicHttpContext;
+
+/**
+ * {@hide}
+ */
+abstract class Connection {
+
+ /**
+ * Allow a TCP connection 60 idle seconds before erroring out
+ */
+ static final int SOCKET_TIMEOUT = 60000;
+
+ private static final int SEND = 0;
+ private static final int READ = 1;
+ private static final int DRAIN = 2;
+ private static final int DONE = 3;
+ private static final String[] states = {"SEND", "READ", "DRAIN", "DONE"};
+
+ Context mContext;
+
+ /** The low level connection */
+ protected AndroidHttpClientConnection mHttpClientConnection = null;
+
+ /**
+ * The server SSL certificate associated with this connection
+ * (null if the connection is not secure)
+ * It would be nice to store the whole certificate chain, but
+ * we want to keep things as light-weight as possible
+ */
+ protected SslCertificate mCertificate = null;
+
+ /**
+ * The host this connection is connected to. If using proxy,
+ * this is set to the proxy address
+ */
+ HttpHost mHost;
+
+ /** true if the connection can be reused for sending more requests */
+ private boolean mCanPersist;
+
+ /** context required by ConnectionReuseStrategy. */
+ private HttpContext mHttpContext;
+
+ /** set when cancelled */
+ private static int STATE_NORMAL = 0;
+ private static int STATE_CANCEL_REQUESTED = 1;
+ private int mActive = STATE_NORMAL;
+
+ /** The number of times to try to re-connect (if connect fails). */
+ private final static int RETRY_REQUEST_LIMIT = 2;
+
+ private static final int MIN_PIPE = 2;
+ private static final int MAX_PIPE = 3;
+
+ /**
+ * Doesn't seem to exist anymore in the new HTTP client, so copied here.
+ */
+ private static final String HTTP_CONNECTION = "http.connection";
+
+ RequestQueue.ConnectionManager mConnectionManager;
+ RequestFeeder mRequestFeeder;
+
+ /**
+ * Buffer for feeding response blocks to webkit. One block per
+ * connection reduces memory churn.
+ */
+ private byte[] mBuf;
+
+ protected Connection(Context context, HttpHost host,
+ RequestQueue.ConnectionManager connectionManager,
+ RequestFeeder requestFeeder) {
+ mContext = context;
+ mHost = host;
+ mConnectionManager = connectionManager;
+ mRequestFeeder = requestFeeder;
+
+ mCanPersist = false;
+ mHttpContext = new BasicHttpContext(null);
+ }
+
+ HttpHost getHost() {
+ return mHost;
+ }
+
+ /**
+ * connection factory: returns an HTTP or HTTPS connection as
+ * necessary
+ */
+ static Connection getConnection(
+ Context context, HttpHost host,
+ RequestQueue.ConnectionManager connectionManager,
+ RequestFeeder requestFeeder) {
+
+ if (host.getSchemeName().equals("http")) {
+ return new HttpConnection(context, host, connectionManager,
+ requestFeeder);
+ }
+
+ // Otherwise, default to https
+ return new HttpsConnection(context, host, connectionManager,
+ requestFeeder);
+ }
+
+ /**
+ * @return The server SSL certificate associated with this
+ * connection (null if the connection is not secure)
+ */
+ /* package */ SslCertificate getCertificate() {
+ return mCertificate;
+ }
+
+ /**
+ * Close current network connection
+ * Note: this runs in non-network thread
+ */
+ void cancel() {
+ mActive = STATE_CANCEL_REQUESTED;
+ closeConnection();
+ if (HttpLog.LOGV) HttpLog.v(
+ "Connection.cancel(): connection closed " + mHost);
+ }
+
+ /**
+ * Process requests in queue
+ * pipelines requests
+ */
+ void processRequests(Request firstRequest) {
+ Request req = null;
+ boolean empty;
+ int error = EventHandler.OK;
+ Exception exception = null;
+
+ LinkedList<Request> pipe = new LinkedList<Request>();
+
+ int minPipe = MIN_PIPE, maxPipe = MAX_PIPE;
+ int state = SEND;
+
+ while (state != DONE) {
+ if (HttpLog.LOGV) HttpLog.v(
+ states[state] + " pipe " + pipe.size());
+
+ /* If a request was cancelled, give other cancel requests
+ some time to go through so we don't uselessly restart
+ connections */
+ if (mActive == STATE_CANCEL_REQUESTED) {
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException x) { /* ignore */ }
+ mActive = STATE_NORMAL;
+ }
+
+ switch (state) {
+ case SEND: {
+ if (pipe.size() == maxPipe) {
+ state = READ;
+ break;
+ }
+ /* get a request */
+ if (firstRequest == null) {
+ req = mRequestFeeder.getRequest(mHost);
+ } else {
+ req = firstRequest;
+ firstRequest = null;
+ }
+ if (req == null) {
+ state = DRAIN;
+ break;
+ }
+ req.setConnection(this);
+
+ /* Don't work on cancelled requests. */
+ if (req.mCancelled) {
+ if (HttpLog.LOGV) HttpLog.v(
+ "processRequests(): skipping cancelled request "
+ + req);
+ req.complete();
+ break;
+ }
+
+ if (mHttpClientConnection == null ||
+ !mHttpClientConnection.isOpen()) {
+ /* If this call fails, the address is bad or
+ the net is down. Punt for now.
+
+ FIXME: blow out entire queue here on
+ connection failure if net up? */
+
+ if (!openHttpConnection(req)) {
+ state = DONE;
+ break;
+ }
+ }
+
+ try {
+ /* FIXME: don't increment failure count if old
+ connection? There should not be a penalty for
+ attempting to reuse an old connection */
+ req.sendRequest(mHttpClientConnection);
+ } catch (HttpException e) {
+ exception = e;
+ error = EventHandler.ERROR;
+ } catch (IOException e) {
+ exception = e;
+ error = EventHandler.ERROR_IO;
+ } catch (IllegalStateException e) {
+ exception = e;
+ error = EventHandler.ERROR_IO;
+ }
+ if (exception != null) {
+ if (httpFailure(req, error, exception) &&
+ !req.mCancelled) {
+ /* retry request if not permanent failure
+ or cancelled */
+ pipe.addLast(req);
+ }
+ exception = null;
+ state = (clearPipe(pipe) ||
+ !mConnectionManager.isNetworkConnected()) ?
+ DONE : SEND;
+ minPipe = maxPipe = 1;
+ break;
+ }
+
+ pipe.addLast(req);
+ if (!mCanPersist) state = READ;
+ break;
+
+ }
+ case DRAIN:
+ case READ: {
+ empty = !mRequestFeeder.haveRequest(mHost);
+ int pipeSize = pipe.size();
+ if (state != DRAIN && pipeSize < minPipe &&
+ !empty && mCanPersist) {
+ state = SEND;
+ break;
+ } else if (pipeSize == 0) {
+ /* Done if no other work to do */
+ state = empty ? DONE : SEND;
+ break;
+ }
+
+ req = (Request)pipe.removeFirst();
+ if (HttpLog.LOGV) HttpLog.v(
+ "processRequests() reading " + req);
+
+ try {
+ req.readResponse(mHttpClientConnection);
+ } catch (ParseException e) {
+ exception = e;
+ error = EventHandler.ERROR_IO;
+ } catch (IOException e) {
+ exception = e;
+ error = EventHandler.ERROR_IO;
+ } catch (IllegalStateException e) {
+ exception = e;
+ error = EventHandler.ERROR_IO;
+ }
+ if (exception != null) {
+ if (httpFailure(req, error, exception) &&
+ !req.mCancelled) {
+ /* retry request if not permanent failure
+ or cancelled */
+ req.reset();
+ pipe.addFirst(req);
+ }
+ exception = null;
+ mCanPersist = false;
+ }
+ if (!mCanPersist) {
+ if (HttpLog.LOGV) HttpLog.v(
+ "processRequests(): no persist, closing " +
+ mHost);
+
+ closeConnection();
+
+ mHttpContext.removeAttribute(HTTP_CONNECTION);
+ clearPipe(pipe);
+ minPipe = maxPipe = 1;
+ /* If network active continue to service this queue */
+ state = mConnectionManager.isNetworkConnected() ?
+ SEND : DONE;
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * After a send/receive failure, any pipelined requests must be
+ * cleared back to the mRequest queue
+ * @return true if mRequests is empty after pipe cleared
+ */
+ private boolean clearPipe(LinkedList<Request> pipe) {
+ boolean empty = true;
+ if (HttpLog.LOGV) HttpLog.v(
+ "Connection.clearPipe(): clearing pipe " + pipe.size());
+ synchronized (mRequestFeeder) {
+ Request tReq;
+ while (!pipe.isEmpty()) {
+ tReq = (Request)pipe.removeLast();
+ if (HttpLog.LOGV) HttpLog.v(
+ "clearPipe() adding back " + mHost + " " + tReq);
+ mRequestFeeder.requeueRequest(tReq);
+ empty = false;
+ }
+ if (empty) empty = mRequestFeeder.haveRequest(mHost);
+ }
+ return empty;
+ }
+
+ /**
+ * @return true on success
+ */
+ private boolean openHttpConnection(Request req) {
+
+ long now = SystemClock.uptimeMillis();
+ int error = EventHandler.OK;
+ Exception exception = null;
+
+ try {
+ // reset the certificate to null before opening a connection
+ mCertificate = null;
+ mHttpClientConnection = openConnection(req);
+ if (mHttpClientConnection != null) {
+ mHttpClientConnection.setSocketTimeout(SOCKET_TIMEOUT);
+ mHttpContext.setAttribute(HTTP_CONNECTION,
+ mHttpClientConnection);
+ } else {
+ // we tried to do SSL tunneling, failed,
+ // and need to drop the request;
+ // we have already informed the handler
+ req.mFailCount = RETRY_REQUEST_LIMIT;
+ return false;
+ }
+ } catch (UnknownHostException e) {
+ if (HttpLog.LOGV) HttpLog.v("Failed to open connection");
+ error = EventHandler.ERROR_LOOKUP;
+ exception = e;
+ } catch (SSLConnectionClosedByUserException e) {
+ // hack: if we have an SSL connection failure,
+ // we don't want to reconnect
+ req.mFailCount = RETRY_REQUEST_LIMIT;
+ // no error message
+ return false;
+ } catch (SSLHandshakeException e) {
+ // hack: if we have an SSL connection failure,
+ // we don't want to reconnect
+ req.mFailCount = RETRY_REQUEST_LIMIT;
+ if (HttpLog.LOGV) HttpLog.v(
+ "SSL exception performing handshake");
+ error = EventHandler.ERROR_FAILED_SSL_HANDSHAKE;
+ exception = e;
+ } catch (IOException e) {
+ error = EventHandler.ERROR_CONNECT;
+ exception = e;
+ }
+
+ if (HttpLog.LOGV) {
+ long now2 = SystemClock.uptimeMillis();
+ HttpLog.v("Connection.openHttpConnection() " +
+ (now2 - now) + " " + mHost);
+ }
+
+ if (error == EventHandler.OK) {
+ return true;
+ } else {
+ if (mConnectionManager.isNetworkConnected() == false ||
+ req.mFailCount < RETRY_REQUEST_LIMIT) {
+ // requeue
+ mRequestFeeder.requeueRequest(req);
+ req.mFailCount++;
+ } else {
+ httpFailure(req, error, exception);
+ }
+ return error == EventHandler.OK;
+ }
+ }
+
+ /**
+ * Helper. Calls the mEventHandler's error() method only if
+ * request failed permanently. Increments mFailcount on failure.
+ *
+ * Increments failcount only if the network is believed to be
+ * connected
+ *
+ * @return true if request can be retried (less than
+ * RETRY_REQUEST_LIMIT failures have occurred).
+ */
+ private boolean httpFailure(Request req, int errorId, Exception e) {
+ boolean ret = true;
+ boolean networkConnected = mConnectionManager.isNetworkConnected();
+
+ // e.printStackTrace();
+ if (HttpLog.LOGV) HttpLog.v(
+ "httpFailure() ******* " + e + " count " + req.mFailCount +
+ " networkConnected " + networkConnected + " " + mHost + " " + req.getUri());
+
+ if (networkConnected && ++req.mFailCount >= RETRY_REQUEST_LIMIT) {
+ ret = false;
+ String error;
+ if (errorId < 0) {
+ error = mContext.getText(
+ EventHandler.errorStringResources[-errorId]).toString();
+ } else {
+ Throwable cause = e.getCause();
+ error = cause != null ? cause.toString() : e.getMessage();
+ }
+ req.mEventHandler.error(errorId, error);
+ req.complete();
+ }
+
+ closeConnection();
+ mHttpContext.removeAttribute(HTTP_CONNECTION);
+
+ return ret;
+ }
+
+ HttpContext getHttpContext() {
+ return mHttpContext;
+ }
+
+ /**
+ * Use same logic as ConnectionReuseStrategy
+ * @see ConnectionReuseStrategy
+ */
+ private boolean keepAlive(HttpEntity entity,
+ ProtocolVersion ver, int connType, final HttpContext context) {
+ org.apache.http.HttpConnection conn = (org.apache.http.HttpConnection)
+ context.getAttribute(ExecutionContext.HTTP_CONNECTION);
+
+ if (conn != null && !conn.isOpen())
+ return false;
+ // do NOT check for stale connection, that is an expensive operation
+
+ if (entity != null) {
+ if (entity.getContentLength() < 0) {
+ if (!entity.isChunked() || ver.lessEquals(HttpVersion.HTTP_1_0)) {
+ // if the content length is not known and is not chunk
+ // encoded, the connection cannot be reused
+ return false;
+ }
+ }
+ }
+ // Check for 'Connection' directive
+ if (connType == Headers.CONN_CLOSE) {
+ return false;
+ } else if (connType == Headers.CONN_KEEP_ALIVE) {
+ return true;
+ }
+ // Resorting to protocol version default close connection policy
+ return !ver.lessEquals(HttpVersion.HTTP_1_0);
+ }
+
+ void setCanPersist(HttpEntity entity, ProtocolVersion ver, int connType) {
+ mCanPersist = keepAlive(entity, ver, connType, mHttpContext);
+ }
+
+ void setCanPersist(boolean canPersist) {
+ mCanPersist = canPersist;
+ }
+
+ boolean getCanPersist() {
+ return mCanPersist;
+ }
+
+ /** typically http or https... set by subclass */
+ abstract String getScheme();
+ abstract void closeConnection();
+ abstract AndroidHttpClientConnection openConnection(Request req) throws IOException;
+
+ /**
+ * Prints request queue to log, for debugging.
+ * returns request count
+ */
+ public synchronized String toString() {
+ return mHost.toString();
+ }
+
+ byte[] getBuf() {
+ if (mBuf == null) mBuf = new byte[8192];
+ return mBuf;
+ }
+
+}
diff --git a/core/java/android/net/http/ConnectionThread.java b/core/java/android/net/http/ConnectionThread.java
new file mode 100644
index 0000000..8e759e2
--- /dev/null
+++ b/core/java/android/net/http/ConnectionThread.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.http;
+
+import android.content.Context;
+import android.os.SystemClock;
+
+import org.apache.http.HttpHost;
+
+import java.lang.Thread;
+
+/**
+ * {@hide}
+ */
+class ConnectionThread extends Thread {
+
+ static final int WAIT_TIMEOUT = 5000;
+ static final int WAIT_TICK = 1000;
+
+ // Performance probe
+ long mStartThreadTime;
+ long mCurrentThreadTime;
+
+ private boolean mWaiting;
+ private volatile boolean mRunning = true;
+ private Context mContext;
+ private RequestQueue.ConnectionManager mConnectionManager;
+ private RequestFeeder mRequestFeeder;
+
+ private int mId;
+ Connection mConnection;
+
+ ConnectionThread(Context context,
+ int id,
+ RequestQueue.ConnectionManager connectionManager,
+ RequestFeeder requestFeeder) {
+ super();
+ mContext = context;
+ setName("http" + id);
+ mId = id;
+ mConnectionManager = connectionManager;
+ mRequestFeeder = requestFeeder;
+ }
+
+ void requestStop() {
+ synchronized (mRequestFeeder) {
+ mRunning = false;
+ mRequestFeeder.notify();
+ }
+ }
+
+ /**
+ * Loop until app shutdown. Runs connections in priority
+ * order.
+ */
+ public void run() {
+ android.os.Process.setThreadPriority(
+ android.os.Process.THREAD_PRIORITY_LESS_FAVORABLE);
+
+ mStartThreadTime = -1;
+ mCurrentThreadTime = SystemClock.currentThreadTimeMillis();
+
+ while (mRunning) {
+ Request request;
+
+ /* Get a request to process */
+ request = mRequestFeeder.getRequest();
+
+ /* wait for work */
+ if (request == null) {
+ synchronized(mRequestFeeder) {
+ if (HttpLog.LOGV) HttpLog.v("ConnectionThread: Waiting for work");
+ mWaiting = true;
+ try {
+ if (mStartThreadTime != -1) {
+ mCurrentThreadTime = SystemClock
+ .currentThreadTimeMillis();
+ }
+ mRequestFeeder.wait();
+ } catch (InterruptedException e) {
+ }
+ mWaiting = false;
+ }
+ } else {
+ if (HttpLog.LOGV) HttpLog.v("ConnectionThread: new request " +
+ request.mHost + " " + request );
+
+ HttpHost proxy = mConnectionManager.getProxyHost();
+
+ HttpHost host;
+ if (false) {
+ // Allow https proxy
+ host = proxy == null ? request.mHost : proxy;
+ } else {
+ // Disallow https proxy -- tmob proxy server
+ // serves a request loop for https reqs
+ host = (proxy == null ||
+ request.mHost.getSchemeName().equals("https")) ?
+ request.mHost : proxy;
+ }
+ mConnection = mConnectionManager.getConnection(mContext, host);
+ mConnection.processRequests(request);
+ if (mConnection.getCanPersist()) {
+ if (!mConnectionManager.recycleConnection(host,
+ mConnection)) {
+ mConnection.closeConnection();
+ }
+ } else {
+ mConnection.closeConnection();
+ }
+ mConnection = null;
+ }
+
+ }
+ }
+
+ public synchronized String toString() {
+ String con = mConnection == null ? "" : mConnection.toString();
+ String active = mWaiting ? "w" : "a";
+ return "cid " + mId + " " + active + " " + con;
+ }
+
+}
diff --git a/core/java/android/net/http/DomainNameChecker.java b/core/java/android/net/http/DomainNameChecker.java
new file mode 100644
index 0000000..e4c8009
--- /dev/null
+++ b/core/java/android/net/http/DomainNameChecker.java
@@ -0,0 +1,277 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.http;
+
+import org.bouncycastle.asn1.x509.X509Name;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.security.cert.X509Certificate;
+import java.security.cert.CertificateParsingException;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+import java.util.Vector;
+
+/**
+ * Implements basic domain-name validation as specified by RFC2818.
+ *
+ * {@hide}
+ */
+public class DomainNameChecker {
+ private static Pattern QUICK_IP_PATTERN;
+ static {
+ try {
+ QUICK_IP_PATTERN = Pattern.compile("^[a-f0-9\\.:]+$");
+ } catch (PatternSyntaxException e) {}
+ }
+
+ 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 = QUICK_IP_PATTERN.matcher(domain).matches();
+ if (rval) {
+ rval = domain.equals(
+ InetAddress.getByName(domain).getHostAddress());
+ }
+ } catch (UnknownHostException e) {
+ String errorMessage = e.getMessage();
+ if (errorMessage == null) {
+ errorMessage = "unknown host exception";
+ }
+
+ if (HttpLog.LOGV) {
+ HttpLog.v("DomainNameChecker.isIpAddress(): " + errorMessage);
+ }
+
+ 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 (HttpLog.LOGV) {
+ HttpLog.v("DomainNameChecker.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 (HttpLog.LOGV) {
+ HttpLog.v("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) {
+ // one way we can get here is if an alternative name starts with
+ // '*' character, which is contrary to one interpretation of the
+ // spec (a valid DNS name must start with a letter); there is no
+ // good way around this, and in order to be compatible we proceed
+ // to check the common name (ie, ignore alternative names)
+ if (HttpLog.LOGV) {
+ String errorMessage = e.getMessage();
+ if (errorMessage == null) {
+ errorMessage = "failed to parse certificate";
+ }
+
+ if (HttpLog.LOGV) {
+ HttpLog.v("DomainNameChecker.matchDns(): " + errorMessage);
+ }
+ }
+ }
+
+ if (!hasDns) {
+ X509Name xName = new X509Name(certificate.getSubjectDN().getName());
+ Vector val = xName.getValues();
+ Vector oid = xName.getOIDs();
+ for (int i = 0; i < oid.size(); i++) {
+ if (oid.elementAt(i).equals(X509Name.CN)) {
+ return matchDns(thisDomain, (String)(val.elementAt(i)));
+ }
+ }
+ }
+
+ 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
+ */
+ private static boolean matchDns(String thisDomain, String thatDomain) {
+ if (HttpLog.LOGV) {
+ HttpLog.v("DomainNameChecker.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:
+ // Z.Y.X matches *.Y.X but does not match *.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;
+ }
+ }
+ }
+ }
+
+ 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/android/net/http/EventHandler.java b/core/java/android/net/http/EventHandler.java
new file mode 100644
index 0000000..830d1f1
--- /dev/null
+++ b/core/java/android/net/http/EventHandler.java
@@ -0,0 +1,147 @@
+/*
+ * 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.net.http;
+
+
+/**
+ * Callbacks in this interface are made as an HTTP request is
+ * processed. The normal order of callbacks is status(), headers(),
+ * then multiple data() then endData(). handleSslErrorRequest(), if
+ * there is an SSL certificate error. error() can occur anywhere
+ * in the transaction.
+ *
+ * {@hide}
+ */
+
+public interface EventHandler {
+
+ /**
+ * Error codes used in the error() callback. Positive error codes
+ * are reserved for codes sent by http servers. Negative error
+ * codes are connection/parsing failures, etc.
+ */
+
+ /** Success */
+ public static final int OK = 0;
+ /** Generic error */
+ public static final int ERROR = -1;
+ /** Server or proxy hostname lookup failed */
+ public static final int ERROR_LOOKUP = -2;
+ /** Unsupported authentication scheme (ie, not basic or digest) */
+ public static final int ERROR_UNSUPPORTED_AUTH_SCHEME = -3;
+ /** User authentication failed on server */
+ public static final int ERROR_AUTH = -4;
+ /** User authentication failed on proxy */
+ public static final int ERROR_PROXYAUTH = -5;
+ /** Could not connect to server */
+ public static final int ERROR_CONNECT = -6;
+ /** Failed to write to or read from server */
+ public static final int ERROR_IO = -7;
+ /** Connection timed out */
+ public static final int ERROR_TIMEOUT = -8;
+ /** Too many redirects */
+ public static final int ERROR_REDIRECT_LOOP = -9;
+ /** Unsupported URI scheme (ie, not http, https, etc) */
+ public static final int ERROR_UNSUPPORTED_SCHEME = -10;
+ /** Failed to perform SSL handshake */
+ public static final int ERROR_FAILED_SSL_HANDSHAKE = -11;
+ /** Bad URL */
+ public static final int ERROR_BAD_URL = -12;
+ /** Generic file error for file:/// loads */
+ public static final int FILE_ERROR = -13;
+ /** File not found error for file:/// loads */
+ public static final int FILE_NOT_FOUND_ERROR = -14;
+ /** Too many requests queued */
+ public static final int TOO_MANY_REQUESTS_ERROR = -15;
+
+ final static int[] errorStringResources = {
+ com.android.internal.R.string.httpErrorOk,
+ com.android.internal.R.string.httpError,
+ com.android.internal.R.string.httpErrorLookup,
+ com.android.internal.R.string.httpErrorUnsupportedAuthScheme,
+ com.android.internal.R.string.httpErrorAuth,
+ com.android.internal.R.string.httpErrorProxyAuth,
+ com.android.internal.R.string.httpErrorConnect,
+ com.android.internal.R.string.httpErrorIO,
+ com.android.internal.R.string.httpErrorTimeout,
+ com.android.internal.R.string.httpErrorRedirectLoop,
+ com.android.internal.R.string.httpErrorUnsupportedScheme,
+ com.android.internal.R.string.httpErrorFailedSslHandshake,
+ com.android.internal.R.string.httpErrorBadUrl,
+ com.android.internal.R.string.httpErrorFile,
+ com.android.internal.R.string.httpErrorFileNotFound,
+ com.android.internal.R.string.httpErrorTooManyRequests
+ };
+
+ /**
+ * Called after status line has been sucessfully processed.
+ * @param major_version HTTP version advertised by server. major
+ * is the part before the "."
+ * @param minor_version HTTP version advertised by server. minor
+ * is the part after the "."
+ * @param code HTTP Status code. See RFC 2616.
+ * @param reason_phrase Textual explanation sent by server
+ */
+ public void status(int major_version,
+ int minor_version,
+ int code,
+ String reason_phrase);
+
+ /**
+ * Called after all headers are successfully processed.
+ */
+ public void headers(Headers headers);
+
+ /**
+ * An array containing all or part of the http body as read from
+ * the server.
+ * @param data A byte array containing the content
+ * @param len The length of valid content in data
+ *
+ * Note: chunked and compressed encodings are handled within
+ * android.net.http. Decoded data is passed through this
+ * interface.
+ */
+ public void data(byte[] data, int len);
+
+ /**
+ * Called when the document is completely read. No more data()
+ * callbacks will be made after this call
+ */
+ public void endData();
+
+ /**
+ * SSL certificate callback called every time a resource is
+ * loaded via a secure connection
+ */
+ public void certificate(SslCertificate certificate);
+
+ /**
+ * There was trouble.
+ * @param id One of the error codes defined below
+ * @param description of error
+ */
+ public void error(int id, String description);
+
+ /**
+ * SSL certificate error callback. Handles SSL error(s) on the way
+ * up to the user. The callback has to make sure that restartConnection() is called,
+ * otherwise the connection will be suspended indefinitely.
+ */
+ public void handleSslErrorRequest(SslError error);
+
+}
diff --git a/core/java/android/net/http/Headers.java b/core/java/android/net/http/Headers.java
new file mode 100644
index 0000000..5d85ba4
--- /dev/null
+++ b/core/java/android/net/http/Headers.java
@@ -0,0 +1,384 @@
+/*
+ * 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.net.http;
+
+import android.util.Config;
+import android.util.Log;
+
+import java.util.ArrayList;
+
+import org.apache.http.HeaderElement;
+import org.apache.http.entity.ContentLengthStrategy;
+import org.apache.http.message.BasicHeaderValueParser;
+import org.apache.http.message.ParserCursor;
+import org.apache.http.protocol.HTTP;
+import org.apache.http.util.CharArrayBuffer;
+
+/**
+ * Manages received headers
+ *
+ * {@hide}
+ */
+public final class Headers {
+ private static final String LOGTAG = "Http";
+
+ // header parsing constant
+ /**
+ * indicate HTTP 1.0 connection close after the response
+ */
+ public final static int CONN_CLOSE = 1;
+ /**
+ * indicate HTTP 1.1 connection keep alive
+ */
+ public final static int CONN_KEEP_ALIVE = 2;
+
+ // initial values.
+ public final static int NO_CONN_TYPE = 0;
+ public final static long NO_TRANSFER_ENCODING = 0;
+ public final static long NO_CONTENT_LENGTH = -1;
+
+ // header string
+ public final static String TRANSFER_ENCODING = "transfer-encoding";
+ public final static String CONTENT_LEN = "content-length";
+ public final static String CONTENT_TYPE = "content-type";
+ public final static String CONTENT_ENCODING = "content-encoding";
+ public final static String CONN_DIRECTIVE = "connection";
+
+ public final static String LOCATION = "location";
+ public final static String PROXY_CONNECTION = "proxy-connection";
+
+ public final static String WWW_AUTHENTICATE = "www-authenticate";
+ public final static String PROXY_AUTHENTICATE = "proxy-authenticate";
+ public final static String CONTENT_DISPOSITION = "content-disposition";
+ public final static String ACCEPT_RANGES = "accept-ranges";
+ public final static String EXPIRES = "expires";
+ public final static String CACHE_CONTROL = "cache-control";
+ public final static String LAST_MODIFIED = "last-modified";
+ public final static String ETAG = "etag";
+ public final static String SET_COOKIE = "set-cookie";
+ public final static String PRAGMA = "pragma";
+ public final static String REFRESH = "refresh";
+
+ // following hash are generated by String.hashCode()
+ private final static int HASH_TRANSFER_ENCODING = 1274458357;
+ private final static int HASH_CONTENT_LEN = -1132779846;
+ private final static int HASH_CONTENT_TYPE = 785670158;
+ private final static int HASH_CONTENT_ENCODING = 2095084583;
+ private final static int HASH_CONN_DIRECTIVE = -775651618;
+ private final static int HASH_LOCATION = 1901043637;
+ private final static int HASH_PROXY_CONNECTION = 285929373;
+ private final static int HASH_WWW_AUTHENTICATE = -243037365;
+ private final static int HASH_PROXY_AUTHENTICATE = -301767724;
+ private final static int HASH_CONTENT_DISPOSITION = -1267267485;
+ private final static int HASH_ACCEPT_RANGES = 1397189435;
+ private final static int HASH_EXPIRES = -1309235404;
+ private final static int HASH_CACHE_CONTROL = -208775662;
+ private final static int HASH_LAST_MODIFIED = 150043680;
+ private final static int HASH_ETAG = 3123477;
+ private final static int HASH_SET_COOKIE = 1237214767;
+ private final static int HASH_PRAGMA = -980228804;
+ private final static int HASH_REFRESH = 1085444827;
+
+ private long transferEncoding;
+ private long contentLength; // Content length of the incoming data
+ private int connectionType;
+
+ private String contentType;
+ private String contentEncoding;
+ private String location;
+ private String wwwAuthenticate;
+ private String proxyAuthenticate;
+ private String contentDisposition;
+ private String acceptRanges;
+ private String expires;
+ private String cacheControl;
+ private String lastModified;
+ private String etag;
+ private String pragma;
+ private String refresh;
+ private ArrayList<String> cookies = new ArrayList<String>(2);
+
+ public Headers() {
+ transferEncoding = NO_TRANSFER_ENCODING;
+ contentLength = NO_CONTENT_LENGTH;
+ connectionType = NO_CONN_TYPE;
+ }
+
+ public void parseHeader(CharArrayBuffer buffer) {
+ int pos = CharArrayBuffers.setLowercaseIndexOf(buffer, ':');
+ if (pos == -1) {
+ return;
+ }
+ String name = buffer.substringTrimmed(0, pos);
+ if (name.length() == 0) {
+ return;
+ }
+ pos++;
+
+ if (HttpLog.LOGV) {
+ String val = buffer.substringTrimmed(pos, buffer.length());
+ HttpLog.v("hdr " + buffer.length() + " " + buffer);
+ }
+
+ switch (name.hashCode()) {
+ case HASH_TRANSFER_ENCODING:
+ if (name.equals(TRANSFER_ENCODING)) {
+ // headers.transferEncoding =
+ HeaderElement[] encodings = BasicHeaderValueParser.DEFAULT
+ .parseElements(buffer, new ParserCursor(pos,
+ buffer.length()));
+ // The chunked encoding must be the last one applied RFC2616,
+ // 14.41
+ int len = encodings.length;
+ if (HTTP.IDENTITY_CODING.equalsIgnoreCase(buffer
+ .substringTrimmed(pos, buffer.length()))) {
+ transferEncoding = ContentLengthStrategy.IDENTITY;
+ } else if ((len > 0)
+ && (HTTP.CHUNK_CODING
+ .equalsIgnoreCase(encodings[len - 1].getName()))) {
+ transferEncoding = ContentLengthStrategy.CHUNKED;
+ } else {
+ transferEncoding = ContentLengthStrategy.IDENTITY;
+ }
+ }
+ break;
+ case HASH_CONTENT_LEN:
+ if (name.equals(CONTENT_LEN)) {
+ try {
+ contentLength = Long.parseLong(buffer.substringTrimmed(pos,
+ buffer.length()));
+ } catch (NumberFormatException e) {
+ if (Config.LOGV) {
+ Log.v(LOGTAG, "Headers.headers(): error parsing"
+ + " content length: " + buffer.toString());
+ }
+ }
+ }
+ break;
+ case HASH_CONTENT_TYPE:
+ if (name.equals(CONTENT_TYPE)) {
+ contentType = buffer.substringTrimmed(pos, buffer.length());
+ }
+ break;
+ case HASH_CONTENT_ENCODING:
+ if (name.equals(CONTENT_ENCODING)) {
+ contentEncoding = buffer.substringTrimmed(pos, buffer.length());
+ }
+ break;
+ case HASH_CONN_DIRECTIVE:
+ if (name.equals(CONN_DIRECTIVE)) {
+ setConnectionType(buffer, pos);
+ }
+ break;
+ case HASH_LOCATION:
+ if (name.equals(LOCATION)) {
+ location = buffer.substringTrimmed(pos, buffer.length());
+ }
+ break;
+ case HASH_PROXY_CONNECTION:
+ if (name.equals(PROXY_CONNECTION)) {
+ setConnectionType(buffer, pos);
+ }
+ break;
+ case HASH_WWW_AUTHENTICATE:
+ if (name.equals(WWW_AUTHENTICATE)) {
+ wwwAuthenticate = buffer.substringTrimmed(pos, buffer.length());
+ }
+ break;
+ case HASH_PROXY_AUTHENTICATE:
+ if (name.equals(PROXY_AUTHENTICATE)) {
+ proxyAuthenticate = buffer.substringTrimmed(pos, buffer
+ .length());
+ }
+ break;
+ case HASH_CONTENT_DISPOSITION:
+ if (name.equals(CONTENT_DISPOSITION)) {
+ contentDisposition = buffer.substringTrimmed(pos, buffer
+ .length());
+ }
+ break;
+ case HASH_ACCEPT_RANGES:
+ if (name.equals(ACCEPT_RANGES)) {
+ acceptRanges = buffer.substringTrimmed(pos, buffer.length());
+ }
+ break;
+ case HASH_EXPIRES:
+ if (name.equals(EXPIRES)) {
+ expires = buffer.substringTrimmed(pos, buffer.length());
+ }
+ break;
+ case HASH_CACHE_CONTROL:
+ if (name.equals(CACHE_CONTROL)) {
+ cacheControl = buffer.substringTrimmed(pos, buffer.length());
+ }
+ break;
+ case HASH_LAST_MODIFIED:
+ if (name.equals(LAST_MODIFIED)) {
+ lastModified = buffer.substringTrimmed(pos, buffer.length());
+ }
+ break;
+ case HASH_ETAG:
+ if (name.equals(ETAG)) {
+ etag = buffer.substringTrimmed(pos, buffer.length());
+ }
+ break;
+ case HASH_SET_COOKIE:
+ if (name.equals(SET_COOKIE)) {
+ cookies.add(buffer.substringTrimmed(pos, buffer.length()));
+ }
+ break;
+ case HASH_PRAGMA:
+ if (name.equals(PRAGMA)) {
+ pragma = buffer.substringTrimmed(pos, buffer.length());
+ }
+ break;
+ case HASH_REFRESH:
+ if (name.equals(REFRESH)) {
+ refresh = buffer.substringTrimmed(pos, buffer.length());
+ }
+ break;
+ default:
+ // ignore
+ }
+ }
+
+ public long getTransferEncoding() {
+ return transferEncoding;
+ }
+
+ public long getContentLength() {
+ return contentLength;
+ }
+
+ public int getConnectionType() {
+ return connectionType;
+ }
+
+ private void setConnectionType(CharArrayBuffer buffer, int pos) {
+ if (CharArrayBuffers.containsIgnoreCaseTrimmed(
+ buffer, pos, HTTP.CONN_CLOSE)) {
+ connectionType = CONN_CLOSE;
+ } else if (CharArrayBuffers.containsIgnoreCaseTrimmed(
+ buffer, pos, HTTP.CONN_KEEP_ALIVE)) {
+ connectionType = CONN_KEEP_ALIVE;
+ }
+ }
+
+ public String getContentType() {
+ return this.contentType;
+ }
+
+ public String getContentEncoding() {
+ return this.contentEncoding;
+ }
+
+ public String getLocation() {
+ return this.location;
+ }
+
+ public String getWwwAuthenticate() {
+ return this.wwwAuthenticate;
+ }
+
+ public String getProxyAuthenticate() {
+ return this.proxyAuthenticate;
+ }
+
+ public String getContentDisposition() {
+ return this.contentDisposition;
+ }
+
+ public String getAcceptRanges() {
+ return this.acceptRanges;
+ }
+
+ public String getExpires() {
+ return this.expires;
+ }
+
+ public String getCacheControl() {
+ return this.cacheControl;
+ }
+
+ public String getLastModified() {
+ return this.lastModified;
+ }
+
+ public String getEtag() {
+ return this.etag;
+ }
+
+ public ArrayList<String> getSetCookie() {
+ return this.cookies;
+ }
+
+ public String getPragma() {
+ return this.pragma;
+ }
+
+ public String getRefresh() {
+ return this.refresh;
+ }
+
+ public void setContentLength(long value) {
+ this.contentLength = value;
+ }
+
+ public void setContentType(String value) {
+ this.contentType = value;
+ }
+
+ public void setContentEncoding(String value) {
+ this.contentEncoding = value;
+ }
+
+ public void setLocation(String value) {
+ this.location = value;
+ }
+
+ public void setWwwAuthenticate(String value) {
+ this.wwwAuthenticate = value;
+ }
+
+ public void setProxyAuthenticate(String value) {
+ this.proxyAuthenticate = value;
+ }
+
+ public void setContentDisposition(String value) {
+ this.contentDisposition = value;
+ }
+
+ public void setAcceptRanges(String value) {
+ this.acceptRanges = value;
+ }
+
+ public void setExpires(String value) {
+ this.expires = value;
+ }
+
+ public void setCacheControl(String value) {
+ this.cacheControl = value;
+ }
+
+ public void setLastModified(String value) {
+ this.lastModified = value;
+ }
+
+ public void setEtag(String value) {
+ this.etag = value;
+ }
+}
diff --git a/core/java/android/net/http/HttpAuthHeader.java b/core/java/android/net/http/HttpAuthHeader.java
new file mode 100644
index 0000000..d41284c
--- /dev/null
+++ b/core/java/android/net/http/HttpAuthHeader.java
@@ -0,0 +1,422 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.http;
+
+/**
+ * HttpAuthHeader: a class to store HTTP authentication-header parameters.
+ * For more information, see: RFC 2617: HTTP Authentication.
+ *
+ * {@hide}
+ */
+public class HttpAuthHeader {
+ /**
+ * Possible HTTP-authentication header tokens to search for:
+ */
+ public final static String BASIC_TOKEN = "Basic";
+ public final static String DIGEST_TOKEN = "Digest";
+
+ private final static String REALM_TOKEN = "realm";
+ private final static String NONCE_TOKEN = "nonce";
+ private final static String STALE_TOKEN = "stale";
+ private final static String OPAQUE_TOKEN = "opaque";
+ private final static String QOP_TOKEN = "qop";
+ private final static String ALGORITHM_TOKEN = "algorithm";
+
+ /**
+ * An authentication scheme. We currently support two different schemes:
+ * HttpAuthHeader.BASIC - basic, and
+ * HttpAuthHeader.DIGEST - digest (algorithm=MD5, QOP="auth").
+ */
+ private int mScheme;
+
+ public static final int UNKNOWN = 0;
+ public static final int BASIC = 1;
+ public static final int DIGEST = 2;
+
+ /**
+ * A flag, indicating that the previous request from the client was
+ * rejected because the nonce value was stale. If stale is TRUE
+ * (case-insensitive), the client may wish to simply retry the request
+ * with a new encrypted response, without reprompting the user for a
+ * new username and password.
+ */
+ private boolean mStale;
+
+ /**
+ * A string to be displayed to users so they know which username and
+ * password to use.
+ */
+ private String mRealm;
+
+ /**
+ * A server-specified data string which should be uniquely generated
+ * each time a 401 response is made.
+ */
+ private String mNonce;
+
+ /**
+ * A string of data, specified by the server, which should be returned
+ * by the client unchanged in the Authorization header of subsequent
+ * requests with URIs in the same protection space.
+ */
+ private String mOpaque;
+
+ /**
+ * This directive is optional, but is made so only for backward
+ * compatibility with RFC 2069 [6]; it SHOULD be used by all
+ * implementations compliant with this version of the Digest scheme.
+ * If present, it is a quoted string of one or more tokens indicating
+ * the "quality of protection" values supported by the server. The
+ * value "auth" indicates authentication; the value "auth-int"
+ * indicates authentication with integrity protection.
+ */
+ private String mQop;
+
+ /**
+ * A string indicating a pair of algorithms used to produce the digest
+ * and a checksum. If this is not present it is assumed to be "MD5".
+ */
+ private String mAlgorithm;
+
+ /**
+ * Is this authentication request a proxy authentication request?
+ */
+ private boolean mIsProxy;
+
+ /**
+ * Username string we get from the user.
+ */
+ private String mUsername;
+
+ /**
+ * Password string we get from the user.
+ */
+ private String mPassword;
+
+ /**
+ * Creates a new HTTP-authentication header object from the
+ * input header string.
+ * The header string is assumed to contain parameters of at
+ * most one authentication-scheme (ensured by the caller).
+ */
+ public HttpAuthHeader(String header) {
+ if (header != null) {
+ parseHeader(header);
+ }
+ }
+
+ /**
+ * @return True iff this is a proxy authentication header.
+ */
+ public boolean isProxy() {
+ return mIsProxy;
+ }
+
+ /**
+ * Marks this header as a proxy authentication header.
+ */
+ public void setProxy() {
+ mIsProxy = true;
+ }
+
+ /**
+ * @return The username string.
+ */
+ public String getUsername() {
+ return mUsername;
+ }
+
+ /**
+ * Sets the username string.
+ */
+ public void setUsername(String username) {
+ mUsername = username;
+ }
+
+ /**
+ * @return The password string.
+ */
+ public String getPassword() {
+ return mPassword;
+ }
+
+ /**
+ * Sets the password string.
+ */
+ public void setPassword(String password) {
+ mPassword = password;
+ }
+
+ /**
+ * @return True iff this is the BASIC-authentication request.
+ */
+ public boolean isBasic () {
+ return mScheme == BASIC;
+ }
+
+ /**
+ * @return True iff this is the DIGEST-authentication request.
+ */
+ public boolean isDigest() {
+ return mScheme == DIGEST;
+ }
+
+ /**
+ * @return The authentication scheme requested. We currently
+ * support two schemes:
+ * HttpAuthHeader.BASIC - basic, and
+ * HttpAuthHeader.DIGEST - digest (algorithm=MD5, QOP="auth").
+ */
+ public int getScheme() {
+ return mScheme;
+ }
+
+ /**
+ * @return True if indicating that the previous request from
+ * the client was rejected because the nonce value was stale.
+ */
+ public boolean getStale() {
+ return mStale;
+ }
+
+ /**
+ * @return The realm value or null if there is none.
+ */
+ public String getRealm() {
+ return mRealm;
+ }
+
+ /**
+ * @return The nonce value or null if there is none.
+ */
+ public String getNonce() {
+ return mNonce;
+ }
+
+ /**
+ * @return The opaque value or null if there is none.
+ */
+ public String getOpaque() {
+ return mOpaque;
+ }
+
+ /**
+ * @return The QOP ("quality-of_protection") value or null if
+ * there is none. The QOP value is always lower-case.
+ */
+ public String getQop() {
+ return mQop;
+ }
+
+ /**
+ * @return The name of the algorithm used or null if there is
+ * none. By default, MD5 is used.
+ */
+ public String getAlgorithm() {
+ return mAlgorithm;
+ }
+
+ /**
+ * @return True iff the authentication scheme requested by the
+ * server is supported; currently supported schemes:
+ * BASIC,
+ * DIGEST (only algorithm="md5", no qop or qop="auth).
+ */
+ public boolean isSupportedScheme() {
+ // it is a good idea to enforce non-null realms!
+ if (mRealm != null) {
+ if (mScheme == BASIC) {
+ return true;
+ } else {
+ if (mScheme == DIGEST) {
+ return
+ mAlgorithm.equals("md5") &&
+ (mQop == null || mQop.equals("auth"));
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Parses the header scheme name and then scheme parameters if
+ * the scheme is supported.
+ */
+ private void parseHeader(String header) {
+ if (HttpLog.LOGV) {
+ HttpLog.v("HttpAuthHeader.parseHeader(): header: " + header);
+ }
+
+ if (header != null) {
+ String parameters = parseScheme(header);
+ if (parameters != null) {
+ // if we have a supported scheme
+ if (mScheme != UNKNOWN) {
+ parseParameters(parameters);
+ }
+ }
+ }
+ }
+
+ /**
+ * Parses the authentication scheme name. If we have a Digest
+ * scheme, sets the algorithm value to the default of MD5.
+ * @return The authentication scheme parameters string to be
+ * parsed later (if the scheme is supported) or null if failed
+ * to parse the scheme (the header value is null?).
+ */
+ private String parseScheme(String header) {
+ if (header != null) {
+ int i = header.indexOf(' ');
+ if (i >= 0) {
+ String scheme = header.substring(0, i).trim();
+ if (scheme.equalsIgnoreCase(DIGEST_TOKEN)) {
+ mScheme = DIGEST;
+
+ // md5 is the default algorithm!!!
+ mAlgorithm = "md5";
+ } else {
+ if (scheme.equalsIgnoreCase(BASIC_TOKEN)) {
+ mScheme = BASIC;
+ }
+ }
+
+ return header.substring(i + 1);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Parses a comma-separated list of authentification scheme
+ * parameters.
+ */
+ private void parseParameters(String parameters) {
+ if (HttpLog.LOGV) {
+ HttpLog.v("HttpAuthHeader.parseParameters():" +
+ " parameters: " + parameters);
+ }
+
+ if (parameters != null) {
+ int i;
+ do {
+ i = parameters.indexOf(',');
+ if (i < 0) {
+ // have only one parameter
+ parseParameter(parameters);
+ } else {
+ parseParameter(parameters.substring(0, i));
+ parameters = parameters.substring(i + 1);
+ }
+ } while (i >= 0);
+ }
+ }
+
+ /**
+ * Parses a single authentication scheme parameter. The parameter
+ * string is expected to follow the format: PARAMETER=VALUE.
+ */
+ private void parseParameter(String parameter) {
+ if (parameter != null) {
+ // here, we are looking for the 1st occurence of '=' only!!!
+ int i = parameter.indexOf('=');
+ if (i >= 0) {
+ String token = parameter.substring(0, i).trim();
+ String value =
+ trimDoubleQuotesIfAny(parameter.substring(i + 1).trim());
+
+ if (HttpLog.LOGV) {
+ HttpLog.v("HttpAuthHeader.parseParameter():" +
+ " token: " + token +
+ " value: " + value);
+ }
+
+ if (token.equalsIgnoreCase(REALM_TOKEN)) {
+ mRealm = value;
+ } else {
+ if (mScheme == DIGEST) {
+ parseParameter(token, value);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * If the token is a known parameter name, parses and initializes
+ * the token value.
+ */
+ private void parseParameter(String token, String value) {
+ if (token != null && value != null) {
+ if (token.equalsIgnoreCase(NONCE_TOKEN)) {
+ mNonce = value;
+ return;
+ }
+
+ if (token.equalsIgnoreCase(STALE_TOKEN)) {
+ parseStale(value);
+ return;
+ }
+
+ if (token.equalsIgnoreCase(OPAQUE_TOKEN)) {
+ mOpaque = value;
+ return;
+ }
+
+ if (token.equalsIgnoreCase(QOP_TOKEN)) {
+ mQop = value.toLowerCase();
+ return;
+ }
+
+ if (token.equalsIgnoreCase(ALGORITHM_TOKEN)) {
+ mAlgorithm = value.toLowerCase();
+ return;
+ }
+ }
+ }
+
+ /**
+ * Parses and initializes the 'stale' paramer value. Any value
+ * different from case-insensitive "true" is considered "false".
+ */
+ private void parseStale(String value) {
+ if (value != null) {
+ if (value.equalsIgnoreCase("true")) {
+ mStale = true;
+ }
+ }
+ }
+
+ /**
+ * Trims double-quotes around a parameter value if there are any.
+ * @return The string value without the outermost pair of double-
+ * quotes or null if the original value is null.
+ */
+ static private String trimDoubleQuotesIfAny(String value) {
+ if (value != null) {
+ int len = value.length();
+ if (len > 2 &&
+ value.charAt(0) == '\"' && value.charAt(len - 1) == '\"') {
+ return value.substring(1, len - 1);
+ }
+ }
+
+ return value;
+ }
+}
diff --git a/core/java/android/net/http/HttpConnection.java b/core/java/android/net/http/HttpConnection.java
new file mode 100644
index 0000000..8b12d0b
--- /dev/null
+++ b/core/java/android/net/http/HttpConnection.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.http;
+
+import android.content.Context;
+
+import java.net.Socket;
+import java.io.IOException;
+
+import org.apache.http.HttpClientConnection;
+import org.apache.http.HttpHost;
+import org.apache.http.impl.DefaultHttpClientConnection;
+import org.apache.http.params.BasicHttpParams;
+import org.apache.http.params.HttpConnectionParams;
+
+/**
+ * A requestConnection connecting to a normal (non secure) http server
+ *
+ * {@hide}
+ */
+class HttpConnection extends Connection {
+
+ HttpConnection(Context context, HttpHost host,
+ RequestQueue.ConnectionManager connectionManager,
+ RequestFeeder requestFeeder) {
+ super(context, host, connectionManager, requestFeeder);
+ }
+
+ /**
+ * Opens the connection to a http server
+ *
+ * @return the opened low level connection
+ * @throws IOException if the connection fails for any reason.
+ */
+ @Override
+ AndroidHttpClientConnection openConnection(Request req) throws IOException {
+
+ // Update the certificate info (connection not secure - set to null)
+ EventHandler eventHandler = req.getEventHandler();
+ mCertificate = null;
+ eventHandler.certificate(mCertificate);
+
+ AndroidHttpClientConnection conn = new AndroidHttpClientConnection();
+ BasicHttpParams params = new BasicHttpParams();
+ Socket sock = new Socket(mHost.getHostName(), mHost.getPort());
+ params.setIntParameter(HttpConnectionParams.SOCKET_BUFFER_SIZE, 8192);
+ conn.bind(sock, params);
+ return conn;
+ }
+
+ /**
+ * Closes the low level connection.
+ *
+ * If an exception is thrown then it is assumed that the
+ * connection will have been closed (to the extent possible)
+ * anyway and the caller does not need to take any further action.
+ *
+ */
+ void closeConnection() {
+ try {
+ if (mHttpClientConnection != null && mHttpClientConnection.isOpen()) {
+ mHttpClientConnection.close();
+ }
+ } catch (IOException e) {
+ if (HttpLog.LOGV) HttpLog.v(
+ "closeConnection(): failed closing connection " +
+ mHost);
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Restart a secure connection suspended waiting for user interaction.
+ */
+ void restartConnection(boolean abort) {
+ // not required for plain http connections
+ }
+
+ String getScheme() {
+ return "http";
+ }
+}
diff --git a/core/java/android/net/http/HttpLog.java b/core/java/android/net/http/HttpLog.java
new file mode 100644
index 0000000..30bf647
--- /dev/null
+++ b/core/java/android/net/http/HttpLog.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * package-level logging flag
+ */
+
+package android.net.http;
+
+import android.os.SystemClock;
+
+import android.util.Log;
+import android.util.Config;
+
+/**
+ * {@hide}
+ */
+class HttpLog {
+ private final static String LOGTAG = "http";
+
+ private static final boolean DEBUG = false;
+ static final boolean LOGV = DEBUG ? Config.LOGD : Config.LOGV;
+
+ static void v(String logMe) {
+ Log.v(LOGTAG, SystemClock.uptimeMillis() + " " + Thread.currentThread().getName() + " " + logMe);
+ }
+
+ static void e(String logMe) {
+ Log.e(LOGTAG, logMe);
+ }
+}
diff --git a/core/java/android/net/http/HttpsConnection.java b/core/java/android/net/http/HttpsConnection.java
new file mode 100644
index 0000000..fe02d3e
--- /dev/null
+++ b/core/java/android/net/http/HttpsConnection.java
@@ -0,0 +1,427 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.http;
+
+import android.content.Context;
+
+import junit.framework.Assert;
+
+import java.io.IOException;
+
+import java.security.cert.X509Certificate;
+
+import java.net.Socket;
+import java.net.InetSocketAddress;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+
+import org.apache.http.Header;
+import org.apache.http.HttpClientConnection;
+import org.apache.http.HttpException;
+import org.apache.http.HttpHost;
+import org.apache.http.HttpRequest;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.ParseException;
+import org.apache.http.ProtocolVersion;
+import org.apache.http.StatusLine;
+import org.apache.http.impl.DefaultHttpClientConnection;
+import org.apache.http.message.BasicHttpRequest;
+import org.apache.http.params.BasicHttpParams;
+import org.apache.http.params.HttpParams;
+import org.apache.http.params.HttpConnectionParams;
+
+/**
+ * Simple exception we throw if the SSL connection is closed by the user.
+ *
+ * {@hide}
+ */
+class SSLConnectionClosedByUserException extends SSLException {
+
+ public SSLConnectionClosedByUserException(String reason) {
+ super(reason);
+ }
+}
+
+/**
+ * A Connection connecting to a secure http server or tunneling through
+ * a http proxy server to a https server.
+ */
+class HttpsConnection extends Connection {
+
+ /**
+ * SSL context
+ */
+ private static SSLContext mSslContext = null;
+
+ /**
+ * SSL socket factory
+ */
+ private static SSLSocketFactory mSslSocketFactory = null;
+
+ static {
+ // initialize the socket factory
+ try {
+ mSslContext = SSLContext.getInstance("TLS");
+ if (mSslContext != null) {
+ // here, trust managers is a single trust-all manager
+ TrustManager[] trustManagers = new TrustManager[] {
+ new X509TrustManager() {
+ public X509Certificate[] getAcceptedIssuers() {
+ return null;
+ }
+
+ public void checkClientTrusted(
+ X509Certificate[] certs, String authType) {
+ }
+
+ public void checkServerTrusted(
+ X509Certificate[] certs, String authType) {
+ }
+ }
+ };
+
+ mSslContext.init(null, trustManagers, null);
+ mSslSocketFactory = mSslContext.getSocketFactory();
+ }
+ } catch (Exception t) {
+ if (HttpLog.LOGV) {
+ HttpLog.v("HttpsConnection: failed to initialize the socket factory");
+ }
+ }
+ }
+
+ /**
+ * @return The shared SSL context.
+ */
+ /*package*/ static SSLContext getContext() {
+ return mSslContext;
+ }
+
+ /**
+ * Object to wait on when suspending the SSL connection
+ */
+ private Object mSuspendLock = new Object();
+
+ /**
+ * True if the connection is suspended pending the result of asking the
+ * user about an error.
+ */
+ private boolean mSuspended = false;
+
+ /**
+ * True if the connection attempt should be aborted due to an ssl
+ * error.
+ */
+ private boolean mAborted = false;
+
+ /**
+ * Contructor for a https connection.
+ */
+ HttpsConnection(Context context, HttpHost host,
+ RequestQueue.ConnectionManager connectionManager,
+ RequestFeeder requestFeeder) {
+ super(context, host, connectionManager, requestFeeder);
+ }
+
+ /**
+ * Sets the server SSL certificate associated with this
+ * connection.
+ * @param certificate The SSL certificate
+ */
+ /* package */ void setCertificate(SslCertificate certificate) {
+ mCertificate = certificate;
+ }
+
+ /**
+ * Opens the connection to a http server or proxy.
+ *
+ * @return the opened low level connection
+ * @throws IOException if the connection fails for any reason.
+ */
+ @Override
+ AndroidHttpClientConnection openConnection(Request req) throws IOException {
+ SSLSocket sslSock = null;
+
+ HttpHost proxyHost = mConnectionManager.getProxyHost();
+ if (proxyHost != null) {
+ // If we have a proxy set, we first send a CONNECT request
+ // to the proxy; if the proxy returns 200 OK, we negotiate
+ // a secure connection to the target server via the proxy.
+ // If the request fails, we drop it, but provide the event
+ // handler with the response status and headers. The event
+ // handler is then responsible for cancelling the load or
+ // issueing a new request.
+ AndroidHttpClientConnection proxyConnection = null;
+ Socket proxySock = null;
+ try {
+ proxySock = new Socket
+ (proxyHost.getHostName(), proxyHost.getPort());
+
+ proxySock.setSoTimeout(60 * 1000);
+
+ proxyConnection = new AndroidHttpClientConnection();
+ HttpParams params = new BasicHttpParams();
+ HttpConnectionParams.setSocketBufferSize(params, 8192);
+
+ proxyConnection.bind(proxySock, params);
+ } catch(IOException e) {
+ if (proxyConnection != null) {
+ proxyConnection.close();
+ }
+
+ String errorMessage = e.getMessage();
+ if (errorMessage == null) {
+ errorMessage =
+ "failed to establish a connection to the proxy";
+ }
+
+ throw new IOException(errorMessage);
+ }
+
+ StatusLine statusLine = null;
+ int statusCode = 0;
+ Headers headers = new Headers();
+ try {
+ BasicHttpRequest proxyReq = new BasicHttpRequest
+ ("CONNECT", mHost.toHostString());
+
+ // add all 'proxy' headers from the original request
+ for (Header h : req.mHttpRequest.getAllHeaders()) {
+ String headerName = h.getName().toLowerCase();
+ if (headerName.startsWith("proxy") || headerName.equals("keep-alive")) {
+ proxyReq.addHeader(h);
+ }
+ }
+
+ proxyConnection.sendRequestHeader(proxyReq);
+ proxyConnection.flush();
+
+ // it is possible to receive informational status
+ // codes prior to receiving actual headers;
+ // all those status codes are smaller than OK 200
+ // a loop is a standard way of dealing with them
+ do {
+ statusLine = proxyConnection.parseResponseHeader(headers);
+ statusCode = statusLine.getStatusCode();
+ } while (statusCode < HttpStatus.SC_OK);
+ } catch (ParseException e) {
+ String errorMessage = e.getMessage();
+ if (errorMessage == null) {
+ errorMessage =
+ "failed to send a CONNECT request";
+ }
+
+ throw new IOException(errorMessage);
+ } catch (HttpException e) {
+ String errorMessage = e.getMessage();
+ if (errorMessage == null) {
+ errorMessage =
+ "failed to send a CONNECT request";
+ }
+
+ throw new IOException(errorMessage);
+ } catch (IOException e) {
+ String errorMessage = e.getMessage();
+ if (errorMessage == null) {
+ errorMessage =
+ "failed to send a CONNECT request";
+ }
+
+ throw new IOException(errorMessage);
+ }
+
+ if (statusCode == HttpStatus.SC_OK) {
+ try {
+ synchronized (mSslSocketFactory) {
+ sslSock = (SSLSocket) mSslSocketFactory.createSocket(
+ proxySock, mHost.getHostName(), mHost.getPort(), true);
+ }
+ } catch(IOException e) {
+ if (sslSock != null) {
+ sslSock.close();
+ }
+
+ String errorMessage = e.getMessage();
+ if (errorMessage == null) {
+ errorMessage =
+ "failed to create an SSL socket";
+ }
+ throw new IOException(errorMessage);
+ }
+ } else {
+ // if the code is not OK, inform the event handler
+ ProtocolVersion version = statusLine.getProtocolVersion();
+
+ req.mEventHandler.status(version.getMajor(),
+ version.getMinor(),
+ statusCode,
+ statusLine.getReasonPhrase());
+ req.mEventHandler.headers(headers);
+ req.mEventHandler.endData();
+
+ proxyConnection.close();
+
+ // here, we return null to indicate that the original
+ // request needs to be dropped
+ return null;
+ }
+ } else {
+ // if we do not have a proxy, we simply connect to the host
+ try {
+ synchronized (mSslSocketFactory) {
+ sslSock = (SSLSocket) mSslSocketFactory.createSocket();
+
+ sslSock.setSoTimeout(SOCKET_TIMEOUT);
+ sslSock.connect(new InetSocketAddress(mHost.getHostName(),
+ mHost.getPort()));
+
+ }
+ } catch(IOException e) {
+ if (sslSock != null) {
+ sslSock.close();
+ }
+
+ String errorMessage = e.getMessage();
+ if (errorMessage == null) {
+ errorMessage = "failed to create an SSL socket";
+ }
+
+ throw new IOException(errorMessage);
+ }
+ }
+
+ // do handshake and validate server certificates
+ SslError error = CertificateChainValidator.getInstance().
+ doHandshakeAndValidateServerCertificates(this, sslSock, mHost.getHostName());
+
+ EventHandler eventHandler = req.getEventHandler();
+
+ // Update the certificate info (to be consistent, it is better to do it
+ // here, before we start handling SSL errors, if any)
+ eventHandler.certificate(mCertificate);
+
+ // Inform the user if there is a problem
+ if (error != null) {
+ // handleSslErrorRequest may immediately unsuspend if it wants to
+ // allow the certificate anyway.
+ // So we mark the connection as suspended, call handleSslErrorRequest
+ // then check if we're still suspended and only wait if we actually
+ // need to.
+ synchronized (mSuspendLock) {
+ mSuspended = true;
+ }
+ // don't hold the lock while calling out to the event handler
+ eventHandler.handleSslErrorRequest(error);
+ synchronized (mSuspendLock) {
+ if (mSuspended) {
+ try {
+ // Put a limit on how long we are waiting; if the timeout
+ // expires (which should never happen unless you choose
+ // to ignore the SSL error dialog for a very long time),
+ // we wake up the thread and abort the request. This is
+ // to prevent us from stalling the network if things go
+ // very bad.
+ mSuspendLock.wait(10 * 60 * 1000);
+ if (mSuspended) {
+ // mSuspended is true if we have not had a chance to
+ // restart the connection yet (ie, the wait timeout
+ // has expired)
+ mSuspended = false;
+ mAborted = true;
+ if (HttpLog.LOGV) {
+ HttpLog.v("HttpsConnection.openConnection():" +
+ " SSL timeout expired and request was cancelled!!!");
+ }
+ }
+ } catch (InterruptedException e) {
+ // ignore
+ }
+ }
+ if (mAborted) {
+ // The user decided not to use this unverified connection
+ // so close it immediately.
+ sslSock.close();
+ throw new SSLConnectionClosedByUserException("connection closed by the user");
+ }
+ }
+ }
+
+ // All went well, we have an open, verified connection.
+ AndroidHttpClientConnection conn = new AndroidHttpClientConnection();
+ BasicHttpParams params = new BasicHttpParams();
+ params.setIntParameter(HttpConnectionParams.SOCKET_BUFFER_SIZE, 8192);
+ conn.bind(sslSock, params);
+ return conn;
+ }
+
+ /**
+ * Closes the low level connection.
+ *
+ * If an exception is thrown then it is assumed that the connection will
+ * have been closed (to the extent possible) anyway and the caller does not
+ * need to take any further action.
+ *
+ */
+ @Override
+ void closeConnection() {
+ // if the connection has been suspended due to an SSL error
+ if (mSuspended) {
+ // wake up the network thread
+ restartConnection(false);
+ }
+
+ try {
+ if (mHttpClientConnection != null && mHttpClientConnection.isOpen()) {
+ mHttpClientConnection.close();
+ }
+ } catch (IOException e) {
+ if (HttpLog.LOGV)
+ HttpLog.v("HttpsConnection.closeConnection():" +
+ " failed closing connection " + mHost);
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Restart a secure connection suspended waiting for user interaction.
+ */
+ void restartConnection(boolean proceed) {
+ if (HttpLog.LOGV) {
+ HttpLog.v("HttpsConnection.restartConnection():" +
+ " proceed: " + proceed);
+ }
+
+ synchronized (mSuspendLock) {
+ if (mSuspended) {
+ mSuspended = false;
+ mAborted = !proceed;
+ mSuspendLock.notify();
+ }
+ }
+ }
+
+ @Override
+ String getScheme() {
+ return "https";
+ }
+}
diff --git a/core/java/android/net/http/IdleCache.java b/core/java/android/net/http/IdleCache.java
new file mode 100644
index 0000000..fda6009
--- /dev/null
+++ b/core/java/android/net/http/IdleCache.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Hangs onto idle live connections for a little while
+ */
+
+package android.net.http;
+
+import org.apache.http.HttpHost;
+
+import android.os.SystemClock;
+
+/**
+ * {@hide}
+ */
+class IdleCache {
+
+ class Entry {
+ HttpHost mHost;
+ Connection mConnection;
+ long mTimeout;
+ };
+
+ private final static int IDLE_CACHE_MAX = 8;
+
+ /* Allow five consecutive empty queue checks before shutdown */
+ private final static int EMPTY_CHECK_MAX = 5;
+
+ /* six second timeout for connections */
+ private final static int TIMEOUT = 6 * 1000;
+ private final static int CHECK_INTERVAL = 2 * 1000;
+ private Entry[] mEntries = new Entry[IDLE_CACHE_MAX];
+
+ private int mCount = 0;
+
+ private IdleReaper mThread = null;
+
+ /* stats */
+ private int mCached = 0;
+ private int mReused = 0;
+
+ IdleCache() {
+ for (int i = 0; i < IDLE_CACHE_MAX; i++) {
+ mEntries[i] = new Entry();
+ }
+ }
+
+ /**
+ * Caches connection, if there is room.
+ * @return true if connection cached
+ */
+ synchronized boolean cacheConnection(
+ HttpHost host, Connection connection) {
+
+ boolean ret = false;
+
+ if (HttpLog.LOGV) {
+ HttpLog.v("IdleCache size " + mCount + " host " + host);
+ }
+
+ if (mCount < IDLE_CACHE_MAX) {
+ long time = SystemClock.uptimeMillis();
+ for (int i = 0; i < IDLE_CACHE_MAX; i++) {
+ Entry entry = mEntries[i];
+ if (entry.mHost == null) {
+ entry.mHost = host;
+ entry.mConnection = connection;
+ entry.mTimeout = time + TIMEOUT;
+ mCount++;
+ if (HttpLog.LOGV) mCached++;
+ ret = true;
+ if (mThread == null) {
+ mThread = new IdleReaper();
+ mThread.start();
+ }
+ break;
+ }
+ }
+ }
+ return ret;
+ }
+
+ synchronized Connection getConnection(HttpHost host) {
+ Connection ret = null;
+
+ if (mCount > 0) {
+ for (int i = 0; i < IDLE_CACHE_MAX; i++) {
+ Entry entry = mEntries[i];
+ HttpHost eHost = entry.mHost;
+ if (eHost != null && eHost.equals(host)) {
+ ret = entry.mConnection;
+ entry.mHost = null;
+ entry.mConnection = null;
+ mCount--;
+ if (HttpLog.LOGV) mReused++;
+ break;
+ }
+ }
+ }
+ return ret;
+ }
+
+ synchronized void clear() {
+ for (int i = 0; mCount > 0 && i < IDLE_CACHE_MAX; i++) {
+ Entry entry = mEntries[i];
+ if (entry.mHost != null) {
+ entry.mHost = null;
+ entry.mConnection.closeConnection();
+ entry.mConnection = null;
+ mCount--;
+ }
+ }
+ }
+
+ private synchronized void clearIdle() {
+ if (mCount > 0) {
+ long time = SystemClock.uptimeMillis();
+ for (int i = 0; i < IDLE_CACHE_MAX; i++) {
+ Entry entry = mEntries[i];
+ if (entry.mHost != null && time > entry.mTimeout) {
+ entry.mHost = null;
+ entry.mConnection.closeConnection();
+ entry.mConnection = null;
+ mCount--;
+ }
+ }
+ }
+ }
+
+ private class IdleReaper extends Thread {
+
+ public void run() {
+ int check = 0;
+
+ setName("IdleReaper");
+ android.os.Process.setThreadPriority(
+ android.os.Process.THREAD_PRIORITY_BACKGROUND);
+ synchronized (IdleCache.this) {
+ while (check < EMPTY_CHECK_MAX) {
+ try {
+ IdleCache.this.wait(CHECK_INTERVAL);
+ } catch (InterruptedException ex) {
+ }
+ if (mCount == 0) {
+ check++;
+ } else {
+ check = 0;
+ clearIdle();
+ }
+ }
+ mThread = null;
+ }
+ if (HttpLog.LOGV) {
+ HttpLog.v("IdleCache IdleReaper shutdown: cached " + mCached +
+ " reused " + mReused);
+ mCached = 0;
+ mReused = 0;
+ }
+ }
+ }
+}
diff --git a/core/java/android/net/http/LoggingEventHandler.java b/core/java/android/net/http/LoggingEventHandler.java
new file mode 100644
index 0000000..1b18651
--- /dev/null
+++ b/core/java/android/net/http/LoggingEventHandler.java
@@ -0,0 +1,90 @@
+/*
+ * 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.
+ */
+
+/**
+ * A test EventHandler: Logs everything received
+ */
+
+package android.net.http;
+
+import android.net.http.Headers;
+
+/**
+ * {@hide}
+ */
+public class LoggingEventHandler implements EventHandler {
+
+ public void requestSent() {
+ HttpLog.v("LoggingEventHandler:requestSent()");
+ }
+
+ public void status(int major_version,
+ int minor_version,
+ int code, /* Status-Code value */
+ String reason_phrase) {
+ if (HttpLog.LOGV) {
+ HttpLog.v("LoggingEventHandler:status() major: " + major_version +
+ " minor: " + minor_version +
+ " code: " + code +
+ " reason: " + reason_phrase);
+ }
+ }
+
+ public void headers(Headers headers) {
+ if (HttpLog.LOGV) {
+ HttpLog.v("LoggingEventHandler:headers()");
+ HttpLog.v(headers.toString());
+ }
+ }
+
+ public void locationChanged(String newLocation, boolean permanent) {
+ if (HttpLog.LOGV) {
+ HttpLog.v("LoggingEventHandler: locationChanged() " + newLocation +
+ " permanent " + permanent);
+ }
+ }
+
+ public void data(byte[] data, int len) {
+ if (HttpLog.LOGV) {
+ HttpLog.v("LoggingEventHandler: data() " + len + " bytes");
+ }
+ // HttpLog.v(new String(data, 0, len));
+ }
+ public void endData() {
+ if (HttpLog.LOGV) {
+ HttpLog.v("LoggingEventHandler: endData() called");
+ }
+ }
+
+ public void certificate(SslCertificate certificate) {
+ if (HttpLog.LOGV) {
+ HttpLog.v("LoggingEventHandler: certificate(): " + certificate);
+ }
+ }
+
+ public void error(int id, String description) {
+ if (HttpLog.LOGV) {
+ HttpLog.v("LoggingEventHandler: error() called Id:" + id +
+ " description " + description);
+ }
+ }
+
+ public void handleSslErrorRequest(SslError error) {
+ if (HttpLog.LOGV) {
+ HttpLog.v("LoggingEventHandler: handleSslErrorRequest():" + error);
+ }
+ }
+}
diff --git a/core/java/android/net/http/Request.java b/core/java/android/net/http/Request.java
new file mode 100644
index 0000000..bcbecf0
--- /dev/null
+++ b/core/java/android/net/http/Request.java
@@ -0,0 +1,456 @@
+/*
+ * 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.net.http;
+
+import java.io.InputStream;
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.zip.GZIPInputStream;
+
+import org.apache.http.entity.InputStreamEntity;
+import org.apache.http.Header;
+import org.apache.http.HttpClientConnection;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpEntityEnclosingRequest;
+import org.apache.http.HttpException;
+import org.apache.http.HttpHost;
+import org.apache.http.HttpRequest;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.ParseException;
+import org.apache.http.ProtocolVersion;
+
+import org.apache.http.StatusLine;
+import org.apache.http.message.BasicHttpRequest;
+import org.apache.http.message.BasicHttpEntityEnclosingRequest;
+import org.apache.http.protocol.RequestContent;
+
+/**
+ * Represents an HTTP request for a given host.
+ *
+ * {@hide}
+ */
+
+class Request {
+
+ /** The eventhandler to call as the request progresses */
+ EventHandler mEventHandler;
+
+ private Connection mConnection;
+
+ /** The Apache http request */
+ BasicHttpRequest mHttpRequest;
+
+ /** The path component of this request */
+ String mPath;
+
+ /** Host serving this request */
+ HttpHost mHost;
+
+ /** Set if I'm using a proxy server */
+ HttpHost mProxyHost;
+
+ /** True if request is .html, .js, .css */
+ boolean mHighPriority;
+
+ /** True if request has been cancelled */
+ volatile boolean mCancelled = false;
+
+ int mFailCount = 0;
+
+ private InputStream mBodyProvider;
+ private int mBodyLength;
+
+ private final static String HOST_HEADER = "Host";
+ private final static String ACCEPT_ENCODING_HEADER = "Accept-Encoding";
+ private final static String CONTENT_LENGTH_HEADER = "content-length";
+
+ /* Used to synchronize waitUntilComplete() requests */
+ private final Object mClientResource = new Object();
+
+ /**
+ * Processor used to set content-length and transfer-encoding
+ * headers.
+ */
+ private static RequestContent requestContentProcessor =
+ new RequestContent();
+
+ /**
+ * Instantiates a new Request.
+ * @param method GET/POST/PUT
+ * @param host The server that will handle this request
+ * @param path path part of URI
+ * @param bodyProvider InputStream providing HTTP body, null if none
+ * @param bodyLength length of body, must be 0 if bodyProvider is null
+ * @param eventHandler request will make progress callbacks on
+ * this interface
+ * @param headers reqeust headers
+ * @param highPriority true for .html, css, .cs
+ */
+ Request(String method, HttpHost host, HttpHost proxyHost, String path,
+ InputStream bodyProvider, int bodyLength,
+ EventHandler eventHandler,
+ Map<String, String> headers, boolean highPriority) {
+ mEventHandler = eventHandler;
+ mHost = host;
+ mProxyHost = proxyHost;
+ mPath = path;
+ mHighPriority = highPriority;
+ mBodyProvider = bodyProvider;
+ mBodyLength = bodyLength;
+
+ if (bodyProvider == null) {
+ mHttpRequest = new BasicHttpRequest(method, getUri());
+ } else {
+ mHttpRequest = new BasicHttpEntityEnclosingRequest(
+ method, getUri());
+ setBodyProvider(bodyProvider, bodyLength);
+ }
+ addHeader(HOST_HEADER, getHostPort());
+
+ /* FIXME: if webcore will make the root document a
+ high-priority request, we can ask for gzip encoding only on
+ high priority reqs (saving the trouble for images, etc) */
+ addHeader(ACCEPT_ENCODING_HEADER, "gzip");
+ addHeaders(headers);
+ }
+
+ /**
+ * @param connection Request served by this connection
+ */
+ void setConnection(Connection connection) {
+ mConnection = connection;
+ }
+
+ /* package */ EventHandler getEventHandler() {
+ return mEventHandler;
+ }
+
+ /**
+ * Add header represented by given pair to request. Header will
+ * be formatted in request as "name: value\r\n".
+ * @param name of header
+ * @param value of header
+ */
+ void addHeader(String name, String value) {
+ if (name == null) {
+ String damage = "Null http header name";
+ HttpLog.e(damage);
+ throw new NullPointerException(damage);
+ }
+ if (value == null || value.length() == 0) {
+ String damage = "Null or empty value for header \"" + name + "\"";
+ HttpLog.e(damage);
+ throw new RuntimeException(damage);
+ }
+ mHttpRequest.addHeader(name, value);
+ }
+
+ /**
+ * Add all headers in given map to this request. This is a helper
+ * method: it calls addHeader for each pair in the map.
+ */
+ void addHeaders(Map<String, String> headers) {
+ if (headers == null) {
+ return;
+ }
+
+ Entry<String, String> entry;
+ Iterator<Entry<String, String>> i = headers.entrySet().iterator();
+ while (i.hasNext()) {
+ entry = i.next();
+ addHeader(entry.getKey(), entry.getValue());
+ }
+ }
+
+ /**
+ * Send the request line and headers
+ */
+ void sendRequest(AndroidHttpClientConnection httpClientConnection)
+ throws HttpException, IOException {
+
+ if (mCancelled) return; // don't send cancelled requests
+
+ if (HttpLog.LOGV) {
+ HttpLog.v("Request.sendRequest() " + mHost.getSchemeName() + "://" + getHostPort());
+ // HttpLog.v(mHttpRequest.getRequestLine().toString());
+ if (false) {
+ Iterator i = mHttpRequest.headerIterator();
+ while (i.hasNext()) {
+ Header header = (Header)i.next();
+ HttpLog.v(header.getName() + ": " + header.getValue());
+ }
+ }
+ }
+
+ requestContentProcessor.process(mHttpRequest,
+ mConnection.getHttpContext());
+ httpClientConnection.sendRequestHeader(mHttpRequest);
+ if (mHttpRequest instanceof HttpEntityEnclosingRequest) {
+ httpClientConnection.sendRequestEntity(
+ (HttpEntityEnclosingRequest) mHttpRequest);
+ }
+
+ if (HttpLog.LOGV) {
+ HttpLog.v("Request.requestSent() " + mHost.getSchemeName() + "://" + getHostPort() + mPath);
+ }
+ }
+
+
+ /**
+ * Receive a single http response.
+ *
+ * @param httpClientConnection the request to receive the response for.
+ */
+ void readResponse(AndroidHttpClientConnection httpClientConnection)
+ throws IOException, ParseException {
+
+ if (mCancelled) return; // don't send cancelled requests
+
+ StatusLine statusLine = null;
+ boolean hasBody = false;
+ boolean reuse = false;
+ httpClientConnection.flush();
+ int statusCode = 0;
+
+ Headers header = new Headers();
+ do {
+ statusLine = httpClientConnection.parseResponseHeader(header);
+ statusCode = statusLine.getStatusCode();
+ } while (statusCode < HttpStatus.SC_OK);
+ if (HttpLog.LOGV) HttpLog.v(
+ "Request.readResponseStatus() " +
+ statusLine.toString().length() + " " + statusLine);
+
+ ProtocolVersion v = statusLine.getProtocolVersion();
+ mEventHandler.status(v.getMajor(), v.getMinor(),
+ statusCode, statusLine.getReasonPhrase());
+ mEventHandler.headers(header);
+ HttpEntity entity = null;
+ hasBody = canResponseHaveBody(mHttpRequest, statusCode);
+
+ if (hasBody)
+ entity = httpClientConnection.receiveResponseEntity(header);
+
+ if (entity != null) {
+ InputStream is = entity.getContent();
+
+ // process gzip content encoding
+ Header contentEncoding = entity.getContentEncoding();
+ InputStream nis = null;
+ try {
+ if (contentEncoding != null &&
+ contentEncoding.getValue().equals("gzip")) {
+ nis = new GZIPInputStream(is);
+ } else {
+ nis = is;
+ }
+
+ /* accumulate enough data to make it worth pushing it
+ * up the stack */
+ byte[] buf = mConnection.getBuf();
+ int len = 0;
+ int count = 0;
+ int lowWater = buf.length / 2;
+ while (len != -1) {
+ len = nis.read(buf, count, buf.length - count);
+ if (len != -1) {
+ count += len;
+ }
+ if (len == -1 || count >= lowWater) {
+ if (HttpLog.LOGV) HttpLog.v("Request.readResponse() " + count);
+ mEventHandler.data(buf, count);
+ count = 0;
+ }
+ }
+ } catch(IOException e) {
+ // don't throw if we have a non-OK status code
+ if (statusCode == HttpStatus.SC_OK) {
+ throw e;
+ }
+ } finally {
+ if (nis != null) {
+ nis.close();
+ }
+ }
+ }
+ mConnection.setCanPersist(entity, statusLine.getProtocolVersion(),
+ header.getConnectionType());
+ mEventHandler.endData();
+ complete();
+
+ if (HttpLog.LOGV) HttpLog.v("Request.readResponse(): done " +
+ mHost.getSchemeName() + "://" + getHostPort() + mPath);
+ }
+
+ /**
+ * Data will not be sent to or received from server after cancel()
+ * call. Does not close connection--use close() below for that.
+ *
+ * Called by RequestHandle from non-network thread
+ */
+ void cancel() {
+ if (HttpLog.LOGV) {
+ HttpLog.v("Request.cancel(): " + getUri());
+ }
+ mCancelled = true;
+ if (mConnection != null) {
+ mConnection.cancel();
+ }
+ }
+
+ String getHostPort() {
+ String myScheme = mHost.getSchemeName();
+ int myPort = mHost.getPort();
+
+ // Only send port when we must... many servers can't deal with it
+ if (myPort != 80 && myScheme.equals("http") ||
+ myPort != 443 && myScheme.equals("https")) {
+ return mHost.toHostString();
+ } else {
+ return mHost.getHostName();
+ }
+ }
+
+ String getUri() {
+ if (mProxyHost == null ||
+ mHost.getSchemeName().equals("https")) {
+ return mPath;
+ }
+ return mHost.getSchemeName() + "://" + getHostPort() + mPath;
+ }
+
+ /**
+ * for debugging
+ */
+ public String toString() {
+ return (mHighPriority ? "P*" : "") + mPath;
+ }
+
+
+ /**
+ * If this request has been sent once and failed, it must be reset
+ * before it can be sent again.
+ */
+ void reset() {
+ /* clear content-length header */
+ mHttpRequest.removeHeaders(CONTENT_LENGTH_HEADER);
+
+ if (mBodyProvider != null) {
+ try {
+ mBodyProvider.reset();
+ } catch (IOException ex) {
+ if (HttpLog.LOGV) HttpLog.v(
+ "failed to reset body provider " +
+ getUri());
+ }
+ setBodyProvider(mBodyProvider, mBodyLength);
+ }
+ }
+
+ /**
+ * Pause thread request completes. Used for synchronous requests,
+ * and testing
+ */
+ void waitUntilComplete() {
+ synchronized (mClientResource) {
+ try {
+ if (HttpLog.LOGV) HttpLog.v("Request.waitUntilComplete()");
+ mClientResource.wait();
+ if (HttpLog.LOGV) HttpLog.v("Request.waitUntilComplete() done waiting");
+ } catch (InterruptedException e) {
+ }
+ }
+ }
+
+ void complete() {
+ synchronized (mClientResource) {
+ mClientResource.notifyAll();
+ }
+ }
+
+ /**
+ * Decide whether a response comes with an entity.
+ * The implementation in this class is based on RFC 2616.
+ * Unknown methods and response codes are supposed to
+ * indicate responses with an entity.
+ * <br/>
+ * Derived executors can override this method to handle
+ * methods and response codes not specified in RFC 2616.
+ *
+ * @param request the request, to obtain the executed method
+ * @param response the response, to obtain the status code
+ */
+
+ private static boolean canResponseHaveBody(final HttpRequest request,
+ final int status) {
+
+ if ("HEAD".equalsIgnoreCase(request.getRequestLine().getMethod())) {
+ return false;
+ }
+ return status >= HttpStatus.SC_OK
+ && status != HttpStatus.SC_NO_CONTENT
+ && status != HttpStatus.SC_NOT_MODIFIED
+ && status != HttpStatus.SC_RESET_CONTENT;
+ }
+
+ /**
+ * Supply an InputStream that provides the body of a request. It's
+ * not great that the caller must also provide the length of the data
+ * returned by that InputStream, but the client needs to know up
+ * front, and I'm not sure how to get this out of the InputStream
+ * itself without a costly readthrough. I'm not sure skip() would
+ * do what we want. If you know a better way, please let me know.
+ */
+ private void setBodyProvider(InputStream bodyProvider, int bodyLength) {
+ if (!bodyProvider.markSupported()) {
+ throw new IllegalArgumentException(
+ "bodyProvider must support mark()");
+ }
+ // Mark beginning of stream
+ bodyProvider.mark(Integer.MAX_VALUE);
+
+ ((BasicHttpEntityEnclosingRequest)mHttpRequest).setEntity(
+ new InputStreamEntity(bodyProvider, bodyLength));
+ }
+
+
+ /**
+ * Handles SSL error(s) on the way down from the user (the user
+ * has already provided their feedback).
+ */
+ public void handleSslErrorResponse(boolean proceed) {
+ HttpsConnection connection = (HttpsConnection)(mConnection);
+ if (connection != null) {
+ connection.restartConnection(proceed);
+ }
+ }
+
+ /**
+ * Helper: calls error() on eventhandler with appropriate message
+ * This should not be called before the mConnection is set.
+ */
+ void error(int errorId, int resourceId) {
+ mEventHandler.error(
+ errorId,
+ mConnection.mContext.getText(
+ resourceId).toString());
+ }
+
+}
diff --git a/core/java/android/net/http/RequestFeeder.java b/core/java/android/net/http/RequestFeeder.java
new file mode 100644
index 0000000..34ca267
--- /dev/null
+++ b/core/java/android/net/http/RequestFeeder.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Supplies Requests to a Connection
+ */
+
+package android.net.http;
+
+import org.apache.http.HttpHost;
+
+/**
+ * {@hide}
+ */
+interface RequestFeeder {
+
+ Request getRequest();
+ Request getRequest(HttpHost host);
+
+ /**
+ * @return true if a request for this host is available
+ */
+ boolean haveRequest(HttpHost host);
+
+ /**
+ * Put request back on head of queue
+ */
+ void requeueRequest(Request request);
+}
diff --git a/core/java/android/net/http/RequestHandle.java b/core/java/android/net/http/RequestHandle.java
new file mode 100644
index 0000000..5d81250
--- /dev/null
+++ b/core/java/android/net/http/RequestHandle.java
@@ -0,0 +1,402 @@
+/*
+ * 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.net.http;
+
+import android.net.ParseException;
+import android.net.WebAddress;
+import android.security.Md5MessageDigest;
+import junit.framework.Assert;
+import android.webkit.CookieManager;
+
+import org.apache.commons.codec.binary.Base64;
+
+import java.io.InputStream;
+import java.lang.Math;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+
+/**
+ * RequestHandle: handles a request session that may include multiple
+ * redirects, HTTP authentication requests, etc.
+ *
+ * {@hide}
+ */
+public class RequestHandle {
+
+ private String mUrl;
+ private WebAddress mUri;
+ private String mMethod;
+ private Map<String, String> mHeaders;
+
+ private RequestQueue mRequestQueue;
+
+ private Request mRequest;
+
+ private InputStream mBodyProvider;
+ private int mBodyLength;
+
+ private int mRedirectCount = 0;
+
+ private final static String AUTHORIZATION_HEADER = "Authorization";
+ private final static String PROXY_AUTHORIZATION_HEADER = "Proxy-Authorization";
+
+ private final static int MAX_REDIRECT_COUNT = 16;
+
+ /**
+ * Creates a new request session.
+ */
+ public RequestHandle(RequestQueue requestQueue, String url, WebAddress uri,
+ String method, Map<String, String> headers,
+ InputStream bodyProvider, int bodyLength, Request request) {
+
+ if (headers == null) {
+ headers = new HashMap<String, String>();
+ }
+ mHeaders = headers;
+ mBodyProvider = bodyProvider;
+ mBodyLength = bodyLength;
+ mMethod = method == null? "GET" : method;
+
+ mUrl = url;
+ mUri = uri;
+
+ mRequestQueue = requestQueue;
+
+ mRequest = request;
+ }
+
+ /**
+ * Cancels this request
+ */
+ public void cancel() {
+ if (mRequest != null) {
+ mRequest.cancel();
+ }
+ }
+
+ /**
+ * Handles SSL error(s) on the way down from the user (the user
+ * has already provided their feedback).
+ */
+ public void handleSslErrorResponse(boolean proceed) {
+ if (mRequest != null) {
+ mRequest.handleSslErrorResponse(proceed);
+ }
+ }
+
+ /**
+ * @return true if we've hit the max redirect count
+ */
+ public boolean isRedirectMax() {
+ return mRedirectCount >= MAX_REDIRECT_COUNT;
+ }
+
+ /**
+ * Create and queue a redirect request.
+ *
+ * @param redirectTo URL to redirect to
+ * @param statusCode HTTP status code returned from original request
+ * @param cacheHeaders Cache header for redirect URL
+ * @return true if setup succeeds, false otherwise (redirect loop
+ * count exceeded)
+ */
+ public boolean setupRedirect(String redirectTo, int statusCode,
+ Map<String, String> cacheHeaders) {
+ if (HttpLog.LOGV) {
+ HttpLog.v("RequestHandle.setupRedirect(): redirectCount " +
+ mRedirectCount);
+ }
+
+ // be careful and remove authentication headers, if any
+ mHeaders.remove(AUTHORIZATION_HEADER);
+ mHeaders.remove(PROXY_AUTHORIZATION_HEADER);
+
+ if (++mRedirectCount == MAX_REDIRECT_COUNT) {
+ // Way too many redirects -- fail out
+ if (HttpLog.LOGV) HttpLog.v(
+ "RequestHandle.setupRedirect(): too many redirects " +
+ mRequest);
+ mRequest.error(EventHandler.ERROR_REDIRECT_LOOP,
+ com.android.internal.R.string.httpErrorRedirectLoop);
+ return false;
+ }
+
+ if (mUrl.startsWith("https:") && redirectTo.startsWith("http:")) {
+ // implement http://www.w3.org/Protocols/rfc2616/rfc2616-sec15.html#sec15.1.3
+ if (HttpLog.LOGV) {
+ HttpLog.v("blowing away the referer on an https -> http redirect");
+ }
+ mHeaders.remove("Referer");
+ }
+
+ mUrl = redirectTo;
+ try {
+ mUri = new WebAddress(mUrl);
+ } catch (ParseException e) {
+ e.printStackTrace();
+ }
+
+ // update the "cookie" header based on the redirected url
+ mHeaders.remove("cookie");
+ String cookie = CookieManager.getInstance().getCookie(mUri);
+ if (cookie != null && cookie.length() > 0) {
+ mHeaders.put("cookie", cookie);
+ }
+
+ if ((statusCode == 302 || statusCode == 303) && mMethod.equals("POST")) {
+ if (HttpLog.LOGV) {
+ HttpLog.v("replacing POST with GET on redirect to " + redirectTo);
+ }
+ mMethod = "GET";
+ }
+ mHeaders.remove("Content-Type");
+ mBodyProvider = null;
+
+ // Update the cache headers for this URL
+ mHeaders.putAll(cacheHeaders);
+
+ createAndQueueNewRequest();
+ return true;
+ }
+
+ /**
+ * Create and queue an HTTP authentication-response (basic) request.
+ */
+ public void setupBasicAuthResponse(boolean isProxy, String username, String password) {
+ String response = computeBasicAuthResponse(username, password);
+ if (HttpLog.LOGV) {
+ HttpLog.v("setupBasicAuthResponse(): response: " + response);
+ }
+ mHeaders.put(authorizationHeader(isProxy), "Basic " + response);
+ setupAuthResponse();
+ }
+
+ /**
+ * Create and queue an HTTP authentication-response (digest) request.
+ */
+ public void setupDigestAuthResponse(boolean isProxy,
+ String username,
+ String password,
+ String realm,
+ String nonce,
+ String QOP,
+ String algorithm,
+ String opaque) {
+
+ String response = computeDigestAuthResponse(
+ username, password, realm, nonce, QOP, algorithm, opaque);
+ if (HttpLog.LOGV) {
+ HttpLog.v("setupDigestAuthResponse(): response: " + response);
+ }
+ mHeaders.put(authorizationHeader(isProxy), "Digest " + response);
+ setupAuthResponse();
+ }
+
+ private void setupAuthResponse() {
+ try {
+ if (mBodyProvider != null) mBodyProvider.reset();
+ } catch (java.io.IOException ex) {
+ if (HttpLog.LOGV) {
+ HttpLog.v("setupAuthResponse() failed to reset body provider");
+ }
+ }
+ createAndQueueNewRequest();
+ }
+
+ /**
+ * @return HTTP request method (GET, PUT, etc).
+ */
+ public String getMethod() {
+ return mMethod;
+ }
+
+ /**
+ * @return Basic-scheme authentication response: BASE64(username:password).
+ */
+ public static String computeBasicAuthResponse(String username, String password) {
+ Assert.assertNotNull(username);
+ Assert.assertNotNull(password);
+
+ // encode username:password to base64
+ return new String(Base64.encodeBase64((username + ':' + password).getBytes()));
+ }
+
+ public void waitUntilComplete() {
+ mRequest.waitUntilComplete();
+ }
+
+ /**
+ * @return Digest-scheme authentication response.
+ */
+ private String computeDigestAuthResponse(String username,
+ String password,
+ String realm,
+ String nonce,
+ String QOP,
+ String algorithm,
+ String opaque) {
+
+ Assert.assertNotNull(username);
+ Assert.assertNotNull(password);
+ Assert.assertNotNull(realm);
+
+ String A1 = username + ":" + realm + ":" + password;
+ String A2 = mMethod + ":" + mUrl;
+
+ // because we do not preemptively send authorization headers, nc is always 1
+ String nc = "000001";
+ String cnonce = computeCnonce();
+ String digest = computeDigest(A1, A2, nonce, QOP, nc, cnonce);
+
+ String response = "";
+ response += "username=" + doubleQuote(username) + ", ";
+ response += "realm=" + doubleQuote(realm) + ", ";
+ response += "nonce=" + doubleQuote(nonce) + ", ";
+ response += "uri=" + doubleQuote(mUrl) + ", ";
+ response += "response=" + doubleQuote(digest) ;
+
+ if (opaque != null) {
+ response += ", opaque=" + doubleQuote(opaque);
+ }
+
+ if (algorithm != null) {
+ response += ", algorithm=" + algorithm;
+ }
+
+ if (QOP != null) {
+ response += ", qop=" + QOP + ", nc=" + nc + ", cnonce=" + doubleQuote(cnonce);
+ }
+
+ return response;
+ }
+
+ /**
+ * @return The right authorization header (dependeing on whether it is a proxy or not).
+ */
+ public static String authorizationHeader(boolean isProxy) {
+ if (!isProxy) {
+ return AUTHORIZATION_HEADER;
+ } else {
+ return PROXY_AUTHORIZATION_HEADER;
+ }
+ }
+
+ /**
+ * @return Double-quoted MD5 digest.
+ */
+ private String computeDigest(
+ String A1, String A2, String nonce, String QOP, String nc, String cnonce) {
+ if (HttpLog.LOGV) {
+ HttpLog.v("computeDigest(): QOP: " + QOP);
+ }
+
+ if (QOP == null) {
+ return KD(H(A1), nonce + ":" + H(A2));
+ } else {
+ if (QOP.equalsIgnoreCase("auth")) {
+ return KD(H(A1), nonce + ":" + nc + ":" + cnonce + ":" + QOP + ":" + H(A2));
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * @return MD5 hash of concat(secret, ":", data).
+ */
+ private String KD(String secret, String data) {
+ return H(secret + ":" + data);
+ }
+
+ /**
+ * @return MD5 hash of param.
+ */
+ private String H(String param) {
+ if (param != null) {
+ Md5MessageDigest md5 = new Md5MessageDigest();
+
+ byte[] d = md5.digest(param.getBytes());
+ if (d != null) {
+ return bufferToHex(d);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * @return HEX buffer representation.
+ */
+ private String bufferToHex(byte[] buffer) {
+ final char hexChars[] =
+ { '0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f' };
+
+ if (buffer != null) {
+ int length = buffer.length;
+ if (length > 0) {
+ StringBuilder hex = new StringBuilder(2 * length);
+
+ for (int i = 0; i < length; ++i) {
+ byte l = (byte) (buffer[i] & 0x0F);
+ byte h = (byte)((buffer[i] & 0xF0) >> 4);
+
+ hex.append(hexChars[h]);
+ hex.append(hexChars[l]);
+ }
+
+ return hex.toString();
+ } else {
+ return "";
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Computes a random cnonce value based on the current time.
+ */
+ private String computeCnonce() {
+ Random rand = new Random();
+ int nextInt = rand.nextInt();
+ nextInt = (nextInt == Integer.MIN_VALUE) ?
+ Integer.MAX_VALUE : Math.abs(nextInt);
+ return Integer.toString(nextInt, 16);
+ }
+
+ /**
+ * "Double-quotes" the argument.
+ */
+ private String doubleQuote(String param) {
+ if (param != null) {
+ return "\"" + param + "\"";
+ }
+
+ return null;
+ }
+
+ /**
+ * Creates and queues new request.
+ */
+ private void createAndQueueNewRequest() {
+ mRequest = mRequestQueue.queueRequest(
+ mUrl, mUri, mMethod, mHeaders, mRequest.mEventHandler,
+ mBodyProvider,
+ mBodyLength, mRequest.mHighPriority).mRequest;
+ }
+}
diff --git a/core/java/android/net/http/RequestQueue.java b/core/java/android/net/http/RequestQueue.java
new file mode 100644
index 0000000..d592995
--- /dev/null
+++ b/core/java/android/net/http/RequestQueue.java
@@ -0,0 +1,647 @@
+/*
+ * 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.
+ */
+
+/**
+ * High level HTTP Interface
+ * Queues requests as necessary
+ */
+
+package android.net.http;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.ConnectivityManager;
+import android.net.NetworkConnectivityListener;
+import android.net.NetworkInfo;
+import android.net.Proxy;
+import android.net.WebAddress;
+import android.os.Handler;
+import android.os.Message;
+import android.os.SystemProperties;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.InputStream;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.ListIterator;
+import java.util.Map;
+
+import org.apache.http.HttpHost;
+
+/**
+ * {@hide}
+ */
+public class RequestQueue implements RequestFeeder {
+
+ private Context mContext;
+
+ /**
+ * Requests, indexed by HttpHost (scheme, host, port)
+ */
+ private LinkedHashMap<HttpHost, LinkedList<Request>> mPending;
+
+ /* Support for notifying a client when queue is empty */
+ private boolean mClientWaiting = false;
+
+ /** true if connected */
+ boolean mNetworkConnected = true;
+
+ private HttpHost mProxyHost = null;
+ private BroadcastReceiver mProxyChangeReceiver;
+
+ private ActivePool mActivePool;
+
+ /* default simultaneous connection count */
+ private static final int CONNECTION_COUNT = 4;
+
+ /**
+ * This intent broadcast when http is paused or unpaused due to
+ * net availability toggling
+ */
+ public final static String HTTP_NETWORK_STATE_CHANGED_INTENT =
+ "android.net.http.NETWORK_STATE";
+ public final static String HTTP_NETWORK_STATE_UP = "up";
+
+ /**
+ * Listen to platform network state. On a change,
+ * (1) kick stack on or off as appropriate
+ * (2) send an intent to my host app telling
+ * it what I've done
+ */
+ private NetworkStateTracker mNetworkStateTracker;
+ class NetworkStateTracker {
+
+ final static int EVENT_DATA_STATE_CHANGED = 100;
+
+ Context mContext;
+ NetworkConnectivityListener mConnectivityListener;
+ NetworkInfo.State mLastNetworkState = NetworkInfo.State.CONNECTED;
+ int mCurrentNetworkType;
+
+ NetworkStateTracker(Context context) {
+ mContext = context;
+ }
+
+ /**
+ * register for updates
+ */
+ protected void enable() {
+ if (mConnectivityListener == null) {
+ /*
+ * Initializing the network type is really unnecessary,
+ * since as soon as we register with the NCL, we'll
+ * get a CONNECTED event for the active network, and
+ * we'll configure the HTTP proxy accordingly. However,
+ * as a fallback in case that doesn't happen for some
+ * reason, initializing to type WIFI would mean that
+ * we'd start out without a proxy. This seems better
+ * than thinking we have a proxy (which is probably
+ * private to the carrier network and therefore
+ * unreachable outside of that network) when we really
+ * shouldn't.
+ */
+ mCurrentNetworkType = ConnectivityManager.TYPE_WIFI;
+ mConnectivityListener = new NetworkConnectivityListener();
+ mConnectivityListener.registerHandler(mHandler, EVENT_DATA_STATE_CHANGED);
+ mConnectivityListener.startListening(mContext);
+ }
+ }
+
+ protected void disable() {
+ if (mConnectivityListener != null) {
+ mConnectivityListener.unregisterHandler(mHandler);
+ mConnectivityListener.stopListening();
+ mConnectivityListener = null;
+ }
+ }
+
+ private Handler mHandler = new Handler() {
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case EVENT_DATA_STATE_CHANGED:
+ networkStateChanged();
+ break;
+ }
+ }
+ };
+
+ int getCurrentNetworkType() {
+ return mCurrentNetworkType;
+ }
+
+ void networkStateChanged() {
+ if (mConnectivityListener == null)
+ return;
+
+
+ NetworkConnectivityListener.State connectivityState = mConnectivityListener.getState();
+ NetworkInfo info = mConnectivityListener.getNetworkInfo();
+ if (info == null) {
+ /**
+ * We've been seeing occasional NPEs here. I believe recent changes
+ * have made this impossible, but in the interest of being totally
+ * paranoid, check and log this here.
+ */
+ HttpLog.v("NetworkStateTracker: connectivity broadcast"
+ + " has null network info - ignoring");
+ return;
+ }
+ NetworkInfo.State state = info.getState();
+
+ if (HttpLog.LOGV) {
+ HttpLog.v("NetworkStateTracker " + info.getTypeName() +
+ " state= " + state + " last= " + mLastNetworkState +
+ " connectivityState= " + connectivityState.toString());
+ }
+
+ boolean newConnection =
+ state != mLastNetworkState && state == NetworkInfo.State.CONNECTED;
+
+ if (state == NetworkInfo.State.CONNECTED) {
+ mCurrentNetworkType = info.getType();
+ setProxyConfig();
+ }
+
+ mLastNetworkState = state;
+ if (connectivityState == NetworkConnectivityListener.State.NOT_CONNECTED) {
+ setNetworkState(false);
+ broadcastState(false);
+ } else if (newConnection) {
+ setNetworkState(true);
+ broadcastState(true);
+ }
+
+ }
+
+ void broadcastState(boolean connected) {
+ Intent intent = new Intent(HTTP_NETWORK_STATE_CHANGED_INTENT);
+ intent.putExtra(HTTP_NETWORK_STATE_UP, connected);
+ mContext.sendBroadcast(intent);
+ }
+ }
+
+ /**
+ * This class maintains active connection threads
+ */
+ class ActivePool implements ConnectionManager {
+ /** Threads used to process requests */
+ ConnectionThread[] mThreads;
+
+ IdleCache mIdleCache;
+
+ private int mTotalRequest;
+ private int mTotalConnection;
+ private int mConnectionCount;
+
+ ActivePool(int connectionCount) {
+ mIdleCache = new IdleCache();
+ mConnectionCount = connectionCount;
+ mThreads = new ConnectionThread[mConnectionCount];
+
+ for (int i = 0; i < mConnectionCount; i++) {
+ mThreads[i] = new ConnectionThread(
+ mContext, i, this, RequestQueue.this);
+ }
+ }
+
+ void startup() {
+ for (int i = 0; i < mConnectionCount; i++) {
+ mThreads[i].start();
+ }
+ }
+
+ void shutdown() {
+ for (int i = 0; i < mConnectionCount; i++) {
+ mThreads[i].requestStop();
+ }
+ }
+
+ public boolean isNetworkConnected() {
+ return mNetworkConnected;
+ }
+
+ void startConnectionThread() {
+ synchronized (RequestQueue.this) {
+ RequestQueue.this.notify();
+ }
+ }
+
+ public void startTiming() {
+ for (int i = 0; i < mConnectionCount; i++) {
+ mThreads[i].mStartThreadTime = mThreads[i].mCurrentThreadTime;
+ }
+ mTotalRequest = 0;
+ mTotalConnection = 0;
+ }
+
+ public void stopTiming() {
+ int totalTime = 0;
+ for (int i = 0; i < mConnectionCount; i++) {
+ ConnectionThread rt = mThreads[i];
+ totalTime += (rt.mCurrentThreadTime - rt.mStartThreadTime);
+ rt.mStartThreadTime = -1;
+ }
+ Log.d("Http", "Http thread used " + totalTime + " ms " + " for "
+ + mTotalRequest + " requests and " + mTotalConnection
+ + " connections");
+ }
+
+ void logState() {
+ StringBuilder dump = new StringBuilder();
+ for (int i = 0; i < mConnectionCount; i++) {
+ dump.append(mThreads[i] + "\n");
+ }
+ HttpLog.v(dump.toString());
+ }
+
+
+ public HttpHost getProxyHost() {
+ return mProxyHost;
+ }
+
+ /**
+ * Turns off persistence on all live connections
+ */
+ void disablePersistence() {
+ for (int i = 0; i < mConnectionCount; i++) {
+ Connection connection = mThreads[i].mConnection;
+ if (connection != null) connection.setCanPersist(false);
+ }
+ mIdleCache.clear();
+ }
+
+ /* Linear lookup -- okay for small thread counts. Might use
+ private HashMap<HttpHost, LinkedList<ConnectionThread>> mActiveMap;
+ if this turns out to be a hotspot */
+ ConnectionThread getThread(HttpHost host) {
+ synchronized(RequestQueue.this) {
+ for (int i = 0; i < mThreads.length; i++) {
+ ConnectionThread ct = mThreads[i];
+ Connection connection = ct.mConnection;
+ if (connection != null && connection.mHost.equals(host)) {
+ return ct;
+ }
+ }
+ }
+ return null;
+ }
+
+ public Connection getConnection(Context context, HttpHost host) {
+ Connection con = mIdleCache.getConnection(host);
+ if (con == null) {
+ mTotalConnection++;
+ con = Connection.getConnection(
+ mContext, host, this, RequestQueue.this);
+ }
+ return con;
+ }
+ public boolean recycleConnection(HttpHost host, Connection connection) {
+ return mIdleCache.cacheConnection(host, connection);
+ }
+
+ }
+
+ /**
+ * A RequestQueue class instance maintains a set of queued
+ * requests. It orders them, makes the requests against HTTP
+ * servers, and makes callbacks to supplied eventHandlers as data
+ * is read. It supports request prioritization, connection reuse
+ * and pipelining.
+ *
+ * @param context application context
+ */
+ public RequestQueue(Context context) {
+ this(context, CONNECTION_COUNT);
+ }
+
+ /**
+ * A RequestQueue class instance maintains a set of queued
+ * requests. It orders them, makes the requests against HTTP
+ * servers, and makes callbacks to supplied eventHandlers as data
+ * is read. It supports request prioritization, connection reuse
+ * and pipelining.
+ *
+ * @param context application context
+ * @param connectionCount The number of simultaneous connections
+ */
+ public RequestQueue(Context context, int connectionCount) {
+ mContext = context;
+
+ mPending = new LinkedHashMap<HttpHost, LinkedList<Request>>(32);
+
+ mActivePool = new ActivePool(connectionCount);
+ mActivePool.startup();
+ }
+
+ /**
+ * Enables data state and proxy tracking
+ */
+ public synchronized void enablePlatformNotifications() {
+ if (HttpLog.LOGV) HttpLog.v("RequestQueue.enablePlatformNotifications() network");
+
+ if (mProxyChangeReceiver == null) {
+ mProxyChangeReceiver =
+ new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context ctx, Intent intent) {
+ setProxyConfig();
+ }
+ };
+ mContext.registerReceiver(mProxyChangeReceiver,
+ new IntentFilter(Proxy.PROXY_CHANGE_ACTION));
+ }
+
+ /* Network state notification is broken on the simulator
+ don't register for notifications on SIM */
+ String device = SystemProperties.get("ro.product.device");
+ boolean simulation = TextUtils.isEmpty(device);
+
+ if (!simulation) {
+ if (mNetworkStateTracker == null) {
+ mNetworkStateTracker = new NetworkStateTracker(mContext);
+ }
+ mNetworkStateTracker.enable();
+ }
+ }
+
+ /**
+ * If platform notifications have been enabled, call this method
+ * to disable before destroying RequestQueue
+ */
+ public synchronized void disablePlatformNotifications() {
+ if (HttpLog.LOGV) HttpLog.v("RequestQueue.disablePlatformNotifications() network");
+
+ if (mNetworkStateTracker != null) {
+ mNetworkStateTracker.disable();
+ }
+
+ if (mProxyChangeReceiver != null) {
+ mContext.unregisterReceiver(mProxyChangeReceiver);
+ mProxyChangeReceiver = null;
+ }
+ }
+
+ /**
+ * Because our IntentReceiver can run within a different thread,
+ * synchronize setting the proxy
+ */
+ private synchronized void setProxyConfig() {
+ if (mNetworkStateTracker.getCurrentNetworkType() == ConnectivityManager.TYPE_WIFI) {
+ mProxyHost = null;
+ } else {
+ String host = Proxy.getHost(mContext);
+ if (HttpLog.LOGV) HttpLog.v("RequestQueue.setProxyConfig " + host);
+ if (host == null) {
+ mProxyHost = null;
+ } else {
+ mActivePool.disablePersistence();
+ mProxyHost = new HttpHost(host, Proxy.getPort(mContext), "http");
+ }
+ }
+ }
+
+ /**
+ * used by webkit
+ * @return proxy host if set, null otherwise
+ */
+ public HttpHost getProxyHost() {
+ return mProxyHost;
+ }
+
+ /**
+ * Queues an HTTP request
+ * @param url The url to load.
+ * @param method "GET" or "POST."
+ * @param headers A hashmap of http headers.
+ * @param eventHandler The event handler for handling returned
+ * data. Callbacks will be made on the supplied instance.
+ * @param bodyProvider InputStream providing HTTP body, null if none
+ * @param bodyLength length of body, must be 0 if bodyProvider is null
+ * @param highPriority If true, queues before low priority
+ * requests if possible
+ */
+ public RequestHandle queueRequest(
+ String url, String method,
+ Map<String, String> headers, EventHandler eventHandler,
+ InputStream bodyProvider, int bodyLength, boolean highPriority) {
+ WebAddress uri = new WebAddress(url);
+ return queueRequest(url, uri, method, headers, eventHandler,
+ bodyProvider, bodyLength, highPriority);
+ }
+
+ /**
+ * Queues an HTTP request
+ * @param url The url to load.
+ * @param uri The uri of the url to load.
+ * @param method "GET" or "POST."
+ * @param headers A hashmap of http headers.
+ * @param eventHandler The event handler for handling returned
+ * data. Callbacks will be made on the supplied instance.
+ * @param bodyProvider InputStream providing HTTP body, null if none
+ * @param bodyLength length of body, must be 0 if bodyProvider is null
+ * @param highPriority If true, queues before low priority
+ * requests if possible
+ */
+ public RequestHandle queueRequest(
+ String url, WebAddress uri, String method, Map<String, String> headers,
+ EventHandler eventHandler,
+ InputStream bodyProvider, int bodyLength,
+ boolean highPriority) {
+
+ if (HttpLog.LOGV) HttpLog.v("RequestQueue.queueRequest " + uri);
+
+ // Ensure there is an eventHandler set
+ if (eventHandler == null) {
+ eventHandler = new LoggingEventHandler();
+ }
+
+ /* Create and queue request */
+ Request req;
+ HttpHost httpHost = new HttpHost(uri.mHost, uri.mPort, uri.mScheme);
+
+ // set up request
+ req = new Request(method, httpHost, mProxyHost, uri.mPath, bodyProvider,
+ bodyLength, eventHandler, headers, highPriority);
+
+ queueRequest(req, highPriority);
+
+ mActivePool.mTotalRequest++;
+
+ // dump();
+ mActivePool.startConnectionThread();
+
+ return new RequestHandle(
+ this, url, uri, method, headers, bodyProvider, bodyLength,
+ req);
+ }
+
+ /**
+ * Called by the NetworkStateTracker -- updates when network connectivity
+ * is lost/restored.
+ *
+ * If isNetworkConnected is true, start processing requests
+ */
+ public void setNetworkState(boolean isNetworkConnected) {
+ if (HttpLog.LOGV) HttpLog.v("RequestQueue.setNetworkState() " + isNetworkConnected);
+ mNetworkConnected = isNetworkConnected;
+ if (isNetworkConnected)
+ mActivePool.startConnectionThread();
+ }
+
+ /**
+ * @return true iff there are any non-active requests pending
+ */
+ synchronized boolean requestsPending() {
+ return !mPending.isEmpty();
+ }
+
+
+ /**
+ * debug tool: prints request queue to log
+ */
+ synchronized void dump() {
+ HttpLog.v("dump()");
+ StringBuilder dump = new StringBuilder();
+ int count = 0;
+ Iterator<Map.Entry<HttpHost, LinkedList<Request>>> iter;
+
+ // mActivePool.log(dump);
+
+ if (!mPending.isEmpty()) {
+ iter = mPending.entrySet().iterator();
+ while (iter.hasNext()) {
+ Map.Entry<HttpHost, LinkedList<Request>> entry = iter.next();
+ String hostName = entry.getKey().getHostName();
+ StringBuilder line = new StringBuilder("p" + count++ + " " + hostName + " ");
+
+ LinkedList<Request> reqList = entry.getValue();
+ ListIterator reqIter = reqList.listIterator(0);
+ while (iter.hasNext()) {
+ Request request = (Request)iter.next();
+ line.append(request + " ");
+ }
+ dump.append(line);
+ dump.append("\n");
+ }
+ }
+ HttpLog.v(dump.toString());
+ }
+
+ /*
+ * RequestFeeder implementation
+ */
+ public synchronized Request getRequest() {
+ Request ret = null;
+
+ if (mNetworkConnected && !mPending.isEmpty()) {
+ ret = removeFirst(mPending);
+ }
+ if (HttpLog.LOGV) HttpLog.v("RequestQueue.getRequest() => " + ret);
+ return ret;
+ }
+
+ /**
+ * @return a request for given host if possible
+ */
+ public synchronized Request getRequest(HttpHost host) {
+ Request ret = null;
+
+ if (mNetworkConnected && mPending.containsKey(host)) {
+ LinkedList<Request> reqList = mPending.get(host);
+ ret = reqList.removeFirst();
+ if (reqList.isEmpty()) {
+ mPending.remove(host);
+ }
+ }
+ if (HttpLog.LOGV) HttpLog.v("RequestQueue.getRequest(" + host + ") => " + ret);
+ return ret;
+ }
+
+ /**
+ * @return true if a request for this host is available
+ */
+ public synchronized boolean haveRequest(HttpHost host) {
+ return mPending.containsKey(host);
+ }
+
+ /**
+ * Put request back on head of queue
+ */
+ public void requeueRequest(Request request) {
+ queueRequest(request, true);
+ }
+
+ /**
+ * This must be called to cleanly shutdown RequestQueue
+ */
+ public void shutdown() {
+ mActivePool.shutdown();
+ }
+
+ protected synchronized void queueRequest(Request request, boolean head) {
+ HttpHost host = request.mHost;
+ LinkedList<Request> reqList;
+ if (mPending.containsKey(host)) {
+ reqList = mPending.get(host);
+ } else {
+ reqList = new LinkedList<Request>();
+ mPending.put(host, reqList);
+ }
+ if (head) {
+ reqList.addFirst(request);
+ } else {
+ reqList.add(request);
+ }
+ }
+
+
+ public void startTiming() {
+ mActivePool.startTiming();
+ }
+
+ public void stopTiming() {
+ mActivePool.stopTiming();
+ }
+
+ /* helper */
+ private Request removeFirst(LinkedHashMap<HttpHost, LinkedList<Request>> requestQueue) {
+ Request ret = null;
+ Iterator<Map.Entry<HttpHost, LinkedList<Request>>> iter = requestQueue.entrySet().iterator();
+ if (iter.hasNext()) {
+ Map.Entry<HttpHost, LinkedList<Request>> entry = iter.next();
+ LinkedList<Request> reqList = entry.getValue();
+ ret = reqList.removeFirst();
+ if (reqList.isEmpty()) {
+ requestQueue.remove(entry.getKey());
+ }
+ }
+ return ret;
+ }
+
+ /**
+ * This interface is exposed to each connection
+ */
+ interface ConnectionManager {
+ boolean isNetworkConnected();
+ HttpHost getProxyHost();
+ Connection getConnection(Context context, HttpHost host);
+ boolean recycleConnection(HttpHost host, Connection connection);
+ }
+}
diff --git a/core/java/android/net/http/SslCertificate.java b/core/java/android/net/http/SslCertificate.java
new file mode 100644
index 0000000..46b2bee
--- /dev/null
+++ b/core/java/android/net/http/SslCertificate.java
@@ -0,0 +1,251 @@
+/*
+ * 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.net.http;
+
+import android.os.Bundle;
+
+import java.text.DateFormat;
+import java.util.Vector;
+
+import java.security.cert.X509Certificate;
+
+import org.bouncycastle.asn1.DERObjectIdentifier;
+import org.bouncycastle.asn1.x509.X509Name;
+
+/**
+ * SSL certificate info (certificate details) class
+ */
+public class SslCertificate {
+
+ /**
+ * Name of the entity this certificate is issued to
+ */
+ private DName mIssuedTo;
+
+ /**
+ * Name of the entity this certificate is issued by
+ */
+ private DName mIssuedBy;
+
+ /**
+ * Not-before date from the validity period
+ */
+ private String mValidNotBefore;
+
+ /**
+ * Not-after date from the validity period
+ */
+ private String mValidNotAfter;
+
+ /**
+ * Bundle key names
+ */
+ private static final String ISSUED_TO = "issued-to";
+ private static final String ISSUED_BY = "issued-by";
+ private static final String VALID_NOT_BEFORE = "valid-not-before";
+ private static final String VALID_NOT_AFTER = "valid-not-after";
+
+ /**
+ * Saves the certificate state to a bundle
+ * @param certificate The SSL certificate to store
+ * @return A bundle with the certificate stored in it or null if fails
+ */
+ public static Bundle saveState(SslCertificate certificate) {
+ Bundle bundle = null;
+
+ if (certificate != null) {
+ bundle = new Bundle();
+
+ bundle.putString(ISSUED_TO, certificate.getIssuedTo().getDName());
+ bundle.putString(ISSUED_BY, certificate.getIssuedBy().getDName());
+
+ bundle.putString(VALID_NOT_BEFORE, certificate.getValidNotBefore());
+ bundle.putString(VALID_NOT_AFTER, certificate.getValidNotAfter());
+ }
+
+ return bundle;
+ }
+
+ /**
+ * Restores the certificate stored in the bundle
+ * @param bundle The bundle with the certificate state stored in it
+ * @return The SSL certificate stored in the bundle or null if fails
+ */
+ public static SslCertificate restoreState(Bundle bundle) {
+ if (bundle != null) {
+ return new SslCertificate(
+ bundle.getString(ISSUED_TO),
+ bundle.getString(ISSUED_BY),
+ bundle.getString(VALID_NOT_BEFORE),
+ bundle.getString(VALID_NOT_AFTER));
+ }
+
+ return null;
+ }
+
+ /**
+ * Creates a new SSL certificate object
+ * @param issuedTo The entity this certificate is issued to
+ * @param issuedBy The entity that issued this certificate
+ * @param validNotBefore The not-before date from the certificate validity period
+ * @param validNotAfter The not-after date from the certificate validity period
+ */
+ public SslCertificate(
+ String issuedTo, String issuedBy, String validNotBefore, String validNotAfter) {
+ mIssuedTo = new DName(issuedTo);
+ mIssuedBy = new DName(issuedBy);
+
+ mValidNotBefore = validNotBefore;
+ mValidNotAfter = validNotAfter;
+ }
+
+ /**
+ * Creates a new SSL certificate object from an X509 certificate
+ * @param certificate X509 certificate
+ */
+ public SslCertificate(X509Certificate certificate) {
+ this(certificate.getSubjectDN().getName(),
+ certificate.getIssuerDN().getName(),
+ DateFormat.getInstance().format(certificate.getNotBefore()),
+ DateFormat.getInstance().format(certificate.getNotAfter()));
+ }
+
+ /**
+ * @return Not-before date from the certificate validity period or
+ * "" if none has been set
+ */
+ public String getValidNotBefore() {
+ return mValidNotBefore != null ? mValidNotBefore : "";
+ }
+
+ /**
+ * @return Not-after date from the certificate validity period or
+ * "" if none has been set
+ */
+ public String getValidNotAfter() {
+ return mValidNotAfter != null ? mValidNotAfter : "";
+ }
+
+ /**
+ * @return Issued-to distinguished name or null if none has been set
+ */
+ public DName getIssuedTo() {
+ return mIssuedTo;
+ }
+
+ /**
+ * @return Issued-by distinguished name or null if none has been set
+ */
+ public DName getIssuedBy() {
+ return mIssuedBy;
+ }
+
+ /**
+ * @return A string representation of this certificate for debugging
+ */
+ public String toString() {
+ return
+ "Issued to: " + mIssuedTo.getDName() + ";\n" +
+ "Issued by: " + mIssuedBy.getDName() + ";\n";
+ }
+
+ /**
+ * A distinguished name helper class: a 3-tuple of:
+ * - common name (CN),
+ * - organization (O),
+ * - organizational unit (OU)
+ */
+ public class DName {
+ /**
+ * Distinguished name (normally includes CN, O, and OU names)
+ */
+ private String mDName;
+
+ /**
+ * Common-name (CN) component of the name
+ */
+ private String mCName;
+
+ /**
+ * Organization (O) component of the name
+ */
+ private String mOName;
+
+ /**
+ * Organizational Unit (OU) component of the name
+ */
+ private String mUName;
+
+ /**
+ * Creates a new distinguished name
+ * @param dName The distinguished name
+ */
+ public DName(String dName) {
+ if (dName != null) {
+ X509Name x509Name = new X509Name(mDName = dName);
+
+ Vector val = x509Name.getValues();
+ Vector oid = x509Name.getOIDs();
+
+ for (int i = 0; i < oid.size(); i++) {
+ if (oid.elementAt(i).equals(X509Name.CN)) {
+ mCName = (String) val.elementAt(i);
+ continue;
+ }
+
+ if (oid.elementAt(i).equals(X509Name.O)) {
+ mOName = (String) val.elementAt(i);
+ continue;
+ }
+
+ if (oid.elementAt(i).equals(X509Name.OU)) {
+ mUName = (String) val.elementAt(i);
+ continue;
+ }
+ }
+ }
+ }
+
+ /**
+ * @return The distinguished name (normally includes CN, O, and OU names)
+ */
+ public String getDName() {
+ return mDName != null ? mDName : "";
+ }
+
+ /**
+ * @return The Common-name (CN) component of this name
+ */
+ public String getCName() {
+ return mCName != null ? mCName : "";
+ }
+
+ /**
+ * @return The Organization (O) component of this name
+ */
+ public String getOName() {
+ return mOName != null ? mOName : "";
+ }
+
+ /**
+ * @return The Organizational Unit (OU) component of this name
+ */
+ public String getUName() {
+ return mUName != null ? mUName : "";
+ }
+ }
+}
diff --git a/core/java/android/net/http/SslError.java b/core/java/android/net/http/SslError.java
new file mode 100644
index 0000000..2788cb1
--- /dev/null
+++ b/core/java/android/net/http/SslError.java
@@ -0,0 +1,144 @@
+/*
+ * 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.net.http;
+
+import java.security.cert.X509Certificate;
+
+/**
+ * One or more individual SSL errors and the associated SSL certificate
+ *
+ * {@hide}
+ */
+public class SslError {
+
+ /**
+ * Individual SSL errors (in the order from the least to the most severe):
+ */
+
+ /**
+ * The certificate is not yet valid
+ */
+ public static final int SSL_NOTYETVALID = 0;
+ /**
+ * The certificate has expired
+ */
+ public static final int SSL_EXPIRED = 1;
+ /**
+ * Hostname mismatch
+ */
+ public static final int SSL_IDMISMATCH = 2;
+ /**
+ * The certificate authority is not trusted
+ */
+ public static final int SSL_UNTRUSTED = 3;
+
+
+ /**
+ * The number of different SSL errors (update if you add a new SSL error!!!)
+ */
+ public static final int SSL_MAX_ERROR = 4;
+
+ /**
+ * The SSL error set bitfield (each individual error is an bit index;
+ * multiple individual errors can be OR-ed)
+ */
+ int mErrors;
+
+ /**
+ * The SSL certificate associated with the error set
+ */
+ SslCertificate mCertificate;
+
+ /**
+ * Creates a new SSL error set object
+ * @param error The SSL error
+ * @param certificate The associated SSL certificate
+ */
+ public SslError(int error, SslCertificate certificate) {
+ addError(error);
+ mCertificate = certificate;
+ }
+
+ /**
+ * Creates a new SSL error set object
+ * @param error The SSL error
+ * @param certificate The associated SSL certificate
+ */
+ public SslError(int error, X509Certificate certificate) {
+ addError(error);
+ mCertificate = new SslCertificate(certificate);
+ }
+
+ /**
+ * @return The SSL certificate associated with the error set
+ */
+ public SslCertificate getCertificate() {
+ return mCertificate;
+ }
+
+ /**
+ * Adds the SSL error to the error set
+ * @param error The SSL error to add
+ * @return True iff the error being added is a known SSL error
+ */
+ public boolean addError(int error) {
+ boolean rval = (0 <= error && error < SslError.SSL_MAX_ERROR);
+ if (rval) {
+ mErrors |= (0x1 << error);
+ }
+
+ return rval;
+ }
+
+ /**
+ * @param error The SSL error to check
+ * @return True iff the set includes the error
+ */
+ public boolean hasError(int error) {
+ boolean rval = (0 <= error && error < SslError.SSL_MAX_ERROR);
+ if (rval) {
+ rval = ((mErrors & (0x1 << error)) != 0);
+ }
+
+ return rval;
+ }
+
+ /**
+ * @return The primary, most severe, SSL error in the set
+ */
+ public int getPrimaryError() {
+ if (mErrors != 0) {
+ // go from the most to the least severe errors
+ for (int error = SslError.SSL_MAX_ERROR - 1; error >= 0; --error) {
+ if ((mErrors & (0x1 << error)) != 0) {
+ return error;
+ }
+ }
+ }
+
+ return 0;
+ }
+
+ /**
+ * @return A String representation of this SSL error object
+ * (used mostly for debugging).
+ */
+ public String toString() {
+ return "primary error: " + getPrimaryError() +
+ " certificate: " + getCertificate();
+ }
+}
diff --git a/core/java/android/net/http/Timer.java b/core/java/android/net/http/Timer.java
new file mode 100644
index 0000000..cc15a30
--- /dev/null
+++ b/core/java/android/net/http/Timer.java
@@ -0,0 +1,41 @@
+/*
+ * 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.net.http;
+
+import android.os.SystemClock;
+
+/**
+ * {@hide}
+ * Debugging tool
+ */
+class Timer {
+
+ private long mStart;
+ private long mLast;
+
+ public Timer() {
+ mStart = mLast = SystemClock.uptimeMillis();
+ }
+
+ public void mark(String message) {
+ long now = SystemClock.uptimeMillis();
+ if (HttpLog.LOGV) {
+ HttpLog.v(message + " " + (now - mLast) + " total " + (now - mStart));
+ }
+ mLast = now;
+ }
+}
diff --git a/core/java/android/net/http/package.html b/core/java/android/net/http/package.html
new file mode 100755
index 0000000..a81cbce
--- /dev/null
+++ b/core/java/android/net/http/package.html
@@ -0,0 +1,2 @@
+<body>
+</body>
diff --git a/core/java/android/net/package.html b/core/java/android/net/package.html
new file mode 100755
index 0000000..47c57e6
--- /dev/null
+++ b/core/java/android/net/package.html
@@ -0,0 +1,5 @@
+<body>
+
+Classes that help with network access, beyond the normal java.net.* APIs.
+
+</body>
diff --git a/core/java/android/os/AsyncResult.java b/core/java/android/os/AsyncResult.java
new file mode 100644
index 0000000..5bad09d
--- /dev/null
+++ b/core/java/android/os/AsyncResult.java
@@ -0,0 +1,68 @@
+/*
+ * 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.os;
+
+import android.os.Message;
+
+/** @hide */
+public class AsyncResult
+{
+
+ /*************************** Instance Variables **************************/
+
+ // Expect either exception or result to be null
+ public Object userObj;
+ public Throwable exception;
+ public Object result;
+
+ /***************************** Class Methods *****************************/
+
+ /** Saves and sets m.obj */
+ public static AsyncResult
+ forMessage(Message m, Object r, Throwable ex)
+ {
+ AsyncResult ret;
+
+ ret = new AsyncResult (m.obj, r, ex);
+
+ m.obj = ret;
+
+ return ret;
+ }
+
+ /** Saves and sets m.obj */
+ public static AsyncResult
+ forMessage(Message m)
+ {
+ AsyncResult ret;
+
+ ret = new AsyncResult (m.obj, null, null);
+
+ m.obj = ret;
+
+ return ret;
+ }
+
+ /** please note, this sets m.obj to be this */
+ public
+ AsyncResult (Object uo, Object r, Throwable ex)
+ {
+ userObj = uo;
+ result = r;
+ exception = ex;
+ }
+}
diff --git a/core/java/android/os/BadParcelableException.java b/core/java/android/os/BadParcelableException.java
new file mode 100644
index 0000000..a1c5bb2
--- /dev/null
+++ b/core/java/android/os/BadParcelableException.java
@@ -0,0 +1,31 @@
+/*
+ * 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.os;
+import android.util.AndroidRuntimeException;
+
+/**
+ * The object you are calling has died, because its hosting process
+ * no longer exists.
+ */
+public class BadParcelableException extends AndroidRuntimeException {
+ public BadParcelableException(String msg) {
+ super(msg);
+ }
+ public BadParcelableException(Exception cause) {
+ super(cause);
+ }
+}
diff --git a/core/java/android/os/Base64Utils.java b/core/java/android/os/Base64Utils.java
new file mode 100644
index 0000000..684a469
--- /dev/null
+++ b/core/java/android/os/Base64Utils.java
@@ -0,0 +1,31 @@
+/*
+ * 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.os;
+
+/**
+ * {@hide}
+ */
+public class Base64Utils
+{
+ // TODO add encode api here if possible
+
+ public static byte [] decodeBase64(String data) {
+ return decodeBase64Native(data);
+ }
+ private static native byte[] decodeBase64Native(String data);
+}
+
diff --git a/core/java/android/os/BatteryManager.java b/core/java/android/os/BatteryManager.java
new file mode 100644
index 0000000..bf47555
--- /dev/null
+++ b/core/java/android/os/BatteryManager.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os;
+
+/**
+ * The BatteryManager class contains strings and constants used for values
+ * in the ACTION_BATTERY_CHANGED Intent.
+ */
+public class BatteryManager {
+
+ // values for "status" field in the ACTION_BATTERY_CHANGED Intent
+ public static final int BATTERY_STATUS_UNKNOWN = 1;
+ public static final int BATTERY_STATUS_CHARGING = 2;
+ public static final int BATTERY_STATUS_DISCHARGING = 3;
+ public static final int BATTERY_STATUS_NOT_CHARGING = 4;
+ public static final int BATTERY_STATUS_FULL = 5;
+
+ // values for "health" field in the ACTION_BATTERY_CHANGED Intent
+ public static final int BATTERY_HEALTH_UNKNOWN = 1;
+ public static final int BATTERY_HEALTH_GOOD = 2;
+ public static final int BATTERY_HEALTH_OVERHEAT = 3;
+ public static final int BATTERY_HEALTH_DEAD = 4;
+ public static final int BATTERY_HEALTH_OVER_VOLTAGE = 5;
+ public static final int BATTERY_HEALTH_UNSPECIFIED_FAILURE = 6;
+
+ // values of the "plugged" field in the ACTION_BATTERY_CHANGED intent
+ public static final int BATTERY_PLUGGED_AC = 1;
+ public static final int BATTERY_PLUGGED_USB = 2;
+
+}
diff --git a/core/java/android/os/Binder.java b/core/java/android/os/Binder.java
new file mode 100644
index 0000000..528e6bd
--- /dev/null
+++ b/core/java/android/os/Binder.java
@@ -0,0 +1,333 @@
+/*
+ * 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.os;
+
+import android.util.Config;
+import android.util.Log;
+
+import java.io.FileDescriptor;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.lang.ref.WeakReference;
+import java.lang.reflect.Modifier;
+
+/**
+ * Base class for a remotable object, the core part of a lightweight
+ * remote procedure call mechanism defined by {@link IBinder}.
+ * This class is an implementation of IBinder that provides
+ * the standard support creating a local implementation of such an object.
+ *
+ * <p>Most developers will not implement this class directly, instead using the
+ * <a href="{@docRoot}reference/aidl.html">aidl</a> tool to describe the desired
+ * interface, having it generate the appropriate Binder subclass. You can,
+ * however, derive directly from Binder to implement your own custom RPC
+ * protocol or simply instantiate a raw Binder object directly to use as a
+ * token that can be shared across processes.
+ *
+ * @see IBinder
+ */
+public class Binder implements IBinder {
+ /*
+ * Set this flag to true to detect anonymous, local or member classes
+ * that extend this Binder class and that are not static. These kind
+ * of classes can potentially create leaks.
+ */
+ private static final boolean FIND_POTENTIAL_LEAKS = false;
+ private static final String TAG = "Binder";
+
+ private int mObject;
+ private IInterface mOwner;
+ private String mDescriptor;
+
+ /**
+ * Return the ID of the process that sent you the current transaction
+ * that is being processed. This pid can be used with higher-level
+ * system services to determine its identity and check permissions.
+ * If the current thread is not currently executing an incoming transaction,
+ * then its own pid is returned.
+ */
+ public static final native int getCallingPid();
+
+ /**
+ * Return the ID of the user assigned to the process that sent you the
+ * current transaction that is being processed. This uid can be used with
+ * higher-level system services to determine its identity and check
+ * permissions. If the current thread is not currently executing an
+ * incoming transaction, then its own uid is returned.
+ */
+ public static final native int getCallingUid();
+
+ /**
+ * Reset the identity of the incoming IPC to the local process. This can
+ * be useful if, while handling an incoming call, you will be calling
+ * on interfaces of other objects that may be local to your process and
+ * need to do permission checks on the calls coming into them (so they
+ * will check the permission of your own local process, and not whatever
+ * process originally called you).
+ *
+ * @return Returns an opaque token that can be used to restore the
+ * original calling identity by passing it to
+ * {@link #restoreCallingIdentity(long)}.
+ *
+ * @see #getCallingPid()
+ * @see #getCallingUid()
+ * @see #restoreCallingIdentity(long)
+ */
+ public static final native long clearCallingIdentity();
+
+ /**
+ * Restore the identity of the incoming IPC back to a previously identity
+ * that was returned by {@link #clearCallingIdentity}.
+ *
+ * @param token The opaque token that was previously returned by
+ * {@link #clearCallingIdentity}.
+ *
+ * @see #clearCallingIdentity
+ */
+ public static final native void restoreCallingIdentity(long token);
+
+ /**
+ * Flush any Binder commands pending in the current thread to the kernel
+ * driver. This can be
+ * useful to call before performing an operation that may block for a long
+ * time, to ensure that any pending object references have been released
+ * in order to prevent the process from holding on to objects longer than
+ * it needs to.
+ */
+ public static final native void flushPendingCommands();
+
+ /**
+ * Add the calling thread to the IPC thread pool. This function does
+ * not return until the current process is exiting.
+ */
+ public static final native void joinThreadPool();
+
+ /**
+ * Default constructor initializes the object.
+ */
+ public Binder() {
+ init();
+
+ if (FIND_POTENTIAL_LEAKS) {
+ final Class<? extends Binder> klass = getClass();
+ if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
+ (klass.getModifiers() & Modifier.STATIC) == 0) {
+ Log.w(TAG, "The following Binder class should be static or leaks might occur: " +
+ klass.getCanonicalName());
+ }
+ }
+ }
+
+ /**
+ * Convenience method for associating a specific interface with the Binder.
+ * After calling, queryLocalInterface() will be implemented for you
+ * to return the given owner IInterface when the corresponding
+ * descriptor is requested.
+ */
+ public void attachInterface(IInterface owner, String descriptor) {
+ mOwner = owner;
+ mDescriptor = descriptor;
+ }
+
+ /**
+ * Default implementation returns an empty interface name.
+ */
+ public String getInterfaceDescriptor() {
+ return mDescriptor;
+ }
+
+ /**
+ * Default implementation always returns true -- if you got here,
+ * the object is alive.
+ */
+ public boolean pingBinder() {
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Note that if you're calling on a local binder, this always returns true
+ * because your process is alive if you're calling it.
+ */
+ public boolean isBinderAlive() {
+ return true;
+ }
+
+ /**
+ * Use information supplied to attachInterface() to return the
+ * associated IInterface if it matches the requested
+ * descriptor.
+ */
+ public IInterface queryLocalInterface(String descriptor) {
+ if (mDescriptor.equals(descriptor)) {
+ return mOwner;
+ }
+ return null;
+ }
+
+ /**
+ * Default implementation is a stub that returns false. You will want
+ * to override this to do the appropriate unmarshalling of transactions.
+ *
+ * <p>If you want to call this, call transact().
+ */
+ protected boolean onTransact(int code, Parcel data, Parcel reply,
+ int flags) throws RemoteException {
+ if (code == INTERFACE_TRANSACTION) {
+ reply.writeString(getInterfaceDescriptor());
+ return true;
+ } else if (code == DUMP_TRANSACTION) {
+ ParcelFileDescriptor fd = data.readFileDescriptor();
+ FileOutputStream fout = fd != null
+ ? new FileOutputStream(fd.getFileDescriptor()) : null;
+ PrintWriter pw = fout != null ? new PrintWriter(fout) : null;
+ if (pw != null) {
+ String[] args = data.readStringArray();
+ dump(fd.getFileDescriptor(), pw, args);
+ pw.flush();
+ }
+ if (fd != null) {
+ try {
+ fd.close();
+ } catch (IOException e) {
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Print the object's state into the given stream.
+ *
+ * @param fd The raw file descriptor that the dump is being sent to.
+ * @param fout The file to which you should dump your state. This will be
+ * closed for you after you return.
+ * @param args additional arguments to the dump request.
+ */
+ protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) {
+ }
+
+ /**
+ * Default implementation rewinds the parcels and calls onTransact. On
+ * the remote side, transact calls into the binder to do the IPC.
+ */
+ public final boolean transact(int code, Parcel data, Parcel reply,
+ int flags) throws RemoteException {
+ if (Config.LOGV) Log.v("Binder", "Transact: " + code + " to " + this);
+ if (data != null) {
+ data.setDataPosition(0);
+ }
+ boolean r = onTransact(code, data, reply, flags);
+ if (reply != null) {
+ reply.setDataPosition(0);
+ }
+ return r;
+ }
+
+ /**
+ * Local implementation is a no-op.
+ */
+ public void linkToDeath(DeathRecipient recipient, int flags) {
+ }
+
+ /**
+ * Local implementation is a no-op.
+ */
+ public boolean unlinkToDeath(DeathRecipient recipient, int flags) {
+ return true;
+ }
+
+ protected void finalize() throws Throwable {
+ try {
+ destroy();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ private native final void init();
+ private native final void destroy();
+ private boolean execTransact(int code, int dataObj, int replyObj,
+ int flags) {
+ Parcel data = Parcel.obtain(dataObj);
+ Parcel reply = Parcel.obtain(replyObj);
+ // theoretically, we should call transact, which will call onTransact,
+ // but all that does is rewind it, and we just got these from an IPC,
+ // so we'll just call it directly.
+ boolean res;
+ try {
+ res = onTransact(code, data, reply, flags);
+ } catch (RemoteException e) {
+ reply.writeException(e);
+ res = true;
+ } catch (RuntimeException e) {
+ reply.writeException(e);
+ res = true;
+ }
+ reply.recycle();
+ data.recycle();
+ return res;
+ }
+}
+
+final class BinderProxy implements IBinder {
+ public native boolean pingBinder();
+ public native boolean isBinderAlive();
+
+ public IInterface queryLocalInterface(String descriptor) {
+ return null;
+ }
+
+ public native String getInterfaceDescriptor() throws RemoteException;
+ public native boolean transact(int code, Parcel data, Parcel reply,
+ int flags) throws RemoteException;
+ public native void linkToDeath(DeathRecipient recipient, int flags)
+ throws RemoteException;
+ public native boolean unlinkToDeath(DeathRecipient recipient, int flags);
+
+ BinderProxy() {
+ mSelf = new WeakReference(this);
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ destroy();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ private native final void destroy();
+
+ private static final void sendDeathNotice(DeathRecipient recipient) {
+ if (Config.LOGV) Log.v("JavaBinder", "sendDeathNotice to " + recipient);
+ try {
+ recipient.binderDied();
+ }
+ catch (RuntimeException exc) {
+ Log.w("BinderNative", "Uncaught exception from death notification",
+ exc);
+ }
+ }
+
+ final private WeakReference mSelf;
+ private int mObject;
+}
diff --git a/core/java/android/os/Broadcaster.java b/core/java/android/os/Broadcaster.java
new file mode 100644
index 0000000..96dc61a
--- /dev/null
+++ b/core/java/android/os/Broadcaster.java
@@ -0,0 +1,212 @@
+/*
+ * 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.os;
+
+/** @hide */
+public class Broadcaster
+{
+ public Broadcaster()
+ {
+ }
+
+ /**
+ * Sign up for notifications about something.
+ *
+ * When this broadcaster pushes a message with senderWhat in the what field,
+ * target will be sent a copy of that message with targetWhat in the what field.
+ */
+ public void request(int senderWhat, Handler target, int targetWhat)
+ {
+ synchronized (this) {
+ Registration r = null;
+ if (mReg == null) {
+ r = new Registration();
+ r.senderWhat = senderWhat;
+ r.targets = new Handler[1];
+ r.targetWhats = new int[1];
+ r.targets[0] = target;
+ r.targetWhats[0] = targetWhat;
+ mReg = r;
+ r.next = r;
+ r.prev = r;
+ } else {
+ // find its place in the map
+ Registration start = mReg;
+ r = start;
+ do {
+ if (r.senderWhat >= senderWhat) {
+ break;
+ }
+ r = r.next;
+ } while (r != start);
+ int n;
+ if (r.senderWhat != senderWhat) {
+ // we didn't find a senderWhat match, but r is right
+ // after where it goes
+ Registration reg = new Registration();
+ reg.senderWhat = senderWhat;
+ reg.targets = new Handler[1];
+ reg.targetWhats = new int[1];
+ reg.next = r;
+ reg.prev = r.prev;
+ r.prev.next = reg;
+ r.prev = reg;
+
+ if (r == mReg && r.senderWhat > reg.senderWhat) {
+ mReg = reg;
+ }
+
+ r = reg;
+ n = 0;
+ } else {
+ n = r.targets.length;
+ Handler[] oldTargets = r.targets;
+ int[] oldWhats = r.targetWhats;
+ // check for duplicates, and don't do it if we are dup.
+ for (int i=0; i<n; i++) {
+ if (oldTargets[i] == target && oldWhats[i] == targetWhat) {
+ return;
+ }
+ }
+ r.targets = new Handler[n+1];
+ System.arraycopy(oldTargets, 0, r.targets, 0, n);
+ r.targetWhats = new int[n+1];
+ System.arraycopy(oldWhats, 0, r.targetWhats, 0, n);
+ }
+ r.targets[n] = target;
+ r.targetWhats[n] = targetWhat;
+ }
+ }
+ }
+
+ /**
+ * Unregister for notifications for this senderWhat/target/targetWhat tuple.
+ */
+ public void cancelRequest(int senderWhat, Handler target, int targetWhat)
+ {
+ synchronized (this) {
+ Registration start = mReg;
+ Registration r = start;
+
+ if (r == null) {
+ return;
+ }
+
+ do {
+ if (r.senderWhat >= senderWhat) {
+ break;
+ }
+ r = r.next;
+ } while (r != start);
+
+ if (r.senderWhat == senderWhat) {
+ Handler[] targets = r.targets;
+ int[] whats = r.targetWhats;
+ int oldLen = targets.length;
+ for (int i=0; i<oldLen; i++) {
+ if (targets[i] == target && whats[i] == targetWhat) {
+ r.targets = new Handler[oldLen-1];
+ r.targetWhats = new int[oldLen-1];
+ if (i > 0) {
+ System.arraycopy(targets, 0, r.targets, 0, i);
+ System.arraycopy(whats, 0, r.targetWhats, 0, i);
+ }
+
+ int remainingLen = oldLen-i-1;
+ if (remainingLen != 0) {
+ System.arraycopy(targets, i+1, r.targets, i,
+ remainingLen);
+ System.arraycopy(whats, i+1, r.targetWhats, i,
+ remainingLen);
+ }
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * For debugging purposes, print the registrations to System.out
+ */
+ public void dumpRegistrations()
+ {
+ synchronized (this) {
+ Registration start = mReg;
+ System.out.println("Broadcaster " + this + " {");
+ if (start != null) {
+ Registration r = start;
+ do {
+ System.out.println(" senderWhat=" + r.senderWhat);
+ int n = r.targets.length;
+ for (int i=0; i<n; i++) {
+ System.out.println(" [" + r.targetWhats[i]
+ + "] " + r.targets[i]);
+ }
+ r = r.next;
+ } while (r != start);
+ }
+ System.out.println("}");
+ }
+ }
+
+ /**
+ * Send out msg. Anyone who has registered via the request() method will be
+ * sent the message.
+ */
+ public void broadcast(Message msg)
+ {
+ synchronized (this) {
+ if (mReg == null) {
+ return;
+ }
+
+ int senderWhat = msg.what;
+ Registration start = mReg;
+ Registration r = start;
+ do {
+ if (r.senderWhat >= senderWhat) {
+ break;
+ }
+ r = r.next;
+ } while (r != start);
+ if (r.senderWhat == senderWhat) {
+ Handler[] targets = r.targets;
+ int[] whats = r.targetWhats;
+ int n = targets.length;
+ for (int i=0; i<n; i++) {
+ Handler target = targets[i];
+ Message m = Message.obtain();
+ m.copyFrom(msg);
+ m.what = whats[i];
+ target.sendMessage(m);
+ }
+ }
+ }
+ }
+
+ private class Registration
+ {
+ Registration next;
+ Registration prev;
+
+ int senderWhat;
+ Handler[] targets;
+ int[] targetWhats;
+ }
+ private Registration mReg;
+}
diff --git a/core/java/android/os/Build.java b/core/java/android/os/Build.java
new file mode 100644
index 0000000..cdf907b
--- /dev/null
+++ b/core/java/android/os/Build.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os;
+
+/**
+ * Information about the current build, extracted from system properties.
+ */
+public class Build {
+ /** Value used for when a build property is unknown. */
+ private static final String UNKNOWN = "unknown";
+
+ /** Either a changelist number, or a label like "M4-rc20". */
+ public static final String ID = getString("ro.build.id");
+
+ /** The name of the overall product. */
+ public static final String PRODUCT = getString("ro.product.name");
+
+ /** The name of the industrial design. */
+ public static final String DEVICE = getString("ro.product.device");
+
+ /** The name of the underlying board, like "goldfish". */
+ public static final String BOARD = getString("ro.product.board");
+
+ /** The brand (e.g., carrier) the software is customized for, if any. */
+ public static final String BRAND = getString("ro.product.brand");
+
+ /** The end-user-visible name for the end product. */
+ public static final String MODEL = getString("ro.product.model");
+
+ /** Various version strings. */
+ public static class VERSION {
+ /**
+ * The internal value used by the underlying source control to
+ * represent this build. E.g., a perforce changelist number
+ * or a git hash.
+ */
+ public static final String INCREMENTAL = getString("ro.build.version.incremental");
+
+ /**
+ * The user-visible version string. E.g., "1.0" or "3.4b5".
+ */
+ public static final String RELEASE = getString("ro.build.version.release");
+
+ /**
+ * The user-visible SDK version of the framework. It is an integer starting at 1.
+ */
+ public static final String SDK = getString("ro.build.version.sdk");
+ }
+
+ /** The type of build, like "user" or "eng". */
+ public static final String TYPE = getString("ro.build.type");
+
+ /** Comma-separated tags describing the build, like "unsigned,debug". */
+ public static final String TAGS = getString("ro.build.tags");
+
+ /** A string that uniquely identifies this build. Do not attempt to parse this value. */
+ public static final String FINGERPRINT = getString("ro.build.fingerprint");
+
+ // The following properties only make sense for internal engineering builds.
+ public static final long TIME = getLong("ro.build.date.utc") * 1000;
+ public static final String USER = getString("ro.build.user");
+ public static final String HOST = getString("ro.build.host");
+
+ private static String getString(String property) {
+ return SystemProperties.get(property, UNKNOWN);
+ }
+
+ private static long getLong(String property) {
+ try {
+ return Long.parseLong(SystemProperties.get(property));
+ } catch (NumberFormatException e) {
+ return -1;
+ }
+ }
+}
diff --git a/core/java/android/os/Bundle.aidl b/core/java/android/os/Bundle.aidl
new file mode 100644
index 0000000..b9e1224
--- /dev/null
+++ b/core/java/android/os/Bundle.aidl
@@ -0,0 +1,20 @@
+/* //device/java/android/android/os/Bundle.aidl
+**
+** Copyright 2007, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.os;
+
+parcelable Bundle;
diff --git a/core/java/android/os/Bundle.java b/core/java/android/os/Bundle.java
new file mode 100644
index 0000000..b669fa2
--- /dev/null
+++ b/core/java/android/os/Bundle.java
@@ -0,0 +1,1452 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os;
+
+import android.util.Log;
+import android.util.SparseArray;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A mapping from String values to various Parcelable types.
+ *
+ */
+public final class Bundle implements Parcelable, Cloneable {
+ private static final String LOG_TAG = "Bundle";
+ public static final Bundle EMPTY;
+
+ static {
+ EMPTY = new Bundle();
+ EMPTY.mMap = Collections.unmodifiableMap(new HashMap<String, Object>());
+ }
+
+ // Invariant - exactly one of mMap / mParcelledData will be null
+ // (except inside a call to unparcel)
+
+ /* package */ Map<String, Object> mMap = null;
+
+ /*
+ * If mParcelledData is non-null, then mMap will be null and the
+ * data are stored as a Parcel containing a Bundle. When the data
+ * are unparcelled, mParcelledData willbe set to null.
+ */
+ /* package */ Parcel mParcelledData = null;
+
+ private boolean mHasFds = false;
+ private boolean mFdsKnown = true;
+
+ /**
+ * The ClassLoader used when unparcelling data from mParcelledData.
+ */
+ private ClassLoader mClassLoader;
+
+ /**
+ * Constructs a new, empty Bundle.
+ */
+ public Bundle() {
+ mMap = new HashMap<String, Object>();
+ mClassLoader = getClass().getClassLoader();
+ }
+
+ /**
+ * Constructs a Bundle whose data is stored as a Parcel. The data
+ * will be unparcelled on first contact, using the assigned ClassLoader.
+ *
+ * @param parcelledData a Parcel containing a Bundle
+ */
+ Bundle(Parcel parcelledData) {
+ readFromParcel(parcelledData);
+ }
+
+ /**
+ * Constructs a new, empty Bundle that uses a specific ClassLoader for
+ * instantiating Parcelable and Serializable objects.
+ *
+ * @param loader An explicit ClassLoader to use when instantiating objects
+ * inside of the Bundle.
+ */
+ public Bundle(ClassLoader loader) {
+ mMap = new HashMap<String, Object>();
+ mClassLoader = loader;
+ }
+
+ /**
+ * Constructs a new, empty Bundle sized to hold the given number of
+ * elements. The Bundle will grow as needed.
+ *
+ * @param capacity the initial capacity of the Bundle
+ */
+ public Bundle(int capacity) {
+ mMap = new HashMap<String, Object>(capacity);
+ mClassLoader = getClass().getClassLoader();
+ }
+
+ /**
+ * Constructs a Bundle containing a copy of the mappings from the given
+ * Bundle.
+ *
+ * @param b a Bundle to be copied.
+ */
+ public Bundle(Bundle b) {
+ if (b.mParcelledData != null) {
+ mParcelledData = Parcel.obtain();
+ mParcelledData.appendFrom(b.mParcelledData, 0, b.mParcelledData.dataSize());
+ mParcelledData.setDataPosition(0);
+ } else {
+ mParcelledData = null;
+ }
+
+ if (b.mMap != null) {
+ mMap = new HashMap<String, Object>(b.mMap);
+ } else {
+ mMap = null;
+ }
+
+ mHasFds = b.mHasFds;
+ mFdsKnown = b.mFdsKnown;
+ mClassLoader = b.mClassLoader;
+ }
+
+ /**
+ * Changes the ClassLoader this Bundle uses when instantiating objects.
+ *
+ * @param loader An explicit ClassLoader to use when instantiating objects
+ * inside of the Bundle.
+ */
+ public void setClassLoader(ClassLoader loader) {
+ mClassLoader = loader;
+ }
+
+ /**
+ * Clones the current Bundle. The internal map is cloned, but the keys and
+ * values to which it refers are copied by reference.
+ */
+ @Override
+ public Object clone() {
+ return new Bundle(this);
+ }
+
+ /**
+ * If the underlying data are stored as a Parcel, unparcel them
+ * using the currently assigned class loader.
+ */
+ /* package */ synchronized void unparcel() {
+ if (mParcelledData == null) {
+ return;
+ }
+
+ mParcelledData.setDataPosition(0);
+ Bundle b = mParcelledData.readBundleUnpacked(mClassLoader);
+ mMap = b.mMap;
+
+ mHasFds = mParcelledData.hasFileDescriptors();
+ mFdsKnown = true;
+
+ mParcelledData.recycle();
+ mParcelledData = null;
+ }
+
+ /**
+ * Returns the number of mappings contained in this Bundle.
+ *
+ * @return the number of mappings as an int.
+ */
+ public int size() {
+ unparcel();
+ return mMap.size();
+ }
+
+ /**
+ * Returns true if the mapping of this Bundle is empty, false otherwise.
+ */
+ public boolean isEmpty() {
+ unparcel();
+ return mMap.isEmpty();
+ }
+
+ /**
+ * Removes all elements from the mapping of this Bundle.
+ */
+ public void clear() {
+ unparcel();
+ mMap.clear();
+ mHasFds = false;
+ mFdsKnown = true;
+ }
+
+ /**
+ * Returns true if the given key is contained in the mapping
+ * of this Bundle.
+ *
+ * @param key a String key
+ * @return true if the key is part of the mapping, false otherwise
+ */
+ public boolean containsKey(String key) {
+ unparcel();
+ return mMap.containsKey(key);
+ }
+
+ /**
+ * Returns the entry with the given key as an object.
+ *
+ * @param key a String key
+ * @return an Object, or null
+ */
+ public Object get(String key) {
+ unparcel();
+ return mMap.get(key);
+ }
+
+ /**
+ * Removes any entry with the given key from the mapping of this Bundle.
+ *
+ * @param key a String key
+ */
+ public void remove(String key) {
+ unparcel();
+ mMap.remove(key);
+ }
+
+ /**
+ * Inserts all mappings from the given Bundle into this Bundle.
+ *
+ * @param map a Bundle
+ */
+ public void putAll(Bundle map) {
+ unparcel();
+ map.unparcel();
+ mMap.putAll(map.mMap);
+
+ // fd state is now known if and only if both bundles already knew
+ mHasFds |= map.mHasFds;
+ mFdsKnown = mFdsKnown && map.mFdsKnown;
+ }
+
+ /**
+ * Returns a Set containing the Strings used as keys in this Bundle.
+ *
+ * @return a Set of String keys
+ */
+ public Set<String> keySet() {
+ unparcel();
+ return mMap.keySet();
+ }
+
+ /**
+ * Reports whether the bundle contains any parcelled file descriptors.
+ */
+ public boolean hasFileDescriptors() {
+ if (!mFdsKnown) {
+ boolean fdFound = false; // keep going until we find one or run out of data
+
+ if (mParcelledData != null) {
+ if (mParcelledData.hasFileDescriptors()) {
+ fdFound = true;
+ }
+ } else {
+ // It's been unparcelled, so we need to walk the map
+ Iterator<Map.Entry<String, Object>> iter = mMap.entrySet().iterator();
+ while (!fdFound && iter.hasNext()) {
+ Object obj = iter.next().getValue();
+ if (obj instanceof Parcelable) {
+ if ((((Parcelable)obj).describeContents()
+ & Parcelable.CONTENTS_FILE_DESCRIPTOR) != 0) {
+ fdFound = true;
+ break;
+ }
+ } else if (obj instanceof Parcelable[]) {
+ Parcelable[] array = (Parcelable[]) obj;
+ for (int n = array.length - 1; n >= 0; n--) {
+ if ((array[n].describeContents()
+ & Parcelable.CONTENTS_FILE_DESCRIPTOR) != 0) {
+ fdFound = true;
+ break;
+ }
+ }
+ } else if (obj instanceof SparseArray) {
+ SparseArray<? extends Parcelable> array =
+ (SparseArray<? extends Parcelable>) obj;
+ for (int n = array.size() - 1; n >= 0; n--) {
+ if ((array.get(n).describeContents()
+ & Parcelable.CONTENTS_FILE_DESCRIPTOR) != 0) {
+ fdFound = true;
+ break;
+ }
+ }
+ } else if (obj instanceof ArrayList) {
+ ArrayList array = (ArrayList) obj;
+ // an ArrayList here might contain either Strings or
+ // Parcelables; only look inside for Parcelables
+ if ((array.size() > 0)
+ && (array.get(0) instanceof Parcelable)) {
+ for (int n = array.size() - 1; n >= 0; n--) {
+ Parcelable p = (Parcelable) array.get(n);
+ if (p != null && ((p.describeContents()
+ & Parcelable.CONTENTS_FILE_DESCRIPTOR) != 0)) {
+ fdFound = true;
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ mHasFds = fdFound;
+ mFdsKnown = true;
+ }
+ return mHasFds;
+ }
+
+ /**
+ * Inserts a Boolean value into the mapping of this Bundle, replacing
+ * any existing value for the given key. Either key or value may be null.
+ *
+ * @param key a String, or null
+ * @param value a Boolean, or null
+ */
+ public void putBoolean(String key, boolean value) {
+ unparcel();
+ mMap.put(key, value);
+ }
+
+ /**
+ * Inserts a byte value into the mapping of this Bundle, replacing
+ * any existing value for the given key.
+ *
+ * @param key a String, or null
+ * @param value a byte
+ */
+ public void putByte(String key, byte value) {
+ unparcel();
+ mMap.put(key, value);
+ }
+
+ /**
+ * Inserts a char value into the mapping of this Bundle, replacing
+ * any existing value for the given key.
+ *
+ * @param key a String, or null
+ * @param value a char, or null
+ */
+ public void putChar(String key, char value) {
+ unparcel();
+ mMap.put(key, value);
+ }
+
+ /**
+ * Inserts a short value into the mapping of this Bundle, replacing
+ * any existing value for the given key.
+ *
+ * @param key a String, or null
+ * @param value a short
+ */
+ public void putShort(String key, short value) {
+ unparcel();
+ mMap.put(key, value);
+ }
+
+ /**
+ * Inserts an int value into the mapping of this Bundle, replacing
+ * any existing value for the given key.
+ *
+ * @param key a String, or null
+ * @param value an int, or null
+ */
+ public void putInt(String key, int value) {
+ unparcel();
+ mMap.put(key, value);
+ }
+
+ /**
+ * Inserts a long value into the mapping of this Bundle, replacing
+ * any existing value for the given key.
+ *
+ * @param key a String, or null
+ * @param value a long
+ */
+ public void putLong(String key, long value) {
+ unparcel();
+ mMap.put(key, value);
+ }
+
+ /**
+ * Inserts a float value into the mapping of this Bundle, replacing
+ * any existing value for the given key.
+ *
+ * @param key a String, or null
+ * @param value a float
+ */
+ public void putFloat(String key, float value) {
+ unparcel();
+ mMap.put(key, value);
+ }
+
+ /**
+ * Inserts a double value into the mapping of this Bundle, replacing
+ * any existing value for the given key.
+ *
+ * @param key a String, or null
+ * @param value a double
+ */
+ public void putDouble(String key, double value) {
+ unparcel();
+ mMap.put(key, value);
+ }
+
+ /**
+ * Inserts a String value into the mapping of this Bundle, replacing
+ * any existing value for the given key. Either key or value may be null.
+ *
+ * @param key a String, or null
+ * @param value a String, or null
+ */
+ public void putString(String key, String value) {
+ unparcel();
+ mMap.put(key, value);
+ }
+
+ /**
+ * Inserts a CharSequence value into the mapping of this Bundle, replacing
+ * any existing value for the given key. Either key or value may be null.
+ *
+ * @param key a String, or null
+ * @param value a CharSequence, or null
+ */
+ public void putCharSequence(String key, CharSequence value) {
+ unparcel();
+ mMap.put(key, value);
+ }
+
+ /**
+ * Inserts a Parcelable value into the mapping of this Bundle, replacing
+ * any existing value for the given key. Either key or value may be null.
+ *
+ * @param key a String, or null
+ * @param value a Parcelable object, or null
+ */
+ public void putParcelable(String key, Parcelable value) {
+ unparcel();
+ mMap.put(key, value);
+ mFdsKnown = false;
+ }
+
+ /**
+ * Inserts an array of Parcelable values into the mapping of this Bundle,
+ * replacing any existing value for the given key. Either key or value may
+ * be null.
+ *
+ * @param key a String, or null
+ * @param value an array of Parcelable objects, or null
+ */
+ public void putParcelableArray(String key, Parcelable[] value) {
+ unparcel();
+ mMap.put(key, value);
+ mFdsKnown = false;
+ }
+
+ /**
+ * Inserts a List of Parcelable values into the mapping of this Bundle,
+ * replacing any existing value for the given key. Either key or value may
+ * be null.
+ *
+ * @param key a String, or null
+ * @param value an ArrayList of Parcelable objects, or null
+ */
+ public void putParcelableArrayList(String key,
+ ArrayList<? extends Parcelable> value) {
+ unparcel();
+ mMap.put(key, value);
+ mFdsKnown = false;
+ }
+
+ /**
+ * Inserts a SparceArray of Parcelable values into the mapping of this
+ * Bundle, replacing any existing value for the given key. Either key
+ * or value may be null.
+ *
+ * @param key a String, or null
+ * @param value a SparseArray of Parcelable objects, or null
+ */
+ public void putSparseParcelableArray(String key,
+ SparseArray<? extends Parcelable> value) {
+ unparcel();
+ mMap.put(key, value);
+ mFdsKnown = false;
+ }
+
+ /**
+ * Inserts an ArrayList<Integer> value into the mapping of this Bundle, replacing
+ * any existing value for the given key. Either key or value may be null.
+ *
+ * @param key a String, or null
+ * @param value an ArrayList<Integer> object, or null
+ */
+ public void putIntegerArrayList(String key, ArrayList<Integer> value) {
+ unparcel();
+ mMap.put(key, value);
+ }
+
+ /**
+ * Inserts an ArrayList<String> value into the mapping of this Bundle, replacing
+ * any existing value for the given key. Either key or value may be null.
+ *
+ * @param key a String, or null
+ * @param value an ArrayList<String> object, or null
+ */
+ public void putStringArrayList(String key, ArrayList<String> value) {
+ unparcel();
+ mMap.put(key, value);
+ }
+
+ /**
+ * Inserts a Serializable value into the mapping of this Bundle, replacing
+ * any existing value for the given key. Either key or value may be null.
+ *
+ * @param key a String, or null
+ * @param value a Serializable object, or null
+ */
+ public void putSerializable(String key, Serializable value) {
+ unparcel();
+ mMap.put(key, value);
+ }
+
+ /**
+ * Inserts a boolean array value into the mapping of this Bundle, replacing
+ * any existing value for the given key. Either key or value may be null.
+ *
+ * @param key a String, or null
+ * @param value a boolean array object, or null
+ */
+ public void putBooleanArray(String key, boolean[] value) {
+ unparcel();
+ mMap.put(key, value);
+ }
+
+ /**
+ * Inserts a byte array value into the mapping of this Bundle, replacing
+ * any existing value for the given key. Either key or value may be null.
+ *
+ * @param key a String, or null
+ * @param value a byte array object, or null
+ */
+ public void putByteArray(String key, byte[] value) {
+ unparcel();
+ mMap.put(key, value);
+ }
+
+ /**
+ * Inserts a short array value into the mapping of this Bundle, replacing
+ * any existing value for the given key. Either key or value may be null.
+ *
+ * @param key a String, or null
+ * @param value a short array object, or null
+ */
+ public void putShortArray(String key, short[] value) {
+ unparcel();
+ mMap.put(key, value);
+ }
+
+ /**
+ * Inserts a char array value into the mapping of this Bundle, replacing
+ * any existing value for the given key. Either key or value may be null.
+ *
+ * @param key a String, or null
+ * @param value a char array object, or null
+ */
+ public void putCharArray(String key, char[] value) {
+ unparcel();
+ mMap.put(key, value);
+ }
+
+ /**
+ * Inserts an int array value into the mapping of this Bundle, replacing
+ * any existing value for the given key. Either key or value may be null.
+ *
+ * @param key a String, or null
+ * @param value an int array object, or null
+ */
+ public void putIntArray(String key, int[] value) {
+ unparcel();
+ mMap.put(key, value);
+ }
+
+ /**
+ * Inserts a long array value into the mapping of this Bundle, replacing
+ * any existing value for the given key. Either key or value may be null.
+ *
+ * @param key a String, or null
+ * @param value a long array object, or null
+ */
+ public void putLongArray(String key, long[] value) {
+ unparcel();
+ mMap.put(key, value);
+ }
+
+ /**
+ * Inserts a float array value into the mapping of this Bundle, replacing
+ * any existing value for the given key. Either key or value may be null.
+ *
+ * @param key a String, or null
+ * @param value a float array object, or null
+ */
+ public void putFloatArray(String key, float[] value) {
+ unparcel();
+ mMap.put(key, value);
+ }
+
+ /**
+ * Inserts a double array value into the mapping of this Bundle, replacing
+ * any existing value for the given key. Either key or value may be null.
+ *
+ * @param key a String, or null
+ * @param value a double array object, or null
+ */
+ public void putDoubleArray(String key, double[] value) {
+ unparcel();
+ mMap.put(key, value);
+ }
+
+ /**
+ * Inserts a String array value into the mapping of this Bundle, replacing
+ * any existing value for the given key. Either key or value may be null.
+ *
+ * @param key a String, or null
+ * @param value a String array object, or null
+ */
+ public void putStringArray(String key, String[] value) {
+ unparcel();
+ mMap.put(key, value);
+ }
+
+ /**
+ * Inserts a Bundle value into the mapping of this Bundle, replacing
+ * any existing value for the given key. Either key or value may be null.
+ *
+ * @param key a String, or null
+ * @param value a Bundle object, or null
+ */
+ public void putBundle(String key, Bundle value) {
+ unparcel();
+ mMap.put(key, value);
+ }
+
+ /**
+ * Inserts an IBinder value into the mapping of this Bundle, replacing
+ * any existing value for the given key. Either key or value may be null.
+ *
+ * @param key a String, or null
+ * @param value an IBinder object, or null
+ *
+ * @deprecated
+ * @hide
+ */
+ @Deprecated
+ public void putIBinder(String key, IBinder value) {
+ unparcel();
+ mMap.put(key, value);
+ }
+
+ /**
+ * Returns the value associated with the given key, or false if
+ * no mapping of the desired type exists for the given key.
+ *
+ * @param key a String
+ * @return a boolean value
+ */
+ public boolean getBoolean(String key) {
+ unparcel();
+ return getBoolean(key, false);
+ }
+
+ // Log a message if the value was non-null but not of the expected type
+ private void typeWarning(String key, Object value, String className,
+ Object defaultValue, ClassCastException e) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("Key ");
+ sb.append(key);
+ sb.append(" expected ");
+ sb.append(className);
+ sb.append(" but value was a ");
+ sb.append(value.getClass().getName());
+ sb.append(". The default value ");
+ sb.append(defaultValue);
+ sb.append(" was returned.");
+ Log.w(LOG_TAG, sb.toString());
+ Log.w(LOG_TAG, "Attempt to cast generated internal exception:", e);
+ }
+
+ private void typeWarning(String key, Object value, String className,
+ ClassCastException e) {
+ typeWarning(key, value, className, "<null>", e);
+ }
+
+ /**
+ * Returns the value associated with the given key, or defaultValue if
+ * no mapping of the desired type exists for the given key.
+ *
+ * @param key a String
+ * @return a boolean value
+ */
+ public boolean getBoolean(String key, boolean defaultValue) {
+ unparcel();
+ Object o = mMap.get(key);
+ if (o == null) {
+ return defaultValue;
+ }
+ try {
+ return (Boolean) o;
+ } catch (ClassCastException e) {
+ typeWarning(key, o, "Boolean", defaultValue, e);
+ return defaultValue;
+ }
+ }
+
+ /**
+ * Returns the value associated with the given key, or (byte) 0 if
+ * no mapping of the desired type exists for the given key.
+ *
+ * @param key a String
+ * @return a byte value
+ */
+ public byte getByte(String key) {
+ unparcel();
+ return getByte(key, (byte) 0);
+ }
+
+ /**
+ * Returns the value associated with the given key, or defaultValue if
+ * no mapping of the desired type exists for the given key.
+ *
+ * @param key a String
+ * @return a byte value
+ */
+ public Byte getByte(String key, byte defaultValue) {
+ unparcel();
+ Object o = mMap.get(key);
+ if (o == null) {
+ return defaultValue;
+ }
+ try {
+ return (Byte) o;
+ } catch (ClassCastException e) {
+ typeWarning(key, o, "Byte", defaultValue, e);
+ return defaultValue;
+ }
+ }
+
+ /**
+ * Returns the value associated with the given key, or false if
+ * no mapping of the desired type exists for the given key.
+ *
+ * @param key a String
+ * @return a char value
+ */
+ public char getChar(String key) {
+ unparcel();
+ return getChar(key, (char) 0);
+ }
+
+ /**
+ * Returns the value associated with the given key, or (char) 0 if
+ * no mapping of the desired type exists for the given key.
+ *
+ * @param key a String
+ * @return a char value
+ */
+ public char getChar(String key, char defaultValue) {
+ unparcel();
+ Object o = mMap.get(key);
+ if (o == null) {
+ return defaultValue;
+ }
+ try {
+ return (Character) o;
+ } catch (ClassCastException e) {
+ typeWarning(key, o, "Character", defaultValue, e);
+ return defaultValue;
+ }
+ }
+
+ /**
+ * Returns the value associated with the given key, or (short) 0 if
+ * no mapping of the desired type exists for the given key.
+ *
+ * @param key a String
+ * @return a short value
+ */
+ public short getShort(String key) {
+ unparcel();
+ return getShort(key, (short) 0);
+ }
+
+ /**
+ * Returns the value associated with the given key, or defaultValue if
+ * no mapping of the desired type exists for the given key.
+ *
+ * @param key a String
+ * @return a short value
+ */
+ public short getShort(String key, short defaultValue) {
+ unparcel();
+ Object o = mMap.get(key);
+ if (o == null) {
+ return defaultValue;
+ }
+ try {
+ return (Short) o;
+ } catch (ClassCastException e) {
+ typeWarning(key, o, "Short", defaultValue, e);
+ return defaultValue;
+ }
+ }
+
+ /**
+ * Returns the value associated with the given key, or 0 if
+ * no mapping of the desired type exists for the given key.
+ *
+ * @param key a String
+ * @return an int value
+ */
+ public int getInt(String key) {
+ unparcel();
+ return getInt(key, 0);
+ }
+
+ /**
+ * Returns the value associated with the given key, or defaultValue if
+ * no mapping of the desired type exists for the given key.
+ *
+ * @param key a String
+ * @return an int value
+ */
+ public int getInt(String key, int defaultValue) {
+ unparcel();
+ Object o = mMap.get(key);
+ if (o == null) {
+ return defaultValue;
+ }
+ try {
+ return (Integer) o;
+ } catch (ClassCastException e) {
+ typeWarning(key, o, "Integer", defaultValue, e);
+ return defaultValue;
+ }
+ }
+
+ /**
+ * Returns the value associated with the given key, or 0L if
+ * no mapping of the desired type exists for the given key.
+ *
+ * @param key a String
+ * @return a long value
+ */
+ public long getLong(String key) {
+ unparcel();
+ return getLong(key, 0L);
+ }
+
+ /**
+ * Returns the value associated with the given key, or defaultValue if
+ * no mapping of the desired type exists for the given key.
+ *
+ * @param key a String
+ * @return a long value
+ */
+ public long getLong(String key, long defaultValue) {
+ unparcel();
+ Object o = mMap.get(key);
+ if (o == null) {
+ return defaultValue;
+ }
+ try {
+ return (Long) o;
+ } catch (ClassCastException e) {
+ typeWarning(key, o, "Long", defaultValue, e);
+ return defaultValue;
+ }
+ }
+
+ /**
+ * Returns the value associated with the given key, or 0.0f if
+ * no mapping of the desired type exists for the given key.
+ *
+ * @param key a String
+ * @return a float value
+ */
+ public float getFloat(String key) {
+ unparcel();
+ return getFloat(key, 0.0f);
+ }
+
+ /**
+ * Returns the value associated with the given key, or defaultValue if
+ * no mapping of the desired type exists for the given key.
+ *
+ * @param key a String
+ * @return a float value
+ */
+ public float getFloat(String key, float defaultValue) {
+ unparcel();
+ Object o = mMap.get(key);
+ if (o == null) {
+ return defaultValue;
+ }
+ try {
+ return (Float) o;
+ } catch (ClassCastException e) {
+ typeWarning(key, o, "Float", defaultValue, e);
+ return defaultValue;
+ }
+ }
+
+ /**
+ * Returns the value associated with the given key, or 0.0 if
+ * no mapping of the desired type exists for the given key.
+ *
+ * @param key a String
+ * @return a double value
+ */
+ public double getDouble(String key) {
+ unparcel();
+ return getDouble(key, 0.0);
+ }
+
+ /**
+ * Returns the value associated with the given key, or defaultValue if
+ * no mapping of the desired type exists for the given key.
+ *
+ * @param key a String
+ * @return a double value
+ */
+ public double getDouble(String key, double defaultValue) {
+ unparcel();
+ Object o = mMap.get(key);
+ if (o == null) {
+ return defaultValue;
+ }
+ try {
+ return (Double) o;
+ } catch (ClassCastException e) {
+ typeWarning(key, o, "Double", defaultValue, e);
+ return defaultValue;
+ }
+ }
+
+
+ /**
+ * Returns the value associated with the given key, or null if
+ * no mapping of the desired type exists for the given key or a null
+ * value is explicitly associated with the key.
+ *
+ * @param key a String, or null
+ * @return a String value, or null
+ */
+ public String getString(String key) {
+ unparcel();
+ Object o = mMap.get(key);
+ if (o == null) {
+ return null;
+ }
+ try {
+ return (String) o;
+ } catch (ClassCastException e) {
+ typeWarning(key, o, "String", e);
+ return null;
+ }
+ }
+
+ /**
+ * Returns the value associated with the given key, or null if
+ * no mapping of the desired type exists for the given key or a null
+ * value is explicitly associated with the key.
+ *
+ * @param key a String, or null
+ * @return a CharSequence value, or null
+ */
+ public CharSequence getCharSequence(String key) {
+ unparcel();
+ Object o = mMap.get(key);
+ if (o == null) {
+ return null;
+ }
+ try {
+ return (CharSequence) o;
+ } catch (ClassCastException e) {
+ typeWarning(key, o, "CharSequence", e);
+ return null;
+ }
+ }
+
+ /**
+ * Returns the value associated with the given key, or null if
+ * no mapping of the desired type exists for the given key or a null
+ * value is explicitly associated with the key.
+ *
+ * @param key a String, or null
+ * @return a Bundle value, or null
+ */
+ public Bundle getBundle(String key) {
+ unparcel();
+ Object o = mMap.get(key);
+ if (o == null) {
+ return null;
+ }
+ try {
+ return (Bundle) o;
+ } catch (ClassCastException e) {
+ typeWarning(key, o, "Bundle", e);
+ return null;
+ }
+ }
+
+ /**
+ * Returns the value associated with the given key, or null if
+ * no mapping of the desired type exists for the given key or a null
+ * value is explicitly associated with the key.
+ *
+ * @param key a String, or null
+ * @return a Parcelable value, or null
+ */
+ public <T extends Parcelable> T getParcelable(String key) {
+ unparcel();
+ Object o = mMap.get(key);
+ if (o == null) {
+ return null;
+ }
+ try {
+ return (T) o;
+ } catch (ClassCastException e) {
+ typeWarning(key, o, "Parcelable", e);
+ return null;
+ }
+ }
+
+ /**
+ * Returns the value associated with the given key, or null if
+ * no mapping of the desired type exists for the given key or a null
+ * value is explicitly associated with the key.
+ *
+ * @param key a String, or null
+ * @return a Parcelable[] value, or null
+ */
+ public Parcelable[] getParcelableArray(String key) {
+ unparcel();
+ Object o = mMap.get(key);
+ if (o == null) {
+ return null;
+ }
+ try {
+ return (Parcelable[]) o;
+ } catch (ClassCastException e) {
+ typeWarning(key, o, "Parcelable[]", e);
+ return null;
+ }
+ }
+
+ /**
+ * Returns the value associated with the given key, or null if
+ * no mapping of the desired type exists for the given key or a null
+ * value is explicitly associated with the key.
+ *
+ * @param key a String, or null
+ * @return an ArrayList<T> value, or null
+ */
+ public <T extends Parcelable> ArrayList<T> getParcelableArrayList(String key) {
+ unparcel();
+ Object o = mMap.get(key);
+ if (o == null) {
+ return null;
+ }
+ try {
+ return (ArrayList<T>) o;
+ } catch (ClassCastException e) {
+ typeWarning(key, o, "ArrayList", e);
+ return null;
+ }
+ }
+
+ /**
+ * Returns the value associated with the given key, or null if
+ * no mapping of the desired type exists for the given key or a null
+ * value is explicitly associated with the key.
+ *
+ * @param key a String, or null
+ *
+ * @return a SparseArray of T values, or null
+ */
+ public <T extends Parcelable> SparseArray<T> getSparseParcelableArray(String key) {
+ unparcel();
+ Object o = mMap.get(key);
+ if (o == null) {
+ return null;
+ }
+ try {
+ return (SparseArray<T>) o;
+ } catch (ClassCastException e) {
+ typeWarning(key, o, "SparseArray", e);
+ return null;
+ }
+ }
+
+ /**
+ * Returns the value associated with the given key, or null if
+ * no mapping of the desired type exists for the given key or a null
+ * value is explicitly associated with the key.
+ *
+ * @param key a String, or null
+ * @return a Serializable value, or null
+ */
+ public Serializable getSerializable(String key) {
+ unparcel();
+ Object o = mMap.get(key);
+ if (o == null) {
+ return null;
+ }
+ try {
+ return (Serializable) o;
+ } catch (ClassCastException e) {
+ typeWarning(key, o, "Serializable", e);
+ return null;
+ }
+ }
+
+ /**
+ * Returns the value associated with the given key, or null if
+ * no mapping of the desired type exists for the given key or a null
+ * value is explicitly associated with the key.
+ *
+ * @param key a String, or null
+ * @return an ArrayList<String> value, or null
+ */
+ public ArrayList<Integer> getIntegerArrayList(String key) {
+ unparcel();
+ Object o = mMap.get(key);
+ if (o == null) {
+ return null;
+ }
+ try {
+ return (ArrayList<Integer>) o;
+ } catch (ClassCastException e) {
+ typeWarning(key, o, "ArrayList<Integer>", e);
+ return null;
+ }
+ }
+
+ /**
+ * Returns the value associated with the given key, or null if
+ * no mapping of the desired type exists for the given key or a null
+ * value is explicitly associated with the key.
+ *
+ * @param key a String, or null
+ * @return an ArrayList<String> value, or null
+ */
+ public ArrayList<String> getStringArrayList(String key) {
+ unparcel();
+ Object o = mMap.get(key);
+ if (o == null) {
+ return null;
+ }
+ try {
+ return (ArrayList<String>) o;
+ } catch (ClassCastException e) {
+ typeWarning(key, o, "ArrayList<String>", e);
+ return null;
+ }
+ }
+
+ /**
+ * Returns the value associated with the given key, or null if
+ * no mapping of the desired type exists for the given key or a null
+ * value is explicitly associated with the key.
+ *
+ * @param key a String, or null
+ * @return a boolean[] value, or null
+ */
+ public boolean[] getBooleanArray(String key) {
+ unparcel();
+ Object o = mMap.get(key);
+ if (o == null) {
+ return null;
+ }
+ try {
+ return (boolean[]) o;
+ } catch (ClassCastException e) {
+ typeWarning(key, o, "byte[]", e);
+ return null;
+ }
+ }
+
+ /**
+ * Returns the value associated with the given key, or null if
+ * no mapping of the desired type exists for the given key or a null
+ * value is explicitly associated with the key.
+ *
+ * @param key a String, or null
+ * @return a byte[] value, or null
+ */
+ public byte[] getByteArray(String key) {
+ unparcel();
+ Object o = mMap.get(key);
+ if (o == null) {
+ return null;
+ }
+ try {
+ return (byte[]) o;
+ } catch (ClassCastException e) {
+ typeWarning(key, o, "byte[]", e);
+ return null;
+ }
+ }
+
+ /**
+ * Returns the value associated with the given key, or null if
+ * no mapping of the desired type exists for the given key or a null
+ * value is explicitly associated with the key.
+ *
+ * @param key a String, or null
+ * @return a short[] value, or null
+ */
+ public short[] getShortArray(String key) {
+ unparcel();
+ Object o = mMap.get(key);
+ if (o == null) {
+ return null;
+ }
+ try {
+ return (short[]) o;
+ } catch (ClassCastException e) {
+ typeWarning(key, o, "short[]", e);
+ return null;
+ }
+ }
+
+ /**
+ * Returns the value associated with the given key, or null if
+ * no mapping of the desired type exists for the given key or a null
+ * value is explicitly associated with the key.
+ *
+ * @param key a String, or null
+ * @return a char[] value, or null
+ */
+ public char[] getCharArray(String key) {
+ unparcel();
+ Object o = mMap.get(key);
+ if (o == null) {
+ return null;
+ }
+ try {
+ return (char[]) o;
+ } catch (ClassCastException e) {
+ typeWarning(key, o, "char[]", e);
+ return null;
+ }
+ }
+
+ /**
+ * Returns the value associated with the given key, or null if
+ * no mapping of the desired type exists for the given key or a null
+ * value is explicitly associated with the key.
+ *
+ * @param key a String, or null
+ * @return an int[] value, or null
+ */
+ public int[] getIntArray(String key) {
+ unparcel();
+ Object o = mMap.get(key);
+ if (o == null) {
+ return null;
+ }
+ try {
+ return (int[]) o;
+ } catch (ClassCastException e) {
+ typeWarning(key, o, "int[]", e);
+ return null;
+ }
+ }
+
+ /**
+ * Returns the value associated with the given key, or null if
+ * no mapping of the desired type exists for the given key or a null
+ * value is explicitly associated with the key.
+ *
+ * @param key a String, or null
+ * @return a long[] value, or null
+ */
+ public long[] getLongArray(String key) {
+ unparcel();
+ Object o = mMap.get(key);
+ if (o == null) {
+ return null;
+ }
+ try {
+ return (long[]) o;
+ } catch (ClassCastException e) {
+ typeWarning(key, o, "long[]", e);
+ return null;
+ }
+ }
+
+ /**
+ * Returns the value associated with the given key, or null if
+ * no mapping of the desired type exists for the given key or a null
+ * value is explicitly associated with the key.
+ *
+ * @param key a String, or null
+ * @return a float[] value, or null
+ */
+ public float[] getFloatArray(String key) {
+ unparcel();
+ Object o = mMap.get(key);
+ if (o == null) {
+ return null;
+ }
+ try {
+ return (float[]) o;
+ } catch (ClassCastException e) {
+ typeWarning(key, o, "float[]", e);
+ return null;
+ }
+ }
+
+ /**
+ * Returns the value associated with the given key, or null if
+ * no mapping of the desired type exists for the given key or a null
+ * value is explicitly associated with the key.
+ *
+ * @param key a String, or null
+ * @return a double[] value, or null
+ */
+ public double[] getDoubleArray(String key) {
+ unparcel();
+ Object o = mMap.get(key);
+ if (o == null) {
+ return null;
+ }
+ try {
+ return (double[]) o;
+ } catch (ClassCastException e) {
+ typeWarning(key, o, "double[]", e);
+ return null;
+ }
+ }
+
+ /**
+ * Returns the value associated with the given key, or null if
+ * no mapping of the desired type exists for the given key or a null
+ * value is explicitly associated with the key.
+ *
+ * @param key a String, or null
+ * @return a String[] value, or null
+ */
+ public String[] getStringArray(String key) {
+ unparcel();
+ Object o = mMap.get(key);
+ if (o == null) {
+ return null;
+ }
+ try {
+ return (String[]) o;
+ } catch (ClassCastException e) {
+ typeWarning(key, o, "String[]", e);
+ return null;
+ }
+ }
+
+ /**
+ * Returns the value associated with the given key, or null if
+ * no mapping of the desired type exists for the given key or a null
+ * value is explicitly associated with the key.
+ *
+ * @param key a String, or null
+ * @return an IBinder value, or null
+ *
+ * @deprecated
+ * @hide
+ */
+ @Deprecated
+ public IBinder getIBinder(String key) {
+ unparcel();
+ Object o = mMap.get(key);
+ if (o == null) {
+ return null;
+ }
+ try {
+ return (IBinder) o;
+ } catch (ClassCastException e) {
+ typeWarning(key, o, "IBinder", e);
+ return null;
+ }
+ }
+
+ public static final Parcelable.Creator<Bundle> CREATOR =
+ new Parcelable.Creator<Bundle>() {
+ public Bundle createFromParcel(Parcel in) {
+ return in.readBundle();
+ }
+
+ public Bundle[] newArray(int size) {
+ return new Bundle[size];
+ }
+ };
+
+ /**
+ * Report the nature of this Parcelable's contents
+ */
+ public int describeContents() {
+ int mask = 0;
+ if (hasFileDescriptors()) {
+ mask |= Parcelable.CONTENTS_FILE_DESCRIPTOR;
+ }
+ return mask;
+ }
+
+ /**
+ * Writes the Bundle contents to a Parcel, typically in order for
+ * it to be passed through an IBinder connection.
+ * @param parcel The parcel to copy this bundle to.
+ */
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeBundle(this);
+ }
+
+ /**
+ * Reads the Parcel contents into this Bundle, typically in order for
+ * it to be passed through an IBinder connection.
+ * @param parcel The parcel to overwrite this bundle from.
+ */
+ public void readFromParcel(Parcel parcel) {
+ mParcelledData = parcel;
+ mHasFds = mParcelledData.hasFileDescriptors();
+ mFdsKnown = true;
+ }
+
+ @Override
+ public synchronized String toString() {
+ if (mParcelledData != null) {
+ return "Bundle[mParcelledData.dataSize=" +
+ mParcelledData.dataSize() + "]";
+ }
+ return "Bundle[" + mMap.toString() + "]";
+ }
+}
diff --git a/core/java/android/os/ConditionVariable.java b/core/java/android/os/ConditionVariable.java
new file mode 100644
index 0000000..95a9259
--- /dev/null
+++ b/core/java/android/os/ConditionVariable.java
@@ -0,0 +1,141 @@
+/*
+ * 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.os;
+
+/**
+ * Class that implements the condition variable locking paradigm.
+ *
+ * <p>
+ * This differs from the built-in java.lang.Object wait() and notify()
+ * in that this class contains the condition to wait on itself. That means
+ * open(), close() and block() are sticky. If open() is called before block(),
+ * block() will not block, and instead return immediately.
+ *
+ * <p>
+ * This class uses itself is at the object to wait on, so if you wait()
+ * or notify() on a ConditionVariable, the results are undefined.
+ */
+public class ConditionVariable
+{
+ private volatile boolean mCondition;
+
+ /**
+ * Create the ConditionVariable in the default closed state.
+ */
+ public ConditionVariable()
+ {
+ mCondition = false;
+ }
+
+ /**
+ * Create the ConditionVariable with the given state.
+ *
+ * <p>
+ * Pass true for opened and false for closed.
+ */
+ public ConditionVariable(boolean state)
+ {
+ mCondition = state;
+ }
+
+ /**
+ * Open the condition, and release all threads that are blocked.
+ *
+ * <p>
+ * Any threads that later approach block() will not block unless close()
+ * is called.
+ */
+ public void open()
+ {
+ synchronized (this) {
+ boolean old = mCondition;
+ mCondition = true;
+ if (!old) {
+ this.notifyAll();
+ }
+ }
+ }
+
+ /**
+ * Reset the condition to the closed state.
+ *
+ * <p>
+ * Any threads that call block() will block until someone calls open.
+ */
+ public void close()
+ {
+ synchronized (this) {
+ mCondition = false;
+ }
+ }
+
+ /**
+ * Block the current thread until the condition is opened.
+ *
+ * <p>
+ * If the condition is already opened, return immediately.
+ */
+ public void block()
+ {
+ synchronized (this) {
+ while (!mCondition) {
+ try {
+ this.wait();
+ }
+ catch (InterruptedException e) {
+ }
+ }
+ }
+ }
+
+ /**
+ * Block the current thread until the condition is opened or until
+ * timeout milliseconds have passed.
+ *
+ * <p>
+ * If the condition is already opened, return immediately.
+ *
+ * @param timeout the minimum time to wait in milliseconds.
+ *
+ * @return true if the condition was opened, false if the call returns
+ * because of the timeout.
+ */
+ public boolean block(long timeout)
+ {
+ // Object.wait(0) means wait forever, to mimic this, we just
+ // call the other block() method in that case. It simplifies
+ // this code for the common case.
+ if (timeout != 0) {
+ synchronized (this) {
+ long now = System.currentTimeMillis();
+ long end = now + timeout;
+ while (!mCondition && now < end) {
+ try {
+ this.wait(end-now);
+ }
+ catch (InterruptedException e) {
+ }
+ now = System.currentTimeMillis();
+ }
+ return mCondition;
+ }
+ } else {
+ this.block();
+ return true;
+ }
+ }
+}
diff --git a/core/java/android/os/CountDownTimer.java b/core/java/android/os/CountDownTimer.java
new file mode 100644
index 0000000..0c5c615
--- /dev/null
+++ b/core/java/android/os/CountDownTimer.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os;
+
+import android.util.Log;
+
+/**
+ * Schedule a countdown until a time in the future, with
+ * regular notifications on intervals along the way.
+ *
+ * Example of showing a 30 second countdown in a text field:
+ *
+ * <pre class="prettyprint">
+ * new CountdownTimer(30000, 1000) {
+ *
+ * public void onTick(long millisUntilFinished) {
+ * mTextField.setText("seconds remaining: " + millisUntilFinished / 1000);
+ * }
+ *
+ * public void onFinish() {
+ * mTextField.setText("done!");
+ * }
+ * }.start();
+ * </pre>
+ *
+ * The calls to {@link #onTick(long)} are synchronized to this object so that
+ * one call to {@link #onTick(long)} won't ever occur before the previous
+ * callback is complete. This is only relevant when the implementation of
+ * {@link #onTick(long)} takes an amount of time to execute that is significant
+ * compared to the countdown interval.
+ */
+public abstract class CountDownTimer {
+
+ /**
+ * Millis since epoch when alarm should stop.
+ */
+ private final long mMillisInFuture;
+
+ /**
+ * The interval in millis that the user receives callbacks
+ */
+ private final long mCountdownInterval;
+
+ private long mStopTimeInFuture;
+
+ /**
+ * @param millisInFuture The number of millis in the future from the call
+ * to {@link #start()} until the countdown is done and {@link #onFinish()}
+ * is called.
+ * @param countDownInterval The interval along the way to receive
+ * {@link #onTick(long)} callbacks.
+ */
+ public CountDownTimer(long millisInFuture, long countDownInterval) {
+ mMillisInFuture = millisInFuture;
+ mCountdownInterval = countDownInterval;
+ }
+
+ /**
+ * Cancel the countdown.
+ */
+ public final void cancel() {
+ mHandler.removeMessages(MSG);
+ }
+
+ /**
+ * Start the countdown.
+ */
+ public synchronized final CountDownTimer start() {
+ if (mMillisInFuture <= 0) {
+ onFinish();
+ return this;
+ }
+ mStopTimeInFuture = SystemClock.elapsedRealtime() + mMillisInFuture;
+ mHandler.sendMessage(mHandler.obtainMessage(MSG));
+ return this;
+ }
+
+
+ /**
+ * Callback fired on regular interval.
+ * @param millisUntilFinished The amount of time until finished.
+ */
+ public abstract void onTick(long millisUntilFinished);
+
+ /**
+ * Callback fired when the time is up.
+ */
+ public abstract void onFinish();
+
+
+ private static final int MSG = 1;
+
+
+ // handles counting down
+ private Handler mHandler = new Handler() {
+
+ @Override
+ public void handleMessage(Message msg) {
+
+ synchronized (CountDownTimer.this) {
+ final long millisLeft = mStopTimeInFuture - SystemClock.elapsedRealtime();
+
+ if (millisLeft <= 0) {
+ onFinish();
+ } else if (millisLeft < mCountdownInterval) {
+ // no tick, just delay until done
+ sendMessageDelayed(obtainMessage(MSG), millisLeft);
+ } else {
+ long lastTickStart = SystemClock.elapsedRealtime();
+ onTick(millisLeft);
+
+ // take into account user's onTick taking time to execute
+ long delay = lastTickStart + mCountdownInterval - SystemClock.elapsedRealtime();
+
+ // special case: user's onTick took more than interval to
+ // complete, skip to next interval
+ while (delay < 0) delay += mCountdownInterval;
+
+ sendMessageDelayed(obtainMessage(MSG), delay);
+ }
+ }
+ }
+ };
+}
diff --git a/core/java/android/os/DeadObjectException.java b/core/java/android/os/DeadObjectException.java
new file mode 100644
index 0000000..94c5387
--- /dev/null
+++ b/core/java/android/os/DeadObjectException.java
@@ -0,0 +1,28 @@
+/*
+ * 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.os;
+import android.os.RemoteException;
+
+/**
+ * The object you are calling has died, because its hosting process
+ * no longer exists.
+ */
+public class DeadObjectException extends RemoteException {
+ public DeadObjectException() {
+ super();
+ }
+}
diff --git a/core/java/android/os/Debug.java b/core/java/android/os/Debug.java
new file mode 100644
index 0000000..b9b2773
--- /dev/null
+++ b/core/java/android/os/Debug.java
@@ -0,0 +1,699 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os;
+
+import java.io.FileOutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+
+import org.apache.harmony.dalvik.ddmc.Chunk;
+import org.apache.harmony.dalvik.ddmc.ChunkHandler;
+import org.apache.harmony.dalvik.ddmc.DdmServer;
+
+import dalvik.bytecode.Opcodes;
+import dalvik.system.VMDebug;
+
+
+/** Provides various debugging functions for Android applications, including
+ * tracing and allocation counts.
+ * <p><strong>Logging Trace Files</strong></p>
+ * <p>Debug can create log files that give details about an application, such as
+ * a call stack and start/stop times for any running methods. See <a
+href="{@docRoot}reference/traceview.html">Running the Traceview Debugging Program</a> for
+ * information about reading trace files. To start logging trace files, call one
+ * of the startMethodTracing() methods. To stop tracing, call
+ * {@link #stopMethodTracing()}.
+ */
+public final class Debug
+{
+ /**
+ * Flags for startMethodTracing(). These can be ORed together.
+ *
+ * TRACE_COUNT_ALLOCS adds the results from startAllocCounting to the
+ * trace key file.
+ */
+ public static final int TRACE_COUNT_ALLOCS = VMDebug.TRACE_COUNT_ALLOCS;
+
+ /**
+ * Flags for printLoadedClasses(). Default behavior is to only show
+ * the class name.
+ */
+ public static final int SHOW_FULL_DETAIL = 1;
+ public static final int SHOW_CLASSLOADER = (1 << 1);
+ public static final int SHOW_INITIALIZED = (1 << 2);
+
+ // set/cleared by waitForDebugger()
+ private static volatile boolean mWaiting = false;
+
+ private Debug() {}
+
+ /*
+ * How long to wait for the debugger to finish sending requests. I've
+ * seen this hit 800msec on the device while waiting for a response
+ * to travel over USB and get processed, so we take that and add
+ * half a second.
+ */
+ private static final int MIN_DEBUGGER_IDLE = 1300; // msec
+
+ /* how long to sleep when polling for activity */
+ private static final int SPIN_DELAY = 200; // msec
+
+ /**
+ * Default trace file path and file
+ */
+ private static final String DEFAULT_TRACE_PATH_PREFIX = "/sdcard/";
+ private static final String DEFAULT_TRACE_BODY = "dmtrace";
+ private static final String DEFAULT_TRACE_EXTENSION = ".trace";
+ private static final String DEFAULT_TRACE_FILE_PATH =
+ DEFAULT_TRACE_PATH_PREFIX + DEFAULT_TRACE_BODY
+ + DEFAULT_TRACE_EXTENSION;
+
+
+ /**
+ * This class is used to retrieved various statistics about the memory mappings for this
+ * process. The returns info broken down by dalvik, native, and other. All results are in kB.
+ */
+ public static class MemoryInfo {
+ /** The proportional set size for dalvik. */
+ public int dalvikPss;
+ /** The private dirty pages used by dalvik. */
+ public int dalvikPrivateDirty;
+ /** The shared dirty pages used by dalvik. */
+ public int dalvikSharedDirty;
+
+ /** The proportional set size for the native heap. */
+ public int nativePss;
+ /** The private dirty pages used by the native heap. */
+ public int nativePrivateDirty;
+ /** The shared dirty pages used by the native heap. */
+ public int nativeSharedDirty;
+
+ /** The proportional set size for everything else. */
+ public int otherPss;
+ /** The private dirty pages used by everything else. */
+ public int otherPrivateDirty;
+ /** The shared dirty pages used by everything else. */
+ public int otherSharedDirty;
+ }
+
+
+ /**
+ * Wait until a debugger attaches. As soon as the debugger attaches,
+ * this returns, so you will need to place a breakpoint after the
+ * waitForDebugger() call if you want to start tracing immediately.
+ */
+ public static void waitForDebugger() {
+ if (isDebuggerConnected())
+ return;
+
+ // if DDMS is listening, inform them of our plight
+ System.out.println("Sending WAIT chunk");
+ byte[] data = new byte[] { 0 }; // 0 == "waiting for debugger"
+ Chunk waitChunk = new Chunk(ChunkHandler.type("WAIT"), data, 0, 1);
+ DdmServer.sendChunk(waitChunk);
+
+ mWaiting = true;
+ while (!isDebuggerConnected()) {
+ try { Thread.sleep(SPIN_DELAY); }
+ catch (InterruptedException ie) {}
+ }
+ mWaiting = false;
+
+ System.out.println("Debugger has connected");
+
+ /*
+ * There is no "ready to go" signal from the debugger, and we're
+ * not allowed to suspend ourselves -- the debugger expects us to
+ * be running happily, and gets confused if we aren't. We need to
+ * allow the debugger a chance to set breakpoints before we start
+ * running again.
+ *
+ * Sit and spin until the debugger has been idle for a short while.
+ */
+ while (true) {
+ long delta = VMDebug.lastDebuggerActivity();
+ if (delta < 0) {
+ System.out.println("debugger detached?");
+ break;
+ }
+
+ if (delta < MIN_DEBUGGER_IDLE) {
+ System.out.println("waiting for debugger to settle...");
+ try { Thread.sleep(SPIN_DELAY); }
+ catch (InterruptedException ie) {}
+ } else {
+ System.out.println("debugger has settled (" + delta + ")");
+ break;
+ }
+ }
+ }
+
+ /**
+ * Returns "true" if one or more threads is waiting for a debugger
+ * to attach.
+ */
+ public static boolean waitingForDebugger() {
+ return mWaiting;
+ }
+
+ /**
+ * Determine if a debugger is currently attached.
+ */
+ public static boolean isDebuggerConnected() {
+ return VMDebug.isDebuggerConnected();
+ }
+
+ /**
+ * Change the JDWP port -- this is a temporary measure.
+ *
+ * If a debugger is currently attached the change may not happen
+ * until after the debugger disconnects.
+ */
+ public static void changeDebugPort(int port) {}
+
+ /**
+ * This is the pathname to the sysfs file that enables and disables
+ * tracing on the qemu emulator.
+ */
+ private static final String SYSFS_QEMU_TRACE_STATE = "/sys/qemu_trace/state";
+
+ /**
+ * Enable qemu tracing. For this to work requires running everything inside
+ * the qemu emulator; otherwise, this method will have no effect. The trace
+ * file is specified on the command line when the emulator is started. For
+ * example, the following command line <br />
+ * <code>emulator -trace foo</code><br />
+ * will start running the emulator and create a trace file named "foo". This
+ * method simply enables writing the trace records to the trace file.
+ *
+ * <p>
+ * The main differences between this and {@link #startMethodTracing()} are
+ * that tracing in the qemu emulator traces every cpu instruction of every
+ * process, including kernel code, so we have more complete information,
+ * including all context switches. We can also get more detailed information
+ * such as cache misses. The sequence of calls is determined by
+ * post-processing the instruction trace. The qemu tracing is also done
+ * without modifying the application or perturbing the timing of calls
+ * because no instrumentation is added to the application being traced.
+ * </p>
+ *
+ * <p>
+ * One limitation of using this method compared to using
+ * {@link #startMethodTracing()} on the real device is that the emulator
+ * does not model all of the real hardware effects such as memory and
+ * bus contention. The emulator also has a simple cache model and cannot
+ * capture all the complexities of a real cache.
+ * </p>
+ */
+ public static void startNativeTracing() {
+ // Open the sysfs file for writing and write "1" to it.
+ PrintWriter outStream = null;
+ try {
+ FileOutputStream fos = new FileOutputStream(SYSFS_QEMU_TRACE_STATE);
+ outStream = new PrintWriter(new OutputStreamWriter(fos));
+ outStream.println("1");
+ } catch (Exception e) {
+ } finally {
+ if (outStream != null)
+ outStream.close();
+ }
+
+ VMDebug.startEmulatorTracing();
+ }
+
+ /**
+ * Stop qemu tracing. See {@link #startNativeTracing()} to start tracing.
+ *
+ * <p>Tracing can be started and stopped as many times as desired. When
+ * the qemu emulator itself is stopped then the buffered trace records
+ * are flushed and written to the trace file. In fact, it is not necessary
+ * to call this method at all; simply killing qemu is sufficient. But
+ * starting and stopping a trace is useful for examining a specific
+ * region of code.</p>
+ */
+ public static void stopNativeTracing() {
+ VMDebug.stopEmulatorTracing();
+
+ // Open the sysfs file for writing and write "0" to it.
+ PrintWriter outStream = null;
+ try {
+ FileOutputStream fos = new FileOutputStream(SYSFS_QEMU_TRACE_STATE);
+ outStream = new PrintWriter(new OutputStreamWriter(fos));
+ outStream.println("0");
+ } catch (Exception e) {
+ // We could print an error message here but we probably want
+ // to quietly ignore errors if we are not running in the emulator.
+ } finally {
+ if (outStream != null)
+ outStream.close();
+ }
+ }
+
+ /**
+ * Enable "emulator traces", in which information about the current
+ * method is made available to the "emulator -trace" feature. There
+ * is no corresponding "disable" call -- this is intended for use by
+ * the framework when tracing should be turned on and left that way, so
+ * that traces captured with F9/F10 will include the necessary data.
+ *
+ * This puts the VM into "profile" mode, which has performance
+ * consequences.
+ *
+ * To temporarily enable tracing, use {@link #startNativeTracing()}.
+ */
+ public static void enableEmulatorTraceOutput() {
+ VMDebug.startEmulatorTracing();
+ }
+
+ /**
+ * Start method tracing with default log name and buffer size. See <a
+href="{@docRoot}reference/traceview.html">Running the Traceview Debugging Program</a> for
+ * information about reading these files. Call stopMethodTracing() to stop
+ * tracing.
+ */
+ public static void startMethodTracing() {
+ VMDebug.startMethodTracing(DEFAULT_TRACE_FILE_PATH, 0, 0);
+ }
+
+ /**
+ * Start method tracing, specifying the trace log file name. The trace
+ * file will be put under "/sdcard" unless an absolute path is given.
+ * See <a
+ href="{@docRoot}reference/traceview.html">Running the Traceview Debugging Program</a> for
+ * information about reading trace files.
+ *
+ * @param traceName Name for the trace log file to create.
+ * If no name argument is given, this value defaults to "/sdcard/dmtrace.trace".
+ * If the files already exist, they will be truncated.
+ * If the trace file given does not end in ".trace", it will be appended for you.
+ */
+ public static void startMethodTracing(String traceName) {
+ startMethodTracing(traceName, 0, 0);
+ }
+
+ /**
+ * Start method tracing, specifying the trace log file name and the
+ * buffer size. The trace files will be put under "/sdcard" unless an
+ * absolute path is given. See <a
+ href="{@docRoot}reference/traceview.html">Running the Traceview Debugging Program</a> for
+ * information about reading trace files.
+ * @param traceName Name for the trace log file to create.
+ * If no name argument is given, this value defaults to "/sdcard/dmtrace.trace".
+ * If the files already exist, they will be truncated.
+ * If the trace file given does not end in ".trace", it will be appended for you.
+ *
+ * @param bufferSize The maximum amount of trace data we gather. If not given, it defaults to 8MB.
+ */
+ public static void startMethodTracing(String traceName, int bufferSize) {
+ startMethodTracing(traceName, bufferSize, 0);
+ }
+
+ /**
+ * Start method tracing, specifying the trace log file name and the
+ * buffer size. The trace files will be put under "/sdcard" unless an
+ * absolute path is given. See <a
+ href="{@docRoot}reference/traceview.html">Running the Traceview Debugging Program</a> for
+ * information about reading trace files.
+ *
+ * <p>
+ * When method tracing is enabled, the VM will run more slowly than
+ * usual, so the timings from the trace files should only be considered
+ * in relative terms (e.g. was run #1 faster than run #2). The times
+ * for native methods will not change, so don't try to use this to
+ * compare the performance of interpreted and native implementations of the
+ * same method. As an alternative, consider using "native" tracing
+ * in the emulator via {@link #startNativeTracing()}.
+ * </p>
+ *
+ * @param traceName Name for the trace log file to create.
+ * If no name argument is given, this value defaults to "/sdcard/dmtrace.trace".
+ * If the files already exist, they will be truncated.
+ * If the trace file given does not end in ".trace", it will be appended for you.
+ * @param bufferSize The maximum amount of trace data we gather. If not given, it defaults to 8MB.
+ */
+ public static void startMethodTracing(String traceName, int bufferSize,
+ int flags) {
+
+ String pathName = traceName;
+ if (pathName.charAt(0) != '/')
+ pathName = DEFAULT_TRACE_PATH_PREFIX + pathName;
+ if (!pathName.endsWith(DEFAULT_TRACE_EXTENSION))
+ pathName = pathName + DEFAULT_TRACE_EXTENSION;
+
+ VMDebug.startMethodTracing(pathName, bufferSize, flags);
+ }
+
+ /**
+ * Stop method tracing.
+ */
+ public static void stopMethodTracing() {
+ VMDebug.stopMethodTracing();
+ }
+
+ /**
+ * Get an indication of thread CPU usage. The value returned
+ * indicates the amount of time that the current thread has spent
+ * executing code or waiting for certain types of I/O.
+ *
+ * The time is expressed in nanoseconds, and is only meaningful
+ * when compared to the result from an earlier call. Note that
+ * nanosecond resolution does not imply nanosecond accuracy.
+ *
+ * On system which don't support this operation, the call returns -1.
+ */
+ public static long threadCpuTimeNanos() {
+ return VMDebug.threadCpuTimeNanos();
+ }
+
+ /**
+ * Count the number and aggregate size of memory allocations between
+ * two points.
+ *
+ * The "start" function resets the counts and enables counting. The
+ * "stop" function disables the counting so that the analysis code
+ * doesn't cause additional allocations. The "get" function returns
+ * the specified value.
+ *
+ * Counts are kept for the system as a whole and for each thread.
+ * The per-thread counts for threads other than the current thread
+ * are not cleared by the "reset" or "start" calls.
+ */
+ public static void startAllocCounting() {
+ VMDebug.startAllocCounting();
+ }
+ public static void stopAllocCounting() {
+ VMDebug.stopAllocCounting();
+ }
+
+ public static int getGlobalAllocCount() {
+ return VMDebug.getAllocCount(VMDebug.KIND_GLOBAL_ALLOCATED_OBJECTS);
+ }
+ public static int getGlobalAllocSize() {
+ return VMDebug.getAllocCount(VMDebug.KIND_GLOBAL_ALLOCATED_BYTES);
+ }
+ public static int getGlobalFreedCount() {
+ return VMDebug.getAllocCount(VMDebug.KIND_GLOBAL_FREED_OBJECTS);
+ }
+ public static int getGlobalFreedSize() {
+ return VMDebug.getAllocCount(VMDebug.KIND_GLOBAL_FREED_BYTES);
+ }
+ public static int getGlobalExternalAllocCount() {
+ return VMDebug.getAllocCount(VMDebug.KIND_GLOBAL_EXT_ALLOCATED_OBJECTS);
+ }
+ public static int getGlobalExternalAllocSize() {
+ return VMDebug.getAllocCount(VMDebug.KIND_GLOBAL_EXT_ALLOCATED_BYTES);
+ }
+ public static int getGlobalExternalFreedCount() {
+ return VMDebug.getAllocCount(VMDebug.KIND_GLOBAL_EXT_FREED_OBJECTS);
+ }
+ public static int getGlobalExternalFreedSize() {
+ return VMDebug.getAllocCount(VMDebug.KIND_GLOBAL_EXT_FREED_BYTES);
+ }
+ public static int getGlobalGcInvocationCount() {
+ return VMDebug.getAllocCount(VMDebug.KIND_GLOBAL_GC_INVOCATIONS);
+ }
+ public static int getThreadAllocCount() {
+ return VMDebug.getAllocCount(VMDebug.KIND_THREAD_ALLOCATED_OBJECTS);
+ }
+ public static int getThreadAllocSize() {
+ return VMDebug.getAllocCount(VMDebug.KIND_THREAD_ALLOCATED_BYTES);
+ }
+ public static int getThreadExternalAllocCount() {
+ return VMDebug.getAllocCount(VMDebug.KIND_THREAD_EXT_ALLOCATED_OBJECTS);
+ }
+ public static int getThreadExternalAllocSize() {
+ return VMDebug.getAllocCount(VMDebug.KIND_THREAD_EXT_ALLOCATED_BYTES);
+ }
+ public static int getThreadGcInvocationCount() {
+ return VMDebug.getAllocCount(VMDebug.KIND_THREAD_GC_INVOCATIONS);
+ }
+
+ public static void resetGlobalAllocCount() {
+ VMDebug.resetAllocCount(VMDebug.KIND_GLOBAL_ALLOCATED_OBJECTS);
+ }
+ public static void resetGlobalAllocSize() {
+ VMDebug.resetAllocCount(VMDebug.KIND_GLOBAL_ALLOCATED_BYTES);
+ }
+ public static void resetGlobalFreedCount() {
+ VMDebug.resetAllocCount(VMDebug.KIND_GLOBAL_FREED_OBJECTS);
+ }
+ public static void resetGlobalFreedSize() {
+ VMDebug.resetAllocCount(VMDebug.KIND_GLOBAL_FREED_BYTES);
+ }
+ public static void resetGlobalExternalAllocCount() {
+ VMDebug.resetAllocCount(VMDebug.KIND_GLOBAL_EXT_ALLOCATED_OBJECTS);
+ }
+ public static void resetGlobalExternalAllocSize() {
+ VMDebug.resetAllocCount(VMDebug.KIND_GLOBAL_EXT_ALLOCATED_BYTES);
+ }
+ public static void resetGlobalExternalFreedCount() {
+ VMDebug.resetAllocCount(VMDebug.KIND_GLOBAL_EXT_FREED_OBJECTS);
+ }
+ public static void resetGlobalExternalFreedSize() {
+ VMDebug.resetAllocCount(VMDebug.KIND_GLOBAL_EXT_FREED_BYTES);
+ }
+ public static void resetGlobalGcInvocationCount() {
+ VMDebug.resetAllocCount(VMDebug.KIND_GLOBAL_GC_INVOCATIONS);
+ }
+ public static void resetThreadAllocCount() {
+ VMDebug.resetAllocCount(VMDebug.KIND_THREAD_ALLOCATED_OBJECTS);
+ }
+ public static void resetThreadAllocSize() {
+ VMDebug.resetAllocCount(VMDebug.KIND_THREAD_ALLOCATED_BYTES);
+ }
+ public static void resetThreadExternalAllocCount() {
+ VMDebug.resetAllocCount(VMDebug.KIND_THREAD_EXT_ALLOCATED_OBJECTS);
+ }
+ public static void resetThreadExternalAllocSize() {
+ VMDebug.resetAllocCount(VMDebug.KIND_THREAD_EXT_ALLOCATED_BYTES);
+ }
+ public static void resetThreadGcInvocationCount() {
+ VMDebug.resetAllocCount(VMDebug.KIND_THREAD_GC_INVOCATIONS);
+ }
+ public static void resetAllCounts() {
+ VMDebug.resetAllocCount(VMDebug.KIND_ALL_COUNTS);
+ }
+
+ /**
+ * Returns the size of the native heap.
+ * @return The size of the native heap in bytes.
+ */
+ public static native long getNativeHeapSize();
+
+ /**
+ * Returns the amount of allocated memory in the native heap.
+ * @return The allocated size in bytes.
+ */
+ public static native long getNativeHeapAllocatedSize();
+
+ /**
+ * Returns the amount of free memory in the native heap.
+ * @return The freed size in bytes.
+ */
+ public static native long getNativeHeapFreeSize();
+
+ /**
+ * Retrieves information about this processes memory usages. This information is broken down by
+ * how much is in use by dalivk, the native heap, and everything else.
+ */
+ public static native void getMemoryInfo(MemoryInfo memoryInfo);
+
+ /**
+ * Establish an object allocation limit in the current thread. Useful
+ * for catching regressions in code that is expected to operate
+ * without causing any allocations.
+ *
+ * Pass in the maximum number of allowed allocations. Use -1 to disable
+ * the limit. Returns the previous limit.
+ *
+ * The preferred way to use this is:
+ *
+ * int prevLimit = -1;
+ * try {
+ * prevLimit = Debug.setAllocationLimit(0);
+ * ... do stuff that's not expected to allocate memory ...
+ * } finally {
+ * Debug.setAllocationLimit(prevLimit);
+ * }
+ *
+ * This allows limits to be nested. The try/finally ensures that the
+ * limit is reset if something fails.
+ *
+ * Exceeding the limit causes a dalvik.system.AllocationLimitError to
+ * be thrown from a memory allocation call. The limit is reset to -1
+ * when this happens.
+ *
+ * The feature may be disabled in the VM configuration. If so, this
+ * call has no effect, and always returns -1.
+ */
+ public static int setAllocationLimit(int limit) {
+ return VMDebug.setAllocationLimit(limit);
+ }
+
+ /**
+ * Establish a global object allocation limit. This is similar to
+ * {@link #setAllocationLimit(int)} but applies to all threads in
+ * the VM. It will coexist peacefully with per-thread limits.
+ *
+ * [ The value of "limit" is currently restricted to 0 (no allocations
+ * allowed) or -1 (no global limit). This may be changed in a future
+ * release. ]
+ */
+ public static int setGlobalAllocationLimit(int limit) {
+ if (limit != 0 && limit != -1)
+ throw new IllegalArgumentException("limit must be 0 or -1");
+ return VMDebug.setGlobalAllocationLimit(limit);
+ }
+
+ /**
+ * Dump a list of all currently loaded class to the log file.
+ *
+ * @param flags See constants above.
+ */
+ public static void printLoadedClasses(int flags) {
+ VMDebug.printLoadedClasses(flags);
+ }
+
+ /**
+ * Get the number of loaded classes.
+ * @return the number of loaded classes.
+ */
+ public static int getLoadedClassCount() {
+ return VMDebug.getLoadedClassCount();
+ }
+
+ /**
+ * Returns the number of sent transactions from this process.
+ * @return The number of sent transactions or -1 if it could not read t.
+ */
+ public static native int getBinderSentTransactions();
+
+ /**
+ * Returns the number of received transactions from the binder driver.
+ * @return The number of received transactions or -1 if it could not read the stats.
+ */
+ public static native int getBinderReceivedTransactions();
+
+ /**
+ * Returns the number of active local Binder objects that exist in the
+ * current process.
+ */
+ public static final native int getBinderLocalObjectCount();
+
+ /**
+ * Returns the number of references to remote proxy Binder objects that
+ * exist in the current process.
+ */
+ public static final native int getBinderProxyObjectCount();
+
+ /**
+ * Returns the number of death notification links to Binder objects that
+ * exist in the current process.
+ */
+ public static final native int getBinderDeathObjectCount();
+
+ /**
+ * API for gathering and querying instruction counts.
+ *
+ * Example usage:
+ * Debug.InstructionCount icount = new Debug.InstructionCount();
+ * icount.resetAndStart();
+ * [... do lots of stuff ...]
+ * if (icount.collect()) {
+ * System.out.println("Total instructions executed: "
+ * + icount.globalTotal());
+ * System.out.println("Method invocations: "
+ * + icount.globalMethodInvocations());
+ * }
+ */
+ public static class InstructionCount {
+ private static final int NUM_INSTR = 256;
+
+ private int[] mCounts;
+
+ public InstructionCount() {
+ mCounts = new int[NUM_INSTR];
+ }
+
+ /**
+ * Reset counters and ensure counts are running. Counts may
+ * have already been running.
+ *
+ * @return true if counting was started
+ */
+ public boolean resetAndStart() {
+ try {
+ VMDebug.startInstructionCounting();
+ VMDebug.resetInstructionCount();
+ } catch (UnsupportedOperationException uoe) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Collect instruction counts. May or may not stop the
+ * counting process.
+ */
+ public boolean collect() {
+ try {
+ VMDebug.stopInstructionCounting();
+ VMDebug.getInstructionCount(mCounts);
+ } catch (UnsupportedOperationException uoe) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Return the total number of instructions executed globally (i.e. in
+ * all threads).
+ */
+ public int globalTotal() {
+ int count = 0;
+ for (int i = 0; i < NUM_INSTR; i++)
+ count += mCounts[i];
+ return count;
+ }
+
+ /**
+ * Return the total number of method-invocation instructions
+ * executed globally.
+ */
+ public int globalMethodInvocations() {
+ int count = 0;
+
+ //count += mCounts[Opcodes.OP_EXECUTE_INLINE];
+ count += mCounts[Opcodes.OP_INVOKE_VIRTUAL];
+ count += mCounts[Opcodes.OP_INVOKE_SUPER];
+ count += mCounts[Opcodes.OP_INVOKE_DIRECT];
+ count += mCounts[Opcodes.OP_INVOKE_STATIC];
+ count += mCounts[Opcodes.OP_INVOKE_INTERFACE];
+ count += mCounts[Opcodes.OP_INVOKE_VIRTUAL_RANGE];
+ count += mCounts[Opcodes.OP_INVOKE_SUPER_RANGE];
+ count += mCounts[Opcodes.OP_INVOKE_DIRECT_RANGE];
+ count += mCounts[Opcodes.OP_INVOKE_STATIC_RANGE];
+ count += mCounts[Opcodes.OP_INVOKE_INTERFACE_RANGE];
+ //count += mCounts[Opcodes.OP_INVOKE_DIRECT_EMPTY];
+ count += mCounts[Opcodes.OP_INVOKE_VIRTUAL_QUICK];
+ count += mCounts[Opcodes.OP_INVOKE_VIRTUAL_QUICK_RANGE];
+ count += mCounts[Opcodes.OP_INVOKE_SUPER_QUICK];
+ count += mCounts[Opcodes.OP_INVOKE_SUPER_QUICK_RANGE];
+ return count;
+ }
+ };
+}
diff --git a/core/java/android/os/Environment.java b/core/java/android/os/Environment.java
new file mode 100644
index 0000000..e37b551
--- /dev/null
+++ b/core/java/android/os/Environment.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os;
+
+import java.io.File;
+
+/**
+ * Provides access to environment variables.
+ */
+public class Environment {
+
+ private static final File ROOT_DIRECTORY
+ = getDirectory("ANDROID_ROOT", "/system");
+
+ /**
+ * Gets the Android root directory.
+ */
+ public static File getRootDirectory() {
+ return ROOT_DIRECTORY;
+ }
+
+ private static final File DATA_DIRECTORY
+ = getDirectory("ANDROID_DATA", "/data");
+
+ private static final File EXTERNAL_STORAGE_DIRECTORY
+ = getDirectory("EXTERNAL_STORAGE", "/sdcard");
+
+ private static final File DOWNLOAD_CACHE_DIRECTORY
+ = getDirectory("DOWNLOAD_CACHE", "/cache");
+
+ /**
+ * Gets the Android data directory.
+ */
+ public static File getDataDirectory() {
+ return DATA_DIRECTORY;
+ }
+
+ /**
+ * Gets the Android external storage directory.
+ */
+ public static File getExternalStorageDirectory() {
+ return EXTERNAL_STORAGE_DIRECTORY;
+ }
+
+ /**
+ * Gets the Android Download/Cache content directory.
+ */
+ public static File getDownloadCacheDirectory() {
+ return DOWNLOAD_CACHE_DIRECTORY;
+ }
+
+ /**
+ * getExternalStorageState() returns MEDIA_REMOVED if the media is not present.
+ */
+ public static final String MEDIA_REMOVED = "removed";
+
+ /**
+ * getExternalStorageState() returns MEDIA_UNMOUNTED if the media is present
+ * but not mounted.
+ */
+ public static final String MEDIA_UNMOUNTED = "unmounted";
+
+ /**
+ * getExternalStorageState() returns MEDIA_MOUNTED if the media is present
+ * and mounted at its mount point with read/write access.
+ */
+ public static final String MEDIA_MOUNTED = "mounted";
+
+ /**
+ * getExternalStorageState() returns MEDIA_MOUNTED_READ_ONLY if the media is present
+ * and mounted at its mount point with read only access.
+ */
+ public static final String MEDIA_MOUNTED_READ_ONLY = "mounted_ro";
+
+ /**
+ * getExternalStorageState() returns MEDIA_SHARED if the media is present
+ * not mounted, and shared via USB mass storage.
+ */
+ public static final String MEDIA_SHARED = "shared";
+
+ /**
+ * getExternalStorageState() returns MEDIA_BAD_REMOVAL if the media was
+ * removed before it was unmounted.
+ */
+ public static final String MEDIA_BAD_REMOVAL = "bad_removal";
+
+ /**
+ * getExternalStorageState() returns MEDIA_UNMOUNTABLE if the media is present
+ * but cannot be mounted. Typically this happens if the file system on the
+ * media is corrupted.
+ */
+ public static final String MEDIA_UNMOUNTABLE = "unmountable";
+
+ /**
+ * Gets the current state of the external storage device.
+ */
+ public static String getExternalStorageState() {
+ return SystemProperties.get("EXTERNAL_STORAGE_STATE", MEDIA_REMOVED);
+ }
+
+ static File getDirectory(String variableName, String defaultPath) {
+ String path = System.getenv(variableName);
+ return path == null ? new File(defaultPath) : new File(path);
+ }
+}
diff --git a/core/java/android/os/Exec.java b/core/java/android/os/Exec.java
new file mode 100644
index 0000000..a50d5fe
--- /dev/null
+++ b/core/java/android/os/Exec.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os;
+
+import java.io.FileDescriptor;
+
+/**
+ * @hide
+ * Tools for executing commands. Not for public consumption.
+ */
+
+public class Exec
+{
+ /**
+ * @param cmd The command to execute
+ * @param arg0 The first argument to the command, may be null
+ * @param arg1 the second argument to the command, may be null
+ * @return the file descriptor of the started process.
+ *
+ */
+ public static FileDescriptor createSubprocess(
+ String cmd, String arg0, String arg1) {
+ return createSubprocess(cmd, arg0, arg1, null);
+ }
+
+ /**
+ * @param cmd The command to execute
+ * @param arg0 The first argument to the command, may be null
+ * @param arg1 the second argument to the command, may be null
+ * @param processId A one-element array to which the process ID of the
+ * started process will be written.
+ * @return the file descriptor of the started process.
+ *
+ */
+ public static native FileDescriptor createSubprocess(
+ String cmd, String arg0, String arg1, int[] processId);
+
+ public static native void setPtyWindowSize(FileDescriptor fd,
+ int row, int col, int xpixel, int ypixel);
+ /**
+ * Causes the calling thread to wait for the process associated with the
+ * receiver to finish executing.
+ *
+ * @return The exit value of the Process being waited on
+ *
+ */
+ public static native int waitFor(int processId);
+}
+
diff --git a/core/java/android/os/FileObserver.java b/core/java/android/os/FileObserver.java
new file mode 100644
index 0000000..d9804ea
--- /dev/null
+++ b/core/java/android/os/FileObserver.java
@@ -0,0 +1,146 @@
+/*
+ * 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.os;
+
+import android.util.Log;
+
+import com.android.internal.os.RuntimeInit;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+public abstract class FileObserver {
+ public static final int ACCESS = 0x00000001; /* File was accessed */
+ public static final int MODIFY = 0x00000002; /* File was modified */
+ public static final int ATTRIB = 0x00000004; /* Metadata changed */
+ public static final int CLOSE_WRITE = 0x00000008; /* Writtable file was closed */
+ public static final int CLOSE_NOWRITE = 0x00000010; /* Unwrittable file closed */
+ public static final int OPEN = 0x00000020; /* File was opened */
+ public static final int MOVED_FROM = 0x00000040; /* File was moved from X */
+ public static final int MOVED_TO = 0x00000080; /* File was moved to Y */
+ public static final int CREATE = 0x00000100; /* Subfile was created */
+ public static final int DELETE = 0x00000200; /* Subfile was deleted */
+ public static final int DELETE_SELF = 0x00000400; /* Self was deleted */
+ public static final int MOVE_SELF = 0x00000800; /* Self was moved */
+ public static final int ALL_EVENTS = ACCESS | MODIFY | ATTRIB | CLOSE_WRITE
+ | CLOSE_NOWRITE | OPEN | MOVED_FROM | MOVED_TO | DELETE | CREATE
+ | DELETE_SELF | MOVE_SELF;
+
+ private static final String LOG_TAG = "FileObserver";
+
+ private static class ObserverThread extends Thread {
+ private HashMap<Integer, WeakReference> m_observers = new HashMap<Integer, WeakReference>();
+ private int m_fd;
+
+ public ObserverThread() {
+ super("FileObserver");
+ m_fd = init();
+ }
+
+ public void run() {
+ observe(m_fd);
+ }
+
+ public int startWatching(String path, int mask, FileObserver observer) {
+ int wfd = startWatching(m_fd, path, mask);
+
+ Integer i = new Integer(wfd);
+ if (wfd >= 0) {
+ synchronized (m_observers) {
+ m_observers.put(i, new WeakReference(observer));
+ }
+ }
+
+ return i;
+ }
+
+ public void stopWatching(int descriptor) {
+ stopWatching(m_fd, descriptor);
+ }
+
+ public void onEvent(int wfd, int mask, String path) {
+ // look up our observer, fixing up the map if necessary...
+ FileObserver observer;
+
+ synchronized (m_observers) {
+ WeakReference weak = m_observers.get(wfd);
+ observer = (FileObserver) weak.get();
+ if (observer == null) {
+ m_observers.remove(wfd);
+ }
+ }
+
+ // ...then call out to the observer without the sync lock held
+ if (observer != null) {
+ try {
+ observer.onEvent(mask, path);
+ } catch (Throwable throwable) {
+ Log.e(LOG_TAG, "Unhandled throwable " + throwable.toString() +
+ " (returned by observer " + observer + ")", throwable);
+ RuntimeInit.crash("FileObserver", throwable);
+ }
+ }
+ }
+
+ private native int init();
+ private native void observe(int fd);
+ private native int startWatching(int fd, String path, int mask);
+ private native void stopWatching(int fd, int wfd);
+ }
+
+ private static ObserverThread s_observerThread;
+
+ static {
+ s_observerThread = new ObserverThread();
+ s_observerThread.start();
+ }
+
+ // instance
+ private String m_path;
+ private Integer m_descriptor;
+ private int m_mask;
+
+ public FileObserver(String path) {
+ this(path, ALL_EVENTS);
+ }
+
+ public FileObserver(String path, int mask) {
+ m_path = path;
+ m_mask = mask;
+ m_descriptor = -1;
+ }
+
+ protected void finalize() {
+ stopWatching();
+ }
+
+ public void startWatching() {
+ if (m_descriptor < 0) {
+ m_descriptor = s_observerThread.startWatching(m_path, m_mask, this);
+ }
+ }
+
+ public void stopWatching() {
+ if (m_descriptor >= 0) {
+ s_observerThread.stopWatching(m_descriptor);
+ m_descriptor = -1;
+ }
+ }
+
+ public abstract void onEvent(int event, String path);
+}
diff --git a/core/java/android/os/FileUtils.java b/core/java/android/os/FileUtils.java
new file mode 100644
index 0000000..51dfb5b
--- /dev/null
+++ b/core/java/android/os/FileUtils.java
@@ -0,0 +1,197 @@
+/*
+ * 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.os;
+
+import java.io.ByteArrayOutputStream;
+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 java.util.regex.Pattern;
+
+
+/**
+ * Tools for managing files. Not for public consumption.
+ * @hide
+ */
+public class FileUtils
+{
+ public static final int S_IRWXU = 00700;
+ public static final int S_IRUSR = 00400;
+ public static final int S_IWUSR = 00200;
+ public static final int S_IXUSR = 00100;
+
+ public static final int S_IRWXG = 00070;
+ public static final int S_IRGRP = 00040;
+ public static final int S_IWGRP = 00020;
+ public static final int S_IXGRP = 00010;
+
+ public static final int S_IRWXO = 00007;
+ public static final int S_IROTH = 00004;
+ public static final int S_IWOTH = 00002;
+ public static final int S_IXOTH = 00001;
+
+
+ /**
+ * File status information. This class maps directly to the POSIX stat structure.
+ * @hide
+ */
+ public static final class FileStatus {
+ public int dev;
+ public int ino;
+ public int mode;
+ public int nlink;
+ public int uid;
+ public int gid;
+ public int rdev;
+ public long size;
+ public int blksize;
+ public long blocks;
+ public long atime;
+ public long mtime;
+ public long ctime;
+ }
+
+ /**
+ * Get the status for the given path. This is equivalent to the POSIX stat(2) system call.
+ * @param path The path of the file to be stat'd.
+ * @param status Optional argument to fill in. It will only fill in the status if the file
+ * exists.
+ * @return true if the file exists and false if it does not exist. If you do not have
+ * permission to stat the file, then this method will return false.
+ */
+ public static native boolean getFileStatus(String path, FileStatus status);
+
+ /** Regular expression for safe filenames: no spaces or metacharacters */
+ private static final Pattern SAFE_FILENAME_PATTERN = Pattern.compile("[\\w%+,./=_-]+");
+
+ public static native int setPermissions(String file, int mode, int uid, int gid);
+
+ public static native int getPermissions(String file, int[] outPermissions);
+
+ /** returns the FAT file system volume ID for the volume mounted
+ * at the given mount point, or -1 for failure
+ * @param mount point for FAT volume
+ * @return volume ID or -1
+ */
+ public static native int getFatVolumeId(String mountPoint);
+
+ // copy a file from srcFile to destFile, return true if succeed, return
+ // false if fail
+ public static boolean copyFile(File srcFile, File destFile) {
+ boolean result = false;
+ try {
+ InputStream in = new FileInputStream(srcFile);
+ try {
+ result = copyToFile(in, destFile);
+ } finally {
+ in.close();
+ }
+ } catch (IOException e) {
+ result = false;
+ }
+ return result;
+ }
+
+ /**
+ * Copy data from a source stream to destFile.
+ * Return true if succeed, return false if failed.
+ */
+ public static boolean copyToFile(InputStream inputStream, File destFile) {
+ try {
+ OutputStream out = new FileOutputStream(destFile);
+ try {
+ byte[] buffer = new byte[4096];
+ int bytesRead;
+ while ((bytesRead = inputStream.read(buffer)) >= 0) {
+ out.write(buffer, 0, bytesRead);
+ }
+ } finally {
+ out.close();
+ }
+ return true;
+ } catch (IOException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Check if a filename is "safe" (no metacharacters or spaces).
+ * @param file The file to check
+ */
+ public static boolean isFilenameSafe(File file) {
+ // Note, we check whether it matches what's known to be safe,
+ // rather than what's known to be unsafe. Non-ASCII, control
+ // characters, etc. are all unsafe by default.
+ return SAFE_FILENAME_PATTERN.matcher(file.getPath()).matches();
+ }
+
+ /**
+ * Read a text file into a String, optionally limiting the length.
+ * @param file to read (will not seek, so things like /proc files are OK)
+ * @param max length (positive for head, negative of tail, 0 for no limit)
+ * @param ellipsis to add of the file was truncated (can be null)
+ * @return the contents of the file, possibly truncated
+ * @throws IOException if something goes wrong reading the file
+ */
+ public static String readTextFile(File file, int max, String ellipsis) throws IOException {
+ InputStream input = new FileInputStream(file);
+ try {
+ if (max > 0) { // "head" mode: read the first N bytes
+ byte[] data = new byte[max + 1];
+ int length = input.read(data);
+ if (length <= 0) return "";
+ if (length <= max) return new String(data, 0, length);
+ if (ellipsis == null) return new String(data, 0, max);
+ return new String(data, 0, max) + ellipsis;
+ } else if (max < 0) { // "tail" mode: read it all, keep the last N
+ int len;
+ boolean rolled = false;
+ byte[] last = null, data = null;
+ do {
+ if (last != null) rolled = true;
+ byte[] tmp = last; last = data; data = tmp;
+ if (data == null) data = new byte[-max];
+ len = input.read(data);
+ } while (len == data.length);
+
+ if (last == null && len <= 0) return "";
+ if (last == null) return new String(data, 0, len);
+ if (len > 0) {
+ rolled = true;
+ System.arraycopy(last, len, last, 0, last.length - len);
+ System.arraycopy(data, 0, last, last.length - len, len);
+ }
+ if (ellipsis == null || !rolled) return new String(last);
+ return ellipsis + new String(last);
+ } else { // "cat" mode: read it all
+ ByteArrayOutputStream contents = new ByteArrayOutputStream();
+ int len;
+ byte[] data = new byte[1024];
+ do {
+ len = input.read(data);
+ if (len > 0) contents.write(data, 0, len);
+ } while (len == data.length);
+ return contents.toString();
+ }
+ } finally {
+ input.close();
+ }
+ }
+}
diff --git a/core/java/android/os/Handler.java b/core/java/android/os/Handler.java
new file mode 100644
index 0000000..b6f38d9
--- /dev/null
+++ b/core/java/android/os/Handler.java
@@ -0,0 +1,548 @@
+/*
+ * 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.os;
+
+import android.util.Log;
+import android.util.Printer;
+
+import java.lang.reflect.Modifier;
+
+/**
+ * A Handler allows you to send and process {@link Message} and Runnable
+ * objects associated with a thread's {@link MessageQueue}. Each Handler
+ * instance is associated with a single thread and that thread's message
+ * queue. When you create a new Handler, it is bound to the thread /
+ * message queue of the thread that is creating it -- from that point on,
+ * it will deliver messages and runnables to that message queue and execute
+ * them as they come out of the message queue.
+ *
+ * <p>There are two main uses for a Handler: (1) to schedule messages and
+ * runnables to be executed as some point in the future; and (2) to enqueue
+ * an action to be performed on a different thread than your own.
+ *
+ * <p>Scheduling messages is accomplished with the
+ * {@link #post}, {@link #postAtTime(Runnable, long)},
+ * {@link #postDelayed}, {@link #sendEmptyMessage},
+ * {@link #sendMessage}, {@link #sendMessageAtTime}, and
+ * {@link #sendMessageDelayed} methods. The <em>post</em> versions allow
+ * you to enqueue Runnable objects to be called by the message queue when
+ * they are received; the <em>sendMessage</em> versions allow you to enqueue
+ * a {@link Message} object containing a bundle of data that will be
+ * processed by the Handler's {@link #handleMessage} method (requiring that
+ * you implement a subclass of Handler).
+ *
+ * <p>When posting or sending to a Handler, you can either
+ * allow the item to be processed as soon as the message queue is ready
+ * to do so, or specify a delay before it gets processed or absolute time for
+ * it to be processed. The latter two allow you to implement timeouts,
+ * ticks, and other timing-based behavior.
+ *
+ * <p>When a
+ * process is created for your application, its main thread is dedicated to
+ * running a message queue that takes care of managing the top-level
+ * application objects (activities, intent receivers, etc) and any windows
+ * they create. You can create your own threads, and communicate back with
+ * the main application thread through a Handler. This is done by calling
+ * the same <em>post</em> or <em>sendMessage</em> methods as before, but from
+ * your new thread. The given Runnable or Message will than be scheduled
+ * in the Handler's message queue and processed when appropriate.
+ */
+public class Handler {
+ /*
+ * Set this flag to true to detect anonymous, local or member classes
+ * that extend this Handler class and that are not static. These kind
+ * of classes can potentially create leaks.
+ */
+ private static final boolean FIND_POTENTIAL_LEAKS = false;
+ private static final String TAG = "Handler";
+
+ /**
+ * Subclasses must implement this to receive messages.
+ */
+ public void handleMessage(Message msg)
+ {
+ }
+
+ /**
+ * Handle system messages here.
+ */
+ public void dispatchMessage(Message msg)
+ {
+ if (msg.callback != null) {
+ handleCallback(msg);
+ } else {
+ handleMessage(msg);
+ }
+ }
+
+ /**
+ * Default constructor associates this handler with the queue for the
+ * current thread.
+ *
+ * If there isn't one, this handler won't be able to receive messages.
+ */
+ public Handler()
+ {
+ if (FIND_POTENTIAL_LEAKS) {
+ final Class<? extends Handler> klass = getClass();
+ if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
+ (klass.getModifiers() & Modifier.STATIC) == 0) {
+ Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
+ klass.getCanonicalName());
+ }
+ }
+
+ mLooper = Looper.myLooper();
+ if (mLooper == null) {
+ throw new RuntimeException(
+ "Can't create handler inside thread that has not called Looper.prepare()");
+ }
+ mQueue = mLooper.mQueue;
+ }
+
+ /**
+ * Use the provided queue instead of the default one.
+ */
+ public Handler(Looper looper)
+ {
+ mLooper = looper;
+ mQueue = looper.mQueue;
+ }
+
+ /**
+ * Returns a new {@link android.os.Message Message} from the global message pool. More efficient than
+ * creating and allocating new instances. The retrieved message has its handler set to this instance (Message.target == this).
+ * If you don't want that facility, just call Message.obtain() instead.
+ */
+ public final Message obtainMessage()
+ {
+ return Message.obtain(this);
+ }
+
+ /**
+ * Same as {@link #obtainMessage()}, except that it also sets the what member of the returned Message.
+ *
+ * @param what Value to assign to the returned Message.what field.
+ * @return A Message from the global message pool.
+ */
+ public final Message obtainMessage(int what)
+ {
+ return Message.obtain(this, what);
+ }
+
+ /**
+ *
+ * Same as {@link #obtainMessage()}, except that it also sets the what and obj members
+ * of the returned Message.
+ *
+ * @param what Value to assign to the returned Message.what field.
+ * @param obj Value to assign to the returned Message.obj field.
+ * @return A Message from the global message pool.
+ */
+ public final Message obtainMessage(int what, Object obj)
+ {
+ return Message.obtain(this, what, obj);
+ }
+
+ /**
+ *
+ * Same as {@link #obtainMessage()}, except that it also sets the what, arg1 and arg2 members of the returned
+ * Message.
+ * @param what Value to assign to the returned Message.what field.
+ * @param arg1 Value to assign to the returned Message.arg1 field.
+ * @param arg2 Value to assign to the returned Message.arg2 field.
+ * @return A Message from the global message pool.
+ */
+ public final Message obtainMessage(int what, int arg1, int arg2)
+ {
+ return Message.obtain(this, what, arg1, arg2);
+ }
+
+ /**
+ *
+ * Same as {@link #obtainMessage()}, except that it also sets the what, obj, arg1,and arg2 values on the
+ * returned Message.
+ * @param what Value to assign to the returned Message.what field.
+ * @param arg1 Value to assign to the returned Message.arg1 field.
+ * @param arg2 Value to assign to the returned Message.arg2 field.
+ * @param obj Value to assign to the returned Message.obj field.
+ * @return A Message from the global message pool.
+ */
+ public final Message obtainMessage(int what, int arg1, int arg2, Object obj)
+ {
+ return Message.obtain(this, what, arg1, arg2, obj);
+ }
+
+ /**
+ * Causes the Runnable r to be added to the message queue.
+ * The runnable will be run on the thread to which this handler is
+ * attached.
+ *
+ * @param r The Runnable that will be executed.
+ *
+ * @return Returns true if the Runnable was successfully placed in to the
+ * message queue. Returns false on failure, usually because the
+ * looper processing the message queue is exiting.
+ */
+ public final boolean post(Runnable r)
+ {
+ return sendMessageDelayed(getPostMessage(r), 0);
+ }
+
+ /**
+ * Causes the Runnable r to be added to the message queue, to be run
+ * at a specific time given by <var>uptimeMillis</var>.
+ * <b>The time-base is {@link android.os.SystemClock#uptimeMillis}.</b>
+ * The runnable will be run on the thread to which this handler is attached.
+ *
+ * @param r The Runnable that will be executed.
+ * @param uptimeMillis The absolute time at which the callback should run,
+ * using the {@link android.os.SystemClock#uptimeMillis} time-base.
+ *
+ * @return Returns true if the Runnable was successfully placed in to the
+ * message queue. Returns false on failure, usually because the
+ * looper processing the message queue is exiting. Note that a
+ * result of true does not mean the Runnable will be processed -- if
+ * the looper is quit before the delivery time of the message
+ * occurs then the message will be dropped.
+ */
+ public final boolean postAtTime(Runnable r, long uptimeMillis)
+ {
+ return sendMessageAtTime(getPostMessage(r), uptimeMillis);
+ }
+
+ /**
+ * Causes the Runnable r to be added to the message queue, to be run
+ * at a specific time given by <var>uptimeMillis</var>.
+ * <b>The time-base is {@link android.os.SystemClock#uptimeMillis}.</b>
+ * The runnable will be run on the thread to which this handler is attached.
+ *
+ * @param r The Runnable that will be executed.
+ * @param uptimeMillis The absolute time at which the callback should run,
+ * using the {@link android.os.SystemClock#uptimeMillis} time-base.
+ *
+ * @return Returns true if the Runnable was successfully placed in to the
+ * message queue. Returns false on failure, usually because the
+ * looper processing the message queue is exiting. Note that a
+ * result of true does not mean the Runnable will be processed -- if
+ * the looper is quit before the delivery time of the message
+ * occurs then the message will be dropped.
+ *
+ * @see android.os.SystemClock#uptimeMillis
+ */
+ public final boolean postAtTime(Runnable r, Object token, long uptimeMillis)
+ {
+ return sendMessageAtTime(getPostMessage(r, token), uptimeMillis);
+ }
+
+ /**
+ * Causes the Runnable r to be added to the message queue, to be run
+ * after the specified amount of time elapses.
+ * The runnable will be run on the thread to which this handler
+ * is attached.
+ *
+ * @param r The Runnable that will be executed.
+ * @param delayMillis The delay (in milliseconds) until the Runnable
+ * will be executed.
+ *
+ * @return Returns true if the Runnable was successfully placed in to the
+ * message queue. Returns false on failure, usually because the
+ * looper processing the message queue is exiting. Note that a
+ * result of true does not mean the Runnable will be processed --
+ * if the looper is quit before the delivery time of the message
+ * occurs then the message will be dropped.
+ */
+ public final boolean postDelayed(Runnable r, long delayMillis)
+ {
+ return sendMessageDelayed(getPostMessage(r), delayMillis);
+ }
+
+ /**
+ * Posts a message to an object that implements Runnable.
+ * Causes the Runnable r to executed on the next iteration through the
+ * message queue. The runnable will be run on the thread to which this
+ * handler is attached.
+ * <b>This method is only for use in very special circumstances -- it
+ * can easily starve the message queue, cause ordering problems, or have
+ * other unexpected side-effects.</b>
+ *
+ * @param r The Runnable that will be executed.
+ *
+ * @return Returns true if the message was successfully placed in to the
+ * message queue. Returns false on failure, usually because the
+ * looper processing the message queue is exiting.
+ */
+ public final boolean postAtFrontOfQueue(Runnable r)
+ {
+ return sendMessageAtFrontOfQueue(getPostMessage(r));
+ }
+
+ /**
+ * Remove any pending posts of Runnable r that are in the message queue.
+ */
+ public final void removeCallbacks(Runnable r)
+ {
+ mQueue.removeMessages(this, r, null);
+ }
+
+ /**
+ * Remove any pending posts of Runnable <var>r</var> with Object
+ * <var>token</var> that are in the message queue.
+ */
+ public final void removeCallbacks(Runnable r, Object token)
+ {
+ mQueue.removeMessages(this, r, token);
+ }
+
+ /**
+ * Pushes a message onto the end of the message queue after all pending messages
+ * before the current time. It will be received in {@link #handleMessage},
+ * in the thread attached to this handler.
+ *
+ * @return Returns true if the message was successfully placed in to the
+ * message queue. Returns false on failure, usually because the
+ * looper processing the message queue is exiting.
+ */
+ public final boolean sendMessage(Message msg)
+ {
+ return sendMessageDelayed(msg, 0);
+ }
+
+ /**
+ * Sends a Message containing only the what value.
+ *
+ * @return Returns true if the message was successfully placed in to the
+ * message queue. Returns false on failure, usually because the
+ * looper processing the message queue is exiting.
+ */
+ public final boolean sendEmptyMessage(int what)
+ {
+ return sendEmptyMessageDelayed(what, 0);
+ }
+
+ /**
+ * Sends a Message containing only the what value, to be delivered
+ * after the specified amount of time elapses.
+ * @see #sendMessageDelayed(android.os.Message, long)
+ *
+ * @return Returns true if the message was successfully placed in to the
+ * message queue. Returns false on failure, usually because the
+ * looper processing the message queue is exiting.
+ */
+ public final boolean sendEmptyMessageDelayed(int what, long delayMillis) {
+ Message msg = Message.obtain();
+ msg.what = what;
+ return sendMessageDelayed(msg, delayMillis);
+ }
+
+ /**
+ * Sends a Message containing only the what value, to be delivered
+ * at a specific time.
+ * @see #sendMessageAtTime(android.os.Message, long)
+ *
+ * @return Returns true if the message was successfully placed in to the
+ * message queue. Returns false on failure, usually because the
+ * looper processing the message queue is exiting.
+ */
+
+ public final boolean sendEmptyMessageAtTime(int what, long uptimeMillis) {
+ Message msg = Message.obtain();
+ msg.what = what;
+ return sendMessageAtTime(msg, uptimeMillis);
+ }
+
+ /**
+ * Enqueue a message into the message queue after all pending messages
+ * before (current time + delayMillis). You will receive it in
+ * {@link #handleMessage}, in the thread attached to this handler.
+ *
+ * @return Returns true if the message was successfully placed in to the
+ * message queue. Returns false on failure, usually because the
+ * looper processing the message queue is exiting. Note that a
+ * result of true does not mean the message will be processed -- if
+ * the looper is quit before the delivery time of the message
+ * occurs then the message will be dropped.
+ */
+ public final boolean sendMessageDelayed(Message msg, long delayMillis)
+ {
+ if (delayMillis < 0) {
+ delayMillis = 0;
+ }
+ return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
+ }
+
+ /**
+ * Enqueue a message into the message queue after all pending messages
+ * before the absolute time (in milliseconds) <var>uptimeMillis</var>.
+ * <b>The time-base is {@link android.os.SystemClock#uptimeMillis}.</b>
+ * You will receive it in {@link #handleMessage}, in the thread attached
+ * to this handler.
+ *
+ * @param uptimeMillis The absolute time at which the message should be
+ * delivered, using the
+ * {@link android.os.SystemClock#uptimeMillis} time-base.
+ *
+ * @return Returns true if the message was successfully placed in to the
+ * message queue. Returns false on failure, usually because the
+ * looper processing the message queue is exiting. Note that a
+ * result of true does not mean the message will be processed -- if
+ * the looper is quit before the delivery time of the message
+ * occurs then the message will be dropped.
+ */
+ public boolean sendMessageAtTime(Message msg, long uptimeMillis)
+ {
+ boolean sent = false;
+ MessageQueue queue = mQueue;
+ if (queue != null) {
+ msg.target = this;
+ sent = queue.enqueueMessage(msg, uptimeMillis);
+ }
+ else {
+ RuntimeException e = new RuntimeException(
+ this + " sendMessageAtTime() called with no mQueue");
+ Log.w("Looper", e.getMessage(), e);
+ }
+ return sent;
+ }
+
+ /**
+ * Enqueue a message at the front of the message queue, to be processed on
+ * the next iteration of the message loop. You will receive it in
+ * {@link #handleMessage}, in the thread attached to this handler.
+ * <b>This method is only for use in very special circumstances -- it
+ * can easily starve the message queue, cause ordering problems, or have
+ * other unexpected side-effects.</b>
+ *
+ * @return Returns true if the message was successfully placed in to the
+ * message queue. Returns false on failure, usually because the
+ * looper processing the message queue is exiting.
+ */
+ public final boolean sendMessageAtFrontOfQueue(Message msg)
+ {
+ boolean sent = false;
+ MessageQueue queue = mQueue;
+ if (queue != null) {
+ msg.target = this;
+ sent = queue.enqueueMessage(msg, 0);
+ }
+ else {
+ RuntimeException e = new RuntimeException(
+ this + " sendMessageAtTime() called with no mQueue");
+ Log.w("Looper", e.getMessage(), e);
+ }
+ return sent;
+ }
+
+ /**
+ * Remove any pending posts of messages with code 'what' that are in the
+ * message queue.
+ */
+ public final void removeMessages(int what) {
+ mQueue.removeMessages(this, what, null, true);
+ }
+
+ /**
+ * Remove any pending posts of messages with code 'what' and whose obj is
+ * 'object' that are in the message queue.
+ */
+ public final void removeMessages(int what, Object object) {
+ mQueue.removeMessages(this, what, object, true);
+ }
+
+ /**
+ * Remove any pending posts of callbacks and sent messages whose
+ * <var>obj</var> is <var>token</var>.
+ */
+ public final void removeCallbacksAndMessages(Object token) {
+ mQueue.removeCallbacksAndMessages(this, token);
+ }
+
+ /**
+ * Check if there are any pending posts of messages with code 'what' in
+ * the message queue.
+ */
+ public final boolean hasMessages(int what) {
+ return mQueue.removeMessages(this, what, null, false);
+ }
+
+ /**
+ * Check if there are any pending posts of messages with code 'what' and
+ * whose obj is 'object' in the message queue.
+ */
+ public final boolean hasMessages(int what, Object object) {
+ return mQueue.removeMessages(this, what, object, false);
+ }
+
+ // if we can get rid of this method, the handler need not remember its loop
+ // we could instead export a getMessageQueue() method...
+ public final Looper getLooper() {
+ return mLooper;
+ }
+
+ public final void dump(Printer pw, String prefix) {
+ pw.println(prefix + this + " @ " + SystemClock.uptimeMillis());
+ if (mLooper == null) {
+ pw.println(prefix + "looper uninitialized");
+ } else {
+ mLooper.dump(pw, prefix + " ");
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "Handler{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + "}";
+ }
+
+ final IMessenger getIMessenger() {
+ synchronized (mQueue) {
+ if (mMessenger != null) {
+ return mMessenger;
+ }
+ mMessenger = new MessengerImpl();
+ return mMessenger;
+ }
+ }
+
+ private final class MessengerImpl extends IMessenger.Stub {
+ public void send(Message msg) {
+ Handler.this.sendMessage(msg);
+ }
+ }
+
+ private final Message getPostMessage(Runnable r) {
+ Message m = Message.obtain();
+ m.callback = r;
+ return m;
+ }
+
+ private final Message getPostMessage(Runnable r, Object token) {
+ Message m = Message.obtain();
+ m.obj = token;
+ m.callback = r;
+ return m;
+ }
+
+ private final void handleCallback(Message message) {
+ message.callback.run();
+ }
+
+ final MessageQueue mQueue;
+ final Looper mLooper;
+ IMessenger mMessenger;
+}
diff --git a/core/java/android/os/HandlerInterface.java b/core/java/android/os/HandlerInterface.java
new file mode 100644
index 0000000..62dc273
--- /dev/null
+++ b/core/java/android/os/HandlerInterface.java
@@ -0,0 +1,27 @@
+/*
+ * 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.os;
+
+/**
+ * @hide
+ * @deprecated
+ */
+public interface HandlerInterface
+{
+ void handleMessage(Message msg);
+}
+
diff --git a/core/java/android/os/HandlerThread.java b/core/java/android/os/HandlerThread.java
new file mode 100644
index 0000000..0ce86db
--- /dev/null
+++ b/core/java/android/os/HandlerThread.java
@@ -0,0 +1,93 @@
+/*
+ * 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.os;
+
+/**
+ * Handy class for starting a new thread that has a looper. The looper can then be
+ * used to create handler classes. Note that start() must still be called.
+ */
+public class HandlerThread extends Thread {
+ private int mPriority;
+ private int mTid = -1;
+ private Looper mLooper;
+
+ public HandlerThread(String name) {
+ super(name);
+ mPriority = Process.THREAD_PRIORITY_DEFAULT;
+ }
+
+ /**
+ * Constructs a HandlerThread.
+ * @param name
+ * @param priority The priority to run the thread at. The value supplied must be from
+ * {@link android.os.Process} and not from java.lang.Thread.
+ */
+ public HandlerThread(String name, int priority) {
+ super(name);
+ mPriority = priority;
+ }
+
+ /**
+ * Call back method that can be explicitly over ridden if needed to execute some
+ * setup before Looper loops.
+ */
+ protected void onLooperPrepared() {
+ }
+
+ public void run() {
+ mTid = Process.myTid();
+ Looper.prepare();
+ synchronized (this) {
+ mLooper = Looper.myLooper();
+ Process.setThreadPriority(mPriority);
+ notifyAll();
+ }
+ onLooperPrepared();
+ Looper.loop();
+ mTid = -1;
+ }
+
+ /**
+ * This method returns the Looper associated with this thread. If this thread not been started
+ * or for any reason is isAlive() returns false, this method will return null. If this thread
+ * has been started, this method will blocked until the looper has been initialized.
+ * @return The looper.
+ */
+ public Looper getLooper() {
+ if (!isAlive()) {
+ return null;
+ }
+
+ // If the thread has been started, wait until the looper has been created.
+ synchronized (this) {
+ while (isAlive() && mLooper == null) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ }
+ }
+ }
+ return mLooper;
+ }
+
+ /**
+ * Returns the identifier of this thread. See Process.myTid().
+ */
+ public int getThreadId() {
+ return mTid;
+ }
+}
diff --git a/core/java/android/os/Hardware.java b/core/java/android/os/Hardware.java
new file mode 100644
index 0000000..3b6c9d7
--- /dev/null
+++ b/core/java/android/os/Hardware.java
@@ -0,0 +1,42 @@
+/*
+ * 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.os;
+
+/**
+ * {@hide}
+ */
+public class Hardware
+{
+ /**
+ * Control the LED.
+ */
+ public static native int setLedState(int colorARGB, int onMS, int offMS);
+
+ /**
+ * Control the Flashlight
+ */
+ public static native boolean getFlashlightEnabled();
+ public static native void setFlashlightEnabled(boolean on);
+ public static native void enableCameraFlash(int milliseconds);
+
+ /**
+ * Control the backlights
+ */
+ public static native void setScreenBacklight(int brightness);
+ public static native void setKeyboardBacklight(boolean on);
+ public static native void setButtonBacklight(boolean on);
+}
diff --git a/core/java/android/os/IBinder.java b/core/java/android/os/IBinder.java
new file mode 100644
index 0000000..3ec0e9b
--- /dev/null
+++ b/core/java/android/os/IBinder.java
@@ -0,0 +1,212 @@
+/*
+ * 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.os;
+
+/**
+ * Base interface for a remotable object, the core part of a lightweight
+ * remote procedure call mechanism designed for high performance when
+ * performing in-process and cross-process calls. This
+ * interface describes the abstract protocol for interacting with a
+ * remotable object. Do not implement this interface directly, instead
+ * extend from {@link Binder}.
+ *
+ * <p>The key IBinder API is {@link #transact transact()} matched by
+ * {@link Binder#onTransact Binder.onTransact()}. These
+ * methods allow you to send a call to an IBinder object and receive a
+ * call coming in to a Binder object, respectively. This transaction API
+ * is synchronous, such that a call to {@link #transact transact()} does not
+ * return until the target has returned from
+ * {@link Binder#onTransact Binder.onTransact()}; this is the
+ * expected behavior when calling an object that exists in the local
+ * process, and the underlying inter-process communication (IPC) mechanism
+ * ensures that these same semantics apply when going across processes.
+ *
+ * <p>The data sent through transact() is a {@link Parcel}, a generic buffer
+ * of data that also maintains some meta-data about its contents. The meta
+ * data is used to manage IBinder object references in the buffer, so that those
+ * references can be maintained as the buffer moves across processes. This
+ * mechanism ensures that when an IBinder is written into a Parcel and sent to
+ * another process, if that other process sends a reference to that same IBinder
+ * back to the original process, then the original process will receive the
+ * same IBinder object back. These semantics allow IBinder/Binder objects to
+ * be used as a unique identity (to serve as a token or for other purposes)
+ * that can be managed across processes.
+ *
+ * <p>The system maintains a pool of transaction threads in each process that
+ * it runs in. These threads are used to dispatch all
+ * IPCs coming in from other processes. For example, when an IPC is made from
+ * process A to process B, the calling thread in A blocks in transact() as
+ * it sends the transaction to process B. The next available pool thread in
+ * B receives the incoming transaction, calls Binder.onTransact() on the target
+ * object, and replies with the result Parcel. Upon receiving its result, the
+ * thread in process A returns to allow its execution to continue. In effect,
+ * other processes appear to use as additional threads that you did not create
+ * executing in your own process.
+ *
+ * <p>The Binder system also supports recursion across processes. For example
+ * if process A performs a transaction to process B, and process B while
+ * handling that transaction calls transact() on an IBinder that is implemented
+ * in A, then the thread in A that is currently waiting for the original
+ * transaction to finish will take care of calling Binder.onTransact() on the
+ * object being called by B. This ensures that the recursion semantics when
+ * calling remote binder object are the same as when calling local objects.
+ *
+ * <p>When working with remote objects, you often want to find out when they
+ * are no longer valid. There are three ways this can be determined:
+ * <ul>
+ * <li> The {@link #transact transact()} method will throw a
+ * {@link RemoteException} exception if you try to call it on an IBinder
+ * whose process no longer exists.
+ * <li> The {@link #pingBinder()} method can be called, and will return false
+ * if the remote process no longer exists.
+ * <li> The {@link #linkToDeath linkToDeath()} method can be used to register
+ * a {@link DeathRecipient} with the IBinder, which will be called when its
+ * containing process goes away.
+ * </ul>
+ *
+ * @see Binder
+ */
+public interface IBinder {
+ /**
+ * The first transaction code available for user commands.
+ */
+ int FIRST_CALL_TRANSACTION = 0x00000001;
+ /**
+ * The last transaction code available for user commands.
+ */
+ int LAST_CALL_TRANSACTION = 0x00ffffff;
+
+ /**
+ * IBinder protocol transaction code: pingBinder().
+ */
+ int PING_TRANSACTION = ('_'<<24)|('P'<<16)|('N'<<8)|'G';
+
+ /**
+ * IBinder protocol transaction code: dump internal state.
+ */
+ int DUMP_TRANSACTION = ('_'<<24)|('D'<<16)|('M'<<8)|'P';
+
+ /**
+ * IBinder protocol transaction code: interrogate the recipient side
+ * of the transaction for its canonical interface descriptor.
+ */
+ int INTERFACE_TRANSACTION = ('_'<<24)|('N'<<16)|('T'<<8)|'F';
+
+ /**
+ * Flag to {@link #transact}: this is a one-way call, meaning that the
+ * caller returns immediately, without waiting for a result from the
+ * callee.
+ */
+ int FLAG_ONEWAY = 0x00000001;
+
+ /**
+ * Get the canonical name of the interface supported by this binder.
+ */
+ public String getInterfaceDescriptor() throws RemoteException;
+
+ /**
+ * Check to see if the object still exists.
+ *
+ * @return Returns false if the
+ * hosting process is gone, otherwise the result (always by default
+ * true) returned by the pingBinder() implementation on the other
+ * side.
+ */
+ public boolean pingBinder();
+
+ /**
+ * Check to see if the process that the binder is in is still alive.
+ *
+ * @return false if the process is not alive. Note that if it returns
+ * true, the process may have died while the call is returning.
+ */
+ public boolean isBinderAlive();
+
+ /**
+ * Attempt to retrieve a local implementation of an interface
+ * for this Binder object. If null is returned, you will need
+ * to instantiate a proxy class to marshall calls through
+ * the transact() method.
+ */
+ public IInterface queryLocalInterface(String descriptor);
+
+ /**
+ * Perform a generic operation with the object.
+ *
+ * @param code The action to perform. This should
+ * be a number between {@link #FIRST_CALL_TRANSACTION} and
+ * {@link #LAST_CALL_TRANSACTION}.
+ * @param data Marshalled data to send to the target. Most not be null.
+ * If you are not sending any data, you must create an empty Parcel
+ * that is given here.
+ * @param reply Marshalled data to be received from the target. May be
+ * null if you are not interested in the return value.
+ * @param flags Additional operation flags. Either 0 for a normal
+ * RPC, or {@link #FLAG_ONEWAY} for a one-way RPC.
+ */
+ public boolean transact(int code, Parcel data, Parcel reply, int flags)
+ throws RemoteException;
+
+ /**
+ * Interface for receiving a callback when the process hosting an IBinder
+ * has gone away.
+ *
+ * @see #linkToDeath
+ */
+ public interface DeathRecipient {
+ public void binderDied();
+ }
+
+ /**
+ * Register the recipient for a notification if this binder
+ * goes away. If this binder object unexpectedly goes away
+ * (typically because its hosting process has been killed),
+ * then the given {@link DeathRecipient}'s
+ * {@link DeathRecipient#binderDied DeathRecipient.binderDied()} method
+ * will be called.
+ *
+ * <p>You will only receive death notifications for remote binders,
+ * as local binders by definition can't die without you dying as well.
+ *
+ * @throws Throws {@link RemoteException} if the target IBinder's
+ * process has already died.
+ *
+ * @see #unlinkToDeath
+ */
+ public void linkToDeath(DeathRecipient recipient, int flags)
+ throws RemoteException;
+
+ /**
+ * Remove a previously registered death notification.
+ * The recipient will no longer be called if this object
+ * dies.
+ *
+ * @return Returns true if the <var>recipient</var> is successfully
+ * unlinked, assuring you that its
+ * {@link DeathRecipient#binderDied DeathRecipient.binderDied()} method
+ * will not be called. Returns false if the target IBinder has already
+ * died, meaning the method has been (or soon will be) called.
+ *
+ * @throws Throws {@link java.util.NoSuchElementException} if the given
+ * <var>recipient</var> has not been registered with the IBinder, and
+ * the IBinder is still alive. Note that if the <var>recipient</var>
+ * was never registered, but the IBinder has already died, then this
+ * exception will <em>not</em> be thrown, and you will receive a false
+ * return value instead.
+ */
+ public boolean unlinkToDeath(DeathRecipient recipient, int flags);
+}
diff --git a/core/java/android/os/ICheckinService.aidl b/core/java/android/os/ICheckinService.aidl
new file mode 100644
index 0000000..aa43852
--- /dev/null
+++ b/core/java/android/os/ICheckinService.aidl
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os;
+
+import android.os.IParentalControlCallback;
+
+/**
+ * System private API for direct access to the checkin service.
+ * Users should use the content provider instead.
+ *
+ * @see android.provider.Checkin
+ * {@hide}
+ */
+interface ICheckinService {
+ /** Direct submission of crash data; returns after writing the crash. */
+ void reportCrashSync(in byte[] crashData);
+
+ /** Asynchronous "fire and forget" version of crash reporting. */
+ oneway void reportCrashAsync(in byte[] crashData);
+
+ /** Reboot into the recovery system and wipe all user data. */
+ void masterClear();
+
+ /**
+ * Determine if the device is under parental control. Return null if
+ * we are unable to check the parental control status.
+ */
+ void getParentalControlState(IParentalControlCallback p);
+}
diff --git a/core/java/android/os/IHardwareService.aidl b/core/java/android/os/IHardwareService.aidl
new file mode 100755
index 0000000..4f6029f
--- /dev/null
+++ b/core/java/android/os/IHardwareService.aidl
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2007, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os;
+
+/** {@hide} */
+interface IHardwareService
+{
+ // Vibrator support
+ void vibrate(long milliseconds);
+ void vibratePattern(in long[] pattern, int repeat, IBinder token);
+ void cancelVibrate();
+
+ // flashlight support
+ boolean getFlashlightEnabled();
+ void setFlashlightEnabled(boolean on);
+ void enableCameraFlash(int milliseconds);
+
+ // backlight support
+ void setScreenBacklight(int brightness);
+ void setKeyboardBacklight(boolean on);
+ void setButtonBacklight(boolean on);
+
+ // LED support
+ void setLedState(int colorARGB, int onMS, int offMS);
+}
+
diff --git a/core/java/android/os/IInterface.java b/core/java/android/os/IInterface.java
new file mode 100644
index 0000000..2a2605a
--- /dev/null
+++ b/core/java/android/os/IInterface.java
@@ -0,0 +1,31 @@
+/*
+ * 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.os;
+
+/**
+ * Base class for Binder interfaces. When defining a new interface,
+ * you must derive it from IInterface.
+ */
+public interface IInterface
+{
+ /**
+ * Retrieve the Binder object associated with this interface.
+ * You must use this instead of a plain cast, so that proxy objects
+ * can return the correct result.
+ */
+ public IBinder asBinder();
+}
diff --git a/core/java/android/os/IMessenger.aidl b/core/java/android/os/IMessenger.aidl
new file mode 100644
index 0000000..e4a8431
--- /dev/null
+++ b/core/java/android/os/IMessenger.aidl
@@ -0,0 +1,25 @@
+/* //device/java/android/android/app/IActivityPendingResult.aidl
+**
+** Copyright 2007, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.os;
+
+import android.os.Message;
+
+/** @hide */
+oneway interface IMessenger {
+ void send(in Message msg);
+}
diff --git a/core/java/android/os/IMountService.aidl b/core/java/android/os/IMountService.aidl
new file mode 100644
index 0000000..0397446
--- /dev/null
+++ b/core/java/android/os/IMountService.aidl
@@ -0,0 +1,51 @@
+/* //device/java/android/android/os/IUsb.aidl
+**
+** Copyright 2007, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.os;
+
+/** WARNING! Update IMountService.h and IMountService.cpp if you change this file.
+ * In particular, the ordering of the methods below must match the
+ * _TRANSACTION enum in IMountService.cpp
+ * @hide
+ */
+interface IMountService
+{
+ /**
+ * Is mass storage support enabled?
+ */
+ boolean getMassStorageEnabled();
+
+ /**
+ * Enable or disable mass storage support.
+ */
+ void setMassStorageEnabled(boolean enabled);
+
+ /**
+ * Is mass storage connected?
+ */
+ boolean getMassStorageConnected();
+
+ /**
+ * Mount external storage at given mount point.
+ */
+ void mountMedia(String mountPoint);
+
+ /**
+ * Safely unmount external storage at given mount point.
+ */
+ void unmountMedia(String mountPoint);
+}
diff --git a/core/java/android/os/INetStatService.aidl b/core/java/android/os/INetStatService.aidl
new file mode 100644
index 0000000..fb840d8
--- /dev/null
+++ b/core/java/android/os/INetStatService.aidl
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os;
+
+/**
+ * Retrieves packet and byte counts for the phone data interface.
+ * Used for the data activity icon and the phone status in Settings.
+ *
+ * {@hide}
+ */
+interface INetStatService {
+ int getTxPackets();
+ int getRxPackets();
+ int getTxBytes();
+ int getRxBytes();
+}
diff --git a/core/java/android/os/IParentalControlCallback.aidl b/core/java/android/os/IParentalControlCallback.aidl
new file mode 100644
index 0000000..2f1a563
--- /dev/null
+++ b/core/java/android/os/IParentalControlCallback.aidl
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os;
+
+import com.google.android.net.ParentalControlState;
+
+/**
+ * This callback interface is used to deliver the parental control state to the calling application.
+ * {@hide}
+ */
+oneway interface IParentalControlCallback {
+ void onResult(in ParentalControlState state);
+}
diff --git a/core/java/android/os/IPermissionController.aidl b/core/java/android/os/IPermissionController.aidl
new file mode 100644
index 0000000..73a68f1
--- /dev/null
+++ b/core/java/android/os/IPermissionController.aidl
@@ -0,0 +1,23 @@
+/* //device/java/android/android/os/IPowerManager.aidl
+**
+** Copyright 2007, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.os;
+
+/** @hide */
+interface IPermissionController {
+ boolean checkPermission(String permission, int pid, int uid);
+}
diff --git a/core/java/android/os/IPowerManager.aidl b/core/java/android/os/IPowerManager.aidl
new file mode 100644
index 0000000..9d05917
--- /dev/null
+++ b/core/java/android/os/IPowerManager.aidl
@@ -0,0 +1,31 @@
+/* //device/java/android/android/os/IPowerManager.aidl
+**
+** Copyright 2007, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.os;
+
+/** @hide */
+interface IPowerManager
+{
+ void acquireWakeLock(int flags, IBinder lock, String tag);
+ void goToSleep(long time);
+ void releaseWakeLock(IBinder lock);
+ void userActivity(long when, boolean noChangeLights);
+ void userActivityWithForce(long when, boolean noChangeLights, boolean force);
+ void setPokeLock(int pokey, IBinder lock, String tag);
+ void setStayOnSetting(boolean val);
+ long getScreenOnTime();
+}
diff --git a/core/java/android/os/IServiceManager.java b/core/java/android/os/IServiceManager.java
new file mode 100644
index 0000000..9a5ff47
--- /dev/null
+++ b/core/java/android/os/IServiceManager.java
@@ -0,0 +1,70 @@
+/*
+ * 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.os;
+
+/**
+ * Basic interface for finding and publishing system services.
+ *
+ * An implementation of this interface is usually published as the
+ * global context object, which can be retrieved via
+ * BinderNative.getContextObject(). An easy way to retrieve this
+ * is with the static method BnServiceManager.getDefault().
+ *
+ * @hide
+ */
+public interface IServiceManager extends IInterface
+{
+ /**
+ * Retrieve an existing service called @a name from the
+ * service manager. Blocks for a few seconds waiting for it to be
+ * published if it does not already exist.
+ */
+ public IBinder getService(String name) throws RemoteException;
+
+ /**
+ * Retrieve an existing service called @a name from the
+ * service manager. Non-blocking.
+ */
+ public IBinder checkService(String name) throws RemoteException;
+
+ /**
+ * Place a new @a service called @a name into the service
+ * manager.
+ */
+ public void addService(String name, IBinder service) throws RemoteException;
+
+ /**
+ * Return a list of all currently running services.
+ */
+ public String[] listServices() throws RemoteException;
+
+ /**
+ * Assign a permission controller to the service manager. After set, this
+ * interface is checked before any services are added.
+ */
+ public void setPermissionController(IPermissionController controller)
+ throws RemoteException;
+
+ static final String descriptor = "android.os.IServiceManager";
+
+ int GET_SERVICE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION;
+ int CHECK_SERVICE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+1;
+ int ADD_SERVICE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+2;
+ int LIST_SERVICES_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+3;
+ int CHECK_SERVICES_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+4;
+ int SET_PERMISSION_CONTROLLER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+5;
+}
diff --git a/core/java/android/os/LocalPowerManager.java b/core/java/android/os/LocalPowerManager.java
new file mode 100644
index 0000000..55d7972
--- /dev/null
+++ b/core/java/android/os/LocalPowerManager.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os;
+
+/** @hide */
+public interface LocalPowerManager {
+ public static final int OTHER_EVENT = 0;
+ public static final int CHEEK_EVENT = 1;
+ public static final int TOUCH_EVENT = 2;
+ public static final int BUTTON_EVENT = 3; // Button and trackball events.
+
+ public static final int POKE_LOCK_IGNORE_CHEEK_EVENTS = 0x1;
+ public static final int POKE_LOCK_SHORT_TIMEOUT = 0x2;
+ public static final int POKE_LOCK_MEDIUM_TIMEOUT = 0x4;
+
+ public static final int POKE_LOCK_TIMEOUT_MASK = 0x6;
+
+ void goToSleep(long time);
+
+ // notify power manager when keyboard is opened/closed
+ void setKeyboardVisibility(boolean visible);
+
+ // when the keyguard is up, it manages the power state, and userActivity doesn't do anything.
+ void enableUserActivity(boolean enabled);
+
+ // the same as the method on PowerManager
+ public void userActivity(long time, boolean noChangeLights, int eventType);
+}
diff --git a/core/java/android/os/Looper.java b/core/java/android/os/Looper.java
new file mode 100644
index 0000000..80b68e2
--- /dev/null
+++ b/core/java/android/os/Looper.java
@@ -0,0 +1,214 @@
+/*
+ * 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.os;
+
+import android.util.Config;
+import android.util.Printer;
+
+/**
+ * Class used to run a message loop for a thread. Threads by default do
+ * not have a message loop associated with them; to create one, call
+ * {@link #prepare} in the thread that is to run the loop, and then
+ * {@link #loop} to have it process messages until the loop is stopped.
+ *
+ * <p>Most interaction with a message loop is through the
+ * {@link Handler} class.
+ *
+ * <p>This is a typical example of the implementation of a Looper thread,
+ * using the separation of {@link #prepare} and {@link #loop} to create an
+ * initial Handler to communicate with the Looper.
+ *
+ * <pre>
+ * class LooperThread extends Thread {
+ * public Handler mHandler;
+ *
+ * public void run() {
+ * Looper.prepare();
+ *
+ * mHandler = new Handler() {
+ * public void handleMessage(Message msg) {
+ * // process incoming messages here
+ * }
+ * };
+ *
+ * Looper.loop();
+ * }
+ * }</pre>
+ */
+public class Looper {
+ private static final boolean DEBUG = false;
+ private static final boolean localLOGV = DEBUG ? Config.LOGD : Config.LOGV;
+
+ // sThreadLocal.get() will return null unless you've called prepare().
+ private static final ThreadLocal sThreadLocal = new ThreadLocal();
+
+ final MessageQueue mQueue;
+ volatile boolean mRun;
+ Thread mThread;
+ private Printer mLogging = null;
+ private static Looper mMainLooper = null;
+
+ /** Initialize the current thread as a looper.
+ * This gives you a chance to create handlers that then reference
+ * this looper, before actually starting the loop. Be sure to call
+ * {@link #loop()} after calling this method, and end it by calling
+ * {@link #quit()}.
+ */
+ public static final void prepare() {
+ if (sThreadLocal.get() != null) {
+ throw new RuntimeException("Only one Looper may be created per thread");
+ }
+ sThreadLocal.set(new Looper());
+ }
+
+ /** Initialize the current thread as a looper, marking it as an application's main
+ * looper. The main looper for your application is created by the Android environment,
+ * so you should never need to call this function yourself.
+ * {@link #prepare()}
+ */
+
+ public static final void prepareMainLooper() {
+ prepare();
+ setMainLooper(myLooper());
+ if (Process.supportsProcesses()) {
+ myLooper().mQueue.mQuitAllowed = false;
+ }
+ }
+
+ private synchronized static void setMainLooper(Looper looper) {
+ mMainLooper = looper;
+ }
+
+ /** Returns the application's main looper, which lives in the main thread of the application.
+ */
+ public synchronized static final Looper getMainLooper() {
+ return mMainLooper;
+ }
+
+ /**
+ * Run the message queue in this thread. Be sure to call
+ * {@link #quit()} to end the loop.
+ */
+ public static final void loop() {
+ Looper me = myLooper();
+ MessageQueue queue = me.mQueue;
+ while (true) {
+ Message msg = queue.next(); // might block
+ //if (!me.mRun) {
+ // break;
+ //}
+ if (msg != null) {
+ if (msg.target == null) {
+ // No target is a magic identifier for the quit message.
+ return;
+ }
+ if (me.mLogging!= null) me.mLogging.println(
+ ">>>>> Dispatching to " + msg.target + " "
+ + msg.callback + ": " + msg.what
+ );
+ msg.target.dispatchMessage(msg);
+ if (me.mLogging!= null) me.mLogging.println(
+ "<<<<< Finished to " + msg.target + " "
+ + msg.callback);
+ msg.recycle();
+ }
+ }
+ }
+
+ /**
+ * Return the Looper object associated with the current thread.
+ */
+ public static final Looper myLooper() {
+ return (Looper)sThreadLocal.get();
+ }
+
+ /**
+ * Control logging of messages as they are processed by this Looper. If
+ * enabled, a log message will be written to <var>printer</var>
+ * at the beginning and ending of each message dispatch, identifying the
+ * target Handler and message contents.
+ *
+ * @param printer A Printer object that will receive log messages, or
+ * null to disable message logging.
+ */
+ public void setMessageLogging(Printer printer) {
+ mLogging = printer;
+ }
+
+ /**
+ * Return the {@link MessageQueue} object associated with the current
+ * thread.
+ */
+ public static final MessageQueue myQueue() {
+ return myLooper().mQueue;
+ }
+
+ private Looper() {
+ mQueue = new MessageQueue();
+ mRun = true;
+ mThread = Thread.currentThread();
+ }
+
+ public void quit() {
+ Message msg = Message.obtain();
+ // NOTE: By enqueueing directly into the message queue, the
+ // message is left with a null target. This is how we know it is
+ // a quit message.
+ mQueue.enqueueMessage(msg, 0);
+ }
+
+ public void dump(Printer pw, String prefix) {
+ pw.println(prefix + this);
+ pw.println(prefix + "mRun=" + mRun);
+ pw.println(prefix + "mThread=" + mThread);
+ pw.println(prefix + "mQueue=" + ((mQueue != null) ? mQueue : "(null"));
+ if (mQueue != null) {
+ synchronized (mQueue) {
+ Message msg = mQueue.mMessages;
+ int n = 0;
+ while (msg != null) {
+ pw.println(prefix + " Message " + n + ": " + msg);
+ n++;
+ msg = msg.next;
+ }
+ pw.println(prefix + "(Total messages: " + n + ")");
+ }
+ }
+ }
+
+ public String toString() {
+ return "Looper{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + "}";
+ }
+
+ static class HandlerException extends Exception {
+
+ HandlerException(Message message, Throwable cause) {
+ super(createMessage(cause), cause);
+ }
+
+ static String createMessage(Throwable cause) {
+ String causeMsg = cause.getMessage();
+ if (causeMsg == null) {
+ causeMsg = cause.toString();
+ }
+ return causeMsg;
+ }
+ }
+}
+
diff --git a/core/java/android/os/MailboxNotAvailableException.java b/core/java/android/os/MailboxNotAvailableException.java
new file mode 100644
index 0000000..574adbd
--- /dev/null
+++ b/core/java/android/os/MailboxNotAvailableException.java
@@ -0,0 +1,37 @@
+/*
+ * 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.os;
+
+/** @hide */
+public class MailboxNotAvailableException extends Throwable
+{
+ /**
+ * This exception represents the case when a request for a
+ * named, published mailbox fails because the requested name has not been published
+ */
+
+ public
+ MailboxNotAvailableException()
+ {
+ }
+
+ public
+ MailboxNotAvailableException(String s)
+ {
+ super(s);
+ }
+}
diff --git a/core/java/android/os/MemoryFile.java b/core/java/android/os/MemoryFile.java
new file mode 100644
index 0000000..76e4f47
--- /dev/null
+++ b/core/java/android/os/MemoryFile.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os;
+
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+
+/**
+ * MemoryFile is a wrapper for the Linux ashmem driver.
+ * MemoryFiles are backed by shared memory, which can be optionally
+ * set to be purgeable.
+ * Purgeable files may have their contents reclaimed by the kernel
+ * in low memory conditions (only if allowPurging is set to true).
+ * After a file is purged, attempts to read or write the file will
+ * cause an IOException to be thrown.
+ */
+public class MemoryFile
+{
+ private static String TAG = "MemoryFile";
+
+ // returns fd
+ private native int native_open(String name, int length);
+ // returns memory address for ashmem region
+ private native int native_mmap(int fd, int length);
+ private native void native_close(int fd);
+ private native int native_read(int fd, int address, byte[] buffer,
+ int srcOffset, int destOffset, int count, boolean isUnpinned);
+ private native void native_write(int fd, int address, byte[] buffer,
+ int srcOffset, int destOffset, int count, boolean isUnpinned);
+ private native void native_pin(int fd, boolean pin);
+
+ private int mFD; // ashmem file descriptor
+ private int mAddress; // address of ashmem memory
+ private int mLength; // total length of our ashmem region
+ private boolean mAllowPurging = false; // true if our ashmem region is unpinned
+
+ /**
+ * MemoryFile constructor.
+ *
+ * @param name optional name for the file (can be null).
+ * @param length of the memory file in bytes.
+ */
+ public MemoryFile(String name, int length) {
+ mLength = length;
+ mFD = native_open(name, length);
+ mAddress = native_mmap(mFD, length);
+ }
+
+ /**
+ * Closes and releases all resources for the memory file.
+ */
+ public void close() {
+ if (mFD > 0) {
+ native_close(mFD);
+ mFD = 0;
+ }
+ }
+
+ @Override
+ protected void finalize() {
+ if (mFD > 0) {
+ Log.e(TAG, "MemoryFile.finalize() called while ashmem still open");
+ close();
+ }
+ }
+
+ /**
+ * Returns the length of the memory file.
+ *
+ * @return file length.
+ */
+ public int length() {
+ return mLength;
+ }
+
+ /**
+ * Is memory file purging enabled?
+ *
+ * @return true if the file may be purged.
+ */
+ public boolean isPurgingAllowed() {
+ return mAllowPurging;
+ }
+
+ /**
+ * Enables or disables purging of the memory file.
+ *
+ * @param allowPurging true if the operating system can purge the contents
+ * of the file in low memory situations
+ * @return previous value of allowPurging
+ */
+ synchronized public boolean allowPurging(boolean allowPurging) throws IOException {
+ boolean oldValue = mAllowPurging;
+ if (oldValue != allowPurging) {
+ native_pin(mFD, !allowPurging);
+ mAllowPurging = allowPurging;
+ }
+ return oldValue;
+ }
+
+ /**
+ * Creates a new InputStream for reading from the memory file.
+ *
+ @return InputStream
+ */
+ public InputStream getInputStream() {
+ return new MemoryInputStream();
+ }
+
+ /**
+ * Creates a new OutputStream for writing to the memory file.
+ *
+ @return OutputStream
+ */
+ public OutputStream getOutputStream() {
+
+ return new MemoryOutputStream();
+ }
+
+ /**
+ * Reads bytes from the memory file.
+ * Will throw an IOException if the file has been purged.
+ *
+ * @param buffer byte array to read bytes into.
+ * @param srcOffset offset into the memory file to read from.
+ * @param destOffset offset into the byte array buffer to read into.
+ * @param count number of bytes to read.
+ * @return number of bytes read.
+ */
+ public int readBytes(byte[] buffer, int srcOffset, int destOffset, int count)
+ throws IOException {
+ if (destOffset < 0 || destOffset > buffer.length || count < 0
+ || count > buffer.length - destOffset
+ || srcOffset < 0 || srcOffset > mLength
+ || count > mLength - srcOffset) {
+ throw new IndexOutOfBoundsException();
+ }
+ return native_read(mFD, mAddress, buffer, srcOffset, destOffset, count, mAllowPurging);
+ }
+
+ /**
+ * Write bytes to the memory file.
+ * Will throw an IOException if the file has been purged.
+ *
+ * @param buffer byte array to write bytes from.
+ * @param srcOffset offset into the byte array buffer to write from.
+ * @param destOffset offset into the memory file to write to.
+ * @param count number of bytes to write.
+ */
+ public void writeBytes(byte[] buffer, int srcOffset, int destOffset, int count)
+ throws IOException {
+ if (srcOffset < 0 || srcOffset > buffer.length || count < 0
+ || count > buffer.length - srcOffset
+ || destOffset < 0 || destOffset > mLength
+ || count > mLength - destOffset) {
+ throw new IndexOutOfBoundsException();
+ }
+ native_write(mFD, mAddress, buffer, srcOffset, destOffset, count, mAllowPurging);
+ }
+
+ private class MemoryInputStream extends InputStream {
+
+ private int mMark = 0;
+ private int mOffset = 0;
+ private byte[] mSingleByte;
+
+ @Override
+ public int available() throws IOException {
+ if (mOffset >= mLength) {
+ return 0;
+ }
+ return mLength - mOffset;
+ }
+
+ @Override
+ public boolean markSupported() {
+ return true;
+ }
+
+ @Override
+ public void mark(int readlimit) {
+ mMark = mOffset;
+ }
+
+ @Override
+ public void reset() throws IOException {
+ mOffset = mMark;
+ }
+
+ @Override
+ public int read() throws IOException {
+ if (mSingleByte == null) {
+ mSingleByte = new byte[1];
+ }
+ int result = read(mSingleByte, 0, 1);
+ if (result != 1) {
+ throw new IOException("read() failed");
+ }
+ return mSingleByte[0];
+ }
+
+ @Override
+ public int read(byte buffer[], int offset, int count) throws IOException {
+ int result = readBytes(buffer, mOffset, offset, count);
+ if (result > 0) {
+ mOffset += result;
+ }
+ return result;
+ }
+
+ @Override
+ public long skip(long n) throws IOException {
+ if (mOffset + n > mLength) {
+ n = mLength - mOffset;
+ }
+ mOffset += n;
+ return n;
+ }
+ }
+
+ private class MemoryOutputStream extends OutputStream {
+
+ private int mOffset = 0;
+ private byte[] mSingleByte;
+
+ @Override
+ public void write(byte buffer[], int offset, int count) throws IOException {
+ writeBytes(buffer, offset, mOffset, count);
+ }
+
+ @Override
+ public void write(int oneByte) throws IOException {
+ if (mSingleByte == null) {
+ mSingleByte = new byte[1];
+ }
+ mSingleByte[0] = (byte)oneByte;
+ write(mSingleByte, 0, 1);
+ }
+ }
+}
diff --git a/core/java/android/os/Message.aidl b/core/java/android/os/Message.aidl
new file mode 100644
index 0000000..e8dbb5a
--- /dev/null
+++ b/core/java/android/os/Message.aidl
@@ -0,0 +1,20 @@
+/* //device/java/android/android/content/Intent.aidl
+**
+** Copyright 2007, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.os;
+
+parcelable Message;
diff --git a/core/java/android/os/Message.java b/core/java/android/os/Message.java
new file mode 100644
index 0000000..4130109
--- /dev/null
+++ b/core/java/android/os/Message.java
@@ -0,0 +1,405 @@
+/*
+ * 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.os;
+
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ *
+ * Defines a message containing a description and arbitrary data object that can be
+ * sent to a {@link Handler}. This object contains two extra int fields and an
+ * extra object field that allow you to not do allocations in many cases.
+ *
+ * <p class="note">While the constructor of Message is public, the best way to get
+ * one of these is to call {@link #obtain Message.obtain()} or one of the
+ * {@link Handler#obtainMessage Handler.obtainMessage()} methods, which will pull
+ * them from a pool of recycled objects.</p>
+ */
+public final class Message implements Parcelable {
+ /**
+ * User-defined message code so that the recipient can identify
+ * what this message is about. Each {@link Handler} has its own name-space
+ * for message codes, so you do not need to worry about yours conflicting
+ * with other handlers.
+ */
+ public int what;
+
+ // Use these fields instead of using the class's Bundle if you can.
+ /** arg1 and arg2 are lower-cost alternatives to using {@link #setData(Bundle) setData()}
+ if you only need to store a few integer values. */
+ public int arg1;
+
+ /** arg1 and arg2 are lower-cost alternatives to using {@link #setData(Bundle) setData()}
+ if you only need to store a few integer values.*/
+ public int arg2;
+
+ /** An arbitrary object to send to the recipient. This must be null when
+ * sending messages across processes. */
+ public Object obj;
+
+ /** Optional Messenger where replies to this message can be sent.
+ */
+ public Messenger replyTo;
+
+ /*package*/ long when;
+
+ /*package*/ Bundle data;
+
+ /*package*/ Handler target;
+
+ /*package*/ Runnable callback;
+
+ // sometimes we store linked lists of these things
+ /*package*/ Message next;
+
+ private static Object mPoolSync = new Object();
+ private static Message mPool;
+ private static int mPoolSize = 0;
+
+ private static final int MAX_POOL_SIZE = 10;
+
+ /**
+ * Return a new Message instance from the global pool. Allows us to
+ * avoid allocating new objects in many cases.
+ */
+ public static Message obtain() {
+ synchronized (mPoolSync) {
+ if (mPool != null) {
+ Message m = mPool;
+ mPool = m.next;
+ m.next = null;
+ return m;
+ }
+ }
+ return new Message();
+ }
+
+ /**
+ * Same as {@link #obtain()}, but copies the values of an existing
+ * message (including its target) into the new one.
+ * @param orig Original message to copy.
+ * @return A Message object from the global pool.
+ */
+ public static Message obtain(Message orig) {
+ Message m = obtain();
+ m.what = orig.what;
+ m.arg1 = orig.arg1;
+ m.arg2 = orig.arg2;
+ m.obj = orig.obj;
+ m.replyTo = orig.replyTo;
+ if (orig.data != null) {
+ m.data = new Bundle(orig.data);
+ }
+ m.target = orig.target;
+ m.callback = orig.callback;
+
+ return m;
+ }
+
+ /**
+ * Same as {@link #obtain()}, but sets the value for the <em>target</em> member on the Message returned.
+ * @param h Handler to assign to the returned Message object's <em>target</em> member.
+ * @return A Message object from the global pool.
+ */
+ public static Message obtain(Handler h) {
+ Message m = obtain();
+ m.target = h;
+
+ return m;
+ }
+
+ /**
+ * Same as {@link #obtain(Handler)}, but assigns a callback Runnable on
+ * the Message that is returned.
+ * @param h Handler to assign to the returned Message object's <em>target</em> member.
+ * @param callback Runnable that will execute when the message is handled.
+ * @return A Message object from the global pool.
+ */
+ public static Message obtain(Handler h, Runnable callback) {
+ Message m = obtain();
+ m.target = h;
+ m.callback = callback;
+
+ return m;
+ }
+
+ /**
+ * Same as {@link #obtain()}, but sets the values for both <em>target</em> and
+ * <em>what</em> members on the Message.
+ * @param h Value to assign to the <em>target</em> member.
+ * @param what Value to assign to the <em>what</em> member.
+ * @return A Message object from the global pool.
+ */
+ public static Message obtain(Handler h, int what) {
+ Message m = obtain();
+ m.target = h;
+ m.what = what;
+
+ return m;
+ }
+
+ /**
+ * Same as {@link #obtain()}, but sets the values of the <em>target</em>, <em>what</em>, and <em>obj</em>
+ * members.
+ * @param h The <em>target</em> value to set.
+ * @param what The <em>what</em> value to set.
+ * @param obj The <em>object</em> method to set.
+ * @return A Message object from the global pool.
+ */
+ public static Message obtain(Handler h, int what, Object obj) {
+ Message m = obtain();
+ m.target = h;
+ m.what = what;
+ m.obj = obj;
+
+ return m;
+ }
+
+ /**
+ * Same as {@link #obtain()}, but sets the values of the <em>target</em>, <em>what</em>,
+ * <em>arg1</em>, and <em>arg2</em> members.
+ *
+ * @param h The <em>target</em> value to set.
+ * @param what The <em>what</em> value to set.
+ * @param arg1 The <em>arg1</em> value to set.
+ * @param arg2 The <em>arg2</em> value to set.
+ * @return A Message object from the global pool.
+ */
+ public static Message obtain(Handler h, int what, int arg1, int arg2) {
+ Message m = obtain();
+ m.target = h;
+ m.what = what;
+ m.arg1 = arg1;
+ m.arg2 = arg2;
+
+ return m;
+ }
+
+ /**
+ * Same as {@link #obtain()}, but sets the values of the <em>target</em>, <em>what</em>,
+ * <em>arg1</em>, <em>arg2</em>, and <em>obj</em> members.
+ *
+ * @param h The <em>target</em> value to set.
+ * @param what The <em>what</em> value to set.
+ * @param arg1 The <em>arg1</em> value to set.
+ * @param arg2 The <em>arg2</em> value to set.
+ * @param obj The <em>obj</em> value to set.
+ * @return A Message object from the global pool.
+ */
+ public static Message obtain(Handler h, int what,
+ int arg1, int arg2, Object obj) {
+ Message m = obtain();
+ m.target = h;
+ m.what = what;
+ m.arg1 = arg1;
+ m.arg2 = arg2;
+ m.obj = obj;
+
+ return m;
+ }
+
+ /**
+ * Return a Message instance to the global pool. You MUST NOT touch
+ * the Message after calling this function -- it has effectively been
+ * freed.
+ */
+ public void recycle() {
+ synchronized (mPoolSync) {
+ if (mPoolSize < MAX_POOL_SIZE) {
+ clearForRecycle();
+
+ next = mPool;
+ mPool = this;
+ }
+ }
+ }
+
+ /**
+ * Make this message like o. Performs a shallow copy of the data field.
+ * Does not copy the linked list fields, nor the timestamp or
+ * target/callback of the original message.
+ */
+ public void copyFrom(Message o) {
+ this.what = o.what;
+ this.arg1 = o.arg1;
+ this.arg2 = o.arg2;
+ this.obj = o.obj;
+ this.replyTo = o.replyTo;
+
+ if (o.data != null) {
+ this.data = (Bundle) o.data.clone();
+ } else {
+ this.data = null;
+ }
+ }
+
+ /**
+ * Return the targeted delivery time of this message, in milliseconds.
+ */
+ public long getWhen() {
+ return when;
+ }
+
+ public void setTarget(Handler target) {
+ this.target = target;
+ }
+
+ /**
+ * Retrieve the a {@link android.os.Handler Handler} implementation that
+ * will receive this message. The object must implement
+ * {@link android.os.Handler#handleMessage(android.os.Message)
+ * Handler.handleMessage()}. Each Handler has its own name-space for
+ * message codes, so you do not need to
+ * worry about yours conflicting with other handlers.
+ */
+ public Handler getTarget() {
+ return target;
+ }
+
+ /**
+ * Retrieve callback object that will execute when this message is handled.
+ * This object must implement Runnable. This is called by
+ * the <em>target</em> {@link Handler} that is receiving this Message to
+ * dispatch it. If
+ * not set, the message will be dispatched to the receiving Handler's
+ * {@link Handler#handleMessage(Message Handler.handleMessage())}. */
+ public Runnable getCallback() {
+ return callback;
+ }
+
+ /**
+ * Obtains a Bundle of arbitrary data associated with this
+ * event, lazily creating it if necessary. Set this value by calling {@link #setData(Bundle)}.
+ */
+ public Bundle getData() {
+ if (data == null) {
+ data = new Bundle();
+ }
+
+ return data;
+ }
+
+ /**
+ * Like getData(), but does not lazily create the Bundle. A null
+ * is returned if the Bundle does not already exist.
+ */
+ public Bundle peekData() {
+ return data;
+ }
+
+ /** Sets a Bundle of arbitrary data values. Use arg1 and arg1 members
+ * as a lower cost way to send a few simple integer values, if you can. */
+ public void setData(Bundle data) {
+ this.data = data;
+ }
+
+ /**
+ * Sends this Message to the Handler specified by {@link #getTarget}.
+ * Throws a null pointer exception if this field has not been set.
+ */
+ public void sendToTarget() {
+ target.sendMessage(this);
+ }
+
+ /*package*/ void clearForRecycle() {
+ what = 0;
+ arg1 = 0;
+ arg2 = 0;
+ obj = null;
+ replyTo = null;
+ when = 0;
+ target = null;
+ callback = null;
+ data = null;
+ }
+
+ /** Constructor (but the preferred way to get a Message is to call {@link #obtain() Message.obtain()}).
+ */
+ public Message() {
+ }
+
+ public String toString() {
+ StringBuilder b = new StringBuilder();
+
+ b.append("{ what=");
+ b.append(what);
+
+ b.append(" when=");
+ b.append(when);
+
+ if (arg1 != 0) {
+ b.append(" arg1=");
+ b.append(arg1);
+ }
+
+ if (arg2 != 0) {
+ b.append(" arg2=");
+ b.append(arg2);
+ }
+
+ if (obj != null) {
+ b.append(" obj=");
+ b.append(obj);
+ }
+
+ b.append(" }");
+
+ return b.toString();
+ }
+
+ public static final Parcelable.Creator<Message> CREATOR
+ = new Parcelable.Creator<Message>() {
+ public Message createFromParcel(Parcel source) {
+ Message msg = Message.obtain();
+ msg.readFromParcel(source);
+ return msg;
+ }
+
+ public Message[] newArray(int size) {
+ return new Message[size];
+ }
+ };
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ if (obj != null || callback != null) {
+ throw new RuntimeException(
+ "Can't marshal objects across processes.");
+ }
+ dest.writeInt(what);
+ dest.writeInt(arg1);
+ dest.writeInt(arg2);
+ dest.writeLong(when);
+ dest.writeBundle(data);
+ Messenger.writeMessengerOrNullToParcel(replyTo, dest);
+ }
+
+ private final void readFromParcel(Parcel source) {
+ what = source.readInt();
+ arg1 = source.readInt();
+ arg2 = source.readInt();
+ when = source.readLong();
+ data = source.readBundle();
+ replyTo = Messenger.readMessengerOrNullFromParcel(source);
+ }
+}
+
diff --git a/core/java/android/os/MessageQueue.java b/core/java/android/os/MessageQueue.java
new file mode 100644
index 0000000..caf0923
--- /dev/null
+++ b/core/java/android/os/MessageQueue.java
@@ -0,0 +1,329 @@
+/*
+ * 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.os;
+
+import java.util.ArrayList;
+
+import android.util.AndroidRuntimeException;
+import android.util.Config;
+import android.util.Log;
+
+import com.android.internal.os.RuntimeInit;
+
+/**
+ * Low-level class holding the list of messages to be dispatched by a
+ * {@link Looper}. Messages are not added directly to a MessageQueue,
+ * but rather through {@link Handler} objects associated with the Looper.
+ *
+ * <p>You can retrieve the MessageQueue for the current thread with
+ * {@link Looper#myQueue() Looper.myQueue()}.
+ */
+public class MessageQueue {
+ Message mMessages;
+ private final ArrayList mIdleHandlers = new ArrayList();
+ private boolean mQuiting = false;
+ boolean mQuitAllowed = true;
+
+ /**
+ * Callback interface for discovering when a thread is going to block
+ * waiting for more messages.
+ */
+ public static interface IdleHandler {
+ /**
+ * Called when the message queue has run out of messages and will now
+ * wait for more. Return true to keep your idle handler active, false
+ * to have it removed. This may be called if there are still messages
+ * pending in the queue, but they are all scheduled to be dispatched
+ * after the current time.
+ */
+ boolean queueIdle();
+ }
+
+ /**
+ * Add a new {@link IdleHandler} to this message queue. This may be
+ * removed automatically for you by returning false from
+ * {@link IdleHandler#queueIdle IdleHandler.queueIdle()} when it is
+ * invoked, or explicitly removing it with {@link #removeIdleHandler}.
+ *
+ * <p>This method is safe to call from any thread.
+ *
+ * @param handler The IdleHandler to be added.
+ */
+ public final void addIdleHandler(IdleHandler handler) {
+ if (handler == null) {
+ throw new NullPointerException("Can't add a null IdleHandler");
+ }
+ synchronized (this) {
+ mIdleHandlers.add(handler);
+ }
+ }
+
+ /**
+ * Remove an {@link IdleHandler} from the queue that was previously added
+ * with {@link #addIdleHandler}. If the given object is not currently
+ * in the idle list, nothing is done.
+ *
+ * @param handler The IdleHandler to be removed.
+ */
+ public final void removeIdleHandler(IdleHandler handler) {
+ synchronized (this) {
+ mIdleHandlers.remove(handler);
+ }
+ }
+
+ MessageQueue() {
+ }
+
+ final Message next() {
+ boolean tryIdle = true;
+
+ while (true) {
+ long now;
+ Object[] idlers = null;
+
+ // Try to retrieve the next message, returning if found.
+ synchronized (this) {
+ now = SystemClock.uptimeMillis();
+ Message msg = pullNextLocked(now);
+ if (msg != null) return msg;
+ if (tryIdle && mIdleHandlers.size() > 0) {
+ idlers = mIdleHandlers.toArray();
+ }
+ }
+
+ // There was no message so we are going to wait... but first,
+ // if there are any idle handlers let them know.
+ boolean didIdle = false;
+ if (idlers != null) {
+ for (Object idler : idlers) {
+ boolean keep = false;
+ try {
+ didIdle = true;
+ keep = ((IdleHandler)idler).queueIdle();
+ } catch (Throwable t) {
+ Log.e("MessageQueue",
+ "IdleHandler threw exception", t);
+ RuntimeInit.crash("MessageQueue", t);
+ }
+
+ if (!keep) {
+ synchronized (this) {
+ mIdleHandlers.remove(idler);
+ }
+ }
+ }
+ }
+
+ // While calling an idle handler, a new message could have been
+ // delivered... so go back and look again for a pending message.
+ if (didIdle) {
+ tryIdle = false;
+ continue;
+ }
+
+ synchronized (this) {
+ // No messages, nobody to tell about it... time to wait!
+ try {
+ if (mMessages != null) {
+ if (mMessages.when-now > 0) {
+ Binder.flushPendingCommands();
+ this.wait(mMessages.when-now);
+ }
+ } else {
+ Binder.flushPendingCommands();
+ this.wait();
+ }
+ }
+ catch (InterruptedException e) {
+ }
+ }
+ }
+ }
+
+ final Message pullNextLocked(long now) {
+ Message msg = mMessages;
+ if (msg != null) {
+ if (now >= msg.when) {
+ mMessages = msg.next;
+ if (Config.LOGV) Log.v(
+ "MessageQueue", "Returning message: " + msg);
+ return msg;
+ }
+ }
+
+ return null;
+ }
+
+ final boolean enqueueMessage(Message msg, long when) {
+ if (msg.when != 0) {
+ throw new AndroidRuntimeException(msg
+ + " This message is already in use.");
+ }
+ if (msg.target == null && !mQuitAllowed) {
+ throw new RuntimeException("Main thread not allowed to quit");
+ }
+ synchronized (this) {
+ if (mQuiting) {
+ RuntimeException e = new RuntimeException(
+ msg.target + " sending message to a Handler on a dead thread");
+ Log.w("MessageQueue", e.getMessage(), e);
+ return false;
+ } else if (msg.target == null) {
+ mQuiting = true;
+ }
+
+ msg.when = when;
+ //Log.d("MessageQueue", "Enqueing: " + msg);
+ Message p = mMessages;
+ if (p == null || when == 0 || when < p.when) {
+ msg.next = p;
+ mMessages = msg;
+ this.notify();
+ } else {
+ Message prev = null;
+ while (p != null && p.when <= when) {
+ prev = p;
+ p = p.next;
+ }
+ msg.next = prev.next;
+ prev.next = msg;
+ this.notify();
+ }
+ }
+ return true;
+ }
+
+ final boolean removeMessages(Handler h, int what, Object object,
+ boolean doRemove) {
+ synchronized (this) {
+ Message p = mMessages;
+ boolean found = false;
+
+ // Remove all messages at front.
+ while (p != null && p.target == h && p.what == what
+ && (object == null || p.obj == object)) {
+ if (!doRemove) return true;
+ found = true;
+ Message n = p.next;
+ mMessages = n;
+ p.recycle();
+ p = n;
+ }
+
+ // Remove all messages after front.
+ while (p != null) {
+ Message n = p.next;
+ if (n != null) {
+ if (n.target == h && n.what == what
+ && (object == null || n.obj == object)) {
+ if (!doRemove) return true;
+ found = true;
+ Message nn = n.next;
+ n.recycle();
+ p.next = nn;
+ continue;
+ }
+ }
+ p = n;
+ }
+
+ return found;
+ }
+ }
+
+ final void removeMessages(Handler h, Runnable r, Object object) {
+ if (r == null) {
+ return;
+ }
+
+ synchronized (this) {
+ Message p = mMessages;
+
+ // Remove all messages at front.
+ while (p != null && p.target == h && p.callback == r
+ && (object == null || p.obj == object)) {
+ Message n = p.next;
+ mMessages = n;
+ p.recycle();
+ p = n;
+ }
+
+ // Remove all messages after front.
+ while (p != null) {
+ Message n = p.next;
+ if (n != null) {
+ if (n.target == h && n.callback == r
+ && (object == null || n.obj == object)) {
+ Message nn = n.next;
+ n.recycle();
+ p.next = nn;
+ continue;
+ }
+ }
+ p = n;
+ }
+ }
+ }
+
+ final void removeCallbacksAndMessages(Handler h, Object object) {
+ synchronized (this) {
+ Message p = mMessages;
+
+ // Remove all messages at front.
+ while (p != null && p.target == h
+ && (object == null || p.obj == object)) {
+ Message n = p.next;
+ mMessages = n;
+ p.recycle();
+ p = n;
+ }
+
+ // Remove all messages after front.
+ while (p != null) {
+ Message n = p.next;
+ if (n != null) {
+ if (n.target == h && (object == null || n.obj == object)) {
+ Message nn = n.next;
+ n.recycle();
+ p.next = nn;
+ continue;
+ }
+ }
+ p = n;
+ }
+ }
+ }
+
+ /*
+ private void dumpQueue_l()
+ {
+ Message p = mMessages;
+ System.out.println(this + " queue is:");
+ while (p != null) {
+ System.out.println(" " + p);
+ p = p.next;
+ }
+ }
+ */
+
+ void poke()
+ {
+ synchronized (this) {
+ this.notify();
+ }
+ }
+}
diff --git a/core/java/android/os/Messenger.aidl b/core/java/android/os/Messenger.aidl
new file mode 100644
index 0000000..e6b8886
--- /dev/null
+++ b/core/java/android/os/Messenger.aidl
@@ -0,0 +1,20 @@
+/* //device/java/android/android/content/Intent.aidl
+**
+** Copyright 2007, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.os;
+
+parcelable Messenger;
diff --git a/core/java/android/os/Messenger.java b/core/java/android/os/Messenger.java
new file mode 100644
index 0000000..1bc554e
--- /dev/null
+++ b/core/java/android/os/Messenger.java
@@ -0,0 +1,141 @@
+/*
+ * 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.os;
+
+/**
+ * Reference to a Handler, which others can use to send messages to it.
+ * This allows for the implementation of message-based communication across
+ * processes, by creating a Messenger pointing to a Handler in one process,
+ * and handing that Messenger to another process.
+ */
+public final class Messenger implements Parcelable {
+ private final IMessenger mTarget;
+
+ /**
+ * Create a new Messenger pointing to the given Handler. Any Message
+ * objects sent through this Messenger will appear in the Handler as if
+ * {@link Handler#sendMessage(Message) Handler.sendMessage(Message)} had
+ * be called directly.
+ *
+ * @param target The Handler that will receive sent messages.
+ */
+ public Messenger(Handler target) {
+ mTarget = target.getIMessenger();
+ }
+
+ /**
+ * Send a Message to this Messenger's Handler.
+ *
+ * @param message The Message to send. Usually retrieved through
+ * {@link Message#obtain() Message.obtain()}.
+ *
+ * @throws RemoteException Throws DeadObjectException if the target
+ * Handler no longer exists.
+ */
+ public void send(Message message) throws RemoteException {
+ mTarget.send(message);
+ }
+
+ /**
+ * Retrieve the IBinder that this Messenger is using to communicate with
+ * its associated Handler.
+ *
+ * @return Returns the IBinder backing this Messenger.
+ */
+ public IBinder getBinder() {
+ return mTarget.asBinder();
+ }
+
+ /**
+ * Comparison operator on two Messenger objects, such that true
+ * is returned then they both point to the same Handler.
+ */
+ public boolean equals(Object otherObj) {
+ if (otherObj == null) {
+ return false;
+ }
+ try {
+ return mTarget.asBinder().equals(((Messenger)otherObj)
+ .mTarget.asBinder());
+ } catch (ClassCastException e) {
+ }
+ return false;
+ }
+
+ public int hashCode() {
+ return mTarget.asBinder().hashCode();
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeStrongBinder(mTarget.asBinder());
+ }
+
+ public static final Parcelable.Creator<Messenger> CREATOR
+ = new Parcelable.Creator<Messenger>() {
+ public Messenger createFromParcel(Parcel in) {
+ IBinder target = in.readStrongBinder();
+ return target != null ? new Messenger(target) : null;
+ }
+
+ public Messenger[] newArray(int size) {
+ return new Messenger[size];
+ }
+ };
+
+ /**
+ * Convenience function for writing either a Messenger or null pointer to
+ * a Parcel. You must use this with {@link #readMessengerOrNullFromParcel}
+ * for later reading it.
+ *
+ * @param messenger The Messenger to write, or null.
+ * @param out Where to write the Messenger.
+ */
+ public static void writeMessengerOrNullToParcel(Messenger messenger,
+ Parcel out) {
+ out.writeStrongBinder(messenger != null ? messenger.mTarget.asBinder()
+ : null);
+ }
+
+ /**
+ * Convenience function for reading either a Messenger or null pointer from
+ * a Parcel. You must have previously written the Messenger with
+ * {@link #writeMessengerOrNullToParcel}.
+ *
+ * @param in The Parcel containing the written Messenger.
+ *
+ * @return Returns the Messenger read from the Parcel, or null if null had
+ * been written.
+ */
+ public static Messenger readMessengerOrNullFromParcel(Parcel in) {
+ IBinder b = in.readStrongBinder();
+ return b != null ? new Messenger(b) : null;
+ }
+
+ /**
+ * Create a Messenger from a raw IBinder, which had previously been
+ * retrieved with {@link #getBinder}.
+ *
+ * @param target The IBinder this Messenger should communicate with.
+ */
+ public Messenger(IBinder target) {
+ mTarget = IMessenger.Stub.asInterface(target);
+ }
+}
diff --git a/core/java/android/os/NetStat.java b/core/java/android/os/NetStat.java
new file mode 100644
index 0000000..7312236
--- /dev/null
+++ b/core/java/android/os/NetStat.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os;
+
+/** @hide */
+public class NetStat{
+
+ /**
+ * Get total number of tx packets sent through ppp0
+ *
+ * @return number of Tx packets through ppp0
+ */
+
+ public native static int netStatGetTxPkts();
+
+ /**
+ * Get total number of rx packets received through ppp0
+ *
+ * @return number of Rx packets through ppp0
+ */
+ public native static int netStatGetRxPkts();
+
+ /**
+ * Get total number of tx bytes received through ppp0
+ *
+ * @return number of Tx bytes through ppp0
+ */
+ public native static int netStatGetTxBytes();
+
+ /**
+ * Get total number of rx bytes received through ppp0
+ *
+ * @return number of Rx bytes through ppp0
+ */
+ public native static int netStatGetRxBytes();
+
+}
diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java
new file mode 100644
index 0000000..9a71f6e
--- /dev/null
+++ b/core/java/android/os/Parcel.java
@@ -0,0 +1,2051 @@
+/*
+ * 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.os;
+
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FileDescriptor;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Container for a message (data and object references) that can
+ * be sent through an IBinder. A Parcel can contain both flattened data
+ * that will be unflattened on the other side of the IPC (using the various
+ * methods here for writing specific types, or the general
+ * {@link Parcelable} interface), and references to live {@link IBinder}
+ * objects that will result in the other side receiving a proxy IBinder
+ * connected with the original IBinder in the Parcel.
+ *
+ * <p class="note">Parcel is <strong>not</strong> a general-purpose
+ * serialization mechanism. This class (and the corresponding
+ * {@link Parcelable} API for placing arbitrary objects into a Parcel) is
+ * designed as a high-performance IPC transport. As such, it is not
+ * appropriate to place any Parcel data in to persistent storage: changes
+ * in the underlying implementation of any of the data in the Parcel can
+ * render older data unreadable.</p>
+ *
+ * <p>The bulk of the Parcel API revolves around reading and writing data
+ * of various types. There are six major classes of such functions available.</p>
+ *
+ * <h3>Primitives</h3>
+ *
+ * <p>The most basic data functions are for writing and reading primitive
+ * data types: {@link #writeByte}, {@link #readByte}, {@link #writeDouble},
+ * {@link #readDouble}, {@link #writeFloat}, {@link #readFloat}, {@link #writeInt},
+ * {@link #readInt}, {@link #writeLong}, {@link #readLong},
+ * {@link #writeString}, {@link #readString}. Most other
+ * data operations are built on top of these. The given data is written and
+ * read using the endianess of the host CPU.</p>
+ *
+ * <h3>Primitive Arrays</h3>
+ *
+ * <p>There are a variety of methods for reading and writing raw arrays
+ * of primitive objects, which generally result in writing a 4-byte length
+ * followed by the primitive data items. The methods for reading can either
+ * read the data into an existing array, or create and return a new array.
+ * These available types are:</p>
+ *
+ * <ul>
+ * <li> {@link #writeBooleanArray(boolean[])},
+ * {@link #readBooleanArray(boolean[])}, {@link #createBooleanArray()}
+ * <li> {@link #writeByteArray(byte[])},
+ * {@link #writeByteArray(byte[], int, int)}, {@link #readByteArray(byte[])},
+ * {@link #createByteArray()}
+ * <li> {@link #writeCharArray(char[])}, {@link #readCharArray(char[])},
+ * {@link #createCharArray()}
+ * <li> {@link #writeDoubleArray(double[])}, {@link #readDoubleArray(double[])},
+ * {@link #createDoubleArray()}
+ * <li> {@link #writeFloatArray(float[])}, {@link #readFloatArray(float[])},
+ * {@link #createFloatArray()}
+ * <li> {@link #writeIntArray(int[])}, {@link #readIntArray(int[])},
+ * {@link #createIntArray()}
+ * <li> {@link #writeLongArray(long[])}, {@link #readLongArray(long[])},
+ * {@link #createLongArray()}
+ * <li> {@link #writeStringArray(String[])}, {@link #readStringArray(String[])},
+ * {@link #createStringArray()}.
+ * <li> {@link #writeSparseBooleanArray(SparseBooleanArray)},
+ * {@link #readSparseBooleanArray()}.
+ * </ul>
+ *
+ * <h3>Parcelables</h3>
+ *
+ * <p>The {@link Parcelable} protocol provides an extremely efficient (but
+ * low-level) protocol for objects to write and read themselves from Parcels.
+ * You can use the direct methods {@link #writeParcelable(Parcelable, int)}
+ * and {@link #readParcelable(ClassLoader)} or
+ * {@link #writeParcelableArray} and
+ * {@link #readParcelableArray(ClassLoader)} to write or read. These
+ * methods write both the class type and its data to the Parcel, allowing
+ * that class to be reconstructed from the appropriate class loader when
+ * later reading.</p>
+ *
+ * <p>There are also some methods that provide a more efficient way to work
+ * with Parcelables: {@link #writeTypedArray},
+ * {@link #writeTypedList(List)},
+ * {@link #readTypedArray} and {@link #readTypedList}. These methods
+ * do not write the class information of the original object: instead, the
+ * caller of the read function must know what type to expect and pass in the
+ * appropriate {@link Parcelable.Creator Parcelable.Creator} instead to
+ * properly construct the new object and read its data. (To more efficient
+ * write and read a single Parceable object, you can directly call
+ * {@link Parcelable#writeToParcel Parcelable.writeToParcel} and
+ * {@link Parcelable.Creator#createFromParcel Parcelable.Creator.createFromParcel}
+ * yourself.)</p>
+ *
+ * <h3>Bundles</h3>
+ *
+ * <p>A special type-safe container, called {@link Bundle}, is available
+ * for key/value maps of heterogeneous values. This has many optimizations
+ * for improved performance when reading and writing data, and its type-safe
+ * API avoids difficult to debug type errors when finally marshalling the
+ * data contents into a Parcel. The methods to use are
+ * {@link #writeBundle(Bundle)}, {@link #readBundle()}, and
+ * {@link #readBundle(ClassLoader)}.
+ *
+ * <h3>Active Objects</h3>
+ *
+ * <p>An unusual feature of Parcel is the ability to read and write active
+ * objects. For these objects the actual contents of the object is not
+ * written, rather a special token referencing the object is written. When
+ * reading the object back from the Parcel, you do not get a new instance of
+ * the object, but rather a handle that operates on the exact same object that
+ * was originally written. There are two forms of active objects available.</p>
+ *
+ * <p>{@link Binder} objects are a core facility of Android's general cross-process
+ * communication system. The {@link IBinder} interface describes an abstract
+ * protocol with a Binder object. Any such interface can be written in to
+ * a Parcel, and upon reading you will receive either the original object
+ * implementing that interface or a special proxy implementation
+ * that communicates calls back to the original object. The methods to use are
+ * {@link #writeStrongBinder(IBinder)},
+ * {@link #writeStrongInterface(IInterface)}, {@link #readStrongBinder()},
+ * {@link #writeBinderArray(IBinder[])}, {@link #readBinderArray(IBinder[])},
+ * {@link #createBinderArray()},
+ * {@link #writeBinderList(List)}, {@link #readBinderList(List)},
+ * {@link #createBinderArrayList()}.</p>
+ *
+ * <p>FileDescriptor objects, representing raw Linux file descriptor identifiers,
+ * can be written and {@link ParcelFileDescriptor} objects returned to operate
+ * on the original file descriptor. The returned file descriptor is a dup
+ * of the original file descriptor: the object and fd is different, but
+ * operating on the same underlying file stream, with the same position, etc.
+ * The methods to use are {@link #writeFileDescriptor(FileDescriptor)},
+ * {@link #readFileDescriptor()}.
+ *
+ * <h3>Untyped Containers</h3>
+ *
+ * <p>A final class of methods are for writing and reading standard Java
+ * containers of arbitrary types. These all revolve around the
+ * {@link #writeValue(Object)} and {@link #readValue(ClassLoader)} methods
+ * which define the types of objects allowed. The container methods are
+ * {@link #writeArray(Object[])}, {@link #readArray(ClassLoader)},
+ * {@link #writeList(List)}, {@link #readList(List, ClassLoader)},
+ * {@link #readArrayList(ClassLoader)},
+ * {@link #writeMap(Map)}, {@link #readMap(Map, ClassLoader)},
+ * {@link #writeSparseArray(SparseArray)},
+ * {@link #readSparseArray(ClassLoader)}.
+ */
+public final class Parcel {
+ private static final boolean DEBUG_RECYCLE = false;
+
+ @SuppressWarnings({"UnusedDeclaration"})
+ private int mObject; // used by native code
+ @SuppressWarnings({"UnusedDeclaration"})
+ private int mOwnObject; // used by native code
+ private RuntimeException mStack;
+
+ private static final int POOL_SIZE = 6;
+ private static final Parcel[] sOwnedPool = new Parcel[POOL_SIZE];
+ private static final Parcel[] sHolderPool = new Parcel[POOL_SIZE];
+
+ private static final int VAL_NULL = -1;
+ private static final int VAL_STRING = 0;
+ private static final int VAL_INTEGER = 1;
+ private static final int VAL_MAP = 2;
+ private static final int VAL_BUNDLE = 3;
+ private static final int VAL_PARCELABLE = 4;
+ private static final int VAL_SHORT = 5;
+ private static final int VAL_LONG = 6;
+ private static final int VAL_FLOAT = 7;
+ private static final int VAL_DOUBLE = 8;
+ private static final int VAL_BOOLEAN = 9;
+ private static final int VAL_CHARSEQUENCE = 10;
+ private static final int VAL_LIST = 11;
+ private static final int VAL_SPARSEARRAY = 12;
+ private static final int VAL_BYTEARRAY = 13;
+ private static final int VAL_STRINGARRAY = 14;
+ private static final int VAL_IBINDER = 15;
+ private static final int VAL_PARCELABLEARRAY = 16;
+ private static final int VAL_OBJECTARRAY = 17;
+ private static final int VAL_INTARRAY = 18;
+ private static final int VAL_LONGARRAY = 19;
+ private static final int VAL_BYTE = 20;
+ private static final int VAL_SERIALIZABLE = 21;
+ private static final int VAL_SPARSEBOOLEANARRAY = 22;
+ private static final int VAL_BOOLEANARRAY = 23;
+
+ private static final int EX_SECURITY = -1;
+ private static final int EX_BAD_PARCELABLE = -2;
+ private static final int EX_ILLEGAL_ARGUMENT = -3;
+ private static final int EX_NULL_POINTER = -4;
+ private static final int EX_ILLEGAL_STATE = -5;
+
+ public final static Parcelable.Creator<String> STRING_CREATOR
+ = new Parcelable.Creator<String>() {
+ public String createFromParcel(Parcel source) {
+ return source.readString();
+ }
+ public String[] newArray(int size) {
+ return new String[size];
+ }
+ };
+
+ /**
+ * Retrieve a new Parcel object from the pool.
+ */
+ public static Parcel obtain() {
+ final Parcel[] pool = sOwnedPool;
+ synchronized (pool) {
+ Parcel p;
+ for (int i=0; i<POOL_SIZE; i++) {
+ p = pool[i];
+ if (p != null) {
+ pool[i] = null;
+ if (DEBUG_RECYCLE) {
+ p.mStack = new RuntimeException();
+ }
+ return p;
+ }
+ }
+ }
+ return new Parcel(0);
+ }
+
+ /**
+ * Put a Parcel object back into the pool. You must not touch
+ * the object after this call.
+ */
+ public final void recycle() {
+ if (DEBUG_RECYCLE) mStack = null;
+ freeBuffer();
+ final Parcel[] pool = mOwnObject != 0 ? sOwnedPool : sHolderPool;
+ synchronized (pool) {
+ for (int i=0; i<POOL_SIZE; i++) {
+ if (pool[i] == null) {
+ pool[i] = this;
+ return;
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the total amount of data contained in the parcel.
+ */
+ public final native int dataSize();
+
+ /**
+ * Returns the amount of data remaining to be read from the
+ * parcel. That is, {@link #dataSize}-{@link #dataPosition}.
+ */
+ public final native int dataAvail();
+
+ /**
+ * Returns the current position in the parcel data. Never
+ * more than {@link #dataSize}.
+ */
+ public final native int dataPosition();
+
+ /**
+ * Returns the total amount of space in the parcel. This is always
+ * >= {@link #dataSize}. The difference between it and dataSize() is the
+ * amount of room left until the parcel needs to re-allocate its
+ * data buffer.
+ */
+ public final native int dataCapacity();
+
+ /**
+ * Change the amount of data in the parcel. Can be either smaller or
+ * larger than the current size. If larger than the current capacity,
+ * more memory will be allocated.
+ *
+ * @param size The new number of bytes in the Parcel.
+ */
+ public final native void setDataSize(int size);
+
+ /**
+ * Move the current read/write position in the parcel.
+ * @param pos New offset in the parcel; must be between 0 and
+ * {@link #dataSize}.
+ */
+ public final native void setDataPosition(int pos);
+
+ /**
+ * Change the capacity (current available space) of the parcel.
+ *
+ * @param size The new capacity of the parcel, in bytes. Can not be
+ * less than {@link #dataSize} -- that is, you can not drop existing data
+ * with this method.
+ */
+ public final native void setDataCapacity(int size);
+
+ /**
+ * Returns the raw bytes of the parcel.
+ *
+ * <p class="note">The data you retrieve here <strong>must not</strong>
+ * be placed in any kind of persistent storage (on local disk, across
+ * a network, etc). For that, you should use standard serialization
+ * or another kind of general serialization mechanism. The Parcel
+ * marshalled representation is highly optimized for local IPC, and as
+ * such does not attempt to maintain compatibility with data created
+ * in different versions of the platform.
+ */
+ public final native byte[] marshall();
+
+ /**
+ * Set the bytes in data to be the raw bytes of this Parcel.
+ */
+ public final native void unmarshall(byte[] data, int offest, int length);
+
+ public final native void appendFrom(Parcel parcel, int offset, int length);
+
+ /**
+ * Report whether the parcel contains any marshalled file descriptors.
+ */
+ public final native boolean hasFileDescriptors();
+
+ /**
+ * Store or read an IBinder interface token in the parcel at the current
+ * {@link #dataPosition}. This is used to validate that the marshalled
+ * transaction is intended for the target interface.
+ */
+ public final native void writeInterfaceToken(String interfaceName);
+ public final native void enforceInterface(String interfaceName);
+
+ /**
+ * Write a byte array into the parcel at the current {#link #dataPosition},
+ * growing {@link #dataCapacity} if needed.
+ * @param b Bytes to place into the parcel.
+ */
+ public final void writeByteArray(byte[] b) {
+ writeByteArray(b, 0, (b != null) ? b.length : 0);
+ }
+
+ /**
+ * Write an byte array into the parcel at the current {#link #dataPosition},
+ * growing {@link #dataCapacity} if needed.
+ * @param b Bytes to place into the parcel.
+ * @param offset Index of first byte to be written.
+ * @param len Number of bytes to write.
+ */
+ public final void writeByteArray(byte[] b, int offset, int len) {
+ if (b == null) {
+ writeInt(-1);
+ return;
+ }
+ if (b.length < offset + len || len < 0 || offset < 0) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+ writeNative(b, offset, len);
+ }
+
+ private native void writeNative(byte[] b, int offset, int len);
+
+ /**
+ * Write an integer value into the parcel at the current dataPosition(),
+ * growing dataCapacity() if needed.
+ */
+ public final native void writeInt(int val);
+
+ /**
+ * Write a long integer value into the parcel at the current dataPosition(),
+ * growing dataCapacity() if needed.
+ */
+ public final native void writeLong(long val);
+
+ /**
+ * Write a floating point value into the parcel at the current
+ * dataPosition(), growing dataCapacity() if needed.
+ */
+ public final native void writeFloat(float val);
+
+ /**
+ * Write a double precision floating point value into the parcel at the
+ * current dataPosition(), growing dataCapacity() if needed.
+ */
+ public final native void writeDouble(double val);
+
+ /**
+ * Write a string value into the parcel at the current dataPosition(),
+ * growing dataCapacity() if needed.
+ */
+ public final native void writeString(String val);
+
+ /**
+ * Write an object into the parcel at the current dataPosition(),
+ * growing dataCapacity() if needed.
+ */
+ public final native void writeStrongBinder(IBinder val);
+
+ /**
+ * Write an object into the parcel at the current dataPosition(),
+ * growing dataCapacity() if needed.
+ */
+ public final void writeStrongInterface(IInterface val) {
+ writeStrongBinder(val == null ? null : val.asBinder());
+ }
+
+ /**
+ * Write a FileDescriptor into the parcel at the current dataPosition(),
+ * growing dataCapacity() if needed.
+ */
+ public final native void writeFileDescriptor(FileDescriptor val);
+
+ /**
+ * Write an byte value into the parcel at the current dataPosition(),
+ * growing dataCapacity() if needed.
+ */
+ public final void writeByte(byte val) {
+ writeInt(val);
+ }
+
+ /**
+ * Please use {@link #writeBundle} instead. Flattens a Map into the parcel
+ * at the current dataPosition(),
+ * growing dataCapacity() if needed. The Map keys must be String objects.
+ * The Map values are written using {@link #writeValue} and must follow
+ * the specification there.
+ *
+ * <p>It is strongly recommended to use {@link #writeBundle} instead of
+ * this method, since the Bundle class provides a type-safe API that
+ * allows you to avoid mysterious type errors at the point of marshalling.
+ */
+ public final void writeMap(Map val) {
+ writeMapInternal((Map<String,Object>) val);
+ }
+
+ /**
+ * Flatten a Map into the parcel at the current dataPosition(),
+ * growing dataCapacity() if needed. The Map keys must be String objects.
+ */
+ private void writeMapInternal(Map<String,Object> val) {
+ if (val == null) {
+ writeInt(-1);
+ return;
+ }
+ Set<Map.Entry<String,Object>> entries = val.entrySet();
+ writeInt(entries.size());
+ for (Map.Entry<String,Object> e : entries) {
+ writeValue(e.getKey());
+ writeValue(e.getValue());
+ }
+ }
+
+ /**
+ * Flatten a Bundle into the parcel at the current dataPosition(),
+ * growing dataCapacity() if needed.
+ */
+ public final void writeBundle(Bundle val) {
+ if (val == null) {
+ writeInt(-1);
+ return;
+ }
+
+ if (val.mParcelledData != null) {
+ int length = val.mParcelledData.dataSize();
+ appendFrom(val.mParcelledData, 0, length);
+ } else {
+ writeInt(-1); // dummy, will hold length
+ int oldPos = dataPosition();
+ writeInt(0x4C444E42); // 'B' 'N' 'D' 'L'
+
+ writeMapInternal(val.mMap);
+ int newPos = dataPosition();
+
+ // Backpatch length
+ setDataPosition(oldPos - 4);
+ int length = newPos - oldPos;
+ writeInt(length);
+ setDataPosition(newPos);
+ }
+ }
+
+ /**
+ * Flatten a List into the parcel at the current dataPosition(), growing
+ * dataCapacity() if needed. The List values are written using
+ * {@link #writeValue} and must follow the specification there.
+ */
+ public final void writeList(List val) {
+ if (val == null) {
+ writeInt(-1);
+ return;
+ }
+ int N = val.size();
+ int i=0;
+ writeInt(N);
+ while (i < N) {
+ writeValue(val.get(i));
+ i++;
+ }
+ }
+
+ /**
+ * Flatten an Object array into the parcel at the current dataPosition(),
+ * growing dataCapacity() if needed. The array values are written using
+ * {@link #writeValue} and must follow the specification there.
+ */
+ public final void writeArray(Object[] val) {
+ if (val == null) {
+ writeInt(-1);
+ return;
+ }
+ int N = val.length;
+ int i=0;
+ writeInt(N);
+ while (i < N) {
+ writeValue(val[i]);
+ i++;
+ }
+ }
+
+ /**
+ * Flatten a generic SparseArray into the parcel at the current
+ * dataPosition(), growing dataCapacity() if needed. The SparseArray
+ * values are written using {@link #writeValue} and must follow the
+ * specification there.
+ */
+ public final void writeSparseArray(SparseArray<Object> val) {
+ if (val == null) {
+ writeInt(-1);
+ return;
+ }
+ int N = val.size();
+ writeInt(N);
+ int i=0;
+ while (i < N) {
+ writeInt(val.keyAt(i));
+ writeValue(val.valueAt(i));
+ i++;
+ }
+ }
+
+ public final void writeSparseBooleanArray(SparseBooleanArray val) {
+ if (val == null) {
+ writeInt(-1);
+ return;
+ }
+ int N = val.size();
+ writeInt(N);
+ int i=0;
+ while (i < N) {
+ writeInt(val.keyAt(i));
+ writeByte((byte)(val.valueAt(i) ? 1 : 0));
+ i++;
+ }
+ }
+
+ public final void writeBooleanArray(boolean[] val) {
+ if (val != null) {
+ int N = val.length;
+ writeInt(N);
+ for (int i=0; i<N; i++) {
+ writeInt(val[i] ? 1 : 0);
+ }
+ } else {
+ writeInt(-1);
+ }
+ }
+
+ public final boolean[] createBooleanArray() {
+ int N = readInt();
+ // >>2 as a fast divide-by-4 works in the create*Array() functions
+ // because dataAvail() will never return a negative number. 4 is
+ // the size of a stored boolean in the stream.
+ if (N >= 0 && N <= (dataAvail() >> 2)) {
+ boolean[] val = new boolean[N];
+ for (int i=0; i<N; i++) {
+ val[i] = readInt() != 0;
+ }
+ return val;
+ } else {
+ return null;
+ }
+ }
+
+ public final void readBooleanArray(boolean[] val) {
+ int N = readInt();
+ if (N == val.length) {
+ for (int i=0; i<N; i++) {
+ val[i] = readInt() != 0;
+ }
+ } else {
+ throw new RuntimeException("bad array lengths");
+ }
+ }
+
+ public final void writeCharArray(char[] val) {
+ if (val != null) {
+ int N = val.length;
+ writeInt(N);
+ for (int i=0; i<N; i++) {
+ writeInt((int)val[i]);
+ }
+ } else {
+ writeInt(-1);
+ }
+ }
+
+ public final char[] createCharArray() {
+ int N = readInt();
+ if (N >= 0 && N <= (dataAvail() >> 2)) {
+ char[] val = new char[N];
+ for (int i=0; i<N; i++) {
+ val[i] = (char)readInt();
+ }
+ return val;
+ } else {
+ return null;
+ }
+ }
+
+ public final void readCharArray(char[] val) {
+ int N = readInt();
+ if (N == val.length) {
+ for (int i=0; i<N; i++) {
+ val[i] = (char)readInt();
+ }
+ } else {
+ throw new RuntimeException("bad array lengths");
+ }
+ }
+
+ public final void writeIntArray(int[] val) {
+ if (val != null) {
+ int N = val.length;
+ writeInt(N);
+ for (int i=0; i<N; i++) {
+ writeInt(val[i]);
+ }
+ } else {
+ writeInt(-1);
+ }
+ }
+
+ public final int[] createIntArray() {
+ int N = readInt();
+ if (N >= 0 && N <= (dataAvail() >> 2)) {
+ int[] val = new int[N];
+ for (int i=0; i<N; i++) {
+ val[i] = readInt();
+ }
+ return val;
+ } else {
+ return null;
+ }
+ }
+
+ public final void readIntArray(int[] val) {
+ int N = readInt();
+ if (N == val.length) {
+ for (int i=0; i<N; i++) {
+ val[i] = readInt();
+ }
+ } else {
+ throw new RuntimeException("bad array lengths");
+ }
+ }
+
+ public final void writeLongArray(long[] val) {
+ if (val != null) {
+ int N = val.length;
+ writeInt(N);
+ for (int i=0; i<N; i++) {
+ writeLong(val[i]);
+ }
+ } else {
+ writeInt(-1);
+ }
+ }
+
+ public final long[] createLongArray() {
+ int N = readInt();
+ // >>3 because stored longs are 64 bits
+ if (N >= 0 && N <= (dataAvail() >> 3)) {
+ long[] val = new long[N];
+ for (int i=0; i<N; i++) {
+ val[i] = readLong();
+ }
+ return val;
+ } else {
+ return null;
+ }
+ }
+
+ public final void readLongArray(long[] val) {
+ int N = readInt();
+ if (N == val.length) {
+ for (int i=0; i<N; i++) {
+ val[i] = readLong();
+ }
+ } else {
+ throw new RuntimeException("bad array lengths");
+ }
+ }
+
+ public final void writeFloatArray(float[] val) {
+ if (val != null) {
+ int N = val.length;
+ writeInt(N);
+ for (int i=0; i<N; i++) {
+ writeFloat(val[i]);
+ }
+ } else {
+ writeInt(-1);
+ }
+ }
+
+ public final float[] createFloatArray() {
+ int N = readInt();
+ // >>2 because stored floats are 4 bytes
+ if (N >= 0 && N <= (dataAvail() >> 2)) {
+ float[] val = new float[N];
+ for (int i=0; i<N; i++) {
+ val[i] = readFloat();
+ }
+ return val;
+ } else {
+ return null;
+ }
+ }
+
+ public final void readFloatArray(float[] val) {
+ int N = readInt();
+ if (N == val.length) {
+ for (int i=0; i<N; i++) {
+ val[i] = readFloat();
+ }
+ } else {
+ throw new RuntimeException("bad array lengths");
+ }
+ }
+
+ public final void writeDoubleArray(double[] val) {
+ if (val != null) {
+ int N = val.length;
+ writeInt(N);
+ for (int i=0; i<N; i++) {
+ writeDouble(val[i]);
+ }
+ } else {
+ writeInt(-1);
+ }
+ }
+
+ public final double[] createDoubleArray() {
+ int N = readInt();
+ // >>3 because stored doubles are 8 bytes
+ if (N >= 0 && N <= (dataAvail() >> 3)) {
+ double[] val = new double[N];
+ for (int i=0; i<N; i++) {
+ val[i] = readDouble();
+ }
+ return val;
+ } else {
+ return null;
+ }
+ }
+
+ public final void readDoubleArray(double[] val) {
+ int N = readInt();
+ if (N == val.length) {
+ for (int i=0; i<N; i++) {
+ val[i] = readDouble();
+ }
+ } else {
+ throw new RuntimeException("bad array lengths");
+ }
+ }
+
+ public final void writeStringArray(String[] val) {
+ if (val != null) {
+ int N = val.length;
+ writeInt(N);
+ for (int i=0; i<N; i++) {
+ writeString(val[i]);
+ }
+ } else {
+ writeInt(-1);
+ }
+ }
+
+ public final String[] createStringArray() {
+ int N = readInt();
+ if (N >= 0) {
+ String[] val = new String[N];
+ for (int i=0; i<N; i++) {
+ val[i] = readString();
+ }
+ return val;
+ } else {
+ return null;
+ }
+ }
+
+ public final void readStringArray(String[] val) {
+ int N = readInt();
+ if (N == val.length) {
+ for (int i=0; i<N; i++) {
+ val[i] = readString();
+ }
+ } else {
+ throw new RuntimeException("bad array lengths");
+ }
+ }
+
+ public final void writeBinderArray(IBinder[] val) {
+ if (val != null) {
+ int N = val.length;
+ writeInt(N);
+ for (int i=0; i<N; i++) {
+ writeStrongBinder(val[i]);
+ }
+ } else {
+ writeInt(-1);
+ }
+ }
+
+ public final IBinder[] createBinderArray() {
+ int N = readInt();
+ if (N >= 0) {
+ IBinder[] val = new IBinder[N];
+ for (int i=0; i<N; i++) {
+ val[i] = readStrongBinder();
+ }
+ return val;
+ } else {
+ return null;
+ }
+ }
+
+ public final void readBinderArray(IBinder[] val) {
+ int N = readInt();
+ if (N == val.length) {
+ for (int i=0; i<N; i++) {
+ val[i] = readStrongBinder();
+ }
+ } else {
+ throw new RuntimeException("bad array lengths");
+ }
+ }
+
+ /**
+ * Flatten a List containing a particular object type into the parcel, at
+ * the current dataPosition() and growing dataCapacity() if needed. The
+ * type of the objects in the list must be one that implements Parcelable.
+ * Unlike the generic writeList() method, however, only the raw data of the
+ * objects is written and not their type, so you must use the corresponding
+ * readTypedList() to unmarshall them.
+ *
+ * @param val The list of objects to be written.
+ *
+ * @see #createTypedArrayList
+ * @see #readTypedList
+ * @see Parcelable
+ */
+ public final <T extends Parcelable> void writeTypedList(List<T> val) {
+ if (val == null) {
+ writeInt(-1);
+ return;
+ }
+ int N = val.size();
+ int i=0;
+ writeInt(N);
+ while (i < N) {
+ T item = val.get(i);
+ if (item != null) {
+ writeInt(1);
+ item.writeToParcel(this, 0);
+ } else {
+ writeInt(0);
+ }
+ i++;
+ }
+ }
+
+ /**
+ * Flatten a List containing String objects into the parcel, at
+ * the current dataPosition() and growing dataCapacity() if needed. They
+ * can later be retrieved with {@link #createStringArrayList} or
+ * {@link #readStringList}.
+ *
+ * @param val The list of strings to be written.
+ *
+ * @see #createStringArrayList
+ * @see #readStringList
+ */
+ public final void writeStringList(List<String> val) {
+ if (val == null) {
+ writeInt(-1);
+ return;
+ }
+ int N = val.size();
+ int i=0;
+ writeInt(N);
+ while (i < N) {
+ writeString(val.get(i));
+ i++;
+ }
+ }
+
+ /**
+ * Flatten a List containing IBinder objects into the parcel, at
+ * the current dataPosition() and growing dataCapacity() if needed. They
+ * can later be retrieved with {@link #createBinderArrayList} or
+ * {@link #readBinderList}.
+ *
+ * @param val The list of strings to be written.
+ *
+ * @see #createBinderArrayList
+ * @see #readBinderList
+ */
+ public final void writeBinderList(List<IBinder> val) {
+ if (val == null) {
+ writeInt(-1);
+ return;
+ }
+ int N = val.size();
+ int i=0;
+ writeInt(N);
+ while (i < N) {
+ writeStrongBinder(val.get(i));
+ i++;
+ }
+ }
+
+ /**
+ * Flatten a heterogeneous array containing a particular object type into
+ * the parcel, at
+ * the current dataPosition() and growing dataCapacity() if needed. The
+ * type of the objects in the array must be one that implements Parcelable.
+ * Unlike the {@link #writeParcelableArray} method, however, only the
+ * raw data of the objects is written and not their type, so you must use
+ * {@link #readTypedArray} with the correct corresponding
+ * {@link Parcelable.Creator} implementation to unmarshall them.
+ *
+ * @param val The array of objects to be written.
+ * @param parcelableFlags Contextual flags as per
+ * {@link Parcelable#writeToParcel(Parcel, int) Parcelable.writeToParcel()}.
+ *
+ * @see #readTypedArray
+ * @see #writeParcelableArray
+ * @see Parcelable.Creator
+ */
+ public final <T extends Parcelable> void writeTypedArray(T[] val,
+ int parcelableFlags) {
+ if (val != null) {
+ int N = val.length;
+ writeInt(N);
+ for (int i=0; i<N; i++) {
+ T item = val[i];
+ if (item != null) {
+ writeInt(1);
+ item.writeToParcel(this, parcelableFlags);
+ } else {
+ writeInt(0);
+ }
+ }
+ } else {
+ writeInt(-1);
+ }
+ }
+
+ /**
+ * Flatten a generic object in to a parcel. The given Object value may
+ * currently be one of the following types:
+ *
+ * <ul>
+ * <li> null
+ * <li> String
+ * <li> Byte
+ * <li> Short
+ * <li> Integer
+ * <li> Long
+ * <li> Float
+ * <li> Double
+ * <li> Boolean
+ * <li> String[]
+ * <li> boolean[]
+ * <li> byte[]
+ * <li> int[]
+ * <li> long[]
+ * <li> Object[] (supporting objects of the same type defined here).
+ * <li> {@link Bundle}
+ * <li> Map (as supported by {@link #writeMap}).
+ * <li> Any object that implements the {@link Parcelable} protocol.
+ * <li> Parcelable[]
+ * <li> CharSequence (as supported by {@link TextUtils#writeToParcel}).
+ * <li> List (as supported by {@link #writeList}).
+ * <li> {@link SparseArray} (as supported by {@link #writeSparseArray}).
+ * <li> {@link IBinder}
+ * <li> Any object that implements Serializable (but see
+ * {@link #writeSerializable} for caveats). Note that all of the
+ * previous types have relatively efficient implementations for
+ * writing to a Parcel; having to rely on the generic serialization
+ * approach is much less efficient and should be avoided whenever
+ * possible.
+ * </ul>
+ */
+ public final void writeValue(Object v) {
+ if (v == null) {
+ writeInt(VAL_NULL);
+ } else if (v instanceof String) {
+ writeInt(VAL_STRING);
+ writeString((String) v);
+ } else if (v instanceof Integer) {
+ writeInt(VAL_INTEGER);
+ writeInt((Integer) v);
+ } else if (v instanceof Map) {
+ writeInt(VAL_MAP);
+ writeMap((Map) v);
+ } else if (v instanceof Bundle) {
+ // Must be before Parcelable
+ writeInt(VAL_BUNDLE);
+ writeBundle((Bundle) v);
+ } else if (v instanceof Parcelable) {
+ writeInt(VAL_PARCELABLE);
+ writeParcelable((Parcelable) v, 0);
+ } else if (v instanceof Short) {
+ writeInt(VAL_SHORT);
+ writeInt(((Short) v).intValue());
+ } else if (v instanceof Long) {
+ writeInt(VAL_LONG);
+ writeLong((Long) v);
+ } else if (v instanceof Float) {
+ writeInt(VAL_FLOAT);
+ writeFloat((Float) v);
+ } else if (v instanceof Double) {
+ writeInt(VAL_DOUBLE);
+ writeDouble((Double) v);
+ } else if (v instanceof Boolean) {
+ writeInt(VAL_BOOLEAN);
+ writeInt((Boolean) v ? 1 : 0);
+ } else if (v instanceof CharSequence) {
+ // Must be after String
+ writeInt(VAL_CHARSEQUENCE);
+ TextUtils.writeToParcel((CharSequence) v, this, 0);
+ } else if (v instanceof List) {
+ writeInt(VAL_LIST);
+ writeList((List) v);
+ } else if (v instanceof SparseArray) {
+ writeInt(VAL_SPARSEARRAY);
+ writeSparseArray((SparseArray) v);
+ } else if (v instanceof boolean[]) {
+ writeInt(VAL_BOOLEANARRAY);
+ writeBooleanArray((boolean[]) v);
+ } else if (v instanceof byte[]) {
+ writeInt(VAL_BYTEARRAY);
+ writeByteArray((byte[]) v);
+ } else if (v instanceof String[]) {
+ writeInt(VAL_STRINGARRAY);
+ writeStringArray((String[]) v);
+ } else if (v instanceof IBinder) {
+ writeInt(VAL_IBINDER);
+ writeStrongBinder((IBinder) v);
+ } else if (v instanceof Parcelable[]) {
+ writeInt(VAL_PARCELABLEARRAY);
+ writeParcelableArray((Parcelable[]) v, 0);
+ } else if (v instanceof Object[]) {
+ writeInt(VAL_OBJECTARRAY);
+ writeArray((Object[]) v);
+ } else if (v instanceof int[]) {
+ writeInt(VAL_INTARRAY);
+ writeIntArray((int[]) v);
+ } else if (v instanceof long[]) {
+ writeInt(VAL_LONGARRAY);
+ writeLongArray((long[]) v);
+ } else if (v instanceof Byte) {
+ writeInt(VAL_BYTE);
+ writeInt((Byte) v);
+ } else if (v instanceof Serializable) {
+ // Must be last
+ writeInt(VAL_SERIALIZABLE);
+ writeSerializable((Serializable) v);
+ } else {
+ throw new RuntimeException("Parcel: unable to marshal value " + v);
+ }
+ }
+
+ /**
+ * Flatten the name of the class of the Parcelable and its contents
+ * into the parcel.
+ *
+ * @param p The Parcelable object to be written.
+ * @param parcelableFlags Contextual flags as per
+ * {@link Parcelable#writeToParcel(Parcel, int) Parcelable.writeToParcel()}.
+ */
+ public final void writeParcelable(Parcelable p, int parcelableFlags) {
+ if (p == null) {
+ writeString(null);
+ return;
+ }
+ String name = p.getClass().getName();
+ writeString(name);
+ p.writeToParcel(this, parcelableFlags);
+ }
+
+ /**
+ * Write a generic serializable object in to a Parcel. It is strongly
+ * recommended that this method be avoided, since the serialization
+ * overhead is extremely large, and this approach will be much slower than
+ * using the other approaches to writing data in to a Parcel.
+ */
+ public final void writeSerializable(Serializable s) {
+ if (s == null) {
+ writeString(null);
+ return;
+ }
+ String name = s.getClass().getName();
+ writeString(name);
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try {
+ ObjectOutputStream oos = new ObjectOutputStream(baos);
+ oos.writeObject(s);
+ oos.close();
+
+ writeByteArray(baos.toByteArray());
+ } catch (IOException ioe) {
+ throw new RuntimeException("Parcelable encountered " +
+ "IOException writing serializable object (name = " + name +
+ ")", ioe);
+ }
+ }
+
+ /**
+ * Special function for writing an exception result at the header of
+ * a parcel, to be used when returning an exception from a transaction.
+ * Note that this currently only supports a few exception types; any other
+ * exception will be re-thrown by this function as a RuntimeException
+ * (to be caught by the system's last-resort exception handling when
+ * dispatching a transaction).
+ *
+ * <p>The supported exception types are:
+ * <ul>
+ * <li>{@link BadParcelableException}
+ * <li>{@link IllegalArgumentException}
+ * <li>{@link IllegalStateException}
+ * <li>{@link NullPointerException}
+ * <li>{@link SecurityException}
+ * </ul>
+ *
+ * @param e The Exception to be written.
+ *
+ * @see #writeNoException
+ * @see #readException
+ */
+ public final void writeException(Exception e) {
+ int code = 0;
+ if (e instanceof SecurityException) {
+ code = EX_SECURITY;
+ } else if (e instanceof BadParcelableException) {
+ code = EX_BAD_PARCELABLE;
+ } else if (e instanceof IllegalArgumentException) {
+ code = EX_ILLEGAL_ARGUMENT;
+ } else if (e instanceof NullPointerException) {
+ code = EX_NULL_POINTER;
+ } else if (e instanceof IllegalStateException) {
+ code = EX_ILLEGAL_STATE;
+ }
+ writeInt(code);
+ if (code == 0) {
+ if (e instanceof RuntimeException) {
+ throw (RuntimeException) e;
+ }
+ throw new RuntimeException(e);
+ }
+ writeString(e.getMessage());
+ }
+
+ /**
+ * Special function for writing information at the front of the Parcel
+ * indicating that no exception occurred.
+ *
+ * @see #writeException
+ * @see #readException
+ */
+ public final void writeNoException() {
+ writeInt(0);
+ }
+
+ /**
+ * Special function for reading an exception result from the header of
+ * a parcel, to be used after receiving the result of a transaction. This
+ * will throw the exception for you if it had been written to the Parcel,
+ * otherwise return and let you read the normal result data from the Parcel.
+ *
+ * @see #writeException
+ * @see #writeNoException
+ */
+ public final void readException() {
+ int code = readInt();
+ if (code == 0) return;
+ String msg = readString();
+ readException(code, msg);
+ }
+
+ /**
+ * Use this function for customized exception handling.
+ * customized method call this method for all unknown case
+ * @param code exception code
+ * @param msg exception message
+ */
+ public final void readException(int code, String msg) {
+ switch (code) {
+ case EX_SECURITY:
+ throw new SecurityException(msg);
+ case EX_BAD_PARCELABLE:
+ throw new BadParcelableException(msg);
+ case EX_ILLEGAL_ARGUMENT:
+ throw new IllegalArgumentException(msg);
+ case EX_NULL_POINTER:
+ throw new NullPointerException(msg);
+ case EX_ILLEGAL_STATE:
+ throw new IllegalStateException(msg);
+ }
+ throw new RuntimeException("Unknown exception code: " + code
+ + " msg " + msg);
+ }
+
+ /**
+ * Read an integer value from the parcel at the current dataPosition().
+ */
+ public final native int readInt();
+
+ /**
+ * Read a long integer value from the parcel at the current dataPosition().
+ */
+ public final native long readLong();
+
+ /**
+ * Read a floating point value from the parcel at the current
+ * dataPosition().
+ */
+ public final native float readFloat();
+
+ /**
+ * Read a double precision floating point value from the parcel at the
+ * current dataPosition().
+ */
+ public final native double readDouble();
+
+ /**
+ * Read a string value from the parcel at the current dataPosition().
+ */
+ public final native String readString();
+
+ /**
+ * Read an object from the parcel at the current dataPosition().
+ */
+ public final native IBinder readStrongBinder();
+
+ /**
+ * Read a FileDescriptor from the parcel at the current dataPosition().
+ */
+ public final ParcelFileDescriptor readFileDescriptor() {
+ FileDescriptor fd = internalReadFileDescriptor();
+ return fd != null ? new ParcelFileDescriptor(fd) : null;
+ }
+
+ private native FileDescriptor internalReadFileDescriptor();
+ /*package*/ static native FileDescriptor openFileDescriptor(String file,
+ int mode) throws FileNotFoundException;
+ /*package*/ static native void closeFileDescriptor(FileDescriptor desc)
+ throws IOException;
+
+ /**
+ * Read a byte value from the parcel at the current dataPosition().
+ */
+ public final byte readByte() {
+ return (byte)(readInt() & 0xff);
+ }
+
+ /**
+ * Please use {@link #readBundle(ClassLoader)} instead (whose data must have
+ * been written with {@link #writeBundle}. Read into an existing Map object
+ * from the parcel at the current dataPosition().
+ */
+ public final void readMap(Map outVal, ClassLoader loader) {
+ int N = readInt();
+ readMapInternal(outVal, N, loader);
+ }
+
+ /**
+ * Read into an existing List object from the parcel at the current
+ * dataPosition(), using the given class loader to load any enclosed
+ * Parcelables. If it is null, the default class loader is used.
+ */
+ public final void readList(List outVal, ClassLoader loader) {
+ int N = readInt();
+ readListInternal(outVal, N, loader);
+ }
+
+ /**
+ * Please use {@link #readBundle(ClassLoader)} instead (whose data must have
+ * been written with {@link #writeBundle}. Read and return a new HashMap
+ * object from the parcel at the current dataPosition(), using the given
+ * class loader to load any enclosed Parcelables. Returns null if
+ * the previously written map object was null.
+ */
+ public final HashMap readHashMap(ClassLoader loader)
+ {
+ int N = readInt();
+ if (N < 0) {
+ return null;
+ }
+ HashMap m = new HashMap(N);
+ readMapInternal(m, N, loader);
+ return m;
+ }
+
+ /**
+ * Read and return a new Bundle object from the parcel at the current
+ * dataPosition(). Returns null if the previously written Bundle object was
+ * null.
+ */
+ public final Bundle readBundle() {
+ return readBundle(null);
+ }
+
+ /**
+ * Read and return a new Bundle object from the parcel at the current
+ * dataPosition(), using the given class loader to initialize the class
+ * loader of the Bundle for later retrieval of Parcelable objects.
+ * Returns null if the previously written Bundle object was null.
+ */
+ public final Bundle readBundle(ClassLoader loader) {
+ int offset = dataPosition();
+ int length = readInt();
+ if (length < 0) {
+ return null;
+ }
+ int magic = readInt();
+ if (magic != 0x4C444E42) {
+ //noinspection ThrowableInstanceNeverThrown
+ String st = Log.getStackTraceString(new RuntimeException());
+ Log.e("Bundle", "readBundle: bad magic number");
+ Log.e("Bundle", "readBundle: trace = " + st);
+ }
+
+ // Advance within this Parcel
+ setDataPosition(offset + length + 4);
+
+ Parcel p = new Parcel(0);
+ p.setDataPosition(0);
+ p.appendFrom(this, offset, length + 4);
+ p.setDataPosition(0);
+ final Bundle bundle = new Bundle(p);
+ if (loader != null) {
+ bundle.setClassLoader(loader);
+ }
+ return bundle;
+ }
+
+ /**
+ * Read and return a new Bundle object from the parcel at the current
+ * dataPosition(). Returns null if the previously written Bundle object was
+ * null. The returned bundle will have its contents fully unpacked using
+ * the given ClassLoader.
+ */
+ /* package */ Bundle readBundleUnpacked(ClassLoader loader) {
+ int length = readInt();
+ if (length == -1) {
+ return null;
+ }
+ int magic = readInt();
+ if (magic != 0x4C444E42) {
+ //noinspection ThrowableInstanceNeverThrown
+ String st = Log.getStackTraceString(new RuntimeException());
+ Log.e("Bundle", "readBundleUnpacked: bad magic number");
+ Log.e("Bundle", "readBundleUnpacked: trace = " + st);
+ }
+ Bundle m = new Bundle(loader);
+ int N = readInt();
+ if (N < 0) {
+ return null;
+ }
+ readMapInternal(m.mMap, N, loader);
+ return m;
+ }
+
+ /**
+ * Read and return a byte[] object from the parcel.
+ */
+ public final native byte[] createByteArray();
+
+ /**
+ * Read a byte[] object from the parcel and copy it into the
+ * given byte array.
+ */
+ public final void readByteArray(byte[] val) {
+ // TODO: make this a native method to avoid the extra copy.
+ byte[] ba = createByteArray();
+ if (ba.length == val.length) {
+ System.arraycopy(ba, 0, val, 0, ba.length);
+ } else {
+ throw new RuntimeException("bad array lengths");
+ }
+ }
+
+ /**
+ * Read and return a String[] object from the parcel.
+ * {@hide}
+ */
+ public final String[] readStringArray() {
+ String[] array = null;
+
+ int length = readInt();
+ if (length >= 0)
+ {
+ array = new String[length];
+
+ for (int i = 0 ; i < length ; i++)
+ {
+ array[i] = readString();
+ }
+ }
+
+ return array;
+ }
+
+ /**
+ * Read and return a new ArrayList object from the parcel at the current
+ * dataPosition(). Returns null if the previously written list object was
+ * null. The given class loader will be used to load any enclosed
+ * Parcelables.
+ */
+ public final ArrayList readArrayList(ClassLoader loader) {
+ int N = readInt();
+ if (N < 0) {
+ return null;
+ }
+ ArrayList l = new ArrayList(N);
+ readListInternal(l, N, loader);
+ return l;
+ }
+
+ /**
+ * Read and return a new Object array from the parcel at the current
+ * dataPosition(). Returns null if the previously written array was
+ * null. The given class loader will be used to load any enclosed
+ * Parcelables.
+ */
+ public final Object[] readArray(ClassLoader loader) {
+ int N = readInt();
+ if (N < 0) {
+ return null;
+ }
+ Object[] l = new Object[N];
+ readArrayInternal(l, N, loader);
+ return l;
+ }
+
+ /**
+ * Read and return a new SparseArray object from the parcel at the current
+ * dataPosition(). Returns null if the previously written list object was
+ * null. The given class loader will be used to load any enclosed
+ * Parcelables.
+ */
+ public final SparseArray readSparseArray(ClassLoader loader) {
+ int N = readInt();
+ if (N < 0) {
+ return null;
+ }
+ SparseArray sa = new SparseArray(N);
+ readSparseArrayInternal(sa, N, loader);
+ return sa;
+ }
+
+ /**
+ * Read and return a new SparseBooleanArray object from the parcel at the current
+ * dataPosition(). Returns null if the previously written list object was
+ * null.
+ */
+ public final SparseBooleanArray readSparseBooleanArray() {
+ int N = readInt();
+ if (N < 0) {
+ return null;
+ }
+ SparseBooleanArray sa = new SparseBooleanArray(N);
+ readSparseBooleanArrayInternal(sa, N);
+ return sa;
+ }
+
+ /**
+ * Read and return a new ArrayList containing a particular object type from
+ * the parcel that was written with {@link #writeTypedList} at the
+ * current dataPosition(). Returns null if the
+ * previously written list object was null. The list <em>must</em> have
+ * previously been written via {@link #writeTypedList} with the same object
+ * type.
+ *
+ * @return A newly created ArrayList containing objects with the same data
+ * as those that were previously written.
+ *
+ * @see #writeTypedList
+ */
+ public final <T> ArrayList<T> createTypedArrayList(Parcelable.Creator<T> c) {
+ int N = readInt();
+ if (N < 0) {
+ return null;
+ }
+ ArrayList<T> l = new ArrayList<T>(N);
+ while (N > 0) {
+ if (readInt() != 0) {
+ l.add(c.createFromParcel(this));
+ } else {
+ l.add(null);
+ }
+ N--;
+ }
+ return l;
+ }
+
+ /**
+ * Read into the given List items containing a particular object type
+ * that were written with {@link #writeTypedList} at the
+ * current dataPosition(). The list <em>must</em> have
+ * previously been written via {@link #writeTypedList} with the same object
+ * type.
+ *
+ * @return A newly created ArrayList containing objects with the same data
+ * as those that were previously written.
+ *
+ * @see #writeTypedList
+ */
+ public final <T> void readTypedList(List<T> list, Parcelable.Creator<T> c) {
+ int M = list.size();
+ int N = readInt();
+ int i = 0;
+ for (; i < M && i < N; i++) {
+ if (readInt() != 0) {
+ list.set(i, c.createFromParcel(this));
+ } else {
+ list.set(i, null);
+ }
+ }
+ for (; i<N; i++) {
+ if (readInt() != 0) {
+ list.add(c.createFromParcel(this));
+ } else {
+ list.add(null);
+ }
+ }
+ for (; i<M; i++) {
+ list.remove(N);
+ }
+ }
+
+ /**
+ * Read and return a new ArrayList containing String objects from
+ * the parcel that was written with {@link #writeStringList} at the
+ * current dataPosition(). Returns null if the
+ * previously written list object was null.
+ *
+ * @return A newly created ArrayList containing strings with the same data
+ * as those that were previously written.
+ *
+ * @see #writeStringList
+ */
+ public final ArrayList<String> createStringArrayList() {
+ int N = readInt();
+ if (N < 0) {
+ return null;
+ }
+ ArrayList<String> l = new ArrayList<String>(N);
+ while (N > 0) {
+ l.add(readString());
+ N--;
+ }
+ return l;
+ }
+
+ /**
+ * Read and return a new ArrayList containing IBinder objects from
+ * the parcel that was written with {@link #writeBinderList} at the
+ * current dataPosition(). Returns null if the
+ * previously written list object was null.
+ *
+ * @return A newly created ArrayList containing strings with the same data
+ * as those that were previously written.
+ *
+ * @see #writeBinderList
+ */
+ public final ArrayList<IBinder> createBinderArrayList() {
+ int N = readInt();
+ if (N < 0) {
+ return null;
+ }
+ ArrayList<IBinder> l = new ArrayList<IBinder>(N);
+ while (N > 0) {
+ l.add(readStrongBinder());
+ N--;
+ }
+ return l;
+ }
+
+ /**
+ * Read into the given List items String objects that were written with
+ * {@link #writeStringList} at the current dataPosition().
+ *
+ * @return A newly created ArrayList containing strings with the same data
+ * as those that were previously written.
+ *
+ * @see #writeStringList
+ */
+ public final void readStringList(List<String> list) {
+ int M = list.size();
+ int N = readInt();
+ int i = 0;
+ for (; i < M && i < N; i++) {
+ list.set(i, readString());
+ }
+ for (; i<N; i++) {
+ list.add(readString());
+ }
+ for (; i<M; i++) {
+ list.remove(N);
+ }
+ }
+
+ /**
+ * Read into the given List items IBinder objects that were written with
+ * {@link #writeBinderList} at the current dataPosition().
+ *
+ * @return A newly created ArrayList containing strings with the same data
+ * as those that were previously written.
+ *
+ * @see #writeBinderList
+ */
+ public final void readBinderList(List<IBinder> list) {
+ int M = list.size();
+ int N = readInt();
+ int i = 0;
+ for (; i < M && i < N; i++) {
+ list.set(i, readStrongBinder());
+ }
+ for (; i<N; i++) {
+ list.add(readStrongBinder());
+ }
+ for (; i<M; i++) {
+ list.remove(N);
+ }
+ }
+
+ /**
+ * Read and return a new array containing a particular object type from
+ * the parcel at the current dataPosition(). Returns null if the
+ * previously written array was null. The array <em>must</em> have
+ * previously been written via {@link #writeTypedArray} with the same
+ * object type.
+ *
+ * @return A newly created array containing objects with the same data
+ * as those that were previously written.
+ *
+ * @see #writeTypedArray
+ */
+ public final <T> T[] createTypedArray(Parcelable.Creator<T> c) {
+ int N = readInt();
+ if (N < 0) {
+ return null;
+ }
+ T[] l = c.newArray(N);
+ for (int i=0; i<N; i++) {
+ if (readInt() != 0) {
+ l[i] = c.createFromParcel(this);
+ }
+ }
+ return l;
+ }
+
+ public final <T> void readTypedArray(T[] val, Parcelable.Creator<T> c) {
+ int N = readInt();
+ if (N == val.length) {
+ for (int i=0; i<N; i++) {
+ if (readInt() != 0) {
+ val[i] = c.createFromParcel(this);
+ } else {
+ val[i] = null;
+ }
+ }
+ } else {
+ throw new RuntimeException("bad array lengths");
+ }
+ }
+
+ /**
+ * @deprecated
+ * @hide
+ */
+ @Deprecated
+ public final <T> T[] readTypedArray(Parcelable.Creator<T> c) {
+ return createTypedArray(c);
+ }
+
+ /**
+ * Write a heterogeneous array of Parcelable objects into the Parcel.
+ * Each object in the array is written along with its class name, so
+ * that the correct class can later be instantiated. As a result, this
+ * has significantly more overhead than {@link #writeTypedArray}, but will
+ * correctly handle an array containing more than one type of object.
+ *
+ * @param value The array of objects to be written.
+ * @param parcelableFlags Contextual flags as per
+ * {@link Parcelable#writeToParcel(Parcel, int) Parcelable.writeToParcel()}.
+ *
+ * @see #writeTypedArray
+ */
+ public final <T extends Parcelable> void writeParcelableArray(T[] value,
+ int parcelableFlags) {
+ if (value != null) {
+ int N = value.length;
+ writeInt(N);
+ for (int i=0; i<N; i++) {
+ writeParcelable(value[i], parcelableFlags);
+ }
+ } else {
+ writeInt(-1);
+ }
+ }
+
+ /**
+ * Read a typed object from a parcel. The given class loader will be
+ * used to load any enclosed Parcelables. If it is null, the default class
+ * loader will be used.
+ */
+ public final Object readValue(ClassLoader loader) {
+ int type = readInt();
+
+ switch (type) {
+ case VAL_NULL:
+ return null;
+
+ case VAL_STRING:
+ return readString();
+
+ case VAL_INTEGER:
+ return readInt();
+
+ case VAL_MAP:
+ return readHashMap(loader);
+
+ case VAL_PARCELABLE:
+ return readParcelable(loader);
+
+ case VAL_SHORT:
+ return (short) readInt();
+
+ case VAL_LONG:
+ return readLong();
+
+ case VAL_FLOAT:
+ return readFloat();
+
+ case VAL_DOUBLE:
+ return readDouble();
+
+ case VAL_BOOLEAN:
+ return readInt() == 1;
+
+ case VAL_CHARSEQUENCE:
+ return TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(this);
+
+ case VAL_LIST:
+ return readArrayList(loader);
+
+ case VAL_BOOLEANARRAY:
+ return createBooleanArray();
+
+ case VAL_BYTEARRAY:
+ return createByteArray();
+
+ case VAL_STRINGARRAY:
+ return readStringArray();
+
+ case VAL_IBINDER:
+ return readStrongBinder();
+
+ case VAL_OBJECTARRAY:
+ return readArray(loader);
+
+ case VAL_INTARRAY:
+ return createIntArray();
+
+ case VAL_LONGARRAY:
+ return createLongArray();
+
+ case VAL_BYTE:
+ return readByte();
+
+ case VAL_SERIALIZABLE:
+ return readSerializable();
+
+ case VAL_PARCELABLEARRAY:
+ return readParcelableArray(loader);
+
+ case VAL_SPARSEARRAY:
+ return readSparseArray(loader);
+
+ case VAL_SPARSEBOOLEANARRAY:
+ return readSparseBooleanArray();
+
+ case VAL_BUNDLE:
+ return readBundle(loader); // loading will be deferred
+
+ default:
+ int off = dataPosition() - 4;
+ throw new RuntimeException(
+ "Parcel " + this + ": Unmarshalling unknown type code " + type + " at offset " + off);
+ }
+ }
+
+ /**
+ * Read and return a new Parcelable from the parcel. The given class loader
+ * will be used to load any enclosed Parcelables. If it is null, the default
+ * class loader will be used.
+ * @param loader A ClassLoader from which to instantiate the Parcelable
+ * object, or null for the default class loader.
+ * @return Returns the newly created Parcelable, or null if a null
+ * object has been written.
+ * @throws BadParcelableException Throws BadParcelableException if there
+ * was an error trying to instantiate the Parcelable.
+ */
+ public final <T extends Parcelable> T readParcelable(ClassLoader loader) {
+ String name = readString();
+ if (name == null) {
+ return null;
+ }
+ Parcelable.Creator<T> creator;
+ synchronized (mCreators) {
+ HashMap<String,Parcelable.Creator> map = mCreators.get(loader);
+ if (map == null) {
+ map = new HashMap<String,Parcelable.Creator>();
+ mCreators.put(loader, map);
+ }
+ creator = map.get(name);
+ if (creator == null) {
+ try {
+ Class c = loader == null ?
+ Class.forName(name) : Class.forName(name, true, loader);
+ Field f = c.getField("CREATOR");
+ creator = (Parcelable.Creator)f.get(null);
+ }
+ catch (IllegalAccessException e) {
+ Log.e("Parcel", "Class not found when unmarshalling: "
+ + name + ", e: " + e);
+ throw new BadParcelableException(
+ "IllegalAccessException when unmarshalling: " + name);
+ }
+ catch (ClassNotFoundException e) {
+ Log.e("Parcel", "Class not found when unmarshalling: "
+ + name + ", e: " + e);
+ throw new BadParcelableException(
+ "ClassNotFoundException when unmarshalling: " + name);
+ }
+ catch (ClassCastException e) {
+ throw new BadParcelableException("Parcelable protocol requires a "
+ + "Parcelable.Creator object called "
+ + " CREATOR on class " + name);
+ }
+ catch (NoSuchFieldException e) {
+ throw new BadParcelableException("Parcelable protocol requires a "
+ + "Parcelable.Creator object called "
+ + " CREATOR on class " + name);
+ }
+ if (creator == null) {
+ throw new BadParcelableException("Parcelable protocol requires a "
+ + "Parcelable.Creator object called "
+ + " CREATOR on class " + name);
+ }
+
+ map.put(name, creator);
+ }
+ }
+
+ return creator.createFromParcel(this);
+ }
+
+ /**
+ * Read and return a new Parcelable array from the parcel.
+ * The given class loader will be used to load any enclosed
+ * Parcelables.
+ * @return the Parcelable array, or null if the array is null
+ */
+ public final Parcelable[] readParcelableArray(ClassLoader loader) {
+ int N = readInt();
+ if (N < 0) {
+ return null;
+ }
+ Parcelable[] p = new Parcelable[N];
+ for (int i = 0; i < N; i++) {
+ p[i] = (Parcelable) readParcelable(loader);
+ }
+ return p;
+ }
+
+ /**
+ * Read and return a new Serializable object from the parcel.
+ * @return the Serializable object, or null if the Serializable name
+ * wasn't found in the parcel.
+ */
+ public final Serializable readSerializable() {
+ String name = readString();
+ if (name == null) {
+ // For some reason we were unable to read the name of the Serializable (either there
+ // is nothing left in the Parcel to read, or the next value wasn't a String), so
+ // return null, which indicates that the name wasn't found in the parcel.
+ return null;
+ }
+
+ byte[] serializedData = createByteArray();
+ ByteArrayInputStream bais = new ByteArrayInputStream(serializedData);
+ try {
+ ObjectInputStream ois = new ObjectInputStream(bais);
+ return (Serializable) ois.readObject();
+ } catch (IOException ioe) {
+ throw new RuntimeException("Parcelable encountered " +
+ "IOException reading a Serializable object (name = " + name +
+ ")", ioe);
+ } catch (ClassNotFoundException cnfe) {
+ throw new RuntimeException("Parcelable encountered" +
+ "ClassNotFoundException reading a Serializable object (name = "
+ + name + ")", cnfe);
+ }
+ }
+
+ // Cache of previously looked up CREATOR.createFromParcel() methods for
+ // particular classes. Keys are the names of the classes, values are
+ // Method objects.
+ private static final HashMap<ClassLoader,HashMap<String,Parcelable.Creator>>
+ mCreators = new HashMap<ClassLoader,HashMap<String,Parcelable.Creator>>();
+
+ static protected final Parcel obtain(int obj) {
+ final Parcel[] pool = sHolderPool;
+ synchronized (pool) {
+ Parcel p;
+ for (int i=0; i<POOL_SIZE; i++) {
+ p = pool[i];
+ if (p != null) {
+ pool[i] = null;
+ if (DEBUG_RECYCLE) {
+ p.mStack = new RuntimeException();
+ }
+ p.init(obj);
+ return p;
+ }
+ }
+ }
+ return new Parcel(obj);
+ }
+
+ private Parcel(int obj) {
+ if (DEBUG_RECYCLE) {
+ mStack = new RuntimeException();
+ }
+ //Log.i("Parcel", "Initializing obj=0x" + Integer.toHexString(obj), mStack);
+ init(obj);
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ if (DEBUG_RECYCLE) {
+ if (mStack != null) {
+ Log.w("Parcel", "Client did not call Parcel.recycle()", mStack);
+ }
+ }
+ destroy();
+ }
+
+ private native void freeBuffer();
+ private native void init(int obj);
+ private native void destroy();
+
+ private void readMapInternal(Map outVal, int N,
+ ClassLoader loader) {
+ while (N > 0) {
+ Object key = readValue(loader);
+ Object value = readValue(loader);
+ outVal.put(key, value);
+ N--;
+ }
+ }
+
+ private void readListInternal(List outVal, int N,
+ ClassLoader loader) {
+ while (N > 0) {
+ Object value = readValue(loader);
+ //Log.d("Parcel", "Unmarshalling value=" + value);
+ outVal.add(value);
+ N--;
+ }
+ }
+
+ private void readArrayInternal(Object[] outVal, int N,
+ ClassLoader loader) {
+ for (int i = 0; i < N; i++) {
+ Object value = readValue(loader);
+ //Log.d("Parcel", "Unmarshalling value=" + value);
+ outVal[i] = value;
+ }
+ }
+
+ private void readSparseArrayInternal(SparseArray outVal, int N,
+ ClassLoader loader) {
+ while (N > 0) {
+ int key = readInt();
+ Object value = readValue(loader);
+ //Log.i("Parcel", "Unmarshalling key=" + key + " value=" + value);
+ outVal.append(key, value);
+ N--;
+ }
+ }
+
+
+ private void readSparseBooleanArrayInternal(SparseBooleanArray outVal, int N) {
+ while (N > 0) {
+ int key = readInt();
+ boolean value = this.readByte() == 1;
+ //Log.i("Parcel", "Unmarshalling key=" + key + " value=" + value);
+ outVal.append(key, value);
+ N--;
+ }
+ }
+}
diff --git a/core/java/android/os/ParcelFileDescriptor.aidl b/core/java/android/os/ParcelFileDescriptor.aidl
new file mode 100644
index 0000000..5857aae
--- /dev/null
+++ b/core/java/android/os/ParcelFileDescriptor.aidl
@@ -0,0 +1,20 @@
+/* //device/java/android/android/os/ParcelFileDescriptor.aidl
+**
+** Copyright 2007, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.os;
+
+parcelable ParcelFileDescriptor;
diff --git a/core/java/android/os/ParcelFileDescriptor.java b/core/java/android/os/ParcelFileDescriptor.java
new file mode 100644
index 0000000..ed138cb
--- /dev/null
+++ b/core/java/android/os/ParcelFileDescriptor.java
@@ -0,0 +1,250 @@
+/*
+ * 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.os;
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.net.Socket;
+
+/**
+ * The FileDescriptor returned by {@link Parcel#readFileDescriptor}, allowing
+ * you to close it when done with it.
+ */
+public class ParcelFileDescriptor implements Parcelable {
+ private final FileDescriptor mFileDescriptor;
+ private boolean mClosed;
+ //this field is to create wrapper for ParcelFileDescriptor using another
+ //PartialFileDescriptor but avoid invoking close twice
+ //consider ParcelFileDescriptor A(fileDescriptor fd), ParcelFileDescriptor B(A)
+ //in this particular case fd.close might be invoked twice.
+ private final ParcelFileDescriptor mParcelDescriptor;
+
+ /**
+ * For use with {@link #open}: if {@link #MODE_CREATE} has been supplied
+ * and this file doesn't already exist, then create the file with
+ * permissions such that any application can read it.
+ */
+ public static final int MODE_WORLD_READABLE = 0x00000001;
+
+ /**
+ * For use with {@link #open}: if {@link #MODE_CREATE} has been supplied
+ * and this file doesn't already exist, then create the file with
+ * permissions such that any application can write it.
+ */
+ public static final int MODE_WORLD_WRITEABLE = 0x00000002;
+
+ /**
+ * For use with {@link #open}: open the file with read-only access.
+ */
+ public static final int MODE_READ_ONLY = 0x10000000;
+
+ /**
+ * For use with {@link #open}: open the file with write-only access.
+ */
+ public static final int MODE_WRITE_ONLY = 0x20000000;
+
+ /**
+ * For use with {@link #open}: open the file with read and write access.
+ */
+ public static final int MODE_READ_WRITE = 0x30000000;
+
+ /**
+ * For use with {@link #open}: create the file if it doesn't already exist.
+ */
+ public static final int MODE_CREATE = 0x08000000;
+
+ /**
+ * For use with {@link #open}: erase contents of file when opening.
+ */
+ public static final int MODE_TRUNCATE = 0x04000000;
+
+ /**
+ * Create a new ParcelFileDescriptor accessing a given file.
+ *
+ * @param file The file to be opened.
+ * @param mode The desired access mode, must be one of
+ * {@link #MODE_READ_ONLY}, {@link #MODE_WRITE_ONLY}, or
+ * {@link #MODE_READ_WRITE}; may also be any combination of
+ * {@link #MODE_CREATE}, {@link #MODE_TRUNCATE},
+ * {@link #MODE_WORLD_READABLE}, and {@link #MODE_WORLD_WRITEABLE}.
+ *
+ * @return Returns a new ParcelFileDescriptor pointing to the given
+ * file.
+ *
+ * @throws FileNotFoundException Throws FileNotFoundException if the given
+ * file does not exist or can not be opened with the requested mode.
+ */
+ public static ParcelFileDescriptor open(File file, int mode)
+ throws FileNotFoundException {
+ String path = file.getPath();
+ SecurityManager security = System.getSecurityManager();
+ if (security != null) {
+ security.checkRead(path);
+ if ((mode&MODE_WRITE_ONLY) != 0) {
+ security.checkWrite(path);
+ }
+ }
+
+ if ((mode&MODE_READ_WRITE) == 0) {
+ throw new IllegalArgumentException(
+ "Must specify MODE_READ_ONLY, MODE_WRITE_ONLY, or MODE_READ_WRITE");
+ }
+
+ FileDescriptor fd = Parcel.openFileDescriptor(path, mode);
+ return new ParcelFileDescriptor(fd);
+ }
+
+ /**
+ * Create a new ParcelFileDescriptor from the specified Socket.
+ *
+ * @param socket The Socket whose FileDescriptor is used to create
+ * a new ParcelFileDescriptor.
+ *
+ * @return A new ParcelFileDescriptor with the FileDescriptor of the
+ * specified Socket.
+ */
+ public static ParcelFileDescriptor fromSocket(Socket socket) {
+ FileDescriptor fd = getFileDescriptorFromSocket(socket);
+ return new ParcelFileDescriptor(fd);
+ }
+
+ // Extracts the file descriptor from the specified socket and returns it untouched
+ private static native FileDescriptor getFileDescriptorFromSocket(Socket socket);
+
+ /**
+ * Retrieve the actual FileDescriptor associated with this object.
+ *
+ * @return Returns the FileDescriptor associated with this object.
+ */
+ public FileDescriptor getFileDescriptor() {
+ return mFileDescriptor;
+ }
+
+ /**
+ * Close the ParcelFileDescriptor. This implementation closes the underlying
+ * OS resources allocated to represent this stream.
+ *
+ * @throws IOException
+ * If an error occurs attempting to close this ParcelFileDescriptor.
+ */
+ public void close() throws IOException {
+ mClosed = true;
+ if (mParcelDescriptor != null) {
+ // If this is a proxy to another file descriptor, just call through to its
+ // close method.
+ mParcelDescriptor.close();
+ } else {
+ Parcel.closeFileDescriptor(mFileDescriptor);
+ }
+ }
+
+ /**
+ * An InputStream you can create on a ParcelFileDescriptor, which will
+ * take care of calling {@link ParcelFileDescriptor#close
+ * ParcelFileDescritor.close()} for you when the stream is closed.
+ */
+ public static class AutoCloseInputStream extends FileInputStream {
+ private final ParcelFileDescriptor mFd;
+
+ public AutoCloseInputStream(ParcelFileDescriptor fd) {
+ super(fd.getFileDescriptor());
+ mFd = fd;
+ }
+
+ @Override
+ public void close() throws IOException {
+ mFd.close();
+ }
+ }
+
+ /**
+ * An OutputStream you can create on a ParcelFileDescriptor, which will
+ * take care of calling {@link ParcelFileDescriptor#close
+ * ParcelFileDescritor.close()} for you when the stream is closed.
+ */
+ public static class AutoCloseOutputStream extends FileOutputStream {
+ private final ParcelFileDescriptor mFd;
+
+ public AutoCloseOutputStream(ParcelFileDescriptor fd) {
+ super(fd.getFileDescriptor());
+ mFd = fd;
+ }
+
+ @Override
+ public void close() throws IOException {
+ mFd.close();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "{ParcelFileDescriptor: " + mFileDescriptor + "}";
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ if (!mClosed) {
+ close();
+ }
+ } finally {
+ super.finalize();
+ }
+ }
+
+ public ParcelFileDescriptor(ParcelFileDescriptor descriptor) {
+ super();
+ mParcelDescriptor = descriptor;
+ mFileDescriptor = mParcelDescriptor.mFileDescriptor;
+ }
+
+ /*package */ParcelFileDescriptor(FileDescriptor descriptor) {
+ super();
+ mFileDescriptor = descriptor;
+ mParcelDescriptor = null;
+ }
+
+ /* Parcelable interface */
+ public int describeContents() {
+ return Parcelable.CONTENTS_FILE_DESCRIPTOR;
+ }
+
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeFileDescriptor(mFileDescriptor);
+ if ((flags&PARCELABLE_WRITE_RETURN_VALUE) != 0 && !mClosed) {
+ try {
+ close();
+ } catch (IOException e) {
+ // Empty
+ }
+ }
+ }
+
+ public static final Parcelable.Creator<ParcelFileDescriptor> CREATOR
+ = new Parcelable.Creator<ParcelFileDescriptor>() {
+ public ParcelFileDescriptor createFromParcel(Parcel in) {
+ return in.readFileDescriptor();
+ }
+ public ParcelFileDescriptor[] newArray(int size) {
+ return new ParcelFileDescriptor[size];
+ }
+ };
+
+}
diff --git a/core/java/android/os/ParcelFormatException.java b/core/java/android/os/ParcelFormatException.java
new file mode 100644
index 0000000..8b6fda0
--- /dev/null
+++ b/core/java/android/os/ParcelFormatException.java
@@ -0,0 +1,31 @@
+/*
+ * 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.os;
+
+/**
+ * The contents of a Parcel (usually during unmarshalling) does not
+ * contain the expected data.
+ */
+public class ParcelFormatException extends RuntimeException {
+ public ParcelFormatException() {
+ super();
+ }
+
+ public ParcelFormatException(String reason) {
+ super(reason);
+ }
+}
diff --git a/core/java/android/os/Parcelable.java b/core/java/android/os/Parcelable.java
new file mode 100644
index 0000000..aee1e0b
--- /dev/null
+++ b/core/java/android/os/Parcelable.java
@@ -0,0 +1,112 @@
+/*
+ * 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.os;
+
+/**
+ * Interface for classes whose instances can be written to
+ * and restored from a {@link Parcel}. Classes implementing the Parcelable
+ * interface must also have a static field called <code>CREATOR</code>, which
+ * is an object implementing the {@link Parcelable.Creator Parcelable.Creator}
+ * interface.
+ *
+ * <p>A typical implementation of Parcelable is:</p>
+ *
+ * <pre>
+ * public class MyParcelable implements Parcelable {
+ * private int mData;
+ *
+ * public void writeToParcel(Parcel out, int flags) {
+ * out.writeInt(mData);
+ * }
+ *
+ * public static final Parcelable.Creator<MyParcelable> CREATOR
+ * = new Parcelable.Creator<MyParcelable>() {
+ * public MyParcelable createFromParcel(Parcel in) {
+ * return new MyParcelable(in);
+ * }
+ *
+ * public MyParcelable[] newArray(int size) {
+ * return new MyParcelable[size];
+ * }
+ * }
+ *
+ * private MyParcelable(Parcel in) {
+ * mData = in.readInt();
+ * }
+ * }</pre>
+ */
+public interface Parcelable {
+ /**
+ * Flag for use with {@link #writeToParcel}: the object being written
+ * is a return value, that is the result of a function such as
+ * "<code>Parcelable someFunction()</code>",
+ * "<code>void someFunction(out Parcelable)</code>", or
+ * "<code>void someFunction(inout Parcelable)</code>". Some implementations
+ * may want to release resources at this point.
+ */
+ public static final int PARCELABLE_WRITE_RETURN_VALUE = 0x0001;
+
+ /**
+ * Bit masks for use with {@link #describeContents}: each bit represents a
+ * kind of object considered to have potential special significance when
+ * marshalled.
+ */
+ public static final int CONTENTS_FILE_DESCRIPTOR = 0x0001;
+
+ /**
+ * Describe the kinds of special objects contained in this Parcelable's
+ * marshalled representation.
+ *
+ * @return a bitmask indicating the set of special object types marshalled
+ * by the Parcelable.
+ */
+ public int describeContents();
+
+ /**
+ * Flatten this object in to a Parcel.
+ *
+ * @param dest The Parcel in which the object should be written.
+ * @param flags Additional flags about how the object should be written.
+ * May be 0 or {@link #PARCELABLE_WRITE_RETURN_VALUE}.
+ */
+ public void writeToParcel(Parcel dest, int flags);
+
+ /**
+ * Interface that must be implemented and provided as a public CREATOR
+ * field that generates instances of your Parcelable class from a Parcel.
+ */
+ public interface Creator<T> {
+ /**
+ * Create a new instance of the Parcelable class, instantiating it
+ * from the given Parcel whose data had previously been written by
+ * {@link Parcelable#writeToParcel Parcelable.writeToParcel()}.
+ *
+ * @param source The Parcel to read the object's data from.
+ * @return Returns a new instance of the Parcelable class.
+ */
+ public T createFromParcel(Parcel source);
+
+ /**
+ * Create a new array of the Parcelable class.
+ *
+ * @param size Size of the array.
+ * @return Returns an array of the Parcelable class, with every entry
+ * initialized to null.
+ */
+ public T[] newArray(int size);
+ }
+}
diff --git a/core/java/android/os/PatternMatcher.aidl b/core/java/android/os/PatternMatcher.aidl
new file mode 100644
index 0000000..86309f1
--- /dev/null
+++ b/core/java/android/os/PatternMatcher.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os;
+
+parcelable PatternMatcher;
diff --git a/core/java/android/os/PatternMatcher.java b/core/java/android/os/PatternMatcher.java
new file mode 100644
index 0000000..56dc837
--- /dev/null
+++ b/core/java/android/os/PatternMatcher.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os;
+
+/**
+ * A simple pattern matcher, which is safe to use on untrusted data: it does
+ * not provide full reg-exp support, only simple globbing that can not be
+ * used maliciously.
+ */
+public class PatternMatcher implements Parcelable {
+ /**
+ * Pattern type: the given pattern must exactly match the string it is
+ * tested against.
+ */
+ public static final int PATTERN_LITERAL = 0;
+
+ /**
+ * Pattern type: the given pattern must match the
+ * beginning of the string it is tested against.
+ */
+ public static final int PATTERN_PREFIX = 1;
+
+ /**
+ * Pattern type: the given pattern is interpreted with a
+ * simple glob syntax for matching against the string it is tested against.
+ * In this syntax, you can use the '*' character to match against zero or
+ * more occurrences of the character immediately before. If the
+ * character before it is '.' it will match any character. The character
+ * '\' can be used as an escape. This essentially provides only the '*'
+ * wildcard part of a normal regexp.
+ */
+ public static final int PATTERN_SIMPLE_GLOB = 2;
+
+ private final String mPattern;
+ private final int mType;
+
+ public PatternMatcher(String pattern, int type) {
+ mPattern = pattern;
+ mType = type;
+ }
+
+ public final String getPath() {
+ return mPattern;
+ }
+
+ public final int getType() {
+ return mType;
+ }
+
+ public boolean match(String str) {
+ return matchPattern(mPattern, str, mType);
+ }
+
+ public String toString() {
+ String type = "? ";
+ switch (mType) {
+ case PATTERN_LITERAL:
+ type = "LITERAL: ";
+ break;
+ case PATTERN_PREFIX:
+ type = "PREFIX: ";
+ break;
+ case PATTERN_SIMPLE_GLOB:
+ type = "GLOB: ";
+ break;
+ }
+ return "PatternMatcher{" + type + mPattern + "}";
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mPattern);
+ dest.writeInt(mType);
+ }
+
+ public PatternMatcher(Parcel src) {
+ mPattern = src.readString();
+ mType = src.readInt();
+ }
+
+ public static final Parcelable.Creator<PatternMatcher> CREATOR
+ = new Parcelable.Creator<PatternMatcher>() {
+ public PatternMatcher createFromParcel(Parcel source) {
+ return new PatternMatcher(source);
+ }
+
+ public PatternMatcher[] newArray(int size) {
+ return new PatternMatcher[size];
+ }
+ };
+
+ static boolean matchPattern(String pattern, String match, int type) {
+ if (match == null) return false;
+ if (type == PATTERN_LITERAL) {
+ return pattern.equals(match);
+ } if (type == PATTERN_PREFIX) {
+ return match.startsWith(pattern);
+ } else if (type != PATTERN_SIMPLE_GLOB) {
+ return false;
+ }
+
+ final int NP = pattern.length();
+ if (NP <= 0) {
+ return match.length() <= 0;
+ }
+ final int NM = match.length();
+ int ip = 0, im = 0;
+ char nextChar = pattern.charAt(0);
+ while ((ip<NP) && (im<NM)) {
+ char c = nextChar;
+ ip++;
+ nextChar = ip < NP ? pattern.charAt(ip) : 0;
+ final boolean escaped = (c == '\\');
+ if (escaped) {
+ c = nextChar;
+ ip++;
+ nextChar = ip < NP ? pattern.charAt(ip) : 0;
+ }
+ if (nextChar == '*') {
+ if (!escaped && c == '.') {
+ if (ip >= (NP-1)) {
+ // at the end with a pattern match, so
+ // all is good without checking!
+ return true;
+ }
+ ip++;
+ nextChar = pattern.charAt(ip);
+ // Consume everything until the next character in the
+ // pattern is found.
+ if (nextChar == '\\') {
+ ip++;
+ nextChar = ip < NP ? pattern.charAt(ip) : 0;
+ }
+ do {
+ if (match.charAt(im) == nextChar) {
+ break;
+ }
+ im++;
+ } while (im < NM);
+ if (im == NM) {
+ // Whoops, the next character in the pattern didn't
+ // exist in the match.
+ return false;
+ }
+ ip++;
+ nextChar = ip < NP ? pattern.charAt(ip) : 0;
+ im++;
+ } else {
+ // Consume only characters matching the one before '*'.
+ do {
+ if (match.charAt(im) != c) {
+ break;
+ }
+ im++;
+ } while (im < NM);
+ ip++;
+ nextChar = ip < NP ? pattern.charAt(ip) : 0;
+ }
+ } else {
+ if (c != '.' && match.charAt(im) != c) return false;
+ im++;
+ }
+ }
+
+ if (ip >= NP && im >= NM) {
+ // Reached the end of both strings, all is good!
+ return true;
+ }
+
+ // One last check: we may have finished the match string, but still
+ // have a '.*' at the end of the pattern, which should still count
+ // as a match.
+ if (ip == NP-2 && pattern.charAt(ip) == '.'
+ && pattern.charAt(ip+1) == '*') {
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/core/java/android/os/Power.java b/core/java/android/os/Power.java
new file mode 100644
index 0000000..0794e6d
--- /dev/null
+++ b/core/java/android/os/Power.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os;
+
+/**
+ * Class that provides access to some of the power management functions.
+ *
+ * {@hide}
+ */
+public class Power
+{
+ // can't instantiate this class
+ private Power()
+ {
+ }
+
+ /**
+ * Wake lock that ensures that the CPU is running. The screen might
+ * not be on.
+ */
+ public static final int PARTIAL_WAKE_LOCK = 1;
+
+ /**
+ * Wake lock that ensures that the screen is on.
+ */
+ public static final int FULL_WAKE_LOCK = 2;
+
+ public static native void acquireWakeLock(int lock, String id);
+ public static native void releaseWakeLock(String id);
+
+ /**
+ * Flag to turn on and off the keyboard light.
+ */
+ public static final int KEYBOARD_LIGHT = 0x00000001;
+
+ /**
+ * Flag to turn on and off the screen backlight.
+ */
+ public static final int SCREEN_LIGHT = 0x00000002;
+
+ /**
+ * Flag to turn on and off the button backlight.
+ */
+ public static final int BUTTON_LIGHT = 0x00000004;
+
+ /**
+ * Flags to turn on and off all the backlights.
+ */
+ public static final int ALL_LIGHTS = (KEYBOARD_LIGHT|SCREEN_LIGHT|BUTTON_LIGHT);
+
+ /**
+ * Brightness value for fully off
+ */
+ public static final int BRIGHTNESS_OFF = 0;
+
+ /**
+ * Brightness value for dim backlight
+ */
+ public static final int BRIGHTNESS_DIM = 20;
+
+ /**
+ * Brightness value for fully on
+ */
+ public static final int BRIGHTNESS_ON = 255;
+
+ /**
+ * Brightness value to use when battery is low
+ */
+ public static final int BRIGHTNESS_LOW_BATTERY = 10;
+
+ /**
+ * Threshold for BRIGHTNESS_LOW_BATTERY (percentage)
+ * Screen will stay dim if battery level is <= LOW_BATTERY_THRESHOLD
+ */
+ public static final int LOW_BATTERY_THRESHOLD = 10;
+
+ /**
+ * Set the brightness for one or more lights
+ *
+ * @param mask flags indicating which lights to change brightness
+ * @param brightness new brightness value (0 = off, 255 = fully bright)
+ */
+ public static native int setLightBrightness(int mask, int brightness);
+
+ /**
+ * Turn the screen on or off
+ *
+ * @param on Whether you want the screen on or off
+ */
+ public static native int setScreenState(boolean on);
+
+ public static native int setLastUserActivityTimeout(long ms);
+
+ /**
+ * Turn the device off.
+ *
+ * This method is considered deprecated in favor of
+ * {@link android.policy.ShutdownThread.shutdownAfterDisablingRadio()}.
+ *
+ * @deprecated
+ * @hide
+ */
+ @Deprecated
+ public static native void shutdown();
+
+ /**
+ * Reboot the device.
+ * @param reason code to pass to the kernel (e.g. "recovery"), or null.
+ */
+ public static native void reboot(String reason);
+}
+
diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java
new file mode 100644
index 0000000..bfcf2fc
--- /dev/null
+++ b/core/java/android/os/PowerManager.java
@@ -0,0 +1,392 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os;
+
+import android.util.Log;
+
+import com.android.internal.os.RuntimeInit;
+
+/**
+ * This class gives you control of the power state of the device.
+ *
+ * <p><b>Device battery life will be significantly affected by the use of this API.</b> Do not
+ * acquire WakeLocks unless you really need them, use the minimum levels possible, and be sure
+ * to release it as soon as you can.
+ *
+ * <p>You can obtain an instance of this class by calling
+ * {@link android.content.Context#getSystemService(java.lang.String) Context.getSystemService()}.
+ *
+ * <p>The primary API you'll use is {@link #newWakeLock(int, String) newWakeLock()}. This will
+ * create a {@link PowerManager.WakeLock} object. You can then use methods on this object to
+ * control the power state of the device. In practice it's quite simple:
+ *
+ * {@samplecode
+ * PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
+ * PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, "My Tag");
+ * wl.acquire();
+ * ..screen will stay on during this section..
+ * wl.release();
+ * }
+ *
+ * <p>The following flags are defined, with varying effects on system power. <i>These flags are
+ * mutually exclusive - you may only specify one of them.</i>
+ * <table border="2" width="85%" align="center" frame="hsides" rules="rows">
+ *
+ * <thead>
+ * <tr><th>Flag Value</th>
+ * <th>CPU</th> <th>Screen</th> <th>Keyboard</th></tr>
+ * </thead>
+ *
+ * <tbody>
+ * <tr><th>{@link #PARTIAL_WAKE_LOCK}</th>
+ * <td>On*</td> <td>Off</td> <td>Off</td>
+ * </tr>
+ *
+ * <tr><th>{@link #SCREEN_DIM_WAKE_LOCK}</th>
+ * <td>On</td> <td>Dim</td> <td>Off</td>
+ * </tr>
+ *
+ * <tr><th>{@link #SCREEN_BRIGHT_WAKE_LOCK}</th>
+ * <td>On</td> <td>Bright</td> <td>Off</td>
+ * </tr>
+ *
+ * <tr><th>{@link #FULL_WAKE_LOCK}</th>
+ * <td>On</td> <td>Bright</td> <td>Bright</td>
+ * </tr>
+ * </tbody>
+ * </table>
+ *
+ * <p>*<i>If you hold a partial wakelock, the CPU will continue to run, irrespective of any timers
+ * and even after the user presses the power button. In all other wakelocks, the CPU will run, but
+ * the user can still put the device to sleep using the power button.</i>
+ *
+ * <p>In addition, you can add two more flags, which affect behavior of the screen only. <i>These
+ * flags have no effect when combined with a {@link #PARTIAL_WAKE_LOCK}.</i>
+ * <table border="2" width="85%" align="center" frame="hsides" rules="rows">
+ *
+ * <thead>
+ * <tr><th>Flag Value</th> <th>Description</th></tr>
+ * </thead>
+ *
+ * <tbody>
+ * <tr><th>{@link #ACQUIRE_CAUSES_WAKEUP}</th>
+ * <td>Normal wake locks don't actually turn on the illumination. Instead, they cause
+ * the illumination to remain on once it turns on (e.g. from user activity). This flag
+ * will force the screen and/or keyboard to turn on immediately, when the WakeLock is
+ * acquired. A typical use would be for notifications which are important for the user to
+ * see immediately.</td>
+ * </tr>
+ *
+ * <tr><th>{@link #ON_AFTER_RELEASE}</th>
+ * <td>If this flag is set, the user activity timer will be reset when the WakeLock is
+ * released, causing the illumination to remain on a bit longer. This can be used to
+ * reduce flicker if you are cycling between wake lock conditions.</td>
+ * </tr>
+ * </tbody>
+ * </table>
+ *
+ *
+ */
+public class PowerManager
+{
+ private static final String TAG = "PowerManager";
+
+ /**
+ * These internal values define the underlying power elements that we might
+ * want to control individually. Eventually we'd like to expose them.
+ */
+ private static final int WAKE_BIT_CPU_STRONG = 1;
+ private static final int WAKE_BIT_CPU_WEAK = 2;
+ private static final int WAKE_BIT_SCREEN_DIM = 4;
+ private static final int WAKE_BIT_SCREEN_BRIGHT = 8;
+ private static final int WAKE_BIT_KEYBOARD_BRIGHT = 16;
+
+ private static final int LOCK_MASK = WAKE_BIT_CPU_STRONG
+ | WAKE_BIT_CPU_WEAK
+ | WAKE_BIT_SCREEN_DIM
+ | WAKE_BIT_SCREEN_BRIGHT
+ | WAKE_BIT_KEYBOARD_BRIGHT;
+
+ /**
+ * Wake lock that ensures that the CPU is running. The screen might
+ * not be on.
+ */
+ public static final int PARTIAL_WAKE_LOCK = WAKE_BIT_CPU_STRONG;
+
+ /**
+ * Wake lock that ensures that the screen and keyboard are on at
+ * full brightness.
+ */
+ public static final int FULL_WAKE_LOCK = WAKE_BIT_CPU_WEAK | WAKE_BIT_SCREEN_BRIGHT
+ | WAKE_BIT_KEYBOARD_BRIGHT;
+
+ /**
+ * Wake lock that ensures that the screen is on at full brightness;
+ * the keyboard backlight will be allowed to go off.
+ */
+ public static final int SCREEN_BRIGHT_WAKE_LOCK = WAKE_BIT_CPU_WEAK | WAKE_BIT_SCREEN_BRIGHT;
+
+ /**
+ * Wake lock that ensures that the screen is on (but may be dimmed);
+ * the keyboard backlight will be allowed to go off.
+ */
+ public static final int SCREEN_DIM_WAKE_LOCK = WAKE_BIT_CPU_WEAK | WAKE_BIT_SCREEN_DIM;
+
+ /**
+ * Normally wake locks don't actually wake the device, they just cause
+ * it to remain on once it's already on. Think of the video player
+ * app as the normal behavior. Notifications that pop up and want
+ * the device to be on are the exception; use this flag to be like them.
+ * <p>
+ * Does not work with PARTIAL_WAKE_LOCKs.
+ */
+ public static final int ACQUIRE_CAUSES_WAKEUP = 0x10000000;
+
+ /**
+ * When this wake lock is released, poke the user activity timer
+ * so the screen stays on for a little longer.
+ * <p>
+ * Will not turn the screen on if it is not already on. See {@link #ACQUIRE_CAUSES_WAKEUP}
+ * if you want that.
+ * <p>
+ * Does not work with PARTIAL_WAKE_LOCKs.
+ */
+ public static final int ON_AFTER_RELEASE = 0x20000000;
+
+ /**
+ * Class lets you say that you need to have the device on.
+ *
+ * <p>Call release when you are done and don't need the lock anymore.
+ */
+ public class WakeLock
+ {
+ static final int RELEASE_WAKE_LOCK = 1;
+
+ Runnable mReleaser = new Runnable() {
+ public void run() {
+ release();
+ }
+ };
+
+ int mFlags;
+ String mTag;
+ IBinder mToken;
+ int mCount = 0;
+ boolean mRefCounted = true;
+ boolean mHeld = false;
+
+ WakeLock(int flags, String tag)
+ {
+ switch (flags & LOCK_MASK) {
+ case PARTIAL_WAKE_LOCK:
+ case SCREEN_DIM_WAKE_LOCK:
+ case SCREEN_BRIGHT_WAKE_LOCK:
+ case FULL_WAKE_LOCK:
+ break;
+ default:
+ throw new IllegalArgumentException();
+ }
+
+ mFlags = flags;
+ mTag = tag;
+ mToken = new Binder();
+ }
+
+ /**
+ * Sets whether this WakeLock is ref counted.
+ *
+ * @param value true for ref counted, false for not ref counted.
+ */
+ public void setReferenceCounted(boolean value)
+ {
+ mRefCounted = value;
+ }
+
+ /**
+ * Makes sure the device is on at the level you asked when you created
+ * the wake lock.
+ */
+ public void acquire()
+ {
+ synchronized (mToken) {
+ if (!mRefCounted || mCount++ == 0) {
+ try {
+ mService.acquireWakeLock(mFlags, mToken, mTag);
+ } catch (RemoteException e) {
+ }
+ mHeld = true;
+ }
+ }
+ }
+
+ /**
+ * Makes sure the device is on at the level you asked when you created
+ * the wake lock. The lock will be released after the given timeout.
+ *
+ * @param timeout Release the lock after the give timeout in milliseconds.
+ */
+ public void acquire(long timeout) {
+ acquire();
+ mHandler.postDelayed(mReleaser, timeout);
+ }
+
+
+ /**
+ * Release your claim to the CPU or screen being on.
+ *
+ * <p>
+ * It may turn off shortly after you release it, or it may not if there
+ * are other wake locks held.
+ */
+ public void release()
+ {
+ synchronized (mToken) {
+ if (!mRefCounted || --mCount == 0) {
+ try {
+ mService.releaseWakeLock(mToken);
+ } catch (RemoteException e) {
+ }
+ mHeld = false;
+ }
+ if (mCount < 0) {
+ throw new RuntimeException("WakeLock under-locked " + mTag);
+ }
+ }
+ }
+
+ public boolean isHeld()
+ {
+ synchronized (mToken) {
+ return mHeld;
+ }
+ }
+
+ public String toString() {
+ synchronized (mToken) {
+ return "WakeLock{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " held=" + mHeld + ", refCount=" + mCount + "}";
+ }
+ }
+
+ @Override
+ protected void finalize() throws Throwable
+ {
+ synchronized (mToken) {
+ if (mHeld) {
+ try {
+ mService.releaseWakeLock(mToken);
+ } catch (RemoteException e) {
+ }
+ RuntimeInit.crash(TAG, new Exception(
+ "WakeLock finalized while still held: "+mTag));
+ }
+ }
+ }
+ }
+
+ /**
+ * Get a wake lock at the level of the flags parameter. Call
+ * {@link WakeLock#acquire() acquire()} on the object to acquire the
+ * wake lock, and {@link WakeLock#release release()} when you are done.
+ *
+ * {@samplecode
+ *PowerManager pm = (PowerManager)mContext.getSystemService(
+ * Context.POWER_SERVICE);
+ *PowerManager.WakeLock wl = pm.newWakeLock(
+ * PowerManager.SCREEN_DIM_WAKE_LOCK
+ * | PowerManager.ON_AFTER_RELEASE,
+ * TAG);
+ *wl.acquire();
+ * // ...
+ *wl.release();
+ * }
+ *
+ * @param flags Combination of flag values defining the requested behavior of the WakeLock.
+ * @param tag Your class name (or other tag) for debugging purposes.
+ *
+ * @see WakeLock#acquire()
+ * @see WakeLock#release()
+ */
+ public WakeLock newWakeLock(int flags, String tag)
+ {
+ return new WakeLock(flags, tag);
+ }
+
+ /**
+ * User activity happened.
+ * <p>
+ * Turns the device from whatever state it's in to full on, and resets
+ * the auto-off timer.
+ *
+ * @param when is used to order this correctly with the wake lock calls.
+ * This time should be in the {@link SystemClock#uptimeMillis
+ * SystemClock.uptimeMillis()} time base.
+ * @param noChangeLights should be true if you don't want the lights to
+ * turn on because of this event. This is set when the power
+ * key goes down. We want the device to stay on while the button
+ * is down, but we're about to turn off. Otherwise the lights
+ * flash on and then off and it looks weird.
+ */
+ public void userActivity(long when, boolean noChangeLights)
+ {
+ try {
+ mService.userActivity(when, noChangeLights);
+ } catch (RemoteException e) {
+ }
+ }
+
+ /**
+ * Force the device to go to sleep. Overrides all the wake locks that are
+ * held.
+ *
+ * @param time is used to order this correctly with the wake lock calls.
+ * The time should be in the {@link SystemClock#uptimeMillis
+ * SystemClock.uptimeMillis()} time base.
+ */
+ public void goToSleep(long time)
+ {
+ try {
+ mService.goToSleep(time);
+ } catch (RemoteException e) {
+ }
+ }
+
+ private PowerManager()
+ {
+ }
+
+ /**
+ * {@hide}
+ */
+ public PowerManager(IPowerManager service, Handler handler)
+ {
+ mService = service;
+ mHandler = handler;
+ }
+
+ /**
+ * TODO: It would be nice to be able to set the poke lock here,
+ * but I'm not sure what would be acceptable as an interface -
+ * either a PokeLock object (like WakeLock) or, possibly just a
+ * method call to set the poke lock.
+ */
+
+ IPowerManager mService;
+ Handler mHandler;
+}
+
diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java
new file mode 100644
index 0000000..2be7768
--- /dev/null
+++ b/core/java/android/os/Process.java
@@ -0,0 +1,694 @@
+/*
+ * 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.os;
+
+import android.net.LocalSocketAddress;
+import android.net.LocalSocket;
+import android.util.Log;
+
+import java.io.BufferedWriter;
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.util.ArrayList;
+
+/*package*/ class ZygoteStartFailedEx extends Exception {
+ /**
+ * Something prevented the zygote process startup from happening normally
+ */
+
+ ZygoteStartFailedEx() {};
+ ZygoteStartFailedEx(String s) {super(s);}
+ ZygoteStartFailedEx(Throwable cause) {super(cause);}
+}
+
+/**
+ * Tools for managing OS processes.
+ */
+public class Process {
+ private static final String LOG_TAG = "Process";
+
+ private static final String ZYGOTE_SOCKET = "zygote";
+
+ /**
+ * Name of a process for running the platform's media services.
+ * {@hide}
+ */
+ public static final String ANDROID_SHARED_MEDIA = "com.android.process.media";
+
+ /**
+ * Name of the process that Google content providers can share.
+ * {@hide}
+ */
+ public static final String GOOGLE_SHARED_APP_CONTENT = "com.google.process.content";
+
+ /**
+ * Defines the UID/GID under which system code runs.
+ */
+ public static final int SYSTEM_UID = 1000;
+
+ /**
+ * Defines the UID/GID under which the telephony code runs.
+ */
+ public static final int PHONE_UID = 1001;
+
+ /**
+ * Defines the start of a range of UIDs (and GIDs), going from this
+ * number to {@link #LAST_APPLICATION_UID} that are reserved for assigning
+ * to applications.
+ */
+ public static final int FIRST_APPLICATION_UID = 10000;
+ /**
+ * Last of application-specific UIDs starting at
+ * {@link #FIRST_APPLICATION_UID}.
+ */
+ public static final int LAST_APPLICATION_UID = 99999;
+
+ /**
+ * Defines a secondary group id for access to the bluetooth hardware.
+ */
+ public static final int BLUETOOTH_GID = 2000;
+
+ /**
+ * Standard priority of application threads.
+ * Use with {@link #setThreadPriority(int)} and
+ * {@link #setThreadPriority(int, int)}, <b>not</b> with the normal
+ * {@link java.lang.Thread} class.
+ */
+ public static final int THREAD_PRIORITY_DEFAULT = 0;
+
+ /*
+ * ***************************************
+ * ** Keep in sync with utils/threads.h **
+ * ***************************************
+ */
+
+ /**
+ * Lowest available thread priority. Only for those who really, really
+ * don't want to run if anything else is happening.
+ * Use with {@link #setThreadPriority(int)} and
+ * {@link #setThreadPriority(int, int)}, <b>not</b> with the normal
+ * {@link java.lang.Thread} class.
+ */
+ public static final int THREAD_PRIORITY_LOWEST = 19;
+
+ /**
+ * Standard priority background threads. This gives your thread a slightly
+ * lower than normal priority, so that it will have less chance of impacting
+ * the responsiveness of the user interface.
+ * Use with {@link #setThreadPriority(int)} and
+ * {@link #setThreadPriority(int, int)}, <b>not</b> with the normal
+ * {@link java.lang.Thread} class.
+ */
+ public static final int THREAD_PRIORITY_BACKGROUND = 10;
+
+ /**
+ * Standard priority of threads that are currently running a user interface
+ * that the user is interacting with. Applications can not normally
+ * change to this priority; the system will automatically adjust your
+ * application threads as the user moves through the UI.
+ * Use with {@link #setThreadPriority(int)} and
+ * {@link #setThreadPriority(int, int)}, <b>not</b> with the normal
+ * {@link java.lang.Thread} class.
+ */
+ public static final int THREAD_PRIORITY_FOREGROUND = -2;
+
+ /**
+ * Standard priority of system display threads, involved in updating
+ * the user interface. Applications can not
+ * normally change to this priority.
+ * Use with {@link #setThreadPriority(int)} and
+ * {@link #setThreadPriority(int, int)}, <b>not</b> with the normal
+ * {@link java.lang.Thread} class.
+ */
+ public static final int THREAD_PRIORITY_DISPLAY = -4;
+
+ /**
+ * Standard priority of the most important display threads, for compositing
+ * the screen and retrieving input events. Applications can not normally
+ * change to this priority.
+ * Use with {@link #setThreadPriority(int)} and
+ * {@link #setThreadPriority(int, int)}, <b>not</b> with the normal
+ * {@link java.lang.Thread} class.
+ */
+ public static final int THREAD_PRIORITY_URGENT_DISPLAY = -8;
+
+ /**
+ * Standard priority of audio threads. Applications can not normally
+ * change to this priority.
+ * Use with {@link #setThreadPriority(int)} and
+ * {@link #setThreadPriority(int, int)}, <b>not</b> with the normal
+ * {@link java.lang.Thread} class.
+ */
+ public static final int THREAD_PRIORITY_AUDIO = -16;
+
+ /**
+ * Standard priority of the most important audio threads.
+ * Applications can not normally change to this priority.
+ * Use with {@link #setThreadPriority(int)} and
+ * {@link #setThreadPriority(int, int)}, <b>not</b> with the normal
+ * {@link java.lang.Thread} class.
+ */
+ public static final int THREAD_PRIORITY_URGENT_AUDIO = -19;
+
+ /**
+ * Minimum increment to make a priority more favorable.
+ */
+ public static final int THREAD_PRIORITY_MORE_FAVORABLE = -1;
+
+ /**
+ * Minimum increment to make a priority less favorable.
+ */
+ public static final int THREAD_PRIORITY_LESS_FAVORABLE = +1;
+
+ public static final int SIGNAL_QUIT = 3;
+ public static final int SIGNAL_KILL = 9;
+ public static final int SIGNAL_USR1 = 10;
+
+ // State for communicating with zygote process
+
+ static LocalSocket sZygoteSocket;
+ static DataInputStream sZygoteInputStream;
+ static BufferedWriter sZygoteWriter;
+
+ /** true if previous zygote open failed */
+ static boolean sPreviousZygoteOpenFailed;
+
+ /**
+ * Start a new process.
+ *
+ * <p>If processes are enabled, a new process is created and the
+ * static main() function of a <var>processClass</var> is executed there.
+ * The process will continue running after this function returns.
+ *
+ * <p>If processes are not enabled, a new thread in the caller's
+ * process is created and main() of <var>processClass</var> called there.
+ *
+ * <p>The niceName parameter, if not an empty string, is a custom name to
+ * give to the process instead of using processClass. This allows you to
+ * make easily identifyable processes even if you are using the same base
+ * <var>processClass</var> to start them.
+ *
+ * @param processClass The class to use as the process's main entry
+ * point.
+ * @param niceName A more readable name to use for the process.
+ * @param uid The user-id under which the process will run.
+ * @param gid The group-id under which the process will run.
+ * @param gids Additional group-ids associated with the process.
+ * @param enableDebugger True if debugging should be enabled for this process.
+ * @param zygoteArgs Additional arguments to supply to the zygote process.
+ *
+ * @return int If > 0 the pid of the new process; if 0 the process is
+ * being emulated by a thread
+ * @throws RuntimeException on fatal start failure
+ *
+ * {@hide}
+ */
+ public static final int start(final String processClass,
+ final String niceName,
+ int uid, int gid, int[] gids,
+ boolean enableDebugger,
+ String[] zygoteArgs)
+ {
+ if (supportsProcesses()) {
+ try {
+ return startViaZygote(processClass, niceName, uid, gid, gids,
+ enableDebugger, zygoteArgs);
+ } catch (ZygoteStartFailedEx ex) {
+ Log.e(LOG_TAG,
+ "Starting VM process through Zygote failed");
+ throw new RuntimeException(
+ "Starting VM process through Zygote failed", ex);
+ }
+ } else {
+ // Running in single-process mode
+
+ Runnable runnable = new Runnable() {
+ public void run() {
+ Process.invokeStaticMain(processClass);
+ }
+ };
+
+ // Thread constructors must not be called with null names (see spec).
+ if (niceName != null) {
+ new Thread(runnable, niceName).start();
+ } else {
+ new Thread(runnable).start();
+ }
+
+ return 0;
+ }
+ }
+
+ /**
+ * Start a new process. Don't supply a custom nice name.
+ * {@hide}
+ */
+ public static final int start(String processClass, int uid, int gid,
+ int[] gids, boolean enableDebugger, String[] zygoteArgs) {
+ return start(processClass, "", uid, gid, gids,
+ enableDebugger, zygoteArgs);
+ }
+
+ private static void invokeStaticMain(String className) {
+ Class cl;
+ Object args[] = new Object[1];
+
+ args[0] = new String[0]; //this is argv
+
+ try {
+ cl = Class.forName(className);
+ cl.getMethod("main", new Class[] { String[].class })
+ .invoke(null, args);
+ } catch (Exception ex) {
+ // can be: ClassNotFoundException,
+ // NoSuchMethodException, SecurityException,
+ // IllegalAccessException, IllegalArgumentException
+ // InvocationTargetException
+ // or uncaught exception from main()
+
+ Log.e(LOG_TAG, "Exception invoking static main on "
+ + className, ex);
+
+ throw new RuntimeException(ex);
+ }
+
+ }
+
+ /** retry interval for opening a zygote socket */
+ static final int ZYGOTE_RETRY_MILLIS = 500;
+
+ /**
+ * Tries to open socket to Zygote process if not already open. If
+ * already open, does nothing. May block and retry.
+ */
+ private static void openZygoteSocketIfNeeded()
+ throws ZygoteStartFailedEx {
+
+ int retryCount;
+
+ if (sPreviousZygoteOpenFailed) {
+ /*
+ * If we've failed before, expect that we'll fail again and
+ * don't pause for retries.
+ */
+ retryCount = 0;
+ } else {
+ retryCount = 10;
+ }
+
+ /*
+ * See bug #811181: Sometimes runtime can make it up before zygote.
+ * Really, we'd like to do something better to avoid this condition,
+ * but for now just wait a bit...
+ */
+ for (int retry = 0
+ ; (sZygoteSocket == null) && (retry < (retryCount + 1))
+ ; retry++ ) {
+
+ if (retry > 0) {
+ try {
+ Log.i("Zygote", "Zygote not up yet, sleeping...");
+ Thread.sleep(ZYGOTE_RETRY_MILLIS);
+ } catch (InterruptedException ex) {
+ // should never happen
+ }
+ }
+
+ try {
+ sZygoteSocket = new LocalSocket();
+
+ sZygoteSocket.connect(new LocalSocketAddress(ZYGOTE_SOCKET,
+ LocalSocketAddress.Namespace.RESERVED));
+
+ sZygoteInputStream
+ = new DataInputStream(sZygoteSocket.getInputStream());
+
+ sZygoteWriter =
+ new BufferedWriter(
+ new OutputStreamWriter(
+ sZygoteSocket.getOutputStream()),
+ 256);
+
+ Log.i("Zygote", "Process: zygote socket opened");
+
+ sPreviousZygoteOpenFailed = false;
+ break;
+ } catch (IOException ex) {
+ if (sZygoteSocket != null) {
+ try {
+ sZygoteSocket.close();
+ } catch (IOException ex2) {
+ Log.e(LOG_TAG,"I/O exception on close after exception",
+ ex2);
+ }
+ }
+
+ sZygoteSocket = null;
+ }
+ }
+
+ if (sZygoteSocket == null) {
+ sPreviousZygoteOpenFailed = true;
+ throw new ZygoteStartFailedEx("connect failed");
+ }
+ }
+
+ /**
+ * Sends an argument list to the zygote process, which starts a new child
+ * and returns the child's pid. Please note: the present implementation
+ * replaces newlines in the argument list with spaces.
+ * @param args argument list
+ * @return PID of new child process
+ * @throws ZygoteStartFailedEx if process start failed for any reason
+ */
+ private static int zygoteSendArgsAndGetPid(ArrayList<String> args)
+ throws ZygoteStartFailedEx {
+
+ int pid;
+
+ openZygoteSocketIfNeeded();
+
+ try {
+ /**
+ * See com.android.internal.os.ZygoteInit.readArgumentList()
+ * Presently the wire format to the zygote process is:
+ * a) a count of arguments (argc, in essence)
+ * b) a number of newline-separated argument strings equal to count
+ *
+ * After the zygote process reads these it will write the pid of
+ * the child or -1 on failure.
+ */
+
+ sZygoteWriter.write(Integer.toString(args.size()));
+ sZygoteWriter.newLine();
+
+ int sz = args.size();
+ for (int i = 0; i < sz; i++) {
+ String arg = args.get(i);
+ if (arg.indexOf('\n') >= 0) {
+ throw new ZygoteStartFailedEx(
+ "embedded newlines not allowed");
+ }
+ sZygoteWriter.write(arg);
+ sZygoteWriter.newLine();
+ }
+
+ sZygoteWriter.flush();
+
+ // Should there be a timeout on this?
+ pid = sZygoteInputStream.readInt();
+
+ if (pid < 0) {
+ throw new ZygoteStartFailedEx("fork() failed");
+ }
+ } catch (IOException ex) {
+ try {
+ if (sZygoteSocket != null) {
+ sZygoteSocket.close();
+ }
+ } catch (IOException ex2) {
+ // we're going to fail anyway
+ Log.e(LOG_TAG,"I/O exception on routine close", ex2);
+ }
+
+ sZygoteSocket = null;
+
+ throw new ZygoteStartFailedEx(ex);
+ }
+
+ return pid;
+ }
+
+ /**
+ * Starts a new process via the zygote mechanism.
+ *
+ * @param processClass Class name whose static main() to run
+ * @param niceName 'nice' process name to appear in ps
+ * @param uid a POSIX uid that the new process should setuid() to
+ * @param gid a POSIX gid that the new process shuold setgid() to
+ * @param gids null-ok; a list of supplementary group IDs that the
+ * new process should setgroup() to.
+ * @param enableDebugger True if debugging should be enabled for this process.
+ * @param extraArgs Additional arguments to supply to the zygote process.
+ * @return PID
+ * @throws ZygoteStartFailedEx if process start failed for any reason
+ */
+ private static int startViaZygote(final String processClass,
+ final String niceName,
+ final int uid, final int gid,
+ final int[] gids,
+ boolean enableDebugger,
+ String[] extraArgs)
+ throws ZygoteStartFailedEx {
+ int pid;
+
+ synchronized(Process.class) {
+ ArrayList<String> argsForZygote = new ArrayList<String>();
+
+ // --runtime-init, --setuid=, --setgid=,
+ // and --setgroups= must go first
+ argsForZygote.add("--runtime-init");
+ argsForZygote.add("--setuid=" + uid);
+ argsForZygote.add("--setgid=" + gid);
+ if (enableDebugger) {
+ argsForZygote.add("--enable-debugger");
+ }
+
+ //TODO optionally enable debuger
+ //argsForZygote.add("--enable-debugger");
+
+ // --setgroups is a comma-separated list
+ if (gids != null && gids.length > 0) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("--setgroups=");
+
+ int sz = gids.length;
+ for (int i = 0; i < sz; i++) {
+ if (i != 0) {
+ sb.append(',');
+ }
+ sb.append(gids[i]);
+ }
+
+ argsForZygote.add(sb.toString());
+ }
+
+ if (niceName != null) {
+ argsForZygote.add("--nice-name=" + niceName);
+ }
+
+ argsForZygote.add(processClass);
+
+ if (extraArgs != null) {
+ for (String arg : extraArgs) {
+ argsForZygote.add(arg);
+ }
+ }
+
+ pid = zygoteSendArgsAndGetPid(argsForZygote);
+ }
+
+ if (pid <= 0) {
+ throw new ZygoteStartFailedEx("zygote start failed:" + pid);
+ }
+
+ return pid;
+ }
+
+ /**
+ * Returns elapsed milliseconds of the time this process has run.
+ * @return Returns the number of milliseconds this process has return.
+ */
+ public static final native long getElapsedCpuTime();
+
+ /**
+ * Returns the identifier of this process, which can be used with
+ * {@link #killProcess} and {@link #sendSignal}.
+ */
+ public static final native int myPid();
+
+ /**
+ * Returns the identifier of the calling thread, which be used with
+ * {@link #setThreadPriority(int, int)}.
+ */
+ public static final native int myTid();
+
+ /**
+ * Returns the UID assigned to a partlicular user name, or -1 if there is
+ * none. If the given string consists of only numbers, it is converted
+ * directly to a uid.
+ */
+ public static final native int getUidForName(String name);
+
+ /**
+ * Returns the GID assigned to a particular user name, or -1 if there is
+ * none. If the given string consists of only numbers, it is converted
+ * directly to a gid.
+ */
+ public static final native int getGidForName(String name);
+
+ /**
+ * Set the priority of a thread, based on Linux priorities.
+ *
+ * @param tid The identifier of the thread/process to change.
+ * @param priority A Linux priority level, from -20 for highest scheduling
+ * priority to 19 for lowest scheduling priority.
+ *
+ * @throws IllegalArgumentException Throws IllegalArgumentException if
+ * <var>tid</var> does not exist.
+ * @throws SecurityException Throws SecurityException if your process does
+ * not have permission to modify the given thread, or to use the given
+ * priority.
+ */
+ public static final native void setThreadPriority(int tid, int priority)
+ throws IllegalArgumentException, SecurityException;
+
+ /**
+ * Set the priority of the calling thread, based on Linux priorities. See
+ * {@link #setThreadPriority(int, int)} for more information.
+ *
+ * @param priority A Linux priority level, from -20 for highest scheduling
+ * priority to 19 for lowest scheduling priority.
+ *
+ * @throws IllegalArgumentException Throws IllegalArgumentException if
+ * <var>tid</var> does not exist.
+ * @throws SecurityException Throws SecurityException if your process does
+ * not have permission to modify the given thread, or to use the given
+ * priority.
+ *
+ * @see #setThreadPriority(int, int)
+ */
+ public static final native void setThreadPriority(int priority)
+ throws IllegalArgumentException, SecurityException;
+
+ /**
+ * Return the current priority of a thread, based on Linux priorities.
+ *
+ * @param tid The identifier of the thread/process to change.
+ *
+ * @return Returns the current priority, as a Linux priority level,
+ * from -20 for highest scheduling priority to 19 for lowest scheduling
+ * priority.
+ *
+ * @throws IllegalArgumentException Throws IllegalArgumentException if
+ * <var>tid</var> does not exist.
+ */
+ public static final native int getThreadPriority(int tid)
+ throws IllegalArgumentException;
+
+ /**
+ * Determine whether the current environment supports multiple processes.
+ *
+ * @return Returns true if the system can run in multiple processes, else
+ * false if everything is running in a single process.
+ */
+ public static final native boolean supportsProcesses();
+
+ /**
+ * Set the out-of-memory badness adjustment for a process.
+ *
+ * @param pid The process identifier to set.
+ * @param amt Adjustment value -- linux allows -16 to +15.
+ *
+ * @return Returns true if the underlying system supports this
+ * feature, else false.
+ *
+ * {@hide}
+ */
+ public static final native boolean setOomAdj(int pid, int amt);
+
+ /**
+ * Change this process's argv[0] parameter. This can be useful to show
+ * more descriptive information in things like the 'ps' command.
+ *
+ * @param text The new name of this process.
+ *
+ * {@hide}
+ */
+ public static final native void setArgV0(String text);
+
+ /**
+ * Kill the process with the given PID.
+ * Note that, though this API allows us to request to
+ * kill any process based on its PID, the kernel will
+ * still impose standard restrictions on which PIDs you
+ * are actually able to kill. Typically this means only
+ * the process running the caller's packages/application
+ * and any additional processes created by that app; packages
+ * sharing a common UID will also be able to kill each
+ * other's processes.
+ */
+ public static final void killProcess(int pid) {
+ sendSignal(pid, SIGNAL_KILL);
+ }
+
+ /** @hide */
+ public static final native int setUid(int uid);
+
+ /** @hide */
+ public static final native int setGid(int uid);
+
+ /**
+ * Send a signal to the given process.
+ *
+ * @param pid The pid of the target process.
+ * @param signal The signal to send.
+ */
+ public static final native void sendSignal(int pid, int signal);
+
+ /** @hide */
+ public static final native int getFreeMemory();
+
+ /** @hide */
+ public static final native void readProcLines(String path,
+ String[] reqFields, long[] outSizes);
+
+ /** @hide */
+ public static final native int[] getPids(String path, int[] lastArray);
+
+ /** @hide */
+ public static final int PROC_TERM_MASK = 0xff;
+ /** @hide */
+ public static final int PROC_ZERO_TERM = 0;
+ /** @hide */
+ public static final int PROC_SPACE_TERM = (int)' ';
+ /** @hide */
+ public static final int PROC_COMBINE = 0x100;
+ /** @hide */
+ public static final int PROC_PARENS = 0x200;
+ /** @hide */
+ public static final int PROC_OUT_STRING = 0x1000;
+ /** @hide */
+ public static final int PROC_OUT_LONG = 0x2000;
+ /** @hide */
+ public static final int PROC_OUT_FLOAT = 0x4000;
+
+ /** @hide */
+ public static final native boolean readProcFile(String file, int[] format,
+ String[] outStrings, long[] outLongs, float[] outFloats);
+
+ /**
+ * Gets the total Pss value for a given process, in bytes.
+ *
+ * @param pid the process to the Pss for
+ * @return the total Pss value for the given process in bytes,
+ * or -1 if the value cannot be determined
+ * @hide
+ */
+ public static final native long getPss(int pid);
+}
diff --git a/core/java/android/os/Registrant.java b/core/java/android/os/Registrant.java
new file mode 100644
index 0000000..c1780b9
--- /dev/null
+++ b/core/java/android/os/Registrant.java
@@ -0,0 +1,124 @@
+/*
+ * 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.os;
+
+import android.os.Handler;
+import android.os.Message;
+
+import java.lang.ref.WeakReference;
+import java.util.HashMap;
+
+/** @hide */
+public class Registrant
+{
+ public
+ Registrant(Handler h, int what, Object obj)
+ {
+ refH = new WeakReference(h);
+ this.what = what;
+ userObj = obj;
+ }
+
+ public void
+ clear()
+ {
+ refH = null;
+ userObj = null;
+ }
+
+ public void
+ notifyRegistrant()
+ {
+ internalNotifyRegistrant (null, null);
+ }
+
+ public void
+ notifyResult(Object result)
+ {
+ internalNotifyRegistrant (result, null);
+ }
+
+ public void
+ notifyException(Throwable exception)
+ {
+ internalNotifyRegistrant (null, exception);
+ }
+
+ /**
+ * This makes a copy of @param ar
+ */
+ public void
+ notifyRegistrant(AsyncResult ar)
+ {
+ internalNotifyRegistrant (ar.result, ar.exception);
+ }
+
+ /*package*/ void
+ internalNotifyRegistrant (Object result, Throwable exception)
+ {
+ Handler h = getHandler();
+
+ if (h == null) {
+ clear();
+ } else {
+ Message msg = Message.obtain();
+
+ msg.what = what;
+
+ msg.obj = new AsyncResult(userObj, result, exception);
+
+ h.sendMessage(msg);
+ }
+ }
+
+ /**
+ * NOTE: May return null if weak reference has been collected
+ */
+
+ public Message
+ messageForRegistrant()
+ {
+ Handler h = getHandler();
+
+ if (h == null) {
+ clear();
+
+ return null;
+ } else {
+ Message msg = h.obtainMessage();
+
+ msg.what = what;
+ msg.obj = userObj;
+
+ return msg;
+ }
+ }
+
+ public Handler
+ getHandler()
+ {
+ if (refH == null)
+ return null;
+
+ return (Handler) refH.get();
+ }
+
+ WeakReference refH;
+ int what;
+ Object userObj;
+}
+
diff --git a/core/java/android/os/RegistrantList.java b/core/java/android/os/RegistrantList.java
new file mode 100644
index 0000000..56b9e2b
--- /dev/null
+++ b/core/java/android/os/RegistrantList.java
@@ -0,0 +1,128 @@
+/*
+ * 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.os;
+
+import android.os.Handler;
+import android.os.Message;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/** @hide */
+public class RegistrantList
+{
+ ArrayList registrants = new ArrayList(); // of Registrant
+
+ public synchronized void
+ add(Handler h, int what, Object obj)
+ {
+ add(new Registrant(h, what, obj));
+ }
+
+ public synchronized void
+ addUnique(Handler h, int what, Object obj)
+ {
+ // if the handler is already in the registrant list, remove it
+ remove(h);
+ add(new Registrant(h, what, obj));
+ }
+
+ public synchronized void
+ add(Registrant r)
+ {
+ removeCleared();
+ registrants.add(r);
+ }
+
+ public synchronized void
+ removeCleared()
+ {
+ for (int i = registrants.size() - 1; i >= 0 ; i--) {
+ Registrant r = (Registrant) registrants.get(i);
+
+ if (r.refH == null) {
+ registrants.remove(i);
+ }
+ }
+ }
+
+ public synchronized int
+ size()
+ {
+ return registrants.size();
+ }
+
+ public synchronized Object
+ get(int index)
+ {
+ return registrants.get(index);
+ }
+
+ private synchronized void
+ internalNotifyRegistrants (Object result, Throwable exception)
+ {
+ for (int i = 0, s = registrants.size(); i < s ; i++) {
+ Registrant r = (Registrant) registrants.get(i);
+ r.internalNotifyRegistrant(result, exception);
+ }
+ }
+
+ public /*synchronized*/ void
+ notifyRegistrants()
+ {
+ internalNotifyRegistrants(null, null);
+ }
+
+ public /*synchronized*/ void
+ notifyException(Throwable exception)
+ {
+ internalNotifyRegistrants (null, exception);
+ }
+
+ public /*synchronized*/ void
+ notifyResult(Object result)
+ {
+ internalNotifyRegistrants (result, null);
+ }
+
+
+ public /*synchronized*/ void
+ notifyRegistrants(AsyncResult ar)
+ {
+ internalNotifyRegistrants(ar.result, ar.exception);
+ }
+
+ public synchronized void
+ remove(Handler h)
+ {
+ for (int i = 0, s = registrants.size() ; i < s ; i++) {
+ Registrant r = (Registrant) registrants.get(i);
+ Handler rh;
+
+ rh = r.getHandler();
+
+ /* Clean up both the requested registrant and
+ * any now-collected registrants
+ */
+ if (rh == null || rh == h) {
+ r.clear();
+ }
+ }
+
+ removeCleared();
+ }
+}
diff --git a/core/java/android/os/RemoteCallbackList.java b/core/java/android/os/RemoteCallbackList.java
new file mode 100644
index 0000000..04e7ef0
--- /dev/null
+++ b/core/java/android/os/RemoteCallbackList.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os;
+
+import java.util.HashMap;
+
+/**
+ * Takes care of the grunt work of maintaining a list of remote interfaces,
+ * typically for the use of performing callbacks from a
+ * {@link android.app.Service} to its clients. In particular, this:
+ *
+ * <ul>
+ * <li> Keeps track of a set of registered {@link IInterface} callbacks,
+ * taking care to identify them through their underlying unique {@link IBinder}
+ * (by calling {@link IInterface#asBinder IInterface.asBinder()}.
+ * <li> Attaches a {@link IBinder.DeathRecipient IBinder.DeathRecipient} to
+ * each registered interface, so that it can be cleaned out of the list if its
+ * process goes away.
+ * <li> Performs locking of the underlying list of interfaces to deal with
+ * multithreaded incoming calls, and a thread-safe way to iterate over a
+ * snapshot of the list without holding its lock.
+ * </ul>
+ *
+ * <p>To use this class, simply create a single instance along with your
+ * service, and call its {@link #register} and {@link #unregister} methods
+ * as client register and unregister with your service. To call back on to
+ * the registered clients, use {@link #beginBroadcast},
+ * {@link #getBroadcastItem}, and {@link #finishBroadcast}.
+ *
+ * <p>If a registered callback's process goes away, this class will take
+ * care of automatically removing it from the list. If you want to do
+ * additional work in this situation, you can create a subclass that
+ * implements the {@link #onCallbackDied} method.
+ */
+public class RemoteCallbackList<E extends IInterface> {
+ /*package*/ HashMap<IBinder, Callback> mCallbacks
+ = new HashMap<IBinder, Callback>();
+ private IInterface[] mActiveBroadcast;
+ private boolean mKilled = false;
+
+ private final class Callback implements IBinder.DeathRecipient {
+ final E mCallback;
+
+ Callback(E callback) {
+ mCallback = callback;
+ }
+
+ public void binderDied() {
+ synchronized (mCallbacks) {
+ mCallbacks.remove(mCallback.asBinder());
+ }
+ onCallbackDied(mCallback);
+ }
+ }
+
+ /**
+ * Add a new callback to the list. This callback will remain in the list
+ * until a corresponding call to {@link #unregister} or its hosting process
+ * goes away. If the callback was already registered (determined by
+ * checking to see if the {@link IInterface#asBinder callback.asBinder()}
+ * object is already in the list), then it will be left as-is.
+ * Registrations are not counted; a single call to {@link #unregister}
+ * will remove a callback after any number calls to register it.
+ *
+ * @param callback The callback interface to be added to the list. Must
+ * not be null -- passing null here will cause a NullPointerException.
+ * Most services will want to check for null before calling this with
+ * an object given from a client, so that clients can't crash the
+ * service with bad data.
+ *
+ * @return Returns true if the callback was successfully added to the list.
+ * Returns false if it was not added, either because {@link #kill} had
+ * previously been called or the callback's process has gone away.
+ *
+ * @see #unregister
+ * @see #kill
+ * @see #onCallbackDied
+ */
+ public boolean register(E callback) {
+ synchronized (mCallbacks) {
+ if (mKilled) {
+ return false;
+ }
+ IBinder binder = callback.asBinder();
+ try {
+ Callback cb = new Callback(callback);
+ binder.linkToDeath(cb, 0);
+ mCallbacks.put(binder, cb);
+ return true;
+ } catch (RemoteException e) {
+ return false;
+ }
+ }
+ }
+
+ /**
+ * Remove from the list a callback that was previously added with
+ * {@link #register}. This uses the
+ * {@link IInterface#asBinder callback.asBinder()} object to correctly
+ * find the previous registration.
+ * Registrations are not counted; a single unregister call will remove
+ * a callback after any number calls to {@link #register} for it.
+ *
+ * @param callback The callback to be removed from the list. Passing
+ * null here will cause a NullPointerException, so you will generally want
+ * to check for null before calling.
+ *
+ * @return Returns true if the callback was found and unregistered. Returns
+ * false if the given callback was not found on the list.
+ *
+ * @see #register
+ */
+ public boolean unregister(E callback) {
+ synchronized (mCallbacks) {
+ Callback cb = mCallbacks.remove(callback.asBinder());
+ if (cb != null) {
+ cb.mCallback.asBinder().unlinkToDeath(cb, 0);
+ return true;
+ }
+ return false;
+ }
+ }
+
+ /**
+ * Disable this callback list. All registered callbacks are unregistered,
+ * and the list is disabled so that future calls to {@link #register} will
+ * fail. This should be used when a Service is stopping, to prevent clients
+ * from registering callbacks after it is stopped.
+ *
+ * @see #register
+ */
+ public void kill() {
+ synchronized (mCallbacks) {
+ for (Callback cb : mCallbacks.values()) {
+ cb.mCallback.asBinder().unlinkToDeath(cb, 0);
+ }
+ mCallbacks.clear();
+ mKilled = true;
+ }
+ }
+
+ /**
+ * Called when the process hosting a callback in the list has gone away.
+ * The default implementation does nothing.
+ *
+ * @param callback The callback whose process has died. Note that, since
+ * its process has died, you can not make any calls on to this interface.
+ * You can, however, retrieve its IBinder and compare it with another
+ * IBinder to see if it is the same object.
+ *
+ * @see #register
+ */
+ public void onCallbackDied(E callback) {
+ }
+
+ /**
+ * Prepare to start making calls to the currently registered callbacks.
+ * This creates a copy of the callback list, which you can retrieve items
+ * from using {@link #getBroadcastItem}. Note that only one broadcast can
+ * be active at a time, so you must be sure to always call this from the
+ * same thread (usually by scheduling with {@link Handler} or
+ * do your own synchronization. You must call {@link #finishBroadcast}
+ * when done.
+ *
+ * <p>A typical loop delivering a broadcast looks like this:
+ *
+ * <pre>
+ * final int N = callbacks.beginBroadcast();
+ * for (int i=0; i<N; i++) {
+ * try {
+ * callbacks.getBroadcastItem(i).somethingHappened();
+ * } catch (RemoteException e) {
+ * // The RemoteCallbackList will take care of removing
+ * // the dead object for us.
+ * }
+ * }
+ * callbacks.finishBroadcast();</pre>
+ *
+ * @return Returns the number of callbacks in the broadcast, to be used
+ * with {@link #getBroadcastItem} to determine the range of indices you
+ * can supply.
+ *
+ * @see #getBroadcastItem
+ * @see #finishBroadcast
+ */
+ public int beginBroadcast() {
+ synchronized (mCallbacks) {
+ final int N = mCallbacks.size();
+ if (N <= 0) {
+ return 0;
+ }
+ IInterface[] active = mActiveBroadcast;
+ if (active == null || active.length < N) {
+ mActiveBroadcast = active = new IInterface[N];
+ }
+ int i=0;
+ for (Callback cb : mCallbacks.values()) {
+ active[i++] = cb.mCallback;
+ }
+ return N;
+ }
+ }
+
+ /**
+ * Retrieve an item in the active broadcast that was previously started
+ * with {@link #beginBroadcast}. This can <em>only</em> be called after
+ * the broadcast is started, and its data is no longer valid after
+ * calling {@link #finishBroadcast}.
+ *
+ * <p>Note that it is possible for the process of one of the returned
+ * callbacks to go away before you call it, so you will need to catch
+ * {@link RemoteException} when calling on to the returned object.
+ * The callback list itself, however, will take care of unregistering
+ * these objects once it detects that it is no longer valid, so you can
+ * handle such an exception by simply ignoring it.
+ *
+ * @param index Which of the registered callbacks you would like to
+ * retrieve. Ranges from 0 to 1-{@link #beginBroadcast}.
+ *
+ * @return Returns the callback interface that you can call. This will
+ * always be non-null.
+ *
+ * @see #beginBroadcast
+ */
+ public E getBroadcastItem(int index) {
+ return (E)mActiveBroadcast[index];
+ }
+
+ /**
+ * Clean up the state of a broadcast previously initiated by calling
+ * {@link #beginBroadcast}. This must always be called when you are done
+ * with a broadcast.
+ *
+ * @see #beginBroadcast
+ */
+ public void finishBroadcast() {
+ IInterface[] active = mActiveBroadcast;
+ if (active != null) {
+ final int N = active.length;
+ for (int i=0; i<N; i++) {
+ active[i] = null;
+ }
+ }
+ }
+}
diff --git a/core/java/android/os/RemoteException.java b/core/java/android/os/RemoteException.java
new file mode 100644
index 0000000..9d76156
--- /dev/null
+++ b/core/java/android/os/RemoteException.java
@@ -0,0 +1,27 @@
+/*
+ * 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.os;
+import android.util.AndroidException;
+
+/**
+ * Parent exception for all Binder remote-invocation errors
+ */
+public class RemoteException extends AndroidException {
+ public RemoteException() {
+ super();
+ }
+}
diff --git a/core/java/android/os/RemoteMailException.java b/core/java/android/os/RemoteMailException.java
new file mode 100644
index 0000000..1ac96d1
--- /dev/null
+++ b/core/java/android/os/RemoteMailException.java
@@ -0,0 +1,31 @@
+/*
+ * 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.os;
+
+/** @hide */
+public class RemoteMailException extends Exception
+{
+ public RemoteMailException()
+ {
+ }
+
+ public RemoteMailException(String s)
+ {
+ super(s);
+ }
+}
+
diff --git a/core/java/android/os/ServiceManager.java b/core/java/android/os/ServiceManager.java
new file mode 100644
index 0000000..b721665
--- /dev/null
+++ b/core/java/android/os/ServiceManager.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os;
+
+import com.android.internal.os.BinderInternal;
+
+import android.util.Log;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/** @hide */
+public final class ServiceManager {
+ private static final String TAG = "ServiceManager";
+
+ private static IServiceManager sServiceManager;
+ private static HashMap<String, IBinder> sCache = new HashMap<String, IBinder>();
+
+ private static IServiceManager getIServiceManager() {
+ if (sServiceManager != null) {
+ return sServiceManager;
+ }
+
+ // Find the service manager
+ sServiceManager = ServiceManagerNative.asInterface(BinderInternal.getContextObject());
+ return sServiceManager;
+ }
+
+ /**
+ * Returns a reference to a service with the given name.
+ *
+ * @param name the name of the service to get
+ * @return a reference to the service, or <code>null</code> if the service doesn't exist
+ */
+ public static IBinder getService(String name) {
+ try {
+ IBinder service = sCache.get(name);
+ if (service != null) {
+ return service;
+ } else {
+ return getIServiceManager().getService(name);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "error in getService", e);
+ }
+ return null;
+ }
+
+ /**
+ * Place a new @a service called @a name into the service
+ * manager.
+ *
+ * @param name the name of the new service
+ * @param service the service object
+ */
+ public static void addService(String name, IBinder service) {
+ try {
+ getIServiceManager().addService(name, service);
+ } catch (RemoteException e) {
+ Log.e(TAG, "error in addService", e);
+ }
+ }
+
+ /**
+ * Retrieve an existing service called @a name from the
+ * service manager. Non-blocking.
+ */
+ public static IBinder checkService(String name) {
+ try {
+ IBinder service = sCache.get(name);
+ if (service != null) {
+ return service;
+ } else {
+ return getIServiceManager().checkService(name);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "error in checkService", e);
+ return null;
+ }
+ }
+
+ /**
+ * Return a list of all currently running services.
+ */
+ public static String[] listServices() throws RemoteException {
+ try {
+ return getIServiceManager().listServices();
+ } catch (RemoteException e) {
+ Log.e(TAG, "error in listServices", e);
+ return null;
+ }
+ }
+
+ /**
+ * This is only intended to be called when the process is first being brought
+ * up and bound by the activity manager. There is only one thread in the process
+ * at that time, so no locking is done.
+ *
+ * @param cache the cache of service references
+ * @hide
+ */
+ public static void initServiceCache(Map<String, IBinder> cache) {
+ if (sCache.size() != 0 && Process.supportsProcesses()) {
+ throw new IllegalStateException("setServiceCache may only be called once");
+ }
+ sCache.putAll(cache);
+ }
+}
diff --git a/core/java/android/os/ServiceManagerNative.java b/core/java/android/os/ServiceManagerNative.java
new file mode 100644
index 0000000..2aab0e6
--- /dev/null
+++ b/core/java/android/os/ServiceManagerNative.java
@@ -0,0 +1,174 @@
+/*
+ * 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.os;
+
+
+/**
+ * Native implementation of the service manager. Most clients will only
+ * care about getDefault() and possibly asInterface().
+ * @hide
+ */
+public abstract class ServiceManagerNative extends Binder implements IServiceManager
+{
+ /**
+ * Cast a Binder object into a service manager interface, generating
+ * a proxy if needed.
+ */
+ static public IServiceManager asInterface(IBinder obj)
+ {
+ if (obj == null) {
+ return null;
+ }
+ IServiceManager in =
+ (IServiceManager)obj.queryLocalInterface(descriptor);
+ if (in != null) {
+ return in;
+ }
+
+ return new ServiceManagerProxy(obj);
+ }
+
+ public ServiceManagerNative()
+ {
+ attachInterface(this, descriptor);
+ }
+
+ public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
+ {
+ try {
+ switch (code) {
+ case IServiceManager.GET_SERVICE_TRANSACTION: {
+ data.enforceInterface(IServiceManager.descriptor);
+ String name = data.readString();
+ IBinder service = getService(name);
+ reply.writeStrongBinder(service);
+ return true;
+ }
+
+ case IServiceManager.CHECK_SERVICE_TRANSACTION: {
+ data.enforceInterface(IServiceManager.descriptor);
+ String name = data.readString();
+ IBinder service = checkService(name);
+ reply.writeStrongBinder(service);
+ return true;
+ }
+
+ case IServiceManager.ADD_SERVICE_TRANSACTION: {
+ data.enforceInterface(IServiceManager.descriptor);
+ String name = data.readString();
+ IBinder service = data.readStrongBinder();
+ addService(name, service);
+ return true;
+ }
+
+ case IServiceManager.LIST_SERVICES_TRANSACTION: {
+ data.enforceInterface(IServiceManager.descriptor);
+ String[] list = listServices();
+ reply.writeStringArray(list);
+ return true;
+ }
+
+ case IServiceManager.SET_PERMISSION_CONTROLLER_TRANSACTION: {
+ data.enforceInterface(IServiceManager.descriptor);
+ IPermissionController controller
+ = IPermissionController.Stub.asInterface(
+ data.readStrongBinder());
+ setPermissionController(controller);
+ return true;
+ }
+ }
+ } catch (RemoteException e) {
+ }
+
+ return false;
+ }
+
+ public IBinder asBinder()
+ {
+ return this;
+ }
+}
+
+class ServiceManagerProxy implements IServiceManager {
+ public ServiceManagerProxy(IBinder remote) {
+ mRemote = remote;
+ }
+
+ public IBinder asBinder() {
+ return mRemote;
+ }
+
+ public IBinder getService(String name) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IServiceManager.descriptor);
+ data.writeString(name);
+ mRemote.transact(GET_SERVICE_TRANSACTION, data, reply, 0);
+ IBinder binder = reply.readStrongBinder();
+ reply.recycle();
+ data.recycle();
+ return binder;
+ }
+
+ public IBinder checkService(String name) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IServiceManager.descriptor);
+ data.writeString(name);
+ mRemote.transact(CHECK_SERVICE_TRANSACTION, data, reply, 0);
+ IBinder binder = reply.readStrongBinder();
+ reply.recycle();
+ data.recycle();
+ return binder;
+ }
+
+ public void addService(String name, IBinder service)
+ throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IServiceManager.descriptor);
+ data.writeString(name);
+ data.writeStrongBinder(service);
+ mRemote.transact(ADD_SERVICE_TRANSACTION, data, reply, 0);
+ reply.recycle();
+ data.recycle();
+ }
+
+ public String[] listServices() throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IServiceManager.descriptor);
+ mRemote.transact(LIST_SERVICES_TRANSACTION, data, reply, 0);
+ String[] list = reply.readStringArray();
+ reply.recycle();
+ data.recycle();
+ return list;
+ }
+
+ public void setPermissionController(IPermissionController controller)
+ throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IServiceManager.descriptor);
+ data.writeStrongBinder(controller.asBinder());
+ mRemote.transact(SET_PERMISSION_CONTROLLER_TRANSACTION, data, reply, 0);
+ reply.recycle();
+ data.recycle();
+ }
+
+ private IBinder mRemote;
+}
diff --git a/core/java/android/os/StatFs.java b/core/java/android/os/StatFs.java
new file mode 100644
index 0000000..912bfdf
--- /dev/null
+++ b/core/java/android/os/StatFs.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os;
+
+/**
+ * Retrieve overall information about the space on a filesystem. This is a
+ * Wrapper for Unix statfs().
+ */
+public class StatFs {
+ /**
+ * Construct a new StatFs for looking at the stats of the
+ * filesystem at <var>path</var>. Upon construction, the stat of
+ * the file system will be performed, and the values retrieved available
+ * from the methods on this class.
+ *
+ * @param path A path in the desired file system to state.
+ */
+ public StatFs(String path) { native_setup(path); }
+
+ /**
+ * Perform a restat of the file system referenced by this object. This
+ * is the same as re-constructing the object with the same file system
+ * path, and the new stat values are available upon return.
+ */
+ public void restat(String path) { native_restat(path); }
+
+ @Override
+ protected void finalize() { native_finalize(); }
+
+ /**
+ * The size, in bytes, of a block on the file system. This corresponds
+ * to the Unix statfs.f_bsize field.
+ */
+ public native int getBlockSize();
+
+ /**
+ * The total number of blocks on the file system. This corresponds
+ * to the Unix statfs.f_blocks field.
+ */
+ public native int getBlockCount();
+
+ /**
+ * The total number of blocks that are free on the file system, including
+ * reserved blocks (that are not available to normal applications). This
+ * corresponds to the Unix statfs.f_bfree field. Most applications will
+ * want to use {@link #getAvailableBlocks()} instead.
+ */
+ public native int getFreeBlocks();
+
+ /**
+ * The number of blocks that are free on the file system and available to
+ * applications. This corresponds to the Unix statfs.f_bavail field.
+ */
+ public native int getAvailableBlocks();
+
+ private int mNativeContext;
+ private native void native_restat(String path);
+ private native void native_setup(String path);
+ private native void native_finalize();
+}
diff --git a/core/java/android/os/SystemClock.java b/core/java/android/os/SystemClock.java
new file mode 100644
index 0000000..2b57b39
--- /dev/null
+++ b/core/java/android/os/SystemClock.java
@@ -0,0 +1,154 @@
+/*
+ * 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.os;
+
+
+/**
+ * Core timekeeping facilities.
+ *
+ * <p> Three different clocks are available, and they should not be confused:
+ *
+ * <ul>
+ * <li> <p> {@link System#currentTimeMillis System.currentTimeMillis()}
+ * is the standard "wall" clock (time and date) expressing milliseconds
+ * since the epoch. The wall clock can be set by the user or the phone
+ * network (see {@link #setCurrentTimeMillis}), so the time may jump
+ * backwards or forwards unpredictably. This clock should only be used
+ * when correspondence with real-world dates and times is important, such
+ * as in a calendar or alarm clock application. Interval or elapsed
+ * time measurements should use a different clock.
+ *
+ * <li> <p> {@link #uptimeMillis} is counted in milliseconds since the
+ * system was booted. This clock stops when the system enters deep
+ * sleep (CPU off, display dark, device waiting for external input),
+ * but is not affected by clock scaling, idle, or other power saving
+ * mechanisms. This is the basis for most interval timing
+ * such as {@link Thread#sleep(long) Thread.sleep(millls)},
+ * {@link Object#wait(long) Object.wait(millis)}, and
+ * {@link System#nanoTime System.nanoTime()}. This clock is guaranteed
+ * to be monotonic, and is the recommended basis for the general purpose
+ * interval timing of user interface events, performance measurements,
+ * and anything else that does not need to measure elapsed time during
+ * device sleep. Most methods that accept a timestamp value expect the
+ * {@link #uptimeMillis} clock.
+ *
+ * <li> <p> {@link #elapsedRealtime} is counted in milliseconds since the
+ * system was booted, including deep sleep. This clock should be used
+ * when measuring time intervals that may span periods of system sleep.
+ * </ul>
+ *
+ * There are several mechanisms for controlling the timing of events:
+ *
+ * <ul>
+ * <li> <p> Standard functions like {@link Thread#sleep(long)
+ * Thread.sleep(millis)} and {@link Object#wait(long) Object.wait(millis)}
+ * are always available. These functions use the {@link #uptimeMillis}
+ * clock; if the device enters sleep, the remainder of the time will be
+ * postponed until the device wakes up. These synchronous functions may
+ * be interrupted with {@link Thread#interrupt Thread.interrupt()}, and
+ * you must handle {@link InterruptedException}.
+ *
+ * <li> <p> {@link #sleep SystemClock.sleep(millis)} is a utility function
+ * very similar to {@link Thread#sleep(long) Thread.sleep(millis)}, but it
+ * ignores {@link InterruptedException}. Use this function for delays if
+ * you do not use {@link Thread#interrupt Thread.interrupt()}, as it will
+ * preserve the interrupted state of the thread.
+ *
+ * <li> <p> The {@link android.os.Handler} class can schedule asynchronous
+ * callbacks at an absolute or relative time. Handler objects also use the
+ * {@link #uptimeMillis} clock, and require an {@link android.os.Looper
+ * event loop} (normally present in any GUI application).
+ *
+ * <li> <p> The {@link android.app.AlarmManager} can trigger one-time or
+ * recurring events which occur even when the device is in deep sleep
+ * or your application is not running. Events may be scheduled with your
+ * choice of {@link java.lang.System#currentTimeMillis} (RTC) or
+ * {@link #elapsedRealtime} (ELAPSED_REALTIME), and cause an
+ * {@link android.content.Intent} broadcast when they occur.
+ * </ul>
+ */
+public final class SystemClock {
+ /**
+ * This class is uninstantiable.
+ */
+ private SystemClock() {
+ // This space intentionally left blank.
+ }
+
+ /**
+ * Waits a given number of milliseconds (of uptimeMillis) before returning.
+ * Similar to {@link java.lang.Thread#sleep(long)}, but does not throw
+ * {@link InterruptedException}; {@link Thread#interrupt()} events are
+ * deferred until the next interruptible operation. Does not return until
+ * at least the specified number of milliseconds has elapsed.
+ *
+ * @param ms to sleep before returning, in milliseconds of uptime.
+ */
+ public static void sleep(long ms)
+ {
+ long start = uptimeMillis();
+ long duration = ms;
+ boolean interrupted = false;
+ do {
+ try {
+ Thread.sleep(duration);
+ }
+ catch (InterruptedException e) {
+ interrupted = true;
+ }
+ duration = start + ms - uptimeMillis();
+ } while (duration > 0);
+
+ if (interrupted) {
+ // Important: we don't want to quietly eat an interrupt() event,
+ // so we make sure to re-interrupt the thread so that the next
+ // call to Thread.sleep() or Object.wait() will be interrupted.
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ /**
+ * Sets the current wall time, in milliseconds. Requires the calling
+ * process to have appropriate permissions.
+ *
+ * @return if the clock was successfully set to the specified time.
+ */
+ native public static boolean setCurrentTimeMillis(long millis);
+
+ /**
+ * Returns milliseconds since boot, not counting time spent in deep sleep.
+ * <b>Note:</b> This value may get reset occasionally (before it would
+ * otherwise wrap around).
+ *
+ * @return milliseconds of non-sleep uptime since boot.
+ */
+ native public static long uptimeMillis();
+
+ /**
+ * Returns milliseconds since boot, including time spent in sleep.
+ *
+ * @return elapsed milliseconds since boot.
+ */
+ native public static long elapsedRealtime();
+
+ /**
+ * Returns milliseconds running in the current thread.
+ *
+ * @return elapsed milliseconds in the thread
+ */
+ public static native long currentThreadTimeMillis();
+}
diff --git a/core/java/android/os/SystemProperties.java b/core/java/android/os/SystemProperties.java
new file mode 100644
index 0000000..c3ae3c2
--- /dev/null
+++ b/core/java/android/os/SystemProperties.java
@@ -0,0 +1,143 @@
+/*
+ * 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.os;
+
+
+/**
+ * Gives access to the system properties store. The system properties
+ * store contains a list of string key-value pairs.
+ *
+ * {@hide}
+ */
+public class SystemProperties
+{
+ public static final int PROP_NAME_MAX = 31;
+ public static final int PROP_VALUE_MAX = 91;
+
+ private static native String native_get(String key);
+ private static native String native_get(String key, String def);
+ private static native void native_set(String key, String def);
+
+ /**
+ * Get the value for the given key.
+ * @return an empty string if the key isn't found
+ * @throws IllegalArgumentException if the key exceeds 32 characters
+ */
+ public static String get(String key) {
+ if (key.length() > PROP_NAME_MAX) {
+ throw new IllegalArgumentException("key.length > " + PROP_NAME_MAX);
+ }
+ return native_get(key);
+ }
+
+ /**
+ * Get the value for the given key.
+ * @return if the key isn't found, return def if it isn't null, or an empty string otherwise
+ * @throws IllegalArgumentException if the key exceeds 32 characters
+ */
+ public static String get(String key, String def) {
+ if (key.length() > PROP_NAME_MAX) {
+ throw new IllegalArgumentException("key.length > " + PROP_NAME_MAX);
+ }
+ return native_get(key, def);
+ }
+
+ /**
+ * Get the value for the given key, and return as an integer.
+ * @param key the key to lookup
+ * @param def a default value to return
+ * @return the key parsed as an integer, or def if the key isn't found or
+ * cannot be parsed
+ * @throws IllegalArgumentException if the key exceeds 32 characters
+ */
+ public static int getInt(String key, int def) {
+ try {
+ return Integer.parseInt(get(key));
+ } catch (NumberFormatException e) {
+ return def;
+ }
+ }
+
+ /**
+ * Get the value for the given key, and return as a long.
+ * @param key the key to lookup
+ * @param def a default value to return
+ * @return the key parsed as a long, or def if the key isn't found or
+ * cannot be parsed
+ * @throws IllegalArgumentException if the key exceeds 32 characters
+ */
+ public static long getLong(String key, long def) {
+ try {
+ return Long.parseLong(get(key));
+ } catch (NumberFormatException e) {
+ return def;
+ }
+ }
+
+ /**
+ * Get the value for the given key, returned as a boolean.
+ * Values 'n', 'no', '0', 'false' or 'off' are considered false.
+ * Values 'y', 'yes', '1', 'true' or 'on' are considered true.
+ * (case insensitive).
+ * If the key does not exist, or has any other value, then the default
+ * result is returned.
+ * @param key the key to lookup
+ * @param def a default value to return
+ * @return the key parsed as a boolean, or def if the key isn't found or is
+ * not able to be parsed as a boolean.
+ * @throws IllegalArgumentException if the key exceeds 32 characters
+ */
+ public static boolean getBoolean(String key, boolean def) {
+ String value = get(key);
+ // Deal with these quick cases first: not found, 0 and 1
+ if (value.equals("")) {
+ return def;
+ } else if (value.equals("0")) {
+ return false;
+ } else if (value.equals("1")) {
+ return true;
+ // now for slower (and hopefully less common) cases
+ } else if (value.equalsIgnoreCase("n") ||
+ value.equalsIgnoreCase("no") ||
+ value.equalsIgnoreCase("false") ||
+ value.equalsIgnoreCase("off")) {
+ return false;
+ } else if (value.equalsIgnoreCase("y") ||
+ value.equalsIgnoreCase("yes") ||
+ value.equalsIgnoreCase("true") ||
+ value.equalsIgnoreCase("on")) {
+ return true;
+ }
+ return def;
+ }
+
+ /**
+ * Set the value for the given key.
+ * @throws IllegalArgumentException if the key exceeds 32 characters
+ * @throws IllegalArgumentException if the value exceeds 92 characters
+ */
+ public static void set(String key, String val) {
+ if (key.length() > PROP_NAME_MAX) {
+ throw new IllegalArgumentException("key.length > " + PROP_NAME_MAX);
+ }
+ if (val != null && val.length() > PROP_VALUE_MAX) {
+ throw new IllegalArgumentException("val.length > " +
+ PROP_VALUE_MAX);
+ }
+ native_set(key, val);
+ }
+}
diff --git a/core/java/android/os/SystemService.java b/core/java/android/os/SystemService.java
new file mode 100644
index 0000000..447cd1f
--- /dev/null
+++ b/core/java/android/os/SystemService.java
@@ -0,0 +1,31 @@
+/*
+ * 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.os;
+
+/** @hide */
+public class SystemService
+{
+ /** Request that the init daemon start a named service. */
+ public static void start(String name) {
+ SystemProperties.set("ctl.start", name);
+ }
+
+ /** Request that the init daemon stop a named service. */
+ public static void stop(String name) {
+ SystemProperties.set("ctl.stop", name);
+ }
+}
diff --git a/core/java/android/os/TokenWatcher.java b/core/java/android/os/TokenWatcher.java
new file mode 100755
index 0000000..ac3cc92
--- /dev/null
+++ b/core/java/android/os/TokenWatcher.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os;
+
+import java.util.WeakHashMap;
+import java.util.Set;
+import android.util.Log;
+
+/**
+ * Helper class that helps you use IBinder objects as reference counted
+ * tokens. IBinders make good tokens because we find out when they are
+ * removed
+ *
+ */
+public abstract class TokenWatcher
+{
+ /**
+ * Construct the TokenWatcher
+ *
+ * @param h A handler to call {@link #acquired} and {@link #released}
+ * on. If you don't care, just call it like this, although your thread
+ * will have to be a Looper thread.
+ * <code>new TokenWatcher(new Handler())</code>
+ * @param tag A debugging tag for this TokenWatcher
+ */
+ public TokenWatcher(Handler h, String tag)
+ {
+ mHandler = h;
+ mTag = tag != null ? tag : "TokenWatcher";
+ }
+
+ /**
+ * Called when the number of active tokens goes from 0 to 1.
+ */
+ public abstract void acquired();
+
+ /**
+ * Called when the number of active tokens goes from 1 to 0.
+ */
+ public abstract void released();
+
+ /**
+ * Record that this token has been acquired. When acquire is called, and
+ * the current count is 0, the acquired method is called on the given
+ * handler.
+ *
+ * @param token An IBinder object. If this token has already been acquired,
+ * no action is taken.
+ * @param tag A string used by the {@link #dump} method for debugging,
+ * to see who has references.
+ */
+ public void acquire(IBinder token, String tag)
+ {
+ synchronized (mTokens) {
+ // explicitly checked to avoid bogus sendNotification calls because
+ // of the WeakHashMap and the GC
+ int oldSize = mTokens.size();
+
+ Death d = new Death(token, tag);
+ try {
+ token.linkToDeath(d, 0);
+ } catch (RemoteException e) {
+ return;
+ }
+ mTokens.put(token, d);
+
+ if (oldSize == 0 && !mAcquired) {
+ sendNotificationLocked(true);
+ mAcquired = true;
+ }
+ }
+ }
+
+ public void cleanup(IBinder token, boolean unlink)
+ {
+ synchronized (mTokens) {
+ Death d = mTokens.remove(token);
+ if (unlink && d != null) {
+ d.token.unlinkToDeath(d, 0);
+ d.token = null;
+ }
+
+ if (mTokens.size() == 0 && mAcquired) {
+ sendNotificationLocked(false);
+ mAcquired = false;
+ }
+ }
+ }
+
+ public void release(IBinder token)
+ {
+ cleanup(token, true);
+ }
+
+ public boolean isAcquired()
+ {
+ synchronized (mTokens) {
+ return mAcquired;
+ }
+ }
+
+ public void dump()
+ {
+ synchronized (mTokens) {
+ Set<IBinder> keys = mTokens.keySet();
+ Log.i(mTag, "Token count: " + mTokens.size());
+ int i = 0;
+ for (IBinder b: keys) {
+ Log.i(mTag, "[" + i + "] " + mTokens.get(b).tag + " - " + b);
+ i++;
+ }
+ }
+ }
+
+ private Runnable mNotificationTask = new Runnable() {
+ public void run()
+ {
+ int value;
+ synchronized (mTokens) {
+ value = mNotificationQueue;
+ mNotificationQueue = -1;
+ }
+ if (value == 1) {
+ acquired();
+ }
+ else if (value == 0) {
+ released();
+ }
+ }
+ };
+
+ private void sendNotificationLocked(boolean on)
+ {
+ int value = on ? 1 : 0;
+ if (mNotificationQueue == -1) {
+ // empty
+ mNotificationQueue = value;
+ mHandler.post(mNotificationTask);
+ }
+ else if (mNotificationQueue != value) {
+ // it's a pair, so cancel it
+ mNotificationQueue = -1;
+ mHandler.removeCallbacks(mNotificationTask);
+ }
+ // else, same so do nothing -- maybe we should warn?
+ }
+
+ private class Death implements IBinder.DeathRecipient
+ {
+ IBinder token;
+ String tag;
+
+ Death(IBinder token, String tag)
+ {
+ this.token = token;
+ this.tag = tag;
+ }
+
+ public void binderDied()
+ {
+ cleanup(token, false);
+ }
+
+ protected void finalize() throws Throwable
+ {
+ try {
+ if (token != null) {
+ Log.w(mTag, "cleaning up leaked reference: " + tag);
+ release(token);
+ }
+ }
+ finally {
+ super.finalize();
+ }
+ }
+ }
+
+ private WeakHashMap<IBinder,Death> mTokens = new WeakHashMap<IBinder,Death>();
+ private Handler mHandler;
+ private String mTag;
+ private int mNotificationQueue = -1;
+ private volatile boolean mAcquired = false;
+}
diff --git a/core/java/android/os/UEventObserver.java b/core/java/android/os/UEventObserver.java
new file mode 100644
index 0000000..b924e84
--- /dev/null
+++ b/core/java/android/os/UEventObserver.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/**
+ * UEventObserver is an abstract class that receives UEvent's from the kernel.<p>
+ *
+ * Subclass UEventObserver, implementing onUEvent(UEvent event), then call
+ * startObserving() with a match string. The UEvent thread will then call your
+ * onUEvent() method when a UEvent occurs that contains your match string.<p>
+ *
+ * Call stopObserving() to stop receiving UEvent's.<p>
+ *
+ * There is only one UEvent thread per process, even if that process has
+ * multiple UEventObserver subclass instances. The UEvent thread starts when
+ * the startObserving() is called for the first time in that process. Once
+ * started the UEvent thread will not stop (although it can stop notifying
+ * UEventObserver's via stopObserving()).<p>
+ *
+ * @hide
+*/
+public abstract class UEventObserver {
+ private static final String TAG = UEventObserver.class.getSimpleName();
+
+ /**
+ * Representation of a UEvent.
+ */
+ static public class UEvent {
+ // collection of key=value pairs parsed from the uevent message
+ public HashMap<String,String> mMap = new HashMap<String,String>();
+
+ public UEvent(String message) {
+ int offset = 0;
+ int length = message.length();
+
+ while (offset < length) {
+ int equals = message.indexOf('=', offset);
+ int at = message.indexOf(0, offset);
+ if (at < 0) break;
+
+ if (equals > offset && equals < at) {
+ // key is before the equals sign, and value is after
+ mMap.put(message.substring(offset, equals),
+ message.substring(equals + 1, at));
+ }
+
+ offset = at + 1;
+ }
+ }
+
+ public String get(String key) {
+ return mMap.get(key);
+ }
+
+ public String get(String key, String defaultValue) {
+ String result = mMap.get(key);
+ return (result == null ? defaultValue : result);
+ }
+
+ public String toString() {
+ return mMap.toString();
+ }
+ }
+
+ private static UEventThread sThread;
+ private static boolean sThreadStarted = false;
+
+ private static class UEventThread extends Thread {
+ /** Many to many mapping of string match to observer.
+ * Multimap would be better, but not available in android, so use
+ * an ArrayList where even elements are the String match and odd
+ * elements the corresponding UEventObserver observer */
+ private ArrayList<Object> mObservers = new ArrayList<Object>();
+
+ UEventThread() {
+ super("UEventObserver");
+ }
+
+ public void run() {
+ native_setup();
+
+ byte[] buffer = new byte[1024];
+ int len;
+ while (true) {
+ len = next_event(buffer);
+ if (len > 0) {
+ String bufferStr = new String(buffer, 0, len); // easier to search a String
+ synchronized (mObservers) {
+ for (int i = 0; i < mObservers.size(); i += 2) {
+ if (bufferStr.indexOf((String)mObservers.get(i)) != -1) {
+ ((UEventObserver)mObservers.get(i+1))
+ .onUEvent(new UEvent(bufferStr));
+ }
+ }
+ }
+ }
+ }
+ }
+ public void addObserver(String match, UEventObserver observer) {
+ synchronized(mObservers) {
+ mObservers.add(match);
+ mObservers.add(observer);
+ }
+ }
+ /** Removes every key/value pair where value=observer from mObservers */
+ public void removeObserver(UEventObserver observer) {
+ synchronized(mObservers) {
+ boolean found = true;
+ while (found) {
+ found = false;
+ for (int i = 0; i < mObservers.size(); i += 2) {
+ if (mObservers.get(i+1) == observer) {
+ mObservers.remove(i+1);
+ mObservers.remove(i);
+ found = true;
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private static native void native_setup();
+ private static native int next_event(byte[] buffer);
+
+ private static final synchronized void ensureThreadStarted() {
+ if (sThreadStarted == false) {
+ sThread = new UEventThread();
+ sThread.start();
+ sThreadStarted = true;
+ }
+ }
+
+ /**
+ * Begin observation of UEvent's.<p>
+ * This method will cause the UEvent thread to start if this is the first
+ * invocation of startObserving in this process.<p>
+ * Once called, the UEvent thread will call onUEvent() when an incoming
+ * UEvent matches the specified string.<p>
+ * This method can be called multiple times to register multiple matches.
+ * Only one call to stopObserving is required even with multiple registered
+ * matches.
+ * @param match A substring of the UEvent to match. Use "" to match all
+ * UEvent's
+ */
+ public final synchronized void startObserving(String match) {
+ ensureThreadStarted();
+ sThread.addObserver(match, this);
+ }
+
+ /**
+ * End observation of UEvent's.<p>
+ * This process's UEvent thread will never call onUEvent() on this
+ * UEventObserver after this call. Repeated calls have no effect.
+ */
+ public final synchronized void stopObserving() {
+ sThread.removeObserver(this);
+ }
+
+ /**
+ * Subclasses of UEventObserver should override this method to handle
+ * UEvents.
+ */
+ public abstract void onUEvent(UEvent event);
+
+ protected void finalize() throws Throwable {
+ try {
+ stopObserving();
+ } finally {
+ super.finalize();
+ }
+ }
+}
diff --git a/core/java/android/os/Vibrator.java b/core/java/android/os/Vibrator.java
new file mode 100644
index 0000000..0f75289
--- /dev/null
+++ b/core/java/android/os/Vibrator.java
@@ -0,0 +1,86 @@
+/*
+ * 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.os;
+
+/**
+ * Class that operates the vibrator on the device.
+ * <p>
+ * If your process exits, any vibration you started with will stop.
+ */
+public class Vibrator
+{
+ IHardwareService mService;
+
+ /** @hide */
+ public Vibrator()
+ {
+ mService = IHardwareService.Stub.asInterface(
+ ServiceManager.getService("hardware"));
+ }
+
+ /**
+ * Turn the vibrator on.
+ *
+ * @param milliseconds How long to vibrate for.
+ */
+ public void vibrate(long milliseconds)
+ {
+ try {
+ mService.vibrate(milliseconds);
+ } catch (RemoteException e) {
+ }
+ }
+
+ /**
+ * Vibrate with a given pattern.
+ *
+ * <p>
+ * Pass in an array of ints that are the times at which to turn on or off
+ * the vibrator. The first one is how long to wait before turning it on,
+ * and then after that it alternates. If you want to repeat, pass the
+ * index into the pattern at which to start the repeat.
+ *
+ * @param pattern an array of longs of times to turn the vibrator on or off.
+ * @param repeat the index into pattern at which to repeat, or -1 if
+ * you don't want to repeat.
+ */
+ public void vibrate(long[] pattern, int repeat)
+ {
+ // catch this here because the server will do nothing. pattern may
+ // not be null, let that be checked, because the server will drop it
+ // anyway
+ if (repeat < pattern.length) {
+ try {
+ mService.vibratePattern(pattern, repeat, new Binder());
+ } catch (RemoteException e) {
+ }
+ } else {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+ }
+
+ /**
+ * Turn the vibrator off.
+ */
+ public void cancel()
+ {
+ try {
+ mService.cancelVibrate();
+ } catch (RemoteException e) {
+ }
+ }
+}
diff --git a/core/java/android/os/package.html b/core/java/android/os/package.html
new file mode 100644
index 0000000..fb0ecda
--- /dev/null
+++ b/core/java/android/os/package.html
@@ -0,0 +1,6 @@
+<HTML>
+<BODY>
+Provides basic operating system services, message passing, and inter-process
+communication on the device.
+</BODY>
+</HTML> \ No newline at end of file
diff --git a/core/java/android/package.html b/core/java/android/package.html
new file mode 100644
index 0000000..b6d2999
--- /dev/null
+++ b/core/java/android/package.html
@@ -0,0 +1,10 @@
+<HTML>
+<BODY>
+Contains the resource classes used by standard Android applications.
+<p>
+This package contains the resource classes that Android defines to be used in
+Android applications. Third party developers can use many of them also for their applications.
+To learn more about how to use these classes, and what a
+resource is, see <a href="{@docRoot}devel/resources-i18n.html">Resources</a>.
+</BODY>
+</HTML>
diff --git a/core/java/android/pim/ContactsAsyncHelper.java b/core/java/android/pim/ContactsAsyncHelper.java
new file mode 100644
index 0000000..a21281e
--- /dev/null
+++ b/core/java/android/pim/ContactsAsyncHelper.java
@@ -0,0 +1,336 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.pim;
+
+import com.android.internal.telephony.CallerInfo;
+import com.android.internal.telephony.Connection;
+
+import android.content.ContentUris;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.provider.Contacts;
+import android.provider.Contacts.People;
+import android.util.Log;
+import android.view.View;
+import android.widget.ImageView;
+
+import java.io.InputStream;
+
+/**
+ * Helper class for async access of images.
+ */
+public class ContactsAsyncHelper extends Handler {
+
+ private static final boolean DBG = false;
+ private static final String LOG_TAG = "ContactsAsyncHelper";
+
+ /**
+ * Interface for a WorkerHandler result return.
+ */
+ public interface OnImageLoadCompleteListener {
+ /**
+ * Called when the image load is complete.
+ *
+ * @param imagePresent true if an image was found
+ */
+ public void onImageLoadComplete(int token, Object cookie, ImageView iView,
+ boolean imagePresent);
+ }
+
+ // constants
+ private static final int EVENT_LOAD_IMAGE = 1;
+ private static final int DEFAULT_TOKEN = -1;
+
+ // static objects
+ private static Handler sThreadHandler;
+ private static ContactsAsyncHelper sInstance;
+
+ static {
+ sInstance = new ContactsAsyncHelper();
+ }
+
+ private static final class WorkerArgs {
+ public Context context;
+ public ImageView view;
+ public Uri uri;
+ public int defaultResource;
+ public Object result;
+ public Object cookie;
+ public OnImageLoadCompleteListener listener;
+ public CallerInfo info;
+ }
+
+ /**
+ * public inner class to help out the ContactsAsyncHelper callers
+ * with tracking the state of the CallerInfo Queries and image
+ * loading.
+ *
+ * Logic contained herein is used to remove the race conditions
+ * that exist as the CallerInfo queries run and mix with the image
+ * loads, which then mix with the Phone state changes.
+ */
+ public static class ImageTracker {
+
+ // Image display states
+ public static final int DISPLAY_UNDEFINED = 0;
+ public static final int DISPLAY_IMAGE = -1;
+ public static final int DISPLAY_DEFAULT = -2;
+
+ // State of the image on the imageview.
+ private CallerInfo mCurrentCallerInfo;
+ private int displayMode;
+
+ public ImageTracker() {
+ mCurrentCallerInfo = null;
+ displayMode = DISPLAY_UNDEFINED;
+ }
+
+ /**
+ * Used to see if the requested call / connection has a
+ * different caller attached to it than the one we currently
+ * have in the CallCard.
+ */
+ public boolean isDifferentImageRequest(CallerInfo ci) {
+ // note, since the connections are around for the lifetime of the
+ // call, and the CallerInfo-related items as well, we can
+ // definitely use a simple != comparison.
+ return (mCurrentCallerInfo != ci);
+ }
+
+ public boolean isDifferentImageRequest(Connection connection) {
+ // if the connection does not exist, see if the
+ // mCurrentCallerInfo is also null to match.
+ if (connection == null) {
+ if (DBG) Log.d(LOG_TAG, "isDifferentImageRequest: connection is null");
+ return (mCurrentCallerInfo != null);
+ }
+ Object o = connection.getUserData();
+
+ // if the call does NOT have a callerInfo attached
+ // then it is ok to query.
+ boolean runQuery = true;
+ if (o instanceof CallerInfo) {
+ runQuery = isDifferentImageRequest((CallerInfo) o);
+ }
+ return runQuery;
+ }
+
+ /**
+ * Simple setter for the CallerInfo object.
+ */
+ public void setPhotoRequest(CallerInfo ci) {
+ mCurrentCallerInfo = ci;
+ }
+
+ /**
+ * Convenience method used to retrieve the URI
+ * representing the Photo file recorded in the attached
+ * CallerInfo Object.
+ */
+ public Uri getPhotoUri() {
+ if (mCurrentCallerInfo != null) {
+ return ContentUris.withAppendedId(People.CONTENT_URI,
+ mCurrentCallerInfo.person_id);
+ }
+ return null;
+ }
+
+ /**
+ * Simple setter for the Photo state.
+ */
+ public void setPhotoState(int state) {
+ displayMode = state;
+ }
+
+ /**
+ * Simple getter for the Photo state.
+ */
+ public int getPhotoState() {
+ return displayMode;
+ }
+ }
+
+ /**
+ * Thread worker class that handles the task of opening the stream and loading
+ * the images.
+ */
+ private class WorkerHandler extends Handler {
+ public WorkerHandler(Looper looper) {
+ super(looper);
+ }
+
+ public void handleMessage(Message msg) {
+ WorkerArgs args = (WorkerArgs) msg.obj;
+
+ switch (msg.arg1) {
+ case EVENT_LOAD_IMAGE:
+ InputStream inputStream = Contacts.People.openContactPhotoInputStream(
+ args.context.getContentResolver(), args.uri);
+ if (inputStream != null) {
+ args.result = Drawable.createFromStream(inputStream, args.uri.toString());
+
+ if (DBG) Log.d(LOG_TAG, "Loading image: " + msg.arg1 +
+ " token: " + msg.what + " image URI: " + args.uri);
+ } else {
+ args.result = null;
+ if (DBG) Log.d(LOG_TAG, "Problem with image: " + msg.arg1 +
+ " token: " + msg.what + " image URI: " + args.uri +
+ ", using default image.");
+ }
+ break;
+ default:
+ }
+
+ // send the reply to the enclosing class.
+ Message reply = ContactsAsyncHelper.this.obtainMessage(msg.what);
+ reply.arg1 = msg.arg1;
+ reply.obj = msg.obj;
+ reply.sendToTarget();
+ }
+ }
+
+ /**
+ * Private constructor for static class
+ */
+ private ContactsAsyncHelper() {
+ HandlerThread thread = new HandlerThread("ContactsAsyncWorker");
+ thread.start();
+ sThreadHandler = new WorkerHandler(thread.getLooper());
+ }
+
+ /**
+ * Convenience method for calls that do not want to deal with listeners and tokens.
+ */
+ public static final void updateImageViewWithContactPhotoAsync(Context context,
+ ImageView imageView, Uri person, int placeholderImageResource) {
+ // Added additional Cookie field in the callee.
+ updateImageViewWithContactPhotoAsync (null, DEFAULT_TOKEN, null, null, context,
+ imageView, person, placeholderImageResource);
+ }
+
+ /**
+ * Convenience method for calls that do not want to deal with listeners and tokens, but have
+ * a CallerInfo object to cache the image to.
+ */
+ public static final void updateImageViewWithContactPhotoAsync(CallerInfo info, Context context,
+ ImageView imageView, Uri person, int placeholderImageResource) {
+ // Added additional Cookie field in the callee.
+ updateImageViewWithContactPhotoAsync (info, DEFAULT_TOKEN, null, null, context,
+ imageView, person, placeholderImageResource);
+ }
+
+
+ /**
+ * Start an image load, attach the result to the specified CallerInfo object.
+ * Note, when the query is started, we make the ImageView INVISIBLE if the
+ * placeholderImageResource value is -1. When we're given a valid (!= -1)
+ * placeholderImageResource value, we make sure the image is visible.
+ */
+ public static final void updateImageViewWithContactPhotoAsync(CallerInfo info, int token,
+ OnImageLoadCompleteListener listener, Object cookie, Context context,
+ ImageView imageView, Uri person, int placeholderImageResource) {
+
+ // in case the source caller info is null, the URI will be null as well.
+ // just update using the placeholder image in this case.
+ if (person == null) {
+ if (DBG) Log.d(LOG_TAG, "target image is null, just display placeholder.");
+ imageView.setVisibility(View.VISIBLE);
+ imageView.setImageResource(placeholderImageResource);
+ return;
+ }
+
+ // Added additional Cookie field in the callee to handle arguments
+ // sent to the callback function.
+
+ // setup arguments
+ WorkerArgs args = new WorkerArgs();
+ args.cookie = cookie;
+ args.context = context;
+ args.view = imageView;
+ args.uri = person;
+ args.defaultResource = placeholderImageResource;
+ args.listener = listener;
+ args.info = info;
+
+ // setup message arguments
+ Message msg = sThreadHandler.obtainMessage(token);
+ msg.arg1 = EVENT_LOAD_IMAGE;
+ msg.obj = args;
+
+ if (DBG) Log.d(LOG_TAG, "Begin loading image: " + args.uri +
+ ", displaying default image for now.");
+
+ // set the default image first, when the query is complete, we will
+ // replace the image with the correct one.
+ if (placeholderImageResource != -1) {
+ imageView.setVisibility(View.VISIBLE);
+ imageView.setImageResource(placeholderImageResource);
+ } else {
+ imageView.setVisibility(View.INVISIBLE);
+ }
+
+ // notify the thread to begin working
+ sThreadHandler.sendMessage(msg);
+ }
+
+ /**
+ * Called when loading is done.
+ */
+ @Override
+ public void handleMessage(Message msg) {
+ WorkerArgs args = (WorkerArgs) msg.obj;
+ switch (msg.arg1) {
+ case EVENT_LOAD_IMAGE:
+ boolean imagePresent = false;
+
+ // if the image has been loaded then display it, otherwise set default.
+ // in either case, make sure the image is visible.
+ if (args.result != null) {
+ args.view.setVisibility(View.VISIBLE);
+ args.view.setImageDrawable((Drawable) args.result);
+ // make sure the cached photo data is updated.
+ if (args.info != null) {
+ args.info.cachedPhoto = (Drawable) args.result;
+ }
+ imagePresent = true;
+ } else if (args.defaultResource != -1) {
+ args.view.setVisibility(View.VISIBLE);
+ args.view.setImageResource(args.defaultResource);
+ }
+
+ // Note that the data is cached.
+ if (args.info != null) {
+ args.info.isCachedPhotoCurrent = true;
+ }
+
+ // notify the listener if it is there.
+ if (args.listener != null) {
+ if (DBG) Log.d(LOG_TAG, "Notifying listener: " + args.listener.toString() +
+ " image: " + args.uri + " completed");
+ args.listener.onImageLoadComplete(msg.what, args.cookie, args.view,
+ imagePresent);
+ }
+ break;
+ default:
+ }
+ }
+}
diff --git a/core/java/android/pim/DateException.java b/core/java/android/pim/DateException.java
new file mode 100644
index 0000000..90bfe7f
--- /dev/null
+++ b/core/java/android/pim/DateException.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.pim;
+
+public class DateException extends Exception
+{
+ public DateException(String message)
+ {
+ super(message);
+ }
+}
+
diff --git a/core/java/android/pim/DateFormat.java b/core/java/android/pim/DateFormat.java
new file mode 100644
index 0000000..802e045
--- /dev/null
+++ b/core/java/android/pim/DateFormat.java
@@ -0,0 +1,493 @@
+/*
+ * 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.pim;
+
+import android.content.Context;
+import android.provider.Settings;
+import android.provider.Settings.SettingNotFoundException;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.SpannedString;
+
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.TimeZone;
+
+/**
+ Utility class for producing strings with formatted date/time.
+
+ <p>
+ This class takes as inputs a format string and a representation of a date/time.
+ The format string controls how the output is generated.
+ </p>
+ <p>
+ Formatting characters may be repeated in order to get more detailed representations
+ of that field. For instance, the format character &apos;M&apos; is used to
+ represent the month. Depending on how many times that character is repeated
+ you get a different representation.
+ </p>
+ <p>
+ For the month of September:<br/>
+ M -&gt; 9<br/>
+ MM -&gt; 09<br/>
+ MMM -&gt; Sep<br/>
+ MMMM -&gt; September
+ </p>
+ <p>
+ The effects of the duplication vary depending on the nature of the field.
+ See the notes on the individual field formatters for details. For purely numeric
+ fields such as <code>HOUR</code> adding more copies of the designator will
+ zero-pad the value to that number of characters.
+ </p>
+ <p>
+ For 7 minutes past the hour:<br/>
+ m -&gt; 7<br/>
+ mm -&gt; 07<br/>
+ mmm -&gt; 007<br/>
+ mmmm -&gt; 0007
+ </p>
+ <p>
+ Examples for April 6, 1970 at 3:23am:<br/>
+ &quot;MM/dd/yy h:mmaa&quot; -&gt; &quot;04/06/70 3:23am&quot<br/>
+ &quot;MMM dd, yyyy h:mmaa&quot; -&gt; &quot;Apr 6, 1970 3:23am&quot<br/>
+ &quot;MMMM dd, yyyy h:mmaa&quot; -&gt; &quot;April 6, 1970 3:23am&quot<br/>
+ &quot;E, MMMM dd, yyyy h:mmaa&quot; -&gt; &quot;Mon, April 6, 1970 3:23am&<br/>
+ &quot;EEEE, MMMM dd, yyyy h:mmaa&quot; -&gt; &quot;Monday, April 6, 1970 3:23am&quot;<br/>
+ &quot;&apos;Best day evar: &apos;M/d/yy&quot; -&gt; &quot;Best day evar: 4/6/70&quot;
+ */
+
+public class DateFormat {
+ /**
+ Text in the format string that should be copied verbatim rather that
+ interpreted as formatting codes must be surrounded by the <code>QUOTE</code>
+ character. If you need to embed a literal <code>QUOTE</code> character in
+ the output text then use two in a row.
+ */
+ public static final char QUOTE = '\'';
+
+ /**
+ This designator indicates whether the <code>HOUR</code> field is before
+ or after noon. The output is lower-case.
+
+ Examples:
+ a -> a or p
+ aa -> am or pm
+ */
+ public static final char AM_PM = 'a';
+
+ /**
+ This designator indicates whether the <code>HOUR</code> field is before
+ or after noon. The output is capitalized.
+
+ Examples:
+ A -> A or P
+ AA -> AM or PM
+ */
+ public static final char CAPITAL_AM_PM = 'A';
+
+ /**
+ This designator indicates the day of the month.
+
+ Examples for the 9th of the month:
+ d -> 9
+ dd -> 09
+ */
+ public static final char DATE = 'd';
+
+ /**
+ This designator indicates the name of the day of the week.
+
+ Examples for Sunday:
+ E -> Sun
+ EEEE -> Sunday
+ */
+ public static final char DAY = 'E';
+
+ /**
+ This designator indicates the hour of the day in 12 hour format.
+
+ Examples for 3pm:
+ h -> 3
+ hh -> 03
+ */
+ public static final char HOUR = 'h';
+
+ /**
+ This designator indicates the hour of the day in 24 hour format.
+
+ Example for 3pm:
+ k -> 15
+
+ Examples for midnight:
+ k -> 0
+ kk -> 00
+ */
+ public static final char HOUR_OF_DAY = 'k';
+
+ /**
+ This designator indicates the minute of the hour.
+
+ Examples for 7 minutes past the hour:
+ m -> 7
+ mm -> 07
+ */
+ public static final char MINUTE = 'm';
+
+ /**
+ This designator indicates the month of the year
+
+ Examples for September:
+ M -> 9
+ MM -> 09
+ MMM -> Sep
+ MMMM -> September
+ */
+ public static final char MONTH = 'M';
+
+ /**
+ This designator indicates the seconds of the minute.
+
+ Examples for 7 seconds past the minute:
+ s -> 7
+ ss -> 07
+ */
+ public static final char SECONDS = 's';
+
+ /**
+ This designator indicates the offset of the timezone from GMT.
+
+ Example for US/Pacific timezone:
+ z -> -0800
+ zz -> PST
+ */
+ public static final char TIME_ZONE = 'z';
+
+ /**
+ This designator indicates the year.
+
+ Examples for 2006
+ y -> 06
+ yyyy -> 2006
+ */
+ public static final char YEAR = 'y';
+
+ /**
+ * @return true if the user has set the system to use a 24 hour time
+ * format, else false.
+ */
+ public static boolean is24HourFormat(Context context) {
+ String value = Settings.System.getString(context.getContentResolver(),
+ Settings.System.TIME_12_24);
+ boolean b24 = !(value == null || value.equals("12"));
+ return b24;
+ }
+
+ /**
+ * Returns a {@link java.text.DateFormat} object that can format the time according
+ * to the current user preference.
+ * @param context the application context
+ * @return the {@link java.text.DateFormat} object that properly formats the time.
+ */
+ public static final java.text.DateFormat getTimeFormat(Context context) {
+ boolean b24 = is24HourFormat(context);
+ return new java.text.SimpleDateFormat(b24 ? "H:mm" : "h:mm a");
+ }
+
+ /**
+ * Returns a {@link java.text.DateFormat} object that can format the date according
+ * to the current user preference.
+ * @param context the application context
+ * @return the {@link java.text.DateFormat} object that properly formats the date.
+ */
+ public static final java.text.DateFormat getDateFormat(Context context) {
+ String value = getDateFormatString(context);
+ return new java.text.SimpleDateFormat(value);
+ }
+
+ /**
+ * Returns a {@link java.text.DateFormat} object that can format the date
+ * in long form (such as December 31, 1999) based on user preference.
+ * @param context the application context
+ * @return the {@link java.text.DateFormat} object that formats the date in long form.
+ */
+ public static final java.text.DateFormat getLongDateFormat(Context context) {
+ String value = getDateFormatString(context);
+ if (value.indexOf('M') < value.indexOf('d')) {
+ value = "MMMM dd, yyyy";
+ } else {
+ value = "dd MMMM, yyyy";
+ }
+ return new java.text.SimpleDateFormat(value);
+ }
+
+ /**
+ * Gets the current date format stored as a char array. The array will contain
+ * 3 elements ({@link #DATE}, {@link #MONTH}, and {@link #YEAR}) in the order
+ * preferred by the user.
+ */
+ public static final char[] getDateFormatOrder(Context context) {
+ char[] order = new char[] {DATE, MONTH, YEAR};
+ String value = getDateFormatString(context);
+ int index = 0;
+ boolean foundDate = false;
+ boolean foundMonth = false;
+ boolean foundYear = false;
+
+ for (char c : value.toCharArray()) {
+ if (!foundDate && (c == DATE)) {
+ foundDate = true;
+ order[index] = DATE;
+ index++;
+ }
+
+ if (!foundMonth && (c == MONTH)) {
+ foundMonth = true;
+ order[index] = MONTH;
+ index++;
+ }
+
+ if (!foundYear && (c == YEAR)) {
+ foundYear = true;
+ order[index] = YEAR;
+ index++;
+ }
+ }
+ return order;
+ }
+
+ private static String getDateFormatString(Context context) {
+ String value = Settings.System.getString(context.getContentResolver(),
+ Settings.System.DATE_FORMAT);
+ if (value == null || value.length() < 6) {
+ value = "MM-dd-yyyy";
+ }
+ return value;
+ }
+
+ public static final CharSequence format(CharSequence inFormat, long inTimeInMillis) {
+ return format(inFormat, new Date(inTimeInMillis));
+ }
+
+ public static final CharSequence format(CharSequence inFormat, Date inDate) {
+ Calendar c = new GregorianCalendar();
+
+ c.setTime(inDate);
+
+ return format(inFormat, c);
+ }
+
+ public static final CharSequence format(CharSequence inFormat, Calendar inDate) {
+ SpannableStringBuilder s = new SpannableStringBuilder(inFormat);
+ int c;
+ int count;
+
+ int len = inFormat.length();
+
+ for (int i = 0; i < len; i += count) {
+ int temp;
+
+ count = 1;
+ c = s.charAt(i);
+
+ if (c == QUOTE) {
+ count = appendQuotedText(s, i, len);
+ len = s.length();
+ continue;
+ }
+
+ while ((i + count < len) && (s.charAt(i + count) == c)) {
+ count++;
+ }
+
+ String replacement;
+
+ switch (c) {
+ case AM_PM:
+ replacement = DateUtils.getAMPMString(inDate.get(Calendar.AM_PM));
+ break;
+
+ case CAPITAL_AM_PM:
+ //FIXME: this is the same as AM_PM? no capital?
+ replacement = DateUtils.getAMPMString(inDate.get(Calendar.AM_PM));
+ break;
+
+ case DATE:
+ replacement = zeroPad(inDate.get(Calendar.DATE), count);
+ break;
+
+ case DAY:
+ temp = inDate.get(Calendar.DAY_OF_WEEK);
+ replacement = DateUtils.getDayOfWeekString(temp,
+ count < 4 ?
+ DateUtils.LENGTH_MEDIUM :
+ DateUtils.LENGTH_LONG);
+ break;
+
+ case HOUR:
+ temp = inDate.get(Calendar.HOUR);
+
+ if (0 == temp)
+ temp = 12;
+
+ replacement = zeroPad(temp, count);
+ break;
+
+ case HOUR_OF_DAY:
+ replacement = zeroPad(inDate.get(Calendar.HOUR_OF_DAY), count);
+ break;
+
+ case MINUTE:
+ replacement = zeroPad(inDate.get(Calendar.MINUTE), count);
+ break;
+
+ case MONTH:
+ replacement = getMonthString(inDate, count);
+ break;
+
+ case SECONDS:
+ replacement = zeroPad(inDate.get(Calendar.SECOND), count);
+ break;
+
+ case TIME_ZONE:
+ replacement = getTimeZoneString(inDate, count);
+ break;
+
+ case YEAR:
+ replacement = getYearString(inDate, count);
+ break;
+
+ default:
+ replacement = null;
+ break;
+ }
+
+ if (replacement != null) {
+ s.replace(i, i + count, replacement);
+ count = replacement.length(); // CARE: count is used in the for loop above
+ len = s.length();
+ }
+ }
+
+ if (inFormat instanceof Spanned)
+ return new SpannedString(s);
+ else
+ return s.toString();
+ }
+
+ private static final String getMonthString(Calendar inDate, int count) {
+ int month = inDate.get(Calendar.MONTH);
+
+ if (count >= 4)
+ return DateUtils.getMonthString(month, DateUtils.LENGTH_LONG);
+ else if (count == 3)
+ return DateUtils.getMonthString(month, DateUtils.LENGTH_MEDIUM);
+ else {
+ // Calendar.JANUARY == 0, so add 1 to month.
+ return zeroPad(month+1, count);
+ }
+ }
+
+ private static final String getTimeZoneString(Calendar inDate, int count) {
+ TimeZone tz = inDate.getTimeZone();
+
+ if (count < 2) { // FIXME: shouldn't this be <= 2 ?
+ return formatZoneOffset(inDate.get(Calendar.DST_OFFSET) +
+ inDate.get(Calendar.ZONE_OFFSET),
+ count);
+ } else {
+ boolean dst = inDate.get(Calendar.DST_OFFSET) != 0;
+ return tz.getDisplayName(dst, TimeZone.SHORT);
+ }
+ }
+
+ private static final String formatZoneOffset(int offset, int count) {
+ offset /= 1000; // milliseconds to seconds
+ StringBuilder tb = new StringBuilder();
+
+ if (offset < 0) {
+ tb.insert(0, "-");
+ offset = -offset;
+ } else {
+ tb.insert(0, "+");
+ }
+
+ int hours = offset / 3600;
+ int minutes = (offset % 3600) / 60;
+
+ tb.append(zeroPad(hours, 2));
+ tb.append(zeroPad(minutes, 2));
+ return tb.toString();
+ }
+
+ private static final String getYearString(Calendar inDate, int count) {
+ int year = inDate.get(Calendar.YEAR);
+ return (count <= 2) ? zeroPad(year % 100, 2) : String.valueOf(year);
+ }
+
+ private static final int appendQuotedText(SpannableStringBuilder s, int i, int len) {
+ if (i + 1 < len && s.charAt(i + 1) == QUOTE) {
+ s.delete(i, i + 1);
+ return 1;
+ }
+
+ int count = 0;
+
+ // delete leading quote
+ s.delete(i, i + 1);
+ len--;
+
+ while (i < len) {
+ char c = s.charAt(i);
+
+ if (c == QUOTE) {
+ // QUOTEQUOTE -> QUOTE
+ if (i + 1 < len && s.charAt(i + 1) == QUOTE) {
+
+ s.delete(i, i + 1);
+ len--;
+ count++;
+ i++;
+ } else {
+ // Closing QUOTE ends quoted text copying
+ s.delete(i, i + 1);
+ break;
+ }
+ } else {
+ i++;
+ count++;
+ }
+ }
+
+ return count;
+ }
+
+ private static final String zeroPad(int inValue, int inMinDigits) {
+ String val = String.valueOf(inValue);
+
+ if (val.length() < inMinDigits) {
+ char[] buf = new char[inMinDigits];
+
+ for (int i = 0; i < inMinDigits; i++)
+ buf[i] = '0';
+
+ val.getChars(0, val.length(), buf, inMinDigits - val.length());
+ val = new String(buf);
+ }
+ return val;
+ }
+}
diff --git a/core/java/android/pim/DateUtils.java b/core/java/android/pim/DateUtils.java
new file mode 100644
index 0000000..2a01f12
--- /dev/null
+++ b/core/java/android/pim/DateUtils.java
@@ -0,0 +1,1408 @@
+/*
+ * 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.pim;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.TimeZone;
+
+import com.android.internal.R;
+
+/**
+ */
+public class DateUtils
+{
+ private static final String TAG = "DateUtils";
+
+ private static final Object sLock = new Object();
+ private static final int[] sDaysLong = new int[] {
+ com.android.internal.R.string.day_of_week_long_sunday,
+ com.android.internal.R.string.day_of_week_long_monday,
+ com.android.internal.R.string.day_of_week_long_tuesday,
+ com.android.internal.R.string.day_of_week_long_wednesday,
+ com.android.internal.R.string.day_of_week_long_thursday,
+ com.android.internal.R.string.day_of_week_long_friday,
+ com.android.internal.R.string.day_of_week_long_saturday,
+ };
+ private static final int[] sDaysMedium = new int[] {
+ com.android.internal.R.string.day_of_week_medium_sunday,
+ com.android.internal.R.string.day_of_week_medium_monday,
+ com.android.internal.R.string.day_of_week_medium_tuesday,
+ com.android.internal.R.string.day_of_week_medium_wednesday,
+ com.android.internal.R.string.day_of_week_medium_thursday,
+ com.android.internal.R.string.day_of_week_medium_friday,
+ com.android.internal.R.string.day_of_week_medium_saturday,
+ };
+ private static final int[] sDaysShort = new int[] {
+ com.android.internal.R.string.day_of_week_short_sunday,
+ com.android.internal.R.string.day_of_week_short_monday,
+ com.android.internal.R.string.day_of_week_short_tuesday,
+ com.android.internal.R.string.day_of_week_short_wednesday,
+ com.android.internal.R.string.day_of_week_short_thursday,
+ com.android.internal.R.string.day_of_week_short_friday,
+ com.android.internal.R.string.day_of_week_short_saturday,
+ };
+ private static final int[] sDaysShorter = new int[] {
+ com.android.internal.R.string.day_of_week_shorter_sunday,
+ com.android.internal.R.string.day_of_week_shorter_monday,
+ com.android.internal.R.string.day_of_week_shorter_tuesday,
+ com.android.internal.R.string.day_of_week_shorter_wednesday,
+ com.android.internal.R.string.day_of_week_shorter_thursday,
+ com.android.internal.R.string.day_of_week_shorter_friday,
+ com.android.internal.R.string.day_of_week_shorter_saturday,
+ };
+ private static final int[] sDaysShortest = new int[] {
+ com.android.internal.R.string.day_of_week_shortest_sunday,
+ com.android.internal.R.string.day_of_week_shortest_monday,
+ com.android.internal.R.string.day_of_week_shortest_tuesday,
+ com.android.internal.R.string.day_of_week_shortest_wednesday,
+ com.android.internal.R.string.day_of_week_shortest_thursday,
+ com.android.internal.R.string.day_of_week_shortest_friday,
+ com.android.internal.R.string.day_of_week_shortest_saturday,
+ };
+ private static final int[] sMonthsLong = new int [] {
+ com.android.internal.R.string.month_long_january,
+ com.android.internal.R.string.month_long_february,
+ com.android.internal.R.string.month_long_march,
+ com.android.internal.R.string.month_long_april,
+ com.android.internal.R.string.month_long_may,
+ com.android.internal.R.string.month_long_june,
+ com.android.internal.R.string.month_long_july,
+ com.android.internal.R.string.month_long_august,
+ com.android.internal.R.string.month_long_september,
+ com.android.internal.R.string.month_long_october,
+ com.android.internal.R.string.month_long_november,
+ com.android.internal.R.string.month_long_december,
+ };
+ private static final int[] sMonthsMedium = new int [] {
+ com.android.internal.R.string.month_medium_january,
+ com.android.internal.R.string.month_medium_february,
+ com.android.internal.R.string.month_medium_march,
+ com.android.internal.R.string.month_medium_april,
+ com.android.internal.R.string.month_medium_may,
+ com.android.internal.R.string.month_medium_june,
+ com.android.internal.R.string.month_medium_july,
+ com.android.internal.R.string.month_medium_august,
+ com.android.internal.R.string.month_medium_september,
+ com.android.internal.R.string.month_medium_october,
+ com.android.internal.R.string.month_medium_november,
+ com.android.internal.R.string.month_medium_december,
+ };
+ private static final int[] sMonthsShortest = new int [] {
+ com.android.internal.R.string.month_shortest_january,
+ com.android.internal.R.string.month_shortest_february,
+ com.android.internal.R.string.month_shortest_march,
+ com.android.internal.R.string.month_shortest_april,
+ com.android.internal.R.string.month_shortest_may,
+ com.android.internal.R.string.month_shortest_june,
+ com.android.internal.R.string.month_shortest_july,
+ com.android.internal.R.string.month_shortest_august,
+ com.android.internal.R.string.month_shortest_september,
+ com.android.internal.R.string.month_shortest_october,
+ com.android.internal.R.string.month_shortest_november,
+ com.android.internal.R.string.month_shortest_december,
+ };
+ private static final int[] sAmPm = new int[] {
+ com.android.internal.R.string.am,
+ com.android.internal.R.string.pm,
+ };
+ private static int sFirstDay;
+ private static Configuration sLastConfig;
+ private static String sStatusDateFormat;
+ private static String 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;
+ public static final long HOUR_IN_MILLIS = MINUTE_IN_MILLIS * 60;
+ public static final long DAY_IN_MILLIS = HOUR_IN_MILLIS * 24;
+ public static final long WEEK_IN_MILLIS = DAY_IN_MILLIS * 7;
+ public static final long YEAR_IN_MILLIS = WEEK_IN_MILLIS * 52;
+
+ // The following FORMAT_* symbols are used for specifying the format of
+ // dates and times in the formatDateRange method.
+ public static final int FORMAT_SHOW_TIME = 0x00001;
+ public static final int FORMAT_SHOW_WEEKDAY = 0x00002;
+ public static final int FORMAT_SHOW_YEAR = 0x00004;
+ public static final int FORMAT_NO_YEAR = 0x00008;
+ public static final int FORMAT_SHOW_DATE = 0x00010;
+ public static final int FORMAT_NO_MONTH_DAY = 0x00020;
+ public static final int FORMAT_24HOUR = 0x00040;
+ public static final int FORMAT_CAP_AMPM = 0x00080;
+ public static final int FORMAT_NO_NOON = 0x00100;
+ public static final int FORMAT_CAP_NOON = 0x00200;
+ public static final int FORMAT_NO_MIDNIGHT = 0x00400;
+ public static final int FORMAT_CAP_MIDNIGHT = 0x00800;
+ public static final int FORMAT_UTC = 0x01000;
+ public static final int FORMAT_ABBREV_TIME = 0x02000;
+ public static final int FORMAT_ABBREV_WEEKDAY = 0x04000;
+ public static final int FORMAT_ABBREV_MONTH = 0x08000;
+ public static final int FORMAT_NUMERIC_DATE = 0x10000;
+ public static final int FORMAT_ABBREV_ALL = (FORMAT_ABBREV_TIME
+ | FORMAT_ABBREV_WEEKDAY | FORMAT_ABBREV_MONTH);
+ public static final int FORMAT_CAP_NOON_MIDNIGHT = (FORMAT_CAP_NOON | FORMAT_CAP_MIDNIGHT);
+ public static final int FORMAT_NO_NOON_MIDNIGHT = (FORMAT_NO_NOON | FORMAT_NO_MIDNIGHT);
+
+ // Date and time format strings that are constant and don't need to be
+ // translated.
+ public static final String HOUR_MINUTE_24 = "%H:%M";
+ public static final String HOUR_MINUTE_AMPM = "%-l:%M%P";
+ public static final String HOUR_MINUTE_CAP_AMPM = "%-l:%M%p";
+ public static final String HOUR_AMPM = "%-l%P";
+ public static final String HOUR_CAP_AMPM = "%-l%p";
+ public static final String MONTH_FORMAT = "%B";
+ public static final String ABBREV_MONTH_FORMAT = "%b";
+ public static final String NUMERIC_MONTH_FORMAT = "%m";
+ public static final String MONTH_DAY_FORMAT = "%-d";
+ public static final String YEAR_FORMAT = "%Y";
+ 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:
+ // {showTime, showYear, showWeekDay}. For example, if showYear and
+ // showWeekDay are both true, then the index would be 3.
+ public static final int sameYearTable[] = {
+ com.android.internal.R.string.same_year_md1_md2,
+ com.android.internal.R.string.same_year_wday1_md1_wday2_md2,
+ com.android.internal.R.string.same_year_mdy1_mdy2,
+ com.android.internal.R.string.same_year_wday1_mdy1_wday2_mdy2,
+ com.android.internal.R.string.same_year_md1_time1_md2_time2,
+ com.android.internal.R.string.same_year_wday1_md1_time1_wday2_md2_time2,
+ com.android.internal.R.string.same_year_mdy1_time1_mdy2_time2,
+ com.android.internal.R.string.same_year_wday1_mdy1_time1_wday2_mdy2_time2,
+
+ // Numeric date strings
+ com.android.internal.R.string.numeric_md1_md2,
+ com.android.internal.R.string.numeric_wday1_md1_wday2_md2,
+ com.android.internal.R.string.numeric_mdy1_mdy2,
+ com.android.internal.R.string.numeric_wday1_mdy1_wday2_mdy2,
+ com.android.internal.R.string.numeric_md1_time1_md2_time2,
+ com.android.internal.R.string.numeric_wday1_md1_time1_wday2_md2_time2,
+ 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:
+ // {showTime, showYear, showWeekDay}. For example, if showYear and
+ // showWeekDay are both true, then the index would be 3.
+ public static final int sameMonthTable[] = {
+ com.android.internal.R.string.same_month_md1_md2,
+ com.android.internal.R.string.same_month_wday1_md1_wday2_md2,
+ com.android.internal.R.string.same_month_mdy1_mdy2,
+ com.android.internal.R.string.same_month_wday1_mdy1_wday2_mdy2,
+ com.android.internal.R.string.same_month_md1_time1_md2_time2,
+ com.android.internal.R.string.same_month_wday1_md1_time1_wday2_md2_time2,
+ com.android.internal.R.string.same_month_mdy1_time1_mdy2_time2,
+ com.android.internal.R.string.same_month_wday1_mdy1_time1_wday2_mdy2_time2,
+
+ com.android.internal.R.string.numeric_md1_md2,
+ com.android.internal.R.string.numeric_wday1_md1_wday2_md2,
+ com.android.internal.R.string.numeric_mdy1_mdy2,
+ com.android.internal.R.string.numeric_wday1_mdy1_wday2_mdy2,
+ com.android.internal.R.string.numeric_md1_time1_md2_time2,
+ com.android.internal.R.string.numeric_wday1_md1_time1_wday2_md2_time2,
+ com.android.internal.R.string.numeric_mdy1_time1_mdy2_time2,
+ com.android.internal.R.string.numeric_wday1_mdy1_time1_wday2_mdy2_time2,
+ };
+
+ /**
+ * Request the full spelled-out name.
+ * For use with the 'abbrev' parameter of {@link #getDayOfWeekStr} and {@link #getMonthStr}.
+ * @more
+ * <p>e.g. "Sunday" or "January"
+ */
+ public static final int LENGTH_LONG = 10;
+
+ /**
+ * Request an abbreviated version of the name.
+ * For use with the 'abbrev' parameter of {@link #getDayOfWeekStr} and {@link #getMonthStr}.
+ * @more
+ * <p>e.g. "Sun" or "Jan"
+ */
+ public static final int LENGTH_MEDIUM = 20;
+
+ /**
+ * Request a shorter abbreviated version of the name.
+ * For use with the 'abbrev' parameter of {@link #getDayOfWeekStr} and {@link #getMonthStr}.
+ * @more
+ * <p>e.g. "Su" or "Jan"
+ * <p>In some languages, the results returned for LENGTH_SHORT may be the same as
+ * return for {@link #LENGTH_MEDIUM}.
+ */
+ public static final int LENGTH_SHORT = 30;
+
+ /**
+ * Request an even shorter abbreviated version of the name.
+ * For use with the 'abbrev' parameter of {@link #getDayOfWeekStr} and {@link #getMonthStr}.
+ * @more
+ * <p>e.g. "M", "Tu", "Th" or "J"
+ * <p>In some languages, the results returned for LENGTH_SHORTEST may be the same as
+ * return for {@link #LENGTH_SHORTER}.
+ */
+ public static final int LENGTH_SHORTER = 40;
+
+ /**
+ * Request an even shorter abbreviated version of the name.
+ * For use with the 'abbrev' parameter of {@link #getDayOfWeekStr} and {@link #getMonthStr}.
+ * @more
+ * <p>e.g. "S", "T", "T" or "J"
+ * <p>In some languages, the results returned for LENGTH_SHORTEST may be the same as
+ * return for {@link #LENGTH_SHORTER}.
+ */
+ public static final int LENGTH_SHORTEST = 50;
+
+
+ /**
+ * Return a string for the day of the week.
+ * @param dayOfWeek One of {@link #Calendar.SUNDAY Calendar.SUNDAY},
+ * {@link #Calendar.MONDAY Calendar.MONDAY}, etc.
+ * @param abbrev One of {@link #LENGTH_LONG}, {@link #LENGTH_SHORT}, {@link #LENGTH_SHORTER}
+ * or {@link #LENGTH_SHORTEST}. For forward compatibility, anything else
+ * will return the same as {#LENGTH_MEDIUM}.
+ * @throws IndexOutOfBoundsException if the dayOfWeek is out of bounds.
+ */
+ public static String getDayOfWeekString(int dayOfWeek, int abbrev) {
+ int[] list;
+ switch (abbrev) {
+ case LENGTH_LONG: list = sDaysLong; break;
+ case LENGTH_MEDIUM: list = sDaysMedium; break;
+ case LENGTH_SHORT: list = sDaysShort; break;
+ case LENGTH_SHORTER: list = sDaysShorter; break;
+ case LENGTH_SHORTEST: list = sDaysShortest; break;
+ default: list = sDaysMedium; break;
+ }
+
+ Resources r = Resources.getSystem();
+ return r.getString(list[dayOfWeek - Calendar.SUNDAY]);
+ }
+
+ /**
+ * Return a string for AM or PM.
+ * @param ampm Either {@link Calendar#AM Calendar.AM} or {@link Calendar#PM Calendar.PM}.
+ * @throws IndexOutOfBoundsException if the ampm is out of bounds.
+ */
+ public static String getAMPMString(int ampm) {
+ Resources r = Resources.getSystem();
+ return r.getString(sAmPm[ampm - Calendar.AM]);
+ }
+
+ /**
+ * Return a string for the day of the week.
+ * @param month One of {@link #Calendar.JANUARY Calendar.JANUARY},
+ * {@link #Calendar.FEBRUARY Calendar.FEBRUARY}, etc.
+ * @param abbrev One of {@link #LENGTH_LONG}, {@link #LENGTH_SHORT}, {@link #LENGTH_SHORTER}
+ * or {@link #LENGTH_SHORTEST}. For forward compatibility, anything else
+ * will return the same as {#LENGTH_MEDIUM}.
+ */
+ public static String getMonthString(int month, int abbrev) {
+ // Note that here we use sMonthsMedium for MEDIUM, SHORT and SHORTER.
+ // This is a shortcut to not spam the translators with too many variations
+ // of the same string. If we find that in a language the distinction
+ // is necessary, we can can add more without changing this API.
+ int[] list;
+ switch (abbrev) {
+ case LENGTH_LONG: list = sMonthsLong; break;
+ case LENGTH_MEDIUM: list = sMonthsMedium; break;
+ case LENGTH_SHORT: list = sMonthsMedium; break;
+ case LENGTH_SHORTER: list = sMonthsMedium; break;
+ case LENGTH_SHORTEST: list = sMonthsShortest; break;
+ default: list = sMonthsMedium; break;
+ }
+
+ Resources r = Resources.getSystem();
+ return r.getString(list[month - Calendar.JANUARY]);
+ }
+
+ public static CharSequence getRelativeTimeSpanString(long startTime) {
+ return getRelativeTimeSpanString(startTime, System.currentTimeMillis(), MINUTE_IN_MILLIS);
+ }
+
+ /**
+ * Returns a string describing 'time' as a time relative to 'now'.
+ * <p>
+ * Time spans in the past are formatted like "42 minutes ago".
+ * Time spans in the future are formatted like "in 42 minutes".
+ *
+ * @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 3 seconds in the
+ * past will be reported as "0 minutes ago" if this is set to MINUTE_IN_MILLIS. Pass one of
+ * 0, MINUTE_IN_MILLIS, HOUR_IN_MILLIS, DAY_IN_MILLIS, WEEK_IN_MILLIS
+ */
+ public static CharSequence getRelativeTimeSpanString(long time, long now, long minResolution) {
+ Resources r = Resources.getSystem();
+
+ // TODO: Assembling strings by hand like this is bad style for i18n.
+ boolean past = (now > time);
+ String prefix = past ? null : r.getString(com.android.internal.R.string.in);
+ String postfix = past ? r.getString(com.android.internal.R.string.ago) : null;
+ return getRelativeTimeSpanString(time, now, minResolution, prefix, postfix);
+ }
+
+ public static CharSequence getRelativeTimeSpanString(long time, long now, long minResolution,
+ String prefix, String postfix) {
+ Resources r = Resources.getSystem();
+
+ long duration = Math.abs(now - time);
+
+ if (duration < MINUTE_IN_MILLIS && minResolution < MINUTE_IN_MILLIS) {
+ long count = duration / SECOND_IN_MILLIS;
+ String singular = r.getString(com.android.internal.R.string.second);
+ String plural = r.getString(com.android.internal.R.string.seconds);
+ return pluralizedSpan(count, singular, plural, prefix, postfix);
+ }
+
+ if (duration < HOUR_IN_MILLIS && minResolution < HOUR_IN_MILLIS) {
+ long count = duration / MINUTE_IN_MILLIS;
+ String singular = r.getString(com.android.internal.R.string.minute);
+ String plural = r.getString(com.android.internal.R.string.minutes);
+ return pluralizedSpan(count, singular, plural, prefix, postfix);
+ }
+
+ if (duration < DAY_IN_MILLIS && minResolution < DAY_IN_MILLIS) {
+ long count = duration / HOUR_IN_MILLIS;
+ String singular = r.getString(com.android.internal.R.string.hour);
+ String plural = r.getString(com.android.internal.R.string.hours);
+ return pluralizedSpan(count, singular, plural, prefix, postfix);
+ }
+
+ if (duration < WEEK_IN_MILLIS && minResolution < WEEK_IN_MILLIS) {
+ return getRelativeDayString(r, time, now);
+ }
+
+ return dateString(time);
+ }
+
+
+ private static final String pluralizedSpan(long count, String singular, String plural,
+ String prefix, String postfix) {
+ StringBuilder s = new StringBuilder();
+
+ if (prefix != null) {
+ s.append(prefix);
+ s.append(" ");
+ }
+
+ s.append(count);
+ s.append(' ');
+ s.append(count == 0 || count > 1 ? plural : singular);
+
+ if (postfix != null) {
+ s.append(" ");
+ s.append(postfix);
+ }
+
+ return s.toString();
+ }
+
+ /**
+ * 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
+ * @return a formatting string
+ */
+ private static final String getRelativeDayString(Resources r, long day, long today) {
+ Time startTime = new Time();
+ startTime.set(day);
+ Time currentTime = new Time();
+ currentTime.set(today);
+
+ int startDay = Time.getJulianDay(day, startTime.gmtoff);
+ int currentDay = Time.getJulianDay(today, currentTime.gmtoff);
+
+ int days = Math.abs(currentDay - startDay);
+ boolean past = (today > day);
+
+ if (days == 1) {
+ if (past) {
+ return r.getString(com.android.internal.R.string.yesterday);
+ } else {
+ return r.getString(com.android.internal.R.string.tomorrow);
+ }
+ } else if (days == 0) {
+ return r.getString(com.android.internal.R.string.today);
+ }
+
+ if (!past) {
+ return r.getString(com.android.internal.R.string.daysDurationFuturePlural, days);
+ } else {
+ return r.getString(com.android.internal.R.string.daysDurationPastPlural, days);
+ }
+ }
+
+ private static void initFormatStrings() {
+ synchronized (sLock) {
+ Resources r = Resources.getSystem();
+ Configuration cfg = r.getConfiguration();
+ if (sLastConfig == null || !sLastConfig.equals(cfg)) {
+ sLastConfig = cfg;
+ sStatusTimeFormat = r.getString(com.android.internal.R.string.status_bar_time_format);
+ sStatusDateFormat = r.getString(com.android.internal.R.string.status_bar_date_format);
+ sElapsedFormatMMSS = r.getString(com.android.internal.R.string.elapsed_time_short_format_mm_ss);
+ sElapsedFormatHMMSS = r.getString(com.android.internal.R.string.elapsed_time_short_format_h_mm_ss);
+ }
+ }
+ }
+
+ /**
+ * Format a time so it appears like it would in the status bar clock.
+ * @deprecated use {@link #DateFormat.getTimeFormat(Context)} instead.
+ * @hide
+ */
+ public static final CharSequence timeString(long millis) {
+ initFormatStrings();
+ return DateFormat.format(sStatusTimeFormat, millis);
+ }
+
+ /**
+ * Format a date so it appears like it would in the status bar clock.
+ * @deprecated use {@link #DateFormat.getDateFormat(Context)} instead.
+ * @hide
+ */
+ public static final CharSequence dateString(long startTime) {
+ initFormatStrings();
+ return DateFormat.format(sStatusDateFormat, startTime);
+ }
+
+ /**
+ * Formats an elapsed time like MM:SS or H:MM:SS
+ * for display on the call-in-progress screen.
+ */
+ public static String formatElapsedTime(long elapsedSeconds) {
+ initFormatStrings();
+
+ long hours = 0;
+ long minutes = 0;
+ long seconds = 0;
+
+ if (elapsedSeconds >= 3600) {
+ hours = elapsedSeconds / 3600;
+ elapsedSeconds -= hours * 3600;
+ }
+ if (elapsedSeconds >= 60) {
+ minutes = elapsedSeconds / 60;
+ elapsedSeconds -= minutes * 60;
+ }
+ seconds = elapsedSeconds;
+
+ String result;
+ if (hours > 0) {
+ return formatElapsedTime(sElapsedFormatHMMSS, hours, minutes, seconds);
+ } else {
+ return formatElapsedTime(sElapsedFormatMMSS, minutes, seconds);
+ }
+ }
+
+ /**
+ * Fast formatting of h:mm:ss
+ */
+ private static String formatElapsedTime(String format, long hours, long minutes, long seconds) {
+ if (FAST_FORMAT_HMMSS.equals(format)) {
+ StringBuffer sb = new StringBuffer(16);
+ sb.append(hours);
+ sb.append(TIME_SEPARATOR);
+ if (minutes < 10) {
+ sb.append(TIME_PADDING);
+ } else {
+ sb.append(toDigitChar(minutes / 10));
+ }
+ sb.append(toDigitChar(minutes % 10));
+ sb.append(TIME_SEPARATOR);
+ if (seconds < 10) {
+ sb.append(TIME_PADDING);
+ } else {
+ sb.append(toDigitChar(seconds / 10));
+ }
+ sb.append(toDigitChar(seconds % 10));
+ return sb.toString();
+ } else {
+ return String.format(format, hours, minutes, seconds);
+ }
+ }
+
+ /**
+ * Fast formatting of m:ss
+ */
+ private static String formatElapsedTime(String format, long minutes, long seconds) {
+ if (FAST_FORMAT_MMSS.equals(format)) {
+ StringBuffer sb = new StringBuffer(16);
+ if (minutes < 10) {
+ sb.append(TIME_PADDING);
+ } else {
+ sb.append(toDigitChar(minutes / 10));
+ }
+ sb.append(toDigitChar(minutes % 10));
+ sb.append(TIME_SEPARATOR);
+ if (seconds < 10) {
+ sb.append(TIME_PADDING);
+ } else {
+ sb.append(toDigitChar(seconds / 10));
+ }
+ sb.append(toDigitChar(seconds % 10));
+ return sb.toString();
+ } else {
+ return String.format(format, minutes, seconds);
+ }
+ }
+
+ 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},
+ * {@link java.text.DateFormat#LONG},
+ * {@link java.text.DateFormat#MEDIUM}
+ * or
+ * {@link java.text.DateFormat#SHORT}
+ *
+ * @param then the date to format
+ * @param now the base time
+ * @param dateStyle how to format the date portion.
+ * @param timeStyle how to format the time portion.
+ */
+ public static final CharSequence formatSameDayTime(long then, long now,
+ int dateStyle, int timeStyle) {
+ Calendar thenCal = new GregorianCalendar();
+ thenCal.setTimeInMillis(then);
+ Date thenDate = thenCal.getTime();
+ Calendar nowCal = new GregorianCalendar();
+ nowCal.setTimeInMillis(now);
+
+ java.text.DateFormat f;
+
+ if (thenCal.get(Calendar.YEAR) == nowCal.get(Calendar.YEAR)
+ && thenCal.get(Calendar.MONTH) == nowCal.get(Calendar.MONTH)
+ && thenCal.get(Calendar.DAY_OF_MONTH) == nowCal.get(Calendar.DAY_OF_MONTH)) {
+ f = java.text.DateFormat.getTimeInstance(timeStyle);
+ } else {
+ f = java.text.DateFormat.getDateInstance(dateStyle);
+ }
+ return f.format(thenDate);
+ }
+
+ /**
+ * @hide
+ * @deprecated use {@link android.pim.Time}
+ */
+ public static Calendar newCalendar(boolean zulu)
+ {
+ if (zulu)
+ return Calendar.getInstance(TimeZone.getTimeZone("GMT"));
+
+ return Calendar.getInstance();
+ }
+
+ /**
+ * @return true if the supplied when is today else false
+ */
+ 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)
+ && (thenMonthDay == time.monthDay);
+ }
+
+ /**
+ * @hide
+ * @deprecated use {@link android.pim.Time}
+ */
+ private static final int ctoi(String str, int index)
+ throws DateException
+ {
+ char c = str.charAt(index);
+ if (c >= '0' && c <= '9') {
+ return (int)(c - '0');
+ }
+ throw new DateException("Expected numeric character. Got '" +
+ c + "'");
+ }
+
+ /**
+ * @hide
+ * @deprecated use {@link android.pim.Time}
+ */
+ private static final int check(int lowerBound, int upperBound, int value)
+ throws DateException
+ {
+ if (value >= lowerBound && value <= upperBound) {
+ return value;
+ }
+ throw new DateException("field out of bounds. max=" + upperBound
+ + " value=" + value);
+ }
+
+ /**
+ * @hide
+ * @deprecated use {@link android.pim.Time}
+ * Return true if this date string is local time
+ */
+ public static boolean isUTC(String s)
+ {
+ if (s.length() == 16 && s.charAt(15) == 'Z') {
+ return true;
+ }
+ if (s.length() == 9 && s.charAt(8) == 'Z') {
+ // XXX not sure if this case possible/valid
+ return true;
+ }
+ return false;
+ }
+
+
+ // note that month in Calendar is 0 based and in all other human
+ // representations, it's 1 based.
+ // Returns if the Z was present, meaning that the time is in UTC
+ /**
+ * @hide
+ * @deprecated use {@link android.pim.Time}
+ */
+ public static boolean parseDateTime(String str, Calendar cal)
+ throws DateException
+ {
+ int len = str.length();
+ boolean dateTime = (len == 15 || len == 16) && str.charAt(8) == 'T';
+ boolean justDate = len == 8;
+ if (dateTime || justDate) {
+ cal.clear();
+ cal.set(Calendar.YEAR,
+ ctoi(str, 0)*1000 + ctoi(str, 1)*100
+ + ctoi(str, 2)*10 + ctoi(str, 3));
+ cal.set(Calendar.MONTH,
+ check(0, 11, ctoi(str, 4)*10 + ctoi(str, 5) - 1));
+ cal.set(Calendar.DAY_OF_MONTH,
+ check(1, 31, ctoi(str, 6)*10 + ctoi(str, 7)));
+ if (dateTime) {
+ cal.set(Calendar.HOUR_OF_DAY,
+ check(0, 23, ctoi(str, 9)*10 + ctoi(str, 10)));
+ cal.set(Calendar.MINUTE,
+ check(0, 59, ctoi(str, 11)*10 + ctoi(str, 12)));
+ cal.set(Calendar.SECOND,
+ check(0, 59, ctoi(str, 13)*10 + ctoi(str, 14)));
+ }
+ if (justDate) {
+ cal.set(Calendar.HOUR_OF_DAY, 0);
+ cal.set(Calendar.MINUTE, 0);
+ cal.set(Calendar.SECOND, 0);
+ return true;
+ }
+ if (len == 15) {
+ return false;
+ }
+ if (str.charAt(15) == 'Z') {
+ return true;
+ }
+ }
+ throw new DateException("Invalid time (expected "
+ + "YYYYMMDDThhmmssZ? got '" + str + "').");
+ }
+
+ /**
+ * Given a timezone string which can be null, and a dateTime string,
+ * set that time into a calendar.
+ * @hide
+ * @deprecated use {@link android.pim.Time}
+ */
+ public static void parseDateTime(String tz, String dateTime, Calendar out)
+ throws DateException
+ {
+ TimeZone timezone;
+ if (DateUtils.isUTC(dateTime)) {
+ timezone = TimeZone.getTimeZone("UTC");
+ }
+ else if (tz == null) {
+ timezone = TimeZone.getDefault();
+ }
+ else {
+ timezone = TimeZone.getTimeZone(tz);
+ }
+
+ Calendar local = new GregorianCalendar(timezone);
+ DateUtils.parseDateTime(dateTime, local);
+
+ out.setTimeInMillis(local.getTimeInMillis());
+ }
+
+
+ /**
+ * Return a string containing the date and time in RFC2445 format.
+ * Ensures that the time is written in UTC. The Calendar class doesn't
+ * really help out with this, so this is slower than it ought to be.
+ *
+ * @param cal the date and time to write
+ * @hide
+ * @deprecated use {@link android.pim.Time}
+ */
+ public static String writeDateTime(Calendar cal)
+ {
+ TimeZone tz = TimeZone.getTimeZone("GMT");
+ GregorianCalendar c = new GregorianCalendar(tz);
+ c.setTimeInMillis(cal.getTimeInMillis());
+ return writeDateTime(c, true);
+ }
+
+ /**
+ * Return a string containing the date and time in RFC2445 format.
+ *
+ * @param cal the date and time to write
+ * @param zulu If the calendar is in UTC, pass true, and a Z will
+ * be written at the end as per RFC2445. Otherwise, the time is
+ * considered in localtime.
+ * @hide
+ * @deprecated use {@link android.pim.Time}
+ */
+ public static String writeDateTime(Calendar cal, boolean zulu)
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.ensureCapacity(16);
+ if (zulu) {
+ sb.setLength(16);
+ sb.setCharAt(15, 'Z');
+ } else {
+ sb.setLength(15);
+ }
+ return writeDateTime(cal, sb);
+ }
+
+ /**
+ * Return a string containing the date and time in RFC2445 format.
+ *
+ * @param cal the date and time to write
+ * @param sb a StringBuilder to use. It is assumed that setLength
+ * has already been called on sb to the appropriate length
+ * which is sb.setLength(zulu ? 16 : 15)
+ * @hide
+ * @deprecated use {@link android.pim.Time}
+ */
+ public static String writeDateTime(Calendar cal, StringBuilder sb)
+ {
+ int n;
+
+ n = cal.get(Calendar.YEAR);
+ sb.setCharAt(3, (char)('0'+n%10));
+ n /= 10;
+ sb.setCharAt(2, (char)('0'+n%10));
+ n /= 10;
+ sb.setCharAt(1, (char)('0'+n%10));
+ n /= 10;
+ sb.setCharAt(0, (char)('0'+n%10));
+
+ n = cal.get(Calendar.MONTH) + 1;
+ sb.setCharAt(5, (char)('0'+n%10));
+ n /= 10;
+ sb.setCharAt(4, (char)('0'+n%10));
+
+ n = cal.get(Calendar.DAY_OF_MONTH);
+ sb.setCharAt(7, (char)('0'+n%10));
+ n /= 10;
+ sb.setCharAt(6, (char)('0'+n%10));
+
+ sb.setCharAt(8, 'T');
+
+ n = cal.get(Calendar.HOUR_OF_DAY);
+ sb.setCharAt(10, (char)('0'+n%10));
+ n /= 10;
+ sb.setCharAt(9, (char)('0'+n%10));
+
+ n = cal.get(Calendar.MINUTE);
+ sb.setCharAt(12, (char)('0'+n%10));
+ n /= 10;
+ sb.setCharAt(11, (char)('0'+n%10));
+
+ n = cal.get(Calendar.SECOND);
+ sb.setCharAt(14, (char)('0'+n%10));
+ n /= 10;
+ sb.setCharAt(13, (char)('0'+n%10));
+
+ return sb.toString();
+ }
+
+ /**
+ * @hide
+ * @deprecated use {@link android.pim.Time}
+ */
+ public static void assign(Calendar lval, Calendar rval)
+ {
+ // there should be a faster way.
+ lval.clear();
+ lval.setTimeInMillis(rval.getTimeInMillis());
+ }
+
+ /**
+ * Creates a string describing a date/time range. The flags argument
+ * is a bitmask of options from the following list:
+ *
+ * <ul>
+ * <li>FORMAT_SHOW_TIME</li>
+ * <li>FORMAT_SHOW_WEEKDAY</li>
+ * <li>FORMAT_SHOW_YEAR</li>
+ * <li>FORMAT_NO_YEAR</li>
+ * <li>FORMAT_SHOW_DATE</li>
+ * <li>FORMAT_NO_MONTH_DAY</li>
+ * <li>FORMAT_24HOUR</li>
+ * <li>FORMAT_CAP_AMPM</li>
+ * <li>FORMAT_NO_NOON</li>
+ * <li>FORMAT_CAP_NOON</li>
+ * <li>FORMAT_NO_MIDNIGHT</li>
+ * <li>FORMAT_CAP_MIDNIGHT</li>
+ * <li>FORMAT_UTC</li>
+ * <li>FORMAT_ABBREV_TIME</li>
+ * <li>FORMAT_ABBREV_WEEKDAY</li>
+ * <li>FORMAT_ABBREV_MONTH</li>
+ * <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.
+ * If neither FORMAT_SHOW_YEAR nor FORMAT_NO_YEAR are set, then the year
+ * is shown only if it is different from the current year, or if the start
+ * and end dates fall on different years.
+ *
+ * <p>
+ * 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.
+ *
+ * <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".
+ *
+ * <p>
+ * If FORMAT_NO_MIDNIGHT is set and 12-hour time is used, then "12am" is
+ * shown instead of "midnight".
+ *
+ * <p>
+ * If FORMAT_CAP_NOON is set and 12-hour time is used, then "Midnight" is
+ * shown instead of "midnight".
+ *
+ * <p>
+ * If FORMAT_24HOUR is set and the time is shown, then the time is
+ * shown in the 24-hour time format.
+ *
+ * <p>
+ * If FORMAT_UTC is set, then the UTC timezone is used for the start
+ * and end milliseconds.
+ *
+ * <p>
+ * If FORMAT_ABBREV_TIME is set and FORMAT_24HOUR is not set, 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>
+ * Example output strings:
+ * <ul>
+ * <li>10:15am</li>
+ * <li>3:00pm - 4:00pm</li>
+ * <li>3pm - 4pm</li>
+ * <li>3PM - 4PM</li>
+ * <li>08:00 - 17:00</li>
+ * <li>Oct 9</li>
+ * <li>Tue, Oct 9</li>
+ * <li>October 9, 2007</li>
+ * <li>Oct 9 - 10</li>
+ * <li>Oct 9 - 10, 2007</li>
+ * <li>Oct 28 - Nov 3, 2007</li>
+ * <li>Dec 31, 2007 - Jan 1, 2008</li>
+ * <li>Oct 9, 8:00am - Oct 10, 5:00pm</li>
+ * </ul>
+ * @param startMillis the start time in UTC milliseconds
+ * @param endMillis the end time in UTC milliseconds
+ * @param flags a bit mask of options
+ *
+ * @return a string with the formatted date/time range.
+ */
+ public static String formatDateRange(long startMillis, long endMillis, int flags) {
+ Resources res = Resources.getSystem();
+ boolean showTime = (flags & FORMAT_SHOW_TIME) != 0;
+ boolean showWeekDay = (flags & FORMAT_SHOW_WEEKDAY) != 0;
+ boolean showYear = (flags & FORMAT_SHOW_YEAR) != 0;
+ boolean noYear = (flags & FORMAT_NO_YEAR) != 0;
+ boolean useUTC = (flags & FORMAT_UTC) != 0;
+ boolean abbrevWeekDay = (flags & FORMAT_ABBREV_WEEKDAY) != 0;
+ boolean abbrevMonth = (flags & FORMAT_ABBREV_MONTH) != 0;
+ boolean use24Hour = (flags & FORMAT_24HOUR) != 0;
+ boolean noMonthDay = (flags & FORMAT_NO_MONTH_DAY) != 0;
+ boolean numericDate = (flags & FORMAT_NUMERIC_DATE) != 0;
+
+ Time startDate;
+ Time endDate;
+
+ if (useUTC) {
+ startDate = new Time(Time.TIMEZONE_UTC);
+ endDate = new Time(Time.TIMEZONE_UTC);
+ } else {
+ startDate = new Time();
+ endDate = new Time();
+ }
+
+ startDate.set(startMillis);
+ endDate.set(endMillis);
+ int startJulianDay = Time.getJulianDay(startMillis, startDate.gmtoff);
+ int endJulianDay = Time.getJulianDay(endMillis, endDate.gmtoff);
+ int 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 an 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 ((endDate.hour | endDate.minute | endDate.second) == 0
+ && (!showTime || dayDistance <= 1) && (startMillis != endMillis)) {
+ endDate.monthDay -= 1;
+ endDate.normalize(true /* ignore isDst */);
+ }
+
+ int startDay = startDate.monthDay;
+ int startMonthNum = startDate.month;
+ int startYear = startDate.year;
+
+ int endDay = endDate.monthDay;
+ int endMonthNum = endDate.month;
+ int endYear = endDate.year;
+
+ String startWeekDayString = "";
+ String endWeekDayString = "";
+ if (showWeekDay) {
+ String weekDayFormat = "";
+ if (abbrevWeekDay) {
+ weekDayFormat = ABBREV_WEEKDAY_FORMAT;
+ } else {
+ weekDayFormat = WEEKDAY_FORMAT;
+ }
+ startWeekDayString = startDate.format(weekDayFormat);
+ endWeekDayString = endDate.format(weekDayFormat);
+ }
+
+ String startTimeString = "";
+ String endTimeString = "";
+ if (showTime) {
+ String startTimeFormat = "";
+ String endTimeFormat = "";
+ if (use24Hour) {
+ startTimeFormat = HOUR_MINUTE_24;
+ endTimeFormat = HOUR_MINUTE_24;
+ } else {
+ boolean abbrevTime = (flags & FORMAT_ABBREV_TIME) != 0;
+ boolean capAMPM = (flags & FORMAT_CAP_AMPM) != 0;
+ boolean noNoon = (flags & FORMAT_NO_NOON) != 0;
+ boolean capNoon = (flags & FORMAT_CAP_NOON) != 0;
+ boolean noMidnight = (flags & FORMAT_NO_MIDNIGHT) != 0;
+ boolean capMidnight = (flags & FORMAT_CAP_MIDNIGHT) != 0;
+
+ boolean startOnTheHour = startDate.minute == 0 && startDate.second == 0;
+ boolean endOnTheHour = endDate.minute == 0 && endDate.second == 0;
+ if (abbrevTime && startOnTheHour) {
+ if (capAMPM) {
+ startTimeFormat = HOUR_CAP_AMPM;
+ } else {
+ startTimeFormat = HOUR_AMPM;
+ }
+ } else {
+ if (capAMPM) {
+ startTimeFormat = HOUR_MINUTE_CAP_AMPM;
+ } else {
+ startTimeFormat = HOUR_MINUTE_AMPM;
+ }
+ }
+ if (abbrevTime && endOnTheHour) {
+ if (capAMPM) {
+ endTimeFormat = HOUR_CAP_AMPM;
+ } else {
+ endTimeFormat = HOUR_AMPM;
+ }
+ } else {
+ if (capAMPM) {
+ endTimeFormat = HOUR_MINUTE_CAP_AMPM;
+ } else {
+ endTimeFormat = HOUR_MINUTE_AMPM;
+ }
+ }
+
+ if (startDate.hour == 12 && startOnTheHour && !noNoon) {
+ if (capNoon) {
+ startTimeFormat = res.getString(com.android.internal.R.string.Noon);
+ } else {
+ startTimeFormat = res.getString(com.android.internal.R.string.noon);
+ }
+ // Don't show the start time starting at midnight. Show
+ // 12am instead.
+ }
+
+ if (endDate.hour == 12 && endOnTheHour && !noNoon) {
+ if (capNoon) {
+ endTimeFormat = res.getString(com.android.internal.R.string.Noon);
+ } else {
+ endTimeFormat = res.getString(com.android.internal.R.string.noon);
+ }
+ } else if (endDate.hour == 0 && endOnTheHour && !noMidnight) {
+ if (capMidnight) {
+ endTimeFormat = res.getString(com.android.internal.R.string.Midnight);
+ } else {
+ endTimeFormat = res.getString(com.android.internal.R.string.midnight);
+ }
+ }
+ }
+ startTimeString = startDate.format(startTimeFormat);
+ endTimeString = endDate.format(endTimeFormat);
+ }
+
+ // Get the current year
+ long millis = System.currentTimeMillis();
+ Time time = new Time();
+ time.set(millis);
+ int currentYear = time.year;
+
+ // Show the year if the user specified FORMAT_SHOW_YEAR or if
+ // the starting and end years are different from each other
+ // or from the current year. But don't show the year if the
+ // user specified FORMAT_NO_YEAR;
+ showYear = showYear || (!noYear && (startYear != endYear || startYear != currentYear));
+
+ String defaultDateFormat, fullFormat, dateRange;
+ if (numericDate) {
+ defaultDateFormat = res.getString(com.android.internal.R.string.numeric_date);
+ } else if (showYear) {
+ if (abbrevMonth) {
+ if (noMonthDay) {
+ defaultDateFormat = res.getString(com.android.internal.R.string.abbrev_month_year);
+ } else {
+ defaultDateFormat = res.getString(com.android.internal.R.string.abbrev_month_day_year);
+ }
+ } else {
+ if (noMonthDay) {
+ defaultDateFormat = res.getString(com.android.internal.R.string.month_year);
+ } else {
+ defaultDateFormat = res.getString(com.android.internal.R.string.month_day_year);
+ }
+ }
+ } else {
+ if (abbrevMonth) {
+ if (noMonthDay) {
+ defaultDateFormat = res.getString(com.android.internal.R.string.abbrev_month);
+ } else {
+ defaultDateFormat = res.getString(com.android.internal.R.string.abbrev_month_day);
+ }
+ } else {
+ if (noMonthDay) {
+ defaultDateFormat = res.getString(com.android.internal.R.string.month);
+ } else {
+ defaultDateFormat = res.getString(com.android.internal.R.string.month_day);
+ }
+ }
+ }
+
+ if (showWeekDay) {
+ if (showTime) {
+ fullFormat = res.getString(com.android.internal.R.string.wday1_date1_time1_wday2_date2_time2);
+ } else {
+ fullFormat = res.getString(com.android.internal.R.string.wday1_date1_wday2_date2);
+ }
+ } else {
+ if (showTime) {
+ fullFormat = res.getString(com.android.internal.R.string.date1_time1_date2_time2);
+ } else {
+ fullFormat = res.getString(com.android.internal.R.string.date1_date2);
+ }
+ }
+
+ if (noMonthDay && startMonthNum == endMonthNum) {
+ // Example: "January, 2008"
+ String startDateString = startDate.format(defaultDateFormat);
+ return startDateString;
+ }
+
+ if (startYear != endYear || noMonthDay) {
+ // Different year or we are not showing the month day number.
+ // Example: "December 31, 2007 - January 1, 2008"
+ // Or: "January - February, 2008"
+ String startDateString = startDate.format(defaultDateFormat);
+ String endDateString = endDate.format(defaultDateFormat);
+
+ // The values that are used in a fullFormat string are specified
+ // by position.
+ dateRange = String.format(fullFormat,
+ startWeekDayString, startDateString, startTimeString,
+ endWeekDayString, endDateString, endTimeString);
+ return dateRange;
+ }
+
+ // Get the month, day, and year strings for the start and end dates
+ String monthFormat;
+ if (numericDate) {
+ monthFormat = NUMERIC_MONTH_FORMAT;
+ } else if (abbrevMonth) {
+ monthFormat = ABBREV_MONTH_FORMAT;
+ } else {
+ monthFormat = MONTH_FORMAT;
+ }
+ String startMonthString = startDate.format(monthFormat);
+ String startMonthDayString = startDate.format(MONTH_DAY_FORMAT);
+ String startYearString = startDate.format(YEAR_FORMAT);
+ String endMonthString = endDate.format(monthFormat);
+ String endMonthDayString = endDate.format(MONTH_DAY_FORMAT);
+ String endYearString = endDate.format(YEAR_FORMAT);
+
+ if (startMonthNum != endMonthNum) {
+ // Same year, different month.
+ // Example: "October 28 - November 3"
+ // or: "Wed, Oct 31 - Sat, Nov 3, 2007"
+ // or: "Oct 31, 8am - Sat, Nov 3, 2007, 5pm"
+
+ int index = 0;
+ if (showWeekDay) index = 1;
+ if (showYear) index += 2;
+ if (showTime) index += 4;
+ if (numericDate) index += 8;
+ int resId = sameYearTable[index];
+ fullFormat = res.getString(resId);
+
+ // The values that are used in a fullFormat string are specified
+ // by position.
+ dateRange = String.format(fullFormat,
+ startWeekDayString, startMonthString, startMonthDayString,
+ startYearString, startTimeString,
+ endWeekDayString, endMonthString, endMonthDayString,
+ endYearString, endTimeString);
+ return dateRange;
+ }
+
+ if (startDay != endDay) {
+ // Same month, different day.
+ int index = 0;
+ if (showWeekDay) index = 1;
+ if (showYear) index += 2;
+ if (showTime) index += 4;
+ if (numericDate) index += 8;
+ int resId = sameMonthTable[index];
+ fullFormat = res.getString(resId);
+
+ // The values that are used in a fullFormat string are specified
+ // by position.
+ dateRange = String.format(fullFormat,
+ startWeekDayString, startMonthString, startMonthDayString,
+ startYearString, startTimeString,
+ endWeekDayString, endMonthString, endMonthDayString,
+ endYearString, endTimeString);
+ return dateRange;
+ }
+
+ // Same start and end day
+ boolean showDate = (flags & FORMAT_SHOW_DATE) != 0;
+
+ // If nothing was specified, then show the date.
+ if (!showTime && !showDate && !showWeekDay) showDate = true;
+
+ // Compute the time string (example: "10:00 - 11:00 am")
+ String timeString = "";
+ if (showTime) {
+ // If the start and end time are the same, then just show the
+ // start time.
+ if (startMillis == endMillis) {
+ // Same start and end time.
+ // Example: "10:15 AM"
+ timeString = startTimeString;
+ } else {
+ // Example: "10:00 - 11:00 am"
+ String timeFormat = res.getString(com.android.internal.R.string.time1_time2);
+ timeString = String.format(timeFormat, startTimeString, endTimeString);
+ }
+ }
+
+ // Figure out which full format to use.
+ fullFormat = "";
+ String dateString = "";
+ if (showDate) {
+ dateString = startDate.format(defaultDateFormat);
+ if (showWeekDay) {
+ if (showTime) {
+ // Example: "10:00 - 11:00 am, Tue, Oct 9"
+ fullFormat = res.getString(com.android.internal.R.string.time_wday_date);
+ } else {
+ // Example: "Tue, Oct 9"
+ fullFormat = res.getString(com.android.internal.R.string.wday_date);
+ }
+ } else {
+ if (showTime) {
+ // Example: "10:00 - 11:00 am, Oct 9"
+ fullFormat = res.getString(com.android.internal.R.string.time_date);
+ } else {
+ // Example: "Oct 9"
+ return dateString;
+ }
+ }
+ } else if (showWeekDay) {
+ if (showTime) {
+ // Example: "10:00 - 11:00 am, Tue"
+ fullFormat = res.getString(com.android.internal.R.string.time_wday);
+ } else {
+ // Example: "Tue"
+ return startWeekDayString;
+ }
+ } else if (showTime) {
+ return timeString;
+ }
+
+ // The values that are used in a fullFormat string are specified
+ // by position.
+ dateRange = String.format(fullFormat, timeString, startWeekDayString, dateString);
+ return dateRange;
+ }
+
+ /**
+ * @return a relative time string to display the time expressed by millis. Times
+ * are counted starting at midnight, which means that assuming that the current
+ * time is March 31st, 0:30:
+ * "millis=0:10 today" will be displayed as "0:10"
+ * "millis=11:30pm the day before" will be displayed as "Mar 30"
+ * A similar scheme is used to dates that are a week, a month or more than a year old.
+ *
+ * @param withPreposition If true, the string returned will include the correct
+ * preposition ("at 9:20am", "in 2008" or "on May 29").
+ */
+ public static CharSequence getRelativeTimeSpanString(Context c, long millis,
+ boolean withPreposition) {
+
+ long span = System.currentTimeMillis() - millis;
+
+ Resources res = c.getResources();
+ if (sNowTime == null) {
+ sNowTime = new Time();
+ sThenTime = new Time();
+ sMonthDayFormat = res.getString(com.android.internal.R.string.abbrev_month_day);
+ }
+
+ sNowTime.setToNow();
+ sThenTime.set(millis);
+
+ if (span < DAY_IN_MILLIS && sNowTime.weekDay == sThenTime.weekDay) {
+ // Same day
+ return getPrepositionDate(res, sThenTime, R.string.preposition_for_time,
+ HOUR_MINUTE_CAP_AMPM, withPreposition);
+ } else if (sNowTime.year != sThenTime.year) {
+ // Different years
+ // TODO: take locale into account so that the display will adjust correctly.
+ return getPrepositionDate(res, sThenTime, R.string.preposition_for_year,
+ NUMERIC_MONTH_FORMAT + "/" + MONTH_DAY_FORMAT + "/" + YEAR_FORMAT_TWO_DIGITS,
+ withPreposition);
+ } else {
+ // Default
+ return getPrepositionDate(res, sThenTime, R.string.preposition_for_date,
+ sMonthDayFormat, withPreposition);
+ }
+ }
+
+ /**
+ * @return A date string suitable for display based on the format and including the
+ * date preposition if withPreposition is true.
+ */
+ private static String getPrepositionDate(Resources res, Time thenTime, int id,
+ String formatString, boolean withPreposition) {
+ String result = thenTime.format(formatString);
+ return withPreposition ? res.getString(id, result) : result;
+ }
+
+ public static CharSequence getRelativeTimeSpanString(Context c, long millis) {
+ return getRelativeTimeSpanString(c, millis, false /* no preposition */);
+ }
+
+ private static Time sNowTime;
+ private static Time sThenTime;
+ private static String sMonthDayFormat;
+}
diff --git a/core/java/android/pim/EventRecurrence.java b/core/java/android/pim/EventRecurrence.java
new file mode 100644
index 0000000..ad671f6
--- /dev/null
+++ b/core/java/android/pim/EventRecurrence.java
@@ -0,0 +1,420 @@
+/*
+ * 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.pim;
+
+import android.content.res.Resources;
+import android.text.TextUtils;
+
+import java.util.Calendar;
+
+public class EventRecurrence
+{
+ /**
+ * Thrown when a recurrence string provided can not be parsed according
+ * to RFC2445.
+ */
+ public static class InvalidFormatException extends RuntimeException
+ {
+ InvalidFormatException(String s) {
+ super(s);
+ }
+ }
+
+ public EventRecurrence()
+ {
+ wkst = MO;
+ }
+
+ /**
+ * Parse an iCalendar/RFC2445 recur type according to Section 4.3.10.
+ */
+ public native void parse(String recur);
+
+ public void setStartDate(Time date) {
+ startDate = date;
+ }
+
+ public static final int SECONDLY = 1;
+ public static final int MINUTELY = 2;
+ public static final int HOURLY = 3;
+ public static final int DAILY = 4;
+ public static final int WEEKLY = 5;
+ public static final int MONTHLY = 6;
+ public static final int YEARLY = 7;
+
+ public static final int SU = 0x00010000;
+ public static final int MO = 0x00020000;
+ public static final int TU = 0x00040000;
+ public static final int WE = 0x00080000;
+ public static final int TH = 0x00100000;
+ public static final int FR = 0x00200000;
+ public static final int SA = 0x00400000;
+
+ public Time startDate;
+ public int freq;
+ public String until;
+ public int count;
+ public int interval;
+ public int wkst;
+
+ public int[] bysecond;
+ public int bysecondCount;
+ public int[] byminute;
+ public int byminuteCount;
+ public int[] byhour;
+ public int byhourCount;
+ public int[] byday;
+ public int[] bydayNum;
+ public int bydayCount;
+ public int[] bymonthday;
+ public int bymonthdayCount;
+ public int[] byyearday;
+ public int byyeardayCount;
+ public int[] byweekno;
+ public int byweeknoCount;
+ public int[] bymonth;
+ public int bymonthCount;
+ public int[] bysetpos;
+ public int bysetposCount;
+
+ /**
+ * Converts one of the Calendar.SUNDAY constants to the SU, MO, etc.
+ * constants. btw, I think we should switch to those here too, to
+ * get rid of this function, if possible.
+ */
+ public static int calendarDay2Day(int day)
+ {
+ switch (day)
+ {
+ case Calendar.SUNDAY:
+ return SU;
+ case Calendar.MONDAY:
+ return MO;
+ case Calendar.TUESDAY:
+ return TU;
+ case Calendar.WEDNESDAY:
+ return WE;
+ case Calendar.THURSDAY:
+ return TH;
+ case Calendar.FRIDAY:
+ return FR;
+ case Calendar.SATURDAY:
+ return SA;
+ default:
+ throw new RuntimeException("bad day of week: " + day);
+ }
+ }
+
+ public static int timeDay2Day(int day)
+ {
+ switch (day)
+ {
+ case Time.SUNDAY:
+ return SU;
+ case Time.MONDAY:
+ return MO;
+ case Time.TUESDAY:
+ return TU;
+ case Time.WEDNESDAY:
+ return WE;
+ case Time.THURSDAY:
+ return TH;
+ case Time.FRIDAY:
+ return FR;
+ case Time.SATURDAY:
+ return SA;
+ default:
+ throw new RuntimeException("bad day of week: " + day);
+ }
+ }
+ public static int day2TimeDay(int day)
+ {
+ switch (day)
+ {
+ case SU:
+ return Time.SUNDAY;
+ case MO:
+ return Time.MONDAY;
+ case TU:
+ return Time.TUESDAY;
+ case WE:
+ return Time.WEDNESDAY;
+ case TH:
+ return Time.THURSDAY;
+ case FR:
+ return Time.FRIDAY;
+ case SA:
+ return Time.SATURDAY;
+ default:
+ throw new RuntimeException("bad day of week: " + day);
+ }
+ }
+
+ /**
+ * Converts one of the SU, MO, etc. constants to the Calendar.SUNDAY
+ * constants. btw, I think we should switch to those here too, to
+ * get rid of this function, if possible.
+ */
+ public static int day2CalendarDay(int day)
+ {
+ switch (day)
+ {
+ case SU:
+ return Calendar.SUNDAY;
+ case MO:
+ return Calendar.MONDAY;
+ case TU:
+ return Calendar.TUESDAY;
+ case WE:
+ return Calendar.WEDNESDAY;
+ case TH:
+ return Calendar.THURSDAY;
+ case FR:
+ return Calendar.FRIDAY;
+ case SA:
+ return Calendar.SATURDAY;
+ default:
+ throw new RuntimeException("bad day of week: " + day);
+ }
+ }
+
+ /**
+ * Converts one of the internal day constants (SU, MO, etc.) to the
+ * two-letter string representing that constant.
+ *
+ * @throws IllegalArgumentException Thrown if the day argument is not one of
+ * the defined day constants.
+ *
+ * @param day one the internal constants SU, MO, etc.
+ * @return the two-letter string for the day ("SU", "MO", etc.)
+ */
+ private static String day2String(int day) {
+ switch (day) {
+ case SU:
+ return "SU";
+ case MO:
+ return "MO";
+ case TU:
+ return "TU";
+ case WE:
+ return "WE";
+ case TH:
+ return "TH";
+ case FR:
+ return "FR";
+ case SA:
+ return "SA";
+ default:
+ throw new IllegalArgumentException("bad day argument: " + day);
+ }
+ }
+
+ private static void appendNumbers(StringBuilder s, String label,
+ int count, int[] values)
+ {
+ if (count > 0) {
+ s.append(label);
+ count--;
+ for (int i=0; i<count; i++) {
+ s.append(values[i]);
+ s.append(",");
+ }
+ s.append(values[count]);
+ }
+ }
+
+ private void appendByDay(StringBuilder s, int i)
+ {
+ int n = this.bydayNum[i];
+ if (n != 0) {
+ s.append(n);
+ }
+
+ String str = day2String(this.byday[i]);
+ s.append(str);
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder s = new StringBuilder();
+
+ s.append("FREQ=");
+ switch (this.freq)
+ {
+ case SECONDLY:
+ s.append("SECONDLY");
+ break;
+ case MINUTELY:
+ s.append("MINUTELY");
+ break;
+ case HOURLY:
+ s.append("HOURLY");
+ break;
+ case DAILY:
+ s.append("DAILY");
+ break;
+ case WEEKLY:
+ s.append("WEEKLY");
+ break;
+ case MONTHLY:
+ s.append("MONTHLY");
+ break;
+ case YEARLY:
+ s.append("YEARLY");
+ break;
+ }
+
+ if (!TextUtils.isEmpty(this.until)) {
+ s.append(";UNTIL=");
+ s.append(until);
+ }
+
+ if (this.count != 0) {
+ s.append(";COUNT=");
+ s.append(this.count);
+ }
+
+ if (this.interval != 0) {
+ s.append(";INTERVAL=");
+ s.append(this.interval);
+ }
+
+ if (this.wkst != 0) {
+ s.append(";WKST=");
+ s.append(day2String(this.wkst));
+ }
+
+ appendNumbers(s, ";BYSECOND=", this.bysecondCount, this.bysecond);
+ appendNumbers(s, ";BYMINUTE=", this.byminuteCount, this.byminute);
+ appendNumbers(s, ";BYSECOND=", this.byhourCount, this.byhour);
+
+ // day
+ int count = this.bydayCount;
+ if (count > 0) {
+ s.append(";BYDAY=");
+ count--;
+ for (int i=0; i<count; i++) {
+ appendByDay(s, i);
+ s.append(",");
+ }
+ appendByDay(s, count);
+ }
+
+ appendNumbers(s, ";BYMONTHDAY=", this.bymonthdayCount, this.bymonthday);
+ appendNumbers(s, ";BYYEARDAY=", this.byyeardayCount, this.byyearday);
+ appendNumbers(s, ";BYWEEKNO=", this.byweeknoCount, this.byweekno);
+ appendNumbers(s, ";BYMONTH=", this.bymonthCount, this.bymonth);
+ appendNumbers(s, ";BYSETPOS=", this.bysetposCount, this.bysetpos);
+
+ return s.toString();
+ }
+
+ public String getRepeatString() {
+ Resources r = Resources.getSystem();
+
+ // TODO Implement "Until" portion of string, as well as custom settings
+ switch (this.freq) {
+ case DAILY:
+ return r.getString(com.android.internal.R.string.daily);
+ case WEEKLY: {
+ if (repeatsOnEveryWeekDay()) {
+ return r.getString(com.android.internal.R.string.every_weekday);
+ } else {
+ String format = r.getString(com.android.internal.R.string.weekly);
+ StringBuilder days = new StringBuilder();
+
+ // Do one less iteration in the loop so the last element is added out of the
+ // loop. This is done so the comma is not placed after the last item.
+ int count = this.bydayCount - 1;
+ if (count >= 0) {
+ for (int i = 0 ; i < count ; i++) {
+ days.append(dayToString(r, this.byday[i]));
+ days.append(",");
+ }
+ days.append(dayToString(r, this.byday[count]));
+
+ return String.format(format, days.toString());
+ }
+
+ // There is no "BYDAY" specifier, so use the day of the
+ // first event. For this to work, the setStartDate()
+ // method must have been used by the caller to set the
+ // date of the first event in the recurrence.
+ if (startDate == null) {
+ return null;
+ }
+
+ int day = timeDay2Day(startDate.weekDay);
+ return String.format(format, dayToString(r, day));
+ }
+ }
+ case MONTHLY: {
+ return r.getString(com.android.internal.R.string.monthly);
+ }
+ case YEARLY:
+ return r.getString(com.android.internal.R.string.yearly);
+ }
+
+ return null;
+ }
+
+ public boolean repeatsOnEveryWeekDay() {
+ if (this.freq != WEEKLY) {
+ return false;
+ }
+
+ int count = this.bydayCount;
+ if (count != 5) {
+ return false;
+ }
+
+ for (int i = 0 ; i < count ; i++) {
+ int day = byday[i];
+ if (day == SU || day == SA) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public boolean repeatsMonthlyOnDayCount() {
+ if (this.freq != MONTHLY) {
+ return false;
+ }
+
+ if (bydayCount != 1 || bymonthdayCount != 0) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private String dayToString(Resources r, int day) {
+ switch (day) {
+ case SU: return r.getString(com.android.internal.R.string.sunday);
+ case MO: return r.getString(com.android.internal.R.string.monday);
+ case TU: return r.getString(com.android.internal.R.string.tuesday);
+ case WE: return r.getString(com.android.internal.R.string.wednesday);
+ case TH: return r.getString(com.android.internal.R.string.thursday);
+ case FR: return r.getString(com.android.internal.R.string.friday);
+ case SA: return r.getString(com.android.internal.R.string.saturday);
+ default: throw new IllegalArgumentException("bad day argument: " + day);
+ }
+ }
+}
diff --git a/core/java/android/pim/ICalendar.java b/core/java/android/pim/ICalendar.java
new file mode 100644
index 0000000..4a5d7e4
--- /dev/null
+++ b/core/java/android/pim/ICalendar.java
@@ -0,0 +1,643 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.pim;
+
+import android.util.Log;
+import android.util.Config;
+
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+import java.util.ArrayList;
+
+/**
+ * Parses RFC 2445 iCalendar objects.
+ */
+public class ICalendar {
+
+ private static final String TAG = "Sync";
+
+ // TODO: keep track of VEVENT, VTODO, VJOURNAL, VFREEBUSY, VTIMEZONE, VALARM
+ // components, by type field or by subclass? subclass would allow us to
+ // enforce grammars.
+
+ /**
+ * Exception thrown when an iCalendar object has invalid syntax.
+ */
+ public static class FormatException extends Exception {
+ public FormatException() {
+ super();
+ }
+
+ public FormatException(String msg) {
+ super(msg);
+ }
+
+ public FormatException(String msg, Throwable cause) {
+ super(msg, cause);
+ }
+ }
+
+ /**
+ * A component within an iCalendar (VEVENT, VTODO, VJOURNAL, VFEEBUSY,
+ * VTIMEZONE, VALARM).
+ */
+ public static class Component {
+
+ // components
+ private static final String BEGIN = "BEGIN";
+ private static final String END = "END";
+ private static final String NEWLINE = "\n";
+ public static final String VCALENDAR = "VCALENDAR";
+ public static final String VEVENT = "VEVENT";
+ public static final String VTODO = "VTODO";
+ public static final String VJOURNAL = "VJOURNAL";
+ public static final String VFREEBUSY = "VFREEBUSY";
+ public static final String VTIMEZONE = "VTIMEZONE";
+ public static final String VALARM = "VALARM";
+
+ private final String mName;
+ private final Component mParent; // see if we can get rid of this
+ private LinkedList<Component> mChildren = null;
+ private final LinkedHashMap<String, ArrayList<Property>> mPropsMap =
+ new LinkedHashMap<String, ArrayList<Property>>();
+
+ /**
+ * Creates a new component with the provided name.
+ * @param name The name of the component.
+ */
+ public Component(String name, Component parent) {
+ mName = name;
+ mParent = parent;
+ }
+
+ /**
+ * Returns the name of the component.
+ * @return The name of the component.
+ */
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Returns the parent of this component.
+ * @return The parent of this component.
+ */
+ public Component getParent() {
+ return mParent;
+ }
+
+ /**
+ * Helper that lazily gets/creates the list of children.
+ * @return The list of children.
+ */
+ protected LinkedList<Component> getOrCreateChildren() {
+ if (mChildren == null) {
+ mChildren = new LinkedList<Component>();
+ }
+ return mChildren;
+ }
+
+ /**
+ * Adds a child component to this component.
+ * @param child The child component.
+ */
+ public void addChild(Component child) {
+ getOrCreateChildren().add(child);
+ }
+
+ /**
+ * Returns a list of the Component children of this component. May be
+ * null, if there are no children.
+ *
+ * @return A list of the children.
+ */
+ public List<Component> getComponents() {
+ return mChildren;
+ }
+
+ /**
+ * Adds a Property to this component.
+ * @param prop
+ */
+ public void addProperty(Property prop) {
+ String name= prop.getName();
+ ArrayList<Property> props = mPropsMap.get(name);
+ if (props == null) {
+ props = new ArrayList<Property>();
+ mPropsMap.put(name, props);
+ }
+ props.add(prop);
+ }
+
+ /**
+ * Returns a set of the property names within this component.
+ * @return A set of property names within this component.
+ */
+ public Set<String> getPropertyNames() {
+ return mPropsMap.keySet();
+ }
+
+ /**
+ * Returns a list of properties with the specified name. Returns null
+ * if there are no such properties.
+ * @param name The name of the property that should be returned.
+ * @return A list of properties with the requested name.
+ */
+ public List<Property> getProperties(String name) {
+ return mPropsMap.get(name);
+ }
+
+ /**
+ * Returns the first property with the specified name. Returns null
+ * if there is no such property.
+ * @param name The name of the property that should be returned.
+ * @return The first property with the specified name.
+ */
+ public Property getFirstProperty(String name) {
+ List<Property> props = mPropsMap.get(name);
+ if (props == null || props.size() == 0) {
+ return null;
+ }
+ return props.get(0);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ toString(sb);
+ sb.append(NEWLINE);
+ return sb.toString();
+ }
+
+ /**
+ * Helper method that appends this component to a StringBuilder. The
+ * caller is responsible for appending a newline at the end of the
+ * component.
+ */
+ public void toString(StringBuilder sb) {
+ sb.append(BEGIN);
+ sb.append(":");
+ sb.append(mName);
+ sb.append(NEWLINE);
+
+ // append the properties
+ for (String propertyName : getPropertyNames()) {
+ for (Property property : getProperties(propertyName)) {
+ property.toString(sb);
+ sb.append(NEWLINE);
+ }
+ }
+
+ // append the sub-components
+ if (mChildren != null) {
+ for (Component component : mChildren) {
+ component.toString(sb);
+ sb.append(NEWLINE);
+ }
+ }
+
+ sb.append(END);
+ sb.append(":");
+ sb.append(mName);
+ }
+ }
+
+ /**
+ * A property within an iCalendar component (e.g., DTSTART, DTEND, etc.,
+ * within a VEVENT).
+ */
+ public static class Property {
+ // properties
+ // TODO: do we want to list these here? the complete list is long.
+ public static final String DTSTART = "DTSTART";
+ public static final String DTEND = "DTEND";
+ public static final String DURATION = "DURATION";
+ public static final String RRULE = "RRULE";
+ public static final String RDATE = "RDATE";
+ public static final String EXRULE = "EXRULE";
+ public static final String EXDATE = "EXDATE";
+ // ... need to add more.
+
+ private final String mName;
+ private LinkedHashMap<String, ArrayList<Parameter>> mParamsMap =
+ new LinkedHashMap<String, ArrayList<Parameter>>();
+ private String mValue; // TODO: make this final?
+
+ /**
+ * Creates a new property with the provided name.
+ * @param name The name of the property.
+ */
+ public Property(String name) {
+ mName = name;
+ }
+
+ /**
+ * Creates a new property with the provided name and value.
+ * @param name The name of the property.
+ * @param value The value of the property.
+ */
+ public Property(String name, String value) {
+ mName = name;
+ mValue = value;
+ }
+
+ /**
+ * Returns the name of the property.
+ * @return The name of the property.
+ */
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Returns the value of this property.
+ * @return The value of this property.
+ */
+ public String getValue() {
+ return mValue;
+ }
+
+ /**
+ * Sets the value of this property.
+ * @param value The desired value for this property.
+ */
+ public void setValue(String value) {
+ mValue = value;
+ }
+
+ /**
+ * Adds a {@link Parameter} to this property.
+ * @param param The parameter that should be added.
+ */
+ public void addParameter(Parameter param) {
+ ArrayList<Parameter> params = mParamsMap.get(param.name);
+ if (params == null) {
+ params = new ArrayList<Parameter>();
+ mParamsMap.put(param.name, params);
+ }
+ params.add(param);
+ }
+
+ /**
+ * Returns the set of parameter names for this property.
+ * @return The set of parameter names for this property.
+ */
+ public Set<String> getParameterNames() {
+ return mParamsMap.keySet();
+ }
+
+ /**
+ * Returns the list of parameters with the specified name. May return
+ * null if there are no such parameters.
+ * @param name The name of the parameters that should be returned.
+ * @return The list of parameters with the specified name.
+ */
+ public List<Parameter> getParameters(String name) {
+ return mParamsMap.get(name);
+ }
+
+ /**
+ * Returns the first parameter with the specified name. May return
+ * nll if there is no such parameter.
+ * @param name The name of the parameter that should be returned.
+ * @return The first parameter with the specified name.
+ */
+ public Parameter getFirstParameter(String name) {
+ ArrayList<Parameter> params = mParamsMap.get(name);
+ if (params == null || params.size() == 0) {
+ return null;
+ }
+ return params.get(0);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ toString(sb);
+ return sb.toString();
+ }
+
+ /**
+ * Helper method that appends this property to a StringBuilder. The
+ * caller is responsible for appending a newline after this property.
+ */
+ public void toString(StringBuilder sb) {
+ sb.append(mName);
+ Set<String> parameterNames = getParameterNames();
+ for (String parameterName : parameterNames) {
+ for (Parameter param : getParameters(parameterName)) {
+ sb.append(";");
+ param.toString(sb);
+ }
+ }
+ sb.append(":");
+ sb.append(mValue);
+ }
+ }
+
+ /**
+ * A parameter defined for an iCalendar property.
+ */
+ // TODO: make this a proper class rather than a struct?
+ public static class Parameter {
+ public String name;
+ public String value;
+
+ /**
+ * Creates a new empty parameter.
+ */
+ public Parameter() {
+ }
+
+ /**
+ * Creates a new parameter with the specified name and value.
+ * @param name The name of the parameter.
+ * @param value The value of the parameter.
+ */
+ public Parameter(String name, String value) {
+ this.name = name;
+ this.value = value;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ toString(sb);
+ return sb.toString();
+ }
+
+ /**
+ * Helper method that appends this parameter to a StringBuilder.
+ */
+ public void toString(StringBuilder sb) {
+ sb.append(name);
+ sb.append("=");
+ sb.append(value);
+ }
+ }
+
+ private static final class ParserState {
+ // public int lineNumber = 0;
+ public String line; // TODO: just point to original text
+ public int index;
+ }
+
+ // use factory method
+ private ICalendar() {
+ }
+
+ // TODO: get rid of this -- handle all of the parsing in one pass through
+ // the text.
+ private static String normalizeText(String text) {
+ // first we deal with line folding, by replacing all "\r\n " strings
+ // with nothing
+ text = text.replaceAll("\r\n ", "");
+
+ // it's supposed to be \r\n, but not everyone does that
+ text = text.replaceAll("\r\n", "\n");
+ text = text.replaceAll("\r", "\n");
+ return text;
+ }
+
+ /**
+ * Parses text into an iCalendar component. Parses into the provided
+ * component, if not null, or parses into a new component. In the latter
+ * case, expects a BEGIN as the first line. Returns the provided or newly
+ * created top-level component.
+ */
+ // TODO: use an index into the text, so we can make this a recursive
+ // function?
+ private static Component parseComponentImpl(Component component,
+ String text)
+ throws FormatException {
+ Component current = component;
+ ParserState state = new ParserState();
+ state.index = 0;
+
+ // split into lines
+ String[] lines = text.split("\n");
+
+ // each line is of the format:
+ // name *(";" param) ":" value
+ for (String line : lines) {
+ try {
+ current = parseLine(line, state, current);
+ // if the provided component was null, we will return the root
+ // NOTE: in this case, if the first line is not a BEGIN, a
+ // FormatException will get thrown.
+ if (component == null) {
+ component = current;
+ }
+ } catch (FormatException fe) {
+ if (Config.LOGV) {
+ Log.v(TAG, "Cannot parse " + line, fe);
+ }
+ // for now, we ignore the parse error. Google Calendar seems
+ // to be emitting some misformatted iCalendar objects.
+ }
+ continue;
+ }
+ return component;
+ }
+
+ /**
+ * Parses a line into the provided component. Creates a new component if
+ * the line is a BEGIN, adding the newly created component to the provided
+ * parent. Returns whatever component is the current one (to which new
+ * properties will be added) in the parse.
+ */
+ private static Component parseLine(String line, ParserState state,
+ Component component)
+ throws FormatException {
+ state.line = line;
+ int len = state.line.length();
+
+ // grab the name
+ char c = 0;
+ for (state.index = 0; state.index < len; ++state.index) {
+ c = line.charAt(state.index);
+ if (c == ';' || c == ':') {
+ break;
+ }
+ }
+ String name = line.substring(0, state.index);
+
+ if (component == null) {
+ if (!Component.BEGIN.equals(name)) {
+ throw new FormatException("Expected BEGIN");
+ }
+ }
+
+ Property property;
+ if (Component.BEGIN.equals(name)) {
+ // start a new component
+ String componentName = extractValue(state);
+ Component child = new Component(componentName, component);
+ if (component != null) {
+ component.addChild(child);
+ }
+ return child;
+ } else if (Component.END.equals(name)) {
+ // finish the current component
+ String componentName = extractValue(state);
+ if (component == null ||
+ !componentName.equals(component.getName())) {
+ throw new FormatException("Unexpected END " + componentName);
+ }
+ return component.getParent();
+ } else {
+ property = new Property(name);
+ }
+
+ if (c == ';') {
+ Parameter parameter = null;
+ while ((parameter = extractParameter(state)) != null) {
+ property.addParameter(parameter);
+ }
+ }
+ String value = extractValue(state);
+ property.setValue(value);
+ component.addProperty(property);
+ return component;
+ }
+
+ /**
+ * Extracts the value ":..." on the current line. The first character must
+ * be a ':'.
+ */
+ private static String extractValue(ParserState state)
+ throws FormatException {
+ String line = state.line;
+ char c = line.charAt(state.index);
+ if (c != ':') {
+ throw new FormatException("Expected ':' before end of line in "
+ + line);
+ }
+ String value = line.substring(state.index + 1);
+ state.index = line.length() - 1;
+ return value;
+ }
+
+ /**
+ * Extracts the next parameter from the line, if any. If there are no more
+ * parameters, returns null.
+ */
+ private static Parameter extractParameter(ParserState state)
+ throws FormatException {
+ String text = state.line;
+ int len = text.length();
+ Parameter parameter = null;
+ int startIndex = -1;
+ int equalIndex = -1;
+ while (state.index < len) {
+ char c = text.charAt(state.index);
+ if (c == ':') {
+ if (parameter != null) {
+ if (equalIndex == -1) {
+ throw new FormatException("Expected '=' within "
+ + "parameter in " + text);
+ }
+ parameter.value = text.substring(equalIndex + 1,
+ state.index);
+ }
+ return parameter; // may be null
+ } else if (c == ';') {
+ if (parameter != null) {
+ if (equalIndex == -1) {
+ throw new FormatException("Expected '=' within "
+ + "parameter in " + text);
+ }
+ parameter.value = text.substring(equalIndex + 1,
+ state.index);
+ return parameter;
+ } else {
+ parameter = new Parameter();
+ startIndex = state.index;
+ }
+ } else if (c == '=') {
+ equalIndex = state.index;
+ if ((parameter == null) || (startIndex == -1)) {
+ throw new FormatException("Expected ';' before '=' in "
+ + text);
+ }
+ parameter.name = text.substring(startIndex + 1, equalIndex);
+ }
+ ++state.index;
+ }
+ throw new FormatException("Expected ':' before end of line in " + text);
+ }
+
+ /**
+ * Parses the provided text into an iCalendar object. The top-level
+ * component must be of type VCALENDAR.
+ * @param text The text to be parsed.
+ * @return The top-level VCALENDAR component.
+ * @throws FormatException Thrown if the text could not be parsed into an
+ * iCalendar VCALENDAR object.
+ */
+ public static Component parseCalendar(String text) throws FormatException {
+ Component calendar = parseComponent(null, text);
+ if (calendar == null || !Component.VCALENDAR.equals(calendar.getName())) {
+ throw new FormatException("Expected " + Component.VCALENDAR);
+ }
+ return calendar;
+ }
+
+ /**
+ * Parses the provided text into an iCalendar event. The top-level
+ * component must be of type VEVENT.
+ * @param text The text to be parsed.
+ * @return The top-level VEVENT component.
+ * @throws FormatException Thrown if the text could not be parsed into an
+ * iCalendar VEVENT.
+ */
+ public static Component parseEvent(String text) throws FormatException {
+ Component event = parseComponent(null, text);
+ if (event == null || !Component.VEVENT.equals(event.getName())) {
+ throw new FormatException("Expected " + Component.VEVENT);
+ }
+ return event;
+ }
+
+ /**
+ * Parses the provided text into an iCalendar component.
+ * @param text The text to be parsed.
+ * @return The top-level component.
+ * @throws FormatException Thrown if the text could not be parsed into an
+ * iCalendar component.
+ */
+ public static Component parseComponent(String text) throws FormatException {
+ return parseComponent(null, text);
+ }
+
+ /**
+ * Parses the provided text, adding to the provided component.
+ * @param component The component to which the parsed iCalendar data should
+ * be added.
+ * @param text The text to be parsed.
+ * @return The top-level component.
+ * @throws FormatException Thrown if the text could not be parsed as an
+ * iCalendar object.
+ */
+ public static Component parseComponent(Component component, String text)
+ throws FormatException {
+ text = normalizeText(text);
+ return parseComponentImpl(component, text);
+ }
+}
diff --git a/core/java/android/pim/RecurrenceSet.java b/core/java/android/pim/RecurrenceSet.java
new file mode 100644
index 0000000..c02ff52
--- /dev/null
+++ b/core/java/android/pim/RecurrenceSet.java
@@ -0,0 +1,398 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.pim;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.provider.Calendar;
+import android.text.TextUtils;
+import android.util.Config;
+import android.util.Log;
+
+import java.util.List;
+
+/**
+ * Basic information about a recurrence, following RFC 2445 Section 4.8.5.
+ * Contains the RRULEs, RDATE, EXRULEs, and EXDATE properties.
+ */
+public class RecurrenceSet {
+
+ private final static String TAG = "CalendarProvider";
+
+ private final static String RULE_SEPARATOR = "\n";
+
+ // TODO: make these final?
+ public EventRecurrence[] rrules = null;
+ public long[] rdates = null;
+ public EventRecurrence[] exrules = null;
+ public long[] exdates = null;
+
+ /**
+ * Creates a new RecurrenceSet from information stored in the
+ * events table in the CalendarProvider.
+ * @param values The values retrieved from the Events table.
+ */
+ public RecurrenceSet(ContentValues values) {
+ String rruleStr = values.getAsString(Calendar.Events.RRULE);
+ String rdateStr = values.getAsString(Calendar.Events.RDATE);
+ String exruleStr = values.getAsString(Calendar.Events.EXRULE);
+ String exdateStr = values.getAsString(Calendar.Events.EXDATE);
+ init(rruleStr, rdateStr, exruleStr, exdateStr);
+ }
+
+ /**
+ * Creates a new RecurrenceSet from information stored in a database
+ * {@link Cursor} pointing to the events table in the
+ * CalendarProvider. The cursor must contain the RRULE, RDATE, EXRULE,
+ * and EXDATE columns.
+ *
+ * @param cursor The cursor containing the RRULE, RDATE, EXRULE, and EXDATE
+ * columns.
+ */
+ public RecurrenceSet(Cursor cursor) {
+ int rruleColumn = cursor.getColumnIndex(Calendar.Events.RRULE);
+ int rdateColumn = cursor.getColumnIndex(Calendar.Events.RDATE);
+ int exruleColumn = cursor.getColumnIndex(Calendar.Events.EXRULE);
+ int exdateColumn = cursor.getColumnIndex(Calendar.Events.EXDATE);
+ String rruleStr = cursor.getString(rruleColumn);
+ String rdateStr = cursor.getString(rdateColumn);
+ String exruleStr = cursor.getString(exruleColumn);
+ String exdateStr = cursor.getString(exdateColumn);
+ init(rruleStr, rdateStr, exruleStr, exdateStr);
+ }
+
+ public RecurrenceSet(String rruleStr, String rdateStr,
+ String exruleStr, String exdateStr) {
+ init(rruleStr, rdateStr, exruleStr, exdateStr);
+ }
+
+ private void init(String rruleStr, String rdateStr,
+ String exruleStr, String exdateStr) {
+ if (!TextUtils.isEmpty(rruleStr) || !TextUtils.isEmpty(rdateStr)) {
+
+ if (!TextUtils.isEmpty(rruleStr)) {
+ String[] rruleStrs = rruleStr.split(RULE_SEPARATOR);
+ rrules = new EventRecurrence[rruleStrs.length];
+ for (int i = 0; i < rruleStrs.length; ++i) {
+ EventRecurrence rrule = new EventRecurrence();
+ rrule.parse(rruleStrs[i]);
+ rrules[i] = rrule;
+ }
+ }
+
+ if (!TextUtils.isEmpty(rdateStr)) {
+ rdates = parseRecurrenceDates(rdateStr);
+ }
+
+ if (!TextUtils.isEmpty(exruleStr)) {
+ String[] exruleStrs = exruleStr.split(RULE_SEPARATOR);
+ exrules = new EventRecurrence[exruleStrs.length];
+ for (int i = 0; i < exruleStrs.length; ++i) {
+ EventRecurrence exrule = new EventRecurrence();
+ exrule.parse(exruleStr);
+ exrules[i] = exrule;
+ }
+ }
+
+ if (!TextUtils.isEmpty(exdateStr)) {
+ exdates = parseRecurrenceDates(exdateStr);
+ }
+ }
+ }
+
+ /**
+ * Returns whether or not a recurrence is defined in this RecurrenceSet.
+ * @return Whether or not a recurrence is defined in this RecurrenceSet.
+ */
+ public boolean hasRecurrence() {
+ return (rrules != null || rdates != null);
+ }
+
+ /**
+ * Parses the provided RDATE or EXDATE string into an array of longs
+ * representing each date/time in the recurrence.
+ * @param recurrence The recurrence to be parsed.
+ * @return The list of date/times.
+ */
+ public static long[] parseRecurrenceDates(String recurrence) {
+ // TODO: use "local" time as the default. will need to handle times
+ // that end in "z" (UTC time) explicitly at that point.
+ String tz = Time.TIMEZONE_UTC;
+ int tzidx = recurrence.indexOf(";");
+ if (tzidx != -1) {
+ tz = recurrence.substring(0, tzidx);
+ recurrence = recurrence.substring(tzidx + 1);
+ }
+ Time time = new Time(tz);
+ boolean rdateNotInUtc = !tz.equals(Time.TIMEZONE_UTC);
+ String[] rawDates = recurrence.split(",");
+ int n = rawDates.length;
+ long[] dates = new long[n];
+ for (int i = 0; i<n; ++i) {
+ // The timezone is updated to UTC if the time string specified 'Z'.
+ time.parse2445(rawDates[i]);
+ dates[i] = time.toMillis(false /* use isDst */);
+ time.timezone = tz;
+ }
+ return dates;
+ }
+
+ /**
+ * Populates the database map of values with the appropriate RRULE, RDATE,
+ * EXRULE, and EXDATE values extracted from the parsed iCalendar component.
+ * @param component The iCalendar component containing the desired
+ * recurrence specification.
+ * @param values The db values that should be updated.
+ * @return true if the component contained the necessary information
+ * to specify a recurrence. The required fields are DTSTART,
+ * one of DTEND/DURATION, and one of RRULE/RDATE. Returns false if
+ * there was an error, including if the date is out of range.
+ */
+ public static boolean populateContentValues(ICalendar.Component component,
+ ContentValues values) {
+ ICalendar.Property dtstartProperty =
+ component.getFirstProperty("DTSTART");
+ String dtstart = dtstartProperty.getValue();
+ ICalendar.Parameter tzidParam =
+ dtstartProperty.getFirstParameter("TZID");
+ // NOTE: the timezone may be null, if this is a floating time.
+ String tzid = tzidParam == null ? null : tzidParam.value;
+ Time start = new Time(tzidParam == null ? Time.TIMEZONE_UTC : tzid);
+ boolean inUtc = start.parse2445(dtstart);
+ boolean allDay = start.allDay;
+
+ if (inUtc) {
+ tzid = Time.TIMEZONE_UTC;
+ }
+
+ String duration = computeDuration(start, component);
+ String rrule = flattenProperties(component, "RRULE");
+ String rdate = extractDates(component.getFirstProperty("RDATE"));
+ String exrule = flattenProperties(component, "EXRULE");
+ String exdate = extractDates(component.getFirstProperty("EXDATE"));
+
+ if ((TextUtils.isEmpty(dtstart))||
+ (TextUtils.isEmpty(duration))||
+ ((TextUtils.isEmpty(rrule))&&
+ (TextUtils.isEmpty(rdate)))) {
+ if (Config.LOGD) {
+ Log.d(TAG, "Recurrence missing DTSTART, DTEND/DURATION, "
+ + "or RRULE/RDATE: "
+ + component.toString());
+ }
+ return false;
+ }
+
+ if (allDay) {
+ // TODO: also change tzid to be UTC? that would be consistent, but
+ // that would not reflect the original timezone value back to the
+ // server.
+ start.timezone = Time.TIMEZONE_UTC;
+ }
+ long millis = start.toMillis(false /* use isDst */);
+ values.put(Calendar.Events.DTSTART, millis);
+ if (millis == -1) {
+ if (Config.LOGD) {
+ Log.d(TAG, "DTSTART is out of range: " + component.toString());
+ }
+ return false;
+ }
+
+ values.put(Calendar.Events.RRULE, rrule);
+ values.put(Calendar.Events.RDATE, rdate);
+ values.put(Calendar.Events.EXRULE, exrule);
+ values.put(Calendar.Events.EXDATE, exdate);
+ values.put(Calendar.Events.EVENT_TIMEZONE, tzid);
+ values.put(Calendar.Events.DURATION, duration);
+ values.put(Calendar.Events.ALL_DAY, allDay ? 1 : 0);
+ return true;
+ }
+
+ public static boolean populateComponent(Cursor cursor,
+ ICalendar.Component component) {
+
+ int dtstartColumn = cursor.getColumnIndex(Calendar.Events.DTSTART);
+ int durationColumn = cursor.getColumnIndex(Calendar.Events.DURATION);
+ int tzidColumn = cursor.getColumnIndex(Calendar.Events.EVENT_TIMEZONE);
+ int rruleColumn = cursor.getColumnIndex(Calendar.Events.RRULE);
+ int rdateColumn = cursor.getColumnIndex(Calendar.Events.RDATE);
+ int exruleColumn = cursor.getColumnIndex(Calendar.Events.EXRULE);
+ int exdateColumn = cursor.getColumnIndex(Calendar.Events.EXDATE);
+ int allDayColumn = cursor.getColumnIndex(Calendar.Events.ALL_DAY);
+
+
+ long dtstart = -1;
+ if (!cursor.isNull(dtstartColumn)) {
+ dtstart = cursor.getLong(dtstartColumn);
+ }
+ String duration = cursor.getString(durationColumn);
+ String tzid = cursor.getString(tzidColumn);
+ String rruleStr = cursor.getString(rruleColumn);
+ String rdateStr = cursor.getString(rdateColumn);
+ String exruleStr = cursor.getString(exruleColumn);
+ String exdateStr = cursor.getString(exdateColumn);
+ boolean allDay = cursor.getInt(allDayColumn) == 1;
+
+ if ((dtstart == -1) ||
+ (TextUtils.isEmpty(duration))||
+ ((TextUtils.isEmpty(rruleStr))&&
+ (TextUtils.isEmpty(rdateStr)))) {
+ // no recurrence.
+ return false;
+ }
+
+ ICalendar.Property dtstartProp = new ICalendar.Property("DTSTART");
+ Time dtstartTime = null;
+ if (!TextUtils.isEmpty(tzid)) {
+ if (!allDay) {
+ dtstartProp.addParameter(new ICalendar.Parameter("TZID", tzid));
+ }
+ dtstartTime = new Time(tzid);
+ } else {
+ // use the "floating" timezone
+ dtstartTime = new Time(Time.TIMEZONE_UTC);
+ }
+
+ dtstartTime.set(dtstart);
+ // make sure the time is printed just as a date, if all day.
+ // TODO: android.pim.Time really should take care of this for us.
+ if (allDay) {
+ dtstartProp.addParameter(new ICalendar.Parameter("VALUE", "DATE"));
+ dtstartTime.allDay = true;
+ dtstartTime.hour = 0;
+ dtstartTime.minute = 0;
+ dtstartTime.second = 0;
+ }
+
+ dtstartProp.setValue(dtstartTime.format2445());
+ component.addProperty(dtstartProp);
+ ICalendar.Property durationProp = new ICalendar.Property("DURATION");
+ durationProp.setValue(duration);
+ component.addProperty(durationProp);
+
+ addPropertiesForRuleStr(component, "RRULE", rruleStr);
+ addPropertyForDateStr(component, "RDATE", rdateStr);
+ addPropertiesForRuleStr(component, "EXRULE", exruleStr);
+ addPropertyForDateStr(component, "EXDATE", exdateStr);
+ return true;
+ }
+
+ private static void addPropertiesForRuleStr(ICalendar.Component component,
+ String propertyName,
+ String ruleStr) {
+ if (TextUtils.isEmpty(ruleStr)) {
+ return;
+ }
+ String[] rrules = ruleStr.split(RULE_SEPARATOR);
+ for (String rrule : rrules) {
+ ICalendar.Property prop = new ICalendar.Property(propertyName);
+ prop.setValue(rrule);
+ component.addProperty(prop);
+ }
+ }
+
+ private static void addPropertyForDateStr(ICalendar.Component component,
+ String propertyName,
+ String dateStr) {
+ if (TextUtils.isEmpty(dateStr)) {
+ return;
+ }
+
+ ICalendar.Property prop = new ICalendar.Property(propertyName);
+ String tz = null;
+ int tzidx = dateStr.indexOf(";");
+ if (tzidx != -1) {
+ tz = dateStr.substring(0, tzidx);
+ dateStr = dateStr.substring(tzidx + 1);
+ }
+ if (!TextUtils.isEmpty(tz)) {
+ prop.addParameter(new ICalendar.Parameter("TZID", tz));
+ }
+ prop.setValue(dateStr);
+ component.addProperty(prop);
+ }
+
+ private static String computeDuration(Time start,
+ ICalendar.Component component) {
+ // see if a duration is defined
+ ICalendar.Property durationProperty =
+ component.getFirstProperty("DURATION");
+ if (durationProperty != null) {
+ // just return the duration
+ return durationProperty.getValue();
+ }
+
+ // must compute a duration from the DTEND
+ ICalendar.Property dtendProperty =
+ component.getFirstProperty("DTEND");
+ if (dtendProperty == null) {
+ // no DURATION, no DTEND: 0 second duration
+ return "+P0S";
+ }
+ ICalendar.Parameter endTzidParameter =
+ dtendProperty.getFirstParameter("TZID");
+ String endTzid = (endTzidParameter == null)
+ ? start.timezone : endTzidParameter.value;
+
+ Time end = new Time(endTzid);
+ end.parse2445(dtendProperty.getValue());
+ long durationMillis = end.toMillis(false /* use isDst */)
+ - start.toMillis(false /* use isDst */);
+ long durationSeconds = (durationMillis / 1000);
+ return "P" + durationSeconds + "S";
+ }
+
+ private static String flattenProperties(ICalendar.Component component,
+ String name) {
+ List<ICalendar.Property> properties = component.getProperties(name);
+ if (properties == null || properties.isEmpty()) {
+ return null;
+ }
+
+ if (properties.size() == 1) {
+ return properties.get(0).getValue();
+ }
+
+ StringBuilder sb = new StringBuilder();
+
+ boolean first = true;
+ for (ICalendar.Property property : component.getProperties(name)) {
+ if (first) {
+ first = false;
+ } else {
+ // TODO: use commas. our RECUR parsing should handle that
+ // anyway.
+ sb.append(RULE_SEPARATOR);
+ }
+ sb.append(property.getValue());
+ }
+ return sb.toString();
+ }
+
+ private static String extractDates(ICalendar.Property recurrence) {
+ if (recurrence == null) {
+ return null;
+ }
+ ICalendar.Parameter tzidParam =
+ recurrence.getFirstParameter("TZID");
+ if (tzidParam != null) {
+ return tzidParam.value + ";" + recurrence.getValue();
+ }
+ return recurrence.getValue();
+ }
+}
diff --git a/core/java/android/pim/Time.java b/core/java/android/pim/Time.java
new file mode 100644
index 0000000..59ba87b
--- /dev/null
+++ b/core/java/android/pim/Time.java
@@ -0,0 +1,570 @@
+/*
+ * 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.pim;
+
+
+
+import java.util.TimeZone;
+
+/**
+ * {@hide}
+ *
+ * The Time class is a faster replacement for the java.util.Calendar and
+ * java.util.GregorianCalendar classes. An instance of the Time class represents
+ * a moment in time, specified with second precision. It is modelled after
+ * struct tm, and in fact, uses struct tm to implement most of the
+ * functionality.
+ */
+public class Time {
+ public static final String TIMEZONE_UTC = "UTC";
+
+ /**
+ * The Julian day of the epoch, that is, January 1, 1970 on the Gregorian
+ * calendar.
+ */
+ public static final int EPOCH_JULIAN_DAY = 2440588;
+
+ /**
+ * True if this is an allDay event. The hour, minute, second fields are
+ * all zero, and the date is displayed the same in all time zones.
+ */
+ public boolean allDay;
+
+ /**
+ * Seconds [0-61] (2 leap seconds allowed)
+ */
+ public int second;
+
+ /**
+ * Minute [0-59]
+ */
+ public int minute;
+
+ /**
+ * Hour of day [0-23]
+ */
+ public int hour;
+
+ /**
+ * Day of month [1-31]
+ */
+ public int monthDay;
+
+ /**
+ * Month [0-11]
+ */
+ public int month;
+
+ /**
+ * Year. TBD. Is this years since 1900 like in struct tm?
+ */
+ public int year;
+
+ /**
+ * Day of week [0-6]
+ */
+ public int weekDay;
+
+ /**
+ * Day of year [0-365]
+ */
+ public int yearDay;
+
+ /**
+ * This time is in daylight savings time. One of:
+ * <ul>
+ * <li><b>positive</b> - in dst</li>
+ * <li><b>0</b> - not in dst</li>
+ * <li><b>negative</b> - unknown</li>
+ */
+ public int isDst;
+
+ /**
+ * Offset from UTC (in seconds).
+ */
+ public long gmtoff;
+
+ /**
+ * The timezone for this Time. Should not be null.
+ */
+ public String timezone;
+
+ /*
+ * Define symbolic constants for accessing the fields in this class. Used in
+ * getActualMaximum().
+ */
+ public static final int SECOND = 1;
+ public static final int MINUTE = 2;
+ public static final int HOUR = 3;
+ public static final int MONTH_DAY = 4;
+ public static final int MONTH = 5;
+ public static final int YEAR = 6;
+ public static final int WEEK_DAY = 7;
+ public static final int YEAR_DAY = 8;
+ public static final int WEEK_NUM = 9;
+
+ public static final int SUNDAY = 0;
+ public static final int MONDAY = 1;
+ public static final int TUESDAY = 2;
+ public static final int WEDNESDAY = 3;
+ public static final int THURSDAY = 4;
+ public static final int FRIDAY = 5;
+ public static final int SATURDAY = 6;
+
+ /**
+ * Construct a Time object in the timezone named by the string
+ * argument "timezone". The time is initialized to Jan 1, 1970.
+ */
+ public Time(String timezone) {
+ if (timezone == null) {
+ throw new NullPointerException("timezone is null!");
+ }
+ this.timezone = timezone;
+ this.year = 1970;
+ this.monthDay = 1;
+ // Set the daylight-saving indicator to the unknown value -1 so that
+ // it will be recomputed.
+ this.isDst = -1;
+ }
+
+ /**
+ * Construct a Time object in the local timezone. The time is initialized to
+ * Jan 1, 1970.
+ */
+ public Time() {
+ this(TimeZone.getDefault().getID());
+ }
+
+ /**
+ * A copy constructor. Construct a Time object by copying the given
+ * Time object. No normalization occurs.
+ *
+ * @param other
+ */
+ public Time(Time other) {
+ set(other);
+ }
+
+ /**
+ * Ensures the values in each field are in range. For example if the
+ * current value of this calendar is March 32, normalize() will convert it
+ * to April 1. It also fills in weekDay, yearDay, isDst and gmtoff.
+ *
+ * <p>
+ * If "ignoreDst" is true, then this method sets the "isDst" field to -1
+ * (the "unknown" value) before normalizing. It then computes the
+ * correct value for "isDst".
+ *
+ * <p>
+ * See {@link #toMillis(boolean)} for more information about when to
+ * use <tt>true</tt> or <tt>false</tt> for "ignoreDst".
+ *
+ * @return the UTC milliseconds since the epoch
+ */
+ native public long normalize(boolean ignoreDst);
+
+ /**
+ * Convert this time object so the time represented remains the same, but is
+ * instead located in a different timezone. This method automatically calls
+ * normalize() in some cases
+ */
+ native public void switchTimezone(String timezone);
+
+ private static final int[] DAYS_PER_MONTH = { 31, 28, 31, 30, 31, 30, 31,
+ 31, 30, 31, 30, 31 };
+
+ /**
+ * Return the maximum possible value for the given field given the value of
+ * the other fields. Requires that it be normalized for MONTH_DAY and
+ * YEAR_DAY.
+ */
+ public int getActualMaximum(int field) {
+ switch (field) {
+ case SECOND:
+ return 59; // leap seconds, bah humbug
+ case MINUTE:
+ return 59;
+ case HOUR:
+ return 23;
+ case MONTH_DAY: {
+ int n = DAYS_PER_MONTH[this.month];
+ if (n != 28) {
+ return n;
+ } else {
+ int y = this.year;
+ return ((y % 4) == 0 && ((y % 100) != 0 || (y % 400) == 0)) ? 29 : 28;
+ }
+ }
+ case MONTH:
+ return 11;
+ case YEAR:
+ return 2037;
+ case WEEK_DAY:
+ return 6;
+ case YEAR_DAY: {
+ int y = this.year;
+ // Year days are numbered from 0, so the last one is usually 364.
+ return ((y % 4) == 0 && ((y % 100) != 0 || (y % 400) == 0)) ? 365 : 364;
+ }
+ case WEEK_NUM:
+ throw new RuntimeException("WEEK_NUM not implemented");
+ default:
+ throw new RuntimeException("bad field=" + field);
+ }
+ }
+
+ /**
+ * Clears all values, setting the timezone to the given timezone. Sets isDst
+ * to a negative value to mean "unknown".
+ */
+ public void clear(String timezone) {
+ if (timezone == null) {
+ throw new NullPointerException("timezone is null!");
+ }
+ this.timezone = timezone;
+ this.allDay = false;
+ this.second = 0;
+ this.minute = 0;
+ this.hour = 0;
+ this.monthDay = 0;
+ this.month = 0;
+ this.year = 0;
+ this.weekDay = 0;
+ this.yearDay = 0;
+ this.gmtoff = 0;
+ this.isDst = -1;
+ }
+
+ /**
+ * return a negative number if a is less than b, a positive number if a is
+ * greater than b, and 0 if they are equal.
+ */
+ native public static int compare(Time a, Time b);
+
+ /**
+ * Print the current value given the format string provided. See man
+ * strftime for what means what. The final string must be less than 256
+ * characters.
+ */
+ native public String format(String format);
+
+ /**
+ * Return the current time in YYYYMMDDTHHMMSS<tz> format
+ */
+ @Override
+ native public String toString();
+
+ /**
+ * Parse a time in the current zone in YYYYMMDDTHHMMSS format.
+ */
+ native public void parse(String s);
+
+ /**
+ * Parse a time in RFC 2445 format. Returns whether or not the time is in
+ * UTC (ends with Z).
+ *
+ * @param s the string to parse
+ * @return true if the resulting time value is in UTC time
+ */
+ public boolean parse2445(String s) {
+ if (nativeParse2445(s)) {
+ timezone = TIMEZONE_UTC;
+ return true;
+ }
+ return false;
+ }
+
+ native private boolean nativeParse2445(String s);
+
+ /**
+ * Parse a time in RFC 3339 format. This method also parses simple dates
+ * (that is, strings that contain no time or time offset). If the string
+ * contains a time and time offset, then the time offset will be used to
+ * convert the time value to UTC.
+ * Returns true if the resulting time value is in UTC time.
+ *
+ * @param s the string to parse
+ * @return true if the resulting time value is in UTC time
+ */
+ public boolean parse3339(String s) {
+ if (nativeParse3339(s)) {
+ timezone = TIMEZONE_UTC;
+ return true;
+ }
+ return false;
+ }
+
+ native private boolean nativeParse3339(String s);
+
+ /**
+ * Returns the timezone string that is currently set for the device.
+ */
+ public static String getCurrentTimezone() {
+ return TimeZone.getDefault().getID();
+ }
+
+ /**
+ * Sets the time of the given Time object to the current time.
+ */
+ native public void setToNow();
+
+ /**
+ * Converts this time to milliseconds. Suitable for interacting with the
+ * standard java libraries. The time is in UTC milliseconds since the epoch.
+ * This does an implicit normalization to compute the milliseconds but does
+ * <em>not</em> change any of the fields in this Time object. If you want
+ * to normalize the fields in this Time object and also get the milliseconds
+ * then use {@link #normalize(boolean)}.
+ *
+ * <p>
+ * If "ignoreDst" is false, then this method uses the current setting of the
+ * "isDst" field and will adjust the returned time if the "isDst" field is
+ * wrong for the given time. See the sample code below for an example of
+ * this.
+ *
+ * <p>
+ * If "ignoreDst" is true, then this method ignores the current setting of
+ * the "isDst" field in this Time object and will instead figure out the
+ * correct value of "isDst" (as best it can) from the fields in this
+ * Time object. The only case where this method cannot figure out the
+ * correct value of the "isDst" field is when the time is inherently
+ * ambiguous because it falls in the hour that is repeated when switching
+ * from Daylight-Saving Time to Standard Time.
+ *
+ * <p>
+ * Here is an example where <tt>toMillis(true)</tt> adjusts the time,
+ * assuming that DST changes at 2am on Sunday, Nov 4, 2007.
+ *
+ * <pre>
+ * Time time = new Time();
+ * time.set(2007, 10, 4); // set the date to Nov 4, 2007, 12am
+ * time.normalize(); // this sets isDst = 1
+ * time.monthDay += 1; // changes the date to Nov 5, 2007, 12am
+ * millis = time.toMillis(false); // millis is Nov 4, 2007, 11pm
+ * millis = time.toMillis(true); // millis is Nov 5, 2007, 12am
+ * </pre>
+ *
+ * <p>
+ * To avoid this problem, use <tt>toMillis(true)</tt>
+ * after adding or subtracting days or explicitly setting the "monthDay"
+ * field. On the other hand, if you are adding
+ * or subtracting hours or minutes, then you should use
+ * <tt>toMillis(false)</tt>.
+ *
+ * <p>
+ * You should also use <tt>toMillis(false)</tt> if you want
+ * to read back the same milliseconds that you set with {@link #set(long)}
+ * or {@link #set(Time)} or after parsing a date string.
+ */
+ native public long toMillis(boolean ignoreDst);
+
+ /**
+ * Sets the fields in this Time object given the UTC milliseconds. After
+ * this method returns, all the fields are normalized.
+ * This also sets the "isDst" field to the correct value.
+ *
+ * @param millis the time in UTC milliseconds since the epoch.
+ */
+ native public void set(long millis);
+
+ /**
+ * Format according to RFC 2445 DATETIME type.
+ *
+ * <p>
+ * The same as format("%Y%m%dT%H%M%S").
+ */
+ native public String format2445();
+
+ /**
+ * Copy the value of that to this Time object. No normalization happens.
+ */
+ public void set(Time that) {
+ this.timezone = that.timezone;
+ this.allDay = that.allDay;
+ this.second = that.second;
+ this.minute = that.minute;
+ this.hour = that.hour;
+ this.monthDay = that.monthDay;
+ this.month = that.month;
+ this.year = that.year;
+ this.weekDay = that.weekDay;
+ this.yearDay = that.yearDay;
+ this.isDst = that.isDst;
+ this.gmtoff = that.gmtoff;
+ }
+
+ /**
+ * Set the fields. Sets weekDay, yearDay and gmtoff to 0. Call
+ * normalize() if you need those.
+ */
+ public void set(int second, int minute, int hour, int monthDay, int month, int year) {
+ this.allDay = false;
+ this.second = second;
+ this.minute = minute;
+ this.hour = hour;
+ this.monthDay = monthDay;
+ this.month = month;
+ this.year = year;
+ this.weekDay = 0;
+ this.yearDay = 0;
+ this.isDst = -1;
+ this.gmtoff = 0;
+ }
+
+ public void set(int monthDay, int month, int year) {
+ this.allDay = true;
+ this.second = 0;
+ this.minute = 0;
+ this.hour = 0;
+ this.monthDay = monthDay;
+ this.month = month;
+ this.year = year;
+ this.weekDay = 0;
+ this.yearDay = 0;
+ this.isDst = -1;
+ this.gmtoff = 0;
+ }
+
+ public boolean before(Time that) {
+ return Time.compare(this, that) < 0;
+ }
+
+ public boolean after(Time that) {
+ return Time.compare(this, that) > 0;
+ }
+
+ /**
+ * This array is indexed by the weekDay field (SUNDAY=0, MONDAY=1, etc.)
+ * and gives a number that can be added to the yearDay to give the
+ * closest Thursday yearDay.
+ */
+ private static final int[] sThursdayOffset = { -3, 3, 2, 1, 0, -1, -2 };
+
+ /**
+ * Computes the week number according to ISO 8601. The current Time
+ * object must already be normalized because this method uses the
+ * yearDay and weekDay fields.
+ *
+ * In IS0 8601, weeks start on Monday.
+ * The first week of the year (week 1) is defined by ISO 8601 as the
+ * first week with four or more of its days in the starting year.
+ * Or equivalently, the week containing January 4. Or equivalently,
+ * the week with the year's first Thursday in it.
+ *
+ * The week number can be calculated by counting Thursdays. Week N
+ * contains the Nth Thursday of the year.
+ *
+ * @return the ISO week number.
+ */
+ public int getWeekNumber() {
+ // Get the year day for the closest Thursday
+ int closestThursday = yearDay + sThursdayOffset[weekDay];
+
+ // Year days start at 0
+ if (closestThursday >= 0 && closestThursday <= 364) {
+ return closestThursday / 7 + 1;
+ }
+
+ // The week crosses a year boundary.
+ Time temp = new Time(this);
+ temp.monthDay += sThursdayOffset[weekDay];
+ temp.normalize(true /* ignore isDst */);
+ return temp.yearDay / 7 + 1;
+ }
+
+ public String format3339(boolean allDay) {
+ if (allDay) {
+ return format("%Y-%m-%d");
+ } else if (TIMEZONE_UTC.equals(timezone)) {
+ return format("%Y-%m-%dT%H:%M:%S.000Z");
+ } else {
+ String base = format("%Y-%m-%dT%H:%M:%S.000");
+ String sign = (gmtoff < 0) ? "-" : "+";
+ int offset = (int)Math.abs(gmtoff);
+ int minutes = (offset % 3600) / 60;
+ int hours = offset / 3600;
+
+ return String.format("%s%s%02d:%02d", base, sign, hours, minutes);
+ }
+ }
+
+ public static boolean isEpoch(Time time) {
+ long millis = time.toMillis(true);
+ return getJulianDay(millis, 0) == EPOCH_JULIAN_DAY;
+ }
+
+ /**
+ * Computes the Julian day number, given the UTC milliseconds
+ * and the offset (in seconds) from UTC. The Julian day for a given
+ * date will be the same for every timezone. For example, the Julian
+ * day for July 1, 2008 is 2454649. This is the same value no matter
+ * what timezone is being used. The Julian day is useful for testing
+ * if two events occur on the same day and for determining the relative
+ * time of an event from the present ("yesterday", "3 days ago", etc.).
+ *
+ * <p>
+ * Use {@link #toMillis(boolean)} to get the milliseconds.
+ *
+ * @param millis the time in UTC milliseconds
+ * @param gmtoff the offset from UTC in seconds
+ * @return the Julian day
+ */
+ public static int getJulianDay(long millis, long gmtoff) {
+ long offsetMillis = gmtoff * 1000;
+ long julianDay = (millis + offsetMillis) / DateUtils.DAY_IN_MILLIS;
+ return (int) julianDay + EPOCH_JULIAN_DAY;
+ }
+
+ /**
+ * <p>Sets the time from the given Julian day number, which must be based on
+ * the same timezone that is set in this Time object. The "gmtoff" field
+ * need not be initialized because the given Julian day may have a different
+ * GMT offset than whatever is currently stored in this Time object anyway.
+ * After this method returns all the fields will be normalized and the time
+ * will be set to 12am at the beginning of the given Julian day.
+ *
+ * <p>
+ * The only exception to this is if 12am does not exist for that day because
+ * of daylight saving time. For example, Cairo, Eqypt moves time ahead one
+ * hour at 12am on April 25, 2008 and there are a few other places that
+ * also change daylight saving time at 12am. In those cases, the time
+ * will be set to 1am.
+ *
+ * @param julianDay the Julian day in the timezone for this Time object
+ * @return the UTC milliseconds for the beginning of the Julian day
+ */
+ public long setJulianDay(int julianDay) {
+ // Don't bother with the GMT offset since we don't know the correct
+ // value for the given Julian day. Just get close and then adjust
+ // the day.
+ long millis = (julianDay - EPOCH_JULIAN_DAY) * DateUtils.DAY_IN_MILLIS;
+ set(millis);
+
+ // Figure out how close we are to the requested Julian day.
+ // We can't be off by more than a day.
+ int approximateDay = getJulianDay(millis, gmtoff);
+ int diff = julianDay - approximateDay;
+ monthDay += diff;
+
+ // Set the time to 12am and re-normalize.
+ hour = 0;
+ minute = 0;
+ second = 0;
+ millis = normalize(true);
+ return millis;
+ }
+}
diff --git a/core/java/android/pim/package.html b/core/java/android/pim/package.html
new file mode 100644
index 0000000..75237c9
--- /dev/null
+++ b/core/java/android/pim/package.html
@@ -0,0 +1,7 @@
+<HTML>
+<BODY>
+{@hide}
+Provides helpers for working with PIM (Personal Information Manager) data used
+by contact lists and calendars.
+</BODY>
+</HTML> \ No newline at end of file
diff --git a/core/java/android/preference/CheckBoxPreference.java b/core/java/android/preference/CheckBoxPreference.java
new file mode 100644
index 0000000..4bebf87
--- /dev/null
+++ b/core/java/android/preference/CheckBoxPreference.java
@@ -0,0 +1,295 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.preference;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.res.TypedArray;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.Checkable;
+import android.widget.TextView;
+
+/**
+ * The {@link CheckBoxPreference} is a preference that provides checkbox widget
+ * functionality.
+ * <p>
+ * This preference will store a boolean into the SharedPreferences.
+ *
+ * @attr ref android.R.styleable#CheckBoxPreference_summaryOff
+ * @attr ref android.R.styleable#CheckBoxPreference_summaryOn
+ * @attr ref android.R.styleable#CheckBoxPreference_disableDependentsState
+ */
+public class CheckBoxPreference extends Preference {
+
+ private CharSequence mSummaryOn;
+ private CharSequence mSummaryOff;
+
+ private boolean mChecked;
+
+ private boolean mDisableDependentsState;
+
+ public CheckBoxPreference(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ TypedArray a = context.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.CheckBoxPreference, defStyle, 0);
+ mSummaryOn = a.getString(com.android.internal.R.styleable.CheckBoxPreference_summaryOn);
+ mSummaryOff = a.getString(com.android.internal.R.styleable.CheckBoxPreference_summaryOff);
+ mDisableDependentsState = a.getBoolean(
+ com.android.internal.R.styleable.CheckBoxPreference_disableDependentsState, false);
+ a.recycle();
+ }
+
+ public CheckBoxPreference(Context context, AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.checkBoxPreferenceStyle);
+ }
+
+ public CheckBoxPreference(Context context) {
+ this(context, null);
+ }
+
+ @Override
+ protected void onBindView(View view) {
+ super.onBindView(view);
+
+ View checkboxView = view.findViewById(com.android.internal.R.id.checkbox);
+ if (checkboxView != null && checkboxView instanceof Checkable) {
+ ((Checkable) checkboxView).setChecked(mChecked);
+ }
+
+ // Sync the summary view
+ TextView summaryView = (TextView) view.findViewById(com.android.internal.R.id.summary);
+ if (summaryView != null) {
+ boolean useDefaultSummary = true;
+ if (mChecked && mSummaryOn != null) {
+ summaryView.setText(mSummaryOn);
+ useDefaultSummary = false;
+ } else if (!mChecked && mSummaryOff != null) {
+ summaryView.setText(mSummaryOff);
+ useDefaultSummary = false;
+ }
+
+ if (useDefaultSummary) {
+ final CharSequence summary = getSummary();
+ if (summary != null) {
+ summaryView.setText(summary);
+ useDefaultSummary = false;
+ }
+ }
+
+ int newVisibility = View.GONE;
+ if (!useDefaultSummary) {
+ // Someone has written to it
+ newVisibility = View.VISIBLE;
+ }
+ if (newVisibility != summaryView.getVisibility()) {
+ summaryView.setVisibility(newVisibility);
+ }
+ }
+ }
+
+ @Override
+ protected void onClick() {
+ super.onClick();
+
+ boolean newValue = !isChecked();
+
+ if (!callChangeListener(newValue)) {
+ return;
+ }
+
+ setChecked(newValue);
+ }
+
+ /**
+ * Sets the checked state and saves it to the {@link SharedPreferences}.
+ *
+ * @param checked The checked state.
+ */
+ public void setChecked(boolean checked) {
+ mChecked = checked;
+
+ persistBoolean(checked);
+
+ notifyDependencyChange(shouldDisableDependents());
+
+ notifyChanged();
+ }
+
+ /**
+ * Returns the checked state.
+ *
+ * @return The checked state.
+ */
+ public boolean isChecked() {
+ return mChecked;
+ }
+
+ @Override
+ public boolean shouldDisableDependents() {
+ boolean shouldDisable = mDisableDependentsState ? mChecked : !mChecked;
+ return shouldDisable || super.shouldDisableDependents();
+ }
+
+ /**
+ * Sets the summary to be shown when checked.
+ *
+ * @param summary The summary to be shown when checked.
+ */
+ public void setSummaryOn(CharSequence summary) {
+ mSummaryOn = summary;
+ if (isChecked()) {
+ notifyChanged();
+ }
+ }
+
+ /**
+ * @see #setSummaryOn(CharSequence)
+ * @param summaryResId The summary as a resource.
+ */
+ public void setSummaryOn(int summaryResId) {
+ setSummaryOn(getContext().getString(summaryResId));
+ }
+
+ /**
+ * Returns the summary to be shown when checked.
+ * @return The summary.
+ */
+ public CharSequence getSummaryOn() {
+ return mSummaryOn;
+ }
+
+ /**
+ * Sets the summary to be shown when unchecked.
+ *
+ * @param summary The summary to be shown when unchecked.
+ */
+ public void setSummaryOff(CharSequence summary) {
+ mSummaryOff = summary;
+ if (!isChecked()) {
+ notifyChanged();
+ }
+ }
+
+ /**
+ * @see #setSummaryOff(CharSequence)
+ * @param summaryResId The summary as a resource.
+ */
+ public void setSummaryOff(int summaryResId) {
+ setSummaryOff(getContext().getString(summaryResId));
+ }
+
+ /**
+ * Returns the summary to be shown when unchecked.
+ * @return The summary.
+ */
+ public CharSequence getSummaryOff() {
+ return mSummaryOff;
+ }
+
+ /**
+ * Returns whether dependents are disabled when this preference is on ({@code true})
+ * or when this preference is off ({@code false}).
+ *
+ * @return Whether dependents are disabled when this preference is on ({@code true})
+ * or when this preference is off ({@code false}).
+ */
+ public boolean getDisableDependentsState() {
+ return mDisableDependentsState;
+ }
+
+ /**
+ * Sets whether dependents are disabled when this preference is on ({@code true})
+ * or when this preference is off ({@code false}).
+ *
+ * @param disableDependentsState The preference state that should disable dependents.
+ */
+ public void setDisableDependentsState(boolean disableDependentsState) {
+ mDisableDependentsState = disableDependentsState;
+ }
+
+ @Override
+ protected Object onGetDefaultValue(TypedArray a, int index) {
+ return a.getBoolean(index, false);
+ }
+
+ @Override
+ protected void onSetInitialValue(boolean restoreValue, Object defaultValue) {
+ setChecked(restoreValue ? getPersistedBoolean(mChecked)
+ : (Boolean) defaultValue);
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ final Parcelable superState = super.onSaveInstanceState();
+ if (isPersistent()) {
+ // No need to save instance state since it's persistent
+ return superState;
+ }
+
+ final SavedState myState = new SavedState(superState);
+ myState.checked = isChecked();
+ return myState;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ if (state == null || !state.getClass().equals(SavedState.class)) {
+ // Didn't save state for us in onSaveInstanceState
+ super.onRestoreInstanceState(state);
+ return;
+ }
+
+ SavedState myState = (SavedState) state;
+ super.onRestoreInstanceState(myState.getSuperState());
+ setChecked(myState.checked);
+ }
+
+ private static class SavedState extends BaseSavedState {
+ boolean checked;
+
+ public SavedState(Parcel source) {
+ super(source);
+ checked = source.readInt() == 1;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeInt(checked ? 1 : 0);
+ }
+
+ public SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ public static final Parcelable.Creator<SavedState> CREATOR =
+ new Parcelable.Creator<SavedState>() {
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+
+}
diff --git a/core/java/android/preference/DialogPreference.java b/core/java/android/preference/DialogPreference.java
new file mode 100644
index 0000000..3eb65e2
--- /dev/null
+++ b/core/java/android/preference/DialogPreference.java
@@ -0,0 +1,445 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.preference;
+
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.SharedPreferences;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.TextView;
+
+/**
+ * The {@link DialogPreference} class is a base class for preferences that are
+ * dialog-based. These preferences will, when clicked, open a dialog showing the
+ * actual preference controls.
+ *
+ * @attr ref android.R.styleable#DialogPreference_dialogTitle
+ * @attr ref android.R.styleable#DialogPreference_dialogMessage
+ * @attr ref android.R.styleable#DialogPreference_dialogIcon
+ * @attr ref android.R.styleable#DialogPreference_dialogLayout
+ * @attr ref android.R.styleable#DialogPreference_positiveButtonText
+ * @attr ref android.R.styleable#DialogPreference_negativeButtonText
+ */
+public abstract class DialogPreference extends Preference implements
+ DialogInterface.OnClickListener, DialogInterface.OnDismissListener,
+ PreferenceManager.OnActivityDestroyListener {
+ private AlertDialog.Builder mBuilder;
+
+ private CharSequence mDialogTitle;
+ private CharSequence mDialogMessage;
+ private Drawable mDialogIcon;
+ private CharSequence mPositiveButtonText;
+ private CharSequence mNegativeButtonText;
+ private int mDialogLayoutResId;
+
+ /** The dialog, if it is showing. */
+ private Dialog mDialog;
+
+ /** Which button was clicked. */
+ private int mWhichButtonClicked;
+
+ public DialogPreference(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ TypedArray a = context.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.DialogPreference, defStyle, 0);
+ mDialogTitle = a.getString(com.android.internal.R.styleable.DialogPreference_dialogTitle);
+ if (mDialogTitle == null) {
+ // Fallback on the regular title of the preference
+ // (the one that is seen in the list)
+ mDialogTitle = getTitle();
+ }
+ mDialogMessage = a.getString(com.android.internal.R.styleable.DialogPreference_dialogMessage);
+ mDialogIcon = a.getDrawable(com.android.internal.R.styleable.DialogPreference_dialogIcon);
+ mPositiveButtonText = a.getString(com.android.internal.R.styleable.DialogPreference_positiveButtonText);
+ mNegativeButtonText = a.getString(com.android.internal.R.styleable.DialogPreference_negativeButtonText);
+ mDialogLayoutResId = a.getResourceId(com.android.internal.R.styleable.DialogPreference_dialogLayout,
+ mDialogLayoutResId);
+ a.recycle();
+
+ }
+
+ public DialogPreference(Context context, AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.dialogPreferenceStyle);
+ }
+
+ /**
+ * Sets the title of the dialog. This will be shown on subsequent dialogs.
+ *
+ * @param dialogTitle The title.
+ */
+ public void setDialogTitle(CharSequence dialogTitle) {
+ mDialogTitle = dialogTitle;
+ }
+
+ /**
+ * @see #setDialogTitle(CharSequence)
+ * @param dialogTitleResId The dialog title as a resource.
+ */
+ public void setDialogTitle(int dialogTitleResId) {
+ setDialogTitle(getContext().getString(dialogTitleResId));
+ }
+
+ /**
+ * Returns the title to be shown on subsequent dialogs.
+ * @return The title.
+ */
+ public CharSequence getDialogTitle() {
+ return mDialogTitle;
+ }
+
+ /**
+ * Sets the message of the dialog. This will be shown on subsequent dialogs.
+ * <p>
+ * This message forms the content View of the dialog and conflicts with
+ * list-based dialogs, for example. If setting a custom View on a dialog via
+ * {@link #setDialogLayoutResource(int)}, include a text View with ID
+ * {@link android.R.id#message} and it will be populated with this message.
+ *
+ * @param dialogMessage The message.
+ */
+ public void setDialogMessage(CharSequence dialogMessage) {
+ mDialogMessage = dialogMessage;
+ }
+
+ /**
+ * @see #setDialogMessage(CharSequence)
+ * @param dialogMessageResId The dialog message as a resource.
+ */
+ public void setDialogMessage(int dialogMessageResId) {
+ setDialogMessage(getContext().getString(dialogMessageResId));
+ }
+
+ /**
+ * Returns the message to be shown on subsequent dialogs.
+ * @return The message.
+ */
+ public CharSequence getDialogMessage() {
+ return mDialogMessage;
+ }
+
+ /**
+ * Sets the icon of the dialog. This will be shown on subsequent dialogs.
+ *
+ * @param dialogIcon The icon, as a {@link Drawable}.
+ */
+ public void setDialogIcon(Drawable dialogIcon) {
+ mDialogIcon = dialogIcon;
+ }
+
+ /**
+ * Sets the icon (resource ID) of the dialog. This will be shown on
+ * subsequent dialogs.
+ *
+ * @param dialogIconRes The icon, as a resource ID.
+ */
+ public void setDialogIcon(int dialogIconRes) {
+ mDialogIcon = getContext().getResources().getDrawable(dialogIconRes);
+ }
+
+ /**
+ * Returns the icon to be shown on subsequent dialogs.
+ * @return The icon, as a {@link Drawable}.
+ */
+ public Drawable getDialogIcon() {
+ return mDialogIcon;
+ }
+
+ /**
+ * Sets the text of the positive button of the dialog. This will be shown on
+ * subsequent dialogs.
+ *
+ * @param positiveButtonText The text of the positive button.
+ */
+ public void setPositiveButtonText(CharSequence positiveButtonText) {
+ mPositiveButtonText = positiveButtonText;
+ }
+
+ /**
+ * @see #setPositiveButtonText(CharSequence)
+ * @param positiveButtonTextResId The positive button text as a resource.
+ */
+ public void setPositiveButtonText(int positiveButtonTextResId) {
+ setPositiveButtonText(getContext().getString(positiveButtonTextResId));
+ }
+
+ /**
+ * Returns the text of the positive button to be shown on subsequent
+ * dialogs.
+ *
+ * @return The text of the positive button.
+ */
+ public CharSequence getPositiveButtonText() {
+ return mPositiveButtonText;
+ }
+
+ /**
+ * Sets the text of the negative button of the dialog. This will be shown on
+ * subsequent dialogs.
+ *
+ * @param negativeButtonText The text of the negative button.
+ */
+ public void setNegativeButtonText(CharSequence negativeButtonText) {
+ mNegativeButtonText = negativeButtonText;
+ }
+
+ /**
+ * @see #setNegativeButtonText(CharSequence)
+ * @param negativeButtonTextResId The negative button text as a resource.
+ */
+ public void setNegativeButtonText(int negativeButtonTextResId) {
+ setNegativeButtonText(getContext().getString(negativeButtonTextResId));
+ }
+
+ /**
+ * Returns the text of the negative button to be shown on subsequent
+ * dialogs.
+ *
+ * @return The text of the negative button.
+ */
+ public CharSequence getNegativeButtonText() {
+ return mNegativeButtonText;
+ }
+
+ /**
+ * Sets the layout resource that is inflated as the {@link View} to be shown
+ * as the content View of subsequent dialogs.
+ *
+ * @param dialogLayoutResId The layout resource ID to be inflated.
+ * @see #setDialogMessage(CharSequence)
+ */
+ public void setDialogLayoutResource(int dialogLayoutResId) {
+ mDialogLayoutResId = dialogLayoutResId;
+ }
+
+ /**
+ * Returns the layout resource that is used as the content View for
+ * subsequent dialogs.
+ *
+ * @return The layout resource.
+ */
+ public int getDialogLayoutResource() {
+ return mDialogLayoutResId;
+ }
+
+ /**
+ * Prepares the dialog builder to be shown when the preference is clicked.
+ * Use this to set custom properties on the dialog.
+ * <p>
+ * Do not {@link AlertDialog.Builder#create()} or
+ * {@link AlertDialog.Builder#show()}.
+ */
+ protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
+ }
+
+ @Override
+ protected void onClick() {
+ showDialog(null);
+ }
+
+ /**
+ * Shows the dialog associated with this Preference. This is normally initiated
+ * automatically on clicking on the preference. Call this method if you need to
+ * show the dialog on some other event.
+ *
+ * @param state Optional instance state to restore on the dialog
+ */
+ protected void showDialog(Bundle state) {
+ Context context = getContext();
+
+ mWhichButtonClicked = DialogInterface.BUTTON2;
+
+ mBuilder = new AlertDialog.Builder(context)
+ .setTitle(mDialogTitle)
+ .setIcon(mDialogIcon)
+ .setPositiveButton(mPositiveButtonText, this)
+ .setNegativeButton(mNegativeButtonText, this);
+
+ View contentView = onCreateDialogView();
+ if (contentView != null) {
+ onBindDialogView(contentView);
+ mBuilder.setView(contentView);
+ } else {
+ mBuilder.setMessage(mDialogMessage);
+ }
+
+ onPrepareDialogBuilder(mBuilder);
+
+ getPreferenceManager().registerOnActivityDestroyListener(this);
+
+ // Create the dialog
+ final Dialog dialog = mDialog = mBuilder.create();
+ if (state != null) {
+ dialog.onRestoreInstanceState(state);
+ }
+ dialog.setOnDismissListener(this);
+ dialog.show();
+ }
+
+ /**
+ * Creates the content view for the dialog (if a custom content view is
+ * required). By default, it inflates the dialog layout resource if it is
+ * set.
+ *
+ * @return The content View for the dialog.
+ * @see #setLayoutResource(int)
+ */
+ protected View onCreateDialogView() {
+ if (mDialogLayoutResId == 0) {
+ return null;
+ }
+
+ LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(
+ Context.LAYOUT_INFLATER_SERVICE);
+ return inflater.inflate(mDialogLayoutResId, null);
+ }
+
+ /**
+ * Binds views in the content View of the dialog to data.
+ * <p>
+ * Make sure to call through to the superclass implementation.
+ *
+ * @param view The content View of the dialog, if it is custom.
+ */
+ protected void onBindDialogView(View view) {
+ View dialogMessageView = view.findViewById(com.android.internal.R.id.message);
+
+ if (dialogMessageView != null) {
+ final CharSequence message = getDialogMessage();
+ int newVisibility = View.GONE;
+
+ if (!TextUtils.isEmpty(message)) {
+ if (dialogMessageView instanceof TextView) {
+ ((TextView) dialogMessageView).setText(message);
+ }
+
+ newVisibility = View.VISIBLE;
+ }
+
+ if (dialogMessageView.getVisibility() != newVisibility) {
+ dialogMessageView.setVisibility(newVisibility);
+ }
+ }
+ }
+
+ public void onClick(DialogInterface dialog, int which) {
+ mWhichButtonClicked = which;
+ }
+
+ public void onDismiss(DialogInterface dialog) {
+
+ getPreferenceManager().unregisterOnActivityDestroyListener(this);
+
+ mDialog = null;
+ onDialogClosed(mWhichButtonClicked == DialogInterface.BUTTON1);
+ }
+
+ /**
+ * Called when the dialog is dismissed and should be used to save data to
+ * the {@link SharedPreferences}.
+ *
+ * @param positiveResult Whether the positive button was clicked (true), or
+ * the negative button was clicked or the dialog was canceled (false).
+ */
+ protected void onDialogClosed(boolean positiveResult) {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void onActivityDestroy() {
+
+ if (mDialog == null || !mDialog.isShowing()) {
+ return;
+ }
+
+ mDialog.dismiss();
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ final Parcelable superState = super.onSaveInstanceState();
+ if (mDialog == null || !mDialog.isShowing()) {
+ return superState;
+ }
+
+ final SavedState myState = new SavedState(superState);
+ myState.isDialogShowing = true;
+ myState.dialogBundle = mDialog.onSaveInstanceState();
+ return myState;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ if (state == null || !state.getClass().equals(SavedState.class)) {
+ // Didn't save state for us in onSaveInstanceState
+ super.onRestoreInstanceState(state);
+ return;
+ }
+
+ SavedState myState = (SavedState) state;
+ super.onRestoreInstanceState(myState.getSuperState());
+ if (myState.isDialogShowing) {
+ showDialog(myState.dialogBundle);
+ }
+ }
+
+ private static class SavedState extends BaseSavedState {
+ boolean isDialogShowing;
+ Bundle dialogBundle;
+
+ public SavedState(Parcel source) {
+ super(source);
+ isDialogShowing = source.readInt() == 1;
+ dialogBundle = source.readBundle();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeInt(isDialogShowing ? 1 : 0);
+ dest.writeBundle(dialogBundle);
+ }
+
+ public SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ public static final Parcelable.Creator<SavedState> CREATOR =
+ new Parcelable.Creator<SavedState>() {
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+
+}
diff --git a/core/java/android/preference/EditTextPreference.java b/core/java/android/preference/EditTextPreference.java
new file mode 100644
index 0000000..be56003
--- /dev/null
+++ b/core/java/android/preference/EditTextPreference.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.preference;
+
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.res.TypedArray;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+
+/**
+ * The {@link EditTextPreference} class is a preference that allows for string
+ * input.
+ * <p>
+ * It is a subclass of {@link DialogPreference} and shows the {@link EditText}
+ * in a dialog. This {@link EditText} can be modified either programmatically
+ * via {@link #getEditText()}, or through XML by setting any EditText
+ * attributes on the EditTextPreference.
+ * <p>
+ * This preference will store a string into the SharedPreferences.
+ * <p>
+ * See {@link android.R.styleable#EditText EditText Attributes}.
+ */
+public class EditTextPreference extends DialogPreference {
+ /**
+ * The edit text shown in the dialog.
+ */
+ private EditText mEditText;
+
+ private String mText;
+
+ public EditTextPreference(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ mEditText = new EditText(context, attrs);
+
+ // Give it an ID so it can be saved/restored
+ mEditText.setId(com.android.internal.R.id.edit);
+
+ /*
+ * The preference framework and view framework both have an 'enabled'
+ * attribute. Most likely, the 'enabled' specified in this XML is for
+ * the preference framework, but it was also given to the view framework.
+ * We reset the enabled state.
+ */
+ mEditText.setEnabled(true);
+ }
+
+ public EditTextPreference(Context context, AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.editTextPreferenceStyle);
+ }
+
+ public EditTextPreference(Context context) {
+ this(context, null);
+ }
+
+ /**
+ * Saves the text to the {@link SharedPreferences}.
+ *
+ * @param text The text to save
+ */
+ public void setText(String text) {
+ final boolean wasBlocking = shouldDisableDependents();
+
+ mText = text;
+
+ persistString(text);
+
+ final boolean isBlocking = shouldDisableDependents();
+ if (isBlocking != wasBlocking) {
+ notifyDependencyChange(isBlocking);
+ }
+ }
+
+ /**
+ * Gets the text from the {@link SharedPreferences}.
+ *
+ * @return The current preference value.
+ */
+ public String getText() {
+ return mText;
+ }
+
+ @Override
+ protected void onBindDialogView(View view) {
+ super.onBindDialogView(view);
+
+ EditText editText = mEditText;
+ editText.setText(getText());
+
+ ViewParent oldParent = editText.getParent();
+ if (oldParent != view) {
+ if (oldParent != null) {
+ ((ViewGroup) oldParent).removeView(editText);
+ }
+ onAddEditTextToDialogView(view, editText);
+ }
+ }
+
+ /**
+ * Adds the EditText widget of this preference to the dialog's view.
+ *
+ * @param dialogView The dialog view.
+ */
+ protected void onAddEditTextToDialogView(View dialogView, EditText editText) {
+ ViewGroup container = (ViewGroup) dialogView
+ .findViewById(com.android.internal.R.id.edittext_container);
+ if (container != null) {
+ container.addView(editText, ViewGroup.LayoutParams.FILL_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+ }
+ }
+
+ @Override
+ protected void onDialogClosed(boolean positiveResult) {
+ super.onDialogClosed(positiveResult);
+
+ if (positiveResult) {
+ String value = mEditText.getText().toString();
+ if (callChangeListener(value)) {
+ setText(value);
+ }
+ }
+ }
+
+ @Override
+ protected Object onGetDefaultValue(TypedArray a, int index) {
+ return a.getString(index);
+ }
+
+ @Override
+ protected void onSetInitialValue(boolean restoreValue, Object defaultValue) {
+ setText(restoreValue ? getPersistedString(mText) : (String) defaultValue);
+ }
+
+ @Override
+ public boolean shouldDisableDependents() {
+ return TextUtils.isEmpty(mText) || super.shouldDisableDependents();
+ }
+
+ /**
+ * Returns the {@link EditText} widget that will be shown in the dialog.
+ *
+ * @return The {@link EditText} widget that will be shown in the dialog.
+ */
+ public EditText getEditText() {
+ return mEditText;
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ final Parcelable superState = super.onSaveInstanceState();
+ if (isPersistent()) {
+ // No need to save instance state since it's persistent
+ return superState;
+ }
+
+ final SavedState myState = new SavedState(superState);
+ myState.text = getText();
+ return myState;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ if (state == null || !state.getClass().equals(SavedState.class)) {
+ // Didn't save state for us in onSaveInstanceState
+ super.onRestoreInstanceState(state);
+ return;
+ }
+
+ SavedState myState = (SavedState) state;
+ super.onRestoreInstanceState(myState.getSuperState());
+ setText(myState.text);
+ }
+
+ private static class SavedState extends BaseSavedState {
+ String text;
+
+ public SavedState(Parcel source) {
+ super(source);
+ text = source.readString();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeString(text);
+ }
+
+ public SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ public static final Parcelable.Creator<SavedState> CREATOR =
+ new Parcelable.Creator<SavedState>() {
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+
+}
diff --git a/core/java/android/preference/GenericInflater.java b/core/java/android/preference/GenericInflater.java
new file mode 100644
index 0000000..3003290
--- /dev/null
+++ b/core/java/android/preference/GenericInflater.java
@@ -0,0 +1,520 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.preference;
+
+import java.io.IOException;
+import java.lang.reflect.Constructor;
+import java.util.HashMap;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.content.Context;
+import android.content.res.XmlResourceParser;
+import android.util.AttributeSet;
+import android.util.Xml;
+import android.view.ContextThemeWrapper;
+import android.view.InflateException;
+import android.view.LayoutInflater;
+
+// TODO: fix generics
+/**
+ * Generic XML inflater. This has been adapted from {@link LayoutInflater} and
+ * quickly passed over to use generics.
+ *
+ * @hide
+ * @param T The type of the items to inflate
+ * @param P The type of parents (that is those items that contain other items).
+ * Must implement {@link GenericInflater.Parent}
+ */
+abstract class GenericInflater<T, P extends GenericInflater.Parent> {
+ private final boolean DEBUG = false;
+
+ protected final Context mContext;
+
+ // these are optional, set by the caller
+ private boolean mFactorySet;
+ private Factory<T> mFactory;
+
+ private final Object[] mConstructorArgs = new Object[2];
+
+ private static final Class[] mConstructorSignature = new Class[] {
+ Context.class, AttributeSet.class};
+
+ private static final HashMap sConstructorMap = new HashMap();
+
+ private String mDefaultPackage;
+
+ public interface Parent<T> {
+ public void addItemFromInflater(T child);
+ }
+
+ public interface Factory<T> {
+ /**
+ * Hook you can supply that is called when inflating from a
+ * inflater. You can use this to customize the tag
+ * names available in your XML files.
+ * <p>
+ * Note that it is good practice to prefix these custom names with your
+ * package (i.e., com.coolcompany.apps) to avoid conflicts with system
+ * names.
+ *
+ * @param name Tag name to be inflated.
+ * @param context The context the item is being created in.
+ * @param attrs Inflation attributes as specified in XML file.
+ * @return Newly created item. Return null for the default behavior.
+ */
+ public T onCreateItem(String name, Context context, AttributeSet attrs);
+ }
+
+ private static class FactoryMerger<T> implements Factory<T> {
+ private final Factory<T> mF1, mF2;
+
+ FactoryMerger(Factory<T> f1, Factory<T> f2) {
+ mF1 = f1;
+ mF2 = f2;
+ }
+
+ public T onCreateItem(String name, Context context, AttributeSet attrs) {
+ T v = mF1.onCreateItem(name, context, attrs);
+ if (v != null) return v;
+ return mF2.onCreateItem(name, context, attrs);
+ }
+ }
+
+ /**
+ * Create a new inflater instance associated with a
+ * particular Context.
+ *
+ * @param context The Context in which this inflater will
+ * create its items; most importantly, this supplies the theme
+ * from which the default values for their attributes are
+ * retrieved.
+ */
+ protected GenericInflater(Context context) {
+ mContext = context;
+ }
+
+ /**
+ * Create a new inflater instance that is a copy of an
+ * existing inflater, optionally with its Context
+ * changed. For use in implementing {@link #cloneInContext}.
+ *
+ * @param original The original inflater to copy.
+ * @param newContext The new Context to use.
+ */
+ protected GenericInflater(GenericInflater<T,P> original, Context newContext) {
+ mContext = newContext;
+ mFactory = original.mFactory;
+ }
+
+ /**
+ * Create a copy of the existing inflater object, with the copy
+ * pointing to a different Context than the original. This is used by
+ * {@link ContextThemeWrapper} to create a new inflater to go along
+ * with the new Context theme.
+ *
+ * @param newContext The new Context to associate with the new inflater.
+ * May be the same as the original Context if desired.
+ *
+ * @return Returns a brand spanking new inflater object associated with
+ * the given Context.
+ */
+ public abstract GenericInflater cloneInContext(Context newContext);
+
+ /**
+ * Sets the default package that will be searched for classes to construct
+ * for tag names that have no explicit package.
+ *
+ * @param defaultPackage The default package. This will be prepended to the
+ * tag name, so it should end with a period.
+ */
+ public void setDefaultPackage(String defaultPackage) {
+ mDefaultPackage = defaultPackage;
+ }
+
+ /**
+ * Returns the default package, or null if it is not set.
+ *
+ * @see #setDefaultPackage(String)
+ * @return The default package.
+ */
+ public String getDefaultPackage() {
+ return mDefaultPackage;
+ }
+
+ /**
+ * Return the context we are running in, for access to resources, class
+ * loader, etc.
+ */
+ public Context getContext() {
+ return mContext;
+ }
+
+ /**
+ * Return the current factory (or null). This is called on each element
+ * name. If the factory returns an item, add that to the hierarchy. If it
+ * returns null, proceed to call onCreateItem(name).
+ */
+ public final Factory<T> getFactory() {
+ return mFactory;
+ }
+
+ /**
+ * Attach a custom Factory interface for creating items while using this
+ * inflater. This must not be null, and can only be set
+ * once; after setting, you can not change the factory. This is called on
+ * each element name as the XML is parsed. If the factory returns an item,
+ * that is added to the hierarchy. If it returns null, the next factory
+ * default {@link #onCreateItem} method is called.
+ * <p>
+ * If you have an existing inflater and want to add your
+ * own factory to it, use {@link #cloneInContext} to clone the existing
+ * instance and then you can use this function (once) on the returned new
+ * instance. This will merge your own factory with whatever factory the
+ * original instance is using.
+ */
+ public void setFactory(Factory<T> factory) {
+ if (mFactorySet) {
+ throw new IllegalStateException("" +
+ "A factory has already been set on this inflater");
+ }
+ if (factory == null) {
+ throw new NullPointerException("Given factory can not be null");
+ }
+ mFactorySet = true;
+ if (mFactory == null) {
+ mFactory = factory;
+ } else {
+ mFactory = new FactoryMerger<T>(factory, mFactory);
+ }
+ }
+
+
+ /**
+ * Inflate a new item hierarchy from the specified xml resource. Throws
+ * InflaterException if there is an error.
+ *
+ * @param resource ID for an XML resource to load (e.g.,
+ * <code>R.layout.main_page</code>)
+ * @param root Optional parent of the generated hierarchy.
+ * @return The root of the inflated hierarchy. If root was supplied,
+ * this is the root item; otherwise it is the root of the inflated
+ * XML file.
+ */
+ public T inflate(int resource, P root) {
+ return inflate(resource, root, root != null);
+ }
+
+ /**
+ * Inflate a new hierarchy from the specified xml node. Throws
+ * InflaterException if there is an error. *
+ * <p>
+ * <em><strong>Important</strong></em>&nbsp;&nbsp;&nbsp;For performance
+ * reasons, inflation relies heavily on pre-processing of XML files
+ * that is done at build time. Therefore, it is not currently possible to
+ * use inflater with an XmlPullParser over a plain XML file at runtime.
+ *
+ * @param parser XML dom node containing the description of the
+ * hierarchy.
+ * @param root Optional parent of the generated hierarchy.
+ * @return The root of the inflated hierarchy. If root was supplied,
+ * this is the that; otherwise it is the root of the inflated
+ * XML file.
+ */
+ public T inflate(XmlPullParser parser, P root) {
+ return inflate(parser, root, root != null);
+ }
+
+ /**
+ * Inflate a new hierarchy from the specified xml resource. Throws
+ * InflaterException if there is an error.
+ *
+ * @param resource ID for an XML resource to load (e.g.,
+ * <code>R.layout.main_page</code>)
+ * @param root Optional root to be the parent of the generated hierarchy (if
+ * <em>attachToRoot</em> is true), or else simply an object that
+ * provides a set of values for root of the returned
+ * hierarchy (if <em>attachToRoot</em> is false.)
+ * @param attachToRoot Whether the inflated hierarchy should be attached to
+ * the root parameter?
+ * @return The root of the inflated hierarchy. If root was supplied and
+ * attachToRoot is true, this is root; otherwise it is the root of
+ * the inflated XML file.
+ */
+ public T inflate(int resource, P root, boolean attachToRoot) {
+ if (DEBUG) System.out.println("INFLATING from resource: " + resource);
+ XmlResourceParser parser = getContext().getResources().getXml(resource);
+ try {
+ return inflate(parser, root, attachToRoot);
+ } finally {
+ parser.close();
+ }
+ }
+
+ /**
+ * Inflate a new hierarchy from the specified XML node. Throws
+ * InflaterException if there is an error.
+ * <p>
+ * <em><strong>Important</strong></em>&nbsp;&nbsp;&nbsp;For performance
+ * reasons, inflation relies heavily on pre-processing of XML files
+ * that is done at build time. Therefore, it is not currently possible to
+ * use inflater with an XmlPullParser over a plain XML file at runtime.
+ *
+ * @param parser XML dom node containing the description of the
+ * hierarchy.
+ * @param root Optional to be the parent of the generated hierarchy (if
+ * <em>attachToRoot</em> is true), or else simply an object that
+ * provides a set of values for root of the returned
+ * hierarchy (if <em>attachToRoot</em> is false.)
+ * @param attachToRoot Whether the inflated hierarchy should be attached to
+ * the root parameter?
+ * @return The root of the inflated hierarchy. If root was supplied and
+ * attachToRoot is true, this is root; otherwise it is the root of
+ * the inflated XML file.
+ */
+ public T inflate(XmlPullParser parser, P root,
+ boolean attachToRoot) {
+ synchronized (mConstructorArgs) {
+ final AttributeSet attrs = Xml.asAttributeSet(parser);
+ mConstructorArgs[0] = mContext;
+ T result = (T) root;
+
+ try {
+ // Look for the root node.
+ int type;
+ while ((type = parser.next()) != parser.START_TAG
+ && type != parser.END_DOCUMENT) {
+ ;
+ }
+
+ if (type != parser.START_TAG) {
+ throw new InflateException(parser.getPositionDescription()
+ + ": No start tag found!");
+ }
+
+ if (DEBUG) {
+ System.out.println("**************************");
+ System.out.println("Creating root: "
+ + parser.getName());
+ System.out.println("**************************");
+ }
+ // Temp is the root that was found in the xml
+ T xmlRoot = createItemFromTag(parser, parser.getName(),
+ attrs);
+
+ result = (T) onMergeRoots(root, attachToRoot, (P) xmlRoot);
+
+ if (DEBUG) {
+ System.out.println("-----> start inflating children");
+ }
+ // Inflate all children under temp
+ rInflate(parser, result, attrs);
+ if (DEBUG) {
+ System.out.println("-----> done inflating children");
+ }
+
+ } catch (InflateException e) {
+ throw e;
+
+ } catch (XmlPullParserException e) {
+ InflateException ex = new InflateException(e.getMessage());
+ ex.initCause(e);
+ throw ex;
+ } catch (IOException e) {
+ InflateException ex = new InflateException(
+ parser.getPositionDescription()
+ + ": " + e.getMessage());
+ ex.initCause(e);
+ throw ex;
+ }
+
+ return result;
+ }
+ }
+
+ /**
+ * Low-level function for instantiating by name. This attempts to
+ * instantiate class of the given <var>name</var> found in this
+ * inflater's ClassLoader.
+ *
+ * <p>
+ * There are two things that can happen in an error case: either the
+ * exception describing the error will be thrown, or a null will be
+ * returned. You must deal with both possibilities -- the former will happen
+ * the first time createItem() is called for a class of a particular name,
+ * the latter every time there-after for that class name.
+ *
+ * @param name The full name of the class to be instantiated.
+ * @param attrs The XML attributes supplied for this instance.
+ *
+ * @return The newly instantied item, or null.
+ */
+ public final T createItem(String name, String prefix, AttributeSet attrs)
+ throws ClassNotFoundException, InflateException {
+ Constructor constructor = (Constructor) sConstructorMap.get(name);
+
+ try {
+ if (null == constructor) {
+ // Class not found in the cache, see if it's real,
+ // and try to add it
+ Class clazz = mContext.getClassLoader().loadClass(
+ prefix != null ? (prefix + name) : name);
+ constructor = clazz.getConstructor(mConstructorSignature);
+ sConstructorMap.put(name, constructor);
+ }
+
+ Object[] args = mConstructorArgs;
+ args[1] = attrs;
+ return (T) constructor.newInstance(args);
+
+ } catch (NoSuchMethodException e) {
+ InflateException ie = new InflateException(attrs
+ .getPositionDescription()
+ + ": Error inflating class "
+ + (prefix != null ? (prefix + name) : name));
+ ie.initCause(e);
+ throw ie;
+
+ } catch (ClassNotFoundException e) {
+ // If loadClass fails, we should propagate the exception.
+ throw e;
+ } catch (Exception e) {
+ InflateException ie = new InflateException(attrs
+ .getPositionDescription()
+ + ": Error inflating class "
+ + constructor.getClass().getName());
+ ie.initCause(e);
+ throw ie;
+ }
+ }
+
+ /**
+ * This routine is responsible for creating the correct subclass of item
+ * given the xml element name. Override it to handle custom item objects. If
+ * you override this in your subclass be sure to call through to
+ * super.onCreateItem(name) for names you do not recognize.
+ *
+ * @param name The fully qualified class name of the item to be create.
+ * @param attrs An AttributeSet of attributes to apply to the item.
+ * @return The item created.
+ */
+ protected T onCreateItem(String name, AttributeSet attrs) throws ClassNotFoundException {
+ return createItem(name, mDefaultPackage, attrs);
+ }
+
+ private final T createItemFromTag(XmlPullParser parser, String name, AttributeSet attrs) {
+ if (DEBUG) System.out.println("******** Creating item: " + name);
+
+ try {
+ T item = (mFactory == null) ? null : mFactory.onCreateItem(name, mContext, attrs);
+
+ if (item == null) {
+ if (-1 == name.indexOf('.')) {
+ item = onCreateItem(name, attrs);
+ } else {
+ item = createItem(name, null, attrs);
+ }
+ }
+
+ if (DEBUG) System.out.println("Created item is: " + item);
+ return item;
+
+ } catch (InflateException e) {
+ throw e;
+
+ } catch (ClassNotFoundException e) {
+ InflateException ie = new InflateException(attrs
+ .getPositionDescription()
+ + ": Error inflating class " + name);
+ ie.initCause(e);
+ throw ie;
+
+ } catch (Exception e) {
+ InflateException ie = new InflateException(attrs
+ .getPositionDescription()
+ + ": Error inflating class " + name);
+ ie.initCause(e);
+ throw ie;
+ }
+ }
+
+ /**
+ * Recursive method used to descend down the xml hierarchy and instantiate
+ * items, instantiate their children, and then call onFinishInflate().
+ */
+ private void rInflate(XmlPullParser parser, T parent, final AttributeSet attrs)
+ throws XmlPullParserException, IOException {
+ final int depth = parser.getDepth();
+
+ int type;
+ while (((type = parser.next()) != parser.END_TAG ||
+ parser.getDepth() > depth) && type != parser.END_DOCUMENT) {
+
+ if (type != parser.START_TAG) {
+ continue;
+ }
+
+ if (onCreateCustomFromTag(parser, parent, attrs)) {
+ continue;
+ }
+
+ if (DEBUG) {
+ System.out.println("Now inflating tag: " + parser.getName());
+ }
+ String name = parser.getName();
+
+ T item = createItemFromTag(parser, name, attrs);
+
+ if (DEBUG) {
+ System.out
+ .println("Creating params from parent: " + parent);
+ }
+
+ ((P) parent).addItemFromInflater(item);
+
+ if (DEBUG) {
+ System.out.println("-----> start inflating children");
+ }
+ rInflate(parser, item, attrs);
+ if (DEBUG) {
+ System.out.println("-----> done inflating children");
+ }
+ }
+
+ }
+
+ /**
+ * Before this inflater tries to create an item from the tag, this method
+ * will be called. The parser will be pointing to the start of a tag, you
+ * must stop parsing and return when you reach the end of this element!
+ *
+ * @param parser XML dom node containing the description of the hierarchy.
+ * @param parent The item that should be the parent of whatever you create.
+ * @param attrs An AttributeSet of attributes to apply to the item.
+ * @return Whether you created a custom object (true), or whether this
+ * inflater should proceed to create an item.
+ */
+ protected boolean onCreateCustomFromTag(XmlPullParser parser, T parent,
+ final AttributeSet attrs) throws XmlPullParserException {
+ return false;
+ }
+
+ protected P onMergeRoots(P givenRoot, boolean attachToGivenRoot, P xmlRoot) {
+ return xmlRoot;
+ }
+}
diff --git a/core/java/android/preference/ListPreference.java b/core/java/android/preference/ListPreference.java
new file mode 100644
index 0000000..6c98ded
--- /dev/null
+++ b/core/java/android/preference/ListPreference.java
@@ -0,0 +1,277 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.preference;
+
+
+import android.app.AlertDialog.Builder;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.res.TypedArray;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+
+/**
+ * The {@link ListPreference} is a preference that displays a list of entries as
+ * a dialog.
+ * <p>
+ * This preference will store a string into the SharedPreferences. This string will be the value
+ * from the {@link #setEntryValues(CharSequence[])} array.
+ *
+ * @attr ref android.R.styleable#ListPreference_entries
+ * @attr ref android.R.styleable#ListPreference_entryValues
+ */
+public class ListPreference extends DialogPreference {
+ private CharSequence[] mEntries;
+ private CharSequence[] mEntryValues;
+ private String mValue;
+ private int mClickedDialogEntryIndex;
+
+ public ListPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ TypedArray a = context.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.ListPreference, 0, 0);
+ mEntries = a.getTextArray(com.android.internal.R.styleable.ListPreference_entries);
+ mEntryValues = a.getTextArray(com.android.internal.R.styleable.ListPreference_entryValues);
+ a.recycle();
+ }
+
+ public ListPreference(Context context) {
+ this(context, null);
+ }
+
+ /**
+ * Sets the human-readable entries to be shown in the list. This will be
+ * shown in subsequent dialogs.
+ * <p>
+ * Each entry must have a corresponding index in
+ * {@link #setEntryValues(CharSequence[])}.
+ *
+ * @param entries The entries.
+ * @see #setEntryValues(CharSequence[])
+ */
+ public void setEntries(CharSequence[] entries) {
+ mEntries = entries;
+ }
+
+ /**
+ * @see #setEntries(CharSequence[])
+ * @param entriesResId The entries array as a resource.
+ */
+ public void setEntries(int entriesResId) {
+ setEntries(getContext().getResources().getTextArray(entriesResId));
+ }
+
+ /**
+ * The list of entries to be shown in the list in subsequent dialogs.
+ *
+ * @return The list as an array.
+ */
+ public CharSequence[] getEntries() {
+ return mEntries;
+ }
+
+ /**
+ * The array to find the value to save for a preference when an entry from
+ * entries is selected. If a user clicks on the second item in entries, the
+ * second item in this array will be saved to the preference.
+ *
+ * @param entryValues The array to be used as values to save for the preference.
+ */
+ public void setEntryValues(CharSequence[] entryValues) {
+ mEntryValues = entryValues;
+ }
+
+ /**
+ * @see #setEntryValues(CharSequence[])
+ * @param entryValuesResId The entry values array as a resource.
+ */
+ public void setEntryValues(int entryValuesResId) {
+ setEntryValues(getContext().getResources().getTextArray(entryValuesResId));
+ }
+
+ /**
+ * Returns the array of values to be saved for the preference.
+ *
+ * @return The array of values.
+ */
+ public CharSequence[] getEntryValues() {
+ return mEntryValues;
+ }
+
+ /**
+ * Sets the value of the key. This should be one of the entries in
+ * {@link #getEntryValues()}.
+ *
+ * @param value The value to set for the key.
+ */
+ public void setValue(String value) {
+ mValue = value;
+
+ persistString(value);
+ }
+
+ /**
+ * Sets the value to the given index from the entry values.
+ *
+ * @param index The index of the value to set.
+ */
+ public void setValueIndex(int index) {
+ if (mEntryValues != null) {
+ setValue(mEntryValues[index].toString());
+ }
+ }
+
+ /**
+ * Returns the value of the key. This should be one of the entries in
+ * {@link #getEntryValues()}.
+ *
+ * @return The value of the key.
+ */
+ public String getValue() {
+ return mValue;
+ }
+
+ /**
+ * Returns the entry corresponding to the current value.
+ *
+ * @return The entry corresponding to the current value, or null.
+ */
+ public CharSequence getEntry() {
+ int index = getValueIndex();
+ return index >= 0 && mEntries != null ? mEntries[index] : null;
+ }
+
+ /**
+ * Returns the index of the given value (in the entry values array).
+ *
+ * @param value The value whose index should be returned.
+ * @return The index of the value, or -1 if not found.
+ */
+ public int findIndexOfValue(String value) {
+ if (value != null && mEntryValues != null) {
+ for (int i = mEntryValues.length - 1; i >= 0; i--) {
+ if (mEntryValues[i].equals(value)) {
+ return i;
+ }
+ }
+ }
+ return -1;
+ }
+
+ private int getValueIndex() {
+ return findIndexOfValue(mValue);
+ }
+
+ @Override
+ protected void onPrepareDialogBuilder(Builder builder) {
+ super.onPrepareDialogBuilder(builder);
+
+ if (mEntries == null || mEntryValues == null) {
+ throw new IllegalStateException(
+ "ListPreference requires an entries array and an entryValues array.");
+ }
+
+ mClickedDialogEntryIndex = getValueIndex();
+ builder.setSingleChoiceItems(mEntries, mClickedDialogEntryIndex,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ mClickedDialogEntryIndex = which;
+ }
+ });
+ }
+
+ @Override
+ protected void onDialogClosed(boolean positiveResult) {
+ super.onDialogClosed(positiveResult);
+
+ if (positiveResult && mClickedDialogEntryIndex >= 0 && mEntryValues != null) {
+ String value = mEntryValues[mClickedDialogEntryIndex].toString();
+ if (callChangeListener(value)) {
+ setValue(value);
+ }
+ }
+ }
+
+ @Override
+ protected Object onGetDefaultValue(TypedArray a, int index) {
+ return a.getString(index);
+ }
+
+ @Override
+ protected void onSetInitialValue(boolean restoreValue, Object defaultValue) {
+ setValue(restoreValue ? getPersistedString(mValue) : (String) defaultValue);
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ final Parcelable superState = super.onSaveInstanceState();
+ if (isPersistent()) {
+ // No need to save instance state since it's persistent
+ return superState;
+ }
+
+ final SavedState myState = new SavedState(superState);
+ myState.value = getValue();
+ return myState;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ if (state == null || !state.getClass().equals(SavedState.class)) {
+ // Didn't save state for us in onSaveInstanceState
+ super.onRestoreInstanceState(state);
+ return;
+ }
+
+ SavedState myState = (SavedState) state;
+ super.onRestoreInstanceState(myState.getSuperState());
+ setValue(myState.value);
+ }
+
+ private static class SavedState extends BaseSavedState {
+ String value;
+
+ public SavedState(Parcel source) {
+ super(source);
+ value = source.readString();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeString(value);
+ }
+
+ public SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ public static final Parcelable.Creator<SavedState> CREATOR =
+ new Parcelable.Creator<SavedState>() {
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+
+}
diff --git a/core/java/android/preference/OnDependencyChangeListener.java b/core/java/android/preference/OnDependencyChangeListener.java
new file mode 100644
index 0000000..ce25e34
--- /dev/null
+++ b/core/java/android/preference/OnDependencyChangeListener.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.preference;
+
+/**
+ * Interface definition for a callback to be invoked when this
+ * {@link Preference} changes with respect to enabling/disabling
+ * dependents.
+ */
+interface OnDependencyChangeListener {
+ /**
+ * Called when this preference has changed in a way that dependents should
+ * care to change their state.
+ *
+ * @param disablesDependent Whether the dependent should be disabled.
+ */
+ void onDependencyChanged(Preference dependency, boolean disablesDependent);
+}
diff --git a/core/java/android/preference/Preference.java b/core/java/android/preference/Preference.java
new file mode 100644
index 0000000..1db7525
--- /dev/null
+++ b/core/java/android/preference/Preference.java
@@ -0,0 +1,1577 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.preference;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.TypedArray;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import com.android.internal.util.CharSequences;
+import android.view.AbsSavedState;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ListView;
+import android.widget.TextView;
+
+/**
+ * The {@link Preference} class represents the basic preference UI building
+ * block that is displayed by a {@link PreferenceActivity} in the form of a
+ * {@link ListView}. This class provides the {@link View} to be displayed in
+ * the activity and associates with a {@link SharedPreferences} to
+ * store/retrieve the preference data.
+ * <p>
+ * When specifying a preference hierarchy in XML, each tag name can point to a
+ * subclass of {@link Preference}, similar to the view hierarchy and layouts.
+ * <p>
+ * This class contains a {@code key} that will be used as the key into the
+ * {@link SharedPreferences}. It is up to the subclass to decide how to store
+ * the value.
+ *
+ * @attr ref android.R.styleable#Preference_key
+ * @attr ref android.R.styleable#Preference_title
+ * @attr ref android.R.styleable#Preference_summary
+ * @attr ref android.R.styleable#Preference_order
+ * @attr ref android.R.styleable#Preference_layout
+ * @attr ref android.R.styleable#Preference_widgetLayout
+ * @attr ref android.R.styleable#Preference_enabled
+ * @attr ref android.R.styleable#Preference_selectable
+ * @attr ref android.R.styleable#Preference_dependency
+ * @attr ref android.R.styleable#Preference_persistent
+ * @attr ref android.R.styleable#Preference_defaultValue
+ * @attr ref android.R.styleable#Preference_shouldDisableView
+ */
+public class Preference implements Comparable<Preference>, OnDependencyChangeListener {
+ /**
+ * Specify for {@link #setOrder(int)} if a specific order is not required.
+ */
+ public static final int DEFAULT_ORDER = Integer.MAX_VALUE;
+
+ private Context mContext;
+ private PreferenceManager mPreferenceManager;
+
+ /**
+ * Set when added to hierarchy since we need a unique ID within that
+ * hierarchy.
+ */
+ private long mId;
+
+ private OnPreferenceChangeListener mOnChangeListener;
+ private OnPreferenceClickListener mOnClickListener;
+
+ private int mOrder = DEFAULT_ORDER;
+ private CharSequence mTitle;
+ private CharSequence mSummary;
+ private String mKey;
+ private Intent mIntent;
+ private boolean mEnabled = true;
+ private boolean mSelectable = true;
+ private boolean mRequiresKey;
+ private boolean mPersistent = true;
+ private String mDependencyKey;
+ private Object mDefaultValue;
+
+ /**
+ * @see #setShouldDisableView(boolean)
+ */
+ private boolean mShouldDisableView = true;
+
+ private int mLayoutResId = com.android.internal.R.layout.preference;
+ private int mWidgetLayoutResId;
+ private boolean mHasSpecifiedLayout = false;
+
+ private OnPreferenceChangeInternalListener mListener;
+
+ private List<Preference> mDependents;
+
+ private boolean mBaseMethodCalled;
+
+ /**
+ * Interface definition for a callback to be invoked when this
+ * {@link Preference Preference's} value has been changed by the user and is
+ * about to be set and/or persisted. This gives the client a chance
+ * to prevent setting and/or persisting the value.
+ */
+ public interface OnPreferenceChangeListener {
+ /**
+ * Called when this preference has been changed by the user. This is
+ * called before the preference's state is about to be updated and
+ * before the state is persisted.
+ *
+ * @param preference This preference.
+ * @param newValue The new value of the preference.
+ * @return Whether or not to update this preference's state with the new value.
+ */
+ boolean onPreferenceChange(Preference preference, Object newValue);
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when a preference is
+ * clicked.
+ */
+ public interface OnPreferenceClickListener {
+ /**
+ * Called when a preference has been clicked.
+ *
+ * @param preference The preference that was clicked.
+ * @return Whether the click was handled.
+ */
+ boolean onPreferenceClick(Preference preference);
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when this
+ * {@link Preference} is changed or if this is a group, there is an
+ * addition/removal of {@link Preference}(s). This is used internally.
+ */
+ interface OnPreferenceChangeInternalListener {
+ /**
+ * Called when this preference has changed.
+ *
+ * @param preference This preference.
+ */
+ void onPreferenceChange(Preference preference);
+
+ /**
+ * Called when this group has added/removed {@link Preference}(s).
+ *
+ * @param preference This preference.
+ */
+ void onPreferenceHierarchyChange(Preference preference);
+ }
+
+ /**
+ * Perform inflation from XML and apply a class-specific base style. This
+ * constructor of {@link Preference} allows subclasses to use their own base
+ * style when they are inflating. For example, a {@link CheckBoxPreference}'s
+ * constructor would call this version of the super class constructor and
+ * supply {@code android.R.attr.checkBoxPreferenceStyle} for <var>defStyle</var>;
+ * this allows the theme's checkbox preference style to modify all of the base
+ * preference attributes as well as the {@link CheckBoxPreference} class's
+ * attributes.
+ *
+ * @param context The Context this is associated with, through which it can
+ * access the current theme, resources, {@link SharedPreferences},
+ * etc.
+ * @param attrs The attributes of the XML tag that is inflating the preference.
+ * @param defStyle The default style to apply to this preference. If 0, no style
+ * will be applied (beyond what is included in the theme). This
+ * may either be an attribute resource, whose value will be
+ * retrieved from the current theme, or an explicit style
+ * resource.
+ * @see #Preference(Context, AttributeSet)
+ */
+ public Preference(Context context, AttributeSet attrs, int defStyle) {
+ mContext = context;
+
+ TypedArray a = context.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.Preference);
+ if (a.hasValue(com.android.internal.R.styleable.Preference_layout) ||
+ a.hasValue(com.android.internal.R.styleable.Preference_widgetLayout)) {
+ // This preference has a custom layout defined (not one taken from
+ // the default style)
+ mHasSpecifiedLayout = true;
+ }
+ a.recycle();
+
+ a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.Preference,
+ defStyle, 0);
+ for (int i = a.getIndexCount(); i >= 0; i--) {
+ int attr = a.getIndex(i);
+ switch (attr) {
+ case com.android.internal.R.styleable.Preference_key:
+ mKey = a.getString(attr);
+ break;
+
+ case com.android.internal.R.styleable.Preference_title:
+ mTitle = a.getString(attr);
+ break;
+
+ case com.android.internal.R.styleable.Preference_summary:
+ mSummary = a.getString(attr);
+ break;
+
+ case com.android.internal.R.styleable.Preference_order:
+ mOrder = a.getInt(attr, mOrder);
+ break;
+
+ case com.android.internal.R.styleable.Preference_layout:
+ mLayoutResId = a.getResourceId(attr, mLayoutResId);
+ break;
+
+ case com.android.internal.R.styleable.Preference_widgetLayout:
+ mWidgetLayoutResId = a.getResourceId(attr, mWidgetLayoutResId);
+ break;
+
+ case com.android.internal.R.styleable.Preference_enabled:
+ mEnabled = a.getBoolean(attr, true);
+ break;
+
+ case com.android.internal.R.styleable.Preference_selectable:
+ mSelectable = a.getBoolean(attr, true);
+ break;
+
+ case com.android.internal.R.styleable.Preference_persistent:
+ mPersistent = a.getBoolean(attr, mPersistent);
+ break;
+
+ case com.android.internal.R.styleable.Preference_dependency:
+ mDependencyKey = a.getString(attr);
+ break;
+
+ case com.android.internal.R.styleable.Preference_defaultValue:
+ mDefaultValue = onGetDefaultValue(a, attr);
+ break;
+
+ case com.android.internal.R.styleable.Preference_shouldDisableView:
+ mShouldDisableView = a.getBoolean(attr, mShouldDisableView);
+ break;
+ }
+ }
+ a.recycle();
+ }
+
+ /**
+ * Constructor that is called when inflating a preference from XML. This is
+ * called when a preference is being constructed from an XML file, supplying
+ * attributes that were specified in the XML file. This version uses a
+ * default style of 0, so the only attribute values applied are those in the
+ * Context's Theme and the given AttributeSet.
+ *
+ * @param context The Context this is associated with, through which it can
+ * access the current theme, resources, {@link SharedPreferences},
+ * etc.
+ * @param attrs The attributes of the XML tag that is inflating the
+ * preference.
+ * @see #Preference(Context, AttributeSet, int)
+ */
+ public Preference(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ /**
+ * Constructor to create a Preference.
+ *
+ * @param context The context to store preference values.
+ */
+ public Preference(Context context) {
+ this(context, null);
+ }
+
+ /**
+ * Called when {@link Preference} is being inflated and the default value
+ * attribute needs to be read. Since different preference types have
+ * different value types, the subclass should get and return the default
+ * value which will be its value type.
+ * <p>
+ * For example, if the value type is String, the body of the method would
+ * proxy to {@link TypedArray#getString(int)}.
+ *
+ * @param a The set of attributes.
+ * @param index The index of the default value attribute.
+ * @return The default value of this preference type.
+ */
+ protected Object onGetDefaultValue(TypedArray a, int index) {
+ return null;
+ }
+
+ /**
+ * Sets an {@link Intent} to be used for
+ * {@link Context#startActivity(Intent)} when the preference is clicked.
+ *
+ * @param intent The intent associated with the preference.
+ */
+ public void setIntent(Intent intent) {
+ mIntent = intent;
+ }
+
+ /**
+ * Return the {@link Intent} associated with this preference.
+ *
+ * @return The {@link Intent} last set via {@link #setIntent(Intent)} or XML.
+ */
+ public Intent getIntent() {
+ return mIntent;
+ }
+
+ /**
+ * Sets the layout resource that is inflated as the {@link View} to be shown
+ * for this preference. In most cases, the default layout is sufficient for
+ * custom preferences and only the widget layout needs to be changed.
+ * <p>
+ * This layout should contain a {@link ViewGroup} with ID
+ * {@link android.R.id#widget_frame} to be the parent of the specific widget
+ * for this preference. It should similarly contain
+ * {@link android.R.id#title} and {@link android.R.id#summary}.
+ *
+ * @param layoutResId The layout resource ID to be inflated and returned as
+ * a {@link View}.
+ * @see #setWidgetLayoutResource(int)
+ */
+ public void setLayoutResource(int layoutResId) {
+
+ if (!mHasSpecifiedLayout) {
+ mHasSpecifiedLayout = true;
+ }
+
+ mLayoutResId = layoutResId;
+ }
+
+ /**
+ * Gets the layout resource that will be shown as the {@link View} for this preference.
+ *
+ * @return The layout resource ID.
+ */
+ public int getLayoutResource() {
+ return mLayoutResId;
+ }
+
+ /**
+ * Sets The layout for the controllable widget portion of a preference. This
+ * is inflated into the main layout. For example, a checkbox preference
+ * would specify a custom layout (consisting of just the CheckBox) here,
+ * instead of creating its own main layout.
+ *
+ * @param widgetLayoutResId The layout resource ID to be inflated into the
+ * main layout.
+ * @see #setLayoutResource(int)
+ */
+ public void setWidgetLayoutResource(int widgetLayoutResId) {
+ mWidgetLayoutResId = widgetLayoutResId;
+ }
+
+ /**
+ * Gets the layout resource for the controllable widget portion of a preference.
+ *
+ * @return The layout resource ID.
+ */
+ public int getWidgetLayoutResource() {
+ return mWidgetLayoutResId;
+ }
+
+ /**
+ * Gets the View that will be shown in the {@link PreferenceActivity}.
+ *
+ * @param convertView The old view to reuse, if possible. Note: You should
+ * check that this view is non-null and of an appropriate type
+ * before using. If it is not possible to convert this view to
+ * display the correct data, this method can create a new view.
+ * @param parent The parent that this view will eventually be attached to.
+ * @return Returns the same Preference object, for chaining multiple calls
+ * into a single statement.
+ * @see #onCreateView(ViewGroup)
+ * @see #onBindView(View)
+ */
+ public View getView(View convertView, ViewGroup parent) {
+ if (convertView == null) {
+ convertView = onCreateView(parent);
+ }
+ onBindView(convertView);
+ return convertView;
+ }
+
+ /**
+ * Creates the View to be shown for this preference in the
+ * {@link PreferenceActivity}. The default behavior is to inflate the main
+ * layout of this preference (see {@link #setLayoutResource(int)}. If
+ * changing this behavior, please specify a {@link ViewGroup} with ID
+ * {@link android.R.id#widget_frame}.
+ * <p>
+ * Make sure to call through to the superclass's implementation.
+ *
+ * @param parent The parent that this view will eventually be attached to.
+ * @return The View that displays this preference.
+ * @see #onBindView(View)
+ */
+ protected View onCreateView(ViewGroup parent) {
+ final LayoutInflater layoutInflater =
+ (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ final View layout = layoutInflater.inflate(mLayoutResId, parent, false);
+
+ if (mWidgetLayoutResId != 0) {
+ final ViewGroup widgetFrame = (ViewGroup)layout.findViewById(com.android.internal.R.id.widget_frame);
+ layoutInflater.inflate(mWidgetLayoutResId, widgetFrame);
+ }
+
+ return layout;
+ }
+
+ /**
+ * Binds the created View to the data for the preference.
+ * <p>
+ * This is a good place to grab references to custom Views in the layout and
+ * set properties on them.
+ * <p>
+ * Make sure to call through to the superclass's implementation.
+ *
+ * @param view The View that shows this preference.
+ * @see #onCreateView(ViewGroup)
+ */
+ protected void onBindView(View view) {
+ TextView textView = (TextView) view.findViewById(com.android.internal.R.id.title);
+ if (textView != null) {
+ textView.setText(getTitle());
+ }
+
+ textView = (TextView) view.findViewById(com.android.internal.R.id.summary);
+ if (textView != null) {
+ final CharSequence summary = getSummary();
+ if (!TextUtils.isEmpty(summary)) {
+ if (textView.getVisibility() != View.VISIBLE) {
+ textView.setVisibility(View.VISIBLE);
+ }
+
+ textView.setText(getSummary());
+ } else {
+ if (textView.getVisibility() != View.GONE) {
+ textView.setVisibility(View.GONE);
+ }
+ }
+ }
+
+ if (mShouldDisableView) {
+ setEnabledStateOnViews(view, mEnabled);
+ }
+ }
+
+ /**
+ * Makes sure the view (and any children) get the enabled state changed.
+ */
+ private void setEnabledStateOnViews(View v, boolean enabled) {
+ v.setEnabled(enabled);
+
+ if (v instanceof ViewGroup) {
+ final ViewGroup vg = (ViewGroup) v;
+ for (int i = vg.getChildCount() - 1; i >= 0; i--) {
+ setEnabledStateOnViews(vg.getChildAt(i), enabled);
+ }
+ }
+ }
+
+ /**
+ * Sets the order of this {@link Preference} with respect to other
+ * {@link Preference} on the same level. If this is not specified, the
+ * default behavior is to sort alphabetically. The
+ * {@link PreferenceGroup#setOrderingAsAdded(boolean)} can be used to order
+ * preferences based on the order they appear in the XML.
+ *
+ * @param order The order for this preference. A lower value will be shown
+ * first. Use {@link #DEFAULT_ORDER} to sort alphabetically or
+ * allow ordering from XML.
+ * @see PreferenceGroup#setOrderingAsAdded(boolean)
+ * @see #DEFAULT_ORDER
+ */
+ public void setOrder(int order) {
+ if (order != mOrder) {
+ mOrder = order;
+
+ // Reorder the list
+ notifyHierarchyChanged();
+ }
+ }
+
+ /**
+ * Gets the order of this {@link Preference}.
+ *
+ * @return The order of this {@link Preference}.
+ * @see #setOrder(int)
+ */
+ public int getOrder() {
+ return mOrder;
+ }
+
+ /**
+ * Sets the title for the preference. This title will be placed into the ID
+ * {@link android.R.id#title} within the View created by
+ * {@link #onCreateView(ViewGroup)}.
+ *
+ * @param title The title of the preference.
+ */
+ public void setTitle(CharSequence title) {
+ if (title == null && mTitle != null || title != null && !title.equals(mTitle)) {
+ mTitle = title;
+ notifyChanged();
+ }
+ }
+
+ /**
+ * @see #setTitle(CharSequence)
+ * @param titleResId The title as a resource ID.
+ */
+ public void setTitle(int titleResId) {
+ setTitle(mContext.getString(titleResId));
+ }
+
+ /**
+ * Returns the title of the preference.
+ *
+ * @return The title.
+ * @see #setTitle(CharSequence)
+ */
+ public CharSequence getTitle() {
+ return mTitle;
+ }
+
+ /**
+ * Returns the summary of the preference.
+ *
+ * @return The summary.
+ * @see #setSummary(CharSequence)
+ */
+ public CharSequence getSummary() {
+ return mSummary;
+ }
+
+ /**
+ * Sets the summary for the preference. This summary will be placed into the
+ * ID {@link android.R.id#summary} within the View created by
+ * {@link #onCreateView(ViewGroup)}.
+ *
+ * @param summary The summary of the preference.
+ */
+ public void setSummary(CharSequence summary) {
+ if (summary == null && mSummary != null || summary != null && !summary.equals(mSummary)) {
+ mSummary = summary;
+ notifyChanged();
+ }
+ }
+
+ /**
+ * @see #setSummary(CharSequence)
+ * @param summaryResId The summary as a resource.
+ */
+ public void setSummary(int summaryResId) {
+ setSummary(mContext.getString(summaryResId));
+ }
+
+ /**
+ * Sets whether this preference is enabled. If disabled, the preference will
+ * not handle clicks.
+ *
+ * @param enabled Whether the preference is enabled.
+ */
+ public void setEnabled(boolean enabled) {
+ if (mEnabled != enabled) {
+ mEnabled = enabled;
+
+ // Enabled state can change dependent preferences' states, so notify
+ notifyDependencyChange(shouldDisableDependents());
+
+ notifyChanged();
+ }
+ }
+
+ /**
+ * Whether this {@link Preference} should be enabled in the list.
+ *
+ * @return Whether this preference is enabled.
+ */
+ public boolean isEnabled() {
+ return mEnabled;
+ }
+
+ /**
+ * Sets whether this preference is selectable.
+ *
+ * @param selectable Whether the preference is selectable.
+ */
+ public void setSelectable(boolean selectable) {
+ if (mSelectable != selectable) {
+ mSelectable = selectable;
+ notifyChanged();
+ }
+ }
+
+ /**
+ * Whether this {@link Preference} should be selectable in the list.
+ *
+ * @return Whether this preference is selectable.
+ */
+ public boolean isSelectable() {
+ return mSelectable;
+ }
+
+ /**
+ * Sets whether this {@link Preference} should disable its view when it gets
+ * disabled.
+ * <p>
+ * For example, set this and {@link #setEnabled(boolean)} to false for
+ * preferences that are only displaying information and 1) should not be
+ * clickable 2) should not have the view set to the disabled state.
+ *
+ * @param shouldDisableView Whether this preference should disable its view
+ * when the preference is disabled.
+ */
+ public void setShouldDisableView(boolean shouldDisableView) {
+ mShouldDisableView = shouldDisableView;
+ notifyChanged();
+ }
+
+ /**
+ * @see #setShouldDisableView(boolean)
+ * @return Whether this preference should disable its view when it is disabled.
+ */
+ public boolean getShouldDisableView() {
+ return mShouldDisableView;
+ }
+
+ /**
+ * Returns a unique ID for this preference. This ID should be unique across all
+ * preferences in a hierarchy.
+ *
+ * @return A unique ID for this preference.
+ */
+ long getId() {
+ return mId;
+ }
+
+ /**
+ * Processes a click on the preference. This includes saving the value to
+ * the {@link SharedPreferences}. However, the overridden method should
+ * call {@link #callChangeListener(Object)} to make sure the client wants to
+ * update the preference's state with the new value.
+ */
+ protected void onClick() {
+ }
+
+ /**
+ * Sets the key for the preference which is used as a key to the
+ * {@link SharedPreferences}. This should be unique for the package.
+ *
+ * @param key The key for the preference.
+ * @see #getId()
+ */
+ public void setKey(String key) {
+ mKey = key;
+
+ if (mRequiresKey && !hasKey()) {
+ requireKey();
+ }
+ }
+
+ /**
+ * Gets the key for the preference, which is also the key used for storing
+ * values into SharedPreferences.
+ *
+ * @return The key.
+ */
+ public String getKey() {
+ return mKey;
+ }
+
+ /**
+ * Checks whether the key is present, and if it isn't throws an
+ * exception. This should be called by subclasses that store preferences in
+ * the {@link SharedPreferences}.
+ */
+ void requireKey() {
+ if (mKey == null) {
+ throw new IllegalStateException("Preference does not have a key assigned.");
+ }
+
+ mRequiresKey = true;
+ }
+
+ /**
+ * Returns whether this {@link Preference} has a valid key.
+ *
+ * @return Whether the key exists and is not a blank string.
+ */
+ public boolean hasKey() {
+ return !TextUtils.isEmpty(mKey);
+ }
+
+ /**
+ * Returns whether this {@link Preference} is persistent. If it is persistent, it stores its value(s) into
+ * the persistent {@link SharedPreferences} storage.
+ *
+ * @return Whether this is persistent.
+ */
+ public boolean isPersistent() {
+ return mPersistent;
+ }
+
+ /**
+ * Convenience method of whether at the given time this method is called,
+ * the {@link Preference} should store/restore its value(s) into the
+ * {@link SharedPreferences}. This, at minimum, checks whether the
+ * {@link Preference} is persistent and it currently has a key. Before you
+ * save/restore from the {@link SharedPreferences}, check this first.
+ *
+ * @return Whether to persist the value.
+ */
+ protected boolean shouldPersist() {
+ return mPreferenceManager != null && isPersistent() && hasKey();
+ }
+
+ /**
+ * Sets whether this {@link Preference} is persistent. If it is persistent,
+ * it stores its value(s) into the persistent {@link SharedPreferences}
+ * storage.
+ *
+ * @param persistent Whether it should store its value(s) into the {@link SharedPreferences}.
+ */
+ public void setPersistent(boolean persistent) {
+ mPersistent = persistent;
+ }
+
+ /**
+ * Call this method after the user changes the preference, but before the
+ * internal state is set. This allows the client to ignore the user value.
+ *
+ * @param newValue The new value of the preference.
+ * @return Whether or not the user value should be set as the preference
+ * value (and persisted).
+ */
+ protected boolean callChangeListener(Object newValue) {
+ return mOnChangeListener == null ? true : mOnChangeListener.onPreferenceChange(this, newValue);
+ }
+
+ /**
+ * Sets the callback to be invoked when this preference is changed by the
+ * user (but before the internal state has been updated).
+ *
+ * @param onPreferenceChangeListener The callback to be invoked.
+ */
+ public void setOnPreferenceChangeListener(OnPreferenceChangeListener onPreferenceChangeListener) {
+ mOnChangeListener = onPreferenceChangeListener;
+ }
+
+ /**
+ * Gets the callback to be invoked when this preference is changed by the
+ * user (but before the internal state has been updated).
+ *
+ * @return The callback to be invoked.
+ */
+ public OnPreferenceChangeListener getOnPreferenceChangeListener() {
+ return mOnChangeListener;
+ }
+
+ /**
+ * Sets the callback to be invoked when this preference is clicked.
+ *
+ * @param onPreferenceClickListener The callback to be invoked.
+ */
+ public void setOnPreferenceClickListener(OnPreferenceClickListener onPreferenceClickListener) {
+ mOnClickListener = onPreferenceClickListener;
+ }
+
+ /**
+ * Gets the callback to be invoked when this preference is clicked.
+ *
+ * @return The callback to be invoked.
+ */
+ public OnPreferenceClickListener getOnPreferenceClickListener() {
+ return mOnClickListener;
+ }
+
+ /**
+ * Called when a click should be performed.
+ *
+ * @param preferenceScreen Optional {@link PreferenceScreen} whose hierarchy click
+ * listener should be called in the proper order (between other
+ * processing).
+ */
+ void performClick(PreferenceScreen preferenceScreen) {
+
+ if (!isEnabled()) {
+ return;
+ }
+
+ onClick();
+
+ if (mOnClickListener != null && mOnClickListener.onPreferenceClick(this)) {
+ return;
+ }
+
+ PreferenceManager preferenceManager = getPreferenceManager();
+ if (preferenceManager != null) {
+ PreferenceManager.OnPreferenceTreeClickListener listener = preferenceManager
+ .getOnPreferenceTreeClickListener();
+ if (preferenceScreen != null && listener != null
+ && listener.onPreferenceTreeClick(preferenceScreen, this)) {
+ return;
+ }
+ }
+
+ if (mIntent != null) {
+ Context context = getContext();
+ context.startActivity(mIntent);
+ }
+ }
+
+ /**
+ * Returns the context of this preference. Each preference in a preference hierarchy can be
+ * from different context (for example, if multiple activities provide preferences into a single
+ * {@link PreferenceActivity}). This context will be used to save the preference valus.
+ *
+ * @return The context of this preference.
+ */
+ public Context getContext() {
+ return mContext;
+ }
+
+ /**
+ * Returns the {@link SharedPreferences} where this preference can read its
+ * value(s). Usually, it's easier to use one of the helper read methods:
+ * {@link #getPersistedBoolean(boolean)}, {@link #getPersistedFloat(float)},
+ * {@link #getPersistedInt(int)}, {@link #getPersistedLong(long)},
+ * {@link #getPersistedString(String)}. To save values, see
+ * {@link #getEditor()}.
+ * <p>
+ * In some cases, writes to the {@link #getEditor()} will not be committed
+ * right away and hence not show up in the returned
+ * {@link SharedPreferences}, this is intended behavior to improve
+ * performance.
+ *
+ * @return The {@link SharedPreferences} where this preference reads its
+ * value(s), or null if it isn't attached to a preference hierarchy.
+ * @see #getEditor()
+ */
+ public SharedPreferences getSharedPreferences() {
+ if (mPreferenceManager == null) {
+ return null;
+ }
+
+ return mPreferenceManager.getSharedPreferences();
+ }
+
+ /**
+ * Returns an {@link SharedPreferences.Editor} where this preference can
+ * save its value(s). Usually it's easier to use one of the helper save
+ * methods: {@link #persistBoolean(boolean)}, {@link #persistFloat(float)},
+ * {@link #persistInt(int)}, {@link #persistLong(long)},
+ * {@link #persistString(String)}. To read values, see
+ * {@link #getSharedPreferences()}. If {@link #shouldCommit()} returns
+ * true, it is this Preference's responsibility to commit.
+ * <p>
+ * In some cases, writes to this will not be committed right away and hence
+ * not show up in the shared preferences, this is intended behavior to
+ * improve performance.
+ *
+ * @return A {@link SharedPreferences.Editor} where this preference saves
+ * its value(s), or null if it isn't attached to a preference
+ * hierarchy.
+ * @see #shouldCommit()
+ * @see #getSharedPreferences()
+ */
+ public SharedPreferences.Editor getEditor() {
+ if (mPreferenceManager == null) {
+ return null;
+ }
+
+ return mPreferenceManager.getEditor();
+ }
+
+ /**
+ * Returns whether the {@link Preference} should commit its saved value(s) in
+ * {@link #getEditor()}. This may return false in situations where batch
+ * committing is being done (by the manager) to improve performance.
+ *
+ * @return Whether the Preference should commit its saved value(s).
+ * @see #getEditor()
+ */
+ public boolean shouldCommit() {
+ if (mPreferenceManager == null) {
+ return false;
+ }
+
+ return mPreferenceManager.shouldCommit();
+ }
+
+ /**
+ * Compares preferences based on order (if set), otherwise alphabetically on title.
+ * <p>
+ * {@inheritDoc}
+ */
+ public int compareTo(Preference another) {
+ if (mOrder != DEFAULT_ORDER
+ || (mOrder == DEFAULT_ORDER && another.mOrder != DEFAULT_ORDER)) {
+ // Do order comparison
+ return mOrder - another.mOrder;
+ } else if (mTitle == null) {
+ return 1;
+ } else if (another.mTitle == null) {
+ return -1;
+ } else {
+ // Do name comparison
+ return CharSequences.compareToIgnoreCase(mTitle, another.mTitle);
+ }
+ }
+
+ /**
+ * Sets the internal change listener.
+ *
+ * @param listener The listener.
+ * @see #notifyChanged()
+ */
+ final void setOnPreferenceChangeInternalListener(OnPreferenceChangeInternalListener listener) {
+ mListener = listener;
+ }
+
+ /**
+ * Should be called when the data of this {@link Preference} has changed.
+ */
+ protected void notifyChanged() {
+ if (mListener != null) {
+ mListener.onPreferenceChange(this);
+ }
+ }
+
+ /**
+ * Should be called this is a group a {@link Preference} has been
+ * added/removed from this group, or the ordering should be
+ * re-evaluated.
+ */
+ protected void notifyHierarchyChanged() {
+ if (mListener != null) {
+ mListener.onPreferenceHierarchyChange(this);
+ }
+ }
+
+ /**
+ * Gets the {@link PreferenceManager} that manages this preference's tree.
+ *
+ * @return The {@link PreferenceManager}.
+ */
+ public PreferenceManager getPreferenceManager() {
+ return mPreferenceManager;
+ }
+
+ /**
+ * Called when this preference has been attached to a preference hierarchy.
+ * Make sure to call the super implementation.
+ *
+ * @param preferenceManager The preference manager of the hierarchy.
+ */
+ protected void onAttachedToHierarchy(PreferenceManager preferenceManager) {
+ mPreferenceManager = preferenceManager;
+
+ mId = preferenceManager.getNextId();
+
+ dispatchSetInitialValue();
+ }
+
+ /**
+ * Called when the preference hierarchy has been attached to the
+ * {@link PreferenceActivity}. This can also be called when this
+ * {@link Preference} has been attached to a group that was already attached
+ * to the {@link PreferenceActivity}.
+ */
+ protected void onAttachedToActivity() {
+ // At this point, the hierarchy that this preference is in is connected
+ // with all other preferences.
+ registerDependency();
+ }
+
+ private void registerDependency() {
+
+ if (TextUtils.isEmpty(mDependencyKey)) return;
+
+ Preference preference = findPreferenceInHierarchy(mDependencyKey);
+ if (preference != null) {
+ preference.registerDependent(this);
+ } else {
+ throw new IllegalStateException("Dependency \"" + mDependencyKey
+ + "\" not found for preference \"" + mKey + "\" (title: \"" + mTitle + "\"");
+ }
+ }
+
+ private void unregisterDependency() {
+ if (mDependencyKey != null) {
+ final Preference oldDependency = findPreferenceInHierarchy(mDependencyKey);
+ if (oldDependency != null) {
+ oldDependency.unregisterDependent(this);
+ }
+ }
+ }
+
+ /**
+ * Find a Preference in this hierarchy (the whole thing,
+ * even above/below your {@link PreferenceScreen} screen break) with the given
+ * key.
+ * <p>
+ * This only functions after we have been attached to a hierarchy.
+ *
+ * @param key The key of the {@link Preference} to find.
+ * @return The {@link Preference} object of a preference
+ * with the given key.
+ */
+ protected Preference findPreferenceInHierarchy(String key) {
+ if (TextUtils.isEmpty(key) || mPreferenceManager == null) {
+ return null;
+ }
+
+ return mPreferenceManager.findPreference(key);
+ }
+
+ /**
+ * Adds a dependent Preference on this preference so we can notify it.
+ * Usually, the dependent preference registers itself (it's good for it to
+ * know it depends on something), so please use
+ * {@link Preference#setDependency(String)} on the dependent preference.
+ *
+ * @param dependent The dependent Preference that will be enabled/disabled
+ * according to the state of this preference.
+ */
+ private void registerDependent(Preference dependent) {
+ if (mDependents == null) {
+ mDependents = new ArrayList<Preference>();
+ }
+
+ mDependents.add(dependent);
+
+ dependent.onDependencyChanged(this, shouldDisableDependents());
+ }
+
+ /**
+ * Removes a dependent Preference on this preference.
+ *
+ * @param dependent The dependent Preference that will be enabled/disabled
+ * according to the state of this preference.
+ * @return Returns the same Preference object, for chaining multiple calls
+ * into a single statement.
+ */
+ private void unregisterDependent(Preference dependent) {
+ if (mDependents != null) {
+ mDependents.remove(dependent);
+ }
+ }
+
+ /**
+ * Notifies any listening dependents of a change that affects the
+ * dependency.
+ *
+ * @param disableDependents Whether this {@link Preference} should disable
+ * its dependents.
+ */
+ public void notifyDependencyChange(boolean disableDependents) {
+ final List<Preference> dependents = mDependents;
+
+ if (dependents == null) {
+ return;
+ }
+
+ final int dependentsCount = dependents.size();
+ for (int i = 0; i < dependentsCount; i++) {
+ dependents.get(i).onDependencyChanged(this, disableDependents);
+ }
+ }
+
+ /**
+ * Called when the dependency changes.
+ *
+ * @param dependency The preference that this preference depends on.
+ * @param disableDependent Whether to disable this preference.
+ */
+ public void onDependencyChanged(Preference dependency, boolean disableDependent) {
+ setEnabled(!disableDependent);
+ }
+
+ /**
+ * Should return whether this preference's dependents should currently be
+ * disabled.
+ *
+ * @return True if the dependents should be disabled, otherwise false.
+ */
+ public boolean shouldDisableDependents() {
+ return !isEnabled();
+ }
+
+ /**
+ * Sets the key of a Preference that this Preference will depend on. If that
+ * Preference is not set or is off, this Preference will be disabled.
+ *
+ * @param dependencyKey The key of the Preference that this depends on.
+ */
+ public void setDependency(String dependencyKey) {
+ // Unregister the old dependency, if we had one
+ unregisterDependency();
+
+ // Register the new
+ mDependencyKey = dependencyKey;
+ registerDependency();
+ }
+
+ /**
+ * Returns the key of the dependency on this preference.
+ *
+ * @return The key of the dependency.
+ * @see #setDependency(String)
+ */
+ public String getDependency() {
+ return mDependencyKey;
+ }
+
+ /**
+ * Called when this Preference is being removed from the hierarchy. You
+ * should remove any references to this Preference that you know about. Make
+ * sure to call through to the superclass implementation.
+ */
+ protected void onPrepareForRemoval() {
+ unregisterDependency();
+ }
+
+ /**
+ * Sets the default value for the preference, which will be set either if
+ * persistence is off or persistence is on and the preference is not found
+ * in the persistent storage.
+ *
+ * @param defaultValue The default value.
+ */
+ public void setDefaultValue(Object defaultValue) {
+ mDefaultValue = defaultValue;
+ }
+
+ private void dispatchSetInitialValue() {
+ // By now, we know if we are persistent.
+ final boolean shouldPersist = shouldPersist();
+ if (!shouldPersist || !getSharedPreferences().contains(mKey)) {
+ if (mDefaultValue != null) {
+ onSetInitialValue(false, mDefaultValue);
+ }
+ } else {
+ onSetInitialValue(true, null);
+ }
+ }
+
+ /**
+ * Implement this to set the initial value of the Preference. If the
+ * restoreValue flag is true, you should restore the value from the shared
+ * preferences. If false, you should set (and possibly store to shared
+ * preferences if {@link #shouldPersist()}) to defaultValue.
+ * <p>
+ * This may not always be called. One example is if it should not persist
+ * but there is no default value given.
+ *
+ * @param restorePersistedValue Whether to restore the persisted value
+ * (true), or use the given default value (false).
+ * @param defaultValue The default value. Only use if restoreValue is false.
+ */
+ protected void onSetInitialValue(boolean restorePersistedValue, Object defaultValue) {
+ }
+
+ private void tryCommit(SharedPreferences.Editor editor) {
+ if (mPreferenceManager.shouldCommit()) {
+ editor.commit();
+ }
+ }
+
+ /**
+ * Attempts to persist a String to the SharedPreferences.
+ * <p>
+ * This will check if the Preference is persistent, get an editor from
+ * the preference manager, put the string, check if we should commit (and
+ * commit if so).
+ *
+ * @param value The value to persist.
+ * @return Whether the Preference is persistent. (This is not whether the
+ * value was persisted, since we may not necessarily commit if there
+ * will be a batch commit later.)
+ * @see #getPersistedString(String)
+ */
+ protected boolean persistString(String value) {
+ if (shouldPersist()) {
+ // Shouldn't store null
+ if (value == getPersistedString(null)) {
+ // It's already there, so the same as persisting
+ return true;
+ }
+
+ SharedPreferences.Editor editor = mPreferenceManager.getEditor();
+ editor.putString(mKey, value);
+ tryCommit(editor);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Attempts to get a persisted String from the SharedPreferences.
+ * <p>
+ * This will check if the Preference is persistent, get the shared
+ * preferences from the preference manager, get the value.
+ *
+ * @param defaultReturnValue The default value to return if either the
+ * Preference is not persistent or the Preference is not in the
+ * shared preferences.
+ * @return The value from the shared preferences or the default return
+ * value.
+ * @see #persistString(String)
+ */
+ protected String getPersistedString(String defaultReturnValue) {
+ if (!shouldPersist()) {
+ return defaultReturnValue;
+ }
+
+ return mPreferenceManager.getSharedPreferences().getString(mKey, defaultReturnValue);
+ }
+
+ /**
+ * Attempts to persist an int to the SharedPreferences.
+ *
+ * @param value The value to persist.
+ * @return Whether the Preference is persistent. (This is not whether the
+ * value was persisted, since we may not necessarily commit if there
+ * will be a batch commit later.)
+ * @see #persistString(String)
+ * @see #getPersistedInt(int)
+ */
+ protected boolean persistInt(int value) {
+ if (shouldPersist()) {
+ if (value == getPersistedInt(~value)) {
+ // It's already there, so the same as persisting
+ return true;
+ }
+
+ SharedPreferences.Editor editor = mPreferenceManager.getEditor();
+ editor.putInt(mKey, value);
+ tryCommit(editor);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Attempts to get a persisted int from the SharedPreferences.
+ *
+ * @param defaultReturnValue The default value to return if either the
+ * Preference is not persistent or the Preference is not in the
+ * shared preferences.
+ * @return The value from the shared preferences or the default return
+ * value.
+ * @see #getPersistedString(String)
+ * @see #persistInt(int)
+ */
+ protected int getPersistedInt(int defaultReturnValue) {
+ if (!shouldPersist()) {
+ return defaultReturnValue;
+ }
+
+ return mPreferenceManager.getSharedPreferences().getInt(mKey, defaultReturnValue);
+ }
+
+ /**
+ * Attempts to persist a float to the SharedPreferences.
+ *
+ * @param value The value to persist.
+ * @return Whether the Preference is persistent. (This is not whether the
+ * value was persisted, since we may not necessarily commit if there
+ * will be a batch commit later.)
+ * @see #persistString(String)
+ * @see #getPersistedFloat(float)
+ */
+ protected boolean persistFloat(float value) {
+ if (shouldPersist()) {
+ if (value == getPersistedFloat(Float.NaN)) {
+ // It's already there, so the same as persisting
+ return true;
+ }
+
+ SharedPreferences.Editor editor = mPreferenceManager.getEditor();
+ editor.putFloat(mKey, value);
+ tryCommit(editor);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Attempts to get a persisted float from the SharedPreferences.
+ *
+ * @param defaultReturnValue The default value to return if either the
+ * Preference is not persistent or the Preference is not in the
+ * shared preferences.
+ * @return The value from the shared preferences or the default return
+ * value.
+ * @see #getPersistedString(String)
+ * @see #persistFloat(float)
+ */
+ protected float getPersistedFloat(float defaultReturnValue) {
+ if (!shouldPersist()) {
+ return defaultReturnValue;
+ }
+
+ return mPreferenceManager.getSharedPreferences().getFloat(mKey, defaultReturnValue);
+ }
+
+ /**
+ * Attempts to persist a long to the SharedPreferences.
+ *
+ * @param value The value to persist.
+ * @return Whether the Preference is persistent. (This is not whether the
+ * value was persisted, since we may not necessarily commit if there
+ * will be a batch commit later.)
+ * @see #persistString(String)
+ * @see #getPersistedLong(long)
+ */
+ protected boolean persistLong(long value) {
+ if (shouldPersist()) {
+ if (value == getPersistedLong(~value)) {
+ // It's already there, so the same as persisting
+ return true;
+ }
+
+ SharedPreferences.Editor editor = mPreferenceManager.getEditor();
+ editor.putLong(mKey, value);
+ tryCommit(editor);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Attempts to get a persisted long from the SharedPreferences.
+ *
+ * @param defaultReturnValue The default value to return if either the
+ * Preference is not persistent or the Preference is not in the
+ * shared preferences.
+ * @return The value from the shared preferences or the default return
+ * value.
+ * @see #getPersistedString(String)
+ * @see #persistLong(long)
+ */
+ protected long getPersistedLong(long defaultReturnValue) {
+ if (!shouldPersist()) {
+ return defaultReturnValue;
+ }
+
+ return mPreferenceManager.getSharedPreferences().getLong(mKey, defaultReturnValue);
+ }
+
+ /**
+ * Attempts to persist a boolean to the SharedPreferences.
+ *
+ * @param value The value to persist.
+ * @return Whether the Preference is persistent. (This is not whether the
+ * value was persisted, since we may not necessarily commit if there
+ * will be a batch commit later.)
+ * @see #persistString(String)
+ * @see #getPersistedBoolean(boolean)
+ */
+ protected boolean persistBoolean(boolean value) {
+ if (shouldPersist()) {
+ if (value == getPersistedBoolean(!value)) {
+ // It's already there, so the same as persisting
+ return true;
+ }
+
+ SharedPreferences.Editor editor = mPreferenceManager.getEditor();
+ editor.putBoolean(mKey, value);
+ tryCommit(editor);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Attempts to get a persisted boolean from the SharedPreferences.
+ *
+ * @param defaultReturnValue The default value to return if either the
+ * Preference is not persistent or the Preference is not in the
+ * shared preferences.
+ * @return The value from the shared preferences or the default return
+ * value.
+ * @see #getPersistedString(String)
+ * @see #persistBoolean(boolean)
+ */
+ protected boolean getPersistedBoolean(boolean defaultReturnValue) {
+ if (!shouldPersist()) {
+ return defaultReturnValue;
+ }
+
+ return mPreferenceManager.getSharedPreferences().getBoolean(mKey, defaultReturnValue);
+ }
+
+ boolean hasSpecifiedLayout() {
+ return mHasSpecifiedLayout;
+ }
+
+ @Override
+ public String toString() {
+ return getFilterableStringBuilder().toString();
+ }
+
+ /**
+ * Returns the text that will be used to filter this preference depending on
+ * user input.
+ * <p>
+ * If overridding and calling through to the superclass, make sure to prepend
+ * your additions with a space.
+ *
+ * @return Text as a {@link StringBuilder} that will be used to filter this
+ * preference. By default, this is the title and summary
+ * (concatenated with a space).
+ */
+ StringBuilder getFilterableStringBuilder() {
+ StringBuilder sb = new StringBuilder();
+ CharSequence title = getTitle();
+ if (!TextUtils.isEmpty(title)) {
+ sb.append(title).append(' ');
+ }
+ CharSequence summary = getSummary();
+ if (!TextUtils.isEmpty(summary)) {
+ sb.append(summary).append(' ');
+ }
+ // Drop the last space
+ sb.setLength(sb.length() - 1);
+ return sb;
+ }
+
+ /**
+ * Store this preference hierarchy's frozen state into the given container.
+ *
+ * @param container The Bundle in which to save the preference's icicles.
+ *
+ * @see #restoreHierarchyState
+ * @see #dispatchSaveInstanceState
+ * @see #onSaveInstanceState
+ */
+ public void saveHierarchyState(Bundle container) {
+ dispatchSaveInstanceState(container);
+ }
+
+ /**
+ * Called by {@link #saveHierarchyState} to store the icicles for this preference and its children.
+ * May be overridden to modify how freezing happens to a preference's children; for example, some
+ * preferences may want to not store icicles for their children.
+ *
+ * @param container The Bundle in which to save the preference's icicles.
+ *
+ * @see #dispatchRestoreInstanceState
+ * @see #saveHierarchyState
+ * @see #onSaveInstanceState
+ */
+ void dispatchSaveInstanceState(Bundle container) {
+ if (hasKey()) {
+ mBaseMethodCalled = false;
+ Parcelable state = onSaveInstanceState();
+ if (!mBaseMethodCalled) {
+ throw new IllegalStateException(
+ "Derived class did not call super.onSaveInstanceState()");
+ }
+ if (state != null) {
+ container.putParcelable(mKey, state);
+ }
+ }
+ }
+
+ /**
+ * Hook allowing a preference to generate a representation of its internal
+ * state that can later be used to create a new instance with that same
+ * state. This state should only contain information that is not persistent
+ * or can be reconstructed later.
+ *
+ * @return Returns a Parcelable object containing the preference's current
+ * dynamic state, or null if there is nothing interesting to save.
+ * The default implementation returns null.
+ * @see #onRestoreInstanceState
+ * @see #saveHierarchyState
+ * @see #dispatchSaveInstanceState
+ */
+ protected Parcelable onSaveInstanceState() {
+ mBaseMethodCalled = true;
+ return BaseSavedState.EMPTY_STATE;
+ }
+
+ /**
+ * Restore this preference hierarchy's frozen state from the given container.
+ *
+ * @param container The Bundle which holds previously frozen icicles.
+ *
+ * @see #saveHierarchyState
+ * @see #dispatchRestoreInstanceState
+ * @see #onRestoreInstanceState
+ */
+ public void restoreHierarchyState(Bundle container) {
+ dispatchRestoreInstanceState(container);
+ }
+
+ /**
+ * Called by {@link #restoreHierarchyState} to retrieve the icicles for this
+ * preference and its children. May be overridden to modify how restoreing
+ * happens to a preference's children; for example, some preferences may
+ * want to not store icicles for their children.
+ *
+ * @param container The Bundle which holds previously frozen icicles.
+ * @see #dispatchSaveInstanceState
+ * @see #restoreHierarchyState
+ * @see #onRestoreInstanceState
+ */
+ void dispatchRestoreInstanceState(Bundle container) {
+ if (hasKey()) {
+ Parcelable state = container.getParcelable(mKey);
+ if (state != null) {
+ mBaseMethodCalled = false;
+ onRestoreInstanceState(state);
+ if (!mBaseMethodCalled) {
+ throw new IllegalStateException(
+ "Derived class did not call super.onRestoreInstanceState()");
+ }
+ }
+ }
+ }
+
+ /**
+ * Hook allowing a preference to re-apply a representation of its internal
+ * state that had previously been generated by {@link #onSaveInstanceState}.
+ * This function will never be called with a null icicle.
+ *
+ * @param state The frozen state that had previously been returned by
+ * {@link #onSaveInstanceState}.
+ * @see #onSaveInstanceState
+ * @see #restoreHierarchyState
+ * @see #dispatchRestoreInstanceState
+ */
+ protected void onRestoreInstanceState(Parcelable state) {
+ mBaseMethodCalled = true;
+ if (state != BaseSavedState.EMPTY_STATE && state != null) {
+ throw new IllegalArgumentException("Wrong state class -- expecting Preference State");
+ }
+ }
+
+ public static class BaseSavedState extends AbsSavedState {
+ public BaseSavedState(Parcel source) {
+ super(source);
+ }
+
+ public BaseSavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ public static final Parcelable.Creator<BaseSavedState> CREATOR =
+ new Parcelable.Creator<BaseSavedState>() {
+ public BaseSavedState createFromParcel(Parcel in) {
+ return new BaseSavedState(in);
+ }
+
+ public BaseSavedState[] newArray(int size) {
+ return new BaseSavedState[size];
+ }
+ };
+ }
+
+}
diff --git a/core/java/android/preference/PreferenceActivity.java b/core/java/android/preference/PreferenceActivity.java
new file mode 100644
index 0000000..98144ca
--- /dev/null
+++ b/core/java/android/preference/PreferenceActivity.java
@@ -0,0 +1,284 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.preference;
+
+import android.app.Activity;
+import android.app.ListActivity;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.view.View;
+import android.view.Window;
+
+/**
+ * The {@link PreferenceActivity} activity shows a hierarchy of preferences as
+ * lists, possibly spanning multiple screens. These preferences will
+ * automatically save to {@link SharedPreferences} as the user interacts with
+ * them. To retrieve an instance of {@link SharedPreferences} that the
+ * preference hierarchy in this activity will use, call
+ * {@link PreferenceManager#getDefaultSharedPreferences(android.content.Context)}
+ * with a context in the same package as this activity.
+ * <p>
+ * Furthermore, the preferences shown will follow the visual style of system
+ * preferences. It is easy to create a hierarchy of preferences (that can be
+ * shown on multiple screens) via XML. For these reasons, it is recommended to
+ * use this activity (as a superclass) to deal with preferences in applications.
+ * <p>
+ * A {@link PreferenceScreen} object should be at the top of the preference
+ * hierarchy. Furthermore, subsequent {@link PreferenceScreen} in the hierarchy
+ * denote a screen break--that is the preferences contained within subsequent
+ * {@link PreferenceScreen} should be shown on another screen. The preference
+ * framework handles showing these other screens from the preference hierarchy.
+ * <p>
+ * The preference hierarchy can be formed in multiple ways:
+ * <li> From an XML file specifying the hierarchy
+ * <li> From different {@link Activity Activities} that each specify its own
+ * preferences in an XML file via {@link Activity} meta-data
+ * <li> From an object hierarchy rooted with {@link PreferenceScreen}
+ * <p>
+ * To inflate from XML, use the {@link #addPreferencesFromResource(int)}. The
+ * root element should be a {@link PreferenceScreen}. Subsequent elements can point
+ * to actual {@link Preference} subclasses. As mentioned above, subsequent
+ * {@link PreferenceScreen} in the hierarchy will result in the screen break.
+ * <p>
+ * To specify an {@link Intent} to query {@link Activity Activities} that each
+ * have preferences, use {@link #addPreferencesFromIntent}. Each
+ * {@link Activity} can specify meta-data in the manifest (via the key
+ * {@link PreferenceManager#METADATA_KEY_PREFERENCES}) that points to an XML
+ * resource. These XML resources will be inflated into a single preference
+ * hierarchy and shown by this activity.
+ * <p>
+ * To specify an object hierarchy rooted with {@link PreferenceScreen}, use
+ * {@link #setPreferenceScreen(PreferenceScreen)}.
+ * <p>
+ * As a convenience, this activity implements a click listener for any
+ * preference in the current hierarchy, see
+ * {@link #onPreferenceTreeClick(PreferenceScreen, Preference)}.
+ *
+ * @see Preference
+ * @see PreferenceScreen
+ */
+public abstract class PreferenceActivity extends ListActivity implements
+ PreferenceManager.OnPreferenceTreeClickListener {
+
+ private static final String PREFERENCES_TAG = "android:preferences";
+
+ private PreferenceManager mPreferenceManager;
+
+ /**
+ * The starting request code given out to preference framework.
+ */
+ private static final int FIRST_REQUEST_CODE = 100;
+
+ private static final int MSG_BIND_PREFERENCES = 0;
+ private Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+
+ case MSG_BIND_PREFERENCES:
+ bindPreferences();
+ break;
+ }
+ }
+ };
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
+
+ setContentView(com.android.internal.R.layout.preference_list_content);
+
+ mPreferenceManager = onCreatePreferenceManager();
+ getListView().setScrollBarStyle(View.SCROLLBARS_INSIDE_INSET);
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+
+ mPreferenceManager.dispatchActivityStop();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+
+ mPreferenceManager.dispatchActivityDestroy();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+
+ final PreferenceScreen preferenceScreen = getPreferenceScreen();
+ if (preferenceScreen != null) {
+ Bundle container = new Bundle();
+ preferenceScreen.saveHierarchyState(container);
+ outState.putBundle(PREFERENCES_TAG, container);
+ }
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Bundle state) {
+ super.onRestoreInstanceState(state);
+
+ Bundle container = state.getBundle(PREFERENCES_TAG);
+ if (container != null) {
+ final PreferenceScreen preferenceScreen = getPreferenceScreen();
+ if (preferenceScreen != null) {
+ preferenceScreen.restoreHierarchyState(container);
+ }
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ mPreferenceManager.dispatchActivityResult(requestCode, resultCode, data);
+ }
+
+ @Override
+ public void onContentChanged() {
+ super.onContentChanged();
+ postBindPreferences();
+ }
+
+ /**
+ * Posts a message to bind the preferences to the list view.
+ * <p>
+ * Binding late is preferred as any custom preference types created in
+ * {@link #onCreate(Bundle)} are able to have their views recycled.
+ */
+ private void postBindPreferences() {
+ if (mHandler.hasMessages(MSG_BIND_PREFERENCES)) return;
+ mHandler.obtainMessage(MSG_BIND_PREFERENCES).sendToTarget();
+ }
+
+ private void bindPreferences() {
+ final PreferenceScreen preferenceScreen = getPreferenceScreen();
+ if (preferenceScreen != null) {
+ preferenceScreen.bind(getListView());
+ }
+ }
+
+ /**
+ * Creates the {@link PreferenceManager}.
+ *
+ * @return The {@link PreferenceManager} used by this activity.
+ */
+ private PreferenceManager onCreatePreferenceManager() {
+ PreferenceManager preferenceManager = new PreferenceManager(this, FIRST_REQUEST_CODE);
+ preferenceManager.setOnPreferenceTreeClickListener(this);
+ return preferenceManager;
+ }
+
+ /**
+ * Returns the {@link PreferenceManager} used by this activity.
+ * @return The {@link PreferenceManager}.
+ */
+ public PreferenceManager getPreferenceManager() {
+ return mPreferenceManager;
+ }
+
+ private void requirePreferenceManager() {
+ if (mPreferenceManager == null) {
+ throw new RuntimeException("This should be called after super.onCreate.");
+ }
+ }
+
+ /**
+ * Sets the root of the preference hierarchy that this activity is showing.
+ *
+ * @param preferenceScreen The root {@link PreferenceScreen} of the preference hierarchy.
+ */
+ public void setPreferenceScreen(PreferenceScreen preferenceScreen) {
+ if (mPreferenceManager.setPreferences(preferenceScreen) && preferenceScreen != null) {
+ postBindPreferences();
+ }
+ }
+
+ /**
+ * Gets the root of the preference hierarchy that this activity is showing.
+ *
+ * @return The {@link PreferenceScreen} that is the root of the preference
+ * hierarchy.
+ */
+ public PreferenceScreen getPreferenceScreen() {
+ return mPreferenceManager.getPreferenceScreen();
+ }
+
+ /**
+ * Adds preferences from activities that match the given {@link Intent}.
+ *
+ * @param intent The {@link Intent} to query activities.
+ */
+ public void addPreferencesFromIntent(Intent intent) {
+ requirePreferenceManager();
+
+ setPreferenceScreen(mPreferenceManager.inflateFromIntent(intent, getPreferenceScreen()));
+ }
+
+ /**
+ * Inflates the given XML resource and adds the preference hierarchy to the current
+ * preference hierarchy.
+ *
+ * @param preferencesResId The XML resource ID to inflate.
+ */
+ public void addPreferencesFromResource(int preferencesResId) {
+ requirePreferenceManager();
+
+ setPreferenceScreen(mPreferenceManager.inflateFromResource(this, preferencesResId,
+ getPreferenceScreen()));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) {
+ return false;
+ }
+
+ /**
+ * Finds a {@link Preference} based on its key.
+ *
+ * @param key The key of the preference to retrieve.
+ * @return The {@link Preference} with the key, or null.
+ * @see PreferenceGroup#findPreference(CharSequence)
+ */
+ public Preference findPreference(CharSequence key) {
+
+ if (mPreferenceManager == null) {
+ return null;
+ }
+
+ return mPreferenceManager.findPreference(key);
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ if (mPreferenceManager != null) {
+ mPreferenceManager.dispatchNewIntent(intent);
+ }
+ }
+
+}
diff --git a/core/java/android/preference/PreferenceCategory.java b/core/java/android/preference/PreferenceCategory.java
new file mode 100644
index 0000000..a1b6f09
--- /dev/null
+++ b/core/java/android/preference/PreferenceCategory.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.preference;
+
+import java.util.Map;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+/**
+ * The {@link PreferenceCategory} class is used to group {@link Preference}s
+ * and provide a disabled title above the group.
+ */
+public class PreferenceCategory extends PreferenceGroup {
+ private static final String TAG = "PreferenceCategory";
+
+ public PreferenceCategory(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public PreferenceCategory(Context context, AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.preferenceCategoryStyle);
+ }
+
+ public PreferenceCategory(Context context) {
+ this(context, null);
+ }
+
+ @Override
+ protected boolean onPrepareAddPreference(Preference preference) {
+ if (preference instanceof PreferenceCategory) {
+ throw new IllegalArgumentException(
+ "Cannot add a " + TAG + " directly to a " + TAG);
+ }
+
+ return super.onPrepareAddPreference(preference);
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return false;
+ }
+
+}
diff --git a/core/java/android/preference/PreferenceGroup.java b/core/java/android/preference/PreferenceGroup.java
new file mode 100644
index 0000000..55b3753
--- /dev/null
+++ b/core/java/android/preference/PreferenceGroup.java
@@ -0,0 +1,320 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.preference;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+
+/**
+ * The {@link PreferenceGroup} class is a container for multiple
+ * {@link Preference}s. It is a base class for {@link Preference} that are
+ * parents, such as {@link PreferenceCategory} and {@link PreferenceScreen}.
+ *
+ * @attr ref android.R.styleable#PreferenceGroup_orderingFromXml
+ */
+public abstract class PreferenceGroup extends Preference implements GenericInflater.Parent<Preference> {
+ /**
+ * The container for child {@link Preference}s. This is sorted based on the
+ * ordering, please use {@link #addPreference(Preference)} instead of adding
+ * to this directly.
+ */
+ private List<Preference> mPreferenceList;
+
+ private boolean mOrderingAsAdded = true;
+
+ private int mCurrentPreferenceOrder = 0;
+
+ private boolean mAttachedToActivity = false;
+
+ public PreferenceGroup(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ mPreferenceList = new ArrayList<Preference>();
+
+ TypedArray a = context.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.PreferenceGroup, defStyle, 0);
+ mOrderingAsAdded = a.getBoolean(com.android.internal.R.styleable.PreferenceGroup_orderingFromXml,
+ mOrderingAsAdded);
+ a.recycle();
+ }
+
+ public PreferenceGroup(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ /**
+ * Whether to order the {@link Preference} children of this group as they
+ * are added. If this is false, the ordering will follow each Preference
+ * order and default to alphabetic for those without an order.
+ * <p>
+ * If this is called after preferences are added, they will not be
+ * re-ordered in the order they were added, hence call this method early on.
+ *
+ * @param orderingAsAdded Whether to order according to the order added.
+ * @see Preference#setOrder(int)
+ */
+ public void setOrderingAsAdded(boolean orderingAsAdded) {
+ mOrderingAsAdded = orderingAsAdded;
+ }
+
+ /**
+ * Whether this group is ordering preferences in the order they are added.
+ *
+ * @return Whether this group orders based on the order the children are added.
+ * @see #setOrderingAsAdded(boolean)
+ */
+ public boolean isOrderingAsAdded() {
+ return mOrderingAsAdded;
+ }
+
+ /**
+ * Called by the inflater to add an item to this group.
+ */
+ public void addItemFromInflater(Preference preference) {
+ addPreference(preference);
+ }
+
+ /**
+ * Returns the number of children {@link Preference}s.
+ * @return The number of preference children in this group.
+ */
+ public int getPreferenceCount() {
+ return mPreferenceList.size();
+ }
+
+ /**
+ * Returns the {@link Preference} at a particular index.
+ *
+ * @param index The index of the {@link Preference} to retrieve.
+ * @return The {@link Preference}.
+ */
+ public Preference getPreference(int index) {
+ return mPreferenceList.get(index);
+ }
+
+ /**
+ * Adds a {@link Preference} at the correct position based on the
+ * preference's order.
+ *
+ * @param preference The preference to add.
+ * @return Whether the preference is now in this group.
+ */
+ public boolean addPreference(Preference preference) {
+ if (mPreferenceList.contains(preference)) {
+ // Exists
+ return true;
+ }
+
+ if (preference.getOrder() == Preference.DEFAULT_ORDER) {
+ if (mOrderingAsAdded) {
+ preference.setOrder(mCurrentPreferenceOrder++);
+ }
+
+ if (preference instanceof PreferenceGroup) {
+ // TODO: fix (method is called tail recursively when inflating,
+ // so we won't end up properly passing this flag down to children
+ ((PreferenceGroup)preference).setOrderingAsAdded(mOrderingAsAdded);
+ }
+ }
+
+ int insertionIndex = Collections.binarySearch(mPreferenceList, preference);
+ if (insertionIndex < 0) {
+ insertionIndex = insertionIndex * -1 - 1;
+ }
+
+ if (!onPrepareAddPreference(preference)) {
+ return false;
+ }
+
+ synchronized(this) {
+ mPreferenceList.add(insertionIndex, preference);
+ }
+
+ preference.onAttachedToHierarchy(getPreferenceManager());
+
+ if (mAttachedToActivity) {
+ preference.onAttachedToActivity();
+ }
+
+ notifyHierarchyChanged();
+
+ return true;
+ }
+
+ /**
+ * Removes a {@link Preference} from this group.
+ *
+ * @param preference The preference to remove.
+ * @return Whether the preference was found and removed.
+ */
+ public boolean removePreference(Preference preference) {
+ final boolean returnValue = removePreferenceInt(preference);
+ notifyHierarchyChanged();
+ return returnValue;
+ }
+
+ private boolean removePreferenceInt(Preference preference) {
+ synchronized(this) {
+ preference.onPrepareForRemoval();
+ return mPreferenceList.remove(preference);
+ }
+ }
+
+ /**
+ * Removes all {@link Preference Preferences} from this group.
+ */
+ public void removeAll() {
+ synchronized(this) {
+ List<Preference> preferenceList = mPreferenceList;
+ for (int i = preferenceList.size() - 1; i >= 0; i--) {
+ removePreferenceInt(preferenceList.get(0));
+ }
+ }
+ notifyHierarchyChanged();
+ }
+
+ /**
+ * Prepares a {@link Preference} to be added to the group.
+ *
+ * @param preference The preference to add.
+ * @return Whether to allow adding the preference (true), or not (false).
+ */
+ protected boolean onPrepareAddPreference(Preference preference) {
+ if (!super.isEnabled()) {
+ preference.setEnabled(false);
+ }
+
+ return true;
+ }
+
+ /**
+ * Finds a {@link Preference} based on its key. If two {@link Preference}
+ * share the same key (not recommended), the first to appear will be
+ * returned (to retrieve the other preference with the same key, call this
+ * method on the first preference). If this preference has the key, it will
+ * not be returned.
+ * <p>
+ * This will recursively search for the preference into children that are
+ * also {@link PreferenceGroup PreferenceGroups}.
+ *
+ * @param key The key of the preference to retrieve.
+ * @return The {@link Preference} with the key, or null.
+ */
+ public Preference findPreference(CharSequence key) {
+ final int preferenceCount = getPreferenceCount();
+ for (int i = 0; i < preferenceCount; i++) {
+ final Preference preference = getPreference(i);
+ final String curKey = preference.getKey();
+
+ if (curKey != null && curKey.equals(key)) {
+ return preference;
+ }
+
+ if (preference instanceof PreferenceGroup) {
+ final Preference returnedPreference = ((PreferenceGroup)preference)
+ .findPreference(key);
+ if (returnedPreference != null) {
+ return returnedPreference;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Whether this preference group should be shown on the same screen as its
+ * contained preferences.
+ *
+ * @return True if the contained preferences should be shown on the same
+ * screen as this preference.
+ */
+ protected boolean isOnSameScreenAsChildren() {
+ return true;
+ }
+
+ @Override
+ protected void onAttachedToActivity() {
+ super.onAttachedToActivity();
+
+ // Mark as attached so if a preference is later added to this group, we
+ // can tell it we are already attached
+ mAttachedToActivity = true;
+
+ // Dispatch to all contained preferences
+ final int preferenceCount = getPreferenceCount();
+ for (int i = 0; i < preferenceCount; i++) {
+ getPreference(i).onAttachedToActivity();
+ }
+ }
+
+ @Override
+ protected void onPrepareForRemoval() {
+ super.onPrepareForRemoval();
+
+ // We won't be attached to the activity anymore
+ mAttachedToActivity = false;
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ super.setEnabled(enabled);
+
+ // Dispatch to all contained preferences
+ final int preferenceCount = getPreferenceCount();
+ for (int i = 0; i < preferenceCount; i++) {
+ getPreference(i).setEnabled(enabled);
+ }
+ }
+
+ void sortPreferences() {
+ synchronized (this) {
+ Collections.sort(mPreferenceList);
+ }
+ }
+
+ @Override
+ protected void dispatchSaveInstanceState(Bundle container) {
+ super.dispatchSaveInstanceState(container);
+
+ // Dispatch to all contained preferences
+ final int preferenceCount = getPreferenceCount();
+ for (int i = 0; i < preferenceCount; i++) {
+ getPreference(i).dispatchSaveInstanceState(container);
+ }
+ }
+
+ @Override
+ protected void dispatchRestoreInstanceState(Bundle container) {
+ super.dispatchRestoreInstanceState(container);
+
+ // Dispatch to all contained preferences
+ final int preferenceCount = getPreferenceCount();
+ for (int i = 0; i < preferenceCount; i++) {
+ getPreference(i).dispatchRestoreInstanceState(container);
+ }
+ }
+
+}
diff --git a/core/java/android/preference/PreferenceGroupAdapter.java b/core/java/android/preference/PreferenceGroupAdapter.java
new file mode 100644
index 0000000..e2a3157
--- /dev/null
+++ b/core/java/android/preference/PreferenceGroupAdapter.java
@@ -0,0 +1,246 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.preference;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import android.os.Handler;
+import android.preference.Preference.OnPreferenceChangeInternalListener;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Adapter;
+import android.widget.BaseAdapter;
+import android.widget.ListView;
+
+/**
+ * An adapter that returns the {@link Preference} contained in this group.
+ * In most cases, this adapter should be the base class for any custom
+ * adapters from {@link Preference#getAdapter()}.
+ * <p>
+ * This adapter obeys the
+ * {@link Preference}'s adapter rule (the
+ * {@link Adapter#getView(int, View, ViewGroup)} should be used instead of
+ * {@link Preference#getView(ViewGroup)} if a {@link Preference} has an
+ * adapter via {@link Preference#getAdapter()}).
+ * <p>
+ * This adapter also propagates data change/invalidated notifications upward.
+ * <p>
+ * This adapter does not include this {@link PreferenceGroup} in the returned
+ * adapter, use {@link PreferenceCategoryAdapter} instead.
+ *
+ * @see PreferenceCategoryAdapter
+ */
+class PreferenceGroupAdapter extends BaseAdapter implements OnPreferenceChangeInternalListener {
+
+ private static final String TAG = "PreferenceGroupAdapter";
+
+ /**
+ * The group that we are providing data from.
+ */
+ private PreferenceGroup mPreferenceGroup;
+
+ /**
+ * Maps a position into this adapter -> {@link Preference}. These
+ * {@link Preference}s don't have to be direct children of this
+ * {@link PreferenceGroup}, they can be grand children or younger)
+ */
+ private List<Preference> mPreferenceList;
+
+ /**
+ * List of unique Preference and its subclasses' names. This is used to find
+ * out how many types of views this adapter can return. Once the count is
+ * returned, this cannot be modified (since the ListView only checks the
+ * count once--when the adapter is being set). We will not recycle views for
+ * Preference subclasses seen after the count has been returned.
+ */
+ private List<String> mPreferenceClassNames;
+
+ /**
+ * Blocks the mPreferenceClassNames from being changed anymore.
+ */
+ private boolean mHasReturnedViewTypeCount = false;
+
+ private volatile boolean mIsSyncing = false;
+
+ private Handler mHandler = new Handler();
+
+ private Runnable mSyncRunnable = new Runnable() {
+ public void run() {
+ syncMyPreferences();
+ }
+ };
+
+ public PreferenceGroupAdapter(PreferenceGroup preferenceGroup) {
+ mPreferenceGroup = preferenceGroup;
+ mPreferenceList = new ArrayList<Preference>();
+ mPreferenceClassNames = new ArrayList<String>();
+
+ syncMyPreferences();
+ }
+
+ private void syncMyPreferences() {
+ synchronized(this) {
+ if (mIsSyncing) {
+ return;
+ }
+
+ mIsSyncing = true;
+ }
+
+ List<Preference> newPreferenceList = new ArrayList<Preference>(mPreferenceList.size());
+ flattenPreferenceGroup(newPreferenceList, mPreferenceGroup);
+ mPreferenceList = newPreferenceList;
+
+ notifyDataSetChanged();
+
+ synchronized(this) {
+ mIsSyncing = false;
+ notifyAll();
+ }
+ }
+
+ private void flattenPreferenceGroup(List<Preference> preferences, PreferenceGroup group) {
+ // TODO: shouldn't always?
+ group.sortPreferences();
+
+ final int groupSize = group.getPreferenceCount();
+ for (int i = 0; i < groupSize; i++) {
+ final Preference preference = group.getPreference(i);
+
+ preferences.add(preference);
+
+ if (!mHasReturnedViewTypeCount) {
+ addPreferenceClassName(preference);
+ }
+
+ if (preference instanceof PreferenceGroup) {
+ final PreferenceGroup preferenceAsGroup = (PreferenceGroup) preference;
+ if (preferenceAsGroup.isOnSameScreenAsChildren()) {
+ flattenPreferenceGroup(preferences, preferenceAsGroup);
+ preference.setOnPreferenceChangeInternalListener(this);
+ }
+ } else {
+ preference.setOnPreferenceChangeInternalListener(this);
+ }
+ }
+ }
+
+ private void addPreferenceClassName(Preference preference) {
+ final String name = preference.getClass().getName();
+ int insertPos = Collections.binarySearch(mPreferenceClassNames, name);
+
+ // Only insert if it doesn't exist (when it is negative).
+ if (insertPos < 0) {
+ // Convert to insert index
+ insertPos = insertPos * -1 - 1;
+ mPreferenceClassNames.add(insertPos, name);
+ }
+ }
+
+ public int getCount() {
+ return mPreferenceList.size();
+ }
+
+ public Preference getItem(int position) {
+ if (position < 0 || position >= getCount()) return null;
+ return mPreferenceList.get(position);
+ }
+
+ public long getItemId(int position) {
+ if (position < 0 || position >= getCount()) return ListView.INVALID_ROW_ID;
+ return this.getItem(position).getId();
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ final Preference preference = this.getItem(position);
+
+ if (preference.hasSpecifiedLayout()) {
+ // If the preference had specified a layout (as opposed to the
+ // default), don't use convert views.
+ convertView = null;
+ } else {
+ // TODO: better way of doing this
+ final String name = preference.getClass().getName();
+ if (Collections.binarySearch(mPreferenceClassNames, name) < 0) {
+ convertView = null;
+ }
+ }
+
+ return preference.getView(convertView, parent);
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ if (position < 0 || position >= getCount()) return true;
+ return this.getItem(position).isSelectable();
+ }
+
+ @Override
+ public boolean areAllItemsEnabled() {
+ // There should always be a preference group, and these groups are always
+ // disabled
+ return false;
+ }
+
+ public void onPreferenceChange(Preference preference) {
+ notifyDataSetChanged();
+ }
+
+ public void onPreferenceHierarchyChange(Preference preference) {
+ mHandler.removeCallbacks(mSyncRunnable);
+ mHandler.post(mSyncRunnable);
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ if (!mHasReturnedViewTypeCount) {
+ mHasReturnedViewTypeCount = true;
+ }
+
+ final Preference preference = this.getItem(position);
+ if (preference.hasSpecifiedLayout()) {
+ return IGNORE_ITEM_VIEW_TYPE;
+ }
+
+ final String name = preference.getClass().getName();
+ int viewType = Collections.binarySearch(mPreferenceClassNames, name);
+ if (viewType < 0) {
+ // This is a class that was seen after we returned the count, so
+ // don't recycle it.
+ return IGNORE_ITEM_VIEW_TYPE;
+ } else {
+ return viewType;
+ }
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ if (!mHasReturnedViewTypeCount) {
+ mHasReturnedViewTypeCount = true;
+ }
+
+ return mPreferenceClassNames.size();
+ }
+
+}
diff --git a/core/java/android/preference/PreferenceInflater.java b/core/java/android/preference/PreferenceInflater.java
new file mode 100644
index 0000000..779e746
--- /dev/null
+++ b/core/java/android/preference/PreferenceInflater.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.preference;
+
+import java.io.IOException;
+import java.util.Map;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.app.AliasActivity;
+import android.content.Context;
+import android.content.Intent;
+import android.util.AttributeSet;
+import android.util.Log;
+
+/**
+ * The {@link PreferenceInflater} is used to inflate preference hierarchies from
+ * XML files.
+ * <p>
+ * Do not construct this directly, instead use
+ * {@link Context#getSystemService(String)} with
+ * {@link Context#PREFERENCE_INFLATER_SERVICE}.
+ */
+class PreferenceInflater extends GenericInflater<Preference, PreferenceGroup> {
+ private static final String TAG = "PreferenceInflater";
+ private static final String INTENT_TAG_NAME = "intent";
+
+ private PreferenceManager mPreferenceManager;
+
+ public PreferenceInflater(Context context, PreferenceManager preferenceManager) {
+ super(context);
+ init(preferenceManager);
+ }
+
+ PreferenceInflater(GenericInflater<Preference, PreferenceGroup> original, PreferenceManager preferenceManager, Context newContext) {
+ super(original, newContext);
+ init(preferenceManager);
+ }
+
+ @Override
+ public GenericInflater<Preference, PreferenceGroup> cloneInContext(Context newContext) {
+ return new PreferenceInflater(this, mPreferenceManager, newContext);
+ }
+
+ private void init(PreferenceManager preferenceManager) {
+ mPreferenceManager = preferenceManager;
+ setDefaultPackage("android.preference.");
+ }
+
+ @Override
+ protected boolean onCreateCustomFromTag(XmlPullParser parser, Preference parentPreference,
+ AttributeSet attrs) throws XmlPullParserException {
+ final String tag = parser.getName();
+
+ if (tag.equals(INTENT_TAG_NAME)) {
+ Intent intent = null;
+
+ try {
+ intent = Intent.parseIntent(getContext().getResources(), parser, attrs);
+ } catch (IOException e) {
+ Log.w(TAG, "Could not parse Intent.");
+ Log.w(TAG, e);
+ }
+
+ if (intent != null) {
+ parentPreference.setIntent(intent);
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ protected PreferenceGroup onMergeRoots(PreferenceGroup givenRoot, boolean attachToGivenRoot,
+ PreferenceGroup xmlRoot) {
+ // If we were given a Preferences, use it as the root (ignoring the root
+ // Preferences from the XML file).
+ if (givenRoot == null) {
+ xmlRoot.onAttachedToHierarchy(mPreferenceManager);
+ return xmlRoot;
+ } else {
+ return givenRoot;
+ }
+ }
+
+}
diff --git a/core/java/android/preference/PreferenceManager.java b/core/java/android/preference/PreferenceManager.java
new file mode 100644
index 0000000..9963544
--- /dev/null
+++ b/core/java/android/preference/PreferenceManager.java
@@ -0,0 +1,790 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.preference;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.XmlResourceParser;
+import android.os.Bundle;
+import android.util.Log;
+
+/**
+ * The {@link PreferenceManager} is used to help create preference hierarchies
+ * from activities or XML.
+ * <p>
+ * In most cases, clients should use
+ * {@link PreferenceActivity#addPreferencesFromIntent} or
+ * {@link PreferenceActivity#addPreferencesFromResource(int)}.
+ *
+ * @see PreferenceActivity
+ */
+public class PreferenceManager {
+
+ private static final String TAG = "PreferenceManager";
+
+ /**
+ * The Activity meta-data key for its XML preference hierarchy.
+ */
+ public static final String METADATA_KEY_PREFERENCES = "android.preference";
+
+ public static final String KEY_HAS_SET_DEFAULT_VALUES = "_has_set_default_values";
+
+ /**
+ * @see #getActivity()
+ */
+ private Activity mActivity;
+
+ /**
+ * The context to use. This should always be set.
+ *
+ * @see #mActivity
+ */
+ private Context mContext;
+
+ /**
+ * The counter for unique IDs.
+ */
+ private long mNextId = 0;
+
+ /**
+ * The counter for unique request codes.
+ */
+ private int mNextRequestCode;
+
+ /**
+ * Cached shared preferences.
+ */
+ private SharedPreferences mSharedPreferences;
+
+ /**
+ * If in no-commit mode, the shared editor to give out (which will be
+ * committed when exiting no-commit mode).
+ */
+ private SharedPreferences.Editor mEditor;
+
+ /**
+ * Blocks commits from happening on the shared editor. This is used when
+ * inflating the hierarchy. Do not set this directly, use {@link #setNoCommit(boolean)}
+ */
+ private boolean mNoCommit;
+
+ /**
+ * The SharedPreferences name that will be used for all {@link Preference}s
+ * managed by this instance.
+ */
+ private String mSharedPreferencesName;
+
+ /**
+ * The SharedPreferences mode that will be used for all {@link Preference}s
+ * managed by this instance.
+ */
+ private int mSharedPreferencesMode;
+
+ /**
+ * The {@link PreferenceScreen} at the root of the preference hierarchy.
+ */
+ private PreferenceScreen mPreferenceScreen;
+
+ /**
+ * List of activity result listeners.
+ */
+ private List<OnActivityResultListener> mActivityResultListeners;
+
+ /**
+ * List of activity stop listeners.
+ */
+ private List<OnActivityStopListener> mActivityStopListeners;
+
+ /**
+ * List of activity destroy listeners.
+ */
+ private List<OnActivityDestroyListener> mActivityDestroyListeners;
+
+ /**
+ * List of dialogs that should be dismissed when we receive onNewIntent in
+ * our PreferenceActivity.
+ */
+ private List<DialogInterface> mPreferencesScreens;
+
+ private OnPreferenceTreeClickListener mOnPreferenceTreeClickListener;
+
+ PreferenceManager(Activity activity, int firstRequestCode) {
+ mActivity = activity;
+ mNextRequestCode = firstRequestCode;
+
+ init(activity);
+ }
+
+ /**
+ * This constructor should ONLY be used when getting default values from
+ * an XML preference hierarchy.
+ * <p>
+ * The {@link PreferenceManager#PreferenceManager(Activity)}
+ * should be used ANY time a preference will be displayed, since some preference
+ * types need an Activity for managed queries.
+ */
+ private PreferenceManager(Context context) {
+ init(context);
+ }
+
+ private void init(Context context) {
+ mContext = context;
+
+ setSharedPreferencesName(getDefaultSharedPreferencesName(context));
+ }
+
+ /**
+ * Returns a list of {@link Activity} (indirectly) that match a given
+ * {@link Intent}.
+ *
+ * @param queryIntent The Intent to match.
+ * @return The list of {@link ResolveInfo} that point to the matched
+ * activities.
+ */
+ private List<ResolveInfo> queryIntentActivities(Intent queryIntent) {
+ return mContext.getPackageManager().queryIntentActivities(queryIntent,
+ PackageManager.GET_META_DATA);
+ }
+
+ /**
+ * Inflates a preference hierarchy from the preference hierarchies of
+ * {@link Activity Activities} that match the given {@link Intent}. An
+ * {@link Activity} defines its preference hierarchy with meta-data using
+ * the {@link #METADATA_KEY_PREFERENCES} key.
+ * <p>
+ * If a preference hierarchy is given, the new preference hierarchies will
+ * be merged in.
+ *
+ * @param queryIntent The intent to match activities.
+ * @param rootPreferences Optional existing hierarchy to merge the new
+ * hierarchies into.
+ * @return The root hierarchy (if one was not provided, the new hierarchy's
+ * root).
+ */
+ PreferenceScreen inflateFromIntent(Intent queryIntent, PreferenceScreen rootPreferences) {
+ final List<ResolveInfo> activities = queryIntentActivities(queryIntent);
+ final HashSet<String> inflatedRes = new HashSet<String>();
+
+ for (int i = activities.size() - 1; i >= 0; i--) {
+ final ActivityInfo activityInfo = activities.get(i).activityInfo;
+ final Bundle metaData = activityInfo.metaData;
+
+ if ((metaData == null) || !metaData.containsKey(METADATA_KEY_PREFERENCES)) {
+ continue;
+ }
+
+ // Need to concat the package with res ID since the same res ID
+ // can be re-used across contexts
+ final String uniqueResId = activityInfo.packageName + ":"
+ + activityInfo.metaData.getInt(METADATA_KEY_PREFERENCES);
+
+ if (!inflatedRes.contains(uniqueResId)) {
+ inflatedRes.add(uniqueResId);
+
+ final Context context;
+ try {
+ context = mContext.createPackageContext(activityInfo.packageName, 0);
+ } catch (NameNotFoundException e) {
+ Log.w(TAG, "Could not create context for " + activityInfo.packageName + ": "
+ + Log.getStackTraceString(e));
+ continue;
+ }
+
+ final PreferenceInflater inflater = new PreferenceInflater(context, this);
+ final XmlResourceParser parser = activityInfo.loadXmlMetaData(context
+ .getPackageManager(), METADATA_KEY_PREFERENCES);
+ rootPreferences = (PreferenceScreen) inflater
+ .inflate(parser, rootPreferences, true);
+ parser.close();
+ }
+ }
+
+ rootPreferences.onAttachedToHierarchy(this);
+
+ return rootPreferences;
+ }
+
+ /**
+ * Inflates a preference hierarchy from XML. If a preference hierarchy is
+ * given, the new preference hierarchies will be merged in.
+ *
+ * @param context The context of the resource.
+ * @param resId The resource ID of the XML to inflate.
+ * @param rootPreferences Optional existing hierarchy to merge the new
+ * hierarchies into.
+ * @return The root hierarchy (if one was not provided, the new hierarchy's
+ * root).
+ */
+ PreferenceScreen inflateFromResource(Context context, int resId,
+ PreferenceScreen rootPreferences) {
+ // Block commits
+ setNoCommit(true);
+
+ final PreferenceInflater inflater = new PreferenceInflater(context, this);
+ rootPreferences = (PreferenceScreen) inflater.inflate(resId, rootPreferences, true);
+ rootPreferences.onAttachedToHierarchy(this);
+
+ // Unblock commits
+ setNoCommit(false);
+
+ return rootPreferences;
+ }
+
+ public PreferenceScreen createPreferenceScreen(Context context) {
+ final PreferenceScreen preferenceScreen = new PreferenceScreen(context, null);
+ preferenceScreen.onAttachedToHierarchy(this);
+ return preferenceScreen;
+ }
+
+ /**
+ * Called by a preference to get a unique ID in its hierarchy.
+ *
+ * @return A unique ID.
+ */
+ long getNextId() {
+ synchronized (this) {
+ return mNextId++;
+ }
+ }
+
+ /**
+ * Returns the current name of the SharedPreferences file that preferences managed by
+ * this will use.
+ *
+ * @return The name that can be passed to {@link Context#getSharedPreferences(String, int)}.
+ * @see Context#getSharedPreferences(String, int)
+ */
+ public String getSharedPreferencesName() {
+ return mSharedPreferencesName;
+ }
+
+ /**
+ * Sets the name of the SharedPreferences file that preferences managed by this
+ * will use.
+ *
+ * @param sharedPreferencesName The name of the SharedPreferences file.
+ * @see Context#getSharedPreferences(String, int)
+ */
+ public void setSharedPreferencesName(String sharedPreferencesName) {
+ mSharedPreferencesName = sharedPreferencesName;
+ mSharedPreferences = null;
+ }
+
+ /**
+ * Returns the current mode of the SharedPreferences file that preferences managed by
+ * this will use.
+ *
+ * @return The mode that can be passed to {@link Context#getSharedPreferences(String, int)}.
+ * @see Context#getSharedPreferences(String, int)
+ */
+ public int getSharedPreferencesMode() {
+ return mSharedPreferencesMode;
+ }
+
+ /**
+ * Sets the mode of the SharedPreferences file that preferences managed by this
+ * will use.
+ *
+ * @param sharedPreferencesMode The mode of the SharedPreferences file.
+ * @see Context#getSharedPreferences(String, int)
+ */
+ public void setSharedPreferencesMode(int sharedPreferencesMode) {
+ mSharedPreferencesMode = sharedPreferencesMode;
+ mSharedPreferences = null;
+ }
+
+ /**
+ * Gets a SharedPreferences instance that preferences managed by this will
+ * use.
+ *
+ * @return A SharedPreferences instance pointing to the file that contains
+ * the values of preferences that are managed by this.
+ */
+ public SharedPreferences getSharedPreferences() {
+ if (mSharedPreferences == null) {
+ mSharedPreferences = mContext.getSharedPreferences(mSharedPreferencesName,
+ mSharedPreferencesMode);
+ }
+
+ return mSharedPreferences;
+ }
+
+ /**
+ * Gets a SharedPreferences instance that points to the default file that is
+ * used by the preference framework in the given context.
+ *
+ * @param context The context of the preferences whose values are wanted.
+ * @return A SharedPreferences instance that can be used to retrieve and
+ * listen to values of the preferences.
+ */
+ public static SharedPreferences getDefaultSharedPreferences(Context context) {
+ return context.getSharedPreferences(getDefaultSharedPreferencesName(context),
+ getDefaultSharedPreferencesMode());
+ }
+
+ private static String getDefaultSharedPreferencesName(Context context) {
+ return context.getPackageName() + "_preferences";
+ }
+
+ private static int getDefaultSharedPreferencesMode() {
+ return Context.MODE_PRIVATE;
+ }
+
+ /**
+ * Returns the root of the preference hierarchy managed by this class.
+ *
+ * @return The {@link PreferenceScreen} object that is at the root of the hierarchy.
+ */
+ PreferenceScreen getPreferenceScreen() {
+ return mPreferenceScreen;
+ }
+
+ /**
+ * Sets the root of the preference hierarchy.
+ *
+ * @param preferenceScreen The root {@link PreferenceScreen} of the preference hierarchy.
+ * @return Whether the {@link PreferenceScreen} given is different than the previous.
+ */
+ boolean setPreferences(PreferenceScreen preferenceScreen) {
+ if (preferenceScreen != mPreferenceScreen) {
+ mPreferenceScreen = preferenceScreen;
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Finds a {@link Preference} based on its key.
+ *
+ * @param key The key of the preference to retrieve.
+ * @return The {@link Preference} with the key, or null.
+ * @see PreferenceGroup#findPreference(CharSequence)
+ */
+ public Preference findPreference(CharSequence key) {
+ if (mPreferenceScreen == null) {
+ return null;
+ }
+
+ return mPreferenceScreen.findPreference(key);
+ }
+
+ /**
+ * Sets the default values from a preference hierarchy in XML. This should
+ * be called by the application's main activity.
+ * <p>
+ * If {@code readAgain} is false, this will only set the default values if this
+ * method has never been called in the past (or the
+ * {@link #KEY_HAS_SET_DEFAULT_VALUES} in the default value shared
+ * preferences file is false). To attempt to set the default values again
+ * bypassing this check, set {@code readAgain} to true.
+ *
+ * @param context The context of the shared preferences.
+ * @param resId The resource ID of the preference hierarchy XML file.
+ * @param readAgain Whether to re-read the default values.
+ * <p>
+ * Note: this will NOT reset preferences back to their default
+ * values. For that functionality, use
+ * {@link PreferenceManager#getDefaultSharedPreferences(Context)}
+ * and clear it followed by a call to this method with this
+ * parameter set to true.
+ */
+ public static void setDefaultValues(Context context, int resId, boolean readAgain) {
+
+ // Use the default shared preferences name and mode
+ setDefaultValues(context, getDefaultSharedPreferencesName(context),
+ getDefaultSharedPreferencesMode(), resId, readAgain);
+ }
+
+ /**
+ * Similar to {@link #setDefaultValues(Context, int, boolean)} but allows
+ * the client to provide the filename and mode of the shared preferences
+ * file.
+ *
+ * @see #setDefaultValues(Context, int, boolean)
+ * @see #setSharedPreferencesName(String)
+ * @see #setSharedPreferencesMode(int)
+ */
+ public static void setDefaultValues(Context context, String sharedPreferencesName,
+ int sharedPreferencesMode, int resId, boolean readAgain) {
+ final SharedPreferences defaultValueSp = context.getSharedPreferences(
+ KEY_HAS_SET_DEFAULT_VALUES, Context.MODE_PRIVATE);
+
+ if (readAgain || !defaultValueSp.getBoolean(KEY_HAS_SET_DEFAULT_VALUES, false)) {
+ final PreferenceManager pm = new PreferenceManager(context);
+ pm.setSharedPreferencesName(sharedPreferencesName);
+ pm.setSharedPreferencesMode(sharedPreferencesMode);
+ pm.inflateFromResource(context, resId, null);
+
+ defaultValueSp.edit().putBoolean(KEY_HAS_SET_DEFAULT_VALUES, true).commit();
+ }
+ }
+
+ /**
+ * Returns an editor to use when modifying the shared preferences.
+ * <p>
+ * Do NOT commit unless {@link #shouldCommit()} returns true.
+ *
+ * @return An editor to use to write to shared preferences.
+ * @see #shouldCommit()
+ */
+ SharedPreferences.Editor getEditor() {
+
+ if (mNoCommit) {
+ if (mEditor == null) {
+ mEditor = getSharedPreferences().edit();
+ }
+
+ return mEditor;
+ } else {
+ return getSharedPreferences().edit();
+ }
+ }
+
+ /**
+ * Whether it is the client's responsibility to commit on the
+ * {@link #getEditor()}. This will return false in cases where the writes
+ * should be batched, for example when inflating preferences from XML.
+ *
+ * @return Whether the client should commit.
+ */
+ boolean shouldCommit() {
+ return !mNoCommit;
+ }
+
+ private void setNoCommit(boolean noCommit) {
+ if (!noCommit && mEditor != null) {
+ mEditor.commit();
+ }
+
+ mNoCommit = noCommit;
+ }
+
+ /**
+ * Returns the activity that shows the preferences. This is useful for doing
+ * managed queries, but in most cases the use of {@link #getContext()} is
+ * preferred.
+ * <p>
+ * This will return null if this class was instantiated with a Context
+ * instead of Activity. For example, when setting the default values.
+ *
+ * @return The activity that shows the preferences.
+ * @see #mContext
+ */
+ Activity getActivity() {
+ return mActivity;
+ }
+
+ /**
+ * Returns the context. This is preferred over {@link #getActivity()} when
+ * possible.
+ *
+ * @return The context.
+ */
+ Context getContext() {
+ return mContext;
+ }
+
+ /**
+ * Registers a listener.
+ *
+ * @see OnActivityResultListener
+ */
+ void registerOnActivityResultListener(OnActivityResultListener listener) {
+ synchronized (this) {
+ if (mActivityResultListeners == null) {
+ mActivityResultListeners = new ArrayList<OnActivityResultListener>();
+ }
+
+ if (!mActivityResultListeners.contains(listener)) {
+ mActivityResultListeners.add(listener);
+ }
+ }
+ }
+
+ /**
+ * Unregisters a listener.
+ *
+ * @see OnActivityResultListener
+ */
+ void unregisterOnActivityResultListener(OnActivityResultListener listener) {
+ synchronized (this) {
+ if (mActivityResultListeners != null) {
+ mActivityResultListeners.remove(listener);
+ }
+ }
+ }
+
+ /**
+ * Called by the {@link PreferenceManager} to dispatch a subactivity result.
+ */
+ void dispatchActivityResult(int requestCode, int resultCode, Intent data) {
+ List<OnActivityResultListener> list;
+
+ synchronized (this) {
+ if (mActivityResultListeners == null) return;
+ list = new ArrayList<OnActivityResultListener>(mActivityResultListeners);
+ }
+
+ final int N = list.size();
+ for (int i = 0; i < N; i++) {
+ if (list.get(i).onActivityResult(requestCode, resultCode, data)) {
+ break;
+ }
+ }
+ }
+
+ /**
+ * Registers a listener.
+ *
+ * @see OnActivityStopListener
+ */
+ void registerOnActivityStopListener(OnActivityStopListener listener) {
+ synchronized (this) {
+ if (mActivityStopListeners == null) {
+ mActivityStopListeners = new ArrayList<OnActivityStopListener>();
+ }
+
+ if (!mActivityStopListeners.contains(listener)) {
+ mActivityStopListeners.add(listener);
+ }
+ }
+ }
+
+ /**
+ * Unregisters a listener.
+ *
+ * @see OnActivityStopListener
+ */
+ void unregisterOnActivityStopListener(OnActivityStopListener listener) {
+ synchronized (this) {
+ if (mActivityStopListeners != null) {
+ mActivityStopListeners.remove(listener);
+ }
+ }
+ }
+
+ /**
+ * Called by the {@link PreferenceManager} to dispatch the activity stop
+ * event.
+ */
+ void dispatchActivityStop() {
+ List<OnActivityStopListener> list;
+
+ synchronized (this) {
+ if (mActivityStopListeners == null) return;
+ list = new ArrayList<OnActivityStopListener>(mActivityStopListeners);
+ }
+
+ final int N = list.size();
+ for (int i = 0; i < N; i++) {
+ list.get(i).onActivityStop();
+ }
+ }
+
+ /**
+ * Registers a listener.
+ *
+ * @see OnActivityDestroyListener
+ */
+ void registerOnActivityDestroyListener(OnActivityDestroyListener listener) {
+ synchronized (this) {
+ if (mActivityDestroyListeners == null) {
+ mActivityDestroyListeners = new ArrayList<OnActivityDestroyListener>();
+ }
+
+ if (!mActivityDestroyListeners.contains(listener)) {
+ mActivityDestroyListeners.add(listener);
+ }
+ }
+ }
+
+ /**
+ * Unregisters a listener.
+ *
+ * @see OnActivityDestroyListener
+ */
+ void unregisterOnActivityDestroyListener(OnActivityDestroyListener listener) {
+ synchronized (this) {
+ if (mActivityDestroyListeners != null) {
+ mActivityDestroyListeners.remove(listener);
+ }
+ }
+ }
+
+ /**
+ * Called by the {@link PreferenceManager} to dispatch the activity destroy
+ * event.
+ */
+ void dispatchActivityDestroy() {
+ List<OnActivityDestroyListener> list;
+
+ synchronized (this) {
+ if (mActivityDestroyListeners == null) return;
+ list = new ArrayList<OnActivityDestroyListener>(mActivityDestroyListeners);
+ }
+
+ final int N = list.size();
+ for (int i = 0; i < N; i++) {
+ list.get(i).onActivityDestroy();
+ }
+ }
+
+ /**
+ * Returns a request code that is unique for the activity. Each subsequent
+ * call to this method should return another unique request code.
+ *
+ * @return A unique request code that will never be used by anyone other
+ * than the caller of this method.
+ */
+ int getNextRequestCode() {
+ synchronized (this) {
+ return mNextRequestCode++;
+ }
+ }
+
+ void addPreferencesScreen(DialogInterface screen) {
+ synchronized (this) {
+
+ if (mPreferencesScreens == null) {
+ mPreferencesScreens = new ArrayList<DialogInterface>();
+ }
+
+ mPreferencesScreens.add(screen);
+ }
+ }
+
+ void removePreferencesScreen(DialogInterface screen) {
+ synchronized (this) {
+
+ if (mPreferencesScreens == null) {
+ return;
+ }
+
+ mPreferencesScreens.remove(screen);
+ }
+ }
+
+ /**
+ * Called by {@link PreferenceActivity} to dispatch the new Intent event.
+ *
+ * @param intent The new Intent.
+ */
+ void dispatchNewIntent(Intent intent) {
+
+ // Remove any of the previously shown preferences screens
+ ArrayList<DialogInterface> screensToDismiss;
+
+ synchronized (this) {
+
+ if (mPreferencesScreens == null) {
+ return;
+ }
+
+ screensToDismiss = new ArrayList<DialogInterface>(mPreferencesScreens);
+ mPreferencesScreens.clear();
+ }
+
+ for (int i = screensToDismiss.size() - 1; i >= 0; i--) {
+ screensToDismiss.get(i).dismiss();
+ }
+ }
+
+ /**
+ * Sets the callback to be invoked when a {@link Preference} in the
+ * hierarchy rooted at this {@link PreferenceManager} is clicked.
+ *
+ * @param listener The callback to be invoked.
+ */
+ void setOnPreferenceTreeClickListener(OnPreferenceTreeClickListener listener) {
+ mOnPreferenceTreeClickListener = listener;
+ }
+
+ OnPreferenceTreeClickListener getOnPreferenceTreeClickListener() {
+ return mOnPreferenceTreeClickListener;
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when a
+ * {@link Preference} in the hierarchy rooted at this {@link PreferenceScreen} is
+ * clicked.
+ */
+ interface OnPreferenceTreeClickListener {
+ /**
+ * Called when a preference in the tree rooted at this
+ * {@link PreferenceScreen} has been clicked.
+ *
+ * @param preferenceScreen The {@link PreferenceScreen} that the
+ * preference is located in.
+ * @param preference The preference that was clicked.
+ * @return Whether the click was handled.
+ */
+ boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference);
+ }
+
+ /**
+ * Interface definition for a class that will be called when the container's activity
+ * receives an activity result.
+ */
+ public interface OnActivityResultListener {
+
+ /**
+ * See Activity's onActivityResult.
+ *
+ * @return Whether the request code was handled (in which case
+ * subsequent listeners will not be called.
+ */
+ boolean onActivityResult(int requestCode, int resultCode, Intent data);
+ }
+
+ /**
+ * Interface definition for a class that will be called when the container's activity
+ * is stopped.
+ */
+ public interface OnActivityStopListener {
+
+ /**
+ * See Activity's onStop.
+ */
+ void onActivityStop();
+ }
+
+ /**
+ * Interface definition for a class that will be called when the container's activity
+ * is destroyed.
+ */
+ public interface OnActivityDestroyListener {
+
+ /**
+ * See Activity's onDestroy.
+ */
+ void onActivityDestroy();
+ }
+
+}
diff --git a/core/java/android/preference/PreferenceScreen.java b/core/java/android/preference/PreferenceScreen.java
new file mode 100644
index 0000000..e4ecb88
--- /dev/null
+++ b/core/java/android/preference/PreferenceScreen.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.preference;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.Adapter;
+import android.widget.AdapterView;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+
+/**
+ * The {@link PreferenceScreen} class represents a top-level {@link Preference} that
+ * is the root of a {@link Preference} hierarchy. A {@link PreferenceActivity}
+ * points to an instance of this class to show the preferences. To instantiate
+ * this class, use {@link PreferenceManager#createPreferenceScreen(Context)}.
+ * <p>
+ * This class can appear in two places:
+ * <li> When a {@link PreferenceActivity} points to this, it is used as the root
+ * and is not shown (only the contained preferences are shown).
+ * <li> When it appears inside another preference hierarchy, it is shown and
+ * serves as the gateway to another screen of preferences (either by showing
+ * another screen of preferences as a {@link Dialog} or via a
+ * {@link Context#startActivity(android.content.Intent)} from the
+ * {@link Preference#getIntent()}). The children of this {@link PreferenceScreen}
+ * are NOT shown in the screen that this {@link PreferenceScreen} is shown in.
+ * Instead, a separate screen will be shown when this preference is clicked.
+ * <p>
+ * <code>
+ &lt;PreferenceScreen
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:key="first_preferencescreen"&gt;
+ &lt;CheckBoxPreference
+ android:key="wifi enabled"
+ android:title="WiFi" /&gt;
+ &lt;PreferenceScreen
+ android:key="second_preferencescreen"
+ android:title="WiFi settings"&gt;
+ &lt;CheckBoxPreference
+ android:key="prefer wifi"
+ android:title="Prefer WiFi" /&gt;
+ ... other preferences here ...
+ &lt;/PreferenceScreen&gt;
+ &lt;/PreferenceScreen&gt;
+ * </code>
+ * In this example, the "first_preferencescreen" will be used as the root of the
+ * hierarchy and given to a {@link PreferenceActivity}. The first screen will
+ * show preferences "WiFi" (which can be used to quickly enable/disable WiFi)
+ * and "WiFi settings". The "WiFi settings" is the "second_preferencescreen" and when
+ * clicked will show another screen of preferences such as "Prefer WiFi" (and
+ * the other preferences that are children of the "second_preferencescreen" tag).
+ *
+ * @see PreferenceCategory
+ */
+public final class PreferenceScreen extends PreferenceGroup implements AdapterView.OnItemClickListener,
+ DialogInterface.OnDismissListener {
+
+ private ListAdapter mRootAdapter;
+
+ private Dialog mDialog;
+
+ /**
+ * Do NOT use this constructor, use {@link PreferenceManager#createPreferenceScreen(Context)}.
+ * @hide-
+ */
+ public PreferenceScreen(Context context, AttributeSet attrs) {
+ super(context, attrs, com.android.internal.R.attr.preferenceScreenStyle);
+ }
+
+ /**
+ * Returns an adapter that can be attached to a {@link PreferenceActivity}
+ * to show the preferences contained in this {@link PreferenceScreen}.
+ * <p>
+ * This {@link PreferenceScreen} will NOT appear in the returned adapter, instead
+ * it appears in the hierarchy above this {@link PreferenceScreen}.
+ * <p>
+ * This adapter's {@link Adapter#getItem(int)} should always return a
+ * subclass of {@link Preference}.
+ *
+ * @return An adapter that provides the {@link Preference} contained in this
+ * {@link PreferenceScreen}.
+ * @see PreferenceGroupAdapter
+ */
+ public ListAdapter getRootAdapter() {
+ if (mRootAdapter == null) {
+ mRootAdapter = onCreateRootAdapter();
+ }
+
+ return mRootAdapter;
+ }
+
+ /**
+ * Creates the root adapter.
+ *
+ * @return An adapter that contains the preferences contained in this {@link PreferenceScreen}.
+ * @see #getRootAdapter()
+ */
+ protected ListAdapter onCreateRootAdapter() {
+ return new PreferenceGroupAdapter(this);
+ }
+
+ /**
+ * Binds a {@link ListView} to the preferences contained in this {@link PreferenceScreen} via
+ * {@link #getRootAdapter()}. It also handles passing list item clicks to the corresponding
+ * {@link Preference} contained by this {@link PreferenceScreen}.
+ *
+ * @param listView The list view to attach to.
+ */
+ public void bind(ListView listView) {
+ listView.setOnItemClickListener(this);
+ listView.setAdapter(getRootAdapter());
+
+ onAttachedToActivity();
+ }
+
+ @Override
+ protected void onClick() {
+ if (getIntent() != null || getPreferenceCount() == 0) {
+ return;
+ }
+
+ showDialog(null);
+ }
+
+ private void showDialog(Bundle state) {
+ Context context = getContext();
+ ListView listView = new ListView(context);
+ bind(listView);
+
+ Dialog dialog = mDialog = new Dialog(context, com.android.internal.R.style.Theme_NoTitleBar);
+ dialog.setContentView(listView);
+ dialog.setOnDismissListener(this);
+ if (state != null) {
+ dialog.onRestoreInstanceState(state);
+ }
+
+ // Add the screen to the list of preferences screens opened as dialogs
+ getPreferenceManager().addPreferencesScreen(dialog);
+
+ dialog.show();
+ }
+
+ public void onDismiss(DialogInterface dialog) {
+ mDialog = null;
+ getPreferenceManager().removePreferencesScreen(dialog);
+ }
+
+ /**
+ * Used to get a handle to the dialog.
+ * This is useful for cases where we want to manipulate the dialog
+ * as we would with any other activity or view.
+ */
+ public Dialog getDialog() {
+ return mDialog;
+ }
+
+ public void onItemClick(AdapterView parent, View view, int position, long id) {
+ Object item = getRootAdapter().getItem(position);
+ if (!(item instanceof Preference)) return;
+
+ final Preference preference = (Preference) item;
+ preference.performClick(this);
+ }
+
+ @Override
+ protected boolean isOnSameScreenAsChildren() {
+ return false;
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ final Parcelable superState = super.onSaveInstanceState();
+ final Dialog dialog = mDialog;
+ if (dialog == null || !dialog.isShowing()) {
+ return superState;
+ }
+
+ final SavedState myState = new SavedState(superState);
+ myState.isDialogShowing = true;
+ myState.dialogBundle = dialog.onSaveInstanceState();
+ return myState;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ if (state == null || !state.getClass().equals(SavedState.class)) {
+ // Didn't save state for us in onSaveInstanceState
+ super.onRestoreInstanceState(state);
+ return;
+ }
+
+ SavedState myState = (SavedState) state;
+ super.onRestoreInstanceState(myState.getSuperState());
+ if (myState.isDialogShowing) {
+ showDialog(myState.dialogBundle);
+ }
+ }
+
+ private static class SavedState extends BaseSavedState {
+ boolean isDialogShowing;
+ Bundle dialogBundle;
+
+ public SavedState(Parcel source) {
+ super(source);
+ isDialogShowing = source.readInt() == 1;
+ dialogBundle = source.readBundle();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeInt(isDialogShowing ? 1 : 0);
+ dest.writeBundle(dialogBundle);
+ }
+
+ public SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ public static final Parcelable.Creator<SavedState> CREATOR =
+ new Parcelable.Creator<SavedState>() {
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+
+}
diff --git a/core/java/android/preference/RingtonePreference.java b/core/java/android/preference/RingtonePreference.java
new file mode 100644
index 0000000..97674ce
--- /dev/null
+++ b/core/java/android/preference/RingtonePreference.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.preference;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.TypedArray;
+import android.media.RingtoneManager;
+import android.net.Uri;
+import android.provider.Settings.System;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+
+/**
+ * The {@link RingtonePreference} allows the user to choose one from all of the
+ * available ringtones. The chosen ringtone's URI will be persisted as a string.
+ * <p>
+ * If the user chooses the "Default" item, the saved string will be one of
+ * {@link System#DEFAULT_RINGTONE_URI} or
+ * {@link System#DEFAULT_NOTIFICATION_URI}. If the user chooses the "Silent"
+ * item, the saved string will be an empty string.
+ *
+ * @attr ref android.R.styleable#RingtonePreference_ringtoneType
+ * @attr ref android.R.styleable#RingtonePreference_showDefault
+ * @attr ref android.R.styleable#RingtonePreference_showSilent
+ */
+public class RingtonePreference extends Preference implements
+ PreferenceManager.OnActivityResultListener {
+
+ private static final String TAG = "RingtonePreference";
+
+ private int mRingtoneType;
+ private boolean mShowDefault;
+ private boolean mShowSilent;
+
+ private int mRequestCode;
+
+ public RingtonePreference(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ TypedArray a = context.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.RingtonePreference, defStyle, 0);
+ mRingtoneType = a.getInt(com.android.internal.R.styleable.RingtonePreference_ringtoneType,
+ RingtoneManager.TYPE_RINGTONE);
+ mShowDefault = a.getBoolean(com.android.internal.R.styleable.RingtonePreference_showDefault,
+ true);
+ mShowSilent = a.getBoolean(com.android.internal.R.styleable.RingtonePreference_showSilent,
+ true);
+ a.recycle();
+ }
+
+ public RingtonePreference(Context context, AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.ringtonePreferenceStyle);
+ }
+
+ public RingtonePreference(Context context) {
+ this(context, null);
+ }
+
+ /**
+ * Returns the sound type(s) that are shown in the picker.
+ *
+ * @return The sound type(s) that are shown in the picker.
+ * @see #setRingtoneType(int)
+ */
+ public int getRingtoneType() {
+ return mRingtoneType;
+ }
+
+ /**
+ * Sets the sound type(s) that are shown in the picker.
+ *
+ * @param type The sound type(s) that are shown in the picker.
+ * @see RingtoneManager#EXTRA_RINGTONE_TYPE
+ */
+ public void setRingtoneType(int type) {
+ mRingtoneType = type;
+ }
+
+ /**
+ * Returns whether to a show an item for the default sound/ringtone.
+ *
+ * @return Whether to show an item for the default sound/ringtone.
+ */
+ public boolean getShowDefault() {
+ return mShowDefault;
+ }
+
+ /**
+ * Sets whether to show an item for the default sound/ringtone. The default
+ * to use will be deduced from the sound type(s) being shown.
+ *
+ * @param showDefault Whether to show the default or not.
+ * @see RingtoneManager#EXTRA_RINGTONE_SHOW_DEFAULT
+ */
+ public void setShowDefault(boolean showDefault) {
+ mShowDefault = showDefault;
+ }
+
+ /**
+ * Returns whether to a show an item for 'Silent'.
+ *
+ * @return Whether to show an item for 'Silent'.
+ */
+ public boolean getShowSilent() {
+ return mShowSilent;
+ }
+
+ /**
+ * Sets whether to show an item for 'Silent'.
+ *
+ * @param showSilent Whether to show 'Silent'.
+ * @see RingtoneManager#EXTRA_RINGTONE_SHOW_SILENT
+ */
+ public void setShowSilent(boolean showSilent) {
+ mShowSilent = showSilent;
+ }
+
+ @Override
+ protected void onClick() {
+ // Launch the ringtone picker
+ Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER);
+ onPrepareRingtonePickerIntent(intent);
+ getPreferenceManager().getActivity().startActivityForResult(intent, mRequestCode);
+ }
+
+ /**
+ * Prepares the intent to launch the ringtone picker. This can be modified
+ * to adjust the parameters of the ringtone picker.
+ *
+ * @param ringtonePickerIntent The ringtone picker intent that can be
+ * modified by putting extras.
+ */
+ protected void onPrepareRingtonePickerIntent(Intent ringtonePickerIntent) {
+
+ ringtonePickerIntent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI,
+ onRestoreRingtone());
+
+ ringtonePickerIntent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, mShowDefault);
+ if (mShowDefault) {
+ ringtonePickerIntent.putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI,
+ RingtoneManager.getDefaultUri(getRingtoneType()));
+ }
+
+ ringtonePickerIntent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, mShowSilent);
+ ringtonePickerIntent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, mRingtoneType);
+ }
+
+ /**
+ * Called when a ringtone is chosen.
+ * <p>
+ * By default, this saves the ringtone URI to the persistent storage as a
+ * string.
+ *
+ * @param ringtoneUri The chosen ringtone's {@link Uri}. Can be null.
+ */
+ protected void onSaveRingtone(Uri ringtoneUri) {
+ persistString(ringtoneUri != null ? ringtoneUri.toString() : "");
+ }
+
+ /**
+ * Called when the chooser is about to be shown and the current ringtone
+ * should be marked. Can return null to not mark any ringtone.
+ * <p>
+ * By default, this restores the previous ringtone URI from the persistent
+ * storage.
+ *
+ * @return The ringtone to be marked as the current ringtone.
+ */
+ protected Uri onRestoreRingtone() {
+ final String uriString = getPersistedString(null);
+ return !TextUtils.isEmpty(uriString) ? Uri.parse(uriString) : null;
+ }
+
+ @Override
+ protected Object onGetDefaultValue(TypedArray a, int index) {
+ return a.getString(index);
+ }
+
+ @Override
+ protected void onSetInitialValue(boolean restorePersistedValue, Object defaultValueObj) {
+ String defaultValue = (String) defaultValueObj;
+
+ /*
+ * This method is normally to make sure the internal state and UI
+ * matches either the persisted value or the default value. Since we
+ * don't show the current value in the UI (until the dialog is opened)
+ * and we don't keep local state, if we are restoring the persisted
+ * value we don't need to do anything.
+ */
+ if (restorePersistedValue) {
+ return;
+ }
+
+ // If we are setting to the default value, we should persist it.
+ if (!TextUtils.isEmpty(defaultValue)) {
+ onSaveRingtone(Uri.parse(defaultValue));
+ }
+ }
+
+ @Override
+ protected void onAttachedToHierarchy(PreferenceManager preferenceManager) {
+ super.onAttachedToHierarchy(preferenceManager);
+
+ preferenceManager.registerOnActivityResultListener(this);
+ mRequestCode = preferenceManager.getNextRequestCode();
+ }
+
+ public boolean onActivityResult(int requestCode, int resultCode, Intent data) {
+
+ if (requestCode == mRequestCode) {
+
+ if (data != null) {
+ Uri uri = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
+
+ if (callChangeListener(uri != null ? uri.toString() : "")) {
+ onSaveRingtone(uri);
+ }
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+}
diff --git a/core/java/android/preference/SeekBarPreference.java b/core/java/android/preference/SeekBarPreference.java
new file mode 100644
index 0000000..658c2a7
--- /dev/null
+++ b/core/java/android/preference/SeekBarPreference.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.preference;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.preference.DialogPreference;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.SeekBar;
+
+/**
+ * @hide
+ */
+public class SeekBarPreference extends DialogPreference {
+ private static final String TAG = "SeekBarPreference";
+
+ private Drawable mMyIcon;
+
+ public SeekBarPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ setDialogLayoutResource(com.android.internal.R.layout.seekbar_dialog);
+ setPositiveButtonText(android.R.string.ok);
+ setNegativeButtonText(android.R.string.cancel);
+
+ // Steal the XML dialogIcon attribute's value
+ mMyIcon = getDialogIcon();
+ setDialogIcon(null);
+ }
+
+ @Override
+ protected void onBindDialogView(View view) {
+ super.onBindDialogView(view);
+
+ final ImageView iconView = (ImageView) view.findViewById(android.R.id.icon);
+ if (mMyIcon != null) {
+ iconView.setImageDrawable(mMyIcon);
+ } else {
+ iconView.setVisibility(View.GONE);
+ }
+ }
+
+ protected static SeekBar getSeekBar(View dialogView) {
+ return (SeekBar) dialogView.findViewById(com.android.internal.R.id.seekbar);
+ }
+}
diff --git a/core/java/android/preference/VolumePreference.java b/core/java/android/preference/VolumePreference.java
new file mode 100644
index 0000000..5a0a089
--- /dev/null
+++ b/core/java/android/preference/VolumePreference.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.preference;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.database.ContentObserver;
+import android.media.Ringtone;
+import android.media.RingtoneManager;
+import android.media.AudioManager;
+import android.os.Handler;
+import android.preference.PreferenceManager;
+import android.provider.Settings;
+import android.provider.Settings.System;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.SeekBar;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+
+/**
+ * @hide
+ */
+public class VolumePreference extends SeekBarPreference implements OnSeekBarChangeListener,
+ Runnable, PreferenceManager.OnActivityStopListener {
+
+ private static final String TAG = "VolumePreference";
+
+ private ContentResolver mContentResolver;
+ private Handler mHandler = new Handler();
+
+ private AudioManager mVolume;
+ private int mStreamType;
+ private int mOriginalStreamVolume;
+ private Ringtone mRingtone;
+
+ private int mLastProgress;
+ private SeekBar mSeekBar;
+
+ private ContentObserver mVolumeObserver = new ContentObserver(mHandler) {
+ @Override
+ public void onChange(boolean selfChange) {
+ super.onChange(selfChange);
+
+ if (mSeekBar != null) {
+ mSeekBar.setProgress(System.getInt(mContentResolver,
+ System.VOLUME_SETTINGS[mStreamType], 0));
+ }
+ }
+ };
+
+ public VolumePreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ TypedArray a = context.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.VolumePreference, 0, 0);
+ mStreamType = a.getInt(android.R.styleable.VolumePreference_streamType, 0);
+ a.recycle();
+
+ mContentResolver = context.getContentResolver();
+
+ mVolume = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ }
+
+ @Override
+ protected void onBindDialogView(View view) {
+ super.onBindDialogView(view);
+
+ final SeekBar seekBar = mSeekBar = (SeekBar) view.findViewById(com.android.internal.R.id.seekbar);
+ seekBar.setMax(mVolume.getStreamMaxVolume(mStreamType));
+ mOriginalStreamVolume = mVolume.getStreamVolume(mStreamType);
+ seekBar.setProgress(mOriginalStreamVolume);
+ seekBar.setOnSeekBarChangeListener(this);
+
+ mContentResolver.registerContentObserver(System.getUriFor(System.VOLUME_SETTINGS[mStreamType]), false, mVolumeObserver);
+
+ getPreferenceManager().registerOnActivityStopListener(this);
+ mRingtone = RingtoneManager.getRingtone(getContext(), Settings.System.DEFAULT_RINGTONE_URI);
+ }
+
+ @Override
+ protected void onDialogClosed(boolean positiveResult) {
+ super.onDialogClosed(positiveResult);
+
+ if (!positiveResult) {
+ mVolume.setStreamVolume(mStreamType, mOriginalStreamVolume, 0);
+ }
+
+ cleanup();
+ }
+
+
+ public void onActivityStop() {
+ cleanup();
+ }
+
+ /**
+ * Do clean up. This can be called multiple times!
+ */
+ private void cleanup() {
+ stopSample();
+ if (mVolumeObserver != null) {
+ mContentResolver.unregisterContentObserver(mVolumeObserver);
+ }
+ getPreferenceManager().unregisterOnActivityStopListener(this);
+ mSeekBar = null;
+ }
+
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromTouch) {
+ if (!fromTouch) {
+ return;
+ }
+
+ postSetVolume(progress);
+ }
+
+ private void postSetVolume(int progress) {
+ // Do the volume changing separately to give responsive UI
+ mLastProgress = progress;
+ mHandler.removeCallbacks(this);
+ mHandler.post(this);
+ }
+
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ }
+
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ if (mRingtone != null && !mRingtone.isPlaying()) {
+ sample();
+ }
+ }
+
+ public void run() {
+ mVolume.setStreamVolume(mStreamType, mLastProgress, 0);
+ }
+
+ private void sample() {
+ mRingtone.play();
+ }
+
+ private void stopSample() {
+ if (mRingtone != null) {
+ mRingtone.stop();
+ }
+ }
+
+}
diff --git a/core/java/android/preference/package.html b/core/java/android/preference/package.html
new file mode 100644
index 0000000..d24d5bb
--- /dev/null
+++ b/core/java/android/preference/package.html
@@ -0,0 +1,23 @@
+<HTML>
+<BODY>
+Provides classes that manage application preferences and implement the preferences UI.
+Using these ensures that all the preferences within each application are maintained
+in the same manner and the user experience is consistent with that of the system and
+other applications.
+<p>
+The preferences portion of an application
+should be ran as a separate {@link android.app.Activity} that extends
+the {@link android.preference.PreferenceActivity} class. In the PreferenceActivity, a
+{@link android.preference.PreferenceScreen} object should be the root element of the layout.
+The PreferenceScreen contains {@link android.preference.Preference} elements such as a
+{@link android.preference.CheckBoxPreference}, {@link android.preference.EditTextPreference},
+{@link android.preference.ListPreference}, {@link android.preference.PreferenceCategory},
+or {@link android.preference.RingtonePreference}. </p>
+<p>
+All settings made for a given {@link android.preference.Preference} will be automatically saved
+to the application's instance of {@link android.content.SharedPreferences}. Access to the
+SharedPreferences is simple with {@link android.preference.Preference#getSharedPreferences()}.</p>
+<p>
+Note that saved preferences are accessible only to the application that created them.</p>
+</BODY>
+</HTML>
diff --git a/core/java/android/provider/BaseColumns.java b/core/java/android/provider/BaseColumns.java
new file mode 100644
index 0000000..f594c19
--- /dev/null
+++ b/core/java/android/provider/BaseColumns.java
@@ -0,0 +1,32 @@
+/*
+ * 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.provider;
+
+public interface BaseColumns
+{
+ /**
+ * The unique ID for a row.
+ * <P>Type: INTEGER (long)</P>
+ */
+ public static final String _ID = "_id";
+
+ /**
+ * The count of rows in a directory.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String _COUNT = "_count";
+}
diff --git a/core/java/android/provider/Browser.java b/core/java/android/provider/Browser.java
new file mode 100644
index 0000000..7aaed49
--- /dev/null
+++ b/core/java/android/provider/Browser.java
@@ -0,0 +1,450 @@
+/*
+ * 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.provider;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.net.Uri;
+import android.util.Log;
+import android.webkit.WebIconDatabase;
+
+import java.util.Date;
+
+public class Browser {
+ private static final String LOGTAG = "browser";
+ public static final Uri BOOKMARKS_URI =
+ Uri.parse("content://browser/bookmarks");
+
+ /**
+ * The name of extra data when starting Browser with ACTION_VIEW or
+ * ACTION_SEARCH intent.
+ * <p>
+ * The value should be an integer between 0 and 1000. If not set or set to
+ * 0, the Browser will use default. If set to 100, the Browser will start
+ * with 100%.
+ */
+ public static final String INITIAL_ZOOM_LEVEL = "browser.initialZoomLevel";
+
+ /* if you change column order you must also change indices
+ below */
+ public static final String[] HISTORY_PROJECTION = new String[] {
+ BookmarkColumns._ID, BookmarkColumns.URL, BookmarkColumns.VISITS,
+ BookmarkColumns.DATE, BookmarkColumns.BOOKMARK, BookmarkColumns.TITLE,
+ BookmarkColumns.FAVICON };
+
+ /* these indices dependent on HISTORY_PROJECTION */
+ public static final int HISTORY_PROJECTION_ID_INDEX = 0;
+ public static final int HISTORY_PROJECTION_URL_INDEX = 1;
+ public static final int HISTORY_PROJECTION_VISITS_INDEX = 2;
+ public static final int HISTORY_PROJECTION_DATE_INDEX = 3;
+ public static final int HISTORY_PROJECTION_BOOKMARK_INDEX = 4;
+ public static final int HISTORY_PROJECTION_TITLE_INDEX = 5;
+ public static final int HISTORY_PROJECTION_FAVICON_INDEX = 6;
+
+ /* columns needed to determine whether to truncate history */
+ public static final String[] TRUNCATE_HISTORY_PROJECTION = new String[] {
+ BookmarkColumns._ID, BookmarkColumns.DATE, };
+ public static final int TRUNCATE_HISTORY_PROJECTION_ID_INDEX = 0;
+
+ /* truncate this many history items at a time */
+ public static final int TRUNCATE_N_OLDEST = 5;
+
+ public static final Uri SEARCHES_URI =
+ Uri.parse("content://browser/searches");
+
+ /* if you change column order you must also change indices
+ below */
+ public static final String[] SEARCHES_PROJECTION = new String[] {
+ SearchColumns._ID, SearchColumns.SEARCH, SearchColumns.DATE };
+
+ /* these indices dependent on SEARCHES_PROJECTION */
+ public static final int SEARCHES_PROJECTION_SEARCH_INDEX = 1;
+ public static final int SEARCHES_PROJECTION_DATE_INDEX = 2;
+
+ private static final String SEARCHES_WHERE_CLAUSE = "search = ?";
+
+ /* Set a cap on the count of history items in the history/bookmark
+ table, to prevent db and layout operations from dragging to a
+ crawl. Revisit this cap when/if db/layout performance
+ improvements are made. Note: this does not affect bookmark
+ entries -- if the user wants more bookmarks than the cap, they
+ get them. */
+ private static final int MAX_HISTORY_COUNT = 250;
+
+ /**
+ * Open the AddBookmark activity to save a bookmark. Launch with
+ * and/or url, which can be edited by the user before saving.
+ * @param c Context used to launch the AddBookmark activity.
+ * @param title Title for the bookmark. Can be null or empty string.
+ * @param url Url for the bookmark. Can be null or empty string.
+ */
+ public static final void saveBookmark(Context c,
+ String title,
+ String url) {
+ Intent i = new Intent(Intent.ACTION_INSERT, Browser.BOOKMARKS_URI);
+ i.putExtra("title", title);
+ i.putExtra("url", url);
+ c.startActivity(i);
+ }
+
+ public static final void sendString(Context c, String s) {
+ Intent send = new Intent(Intent.ACTION_SEND);
+ send.setType("text/plain");
+ send.putExtra(Intent.EXTRA_TEXT, s);
+
+ try {
+ c.startActivity(Intent.createChooser(send,
+ c.getText(com.android.internal.R.string.sendText)));
+ } catch(android.content.ActivityNotFoundException ex) {
+ // if no app handles it, do nothing
+ }
+ }
+
+ /**
+ * Return a cursor pointing to a list of all the bookmarks.
+ * @param cr The ContentResolver used to access the database.
+ */
+ public static final Cursor getAllBookmarks(ContentResolver cr) throws
+ IllegalStateException {
+ return cr.query(BOOKMARKS_URI,
+ new String[] { BookmarkColumns.URL },
+ "bookmark = 1", null, null);
+ }
+
+ /**
+ * Return a cursor pointing to a list of all visited site urls.
+ * @param cr The ContentResolver used to access the database.
+ */
+ public static final Cursor getAllVisitedUrls(ContentResolver cr) throws
+ IllegalStateException {
+ return cr.query(BOOKMARKS_URI,
+ new String[] { BookmarkColumns.URL }, null, null, null);
+ }
+
+ /**
+ * Update the visited history to acknowledge that a site has been
+ * visited.
+ * @param cr The ContentResolver used to access the database.
+ * @param url The site being visited.
+ * @param real Whether this is an actual visit, and should be added to the
+ * number of visits.
+ */
+ public static final void updateVisitedHistory(ContentResolver cr,
+ String url, boolean real) {
+ long now = new Date().getTime();
+ try {
+ StringBuilder sb = new StringBuilder(BookmarkColumns.URL + " = ");
+ DatabaseUtils.appendEscapedSQLString(sb, url);
+ Cursor c = cr.query(
+ BOOKMARKS_URI,
+ HISTORY_PROJECTION,
+ sb.toString(),
+ null,
+ null);
+ /* We should only get one answer that is exactly the same. */
+ if (c.moveToFirst()) {
+ ContentValues map = new ContentValues();
+ if (real) {
+ map.put(BookmarkColumns.VISITS, c
+ .getInt(HISTORY_PROJECTION_VISITS_INDEX) + 1);
+ }
+ map.put(BookmarkColumns.DATE, now);
+ cr.update(BOOKMARKS_URI, map, "_id = " + c.getInt(0), null);
+ } else {
+ truncateHistory(cr);
+ ContentValues map = new ContentValues();
+ map.put(BookmarkColumns.URL, url);
+ map.put(BookmarkColumns.VISITS, real ? 1 : 0);
+ map.put(BookmarkColumns.DATE, now);
+ map.put(BookmarkColumns.BOOKMARK, 0);
+ map.put(BookmarkColumns.TITLE, url);
+ map.put(BookmarkColumns.CREATED, 0);
+ cr.insert(BOOKMARKS_URI, map);
+ }
+ c.deactivate();
+ } catch (IllegalStateException e) {
+ return;
+ }
+ }
+
+ /**
+ * If there are more than MAX_HISTORY_COUNT non-bookmark history
+ * items in the bookmark/history table, delete TRUNCATE_N_OLDEST
+ * of them. This is used to keep our history table to a
+ * reasonable size. Note: it does not prune bookmarks. If the
+ * user wants 1000 bookmarks, the user gets 1000 bookmarks.
+ *
+ * @param cr The ContentResolver used to access the database.
+ */
+ public static final void truncateHistory(ContentResolver cr) {
+ try {
+ // Select non-bookmark history, ordered by date
+ Cursor c = cr.query(
+ BOOKMARKS_URI,
+ TRUNCATE_HISTORY_PROJECTION,
+ "bookmark = 0",
+ null,
+ BookmarkColumns.DATE);
+ // Log.v(LOGTAG, "history count " + c.count());
+ if (c.moveToFirst() && c.getCount() >= MAX_HISTORY_COUNT) {
+ /* eliminate oldest history items */
+ for (int i = 0; i < TRUNCATE_N_OLDEST; i++) {
+ // Log.v(LOGTAG, "truncate history " +
+ // c.getInt(TRUNCATE_HISTORY_PROJECTION_ID_INDEX));
+ deleteHistoryWhere(
+ cr, "_id = " +
+ c.getInt(TRUNCATE_HISTORY_PROJECTION_ID_INDEX));
+ if (!c.moveToNext()) break;
+ }
+ }
+ c.deactivate();
+ } catch (IllegalStateException e) {
+ Log.e(LOGTAG, "truncateHistory", e);
+ return;
+ }
+ }
+
+ /**
+ * Returns whether there is any history to clear.
+ * @param cr The ContentResolver used to access the database.
+ * @return boolean True if the history can be cleared.
+ */
+ public static final boolean canClearHistory(ContentResolver cr) {
+ try {
+ Cursor c = cr.query(
+ BOOKMARKS_URI,
+ new String [] { BookmarkColumns._ID,
+ BookmarkColumns.BOOKMARK,
+ BookmarkColumns.VISITS },
+ "bookmark = 0 OR visits > 0",
+ null,
+ null
+ );
+ boolean ret = c.moveToFirst();
+ c.deactivate();
+ return ret;
+ } catch (IllegalStateException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Delete all entries from the bookmarks/history table which are
+ * not bookmarks. Also set all visited bookmarks to unvisited.
+ * @param cr The ContentResolver used to access the database.
+ */
+ public static final void clearHistory(ContentResolver cr) {
+ deleteHistoryWhere(cr, null);
+ }
+
+ /**
+ * Helper function to delete all history items and revert all
+ * bookmarks to zero visits which meet the criteria provided.
+ * @param cr The ContentResolver used to access the database.
+ * @param whereClause String to limit the items affected.
+ * null means all items.
+ */
+ private static final void deleteHistoryWhere(ContentResolver cr,
+ String whereClause) {
+ try {
+ Cursor c = cr.query(BOOKMARKS_URI,
+ HISTORY_PROJECTION,
+ whereClause,
+ null,
+ null);
+ if (!c.moveToFirst()) {
+ c.deactivate();
+ return;
+ }
+
+ final WebIconDatabase iconDb = WebIconDatabase.getInstance();
+ /* Delete favicons, and revert bookmarks which have been visited
+ * to simply bookmarks.
+ */
+ StringBuffer sb = new StringBuffer();
+ boolean firstTime = true;
+ do {
+ String url = c.getString(HISTORY_PROJECTION_URL_INDEX);
+ boolean isBookmark =
+ c.getInt(HISTORY_PROJECTION_BOOKMARK_INDEX) == 1;
+ if (isBookmark) {
+ if (firstTime) {
+ firstTime = false;
+ } else {
+ sb.append(" OR ");
+ }
+ sb.append("( _id = ");
+ sb.append(c.getInt(0));
+ sb.append(" )");
+ } else {
+ iconDb.releaseIconForPageUrl(url);
+ }
+ } while (c.moveToNext());
+ c.deactivate();
+
+ if (!firstTime) {
+ ContentValues map = new ContentValues();
+ map.put(BookmarkColumns.VISITS, 0);
+ map.put(BookmarkColumns.DATE, 0);
+ /* FIXME: Should I also remove the title? */
+ cr.update(BOOKMARKS_URI, map, sb.toString(), null);
+ }
+
+ String deleteWhereClause = BookmarkColumns.BOOKMARK + " = 0";
+ if (whereClause != null) {
+ deleteWhereClause += " AND " + whereClause;
+ }
+ cr.delete(BOOKMARKS_URI, deleteWhereClause, null);
+ } catch (IllegalStateException e) {
+ return;
+ }
+ }
+
+ /**
+ * Delete all history items from begin to end.
+ * @param cr The ContentResolver used to access the database.
+ * @param begin First date to remove. If -1, all dates before end.
+ * Inclusive.
+ * @param end Last date to remove. If -1, all dates after begin.
+ * Non-inclusive.
+ */
+ public static final void deleteHistoryTimeFrame(ContentResolver cr,
+ long begin, long end) {
+ String whereClause;
+ String date = BookmarkColumns.DATE;
+ if (-1 == begin) {
+ if (-1 == end) {
+ clearHistory(cr);
+ return;
+ }
+ whereClause = date + " < " + Long.toString(end);
+ } else if (-1 == end) {
+ whereClause = date + " >= " + Long.toString(begin);
+ } else {
+ whereClause = date + " >= " + Long.toString(begin) + " AND " + date
+ + " < " + Long.toString(end);
+ }
+ deleteHistoryWhere(cr, whereClause);
+ }
+
+ /**
+ * Remove a specific url from the history database.
+ * @param cr The ContentResolver used to access the database.
+ * @param url url to remove.
+ */
+ public static final void deleteFromHistory(ContentResolver cr,
+ String url) {
+ StringBuilder sb = new StringBuilder(BookmarkColumns.URL + " = ");
+ DatabaseUtils.appendEscapedSQLString(sb, url);
+ String matchesUrl = sb.toString();
+ deleteHistoryWhere(cr, matchesUrl);
+ }
+
+ /**
+ * Add a search string to the searches database.
+ * @param cr The ContentResolver used to access the database.
+ * @param search The string to add to the searches database.
+ */
+ public static final void addSearchUrl(ContentResolver cr, String search) {
+ long now = new Date().getTime();
+ try {
+ Cursor c = cr.query(
+ SEARCHES_URI,
+ SEARCHES_PROJECTION,
+ SEARCHES_WHERE_CLAUSE,
+ new String [] { search },
+ null);
+ /* We should only get one answer that is exactly the same. */
+ if (c.moveToFirst()) {
+ ContentValues map = new ContentValues();
+ map.put(BookmarkColumns.DATE, now);
+ cr.update(BOOKMARKS_URI, map, "_id = " + c.getInt(0), null);
+ } else {
+ ContentValues map = new ContentValues();
+ map.put(SearchColumns.SEARCH, search);
+ map.put(SearchColumns.DATE, now);
+ cr.insert(SEARCHES_URI, map);
+ }
+ c.deactivate();
+ } catch (IllegalStateException e) {
+ Log.e(LOGTAG, "addSearchUrl", e);
+ return;
+ }
+ }
+ /**
+ * Remove all searches from the search database.
+ * @param cr The ContentResolver used to access the database.
+ */
+ public static final void clearSearches(ContentResolver cr) {
+ // FIXME: Should this clear the urls to which these searches lead?
+ // (i.e. remove google.com/query= blah blah blah)
+ try {
+ cr.delete(SEARCHES_URI, null, null);
+ } catch (IllegalStateException e) {
+ Log.e(LOGTAG, "clearSearches", e);
+ }
+ }
+
+ /**
+ * Request all icons from the database.
+ * @param cr The ContentResolver used to access the database.
+ * @param where Clause to be used to limit the query from the database.
+ * Must be an allowable string to be passed into a database query.
+ * @param listener IconListener that gets the icons once they are
+ * retrieved.
+ */
+ public static final void requestAllIcons(ContentResolver cr, String where,
+ WebIconDatabase.IconListener listener) {
+ try {
+ final Cursor c = cr.query(
+ BOOKMARKS_URI,
+ HISTORY_PROJECTION,
+ where, null, null);
+ if (c.moveToFirst()) {
+ final WebIconDatabase db = WebIconDatabase.getInstance();
+ do {
+ db.requestIconForPageUrl(
+ c.getString(HISTORY_PROJECTION_URL_INDEX),
+ listener);
+ } while (c.moveToNext());
+ }
+ c.deactivate();
+ } catch (IllegalStateException e) {
+ Log.e(LOGTAG, "requestAllIcons", e);
+ }
+ }
+
+ public static class BookmarkColumns implements BaseColumns {
+ public static final String URL = "url";
+ public static final String VISITS = "visits";
+ public static final String DATE = "date";
+ public static final String BOOKMARK = "bookmark";
+ public static final String TITLE = "title";
+ public static final String CREATED = "created";
+ public static final String FAVICON = "favicon";
+ }
+
+ public static class SearchColumns implements BaseColumns {
+ public static final String URL = "url";
+ public static final String SEARCH = "search";
+ public static final String DATE = "date";
+ }
+}
diff --git a/core/java/android/provider/Calendar.java b/core/java/android/provider/Calendar.java
new file mode 100644
index 0000000..b07f1b8
--- /dev/null
+++ b/core/java/android/provider/Calendar.java
@@ -0,0 +1,1115 @@
+/*
+ * 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.provider;
+
+import com.google.android.gdata.client.AndroidGDataClient;
+import com.google.android.gdata.client.AndroidXmlParserFactory;
+import com.google.wireless.gdata.calendar.client.CalendarClient;
+import com.google.wireless.gdata.calendar.data.EventEntry;
+import com.google.wireless.gdata.calendar.data.Who;
+import com.google.wireless.gdata.calendar.parser.xml.XmlCalendarGDataParserFactory;
+import com.google.wireless.gdata.client.AuthenticationException;
+import com.google.wireless.gdata.client.AllDeletedUnavailableException;
+import com.google.wireless.gdata.data.Entry;
+import com.google.wireless.gdata.data.StringUtils;
+import com.google.wireless.gdata.parser.ParseException;
+import com.android.internal.database.ArrayListCursor;
+
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.pim.DateUtils;
+import android.pim.ICalendar;
+import android.pim.RecurrenceSet;
+import android.pim.Time;
+import android.text.TextUtils;
+import android.util.Config;
+import android.util.Log;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Vector;
+
+/**
+ * The Calendar provider contains all calendar events.
+ *
+ * @hide
+ */
+public final class Calendar {
+
+ public static final String TAG = "Calendar";
+
+ /**
+ * Broadcast Action: An event reminder.
+ */
+ public static final String
+ EVENT_REMINDER_ACTION = "android.intent.action.EVENT_REMINDER";
+
+ /**
+ * These are the symbolic names for the keys used in the extra data
+ * passed in the intent for event reminders.
+ */
+ public static final String EVENT_BEGIN_TIME = "beginTime";
+ public static final String EVENT_END_TIME = "endTime";
+
+ public static final String AUTHORITY = "calendar";
+
+ /**
+ * The content:// style URL for the top-level calendar authority
+ */
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://" + AUTHORITY);
+
+ /**
+ * Columns from the Calendars table that other tables join into themselves.
+ */
+ public interface CalendarsColumns
+ {
+ /**
+ * The color of the calendar
+ * <P>Type: INTEGER (color value)</P>
+ */
+ public static final String COLOR = "color";
+
+ /**
+ * The level of access that the user has for the calendar
+ * <P>Type: INTEGER (one of the values below)</P>
+ */
+ public static final String ACCESS_LEVEL = "access_level";
+
+ /** Cannot access the calendar */
+ public static final int NO_ACCESS = 0;
+ /** Can only see free/busy information about the calendar */
+ public static final int FREEBUSY_ACCESS = 100;
+ /** Can read all event details */
+ public static final int READ_ACCESS = 200;
+ public static final int RESPOND_ACCESS = 300;
+ public static final int OVERRIDE_ACCESS = 400;
+ /** Full access to modify the calendar, but not the access control settings */
+ public static final int CONTRIBUTOR_ACCESS = 500;
+ public static final int EDITOR_ACCESS = 600;
+ /** Full access to the calendar */
+ public static final int OWNER_ACCESS = 700;
+ public static final int ROOT_ACCESS = 800;
+
+ /**
+ * Is the calendar selected to be displayed?
+ * <P>Type: INTEGER (boolean)</P>
+ */
+ public static final String SELECTED = "selected";
+
+ /**
+ * The timezone the calendar's events occurs in
+ * <P>Type: TEXT</P>
+ */
+ public static final String TIMEZONE = "timezone";
+
+ /**
+ * If this calendar is in the list of calendars that are selected for
+ * syncing then "sync_events" is 1, otherwise 0.
+ * <p>Type: INTEGER (boolean)</p>
+ */
+ public static final String SYNC_EVENTS = "sync_events";
+ }
+
+ /**
+ * Contains a list of available calendars.
+ */
+ public static class Calendars implements BaseColumns, SyncConstValue, CalendarsColumns
+ {
+ public static final Cursor query(ContentResolver cr, String[] projection,
+ String where, String orderBy)
+ {
+ return cr.query(CONTENT_URI, projection, where,
+ null, orderBy == null ? DEFAULT_SORT_ORDER : orderBy);
+ }
+
+ /**
+ * Convenience method perform a delete on the Calendar provider
+ *
+ * @param cr the ContentResolver
+ * @param selection the rows to delete
+ * @return the count of rows that were deleted
+ */
+ public static int delete(ContentResolver cr, String selection, String[] selectionArgs)
+ {
+ return cr.delete(CONTENT_URI, selection, selectionArgs);
+ }
+
+ /**
+ * Convenience method to delete all calendars that match the account.
+ *
+ * @param cr the ContentResolver
+ * @param account the account whose rows should be deleted
+ * @return the count of rows that were deleted
+ */
+ public static int deleteCalendarsForAccount(ContentResolver cr,
+ String account) {
+ // delete all calendars that match this account
+ return Calendar.Calendars.delete(cr, Calendar.Calendars._SYNC_ACCOUNT + "=?",
+ new String[] {account});
+ }
+
+ /**
+ * The content:// style URL for this table
+ */
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://calendar/calendars");
+
+ public static final Uri LIVE_CONTENT_URI =
+ Uri.parse("content://calendar/calendars?update=1");
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = "displayName";
+
+ /**
+ * The URL to the calendar
+ * <P>Type: TEXT (URL)</P>
+ */
+ public static final String URL = "url";
+
+ /**
+ * The name of the calendar
+ * <P>Type: TEXT</P>
+ */
+ public static final String NAME = "name";
+
+ /**
+ * The display name of the calendar
+ * <P>Type: TEXT</P>
+ */
+ public static final String DISPLAY_NAME = "displayName";
+
+ /**
+ * The location the of the events in the calendar
+ * <P>Type: TEXT</P>
+ */
+ public static final String LOCATION = "location";
+
+ /**
+ * Should the calendar be hidden in the calendar selection panel?
+ * <P>Type: INTEGER (boolean)</P>
+ */
+ public static final String HIDDEN = "hidden";
+ }
+
+ public interface AttendeesColumns {
+
+ /**
+ * The id of the event.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String EVENT_ID = "event_id";
+
+ /**
+ * The name of the attendee.
+ * <P>Type: STRING</P>
+ */
+ public static final String ATTENDEE_NAME = "attendeeName";
+
+ /**
+ * The email address of the attendee.
+ * <P>Type: STRING</P>
+ */
+ public static final String ATTENDEE_EMAIL = "attendeeEmail";
+
+ /**
+ * The relationship of the attendee to the user.
+ * <P>Type: INTEGER (one of {@link #RELATIONSHIP_ATTENDEE}, ...}.
+ */
+ public static final String ATTENDEE_RELATIONSHIP = "attendeeRelationship";
+
+ public static final int RELATIONSHIP_NONE = 0;
+ public static final int RELATIONSHIP_ATTENDEE = 1;
+ public static final int RELATIONSHIP_ORGANIZER = 2;
+ public static final int RELATIONSHIP_PERFORMER = 3;
+ public static final int RELATIONSHIP_SPEAKER = 4;
+
+ /**
+ * The type of attendee.
+ * <P>Type: Integer (one of {@link #TYPE_REQUIRED}, {@link #TYPE_OPTIONAL})
+ */
+ public static final String ATTENDEE_TYPE = "attendeeType";
+
+ public static final int TYPE_NONE = 0;
+ public static final int TYPE_REQUIRED = 1;
+ public static final int TYPE_OPTIONAL = 2;
+
+ /**
+ * The attendance status of the attendee.
+ * <P>Type: Integer (one of {@link #ATTENDEE_STATUS_ACCEPTED}, ...}.
+ */
+ public static final String ATTENDEE_STATUS = "attendeeStatus";
+
+ public static final int ATTENDEE_STATUS_NONE = 0;
+ public static final int ATTENDEE_STATUS_ACCEPTED = 1;
+ public static final int ATTENDEE_STATUS_DECLINED = 2;
+ public static final int ATTENDEE_STATUS_INVITED = 3;
+ public static final int ATTENDEE_STATUS_TENTATIVE = 4;
+ }
+
+ public static final class Attendees implements BaseColumns,
+ AttendeesColumns, EventsColumns {
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://calendar/attendees");
+
+ // TODO: fill out this class when we actually start utilizing attendees
+ // in the calendar application.
+ }
+
+ /**
+ * Columns from the Events table that other tables join into themselves.
+ */
+ public interface EventsColumns
+ {
+ /**
+ * The calendar the event belongs to
+ * <P>Type: INTEGER (foreign key to the Calendars table)</P>
+ */
+ public static final String CALENDAR_ID = "calendar_id";
+
+ /**
+ * The URI for an HTML version of this event.
+ * <P>Type: TEXT</P>
+ */
+ public static final String HTML_URI = "htmlUri";
+
+ /**
+ * The title of the event
+ * <P>Type: TEXT</P>
+ */
+ public static final String TITLE = "title";
+
+ /**
+ * The description of the event
+ * <P>Type: TEXT</P>
+ */
+ public static final String DESCRIPTION = "description";
+
+ /**
+ * Where the event takes place.
+ * <P>Type: TEXT</P>
+ */
+ public static final String EVENT_LOCATION = "eventLocation";
+
+ /**
+ * The event status
+ * <P>Type: INTEGER (int)</P>
+ */
+ public static final String STATUS = "eventStatus";
+
+ public static final int STATUS_TENTATIVE = 0;
+ public static final int STATUS_CONFIRMED = 1;
+ public static final int STATUS_CANCELED = 2;
+
+ /**
+ * This is a copy of the attendee status for the owner of this event.
+ * This field is copied here so that we can efficiently filter out
+ * events that are declined without having to look in the Attendees
+ * table.
+ *
+ * <P>Type: INTEGER (int)</P>
+ */
+ public static final String SELF_ATTENDEE_STATUS = "selfAttendeeStatus";
+
+ /**
+ * The comments feed uri.
+ * <P>Type: TEXT</P>
+ */
+ public static final String COMMENTS_URI = "commentsUri";
+
+ /**
+ * The time the event starts
+ * <P>Type: INTEGER (long; millis since epoch)</P>
+ */
+ public static final String DTSTART = "dtstart";
+
+ /**
+ * The time the event ends
+ * <P>Type: INTEGER (long; millis since epoch)</P>
+ */
+ public static final String DTEND = "dtend";
+
+ /**
+ * The duration of the event
+ * <P>Type: TEXT (duration in RFC2445 format)</P>
+ */
+ public static final String DURATION = "duration";
+
+ /**
+ * The timezone for the event.
+ * <P>Type: TEXT
+ */
+ public static final String EVENT_TIMEZONE = "eventTimezone";
+
+ /**
+ * Whether the event lasts all day or not
+ * <P>Type: INTEGER (boolean)</P>
+ */
+ public static final String ALL_DAY = "allDay";
+
+ /**
+ * Visibility for the event.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String VISIBILITY = "visibility";
+
+ public static final int VISIBILITY_DEFAULT = 0;
+ public static final int VISIBILITY_CONFIDENTIAL = 1;
+ public static final int VISIBILITY_PRIVATE = 2;
+ public static final int VISIBILITY_PUBLIC = 3;
+
+ /**
+ * Transparency for the event -- does the event consume time on the calendar?
+ * <P>Type: INTEGER</P>
+ */
+ public static final String TRANSPARENCY = "transparency";
+
+ public static final int TRANSPARENCY_OPAQUE = 0;
+
+ public static final int TRANSPARENCY_TRANSPARENT = 1;
+
+ /**
+ * Whether the event has an alarm or not
+ * <P>Type: INTEGER (boolean)</P>
+ */
+ public static final String HAS_ALARM = "hasAlarm";
+
+ /**
+ * Whether the event has extended properties or not
+ * <P>Type: INTEGER (boolean)</P>
+ */
+ public static final String HAS_EXTENDED_PROPERTIES = "hasExtendedProperties";
+
+ /**
+ * The recurrence rule for the event.
+ * than one.
+ * <P>Type: TEXT</P>
+ */
+ public static final String RRULE = "rrule";
+
+ /**
+ * The recurrence dates for the event.
+ * <P>Type: TEXT</P>
+ */
+ public static final String RDATE = "rdate";
+
+ /**
+ * The recurrence exception rule for the event.
+ * <P>Type: TEXT</P>
+ */
+ public static final String EXRULE = "exrule";
+
+ /**
+ * The recurrence exception dates for the event.
+ * <P>Type: TEXT</P>
+ */
+ public static final String EXDATE = "exdate";
+
+ /**
+ * The original event this event is an exception for
+ * <P>Type: TEXT</P>
+ */
+ public static final String ORIGINAL_EVENT = "originalEvent";
+
+ /**
+ * The time of the original instance time this event is an exception for
+ * <P>Type: INTEGER (long; millis since epoch)</P>
+ */
+ public static final String ORIGINAL_INSTANCE_TIME = "originalInstanceTime";
+
+ /**
+ * The last date this event repeats on, or NULL if it never ends
+ * <P>Type: INTEGER (long; millis since epoch)</P>
+ */
+ public static final String LAST_DATE = "lastDate";
+ }
+
+ /**
+ * Contains one entry per calendar event. Recurring events show up as a single entry.
+ */
+ public static final class Events implements BaseColumns, SyncConstValue,
+ EventsColumns, CalendarsColumns {
+
+ private static final String[] FETCH_ENTRY_COLUMNS =
+ new String[] { Events._SYNC_ACCOUNT, Events._SYNC_ID };
+
+ private static final String[] ATTENDEES_COLUMNS =
+ new String[] { AttendeesColumns.ATTENDEE_NAME,
+ AttendeesColumns.ATTENDEE_EMAIL,
+ AttendeesColumns.ATTENDEE_RELATIONSHIP,
+ AttendeesColumns.ATTENDEE_TYPE,
+ AttendeesColumns.ATTENDEE_STATUS };
+
+ private static CalendarClient sCalendarClient = null;
+
+ public static final Cursor query(ContentResolver cr, String[] projection) {
+ return cr.query(CONTENT_URI, projection, null, null, DEFAULT_SORT_ORDER);
+ }
+
+ public static final Cursor query(ContentResolver cr, String[] projection,
+ String where, String orderBy) {
+ return cr.query(CONTENT_URI, projection, where,
+ null, orderBy == null ? DEFAULT_SORT_ORDER : orderBy);
+ }
+
+ private static String extractValue(ICalendar.Component component,
+ String propertyName) {
+ ICalendar.Property property =
+ component.getFirstProperty(propertyName);
+ if (property != null) {
+ return property.getValue();
+ }
+ return null;
+ }
+
+ public static final Uri insertVEvent(ContentResolver cr,
+ ICalendar.Component event, long calendarId, int status,
+ ContentValues values) {
+
+ // TODO: define VEVENT component names as constants in some
+ // appropriate class (ICalendar.Component?).
+
+ values.clear();
+
+ // title
+ String title = extractValue(event, "SUMMARY");
+ if (TextUtils.isEmpty(title)) {
+ if (Config.LOGD) {
+ Log.d(TAG, "No SUMMARY provided for event. "
+ + "Cannot import.");
+ }
+ return null;
+ }
+ values.put(TITLE, title);
+
+ // status
+ values.put(STATUS, status);
+
+ // description
+ String description = extractValue(event, "DESCRIPTION");
+ if (!TextUtils.isEmpty(description)) {
+ values.put(DESCRIPTION, description);
+ }
+
+ // where
+ String where = extractValue(event, "LOCATION");
+ if (!StringUtils.isEmpty(where)) {
+ values.put(EVENT_LOCATION, where);
+ }
+
+ // Calendar ID
+ values.put(CALENDAR_ID, calendarId);
+
+ boolean timesSet = false;
+
+ // TODO: deal with VALARMs
+
+ // dtstart & dtend
+ Time time = new Time(Time.TIMEZONE_UTC);
+ String dtstart = null;
+ String dtend = null;
+ String duration = null;
+ ICalendar.Property dtstartProp = event.getFirstProperty("DTSTART");
+ // TODO: handle "floating" timezone (no timezone specified).
+ if (dtstartProp != null) {
+ dtstart = dtstartProp.getValue();
+ if (!TextUtils.isEmpty(dtstart)) {
+ ICalendar.Parameter tzidParam =
+ dtstartProp.getFirstParameter("TZID");
+ if (tzidParam != null && tzidParam.value != null) {
+ time.clear(tzidParam.value);
+ }
+ try {
+ time.parse2445(dtstart);
+ } catch (Exception e) {
+ if (Config.LOGD) {
+ Log.d(TAG, "Cannot parse dtstart " + dtstart, e);
+ }
+ return null;
+ }
+ if (time.allDay) {
+ values.put(ALL_DAY, 1);
+ }
+ values.put(DTSTART, time.toMillis(false /* use isDst */));
+ values.put(EVENT_TIMEZONE, time.timezone);
+ }
+
+ ICalendar.Property dtendProp = event.getFirstProperty("DTEND");
+ if (dtendProp != null) {
+ dtend = dtendProp.getValue();
+ if (!TextUtils.isEmpty(dtend)) {
+ // TODO: make sure the timezones are the same for
+ // start, end.
+ try {
+ time.parse2445(dtend);
+ } catch (Exception e) {
+ if (Config.LOGD) {
+ Log.d(TAG, "Cannot parse dtend " + dtend, e);
+ }
+ return null;
+ }
+ values.put(DTEND, time.toMillis(false /* use isDst */));
+ }
+ } else {
+ // look for a duration
+ ICalendar.Property durationProp =
+ event.getFirstProperty("DURATION");
+ if (durationProp != null) {
+ duration = durationProp.getValue();
+ if (!TextUtils.isEmpty(duration)) {
+ // TODO: check that it is valid?
+ values.put(DURATION, duration);
+ }
+ }
+ }
+ }
+ if (TextUtils.isEmpty(dtstart) ||
+ (TextUtils.isEmpty(dtend) && TextUtils.isEmpty(duration))) {
+ if (Config.LOGD) {
+ Log.d(TAG, "No DTSTART or DTEND/DURATION defined.");
+ }
+ return null;
+ }
+
+ // rrule
+ if (!RecurrenceSet.populateContentValues(event, values)) {
+ return null;
+ }
+
+ return cr.insert(CONTENT_URI, values);
+ }
+
+ /**
+ * Returns a singleton instance of the CalendarClient used to fetch entries from the
+ * calendar server.
+ * @param cr The ContentResolver used to lookup the address of the calendar server in the
+ * settings database.
+ * @return The singleton instance of the CalendarClient used to fetch entries from the
+ * calendar server.
+ */
+ private static synchronized CalendarClient getCalendarClient(ContentResolver cr) {
+ if (sCalendarClient == null) {
+ sCalendarClient = new CalendarClient(
+ new AndroidGDataClient(cr),
+ new XmlCalendarGDataParserFactory(new AndroidXmlParserFactory()));
+ }
+ return sCalendarClient;
+ }
+
+ /**
+ * Extracts the attendees information out of event and adds it to a new ArrayList of columns
+ * within the supplied ArrayList of rows. These rows are expected to be used within an
+ * {@link ArrayListCursor}.
+ */
+ private static final void extractAttendeesIntoArrayList(EventEntry event,
+ ArrayList<ArrayList> rows) {
+ Log.d(TAG, "EVENT: " + event.toString());
+ Vector<Who> attendees = (Vector<Who>) event.getAttendees();
+
+ int numAttendees = attendees == null ? 0 : attendees.size();
+
+ for (int i = 0; i < numAttendees; ++i) {
+ Who attendee = attendees.elementAt(i);
+ ArrayList row = new ArrayList();
+ row.add(attendee.getValue());
+ row.add(attendee.getEmail());
+ row.add(attendee.getRelationship());
+ row.add(attendee.getType());
+ row.add(attendee.getStatus());
+ rows.add(row);
+ }
+ }
+
+ /**
+ * The content:// style URL for this table
+ */
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://calendar/events");
+
+ public static final Uri DELETED_CONTENT_URI =
+ Uri.parse("content://calendar/deleted_events");
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = "";
+ }
+
+ /**
+ * Contains one entry per calendar event instance. Recurring events show up every time
+ * they occur.
+ */
+ public static final class Instances implements BaseColumns, EventsColumns, CalendarsColumns {
+
+ public static final Cursor query(ContentResolver cr, String[] projection,
+ long begin, long end) {
+ Uri.Builder builder = CONTENT_URI.buildUpon();
+ ContentUris.appendId(builder, begin);
+ ContentUris.appendId(builder, end);
+ return cr.query(builder.build(), projection, Calendars.SELECTED + "=1",
+ null, DEFAULT_SORT_ORDER);
+ }
+
+ public static final Cursor query(ContentResolver cr, String[] projection,
+ long begin, long end, String where, String orderBy) {
+ Uri.Builder builder = CONTENT_URI.buildUpon();
+ ContentUris.appendId(builder, begin);
+ ContentUris.appendId(builder, end);
+ if (TextUtils.isEmpty(where)) {
+ where = Calendars.SELECTED + "=1";
+ } else {
+ where = "(" + where + ") AND " + Calendars.SELECTED + "=1";
+ }
+ return cr.query(builder.build(), projection, where,
+ null, orderBy == null ? DEFAULT_SORT_ORDER : orderBy);
+ }
+
+ /**
+ * The content:// style URL for this table
+ */
+ public static final Uri CONTENT_URI = Uri.parse("content://calendar/instances/when");
+
+ /**
+ * The default sort order for this table.
+ */
+ public static final String DEFAULT_SORT_ORDER = "begin ASC";
+
+ /**
+ * The sort order is: events with an earlier start time occur
+ * first and if the start times are the same, then events with
+ * a later end time occur first. The later end time is ordered
+ * first so that long-running events in the calendar views appear
+ * first. If the start and end times of two events are
+ * the same then we sort alphabetically on the title. This isn't
+ * required for correctness, it just adds a nice touch.
+ */
+ public static final String SORT_CALENDAR_VIEW = "begin ASC, end DESC, title ASC";
+
+ /**
+ * The beginning time of the instance, in UTC milliseconds
+ * <P>Type: INTEGER (long; millis since epoch)</P>
+ */
+ public static final String BEGIN = "begin";
+
+ /**
+ * The ending time of the instance, in UTC milliseconds
+ * <P>Type: INTEGER (long; millis since epoch)</P>
+ */
+ public static final String END = "end";
+
+ /**
+ * The event for this instance
+ * <P>Type: INTEGER (long, foreign key to the Events table)</P>
+ */
+ public static final String EVENT_ID = "event_id";
+
+ /**
+ * The Julian start day of the instance, relative to the local timezone
+ * <P>Type: INTEGER (int)</P>
+ */
+ public static final String START_DAY = "startDay";
+
+ /**
+ * The Julian end day of the instance, relative to the local timezone
+ * <P>Type: INTEGER (int)</P>
+ */
+ public static final String END_DAY = "endDay";
+
+ /**
+ * The start minute of the instance measured from midnight in the
+ * local timezone.
+ * <P>Type: INTEGER (int)</P>
+ */
+ public static final String START_MINUTE = "startMinute";
+
+ /**
+ * The end minute of the instance measured from midnight in the
+ * local timezone.
+ * <P>Type: INTEGER (int)</P>
+ */
+ public static final String END_MINUTE = "endMinute";
+ }
+
+ /**
+ * A few Calendar globals are needed in the CalendarProvider for expanding
+ * the Instances table and these are all stored in the first (and only)
+ * row of the CalendarMetaData table.
+ */
+ public interface CalendarMetaDataColumns {
+ /**
+ * The local timezone that was used for precomputing the fields
+ * in the Instances table.
+ */
+ public static final String LOCAL_TIMEZONE = "localTimezone";
+
+ /**
+ * The minimum time used in expanding the Instances table,
+ * in UTC milliseconds.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String MIN_INSTANCE = "minInstance";
+
+ /**
+ * The maximum time used in expanding the Instances table,
+ * in UTC milliseconds.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String MAX_INSTANCE = "maxInstance";
+
+ /**
+ * The minimum Julian day in the BusyBits table.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String MIN_BUSYBITS = "minBusyBits";
+
+ /**
+ * The maximum Julian day in the BusyBits table.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String MAX_BUSYBITS = "maxBusyBits";
+ }
+
+ public static final class CalendarMetaData implements CalendarMetaDataColumns {
+ }
+
+ public interface BusyBitsColumns {
+ /**
+ * The Julian day number.
+ * <P>Type: INTEGER (int)</P>
+ */
+ public static final String DAY = "day";
+
+ /**
+ * The 24 bits representing the 24 1-hour time slots in a day.
+ * If an event in the Instances table overlaps part of a 1-hour
+ * time slot then the corresponding bit is set. The first time slot
+ * (12am to 1am) is bit 0. The last time slot (11pm to midnight)
+ * is bit 23.
+ * <P>Type: INTEGER (int)</P>
+ */
+ public static final String BUSYBITS = "busyBits";
+
+ /**
+ * The number of all-day events that occur on this day.
+ * <P>Type: INTEGER (int)</P>
+ */
+ public static final String ALL_DAY_COUNT = "allDayCount";
+ }
+
+ public static final class BusyBits implements BusyBitsColumns {
+ public static final Uri CONTENT_URI = Uri.parse("content://calendar/busybits/when");
+
+ public static final String[] PROJECTION = { DAY, BUSYBITS, ALL_DAY_COUNT };
+
+ // The number of minutes represented by one busy bit
+ public static final int MINUTES_PER_BUSY_INTERVAL = 60;
+
+ // The number of intervals in a day
+ public static final int INTERVALS_PER_DAY = 24 * 60 / MINUTES_PER_BUSY_INTERVAL;
+
+ /**
+ * Retrieves the busy bits for the Julian days starting at "startDay"
+ * for "numDays".
+ *
+ * @param cr the ContentResolver
+ * @param startDay the first Julian day in the range
+ * @param numDays the number of days to load (must be at least 1)
+ * @return a database cursor
+ */
+ public static final Cursor query(ContentResolver cr, int startDay, int numDays) {
+ if (numDays < 1) {
+ return null;
+ }
+ int endDay = startDay + numDays - 1;
+ Uri.Builder builder = CONTENT_URI.buildUpon();
+ ContentUris.appendId(builder, startDay);
+ ContentUris.appendId(builder, endDay);
+ return cr.query(builder.build(), PROJECTION, null /* selection */,
+ null /* selection args */, DAY);
+ }
+ }
+
+ public interface RemindersColumns {
+ /**
+ * The event the reminder belongs to
+ * <P>Type: INTEGER (foreign key to the Events table)</P>
+ */
+ public static final String EVENT_ID = "event_id";
+
+ /**
+ * The minutes prior to the event that the alarm should ring. -1
+ * specifies that we should use the default value for the system.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String MINUTES = "minutes";
+
+ public static final int MINUTES_DEFAULT = -1;
+
+ /**
+ * The alarm method, as set on the server. DEFAULT, ALERT, EMAIL, and
+ * SMS are possible values; the device will only process DEFAULT and
+ * ALERT reminders (the other types are simply stored so we can send the
+ * same reminder info back to the server when we make changes).
+ */
+ public static final String METHOD = "method";
+
+ public static final int METHOD_DEFAULT = 0;
+ public static final int METHOD_ALERT = 1;
+ public static final int METHOD_EMAIL = 2;
+ public static final int METHOD_SMS = 3;
+ }
+
+ public static final class Reminders implements BaseColumns, RemindersColumns, EventsColumns {
+ public static final String TABLE_NAME = "Reminders";
+ public static final Uri CONTENT_URI = Uri.parse("content://calendar/reminders");
+ }
+
+ public interface CalendarAlertsColumns {
+ /**
+ * The event that the alert belongs to
+ * <P>Type: INTEGER (foreign key to the Events table)</P>
+ */
+ public static final String EVENT_ID = "event_id";
+
+ /**
+ * The start time of the event, in UTC
+ * <P>Type: INTEGER (long; millis since epoch)</P>
+ */
+ public static final String BEGIN = "begin";
+
+ /**
+ * The end time of the event, in UTC
+ * <P>Type: INTEGER (long; millis since epoch)</P>
+ */
+ public static final String END = "end";
+
+ /**
+ * The alarm time of the event, in UTC
+ * <P>Type: INTEGER (long; millis since epoch)</P>
+ */
+ public static final String ALARM_TIME = "alarmTime";
+
+ /**
+ * The state of this alert. It starts out as SCHEDULED, then when
+ * the alarm goes off, it changes to FIRED, and then when the user
+ * sees and dismisses the alarm it changes to DISMISSED.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String STATE = "state";
+
+ public static final int SCHEDULED = 0;
+ public static final int FIRED = 1;
+ public static final int DISMISSED = 2;
+
+ /**
+ * The number of minutes that this alarm precedes the start time
+ * <P>Type: INTEGER </P>
+ */
+ public static final String MINUTES = "minutes";
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = "alarmTime ASC,begin ASC,title ASC";
+ }
+
+ public static final class CalendarAlerts implements BaseColumns,
+ CalendarAlertsColumns, EventsColumns, CalendarsColumns {
+ public static final String TABLE_NAME = "CalendarAlerts";
+ public static final Uri CONTENT_URI = Uri.parse("content://calendar/calendar_alerts");
+
+ /**
+ * This URI is for grouping the query results by event_id and begin
+ * time. This will return one result per instance of an event. So
+ * events with multiple alarms will appear just once, but multiple
+ * instances of a repeating event will show up multiple times.
+ */
+ public static final Uri CONTENT_URI_BY_INSTANCE =
+ Uri.parse("content://calendar/calendar_alerts/by_instance");
+
+ public static final Uri insert(ContentResolver cr, long eventId,
+ long begin, long end, long alarmTime, int minutes) {
+ ContentValues values = new ContentValues();
+ values.put(CalendarAlerts.EVENT_ID, eventId);
+ values.put(CalendarAlerts.BEGIN, begin);
+ values.put(CalendarAlerts.END, end);
+ values.put(CalendarAlerts.ALARM_TIME, alarmTime);
+ values.put(CalendarAlerts.STATE, SCHEDULED);
+ values.put(CalendarAlerts.MINUTES, minutes);
+ return cr.insert(CONTENT_URI, values);
+ }
+
+ public static final Cursor query(ContentResolver cr, String[] projection,
+ String selection, String[] selectionArgs) {
+ return cr.query(CONTENT_URI, projection, selection, selectionArgs,
+ DEFAULT_SORT_ORDER);
+ }
+
+ /**
+ * Finds the next alarm after (or equal to) the given time and returns
+ * the time of that alarm or -1 if no such alarm exists.
+ *
+ * @param cr the ContentResolver
+ * @param millis the time in UTC milliseconds
+ * @return the next alarm time greater than or equal to "millis", or -1
+ * if no such alarm exists.
+ */
+ public static final long findNextAlarmTime(ContentResolver cr, long millis) {
+ String selection = ALARM_TIME + ">=" + millis;
+ // TODO: construct an explicit SQL query so that we can add
+ // "LIMIT 1" to the end and get just one result.
+ String[] projection = new String[] { ALARM_TIME };
+ Cursor cursor = query(cr, projection, selection, null);
+ long alarmTime = -1;
+ try {
+ if (cursor != null && cursor.moveToFirst()) {
+ alarmTime = cursor.getLong(0);
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ return alarmTime;
+ }
+
+ /**
+ * Searches the CalendarAlerts table for alarms that should have fired
+ * but have not and then reschedules them. This method can be called
+ * at boot time to restore alarms that may have been lost due to a
+ * phone reboot.
+ *
+ * @param cr the ContentResolver
+ * @param context the Context
+ * @param manager the AlarmManager
+ */
+ public static final void rescheduleMissedAlarms(ContentResolver cr,
+ Context context, AlarmManager manager) {
+ // Get all the alerts that have been scheduled but have not fired
+ // and should have fired by now and are not too old.
+ long now = System.currentTimeMillis();
+ long ancient = now - 24 * DateUtils.HOUR_IN_MILLIS;
+ String selection = CalendarAlerts.STATE + "=" + CalendarAlerts.SCHEDULED
+ + " AND " + CalendarAlerts.ALARM_TIME + "<" + now
+ + " AND " + CalendarAlerts.ALARM_TIME + ">" + ancient
+ + " AND " + CalendarAlerts.END + ">=" + now;
+ String[] projection = new String[] {
+ _ID,
+ BEGIN,
+ END,
+ ALARM_TIME,
+ };
+ Cursor cursor = CalendarAlerts.query(cr, projection, selection, null);
+ if (cursor == null) {
+ return;
+ }
+
+ try {
+ while (cursor.moveToNext()) {
+ long id = cursor.getLong(0);
+ long begin = cursor.getLong(1);
+ long end = cursor.getLong(2);
+ long alarmTime = cursor.getLong(3);
+ Uri uri = ContentUris.withAppendedId(CONTENT_URI, id);
+ Intent intent = new Intent(android.provider.Calendar.EVENT_REMINDER_ACTION);
+ intent.setData(uri);
+ intent.putExtra(android.provider.Calendar.EVENT_BEGIN_TIME, begin);
+ intent.putExtra(android.provider.Calendar.EVENT_END_TIME, end);
+ PendingIntent sender = PendingIntent.getBroadcast(context,
+ 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
+ manager.set(AlarmManager.RTC_WAKEUP, alarmTime, sender);
+ }
+ } finally {
+ cursor.close();
+ }
+
+ }
+
+ /**
+ * Searches for an entry in the CalendarAlerts table that matches
+ * the given event id, begin time and alarm time. If one is found
+ * then this alarm already exists and this method returns true.
+ *
+ * @param cr the ContentResolver
+ * @param eventId the event id to match
+ * @param begin the start time of the event in UTC millis
+ * @param alarmTime the alarm time of the event in UTC millis
+ * @return true if there is already an alarm for the given event
+ * with the same start time and alarm time.
+ */
+ public static final boolean alarmExists(ContentResolver cr, long eventId,
+ long begin, long alarmTime) {
+ String selection = CalendarAlerts.EVENT_ID + "=" + eventId
+ + " AND " + CalendarAlerts.BEGIN + "=" + begin
+ + " AND " + CalendarAlerts.ALARM_TIME + "=" + alarmTime;
+ // TODO: construct an explicit SQL query so that we can add
+ // "LIMIT 1" to the end and get just one result.
+ String[] projection = new String[] { CalendarAlerts.ALARM_TIME };
+ Cursor cursor = query(cr, projection, selection, null);
+ boolean found = false;
+ try {
+ if (cursor != null && cursor.getCount() > 0) {
+ found = true;
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ return found;
+ }
+ }
+
+ public interface ExtendedPropertiesColumns {
+ /**
+ * The event the extended property belongs to
+ * <P>Type: INTEGER (foreign key to the Events table)</P>
+ */
+ public static final String EVENT_ID = "event_id";
+
+ /**
+ * The name of the extended property. This is a uri of the form
+ * {scheme}#{local-name} convention.
+ * <P>Type: TEXT</P>
+ */
+ public static final String NAME = "name";
+
+ /**
+ * The value of the extended property.
+ * <P>Type: TEXT</P>
+ */
+ public static final String VALUE = "value";
+ }
+
+ public static final class ExtendedProperties implements BaseColumns,
+ ExtendedPropertiesColumns, EventsColumns {
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://calendar/extendedproperties");
+
+ // TODO: fill out this class when we actually start utilizing extendedproperties
+ // in the calendar application.
+ }
+}
diff --git a/core/java/android/provider/CallLog.java b/core/java/android/provider/CallLog.java
new file mode 100644
index 0000000..10fe3f5
--- /dev/null
+++ b/core/java/android/provider/CallLog.java
@@ -0,0 +1,190 @@
+/*
+ * 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.provider;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.net.Uri;
+import android.provider.Contacts.People;
+import com.android.internal.telephony.CallerInfo;
+import android.text.TextUtils;
+import android.util.Log;
+
+/**
+ * The CallLog provider contains information about placed and received calls.
+ */
+public class CallLog {
+ public static final String AUTHORITY = "call_log";
+
+ /**
+ * The content:// style URL for this provider
+ */
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://" + AUTHORITY);
+
+ /**
+ * Contains the recent calls.
+ */
+ public static class Calls implements BaseColumns {
+ /**
+ * The content:// style URL for this table
+ */
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://call_log/calls");
+
+ /**
+ * The content:// style URL for filtering this table on phone numbers
+ */
+ public static final Uri CONTENT_FILTER_URI =
+ Uri.parse("content://call_log/calls/filter");
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = "date DESC";
+
+ /**
+ * The MIME type of {@link #CONTENT_URI} and {@link #CONTENT_FILTER_URI}
+ * providing a directory of calls.
+ */
+ public static final String CONTENT_TYPE = "vnd.android.cursor.dir/calls";
+
+ /**
+ * The MIME type of a {@link #CONTENT_URI} sub-directory of a single
+ * call.
+ */
+ public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/calls";
+
+ /**
+ * The type of the the phone number.
+ * <P>Type: INTEGER (int)</P>
+ */
+ public static final String TYPE = "type";
+
+ public static final int INCOMING_TYPE = 1;
+ public static final int OUTGOING_TYPE = 2;
+ public static final int MISSED_TYPE = 3;
+
+ /**
+ * The phone number as the user entered it.
+ * <P>Type: TEXT</P>
+ */
+ public static final String NUMBER = "number";
+
+ /**
+ * The date the call occured, in milliseconds since the epoch
+ * <P>Type: INTEGER (long)</P>
+ */
+ public static final String DATE = "date";
+
+ /**
+ * The duration of the call in seconds
+ * <P>Type: INTEGER (long)</P>
+ */
+ public static final String DURATION = "duration";
+
+ /**
+ * Whether or not the call has been acknowledged
+ * <P>Type: INTEGER (boolean)</P>
+ */
+ public static final String NEW = "new";
+
+ /**
+ * The cached name associated with the phone number, if it exists.
+ * This value is not guaranteed to be current, if the contact information
+ * associated with this number has changed.
+ * <P>Type: TEXT</P>
+ */
+ public static final String CACHED_NAME = "name";
+
+ /**
+ * The cached number type (Home, Work, etc) associated with the
+ * phone number, if it exists.
+ * This value is not guaranteed to be current, if the contact information
+ * associated with this number has changed.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String CACHED_NUMBER_TYPE = "numbertype";
+
+ /**
+ * The cached number label, for a custom number type, associated with the
+ * phone number, if it exists.
+ * This value is not guaranteed to be current, if the contact information
+ * associated with this number has changed.
+ * <P>Type: TEXT</P>
+ */
+ public static final String CACHED_NUMBER_LABEL = "numberlabel";
+
+ /**
+ * Adds a call to the call log.
+ *
+ * @param ci the CallerInfo object to get the target contact from. Can be null
+ * if the contact is unknown.
+ * @param context the context used to get the ContentResolver
+ * @param number the phone number to be added to the calls db
+ * @param isPrivateNumber <code>true</code> if the call was marked as private by the network
+ * @param callType enumerated values for "incoming", "outgoing", or "missed"
+ * @param start time stamp for the call in milliseconds
+ * @param duration call duration in seconds
+ *
+ * {@hide}
+ */
+ public static Uri addCall(CallerInfo ci, Context context, String number,
+ boolean isPrivateNumber, int callType, long start, int duration) {
+ final ContentResolver resolver = context.getContentResolver();
+
+ if (TextUtils.isEmpty(number)) {
+ if (isPrivateNumber) {
+ number = CallerInfo.PRIVATE_NUMBER;
+ } else {
+ number = CallerInfo.UNKNOWN_NUMBER;
+ }
+ }
+
+ ContentValues values = new ContentValues(5);
+
+ values.put(NUMBER, number);
+ values.put(TYPE, Integer.valueOf(callType));
+ values.put(DATE, Long.valueOf(start));
+ values.put(DURATION, Long.valueOf(duration));
+ values.put(NEW, Integer.valueOf(1));
+ if (ci != null) {
+ values.put(CACHED_NAME, ci.name);
+ values.put(CACHED_NUMBER_TYPE, ci.numberType);
+ values.put(CACHED_NUMBER_LABEL, ci.numberLabel);
+ }
+
+ if ((ci != null) && (ci.person_id > 0)) {
+ People.markAsContacted(resolver, ci.person_id);
+ }
+
+ Uri result = resolver.insert(CONTENT_URI, values);
+
+ removeExpiredEntries(context);
+
+ return result;
+ }
+
+ private static void removeExpiredEntries(Context context) {
+ final ContentResolver resolver = context.getContentResolver();
+ resolver.delete(CONTENT_URI, "_id IN " +
+ "(SELECT _id FROM calls ORDER BY " + DEFAULT_SORT_ORDER
+ + " LIMIT -1 OFFSET 500)", null);
+ }
+ }
+}
diff --git a/core/java/android/provider/Checkin.java b/core/java/android/provider/Checkin.java
new file mode 100644
index 0000000..5b79482
--- /dev/null
+++ b/core/java/android/provider/Checkin.java
@@ -0,0 +1,290 @@
+/*
+ * 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.provider;
+
+import org.apache.commons.codec.binary.Base64;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.database.SQLException;
+import android.net.Uri;
+import android.os.SystemClock;
+import android.server.data.CrashData;
+import android.util.Log;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+
+/**
+ * Contract class for {@link android.server.checkin.CheckinProvider}.
+ * Describes the exposed database schema, and offers methods to add
+ * events and statistics to be uploaded.
+ *
+ * @hide
+ */
+public final class Checkin {
+ public static final String AUTHORITY = "android.server.checkin";
+
+ /**
+ * The events table is a log of important timestamped occurrences.
+ * Each event has a type tag and an optional string value.
+ * If too many events are added before they can be reported, the
+ * content provider will erase older events to limit the table size.
+ */
+ public interface Events extends BaseColumns {
+ public static final String TABLE_NAME = "events";
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://" + AUTHORITY + "/" + TABLE_NAME);
+
+ public static final String TAG = "tag"; // TEXT
+ public static final String VALUE = "value"; // TEXT
+ public static final String DATE = "date"; // INTEGER
+
+ /** Valid tag values. Extend as necessary for your needs. */
+ public enum Tag {
+ BROWSER_BUG_REPORT,
+ CARRIER_BUG_REPORT,
+ CHECKIN_FAILURE,
+ CHECKIN_SUCCESS,
+ FOTA_BEGIN,
+ FOTA_FAILURE,
+ FOTA_INSTALL,
+ FOTA_PROMPT,
+ FOTA_PROMPT_ACCEPT,
+ FOTA_PROMPT_REJECT,
+ FOTA_PROMPT_SKIPPED,
+ GSERVICES_ERROR,
+ GSERVICES_UPDATE,
+ LOGIN_SERVICE_ACCOUNT_TRIED,
+ LOGIN_SERVICE_ACCOUNT_SAVED,
+ LOGIN_SERVICE_AUTHENTICATE,
+ LOGIN_SERVICE_CAPTCHA_ANSWERED,
+ LOGIN_SERVICE_CAPTCHA_SHOWN,
+ LOGIN_SERVICE_PASSWORD_ENTERED,
+ LOGIN_SERVICE_SWITCH_GOOGLE_MAIL,
+ NETWORK_DOWN,
+ NETWORK_UP,
+ PHONE_UI,
+ RADIO_BUG_REPORT,
+ SETUP_COMPLETED,
+ SETUP_INITIATED,
+ SETUP_IO_ERROR,
+ SETUP_NETWORK_ERROR,
+ SETUP_REQUIRED_CAPTCHA,
+ SETUP_RETRIES_EXHAUSTED,
+ SETUP_SERVER_ERROR,
+ SETUP_SERVER_TIMEOUT,
+ SYSTEM_APP_NOT_RESPONDING,
+ SYSTEM_BOOT,
+ SYSTEM_LAST_KMSG,
+ SYSTEM_RECOVERY_LOG,
+ SYSTEM_RESTART,
+ SYSTEM_SERVICE_LOOPING,
+ SYSTEM_TOMBSTONE,
+ TEST,
+ NETWORK_RX_MOBILE,
+ NETWORK_TX_MOBILE,
+ }
+ }
+
+ /**
+ * The stats table is a list of counter values indexed by a tag name.
+ * Each statistic has a count and sum fields, so it can track averages.
+ * When multiple statistics are inserted with the same tag, the count
+ * and sum fields are added together into a single entry in the database.
+ */
+ public interface Stats extends BaseColumns {
+ public static final String TABLE_NAME = "stats";
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://" + AUTHORITY + "/" + TABLE_NAME);
+
+ public static final String TAG = "tag"; // TEXT UNIQUE
+ public static final String COUNT = "count"; // INTEGER
+ public static final String SUM = "sum"; // REAL
+
+ /** Valid tag values. Extend as necessary for your needs. */
+ public enum Tag {
+ CRASHES_REPORTED,
+ CRASHES_TRUNCATED,
+ ELAPSED_REALTIME_SEC,
+ ELAPSED_UPTIME_SEC,
+ HTTP_STATUS,
+ PHONE_GSM_REGISTERED,
+ PHONE_GPRS_ATTEMPTED,
+ PHONE_GPRS_CONNECTED,
+ PHONE_RADIO_RESETS,
+ TEST,
+ NETWORK_RX_MOBILE,
+ NETWORK_TX_MOBILE,
+ }
+ }
+
+ /**
+ * The properties table is a set of tagged values sent with every checkin.
+ * Unlike statistics or events, they are not cleared after being uploaded.
+ * Multiple properties inserted with the same tag overwrite each other.
+ */
+ public interface Properties extends BaseColumns {
+ public static final String TABLE_NAME = "properties";
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://" + AUTHORITY + "/" + TABLE_NAME);
+
+ public static final String TAG = "tag"; // TEXT UNIQUE
+ public static final String VALUE = "value"; // TEXT
+
+ /** Valid tag values, to be extended as necessary. */
+ public enum Tag {
+ DESIRED_BUILD,
+ MARKET_CHECKIN,
+ }
+ }
+
+ /**
+ * The crashes table is a log of crash reports, kept separate from the
+ * general event log because crashes are large, important, and bursty.
+ * Like the events table, the crashes table is pruned on insert.
+ */
+ public interface Crashes extends BaseColumns {
+ public static final String TABLE_NAME = "crashes";
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://" + AUTHORITY + "/" + TABLE_NAME);
+
+ // TODO: one or both of these should be a file attachment, not a column
+ public static final String DATA = "data"; // TEXT
+ public static final String LOGS = "logs"; // TEXT
+ }
+
+ /**
+ * Intents with this action cause a checkin attempt. Normally triggered by
+ * a periodic alarm, these may be sent directly to force immediate checkin.
+ */
+ public interface TriggerIntent {
+ public static final String ACTION = "android.server.checkin.CHECKIN";
+
+ // The category is used for GTalk service messages
+ public static final String CATEGORY = "android.server.checkin.CHECKIN";
+ }
+
+ private static final String TAG = "Checkin";
+
+ /**
+ * Helper function to log an event to the database.
+ *
+ * @param resolver from {@link android.content.Context#getContentResolver}
+ * @param tag identifying the type of event being recorded
+ * @param value associated with event, if any
+ * @return URI of the event that was added
+ */
+ static public Uri logEvent(ContentResolver resolver,
+ Events.Tag tag, String value) {
+ try {
+ // Don't specify the date column; the content provider will add that.
+ ContentValues values = new ContentValues();
+ values.put(Events.TAG, tag.toString());
+ if (value != null) values.put(Events.VALUE, value);
+ return resolver.insert(Events.CONTENT_URI, values);
+ } catch (SQLException e) {
+ Log.e(TAG, "Can't log event: " + tag, e); // Database errors are not fatal.
+ return null;
+ }
+ }
+
+ /**
+ * Helper function to update statistics in the database.
+ * Note that multiple updates to the same tag will be combined.
+ *
+ * @param tag identifying what is being observed
+ * @param count of occurrences
+ * @param sum of some value over these occurrences
+ * @return URI of the statistic that was returned
+ */
+ static public Uri updateStats(ContentResolver resolver,
+ Stats.Tag tag, int count, double sum) {
+ try {
+ ContentValues values = new ContentValues();
+ values.put(Stats.TAG, tag.toString());
+ if (count != 0) values.put(Stats.COUNT, count);
+ if (sum != 0.0) values.put(Stats.SUM, sum);
+ return resolver.insert(Stats.CONTENT_URI, values);
+ } catch (SQLException e) {
+ Log.e(TAG, "Can't update stat: " + tag, e); // Database errors are not fatal.
+ return null;
+ }
+ }
+
+ /** Minimum time to wait after a crash failure before trying again. */
+ static private final long MIN_CRASH_FAILURE_RETRY = 10000; // 10 seconds
+
+ /** {@link SystemClock#elapsedRealtime} of the last time a crash report failed. */
+ static private volatile long sLastCrashFailureRealtime = -MIN_CRASH_FAILURE_RETRY;
+
+ /**
+ * Helper function to report a crash.
+ *
+ * @param resolver from {@link android.content.Context#getContentResolver}
+ * @param crash data from {@link android.server.data.CrashData}
+ * @return URI of the crash report that was added
+ */
+ static public Uri reportCrash(ContentResolver resolver, byte[] crash) {
+ try {
+ // If we are in a situation where crash reports fail (such as a full disk),
+ // it's important that we don't get into a loop trying to report failures.
+ // So discard all crash reports for a few seconds after reporting fails.
+ long realtime = SystemClock.elapsedRealtime();
+ if (realtime - sLastCrashFailureRealtime < MIN_CRASH_FAILURE_RETRY) {
+ Log.e(TAG, "Crash logging skipped, too soon after logging failure");
+ return null;
+ }
+
+ // HACK: we don't support BLOB values, so base64 encode it.
+ byte[] encoded = Base64.encodeBase64(crash);
+ ContentValues values = new ContentValues();
+ values.put(Crashes.DATA, new String(encoded));
+ Uri uri = resolver.insert(Crashes.CONTENT_URI, values);
+ if (uri == null) {
+ Log.e(TAG, "Error reporting crash");
+ sLastCrashFailureRealtime = SystemClock.elapsedRealtime();
+ }
+ return uri;
+ } catch (Throwable t) {
+ // To avoid an infinite crash-reporting loop, swallow all errors and exceptions.
+ Log.e(TAG, "Error reporting crash: " + t);
+ sLastCrashFailureRealtime = SystemClock.elapsedRealtime();
+ return null;
+ }
+ }
+
+ /**
+ * Report a crash in CrashData format.
+ *
+ * @param resolver from {@link android.content.Context#getContentResolver}
+ * @param crash data to report
+ * @return URI of the crash report that was added
+ */
+ static public Uri reportCrash(ContentResolver resolver, CrashData crash) {
+ try {
+ ByteArrayOutputStream data = new ByteArrayOutputStream();
+ crash.write(new DataOutputStream(data));
+ return reportCrash(resolver, data.toByteArray());
+ } catch (Throwable t) {
+ // Swallow all errors and exceptions when writing crash report
+ Log.e(TAG, "Error writing crash: " + t);
+ return null;
+ }
+ }
+}
+
diff --git a/core/java/android/provider/Contacts.java b/core/java/android/provider/Contacts.java
new file mode 100644
index 0000000..91b1853
--- /dev/null
+++ b/core/java/android/provider/Contacts.java
@@ -0,0 +1,1606 @@
+/*
+ * 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.provider;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.Log;
+import android.widget.ImageView;
+
+import com.android.internal.R;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+/**
+ * The Contacts provider stores all information about contacts.
+ */
+public class Contacts {
+ private static final String TAG = "Contacts";
+
+ public static final String AUTHORITY = "contacts";
+
+ /**
+ * The content:// style URL for this provider
+ */
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://" + AUTHORITY);
+
+ /** Signifies an email address row that is stored in the ContactMethods table */
+ public static final int KIND_EMAIL = 1;
+ /** Signifies a postal address row that is stored in the ContactMethods table */
+ public static final int KIND_POSTAL = 2;
+ /** Signifies an IM address row that is stored in the ContactMethods table */
+ public static final int KIND_IM = 3;
+ /** Signifies an Organization row that is stored in the Organizations table */
+ public static final int KIND_ORGANIZATION = 4;
+ /** Signifies an Phone row that is stored in the Phones table */
+ public static final int KIND_PHONE = 5;
+
+ /**
+ * no public constructor since this is a utility class
+ */
+ private Contacts() {}
+
+ /**
+ * Columns from the Settings table that other columns join into themselves.
+ */
+ public interface SettingsColumns {
+ /**
+ * The _SYNC_ACCOUNT to which this setting corresponds. This may be null.
+ * <P>Type: TEXT</P>
+ */
+ public static final String _SYNC_ACCOUNT = "_sync_account";
+
+ /**
+ * The key of this setting.
+ * <P>Type: TEXT</P>
+ */
+ public static final String KEY = "key";
+
+ /**
+ * The value of this setting.
+ * <P>Type: TEXT</P>
+ */
+ public static final String VALUE = "value";
+ }
+
+ /**
+ * The settings over all of the people
+ */
+ public static final class Settings implements BaseColumns, SettingsColumns {
+ /**
+ * no public constructor since this is a utility class
+ */
+ private Settings() {}
+
+ /**
+ * The content:// style URL for this table
+ */
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://contacts/settings");
+
+ /**
+ * The directory twig for this sub-table
+ */
+ public static final String CONTENT_DIRECTORY = "settings";
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = "key ASC";
+
+ /**
+ * A setting that is used to indicate if we should sync down all groups for the
+ * specified account. For this setting the _SYNC_ACCOUNT column must be set.
+ * If this isn't set then we will only sync the groups whose SHOULD_SYNC column
+ * is set to true.
+ * <p>
+ * This is a boolean setting. It is true if it is set and it is anything other than the
+ * emptry string or "0".
+ */
+ public static final String SYNC_EVERYTHING = "syncEverything";
+
+ public static String getSetting(ContentResolver cr, String account, String key) {
+ // For now we only support a single account and the UI doesn't know what
+ // the account name is, so we're using a global setting for SYNC_EVERYTHING.
+ // Some day when we add multiple accounts to the UI this should honor the account
+ // that was asked for.
+ String selectString;
+ String[] selectArgs;
+ if (false) {
+ selectString = (account == null)
+ ? "_sync_account is null AND key=?"
+ : "_sync_account=? AND key=?";
+ selectArgs = (account == null)
+ ? new String[]{key}
+ : new String[]{account, key};
+ } else {
+ selectString = "key=?";
+ selectArgs = new String[] {key};
+ }
+ Cursor cursor = cr.query(Settings.CONTENT_URI, new String[]{VALUE},
+ selectString, selectArgs, null);
+ try {
+ if (!cursor.moveToNext()) return null;
+ return cursor.getString(0);
+ } finally {
+ cursor.close();
+ }
+ }
+
+ public static void setSetting(ContentResolver cr, String account, String key,
+ String value) {
+ ContentValues values = new ContentValues();
+ // For now we only support a single account and the UI doesn't know what
+ // the account name is, so we're using a global setting for SYNC_EVERYTHING.
+ // Some day when we add multiple accounts to the UI this should honor the account
+ // that was asked for.
+ //values.put(_SYNC_ACCOUNT, account);
+ values.put(KEY, key);
+ values.put(VALUE, value);
+ cr.update(Settings.CONTENT_URI, values, null, null);
+ }
+ }
+
+ /**
+ * Columns from the People table that other tables join into themselves.
+ */
+ public interface PeopleColumns {
+ /**
+ * The persons name.
+ * <P>Type: TEXT</P>
+ */
+ public static final String NAME = "name";
+
+ /**
+ * The display name. If name is not null name, else if number is not null number,
+ * else if email is not null email.
+ * <P>Type: TEXT</P>
+ */
+ public static final String DISPLAY_NAME = "display_name";
+
+ /**
+ * Notes about the person.
+ * <P>Type: TEXT</P>
+ */
+ public static final String NOTES = "notes";
+
+ /**
+ * The number of times a person has been contacted
+ * <P>Type: INTEGER</P>
+ */
+ public static final String TIMES_CONTACTED = "times_contacted";
+
+ /**
+ * The last time a person was contacted.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String LAST_TIME_CONTACTED = "last_time_contacted";
+
+ /**
+ * A custom ringtone associated with a person. Not always present.
+ * <P>Type: TEXT (URI to the ringtone)</P>
+ */
+ public static final String CUSTOM_RINGTONE = "custom_ringtone";
+
+ /**
+ * Whether the person should always be sent to voicemail. Not always
+ * present.
+ * <P>Type: INTEGER (0 for false, 1 for true)</P>
+ */
+ public static final String SEND_TO_VOICEMAIL = "send_to_voicemail";
+
+ /**
+ * Is the contact starred?
+ * <P>Type: INTEGER (boolean)</P>
+ */
+ public static final String STARRED = "starred";
+
+ /**
+ * The server version of the photo
+ * <P>Type: TEXT (the version number portion of the photo URI)</P>
+ */
+ public static final String PHOTO_VERSION = "photo_version";
+ }
+
+ /**
+ * This table contains people.
+ */
+ public static final class People implements BaseColumns, SyncConstValue, PeopleColumns,
+ PhonesColumns, PresenceColumns {
+ /**
+ * no public constructor since this is a utility class
+ */
+ private People() {}
+
+ /**
+ * The content:// style URL for this table
+ */
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://contacts/people");
+
+ /**
+ * The content:// style URL for filtering people by name. The filter
+ * argument should be passed as an additional path segment after this URI.
+ */
+ public static final Uri CONTENT_FILTER_URI =
+ Uri.parse("content://contacts/people/filter");
+
+ /**
+ * The content:// style URL for the table that holds the deleted
+ * contacts.
+ */
+ public static final Uri DELETED_CONTENT_URI =
+ Uri.parse("content://contacts/deleted_people");
+
+ /**
+ * The MIME type of {@link #CONTENT_URI} providing a directory of
+ * people.
+ */
+ public static final String CONTENT_TYPE = "vnd.android.cursor.dir/person";
+
+ /**
+ * The MIME type of a {@link #CONTENT_URI} subdirectory of a single
+ * person.
+ */
+ public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/person";
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = People.NAME + " ASC";
+
+ /**
+ * The ID of the persons preferred phone number.
+ * <P>Type: INTEGER (foreign key to phones table on the _ID field)</P>
+ */
+ public static final String PRIMARY_PHONE_ID = "primary_phone";
+
+ /**
+ * The ID of the persons preferred email.
+ * <P>Type: INTEGER (foreign key to contact_methods table on the
+ * _ID field)</P>
+ */
+ public static final String PRIMARY_EMAIL_ID = "primary_email";
+
+ /**
+ * The ID of the persons preferred organization.
+ * <P>Type: INTEGER (foreign key to organizations table on the
+ * _ID field)</P>
+ */
+ public static final String PRIMARY_ORGANIZATION_ID = "primary_organization";
+
+ /**
+ * Mark a person as having been contacted.
+ *
+ * @param resolver the ContentResolver to use
+ * @param personId the person who was contacted
+ */
+ public static void markAsContacted(ContentResolver resolver, long personId) {
+ Uri uri = ContentUris.withAppendedId(CONTENT_URI, personId);
+ uri = Uri.withAppendedPath(uri, "update_contact_time");
+ ContentValues values = new ContentValues();
+ // There is a trigger in place that will update TIMES_CONTACTED when
+ // LAST_TIME_CONTACTED is modified.
+ values.put(LAST_TIME_CONTACTED, System.currentTimeMillis());
+ resolver.update(uri, values, null, null);
+ }
+
+ /**
+ * Adds a person to the My Contacts group.
+ *
+ * @param resolver the resolver to use
+ * @param personId the person to add to the group
+ * @return the URI of the group membership row
+ * @throws IllegalStateException if the My Contacts group can't be found
+ */
+ public static Uri addToMyContactsGroup(ContentResolver resolver, long personId) {
+ long groupId = 0;
+ Cursor groupsCursor = resolver.query(Groups.CONTENT_URI, GROUPS_PROJECTION,
+ Groups.SYSTEM_ID + "='" + Groups.GROUP_MY_CONTACTS + "'", null, null);
+ if (groupsCursor != null) {
+ try {
+ if (groupsCursor.moveToFirst()) {
+ groupId = groupsCursor.getLong(0);
+ }
+ } finally {
+ groupsCursor.close();
+ }
+ }
+
+ if (groupId == 0) {
+ throw new IllegalStateException("Failed to find the My Contacts group");
+ }
+
+ return addToGroup(resolver, personId, groupId);
+ }
+
+ /**
+ * Adds a person to a group referred to by name.
+ *
+ * @param resolver the resolver to use
+ * @param personId the person to add to the group
+ * @param groupName the name of the group to add the contact to
+ * @return the URI of the group membership row
+ * @throws IllegalStateException if the group can't be found
+ */
+ public static Uri addToGroup(ContentResolver resolver, long personId, String groupName) {
+ long groupId = 0;
+ Cursor groupsCursor = resolver.query(Groups.CONTENT_URI, GROUPS_PROJECTION,
+ Groups.NAME + "=?", new String[] { groupName }, null);
+ if (groupsCursor != null) {
+ try {
+ if (groupsCursor.moveToFirst()) {
+ groupId = groupsCursor.getLong(0);
+ }
+ } finally {
+ groupsCursor.close();
+ }
+ }
+
+ if (groupId == 0) {
+ throw new IllegalStateException("Failed to find the My Contacts group");
+ }
+
+ return addToGroup(resolver, personId, groupId);
+ }
+
+ /**
+ * Adds a person to a group.
+ *
+ * @param resolver the resolver to use
+ * @param personId the person to add to the group
+ * @param groupId the group to add the person to
+ * @return the URI of the group membership row
+ */
+ public static Uri addToGroup(ContentResolver resolver, long personId, long groupId) {
+ ContentValues values = new ContentValues();
+ values.put(GroupMembership.PERSON_ID, personId);
+ values.put(GroupMembership.GROUP_ID, groupId);
+ return resolver.insert(GroupMembership.CONTENT_URI, values);
+ }
+
+ private static final String[] GROUPS_PROJECTION = new String[] {
+ Groups._ID,
+ };
+
+ /**
+ * Creates a new contacts and adds it to the "My Contacts" group.
+ *
+ * @param resolver the ContentResolver to use
+ * @param values the values to use when creating the contact
+ * @return the URI of the contact, or null if the operation fails
+ */
+ public static Uri createPersonInMyContactsGroup(ContentResolver resolver,
+ ContentValues values) {
+
+ Uri contactUri = resolver.insert(People.CONTENT_URI, values);
+ if (contactUri == null) {
+ Log.e(TAG, "Failed to create the contact");
+ return null;
+ }
+
+ if (addToMyContactsGroup(resolver, ContentUris.parseId(contactUri)) == null) {
+ resolver.delete(contactUri, null, null);
+ return null;
+ }
+ return contactUri;
+ }
+
+ public static Cursor queryGroups(ContentResolver resolver, long person) {
+ return resolver.query(GroupMembership.CONTENT_URI, null, "person=?",
+ new String[]{String.valueOf(person)}, Groups.DEFAULT_SORT_ORDER);
+ }
+
+ /**
+ * Set the photo for this person. data may be null
+ * @param cr the ContentResolver to use
+ * @param person the Uri of the person whose photo is to be updated
+ * @param data the byte[] that represents the photo
+ */
+ public static void setPhotoData(ContentResolver cr, Uri person, byte[] data) {
+ Uri photoUri = Uri.withAppendedPath(person, Contacts.Photos.CONTENT_DIRECTORY);
+ ContentValues values = new ContentValues();
+ values.put(Photos.DATA, data);
+ cr.update(photoUri, values, null, null);
+ }
+
+ /**
+ * Opens an InputStream for the person's photo and returns the photo as a Bitmap.
+ * If the person's photo isn't present returns the placeholderImageResource instead.
+ * @param person the person whose photo should be used
+ */
+ public static InputStream openContactPhotoInputStream(ContentResolver cr, Uri person) {
+ Uri photoUri = Uri.withAppendedPath(person, Contacts.Photos.CONTENT_DIRECTORY);
+ Cursor cursor = cr.query(photoUri, new String[]{Photos.DATA}, null, null, null);
+ try {
+ if (!cursor.moveToNext()) {
+ return null;
+ }
+ byte[] data = cursor.getBlob(0);
+ if (data == null) {
+ return null;
+ }
+ return new ByteArrayInputStream(data);
+ } finally {
+ cursor.close();
+ }
+ }
+
+ /**
+ * Opens an InputStream for the person's photo and returns the photo as a Bitmap.
+ * If the person's photo isn't present returns the placeholderImageResource instead.
+ * @param context the Context
+ * @param person the person whose photo should be used
+ * @param placeholderImageResource the image resource to use if the person doesn't
+ * have a photo
+ * @param options the decoding options, can be set to null
+ */
+ public static Bitmap loadContactPhoto(Context context, Uri person,
+ int placeholderImageResource, BitmapFactory.Options options) {
+ if (person == null) {
+ return loadPlaceholderPhoto(placeholderImageResource, context, options);
+ }
+
+ InputStream stream = openContactPhotoInputStream(context.getContentResolver(), person);
+ Bitmap bm = stream != null ? BitmapFactory.decodeStream(stream, null, options) : null;
+ if (bm == null) {
+ bm = loadPlaceholderPhoto(placeholderImageResource, context, options);
+ }
+ return bm;
+ }
+
+ private static Bitmap loadPlaceholderPhoto(int placeholderImageResource, Context context,
+ BitmapFactory.Options options) {
+ if (placeholderImageResource == 0) {
+ return null;
+ }
+ return BitmapFactory.decodeResource(context.getResources(),
+ placeholderImageResource, options);
+ }
+
+ /**
+ * A sub directory of a single person that contains all of their Phones.
+ */
+ public static final class Phones implements BaseColumns, PhonesColumns,
+ PeopleColumns {
+ /**
+ * no public constructor since this is a utility class
+ */
+ private Phones() {}
+
+ /**
+ * The directory twig for this sub-table
+ */
+ public static final String CONTENT_DIRECTORY = "phones";
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = "number ASC";
+ }
+
+ /**
+ * A subdirectory of a single person that contains all of their
+ * ContactMethods.
+ */
+ public static final class ContactMethods
+ implements BaseColumns, ContactMethodsColumns, PeopleColumns {
+ /**
+ * no public constructor since this is a utility class
+ */
+ private ContactMethods() {}
+
+ /**
+ * The directory twig for this sub-table
+ */
+ public static final String CONTENT_DIRECTORY = "contact_methods";
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = "data ASC";
+ }
+
+ /**
+ * The extensions for a person
+ */
+ public static class Extensions implements BaseColumns, ExtensionsColumns {
+ /**
+ * no public constructor since this is a utility class
+ */
+ private Extensions() {}
+
+ /**
+ * The directory twig for this sub-table
+ */
+ public static final String CONTENT_DIRECTORY = "extensions";
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = "name ASC";
+
+ /**
+ * The ID of the person this phone number is assigned to.
+ * <P>Type: INTEGER (long)</P>
+ */
+ public static final String PERSON_ID = "person";
+ }
+ }
+
+ /**
+ * Columns from the groups table.
+ */
+ public interface GroupsColumns {
+ /**
+ * The group name.
+ * <P>Type: TEXT</P>
+ */
+ public static final String NAME = "name";
+
+ /**
+ * Notes about the group.
+ * <P>Type: TEXT</P>
+ */
+ public static final String NOTES = "notes";
+
+ /**
+ * Whether this group should be synced if the SYNC_EVERYTHING settings is false
+ * for this group's account.
+ * <P>Type: INTEGER (boolean)</P>
+ */
+ public static final String SHOULD_SYNC = "should_sync";
+
+ /**
+ * The ID of this group if it is a System Group, null otherwise.
+ * <P>Type: TEXT</P>
+ */
+ public static final String SYSTEM_ID = "system_id";
+ }
+
+ /**
+ * This table contains the groups for an account.
+ */
+ public static final class Groups
+ implements BaseColumns, SyncConstValue, GroupsColumns {
+ /**
+ * no public constructor since this is a utility class
+ */
+ private Groups() {}
+
+ /**
+ * The content:// style URL for this table
+ */
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://contacts/groups");
+
+ /**
+ * The content:// style URL for the table that holds the deleted
+ * groups.
+ */
+ public static final Uri DELETED_CONTENT_URI =
+ Uri.parse("content://contacts/deleted_groups");
+
+ /**
+ * The MIME type of {@link #CONTENT_URI} providing a directory of
+ * groups.
+ */
+ public static final String CONTENT_TYPE = "vnd.android.cursor.dir/contactsgroup";
+
+ /**
+ * The MIME type of a {@link #CONTENT_URI} subdirectory of a single
+ * group.
+ */
+ public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/contactsgroup";
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = NAME + " ASC";
+
+ /**
+ *
+ */
+ public static final String GROUP_ANDROID_STARRED = "Starred in Android";
+
+ /**
+ * The "My Contacts" system group.
+ */
+ public static final String GROUP_MY_CONTACTS = "Contacts";
+ }
+
+ /**
+ * Columns from the Phones table that other columns join into themselves.
+ */
+ public interface PhonesColumns {
+ /**
+ * The type of the the phone number.
+ * <P>Type: INTEGER (one of the constants below)</P>
+ */
+ public static final String TYPE = "type";
+
+ public static final int TYPE_CUSTOM = 0;
+ public static final int TYPE_HOME = 1;
+ public static final int TYPE_MOBILE = 2;
+ public static final int TYPE_WORK = 3;
+ public static final int TYPE_FAX_WORK = 4;
+ public static final int TYPE_FAX_HOME = 5;
+ public static final int TYPE_PAGER = 6;
+ public static final int TYPE_OTHER = 7;
+
+ /**
+ * The user provided label for the phone number, only used if TYPE is TYPE_CUSTOM.
+ * <P>Type: TEXT</P>
+ */
+ public static final String LABEL = "label";
+
+ /**
+ * The phone number as the user entered it.
+ * <P>Type: TEXT</P>
+ */
+ public static final String NUMBER = "number";
+
+ /**
+ * The normalized phone number
+ * <P>Type: TEXT</P>
+ */
+ public static final String NUMBER_KEY = "number_key";
+
+ /**
+ * Whether this is the primary phone number
+ * <P>Type: INTEGER (if set, non-0 means true)</P>
+ */
+ public static final String ISPRIMARY = "isprimary";
+ }
+
+ /**
+ * This table stores phone numbers and a reference to the person that the
+ * contact method belongs to. Phone numbers are stored separately from
+ * other contact methods to make caller ID lookup more efficient.
+ */
+ public static final class Phones
+ implements BaseColumns, PhonesColumns, PeopleColumns {
+ /**
+ * no public constructor since this is a utility class
+ */
+ private Phones() {}
+
+ public static final CharSequence getDisplayLabel(Context context, int type,
+ CharSequence label, CharSequence[] labelArray) {
+ CharSequence display = "";
+
+ if (type != People.Phones.TYPE_CUSTOM) {
+ CharSequence[] labels = labelArray != null? labelArray
+ : context.getResources().getTextArray(
+ com.android.internal.R.array.phoneTypes);
+ try {
+ display = labels[type - 1];
+ } catch (ArrayIndexOutOfBoundsException e) {
+ display = labels[People.Phones.TYPE_HOME - 1];
+ }
+ } else {
+ if (!TextUtils.isEmpty(label)) {
+ display = label;
+ }
+ }
+ return display;
+ }
+
+ public static final CharSequence getDisplayLabel(Context context, int type,
+ CharSequence label) {
+ return getDisplayLabel(context, type, label, null);
+ }
+
+ /**
+ * The content:// style URL for this table
+ */
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://contacts/phones");
+
+ /**
+ * The content:// style URL for filtering phone numbers
+ */
+ public static final Uri CONTENT_FILTER_URL =
+ Uri.parse("content://contacts/phones/filter");
+
+ /**
+ * The MIME type of {@link #CONTENT_URI} providing a directory of
+ * phones.
+ */
+ public static final String CONTENT_TYPE = "vnd.android.cursor.dir/phone";
+
+ /**
+ * The MIME type of a {@link #CONTENT_URI} subdirectory of a single
+ * phone.
+ */
+ public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/phone";
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = "name ASC";
+
+ /**
+ * The ID of the person this phone number is assigned to.
+ * <P>Type: INTEGER (long)</P>
+ */
+ public static final String PERSON_ID = "person";
+ }
+
+ public static final class GroupMembership implements BaseColumns, GroupsColumns {
+ /**
+ * no public constructor since this is a utility class
+ */
+ private GroupMembership() {}
+
+ /**
+ * The content:// style URL for this table
+ */
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://contacts/groupmembership");
+
+ /**
+ * The content:// style URL for this table
+ */
+ public static final Uri RAW_CONTENT_URI =
+ Uri.parse("content://contacts/groupmembershipraw");
+
+ /**
+ * The directory twig for this sub-table
+ */
+ public static final String CONTENT_DIRECTORY = "groupmembership";
+ /**
+ * The MIME type of {@link #CONTENT_URI} providing a directory of all
+ * person groups.
+ */
+ public static final String CONTENT_TYPE = "vnd.android.cursor.dir/contactsgroupmembership";
+
+ /**
+ * The MIME type of a {@link #CONTENT_URI} subdirectory of a single
+ * person group.
+ */
+ public static final String CONTENT_ITEM_TYPE =
+ "vnd.android.cursor.item/contactsgroupmembership";
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = "group_id ASC";
+
+ /**
+ * The row id of the accounts group.
+ * <P>Type: TEXT</P>
+ */
+ public static final String GROUP_ID = "group_id";
+
+ /**
+ * The sync id of the group.
+ * <P>Type: TEXT</P>
+ */
+ public static final String GROUP_SYNC_ID = "group_sync_id";
+
+ /**
+ * The account of the group.
+ * <P>Type: TEXT</P>
+ */
+ public static final String GROUP_SYNC_ACCOUNT = "group_sync_account";
+
+ /**
+ * The row id of the person.
+ * <P>Type: TEXT</P>
+ */
+ public static final String PERSON_ID = "person";
+ }
+
+ /**
+ * Columns from the ContactMethods table that other tables join into
+ * themseleves.
+ */
+ public interface ContactMethodsColumns {
+ /**
+ * The kind of the the contact method. For example, email address,
+ * postal address, etc.
+ * <P>Type: INTEGER (one of the values below)</P>
+ */
+ public static final String KIND = "kind";
+
+ /**
+ * The type of the contact method, must be one of the types below.
+ * <P>Type: INTEGER (one of the values below)</P>
+ */
+ public static final String TYPE = "type";
+ public static final int TYPE_CUSTOM = 0;
+ public static final int TYPE_HOME = 1;
+ public static final int TYPE_WORK = 2;
+ public static final int TYPE_OTHER = 3;
+
+ /**
+ * The user defined label for the the contact method.
+ * <P>Type: TEXT</P>
+ */
+ public static final String LABEL = "label";
+
+ /**
+ * The data for the contact method.
+ * <P>Type: TEXT</P>
+ */
+ public static final String DATA = "data";
+
+ /**
+ * Auxiliary data for the contact method.
+ * <P>Type: TEXT</P>
+ */
+ public static final String AUX_DATA = "aux_data";
+
+ /**
+ * Whether this is the primary organization
+ * <P>Type: INTEGER (if set, non-0 means true)</P>
+ */
+ public static final String ISPRIMARY = "isprimary";
+ }
+
+ /**
+ * This table stores all non-phone contact methods and a reference to the
+ * person that the contact method belongs to.
+ */
+ public static final class ContactMethods
+ implements BaseColumns, ContactMethodsColumns, PeopleColumns {
+ /**
+ * The column with latitude data for postal locations
+ * <P>Type: REAL</P>
+ */
+ public static final String POSTAL_LOCATION_LATITUDE = DATA;
+
+ /**
+ * The column with longitude data for postal locations
+ * <P>Type: REAL</P>
+ */
+ public static final String POSTAL_LOCATION_LONGITUDE = AUX_DATA;
+
+ /**
+ * The predefined IM protocol types. The protocol can either be non-present, one
+ * of these types, or a free-form string. These cases are encoded in the AUX_DATA
+ * column as:
+ * - null
+ * - pre:<an integer, one of the protocols below>
+ * - custom:<a string>
+ */
+ public static final int PROTOCOL_AIM = 0;
+ public static final int PROTOCOL_MSN = 1;
+ public static final int PROTOCOL_YAHOO = 2;
+ public static final int PROTOCOL_SKYPE = 3;
+ public static final int PROTOCOL_QQ = 4;
+ public static final int PROTOCOL_GOOGLE_TALK = 5;
+ public static final int PROTOCOL_ICQ = 6;
+ public static final int PROTOCOL_JABBER = 7;
+
+ public static String encodePredefinedImProtocol(int protocol) {
+ return "pre:" + protocol;
+ }
+
+ public static String encodeCustomImProtocol(String protocolString) {
+ return "custom:" + protocolString;
+ }
+
+ public static Object decodeImProtocol(String encodedString) {
+ if (encodedString == null) {
+ return null;
+ }
+
+ if (encodedString.startsWith("pre:")) {
+ return Integer.parseInt(encodedString.substring(4));
+ }
+
+ if (encodedString.startsWith("custom:")) {
+ return encodedString.substring(7);
+ }
+
+ throw new IllegalArgumentException(
+ "the value is not a valid encoded protocol, " + encodedString);
+ }
+
+ /**
+ * This looks up the provider category defined in
+ * {@link android.provider.Im.ProviderCategories} from the predefined IM protocol id.
+ * This is used for interacting with the IM application.
+ *
+ * @param protocol the protocol ID
+ * @return the provider category the IM app uses for the given protocol, or null if no
+ * provider is defined for the given protocol
+ * @hide
+ */
+ public static String lookupProviderCategoryFromId(int protocol) {
+ switch (protocol) {
+ case PROTOCOL_GOOGLE_TALK:
+ return Im.ProviderCategories.GTALK;
+ case PROTOCOL_AIM:
+ return Im.ProviderCategories.AIM;
+ case PROTOCOL_MSN:
+ return Im.ProviderCategories.MSN;
+ case PROTOCOL_YAHOO:
+ return Im.ProviderCategories.YAHOO;
+ case PROTOCOL_ICQ:
+ return Im.ProviderCategories.ICQ;
+ }
+ return null;
+ }
+
+ /**
+ * no public constructor since this is a utility class
+ */
+ private ContactMethods() {}
+
+ public static final CharSequence getDisplayLabel(Context context, int kind,
+ int type, CharSequence label) {
+ CharSequence display = "";
+ switch (kind) {
+ case KIND_EMAIL: {
+ if (type != People.ContactMethods.TYPE_CUSTOM) {
+ CharSequence[] labels = context.getResources().getTextArray(
+ com.android.internal.R.array.emailAddressTypes);
+ try {
+ display = labels[type - 1];
+ } catch (ArrayIndexOutOfBoundsException e) {
+ display = labels[ContactMethods.TYPE_HOME - 1];
+ }
+ } else {
+ if (!TextUtils.isEmpty(label)) {
+ display = label;
+ }
+ }
+ break;
+ }
+
+ case KIND_POSTAL: {
+ if (type != People.ContactMethods.TYPE_CUSTOM) {
+ CharSequence[] labels = context.getResources().getTextArray(
+ com.android.internal.R.array.postalAddressTypes);
+ try {
+ display = labels[type - 1];
+ } catch (ArrayIndexOutOfBoundsException e) {
+ display = labels[ContactMethods.TYPE_HOME - 1];
+ }
+ } else {
+ if (!TextUtils.isEmpty(label)) {
+ display = label;
+ }
+ }
+ break;
+ }
+
+ default:
+ display = context.getString(R.string.untitled);
+ }
+ return display;
+ }
+
+ /**
+ * Add a longitude and latitude location to a postal address.
+ *
+ * @param context the context to use when updating the database
+ * @param postalId the address to update
+ * @param latitude the latitude for the address
+ * @param longitude the longitude for the address
+ */
+ public void addPostalLocation(Context context, long postalId,
+ double latitude, double longitude) {
+ final ContentResolver resolver = context.getContentResolver();
+ // Insert the location
+ ContentValues values = new ContentValues(2);
+ values.put(POSTAL_LOCATION_LATITUDE, latitude);
+ values.put(POSTAL_LOCATION_LONGITUDE, longitude);
+ Uri loc = resolver.insert(CONTENT_URI, values);
+ long locId = ContentUris.parseId(loc);
+
+ // Update the postal address
+ values.clear();
+ values.put(AUX_DATA, locId);
+ resolver.update(ContentUris.withAppendedId(CONTENT_URI, postalId), values, null, null);
+ }
+
+ /**
+ * The content:// style URL for this table
+ */
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://contacts/contact_methods");
+
+ /**
+ * The content:// style URL for sub-directory of e-mail addresses.
+ */
+ public static final Uri CONTENT_EMAIL_URI =
+ Uri.parse("content://contacts/contact_methods/email");
+
+ /**
+ * The MIME type of {@link #CONTENT_URI} providing a directory of
+ * phones.
+ */
+ public static final String CONTENT_TYPE = "vnd.android.cursor.dir/contact-methods";
+
+ /**
+ * The MIME type of a {@link #CONTENT_EMAIL_URI} sub-directory of\
+ * multiple {@link Contacts#KIND_EMAIL} entries.
+ */
+ public static final String CONTENT_EMAIL_TYPE = "vnd.android.cursor.dir/email";
+
+ /**
+ * The MIME type of a {@link #CONTENT_EMAIL_URI} sub-directory of\
+ * multiple {@link Contacts#KIND_POSTAL} entries.
+ */
+ public static final String CONTENT_POSTAL_TYPE = "vnd.android.cursor.dir/postal-address";
+
+ /**
+ * The MIME type of a {@link #CONTENT_URI} sub-directory of a single
+ * {@link Contacts#KIND_EMAIL} entry.
+ */
+ public static final String CONTENT_EMAIL_ITEM_TYPE = "vnd.android.cursor.item/email";
+
+ /**
+ * The MIME type of a {@link #CONTENT_URI} sub-directory of a single
+ * {@link Contacts#KIND_POSTAL} entry.
+ */
+ public static final String CONTENT_POSTAL_ITEM_TYPE
+ = "vnd.android.cursor.item/postal-address";
+
+ /**
+ * The MIME type of a {@link #CONTENT_URI} sub-directory of a single
+ * {@link Contacts#KIND_IM} entry.
+ */
+ public static final String CONTENT_IM_ITEM_TYPE = "vnd.android.cursor.item/jabber-im";
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = "name ASC";
+
+ /**
+ * The ID of the person this contact method is assigned to.
+ * <P>Type: INTEGER (long)</P>
+ */
+ public static final String PERSON_ID = "person";
+ }
+
+ /**
+ * The IM presence columns with some contacts specific columns mixed in.
+ */
+ public interface PresenceColumns extends Im.CommonPresenceColumns {
+ /**
+ * The IM service the presence is coming from. Formatted using either
+ * {@link Contacts.ContactMethods#encodePredefinedImProtocol} or
+ * {@link Contacts.ContactMethods#encodeCustomImProtocol}.
+ * <P>Type: STRING</P>
+ */
+ public static final String IM_PROTOCOL = "im_protocol";
+
+ /**
+ * The IM handle the presence item is for. The handle is scoped to
+ * the {@link #IM_PROTOCOL}.
+ * <P>Type: STRING</P>
+ */
+ public static final String IM_HANDLE = "im_handle";
+
+ /**
+ * The IM account for the local user that the presence data came from.
+ * <P>Type: STRING</P>
+ */
+ public static final String IM_ACCOUNT = "im_account";
+ }
+
+ /**
+ * Contains presence information about contacts.
+ * @hide
+ */
+ public static final class Presence
+ implements BaseColumns, PresenceColumns, PeopleColumns {
+ /**
+ * The content:// style URL for this table
+ */
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://contacts/presence");
+
+ /**
+ * The ID of the person this presence item is assigned to.
+ * <P>Type: INTEGER (long)</P>
+ */
+ public static final String PERSON_ID = "person";
+
+ /**
+ * Gets the resource ID for the proper presence icon.
+ *
+ * @param status the status to get the icon for
+ * @return the resource ID for the proper presence icon
+ */
+ public static final int getPresenceIconResourceId(int status) {
+ switch (status) {
+ case Contacts.People.AVAILABLE:
+ return com.android.internal.R.drawable.presence_online;
+
+ case Contacts.People.IDLE:
+ case Contacts.People.AWAY:
+ return com.android.internal.R.drawable.presence_away;
+
+ case Contacts.People.DO_NOT_DISTURB:
+ return com.android.internal.R.drawable.presence_busy;
+
+ case Contacts.People.INVISIBLE:
+ return com.android.internal.R.drawable.presence_invisible;
+
+ case Contacts.People.OFFLINE:
+ default:
+ return com.android.internal.R.drawable.presence_offline;
+ }
+ }
+
+ /**
+ * Sets a presence icon to the proper graphic
+ *
+ * @param icon the icon to to set
+ * @param serverStatus that status
+ */
+ public static final void setPresenceIcon(ImageView icon, int serverStatus) {
+ icon.setImageResource(getPresenceIconResourceId(serverStatus));
+ }
+ }
+
+ /**
+ * Columns from the Organizations table that other columns join into themselves.
+ */
+ public interface OrganizationColumns {
+ /**
+ * The type of the the phone number.
+ * <P>Type: INTEGER (one of the constants below)</P>
+ */
+ public static final String TYPE = "type";
+
+ public static final int TYPE_CUSTOM = 0;
+ public static final int TYPE_WORK = 1;
+ public static final int TYPE_OTHER = 2;
+
+ /**
+ * The user provided label, only used if TYPE is TYPE_CUSTOM.
+ * <P>Type: TEXT</P>
+ */
+ public static final String LABEL = "label";
+
+ /**
+ * The name of the company for this organization.
+ * <P>Type: TEXT</P>
+ */
+ public static final String COMPANY = "company";
+
+ /**
+ * The title within this organization.
+ * <P>Type: TEXT</P>
+ */
+ public static final String TITLE = "title";
+
+ /**
+ * The person this organization is tied to.
+ * <P>Type: TEXT</P>
+ */
+ public static final String PERSON_ID = "person";
+
+ /**
+ * Whether this is the primary organization
+ * <P>Type: INTEGER (if set, non-0 means true)</P>
+ */
+ public static final String ISPRIMARY = "isprimary";
+ }
+
+ /**
+ * A sub directory of a single person that contains all of their Phones.
+ */
+ public static final class Organizations implements BaseColumns, OrganizationColumns {
+ /**
+ * no public constructor since this is a utility class
+ */
+ private Organizations() {}
+
+ public static final CharSequence getDisplayLabel(Context context, int type,
+ CharSequence label) {
+ CharSequence display = "";
+
+ if (type != TYPE_CUSTOM) {
+ CharSequence[] labels = context.getResources().getTextArray(
+ com.android.internal.R.array.organizationTypes);
+ try {
+ display = labels[type - 1];
+ } catch (ArrayIndexOutOfBoundsException e) {
+ display = labels[People.Phones.TYPE_HOME - 1];
+ }
+ } else {
+ if (!TextUtils.isEmpty(label)) {
+ display = label;
+ }
+ }
+ return display;
+ }
+
+ /**
+ * The content:// style URL for this table
+ */
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://contacts/organizations");
+
+ /**
+ * The directory twig for this sub-table
+ */
+ public static final String CONTENT_DIRECTORY = "organizations";
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = "company, title, isprimary ASC";
+ }
+
+ /**
+ * Columns from the Photos table that other columns join into themselves.
+ */
+ public interface PhotosColumns {
+ /**
+ * The _SYNC_VERSION of the photo that was last downloaded
+ * <P>Type: TEXT</P>
+ */
+ public static final String LOCAL_VERSION = "local_version";
+
+ /**
+ * The person this photo is associated with.
+ * <P>Type: TEXT</P>
+ */
+ public static final String PERSON_ID = "person";
+
+ /**
+ * non-zero if a download is required and the photo isn't marked as a bad resource.
+ * You must specify this in the columns in order to use it in the where clause.
+ * <P>Type: INTEGER(boolean)</P>
+ */
+ public static final String DOWNLOAD_REQUIRED = "download_required";
+
+ /**
+ * non-zero if this photo is known to exist on the server
+ * <P>Type: INTEGER(boolean)</P>
+ */
+ public static final String EXISTS_ON_SERVER = "exists_on_server";
+
+ /**
+ * Contains the description of the upload or download error from
+ * the previous attempt. If null then the previous attempt succeeded.
+ * <P>Type: TEXT</P>
+ */
+ public static final String SYNC_ERROR = "sync_error";
+
+ /**
+ * The image data, or null if there is no image.
+ * <P>Type: BLOB</P>
+ */
+ public static final String DATA = "data";
+
+ }
+
+ /**
+ * The photos over all of the people
+ */
+ public static final class Photos implements BaseColumns, PhotosColumns, SyncConstValue {
+ /**
+ * no public constructor since this is a utility class
+ */
+ private Photos() {}
+
+ /**
+ * The content:// style URL for this table
+ */
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://contacts/photos");
+
+ /**
+ * The directory twig for this sub-table
+ */
+ public static final String CONTENT_DIRECTORY = "photo";
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = "person ASC";
+ }
+
+ public interface ExtensionsColumns {
+ /**
+ * The name of this extension. May not be null. There may be at most one row for each name.
+ * <P>Type: TEXT</P>
+ */
+ public static final String NAME = "name";
+
+ /**
+ * The value of this extension. May not be null.
+ * <P>Type: TEXT</P>
+ */
+ public static final String VALUE = "value";
+ }
+
+ /**
+ * The extensions for a person
+ */
+ public static final class Extensions implements BaseColumns, ExtensionsColumns {
+ /**
+ * no public constructor since this is a utility class
+ */
+ private Extensions() {}
+
+ /**
+ * The content:// style URL for this table
+ */
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://contacts/extensions");
+
+ /**
+ * The MIME type of {@link #CONTENT_URI} providing a directory of
+ * phones.
+ */
+ public static final String CONTENT_TYPE = "vnd.android.cursor.dir/contact_extensions";
+
+ /**
+ * The MIME type of a {@link #CONTENT_URI} subdirectory of a single
+ * phone.
+ */
+ public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/contact_extensions";
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = "person, name ASC";
+
+ /**
+ * The ID of the person this phone number is assigned to.
+ * <P>Type: INTEGER (long)</P>
+ */
+ public static final String PERSON_ID = "person";
+ }
+
+ /**
+ * Contains helper classes used to create or manage {@link android.content.Intent Intents}
+ * that involve contacts.
+ */
+ public static final class Intents {
+ /**
+ * This is the intent that is fired when a search suggestion is clicked on.
+ */
+ public static final String SEARCH_SUGGESTION_CLICKED =
+ "android.provider.Contacts.SEARCH_SUGGESTION_CLICKED";
+
+ /**
+ * This is the intent that is fired when a search suggestion for dialing a number
+ * is clicked on.
+ */
+ public static final String SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED =
+ "android.provider.Contacts.SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED";
+
+ /**
+ * This is the intent that is fired when a search suggestion for creating a contact
+ * is clicked on.
+ */
+ public static final String SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED =
+ "android.provider.Contacts.SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED";
+
+ /**
+ * Starts an Activity that lets the user pick a contact to attach an image to.
+ * After picking the contact it launches the image cropper in face detection mode.
+ */
+ public static final String ATTACH_IMAGE =
+ "com.android.contacts.action.ATTACH_IMAGE";
+
+ /**
+ * Intents related to the Contacts app UI.
+ */
+ public static final class UI {
+ /**
+ * The action for the default contacts list tab.
+ */
+ public static final String LIST_DEFAULT =
+ "com.android.contacts.action.LIST_DEFAULT";
+
+ /**
+ * The action for the contacts list tab.
+ */
+ public static final String LIST_GROUP_ACTION =
+ "com.android.contacts.action.LIST_GROUP";
+
+ /**
+ * When in LIST_GROUP_ACTION mode, this is the group to display.
+ */
+ public static final String GROUP_NAME_EXTRA_KEY = "com.android.contacts.extra.GROUP";
+
+ /**
+ * The action for the all contacts list tab.
+ */
+ public static final String LIST_ALL_CONTACTS_ACTION =
+ "com.android.contacts.action.LIST_ALL_CONTACTS";
+
+ /**
+ * The action for the contacts with phone numbers list tab.
+ */
+ public static final String LIST_CONTACTS_WITH_PHONES_ACTION =
+ "com.android.contacts.action.LIST_CONTACTS_WITH_PHONES";
+
+ /**
+ * The action for the starred contacts list tab.
+ */
+ public static final String LIST_STARRED_ACTION =
+ "com.android.contacts.action.LIST_STARRED";
+
+ /**
+ * The action for the frequent contacts list tab.
+ */
+ public static final String LIST_FREQUENT_ACTION =
+ "com.android.contacts.action.LIST_FREQUENT";
+
+ /**
+ * The action for the "strequent" contacts list tab. It first lists the starred
+ * contacts in alphabetical order and then the frequent contacts in descending
+ * order of the number of times they have been contacted.
+ */
+ public static final String LIST_STREQUENT_ACTION =
+ "com.android.contacts.action.LIST_STREQUENT";
+
+ /**
+ * A key for to be used as an intent extra to set the activity
+ * title to a custom String value.
+ */
+ public static final String TITLE_EXTRA_KEY =
+ "com.android.contacts.extra.TITLE_EXTRA";
+
+ /**
+ * Activity Action: Display a filtered list of contacts
+ * <p>
+ * Input: Extra field {@link #FILTER_TEXT_EXTRA_KEY} is the text to use for
+ * filtering
+ * <p>
+ * Output: Nothing.
+ */
+ public static final String FILTER_CONTACTS_ACTION =
+ "com.android.contacts.action.FILTER_CONTACTS";
+
+ /**
+ * Used as an int extra field in {@link #FILTER_CONTACTS_ACTION}
+ * intents to supply the text on which to filter.
+ */
+ public static final String FILTER_TEXT_EXTRA_KEY =
+ "com.android.contacts.extra.FILTER_TEXT";
+ }
+
+ /**
+ * Convenience class that contains string constants used
+ * to create contact {@link android.content.Intent Intents}.
+ */
+ public static final class Insert {
+ /** The action code to use when adding a contact */
+ public static final String ACTION = Intent.ACTION_INSERT;
+
+ /**
+ * If present, forces a bypass of quick insert mode.
+ */
+ public static final String FULL_MODE = "full_mode";
+
+ /**
+ * The extra field for the contact name.
+ * <P>Type: String</P>
+ */
+ public static final String NAME = "name";
+
+ /**
+ * The extra field for the contact company.
+ * <P>Type: String</P>
+ */
+ public static final String COMPANY = "company";
+
+ /**
+ * The extra field for the contact job title.
+ * <P>Type: String</P>
+ */
+ public static final String JOB_TITLE = "job_title";
+
+ /**
+ * The extra field for the contact notes.
+ * <P>Type: String</P>
+ */
+ public static final String NOTES = "notes";
+
+ /**
+ * The extra field for the contact phone number.
+ * <P>Type: String</P>
+ */
+ public static final String PHONE = "phone";
+
+ /**
+ * The extra field for the contact phone number type.
+ * <P>Type: Either an integer value from {@link android.provider.Contacts.PhonesColumns PhonesColumns},
+ * or a string specifying a type and label.</P>
+ */
+ public static final String PHONE_TYPE = "phone_type";
+
+ /**
+ * The extra field for the phone isprimary flag.
+ * <P>Type: boolean</P>
+ */
+ public static final String PHONE_ISPRIMARY = "phone_isprimary";
+
+ /**
+ * The extra field for the contact email address.
+ * <P>Type: String</P>
+ */
+ public static final String EMAIL = "email";
+
+ /**
+ * The extra field for the contact email type.
+ * <P>Type: Either an integer value from {@link android.provider.Contacts.ContactMethodsColumns ContactMethodsColumns}
+ * or a string specifying a type and label.</P>
+ */
+ public static final String EMAIL_TYPE = "email_type";
+
+ /**
+ * The extra field for the email isprimary flag.
+ * <P>Type: boolean</P>
+ */
+ public static final String EMAIL_ISPRIMARY = "email_isprimary";
+
+ /**
+ * The extra field for the contact postal address.
+ * <P>Type: String</P>
+ */
+ public static final String POSTAL = "postal";
+
+ /**
+ * The extra field for the contact postal address type.
+ * <P>Type: Either an integer value from {@link android.provider.Contacts.ContactMethodsColumns ContactMethodsColumns}
+ * or a string specifying a type and label.</P>
+ */
+ public static final String POSTAL_TYPE = "postal_type";
+
+ /**
+ * The extra field for the postal isprimary flag.
+ * <P>Type: boolean</P>
+ */
+ public static final String POSTAL_ISPRIMARY = "postal_isprimary";
+
+ /**
+ * The extra field for an IM handle.
+ * <P>Type: String</P>
+ */
+ public static final String IM_HANDLE = "im_handle";
+
+ /**
+ * The extra field for the IM protocol
+ * <P>Type: the result of {@link Contacts.ContactMethods#encodePredefinedImProtocol}
+ * or {@link Contacts.ContactMethods#encodeCustomImProtocol}.</P>
+ */
+ public static final String IM_PROTOCOL = "im_protocol";
+
+ /**
+ * The extra field for the IM isprimary flag.
+ * <P>Type: boolean</P>
+ */
+ public static final String IM_ISPRIMARY = "im_isprimary";
+ }
+ }
+}
diff --git a/core/java/android/provider/Downloads.java b/core/java/android/provider/Downloads.java
new file mode 100644
index 0000000..42e9d95
--- /dev/null
+++ b/core/java/android/provider/Downloads.java
@@ -0,0 +1,604 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.provider;
+
+import android.net.Uri;
+
+/**
+ * Exposes constants used to interact with the download manager's
+ * content provider.
+ * The constants URI ... STATUS are the names of columns in the downloads table.
+ *
+ * @hide
+ */
+// For 1.0 the download manager can't deal with abuse from untrusted apps, so
+// this API is hidden.
+public final class Downloads implements BaseColumns {
+ private Downloads() {}
+ /**
+ * The content:// URI for the data table in the provider
+ */
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://downloads/download");
+
+ /**
+ * Broadcast Action: this is sent by the download manager to the app
+ * that had initiated a download when that download completes. The
+ * download's content: uri is specified in the intent's data.
+ */
+ public static final String DOWNLOAD_COMPLETED_ACTION =
+ "android.intent.action.DOWNLOAD_COMPLETED";
+
+ /**
+ * Broadcast Action: this is sent by the download manager to the app
+ * that had initiated a download when the user selects the notification
+ * associated with that download. The download's content: uri is specified
+ * in the intent's data if the click is associated with a single download,
+ * or Downloads.CONTENT_URI if the notification is associated with
+ * multiple downloads.
+ * Note: this is not currently sent for downloads that have completed
+ * successfully.
+ */
+ public static final String NOTIFICATION_CLICKED_ACTION =
+ "android.intent.action.DOWNLOAD_NOTIFICATION_CLICKED";
+
+ /**
+ * The name of the column containing the URI of the data being downloaded.
+ * <P>Type: TEXT</P>
+ * <P>Owner can Init/Read</P>
+ */
+ public static final String URI = "uri";
+
+ /**
+ * The name of the column containing the HTTP method to use for this
+ * download. See the METHOD_* constants for a list of legal values.
+ * <P>Type: INTEGER</P>
+ * <P>Owner can Init/Read</P>
+ */
+ public static final String METHOD = "method";
+
+ /**
+ * The name of the column containing the entity to be sent with the
+ * request of this download. Only use for methods that support sending
+ * entities, i.e. POST.
+ * <P>Type: TEXT</P>
+ * <P>Owner can Init</P>
+ */
+ public static final String ENTITY = "entity";
+
+ /**
+ * The name of the column containing the flags that indicates whether
+ * the initiating application is capable of verifying the integrity of
+ * the downloaded file. When this flag is set, the download manager
+ * performs downloads and reports success even in some situations where
+ * it can't guarantee that the download has completed (e.g. when doing
+ * a byte-range request without an ETag, or when it can't determine
+ * whether a download fully completed).
+ * <P>Type: BOOLEAN</P>
+ * <P>Owner can Init/Read</P>
+ */
+ public static final String NO_INTEGRITY = "no_integrity";
+
+ /**
+ * The name of the column containing the filename that the initiating
+ * application recommends. When possible, the download manager will attempt
+ * to use this filename, or a variation, as the actual name for the file.
+ * <P>Type: TEXT</P>
+ * <P>Owner can Init/Read</P>
+ */
+ public static final String FILENAME_HINT = "hint";
+
+ /**
+ * The name of the column containing the filename where the downloaded data
+ * was actually stored.
+ * <P>Type: TEXT</P>
+ * <P>Owner can Read</P>
+ * <P>UI can Read</P>
+ */
+ public static final String FILENAME = "_data";
+
+ /**
+ * The name of the column containing the MIME type of the downloaded data.
+ * <P>Type: TEXT</P>
+ * <P>Owner can Init/Read</P>
+ * <P>UI can Read</P>
+ */
+ public static final String MIMETYPE = "mimetype";
+
+ /**
+ * The name of the column containing the flag that controls the destination
+ * of the download. See the DESTINATION_* constants for a list of legal values.
+ * <P>Type: INTEGER</P>
+ * <P>Owner can Init/Read</P>
+ * <P>UI can Read</P>
+ */
+ public static final String DESTINATION = "destination";
+
+ /**
+ * The name of the column containing the flags that controls whether
+ * the download must be saved with the filename used for OTA updates.
+ * Must be used with INTERNAL, and the initiating application must hold the
+ * android.permission.DOWNLOAD_OTA_UPDATE permission.
+ * <P>Type: BOOLEAN</P>
+ * <P>Owner can Init/Read</P>
+ * <P>UI can Read</P>
+ */
+ public static final String OTA_UPDATE = "otaupdate";
+
+ /**
+ * The name of the columns containing the flag that controls whether
+ * files with private/inernal/system MIME types can be downloaded.
+ * <P>Type: BOOLEAN</P>
+ * <P>Owner can Init/Read</P>
+ */
+ public static final String NO_SYSTEM_FILES = "no_system";
+
+ /**
+ * The name of the column containing the flags that controls whether the
+ * download is displayed by the UI. See the VISIBILITY_* constants for
+ * a list of legal values.
+ * <P>Type: INTEGER</P>
+ * <P>Owner can Init/Read/Write</P>
+ * <P>UI can Read/Write (only for entries that are visible)</P>
+ */
+ public static final String VISIBILITY = "visibility";
+
+ /**
+ * The name of the column containing the command associated with the
+ * download. After a download is initiated, this is the only column that
+ * applications can modify. See the CONTROL_* constants for a list of legal
+ * values. Note: doesn't do anything in 1.0. The API will be hooked up
+ * in a future version, and is provided here as an indication of things
+ * to come.
+ * <P>Type: INTEGER</P>
+ * <P>Owner can Init/Read/Write</P>
+ * <P>UI can Init/Read/Write</P>
+ * @hide
+ */
+ public static final String CONTROL = "control";
+
+ /**
+ * The name of the column containing the current status of the download.
+ * Applications can read this to follow the progress of each download. See
+ * the STATUS_* constants for a list of legal values.
+ * <P>Type: INTEGER</P>
+ * <P>Owner can Read</P>
+ * <P>UI can Read</P>
+ */
+ public static final String STATUS = "status";
+
+ /**
+ * The name of the column containing the date at which some interesting
+ * status changed in the download. Stored as a System.currentTimeMillis()
+ * value.
+ * <P>Type: BIGINT</P>
+ * <P>Owner can Read</P>
+ * <P>UI can Read</P>
+ */
+ public static final String LAST_MODIFICATION = "lastmod";
+
+ /**
+ * The name of the column containing the number of consecutive connections
+ * that have failed.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String FAILED_CONNECTIONS = "numfailed";
+
+ /**
+ * The name of the column containing the package name of the application
+ * that initiating the download. The download manager will send
+ * notifications to a component in this package when the download completes.
+ * <P>Type: TEXT</P>
+ * <P>Owner can Init/Read</P>
+ * <P>UI can Read</P>
+ */
+ public static final String NOTIFICATION_PACKAGE = "notificationpackage";
+
+ /**
+ * The name of the column containing the component name of the class that
+ * will receive notifications associated with the download. The
+ * package/class combination is passed to
+ * Intent.setClassName(String,String).
+ * <P>Type: TEXT</P>
+ * <P>Owner can Init/Read</P>
+ * <P>UI can Read</P>
+ */
+ public static final String NOTIFICATION_CLASS = "notificationclass";
+
+ /**
+ * If extras are specified when requesting a download they will be provided in the intent that
+ * is sent to the specified class and package when a download has finished.
+ */
+ public static final String NOTIFICATION_EXTRAS = "notificationextras";
+
+ /**
+ * The name of the column contain the values of the cookie to be used for
+ * the download. This is used directly as the value for the Cookie: HTTP
+ * header that gets sent with the request.
+ * <P>Type: TEXT</P>
+ * <P>Owner can Init</P>
+ */
+ public static final String COOKIE_DATA = "cookiedata";
+
+ /**
+ * The name of the column containing the user agent that the initiating
+ * application wants the download manager to use for this download.
+ * <P>Type: TEXT</P>
+ * <P>Owner can Init</P>
+ */
+ public static final String USER_AGENT = "useragent";
+
+ /**
+ * The name of the column containing the referer (sic) that the initiating
+ * application wants the download manager to use for this download.
+ * <P>Type: TEXT</P>
+ * <P>Owner can Init</P>
+ */
+ public static final String REFERER = "referer";
+
+ /**
+ * The name of the column containing the total size of the file being
+ * downloaded.
+ * <P>Type: INTEGER</P>
+ * <P>Owner can Read</P>
+ * <P>UI can Read</P>
+ */
+ public static final String TOTAL_BYTES = "total_bytes";
+
+ /**
+ * The name of the column containing the size of the part of the file that
+ * has been downloaded so far.
+ * <P>Type: INTEGER</P>
+ * <P>Owner can Read</P>
+ * <P>UI can Read</P>
+ */
+ public static final String CURRENT_BYTES = "current_bytes";
+
+ /**
+ * The name of the column containing the entity tag for the response.
+ * <P>Type: TEXT</P>
+ * @hide
+ */
+ public static final String ETAG = "etag";
+
+ /**
+ * The name of the column containing the UID of the application that
+ * initiated the download.
+ * <P>Type: INTEGER</P>
+ * @hide
+ */
+ public static final String UID = "uid";
+
+ /**
+ * The name of the column where the initiating application can provide the
+ * UID of another application that is allowed to access this download. If
+ * multiple applications share the same UID, all those applications will be
+ * allowed to access this download. This column can be updated after the
+ * download is initiated.
+ * <P>Type: INTEGER</P>
+ * <P>Owner can Init/Read/Write</P>
+ */
+ public static final String OTHER_UID = "otheruid";
+
+ /**
+ * The name of the column where the initiating application can provided the
+ * title of this download. The title will be displayed ito the user in the
+ * list of downloads.
+ * <P>Type: TEXT</P>
+ * <P>Owner can Init/Read/Write</P>
+ * <P>UI can Read</P>
+ */
+ public static final String TITLE = "title";
+
+ /**
+ * The name of the column where the initiating application can provide the
+ * description of this download. The description will be displayed to the
+ * user in the list of downloads.
+ * <P>Type: TEXT</P>
+ * <P>Owner can Init/Read/Write</P>
+ * <P>UI can Read</P>
+ */
+ public static final String DESCRIPTION = "description";
+
+ /**
+ * The name of the column where the download manager indicates whether the
+ * media scanner was notified about this download.
+ * <P>Type: BOOLEAN</P>
+ * @hide
+ */
+ public static final String MEDIA_SCANNED = "scanned";
+
+ /*
+ * Lists the destinations that an application can specify for a download.
+ */
+
+ /**
+ * This download will be saved to the external storage. This is the
+ * default behavior, and should be used for any file that the user
+ * can freely access, copy, delete. Even with that destination,
+ * unencrypted DRM files are saved in secure internal storage.
+ * Downloads to the external destination only write files for which
+ * there is a registered handler. The resulting files are accessible
+ * by filename to all applications.
+ */
+ public static final int DESTINATION_EXTERNAL = 0;
+
+ /**
+ * This download will be saved to the download manager's private
+ * partition. This is the behavior used by applications that want to
+ * download private files that are used and deleted soon after they
+ * get downloaded. All file types are allowed, and only the initiating
+ * application can access the file (indirectly through a content
+ * provider).
+ */
+ public static final int DESTINATION_CACHE_PARTITION = 1;
+
+ /**
+ * This download will be saved to the download manager's private
+ * partition and will be purged as necessary to make space. This is
+ * for private files (similar to CACHE_PARTITION) that aren't deleted
+ * immediately after they are used, and are kept around by the download
+ * manager as long as space is available.
+ */
+ public static final int DESTINATION_CACHE_PARTITION_PURGEABLE = 2;
+
+ /**
+ * This download will be saved to the download manager's cache
+ * on the shared data partition. Use CACHE_PARTITION_PURGEABLE instead.
+ */
+ public static final int DESTINATION_DATA_CACHE = 3;
+
+ /* (not javadoc)
+ * This download will be saved to a file specified by the initiating
+ * applications.
+ * @hide
+ */
+ //public static final int DESTINATION_PROVIDER = 4;
+
+ /*
+ * Lists the commands that an application can set to control an ongoing
+ * download. Note: those aren't working.
+ */
+
+ /**
+ * This download can run
+ * @hide
+ */
+ public static final int CONTROL_RUN = 0;
+
+ /**
+ * This download must pause (might be restarted)
+ * @hide
+ */
+ public static final int CONTROL_PAUSE = 1;
+
+ /**
+ * This download must abort (will never be restarted)
+ * @hide
+ */
+ public static final int CONTROL_STOP = 2;
+
+ /*
+ * Lists the states that the download manager can set on a download
+ * to notify applications of the download progress.
+ * The codes follow the HTTP families:<br>
+ * 1xx: informational<br>
+ * 2xx: success<br>
+ * 3xx: redirects (not used by the download manager)<br>
+ * 4xx: client errors<br>
+ * 5xx: server errors
+ */
+
+ /**
+ * Returns whether the status is informational (i.e. 1xx).
+ */
+ public static boolean isStatusInformational(int status) {
+ return (status >= 100 && status < 200);
+ }
+
+ /**
+ * Returns whether the download is suspended. (i.e. whether the download
+ * won't complete without some action from outside the download
+ * manager).
+ */
+ public static boolean isStatusSuspended(int status) {
+ return (status == STATUS_PENDING_PAUSED || status == STATUS_RUNNING_PAUSED);
+ }
+
+ /**
+ * Returns whether the status is a success (i.e. 2xx).
+ */
+ public static boolean isStatusSuccess(int status) {
+ return (status >= 200 && status < 300);
+ }
+
+ /**
+ * Returns whether the status is an error (i.e. 4xx or 5xx).
+ */
+ public static boolean isStatusError(int status) {
+ return (status >= 400 && status < 600);
+ }
+
+ /**
+ * Returns whether the status is a client error (i.e. 4xx).
+ */
+ public static boolean isStatusClientError(int status) {
+ return (status >= 400 && status < 500);
+ }
+
+ /**
+ * Returns whether the status is a server error (i.e. 5xx).
+ */
+ public static boolean isStatusServerError(int status) {
+ return (status >= 500 && status < 600);
+ }
+
+ /**
+ * Returns whether the download has completed (either with success or
+ * error).
+ */
+ public static boolean isStatusCompleted(int status) {
+ return (status >= 200 && status < 300) || (status >= 400 && status < 600);
+ }
+
+ /**
+ * This download hasn't stated yet
+ */
+ public static final int STATUS_PENDING = 190;
+
+ /**
+ * This download hasn't stated yet and is paused
+ */
+ public static final int STATUS_PENDING_PAUSED = 191;
+
+ /**
+ * This download has started
+ */
+ public static final int STATUS_RUNNING = 192;
+
+ /**
+ * This download has started and is paused
+ */
+ public static final int STATUS_RUNNING_PAUSED = 193;
+
+ /**
+ * This download has successfully completed.
+ * Warning: there might be other status values that indicate success
+ * in the future.
+ * Use isSucccess() to capture the entire category.
+ */
+ public static final int STATUS_SUCCESS = 200;
+
+ /**
+ * This request couldn't be parsed. This is also used when processing
+ * requests with unknown/unsupported URI schemes.
+ */
+ public static final int STATUS_BAD_REQUEST = 400;
+
+ /**
+ * The server returned an auth error.
+ */
+ public static final int STATUS_NOT_AUTHORIZED = 401;
+
+ /**
+ * This download can't be performed because the content type cannot be
+ * handled.
+ */
+ public static final int STATUS_NOT_ACCEPTABLE = 406;
+
+ /**
+ * This download cannot be performed because the length cannot be
+ * determined accurately. This is the code for the HTTP error "Length
+ * Required", which is typically used when making requests that require
+ * a content length but don't have one, and it is also used in the
+ * client when a response is received whose length cannot be determined
+ * accurately (therefore making it impossible to know when a download
+ * completes).
+ */
+ public static final int STATUS_LENGTH_REQUIRED = 411;
+
+ /**
+ * This download was interrupted and cannot be resumed.
+ * This is the code for the HTTP error "Precondition Failed", and it is
+ * also used in situations where the client doesn't have an ETag at all.
+ */
+ public static final int STATUS_PRECONDITION_FAILED = 412;
+
+ /**
+ * This download was canceled
+ */
+ public static final int STATUS_CANCELED = 490;
+ /**
+ * @hide
+ * Alternate spelling
+ */
+ public static final int STATUS_CANCELLED = 490;
+
+ /**
+ * This download has completed with an error.
+ * Warning: there will be other status values that indicate errors in
+ * the future. Use isStatusError() to capture the entire category.
+ */
+ public static final int STATUS_UNKNOWN_ERROR = 491;
+ /**
+ * @hide
+ * Legacy name - use STATUS_UNKNOWN_ERROR
+ */
+ public static final int STATUS_ERROR = 491;
+
+ /**
+ * This download couldn't be completed because of a storage issue.
+ * Typically, that's because the filesystem is missing or full.
+ */
+ public static final int STATUS_FILE_ERROR = 492;
+
+ /**
+ * This download couldn't be completed because of an HTTP
+ * redirect code.
+ */
+ public static final int STATUS_UNHANDLED_REDIRECT = 493;
+
+ /**
+ * This download couldn't be completed because of an
+ * unspecified unhandled HTTP code.
+ */
+ public static final int STATUS_UNHANDLED_HTTP_CODE = 494;
+
+ /**
+ * This download couldn't be completed because of an
+ * error receiving or processing data at the HTTP level.
+ */
+ public static final int STATUS_HTTP_DATA_ERROR = 495;
+
+ /**
+ * This download couldn't be completed because of an
+ * HttpException while setting up the request.
+ */
+ public static final int STATUS_HTTP_EXCEPTION = 496;
+
+ /*
+ * Lists the HTTP methods that the download manager can use.
+ */
+
+ /**
+ * GET
+ */
+ public static final int METHOD_GET = 0;
+
+ /**
+ * POST
+ */
+ public static final int METHOD_POST = 1;
+
+ /**
+ * This download is visible but only shows in the notifications
+ * while it's running (a separate download UI would still show it
+ * after completion).
+ */
+ public static final int VISIBILITY_VISIBLE = 0;
+
+ /**
+ * This download is visible and shows in the notifications after
+ * completion.
+ */
+ public static final int VISIBILITY_VISIBLE_NOTIFY_COMPLETED = 1;
+
+ /**
+ * This download doesn't show in the UI or in the notifications.
+ */
+ public static final int VISIBILITY_HIDDEN = 2;
+}
diff --git a/core/java/android/provider/DrmStore.java b/core/java/android/provider/DrmStore.java
new file mode 100644
index 0000000..db71854
--- /dev/null
+++ b/core/java/android/provider/DrmStore.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.provider;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.drm.mobile1.DrmRawContent;
+import android.drm.mobile1.DrmRights;
+import android.drm.mobile1.DrmRightsManager;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * The DRM provider contains forward locked DRM content.
+ *
+ * @hide
+ */
+public final class DrmStore
+{
+ private static final String TAG = "DrmStore";
+
+ public static final String AUTHORITY = "drm";
+
+ /**
+ * This is in the Manifest class of the drm provider, but that isn't visible
+ * in the framework.
+ */
+ private static final String ACCESS_DRM_PERMISSION = "android.permission.ACCESS_DRM";
+
+ /**
+ * Fields for DRM database
+ */
+
+ public interface Columns extends BaseColumns {
+ /**
+ * The data stream for the file
+ * <P>Type: DATA STREAM</P>
+ */
+ public static final String DATA = "_data";
+
+ /**
+ * The size of the file in bytes
+ * <P>Type: INTEGER (long)</P>
+ */
+ public static final String SIZE = "_size";
+
+ /**
+ * The title of the file content
+ * <P>Type: TEXT</P>
+ */
+ public static final String TITLE = "title";
+
+ /**
+ * The MIME type of the file
+ * <P>Type: TEXT</P>
+ */
+ public static final String MIME_TYPE = "mime_type";
+
+ }
+
+ public interface Images extends Columns {
+
+ public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/images");
+ }
+
+ public interface Audio extends Columns {
+
+ public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/audio");
+ }
+
+ /**
+ * Utility function for inserting a file into the DRM content provider.
+ *
+ * @param cr The content resolver to use
+ * @param file The file to insert
+ * @param title The title for the content (or null)
+ * @return uri to the DRM record or null
+ */
+ public static final Intent addDrmFile(ContentResolver cr, File file, String title) {
+ FileInputStream fis = null;
+ OutputStream os = null;
+ Intent result = null;
+
+ try {
+ fis = new FileInputStream(file);
+ DrmRawContent content = new DrmRawContent(fis, (int) file.length(),
+ DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING);
+ String mimeType = content.getContentType();
+
+ DrmRightsManager manager = manager = DrmRightsManager.getInstance();
+ DrmRights rights = manager.queryRights(content);
+ InputStream stream = content.getContentInputStream(rights);
+ long size = stream.available();
+
+ Uri contentUri = null;
+ if (mimeType.startsWith("audio/")) {
+ contentUri = DrmStore.Audio.CONTENT_URI;
+ } else if (mimeType.startsWith("image/")) {
+ contentUri = DrmStore.Images.CONTENT_URI;
+ } else {
+ Log.w(TAG, "unsupported mime type " + mimeType);
+ }
+
+ if (contentUri != null) {
+ ContentValues values = new ContentValues(3);
+ // compute title from file name, if it is not specified
+ if (title == null) {
+ title = file.getName();
+ int lastDot = title.lastIndexOf('.');
+ if (lastDot > 0) {
+ title = title.substring(0, lastDot);
+ }
+ }
+ values.put(DrmStore.Columns.TITLE, title);
+ values.put(DrmStore.Columns.SIZE, size);
+ values.put(DrmStore.Columns.MIME_TYPE, mimeType);
+
+ Uri uri = cr.insert(contentUri, values);
+ if (uri != null) {
+ os = cr.openOutputStream(uri);
+
+ byte[] buffer = new byte[1000];
+ int count;
+
+ while ((count = stream.read(buffer)) != -1) {
+ os.write(buffer, 0, count);
+ }
+ result = new Intent();
+ result.setDataAndType(uri, mimeType);
+
+ }
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "pushing file failed", e);
+ } finally {
+ try {
+ if (fis != null)
+ fis.close();
+ if (os != null)
+ os.close();
+ } catch (IOException e) {
+ Log.e(TAG, "IOException in DrmTest.onCreate()", e);
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Utility function to enforce any permissions required to access DRM
+ * content.
+ *
+ * @param context A context used for checking calling permission.
+ */
+ public static void enforceAccessDrmPermission(Context context) {
+ if (context.checkCallingOrSelfPermission(ACCESS_DRM_PERMISSION) !=
+ PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("Requires DRM permission");
+ }
+ }
+
+}
diff --git a/core/java/android/provider/Gmail.java b/core/java/android/provider/Gmail.java
new file mode 100644
index 0000000..038ba21
--- /dev/null
+++ b/core/java/android/provider/Gmail.java
@@ -0,0 +1,2355 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.provider;
+
+import com.google.android.collect.Lists;
+import com.google.android.collect.Maps;
+import com.google.android.collect.Sets;
+
+import android.content.AsyncQueryHandler;
+import android.content.ContentQueryMap;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.database.DataSetObserver;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.text.Html;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.text.TextUtils.SimpleStringSplitter;
+import android.text.style.CharacterStyle;
+import android.text.util.Regex;
+import android.util.Config;
+import android.util.Log;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Observable;
+import java.util.Observer;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A thin wrapper over the content resolver for accessing the gmail provider.
+ *
+ * @hide
+ */
+public final class Gmail {
+ public static final String GMAIL_AUTH_SERVICE = "mail";
+ // These constants come from google3/java/com/google/caribou/backend/MailLabel.java.
+ public static final String LABEL_SENT = "^f";
+ public static final String LABEL_INBOX = "^i";
+ public static final String LABEL_DRAFT = "^r";
+ public static final String LABEL_UNREAD = "^u";
+ public static final String LABEL_TRASH = "^k";
+ public static final String LABEL_SPAM = "^s";
+ public static final String LABEL_STARRED = "^t";
+ public static final String LABEL_CHAT = "^b"; // 'b' for 'buzz'
+ public static final String LABEL_VOICEMAIL = "^vm";
+ public static final String LABEL_ALL = "^all";
+ // These constants (starting with "^^") are only used locally and are not understood by the
+ // server.
+ public static final String LABEL_VOICEMAIL_INBOX = "^^vmi";
+ public static final String LABEL_CACHED = "^^cached";
+ public static final String LABEL_OUTBOX = "^^out";
+
+ public static final String AUTHORITY = "gmail-ls";
+ private static final String TAG = "gmail-ls";
+ private static final String AUTHORITY_PLUS_CONVERSATIONS =
+ "content://" + AUTHORITY + "/conversations/";
+ private static final String AUTHORITY_PLUS_LABELS =
+ "content://" + AUTHORITY + "/labels/";
+ private static final String AUTHORITY_PLUS_MESSAGES =
+ "content://" + AUTHORITY + "/messages/";
+ private static final String AUTHORITY_PLUS_SETTINGS =
+ "content://" + AUTHORITY + "/settings/";
+
+ public static final Uri BASE_URI = Uri.parse(
+ "content://" + AUTHORITY);
+ private static final Uri LABELS_URI =
+ Uri.parse(AUTHORITY_PLUS_LABELS);
+ private static final Uri CONVERSATIONS_URI =
+ Uri.parse(AUTHORITY_PLUS_CONVERSATIONS);
+ private static final Uri SETTINGS_URI =
+ Uri.parse(AUTHORITY_PLUS_SETTINGS);
+
+ /** Separates email addresses in strings in the database. */
+ public static final String EMAIL_SEPARATOR = "\n";
+ public static final Pattern EMAIL_SEPARATOR_PATTERN = Pattern.compile(EMAIL_SEPARATOR);
+
+ /**
+ * Space-separated lists have separators only between items.
+ */
+ private static final char SPACE_SEPARATOR = ' ';
+ public static final Pattern SPACE_SEPARATOR_PATTERN = Pattern.compile(" ");
+
+ /**
+ * Comma-separated lists have separators between each item, before the first and after the last
+ * item. The empty list is <tt>,</tt>.
+ *
+ * <p>This makes them easier to modify with SQL since it is not a special case to add or
+ * remove the last item. Having a separator on each side of each value also makes it safe to use
+ * SQL's REPLACE to remove an item from a string by using REPLACE(',value,', ',').
+ *
+ * <p>We could use the same separator for both lists but this makes it easier to remember which
+ * kind of list one is dealing with.
+ */
+ private static final char COMMA_SEPARATOR = ',';
+ public static final Pattern COMMA_SEPARATOR_PATTERN = Pattern.compile(",");
+
+ /** Separates attachment info parts in strings in the database. */
+ public static final String ATTACHMENT_INFO_SEPARATOR = "\n";
+ public static final Pattern ATTACHMENT_INFO_SEPARATOR_PATTERN =
+ Pattern.compile(ATTACHMENT_INFO_SEPARATOR);
+
+ public static final Character SENDER_LIST_SEPARATOR = '\n';
+ public static final String SENDER_LIST_TOKEN_ELIDED = "e";
+ public static final String SENDER_LIST_TOKEN_NUM_MESSAGES = "n";
+ public static final String SENDER_LIST_TOKEN_NUM_DRAFTS = "d";
+ public static final String SENDER_LIST_TOKEN_LITERAL = "l";
+ public static final String SENDER_LIST_TOKEN_SENDING = "s";
+ public static final String SENDER_LIST_TOKEN_SEND_FAILED = "f";
+
+ /** Used for finding status in a cursor's extras. */
+ public static final String EXTRA_STATUS = "status";
+
+ public static final String RESPOND_INPUT_COMMAND = "command";
+ public static final String COMMAND_RETRY = "retry";
+ public static final String COMMAND_ACTIVATE = "activate";
+ public static final String RESPOND_OUTPUT_COMMAND_RESPONSE = "commandResponse";
+ public static final String COMMAND_RESPONSE_OK = "ok";
+ public static final String COMMAND_RESPONSE_UNKNOWN = "unknownCommand";
+
+ public static final String INSERT_PARAM_ATTACHMENT_ORIGIN = "origin";
+ public static final String INSERT_PARAM_ATTACHMENT_ORIGIN_EXTRAS = "originExtras";
+
+ private static final Pattern NAME_ADDRESS_PATTERN = Pattern.compile("\"(.*)\"");
+ private static final Pattern UNNAMED_ADDRESS_PATTERN = Pattern.compile("([^<]+)@");
+
+ private static final Map<Integer, Integer> sPriorityToLength = Maps.newHashMap();
+ public static final SimpleStringSplitter sSenderListSplitter =
+ new SimpleStringSplitter(SENDER_LIST_SEPARATOR);
+ public static String[] sSenderFragments = new String[8];
+
+ /**
+ * Returns the name in an address string
+ * @param addressString such as &quot;bobby&quot; &lt;bob@example.com&gt;
+ * @return returns the quoted name in the addressString, otherwise the username from the email
+ * address
+ */
+ public static String getNameFromAddressString(String addressString) {
+ Matcher namedAddressMatch = NAME_ADDRESS_PATTERN.matcher(addressString);
+ if (namedAddressMatch.find()) {
+ String name = namedAddressMatch.group(1);
+ if (name.length() > 0) return name;
+ addressString =
+ addressString.substring(namedAddressMatch.end(), addressString.length());
+ }
+
+ Matcher unnamedAddressMatch = UNNAMED_ADDRESS_PATTERN.matcher(addressString);
+ if (unnamedAddressMatch.find()) {
+ return unnamedAddressMatch.group(1);
+ }
+
+ return addressString;
+ }
+
+ /**
+ * Returns the email address in an address string
+ * @param addressString such as &quot;bobby&quot; &lt;bob@example.com&gt;
+ * @return returns the email address, such as bob@example.com from the example above
+ */
+ public static String getEmailFromAddressString(String addressString) {
+ String result = addressString;
+ Matcher match = Regex.EMAIL_ADDRESS_PATTERN.matcher(addressString);
+ if (match.find()) {
+ result = addressString.substring(match.start(), match.end());
+ }
+
+ return result;
+ }
+
+ /**
+ * Returns whether the label is user-defined (versus system-defined labels such as inbox, whose
+ * names start with "^").
+ */
+ public static boolean isLabelUserDefined(String label) {
+ // TODO: label should never be empty so we should be able to say [label.charAt(0) != '^'].
+ // However, it's a release week and I'm too scared to make that change.
+ return !label.startsWith("^");
+ }
+
+ private static final Set<String> USER_SETTABLE_BUILTIN_LABELS = Sets.newHashSet(
+ Gmail.LABEL_INBOX,
+ Gmail.LABEL_UNREAD,
+ Gmail.LABEL_TRASH,
+ Gmail.LABEL_SPAM,
+ Gmail.LABEL_STARRED);
+
+ /**
+ * Returns whether the label is user-settable. For example, labels such as LABEL_DRAFT should
+ * only be set internally.
+ */
+ public static boolean isLabelUserSettable(String label) {
+ return USER_SETTABLE_BUILTIN_LABELS.contains(label) || isLabelUserDefined(label);
+ }
+
+ /**
+ * Returns the set of labels using the raw labels from a previous getRawLabels()
+ * as input.
+ * @return a copy of the set of labels. To add or remove labels call
+ * MessageCursor.addOrRemoveLabel on each message in the conversation.
+ */
+ public static Set<Long> getLabelIdsFromLabelIdsString(
+ TextUtils.StringSplitter splitter) {
+ Set<Long> labelIds = Sets.newHashSet();
+ for (String labelIdString : splitter) {
+ labelIds.add(Long.valueOf(labelIdString));
+ }
+ return labelIds;
+ }
+
+ /**
+ * @deprecated remove when the activities stop using canonical names to identify labels
+ */
+ public static Set<String> getCanonicalNamesFromLabelIdsString(
+ LabelMap labelMap, TextUtils.StringSplitter splitter) {
+ Set<String> canonicalNames = Sets.newHashSet();
+ for (long labelId : getLabelIdsFromLabelIdsString(splitter)) {
+ final String canonicalName = labelMap.getCanonicalName(labelId);
+ // We will sometimes see labels that the label map does not yet know about or that
+ // do not have names yet.
+ if (!TextUtils.isEmpty(canonicalName)) {
+ canonicalNames.add(canonicalName);
+ } else {
+ Log.w(TAG, "getCanonicalNamesFromLabelIdsString skipping label id: " + labelId);
+ }
+ }
+ return canonicalNames;
+ }
+
+ /**
+ * @return a StringSplitter that is configured to split message label id strings
+ */
+ public static TextUtils.StringSplitter newMessageLabelIdsSplitter() {
+ return new TextUtils.SimpleStringSplitter(SPACE_SEPARATOR);
+ }
+
+ /**
+ * @return a StringSplitter that is configured to split conversation label id strings
+ */
+ public static TextUtils.StringSplitter newConversationLabelIdsSplitter() {
+ return new CommaStringSplitter();
+ }
+
+ /**
+ * A splitter for strings of the form described in the docs for COMMA_SEPARATOR.
+ */
+ private static class CommaStringSplitter extends TextUtils.SimpleStringSplitter {
+
+ public CommaStringSplitter() {
+ super(COMMA_SEPARATOR);
+ }
+
+ @Override
+ public void setString(String string) {
+ // The string should always be at least a single comma.
+ super.setString(string.substring(1));
+ }
+ }
+
+ /**
+ * Creates a single string of the form that getLabelIdsFromLabelIdsString can split.
+ */
+ public static String getLabelIdsStringFromLabelIds(Set<Long> labelIds) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(COMMA_SEPARATOR);
+ for (Long labelId : labelIds) {
+ sb.append(labelId);
+ sb.append(COMMA_SEPARATOR);
+ }
+ return sb.toString();
+ }
+
+ public static final class ConversationColumns {
+ public static final String ID = "_id";
+ public static final String SUBJECT = "subject";
+ public static final String SNIPPET = "snippet";
+ public static final String FROM = "fromAddress";
+ public static final String DATE = "date";
+ public static final String PERSONAL_LEVEL = "personalLevel";
+ /** A list of label names with a space after each one (including the last one). This makes
+ * it easier remove individual labels from this list using SQL. */
+ public static final String LABEL_IDS = "labelIds";
+ public static final String NUM_MESSAGES = "numMessages";
+ public static final String MAX_MESSAGE_ID = "maxMessageId";
+ public static final String HAS_ATTACHMENTS = "hasAttachments";
+ public static final String HAS_MESSAGES_WITH_ERRORS = "hasMessagesWithErrors";
+ public static final String FORCE_ALL_UNREAD = "forceAllUnread";
+
+ private ConversationColumns() {}
+ }
+
+ public static final class MessageColumns {
+
+ public static final String ID = "_id";
+ public static final String MESSAGE_ID = "messageId";
+ public static final String CONVERSATION_ID = "conversation";
+ public static final String SUBJECT = "subject";
+ public static final String SNIPPET = "snippet";
+ public static final String FROM = "fromAddress";
+ public static final String TO = "toAddresses";
+ public static final String CC = "ccAddresses";
+ public static final String BCC = "bccAddresses";
+ public static final String REPLY_TO = "replyToAddresses";
+ public static final String DATE_SENT_MS = "dateSentMs";
+ public static final String DATE_RECEIVED_MS = "dateReceivedMs";
+ public static final String LIST_INFO = "listInfo";
+ public static final String PERSONAL_LEVEL = "personalLevel";
+ public static final String BODY = "body";
+ public static final String EMBEDS_EXTERNAL_RESOURCES = "bodyEmbedsExternalResources";
+ public static final String LABEL_IDS = "labelIds";
+ public static final String JOINED_ATTACHMENT_INFOS = "joinedAttachmentInfos";
+ public static final String ERROR = "error";
+
+ // Fake columns used only for saving or sending messages.
+ public static final String FAKE_SAVE = "save";
+ public static final String FAKE_REF_MESSAGE_ID = "refMessageId";
+
+ private MessageColumns() {}
+ }
+
+ public static final class LabelColumns {
+ public static final String CANONICAL_NAME = "canonicalName";
+ public static final String NAME = "name";
+ public static final String NUM_CONVERSATIONS = "numConversations";
+ public static final String NUM_UNREAD_CONVERSATIONS =
+ "numUnreadConversations";
+
+ private LabelColumns() {}
+ }
+
+ public static final class SettingsColumns {
+ public static final String LABELS_INCLUDED = "labelsIncluded";
+ public static final String LABELS_PARTIAL = "labelsPartial";
+ public static final String CONVERSATION_AGE_DAYS =
+ "conversationAgeDays";
+ public static final String MAX_ATTACHMENET_SIZE_MB =
+ "maxAttachmentSize";
+ }
+
+ // These are the projections that we need when getting cursors from the
+ // content provider.
+ private static String[] CONVERSATION_PROJECTION = {
+ ConversationColumns.ID,
+ ConversationColumns.SUBJECT,
+ ConversationColumns.SNIPPET,
+ ConversationColumns.FROM,
+ ConversationColumns.DATE,
+ ConversationColumns.PERSONAL_LEVEL,
+ ConversationColumns.LABEL_IDS,
+ ConversationColumns.NUM_MESSAGES,
+ ConversationColumns.MAX_MESSAGE_ID,
+ ConversationColumns.HAS_ATTACHMENTS,
+ ConversationColumns.HAS_MESSAGES_WITH_ERRORS,
+ ConversationColumns.FORCE_ALL_UNREAD};
+ private static String[] MESSAGE_PROJECTION = {
+ MessageColumns.ID,
+ MessageColumns.MESSAGE_ID,
+ MessageColumns.CONVERSATION_ID,
+ MessageColumns.SUBJECT,
+ MessageColumns.SNIPPET,
+ MessageColumns.FROM,
+ MessageColumns.TO,
+ MessageColumns.CC,
+ MessageColumns.BCC,
+ MessageColumns.REPLY_TO,
+ MessageColumns.DATE_SENT_MS,
+ MessageColumns.DATE_RECEIVED_MS,
+ MessageColumns.LIST_INFO,
+ MessageColumns.PERSONAL_LEVEL,
+ MessageColumns.BODY,
+ MessageColumns.EMBEDS_EXTERNAL_RESOURCES,
+ MessageColumns.LABEL_IDS,
+ MessageColumns.JOINED_ATTACHMENT_INFOS,
+ MessageColumns.ERROR};
+ private static String[] LABEL_PROJECTION = {
+ BaseColumns._ID,
+ LabelColumns.CANONICAL_NAME,
+ LabelColumns.NAME,
+ LabelColumns.NUM_CONVERSATIONS,
+ LabelColumns.NUM_UNREAD_CONVERSATIONS};
+ private static String[] SETTINGS_PROJECTION = {
+ SettingsColumns.LABELS_INCLUDED,
+ SettingsColumns.LABELS_PARTIAL,
+ SettingsColumns.CONVERSATION_AGE_DAYS,
+ SettingsColumns.MAX_ATTACHMENET_SIZE_MB,
+ };
+
+ private ContentResolver mContentResolver;
+
+ public Gmail(ContentResolver contentResolver) {
+ mContentResolver = contentResolver;
+ }
+
+ /**
+ * Returns source if source is non-null. Returns the empty string otherwise.
+ */
+ private static String toNonnullString(String source) {
+ if (source == null) {
+ return "";
+ } else {
+ return source;
+ }
+ }
+
+ /**
+ * Wraps a Cursor in a ConversationCursor
+ *
+ * @param account the account the cursor is associated with
+ * @param cursor The Cursor to wrap
+ * @return a new ConversationCursor
+ */
+ public ConversationCursor getConversationCursorForCursor(String account, Cursor cursor) {
+ if (TextUtils.isEmpty(account)) {
+ throw new IllegalArgumentException("account is empty");
+ }
+ return new ConversationCursor(this, account, cursor);
+ }
+
+ /**
+ * Asynchronously gets a cursor over all conversations matching a query. The
+ * query is in Gmail's query syntax. When the operation is complete the handler's
+ * onQueryComplete() method is called with the resulting Cursor.
+ *
+ * @param account run the query on this account
+ * @param handler An AsyncQueryHanlder that will be used to run the query
+ * @param token The token to pass to startQuery, which will be passed back to onQueryComplete
+ * @param query a query in Gmail's query syntax
+ */
+ public void runQueryForConversations(String account, AsyncQueryHandler handler, int token,
+ String query) {
+ if (TextUtils.isEmpty(account)) {
+ throw new IllegalArgumentException("account is empty");
+ }
+ handler.startQuery(token, null, Uri.withAppendedPath(CONVERSATIONS_URI, account),
+ CONVERSATION_PROJECTION, query, null, null);
+ }
+
+ /**
+ * Synchronously gets a cursor over all conversations matching a query. The
+ * query is in Gmail's query syntax.
+ *
+ * @param account run the query on this account
+ * @param query a query in Gmail's query syntax
+ */
+ public ConversationCursor getConversationCursorForQuery(String account, String query) {
+ Cursor cursor = mContentResolver.query(
+ Uri.withAppendedPath(CONVERSATIONS_URI, account), CONVERSATION_PROJECTION,
+ query, null, null);
+ return new ConversationCursor(this, account, cursor);
+ }
+
+ /**
+ * Gets a message cursor over the single message with the given id.
+ *
+ * @param account get the cursor for messages in this account
+ * @param messageId the id of the message
+ * @return a cursor over the message
+ */
+ public MessageCursor getMessageCursorForMessageId(String account, long messageId) {
+ if (TextUtils.isEmpty(account)) {
+ throw new IllegalArgumentException("account is empty");
+ }
+ Uri uri = Uri.parse(AUTHORITY_PLUS_MESSAGES + account + "/" + messageId);
+ Cursor cursor = mContentResolver.query(uri, MESSAGE_PROJECTION, null, null, null);
+ return new MessageCursor(this, mContentResolver, account, cursor);
+ }
+
+ /**
+ * Gets a message cursor over the messages that match the query. Note that
+ * this simply finds all of the messages that match and returns them. It
+ * does not return all messages in conversations where any message matches.
+ *
+ * @param account get the cursor for messages in this account
+ * @param query a query in GMail's query syntax. Currently only queries of
+ * the form [label:<label>] are supported
+ * @return a cursor over the messages
+ */
+ public MessageCursor getLocalMessageCursorForQuery(String account, String query) {
+ if (TextUtils.isEmpty(account)) {
+ throw new IllegalArgumentException("account is empty");
+ }
+ Uri uri = Uri.parse(AUTHORITY_PLUS_MESSAGES + account + "/");
+ Cursor cursor = mContentResolver.query(uri, MESSAGE_PROJECTION, query, null, null);
+ return new MessageCursor(this, mContentResolver, account, cursor);
+ }
+
+ /**
+ * Gets a cursor over all of the messages in a conversation.
+ *
+ * @param account get the cursor for messages in this account
+ * @param conversationId the id of the converstion to fetch messages for
+ * @return a cursor over messages in the conversation
+ */
+ public MessageCursor getMessageCursorForConversationId(String account, long conversationId) {
+ if (TextUtils.isEmpty(account)) {
+ throw new IllegalArgumentException("account is empty");
+ }
+ Uri uri = Uri.parse(
+ AUTHORITY_PLUS_CONVERSATIONS + account + "/" + conversationId + "/messages");
+ Cursor cursor = mContentResolver.query(
+ uri, MESSAGE_PROJECTION, null, null, null);
+ return new MessageCursor(this, mContentResolver, account, cursor);
+ }
+
+ /**
+ * Expunge the indicated message. One use of this is to discard drafts.
+ *
+ * @param account the account of the message id
+ * @param messageId the id of the message to expunge
+ */
+ public void expungeMessage(String account, long messageId) {
+ if (TextUtils.isEmpty(account)) {
+ throw new IllegalArgumentException("account is empty");
+ }
+ Uri uri = Uri.parse(AUTHORITY_PLUS_MESSAGES + account + "/" + messageId);
+ mContentResolver.delete(uri, null, null);
+ }
+
+ /**
+ * Adds or removes the label on the conversation.
+ *
+ * @param account the account of the conversation
+ * @param conversationId the conversation
+ * @param maxMessageId the highest message id to whose labels should be changed
+ * @param label the label to add or remove
+ * @param add true to add the label, false to remove it
+ * @throws NonexistentLabelException thrown if the label does not exist
+ */
+ public void addOrRemoveLabelOnConversation(
+ String account, long conversationId, long maxMessageId, String label,
+ boolean add)
+ throws NonexistentLabelException {
+ if (TextUtils.isEmpty(account)) {
+ throw new IllegalArgumentException("account is empty");
+ }
+ if (add) {
+ Uri uri = Uri.parse(
+ AUTHORITY_PLUS_CONVERSATIONS + account + "/" + conversationId + "/labels");
+ ContentValues values = new ContentValues();
+ values.put(LabelColumns.CANONICAL_NAME, label);
+ values.put(ConversationColumns.MAX_MESSAGE_ID, maxMessageId);
+ mContentResolver.insert(uri, values);
+ } else {
+ String encodedLabel;
+ try {
+ encodedLabel = URLEncoder.encode(label, "utf-8");
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException(e);
+ }
+ Uri uri = Uri.parse(
+ AUTHORITY_PLUS_CONVERSATIONS + account + "/"
+ + conversationId + "/labels/" + encodedLabel);
+ mContentResolver.delete(
+ uri, ConversationColumns.MAX_MESSAGE_ID, new String[]{"" + maxMessageId});
+ }
+ }
+
+ /**
+ * Adds or removes the label on the message.
+ *
+ * @param contentResolver the content resolver.
+ * @param account the account of the message
+ * @param conversationId the conversation containing the message
+ * @param messageId the id of the message to whose labels should be changed
+ * @param label the label to add or remove
+ * @param add true to add the label, false to remove it
+ * @throws NonexistentLabelException thrown if the label does not exist
+ */
+ public static void addOrRemoveLabelOnMessage(ContentResolver contentResolver, String account,
+ long conversationId, long messageId, String label, boolean add) {
+
+ // conversationId is unused but we want to start passing it whereever we pass a message id.
+ if (add) {
+ Uri uri = Uri.parse(
+ AUTHORITY_PLUS_MESSAGES + account + "/" + messageId + "/labels");
+ ContentValues values = new ContentValues();
+ values.put(LabelColumns.CANONICAL_NAME, label);
+ contentResolver.insert(uri, values);
+ } else {
+ String encodedLabel;
+ try {
+ encodedLabel = URLEncoder.encode(label, "utf-8");
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException(e);
+ }
+ Uri uri = Uri.parse(
+ AUTHORITY_PLUS_MESSAGES + account + "/" + messageId
+ + "/labels/" + encodedLabel);
+ contentResolver.delete(uri, null, null);
+ }
+ }
+
+ /**
+ * The mail provider will send an intent when certain changes happen in certain labels.
+ * Currently those labels are inbox and voicemail.
+ *
+ * <p>The intent will have the action ACTION_PROVIDER_CHANGED and the extras mentioned below.
+ * The data for the intent will be content://gmail-ls/unread/<name of label>.
+ *
+ * <p>The goal is to support the following user experience:<ul>
+ * <li>When present the new mail indicator reports the number of unread conversations in the
+ * inbox (or some other label).</li>
+ * <li>When the user views the inbox the indicator is removed immediately. They do not have to
+ * read all of the conversations.</li>
+ * <li>If more mail arrives the indicator reappears and shows the total number of unread
+ * conversations in the inbox.</li>
+ * <li>If the user reads the new conversations on the web the indicator disappears on the
+ * phone since there is no unread mail in the inbox that the user hasn't seen.</li>
+ * <li>The phone should vibrate/etc when it transitions from having no unseen unread inbox
+ * mail to having some.</li>
+ */
+
+ /** The account in which the change occurred. */
+ static public final String PROVIDER_CHANGED_EXTRA_ACCOUNT = "account";
+
+ /** The number of unread conversations matching the label. */
+ static public final String PROVIDER_CHANGED_EXTRA_COUNT = "count";
+
+ /** Whether to get the user's attention, perhaps by vibrating. */
+ static public final String PROVIDER_CHANGED_EXTRA_GET_ATTENTION = "getAttention";
+
+ /**
+ * A label that is attached to all of the conversations being notified about. This enables the
+ * receiver of a notification to get a list of matching conversations.
+ */
+ static public final String PROVIDER_CHANGED_EXTRA_TAG_LABEL = "tagLabel";
+
+ /**
+ * Settings for which conversations should be synced to the phone.
+ * Conversations are synced if any message matches any of the following
+ * criteria:
+ *
+ * <ul>
+ * <li>the message has a label in the include set</li>
+ * <li>the message is no older than conversationAgeDays and has a label in the partial set.
+ * </li>
+ * <li>also, pending changes on the server: the message has no user-controllable labels.</li>
+ * </ul>
+ *
+ * <p>A user-controllable label is a user-defined label or star, inbox,
+ * trash, spam, etc. LABEL_UNREAD is not considered user-controllable.
+ */
+ public static class Settings {
+ public long conversationAgeDays;
+ public long maxAttachmentSizeMb;
+ public String[] labelsIncluded;
+ public String[] labelsPartial;
+ }
+
+ /**
+ * Returns the settings.
+ * @param account the account whose setting should be retrieved
+ */
+ public Settings getSettings(String account) {
+ if (TextUtils.isEmpty(account)) {
+ throw new IllegalArgumentException("account is empty");
+ }
+ Settings settings = new Settings();
+ Cursor cursor = mContentResolver.query(
+ Uri.withAppendedPath(SETTINGS_URI, account), SETTINGS_PROJECTION, null, null, null);
+ cursor.moveToNext();
+ settings.labelsIncluded = TextUtils.split(cursor.getString(0), SPACE_SEPARATOR_PATTERN);
+ settings.labelsPartial = TextUtils.split(cursor.getString(1), SPACE_SEPARATOR_PATTERN);
+ settings.conversationAgeDays = Long.parseLong(cursor.getString(2));
+ settings.maxAttachmentSizeMb = Long.parseLong(cursor.getString(3));
+ cursor.close();
+ return settings;
+ }
+
+ /**
+ * Sets the settings. A sync will be scheduled automatically.
+ */
+ public void setSettings(String account, Settings settings) {
+ if (TextUtils.isEmpty(account)) {
+ throw new IllegalArgumentException("account is empty");
+ }
+ ContentValues values = new ContentValues();
+ values.put(
+ SettingsColumns.LABELS_INCLUDED,
+ TextUtils.join(" ", settings.labelsIncluded));
+ values.put(
+ SettingsColumns.LABELS_PARTIAL,
+ TextUtils.join(" ", settings.labelsPartial));
+ values.put(
+ SettingsColumns.CONVERSATION_AGE_DAYS,
+ settings.conversationAgeDays);
+ values.put(
+ SettingsColumns.MAX_ATTACHMENET_SIZE_MB,
+ settings.maxAttachmentSizeMb);
+ mContentResolver.update(Uri.withAppendedPath(SETTINGS_URI, account), values, null, null);
+ }
+
+ /**
+ * Uses sender instructions to build a formatted string.
+ *
+ * <p>Sender list instructions contain compact information about the sender list. Most work that
+ * can be done without knowing how much room will be availble for the sender list is done when
+ * creating the instructions.
+ *
+ * <p>The instructions string consists of tokens separated by SENDER_LIST_SEPARATOR. Here are
+ * the tokens, one per line:<ul>
+ * <li><tt>n</tt></li>
+ * <li><em>int</em>, the number of non-draft messages in the conversation</li>
+ * <li><tt>d</tt</li>
+ * <li><em>int</em>, the number of drafts in the conversation</li>
+ * <li><tt>l</tt></li>
+ * <li><em>literal html to be included in the output</em></li>
+ * <li><tt>s</tt> indicates that the message is sending (in the outbox without errors)</li>
+ * <li><tt>f</tt> indicates that the message failed to send (in the outbox with errors)</li>
+ * <li><em>for each message</em><ul>
+ * <li><em>int</em>, 0 for read, 1 for unread</li>
+ * <li><em>int</em>, the priority of the message. Zero is the most important</li>
+ * <li><em>text</em>, the sender text or blank for messages from 'me'</li>
+ * </ul></li>
+ * <li><tt>e</tt> to indicate that one or more messages have been elided</li>
+ *
+ * <p>The instructions indicate how many messages and drafts are in the conversation and then
+ * describe the most important messages in order, indicating the priority of each message and
+ * whether the message is unread.
+ *
+ * @param instructions instructions as described above
+ * @param sb the SpannableStringBuilder to append to
+ * @param maxChars the number of characters available to display the text
+ * @param unreadStyle the CharacterStyle for unread messages, or null
+ * @param draftsStyle the CharacterStyle for draft messages, or null
+ * @param sendingString the string to use when there are messages scheduled to be sent
+ * @param sendFailedString the string to use when there are messages that mailed to send
+ * @param meString the string to use for messages sent by this user
+ * @param draftString the string to use for "Draft"
+ * @param draftPluralString the string to use for "Drafts"
+ */
+ public static void getSenderSnippet(
+ String instructions, SpannableStringBuilder sb, int maxChars,
+ CharacterStyle unreadStyle,
+ CharacterStyle draftsStyle,
+ CharSequence meString, CharSequence draftString, CharSequence draftPluralString,
+ CharSequence sendingString, CharSequence sendFailedString,
+ boolean forceAllUnread, boolean forceAllRead) {
+ assert !(forceAllUnread && forceAllRead);
+ boolean unreadStatusIsForced = forceAllUnread || forceAllRead;
+ boolean forcedUnreadStatus = forceAllUnread;
+
+ // Measure each fragment. It's ok to iterate over the entire set of fragments because it is
+ // never a long list, even if there are many senders.
+ final Map<Integer, Integer> priorityToLength = sPriorityToLength;
+ priorityToLength.clear();
+
+ int maxFoundPriority = Integer.MIN_VALUE;
+ String numMessagesFragment = "";
+ CharSequence draftsFragment = "";
+ CharSequence sendingFragment = "";
+ CharSequence sendFailedFragment = "";
+
+ sSenderListSplitter.setString(instructions);
+ int numFragments = 0;
+ String[] fragments = sSenderFragments;
+ int currentSize = fragments.length;
+ while (sSenderListSplitter.hasNext()) {
+ fragments[numFragments++] = sSenderListSplitter.next();
+ if (numFragments == currentSize) {
+ sSenderFragments = new String[2 * currentSize];
+ System.arraycopy(fragments, 0, sSenderFragments, 0, currentSize);
+ currentSize *= 2;
+ fragments = sSenderFragments;
+ }
+ }
+
+ for (int i = 0; i < numFragments;) {
+ String fragment0 = fragments[i++];
+ if ("".equals(fragment0)) {
+ // This should be the final fragment.
+ } else if (Gmail.SENDER_LIST_TOKEN_ELIDED.equals(fragment0)) {
+ // ignore
+ } else if (Gmail.SENDER_LIST_TOKEN_NUM_MESSAGES.equals(fragment0)) {
+ numMessagesFragment = " (" + fragments[i++] + ")";
+ } else if (Gmail.SENDER_LIST_TOKEN_NUM_DRAFTS.equals(fragment0)) {
+ String numDraftsString = fragments[i++];
+ int numDrafts = Integer.parseInt(numDraftsString);
+ draftsFragment = numDrafts == 1 ? draftString :
+ draftPluralString + " (" + numDraftsString + ")";
+ } else if (Gmail.SENDER_LIST_TOKEN_LITERAL.equals(fragment0)) {
+ sb.append(Html.fromHtml(fragments[i++]));
+ return;
+ } else if (Gmail.SENDER_LIST_TOKEN_SENDING.equals(fragment0)) {
+ sendingFragment = sendingString;
+ } else if (Gmail.SENDER_LIST_TOKEN_SEND_FAILED.equals(fragment0)) {
+ sendFailedFragment = sendFailedString;
+ } else {
+ String priorityString = fragments[i++];
+ CharSequence nameString = fragments[i++];
+ if (nameString.length() == 0) nameString = meString;
+ int priority = Integer.parseInt(priorityString);
+ priorityToLength.put(priority, nameString.length());
+ maxFoundPriority = Math.max(maxFoundPriority, priority);
+ }
+ }
+
+ // Don't allocate fixedFragment unless we need it
+ SpannableStringBuilder fixedFragment = null;
+ int fixedFragmentLength = 0;
+ if (draftsFragment.length() != 0) {
+ if (fixedFragment == null) {
+ fixedFragment = new SpannableStringBuilder();
+ }
+ fixedFragment.append(draftsFragment);
+ if (draftsStyle != null) {
+ fixedFragment.setSpan(
+ CharacterStyle.wrap(draftsStyle),
+ 0, fixedFragment.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+ if (sendingFragment.length() != 0) {
+ if (fixedFragment == null) {
+ fixedFragment = new SpannableStringBuilder();
+ }
+ if (fixedFragment.length() != 0) fixedFragment.append(", ");
+ fixedFragment.append(sendingFragment);
+ }
+ if (sendFailedFragment.length() != 0) {
+ if (fixedFragment == null) {
+ fixedFragment = new SpannableStringBuilder();
+ }
+ if (fixedFragment.length() != 0) fixedFragment.append(", ");
+ fixedFragment.append(sendFailedFragment);
+ }
+
+ if (fixedFragment != null) {
+ fixedFragmentLength = fixedFragment.length();
+ }
+
+ final boolean normalMessagesExist =
+ numMessagesFragment.length() != 0 || maxFoundPriority != Integer.MIN_VALUE;
+ String preFixedFragement = "";
+ if (normalMessagesExist && fixedFragmentLength != 0) {
+ preFixedFragement = ", ";
+ }
+ int maxPriorityToInclude = -1; // inclusive
+ int numCharsUsed =
+ numMessagesFragment.length() + preFixedFragement.length() + fixedFragmentLength;
+ int numSendersUsed = 0;
+ while (maxPriorityToInclude < maxFoundPriority) {
+ if (priorityToLength.containsKey(maxPriorityToInclude + 1)) {
+ int length = numCharsUsed + priorityToLength.get(maxPriorityToInclude + 1);
+ if (numCharsUsed > 0) length += 2;
+ // We must show at least two senders if they exist. If we don't have space for both
+ // then we will truncate names.
+ if (length > maxChars && numSendersUsed >= 2) {
+ break;
+ }
+ numCharsUsed = length;
+ numSendersUsed++;
+ }
+ maxPriorityToInclude++;
+ }
+
+ int numCharsToRemovePerWord = 0;
+ if (numCharsUsed > maxChars) {
+ numCharsToRemovePerWord = (numCharsUsed - maxChars) / numSendersUsed;
+ }
+
+ boolean elided = false;
+ for (int i = 0; i < numFragments;) {
+ String fragment0 = fragments[i++];
+ if ("".equals(fragment0)) {
+ // This should be the final fragment.
+ } else if (SENDER_LIST_TOKEN_ELIDED.equals(fragment0)) {
+ elided = true;
+ } else if (SENDER_LIST_TOKEN_NUM_MESSAGES.equals(fragment0)) {
+ i++;
+ } else if (SENDER_LIST_TOKEN_NUM_DRAFTS.equals(fragment0)) {
+ i++;
+ } else if (SENDER_LIST_TOKEN_SENDING.equals(fragment0)) {
+ } else if (SENDER_LIST_TOKEN_SEND_FAILED.equals(fragment0)) {
+ } else {
+ final String unreadString = fragment0;
+ final String priorityString = fragments[i++];
+ String nameString = fragments[i++];
+ if (nameString.length() == 0) nameString = meString.toString();
+ if (numCharsToRemovePerWord != 0) {
+ nameString = nameString.substring(
+ 0, Math.max(nameString.length() - numCharsToRemovePerWord, 0));
+ }
+ final boolean unread = unreadStatusIsForced
+ ? forcedUnreadStatus : Integer.parseInt(unreadString) != 0;
+ final int priority = Integer.parseInt(priorityString);
+ if (priority <= maxPriorityToInclude) {
+ if (sb.length() != 0) {
+ sb.append(elided ? " .. " : ", ");
+ }
+ elided = false;
+ int pos = sb.length();
+ sb.append(nameString);
+ if (unread && unreadStyle != null) {
+ sb.setSpan(CharacterStyle.wrap(unreadStyle),
+ pos, sb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ } else {
+ elided = true;
+ }
+ }
+ }
+ sb.append(numMessagesFragment);
+ if (fixedFragmentLength != 0) {
+ sb.append(preFixedFragement);
+ sb.append(fixedFragment);
+ }
+ }
+
+ /**
+ * This is a cursor that only defines methods to move throught the results
+ * and register to hear about changes. All access to the data is left to
+ * subinterfaces.
+ */
+ public static class MailCursor extends ContentObserver {
+
+ // A list of observers of this cursor.
+ private Set<MailCursorObserver> mObservers;
+
+ // Updated values are accumulated here before being written out if the
+ // cursor is asked to persist the changes.
+ private ContentValues mUpdateValues;
+
+ protected Cursor mCursor;
+ protected String mAccount;
+
+ public Cursor getCursor() {
+ return mCursor;
+ }
+
+ /**
+ * Constructs the MailCursor given a regular cursor, registering as a
+ * change observer of the cursor.
+ * @param account the account the cursor is associated with
+ * @param cursor the underlying cursor
+ */
+ protected MailCursor(String account, Cursor cursor) {
+ super(new Handler());
+ mObservers = new HashSet<MailCursorObserver>();
+ mCursor = cursor;
+ mAccount = account;
+ if (mCursor != null) mCursor.registerContentObserver(this);
+ }
+
+ /**
+ * Gets the account associated with this cursor.
+ * @return the account.
+ */
+ public String getAccount() {
+ return mAccount;
+ }
+
+ protected void checkThread() {
+ // Turn this on when activity code no longer runs in the sync thread
+ // after notifications of changes.
+// Thread currentThread = Thread.currentThread();
+// if (currentThread != mThread) {
+// throw new RuntimeException("Accessed from the wrong thread");
+// }
+ }
+
+ /**
+ * Lazily constructs a map of update values to apply to the database
+ * if requested. This map is cleared out when we move to a different
+ * item in the result set.
+ *
+ * @return a map of values to be applied by an update.
+ */
+ protected ContentValues getUpdateValues() {
+ if (mUpdateValues == null) {
+ mUpdateValues = new ContentValues();
+ }
+ return mUpdateValues;
+ }
+
+ /**
+ * Called whenever mCursor is changed to point to a different row.
+ * Subclasses should override this if they need to clear out state
+ * when this happens.
+ *
+ * Subclasses must call the inherited version if they override this.
+ */
+ protected void onCursorPositionChanged() {
+ mUpdateValues = null;
+ }
+
+ // ********* MailCursor
+
+ /**
+ * Returns the numbers of rows in the cursor.
+ *
+ * @return the number of rows in the cursor.
+ */
+ final public int count() {
+ if (mCursor != null) {
+ return mCursor.getCount();
+ } else {
+ return 0;
+ }
+ }
+
+ /**
+ * @return the current position of this cursor, or -1 if this cursor
+ * has not been initialized.
+ */
+ final public int position() {
+ if (mCursor != null) {
+ return mCursor.getPosition();
+ } else {
+ return -1;
+ }
+ }
+
+ /**
+ * Move the cursor to an absolute position. The valid
+ * range of vaues is -1 &lt;= position &lt;= count.
+ *
+ * <p>This method will return true if the request destination was
+ * reachable, otherwise it returns false.
+ *
+ * @param position the zero-based position to move to.
+ * @return whether the requested move fully succeeded.
+ */
+ final public boolean moveTo(int position) {
+ checkCursor();
+ checkThread();
+ boolean moved = mCursor.moveToPosition(position);
+ if (moved) onCursorPositionChanged();
+ return moved;
+ }
+
+ /**
+ * Move the cursor to the next row.
+ *
+ * <p>This method will return false if the cursor is already past the
+ * last entry in the result set.
+ *
+ * @return whether the move succeeded.
+ */
+ final public boolean next() {
+ checkCursor();
+ checkThread();
+ boolean moved = mCursor.moveToNext();
+ if (moved) onCursorPositionChanged();
+ return moved;
+ }
+
+ /**
+ * Release all resources and locks associated with the cursor. The
+ * cursor will not be valid after this function is called.
+ */
+ final public void release() {
+ if (mCursor != null) {
+ mCursor.unregisterContentObserver(this);
+ mCursor.deactivate();
+ }
+ }
+
+ final public void registerContentObserver(ContentObserver observer) {
+ mCursor.registerContentObserver(observer);
+ }
+
+ final public void unregisterContentObserver(ContentObserver observer) {
+ mCursor.unregisterContentObserver(observer);
+ }
+
+ final public void registerDataSetObserver(DataSetObserver observer) {
+ mCursor.registerDataSetObserver(observer);
+ }
+
+ final public void unregisterDataSetObserver(DataSetObserver observer) {
+ mCursor.unregisterDataSetObserver(observer);
+ }
+
+ /**
+ * Register an observer to hear about changes to the cursor.
+ *
+ * @param observer the observer to register
+ */
+ final public void registerObserver(MailCursorObserver observer) {
+ mObservers.add(observer);
+ }
+
+ /**
+ * Unregister an observer.
+ *
+ * @param observer the observer to unregister
+ */
+ final public void unregisterObserver(MailCursorObserver observer) {
+ mObservers.remove(observer);
+ }
+
+ // ********* ContentObserver
+
+ @Override
+ final public boolean deliverSelfNotifications() {
+ return false;
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ if (Config.DEBUG) {
+ Log.d(TAG, "MailCursor is notifying " + mObservers.size() + " observers");
+ }
+ for (MailCursorObserver o: mObservers) {
+ o.onCursorChanged(this);
+ }
+ }
+
+ protected void checkCursor() {
+ if (mCursor == null) {
+ throw new IllegalStateException(
+ "cannot read from an insertion cursor");
+ }
+ }
+
+ /**
+ * Returns the string value of the column, or "" if the value is null.
+ */
+ protected String getStringInColumn(int columnIndex) {
+ checkCursor();
+ return toNonnullString(mCursor.getString(columnIndex));
+ }
+ }
+
+ /**
+ * A MailCursor observer is notified of changes to the result set of a
+ * cursor.
+ */
+ public interface MailCursorObserver {
+
+ /**
+ * Called when the result set of a cursor has changed.
+ *
+ * @param cursor the cursor whose result set has changed.
+ */
+ void onCursorChanged(MailCursor cursor);
+ }
+
+ /**
+ * Thrown when an operation is requested with a label that does not exist.
+ *
+ * TODO: this is here because I wanted a checked exception. However, I don't
+ * think that that is appropriate. In fact, I don't think that we should
+ * throw an exception at all because the label might have been valid when
+ * the caller presented it to the user but removed as a result of a sync.
+ * Maybe we should kill this and eat the errors.
+ */
+ public static class NonexistentLabelException extends Exception {
+ // TODO: Add label name?
+ }
+
+ /**
+ * A cursor over labels.
+ */
+ public final class LabelCursor extends MailCursor {
+
+ private int mNameIndex;
+ private int mNumConversationsIndex;
+ private int mNumUnreadConversationsIndex;
+
+ private LabelCursor(String account, Cursor cursor) {
+ super(account, cursor);
+
+ mNameIndex = mCursor.getColumnIndexOrThrow(LabelColumns.CANONICAL_NAME);
+ mNumConversationsIndex =
+ mCursor.getColumnIndexOrThrow(LabelColumns.NUM_CONVERSATIONS);
+ mNumUnreadConversationsIndex = mCursor.getColumnIndexOrThrow(
+ LabelColumns.NUM_UNREAD_CONVERSATIONS);
+ }
+
+ /**
+ * Gets the canonical name of the current label.
+ *
+ * @return the current label's name.
+ */
+ public String getName() {
+ return getStringInColumn(mNameIndex);
+ }
+
+ /**
+ * Gets the number of conversations with this label.
+ *
+ * @return the number of conversations with this label.
+ */
+ public int getNumConversations() {
+ return mCursor.getInt(mNumConversationsIndex);
+ }
+
+ /**
+ * Gets the number of unread conversations with this label.
+ *
+ * @return the number of unread conversations with this label.
+ */
+ public int getNumUnreadConversations() {
+ return mCursor.getInt(mNumUnreadConversationsIndex);
+ }
+ }
+
+ /**
+ * This is a map of labels. TODO: make it observable.
+ */
+ public static final class LabelMap extends Observable {
+ private final static ContentValues EMPTY_CONTENT_VALUES = new ContentValues();
+
+ private ContentQueryMap mQueryMap;
+ private SortedSet<String> mSortedUserLabels;
+ private Map<String, Long> mCanonicalNameToId;
+
+ private long mLabelIdSent;
+ private long mLabelIdInbox;
+ private long mLabelIdDraft;
+ private long mLabelIdUnread;
+ private long mLabelIdTrash;
+ private long mLabelIdSpam;
+ private long mLabelIdStarred;
+ private long mLabelIdChat;
+ private long mLabelIdVoicemail;
+ private long mLabelIdVoicemailInbox;
+ private long mLabelIdCached;
+ private long mLabelIdOutbox;
+
+ private boolean mLabelsSynced = false;
+
+ public LabelMap(ContentResolver contentResolver, String account, boolean keepUpdated) {
+ if (TextUtils.isEmpty(account)) {
+ throw new IllegalArgumentException("account is empty");
+ }
+ Cursor cursor = contentResolver.query(
+ Uri.withAppendedPath(LABELS_URI, account), LABEL_PROJECTION, null, null, null);
+ init(cursor, keepUpdated);
+ }
+
+ public LabelMap(Cursor cursor, boolean keepUpdated) {
+ init(cursor, keepUpdated);
+ }
+
+ private void init(Cursor cursor, boolean keepUpdated) {
+ mQueryMap = new ContentQueryMap(cursor, BaseColumns._ID, keepUpdated, null);
+ mSortedUserLabels = new TreeSet<String>(java.text.Collator.getInstance());
+ mCanonicalNameToId = Maps.newHashMap();
+ updateDataStructures();
+ mQueryMap.addObserver(new Observer() {
+ public void update(Observable observable, Object data) {
+ updateDataStructures();
+ setChanged();
+ notifyObservers();
+ }
+ });
+ }
+
+ /**
+ * @return whether at least some labels have been synced.
+ */
+ public boolean labelsSynced() {
+ return mLabelsSynced;
+ }
+
+ /**
+ * Updates the data structures that are maintained separately from mQueryMap after the query
+ * map has changed.
+ */
+ private void updateDataStructures() {
+ mSortedUserLabels.clear();
+ mCanonicalNameToId.clear();
+ for (Map.Entry<String, ContentValues> row : mQueryMap.getRows().entrySet()) {
+ long labelId = Long.valueOf(row.getKey());
+ String canonicalName = row.getValue().getAsString(LabelColumns.CANONICAL_NAME);
+ if (isLabelUserDefined(canonicalName)) {
+ mSortedUserLabels.add(canonicalName);
+ }
+ mCanonicalNameToId.put(canonicalName, labelId);
+
+ if (LABEL_SENT.equals(canonicalName)) {
+ mLabelIdSent = labelId;
+ } else if (LABEL_INBOX.equals(canonicalName)) {
+ mLabelIdInbox = labelId;
+ } else if (LABEL_DRAFT.equals(canonicalName)) {
+ mLabelIdDraft = labelId;
+ } else if (LABEL_UNREAD.equals(canonicalName)) {
+ mLabelIdUnread = labelId;
+ } else if (LABEL_TRASH.equals(canonicalName)) {
+ mLabelIdTrash = labelId;
+ } else if (LABEL_SPAM.equals(canonicalName)) {
+ mLabelIdSpam = labelId;
+ } else if (LABEL_STARRED.equals(canonicalName)) {
+ mLabelIdStarred = labelId;
+ } else if (LABEL_CHAT.equals(canonicalName)) {
+ mLabelIdChat = labelId;
+ } else if (LABEL_VOICEMAIL.equals(canonicalName)) {
+ mLabelIdVoicemail = labelId;
+ } else if (LABEL_VOICEMAIL_INBOX.equals(canonicalName)) {
+ mLabelIdVoicemailInbox = labelId;
+ } else if (LABEL_CACHED.equals(canonicalName)) {
+ mLabelIdCached = labelId;
+ } else if (LABEL_OUTBOX.equals(canonicalName)) {
+ mLabelIdOutbox = labelId;
+ }
+ mLabelsSynced = mLabelIdSent != 0
+ && mLabelIdInbox != 0
+ && mLabelIdDraft != 0
+ && mLabelIdUnread != 0
+ && mLabelIdTrash != 0
+ && mLabelIdSpam != 0
+ && mLabelIdStarred != 0
+ && mLabelIdChat != 0
+ && mLabelIdVoicemail != 0;
+ }
+ }
+
+ public long getLabelIdSent() {
+ checkLabelsSynced();
+ return mLabelIdSent;
+ }
+
+ public long getLabelIdInbox() {
+ checkLabelsSynced();
+ return mLabelIdInbox;
+ }
+
+ public long getLabelIdDraft() {
+ checkLabelsSynced();
+ return mLabelIdDraft;
+ }
+
+ public long getLabelIdUnread() {
+ checkLabelsSynced();
+ return mLabelIdUnread;
+ }
+
+ public long getLabelIdTrash() {
+ checkLabelsSynced();
+ return mLabelIdTrash;
+ }
+
+ public long getLabelIdSpam() {
+ checkLabelsSynced();
+ return mLabelIdSpam;
+ }
+
+ public long getLabelIdStarred() {
+ checkLabelsSynced();
+ return mLabelIdStarred;
+ }
+
+ public long getLabelIdChat() {
+ checkLabelsSynced();
+ return mLabelIdChat;
+ }
+
+ public long getLabelIdVoicemail() {
+ checkLabelsSynced();
+ return mLabelIdVoicemail;
+ }
+
+ public long getLabelIdVoicemailInbox() {
+ checkLabelsSynced();
+ return mLabelIdVoicemailInbox;
+ }
+
+ public long getLabelIdCached() {
+ checkLabelsSynced();
+ return mLabelIdCached;
+ }
+
+ public long getLabelIdOutbox() {
+ checkLabelsSynced();
+ return mLabelIdOutbox;
+ }
+
+ private void checkLabelsSynced() {
+ if (!labelsSynced()) {
+ throw new IllegalStateException("LabelMap not initalized");
+ }
+ }
+
+ /** Returns the list of user-defined labels in alphabetical order. */
+ public SortedSet<String> getSortedUserLabels() {
+ return mSortedUserLabels;
+ }
+
+ private static final List<String> SORTED_USER_MEANINGFUL_SYSTEM_LABELS =
+ Lists.newArrayList(
+ LABEL_INBOX, LABEL_STARRED, LABEL_CHAT, LABEL_SENT,
+ LABEL_OUTBOX, LABEL_DRAFT, LABEL_ALL,
+ LABEL_SPAM, LABEL_TRASH);
+
+ public static List<String> getSortedUserMeaningfulSystemLabels() {
+ return SORTED_USER_MEANINGFUL_SYSTEM_LABELS;
+ }
+
+ private static final Set<String> FORCED_INCLUDED_LABELS =
+ Sets.newHashSet(LABEL_OUTBOX, LABEL_DRAFT);
+
+ public static Set<String> getForcedIncludedLabels() {
+ return FORCED_INCLUDED_LABELS;
+ }
+
+ private static final Set<String> FORCED_INCLUDED_OR_PARTIAL_LABELS =
+ Sets.newHashSet(LABEL_INBOX);
+
+ public static Set<String> getForcedIncludedOrPartialLabels() {
+ return FORCED_INCLUDED_OR_PARTIAL_LABELS;
+ }
+
+ private static final Set<String> FORCED_UNSYNCED_LABELS =
+ Sets.newHashSet(LABEL_ALL, LABEL_CHAT, LABEL_SPAM, LABEL_TRASH);
+
+ public static Set<String> getForcedUnsyncedLabels() {
+ return FORCED_UNSYNCED_LABELS;
+ }
+
+ /**
+ * Returns the number of conversation with a given label.
+ * @deprecated
+ */
+ public int getNumConversations(String label) {
+ return getNumConversations(getLabelId(label));
+ }
+
+ /** Returns the number of conversation with a given label. */
+ public int getNumConversations(long labelId) {
+ return getLabelIdValues(labelId).getAsInteger(LabelColumns.NUM_CONVERSATIONS);
+ }
+
+ /**
+ * Returns the number of unread conversation with a given label.
+ * @deprecated
+ */
+ public int getNumUnreadConversations(String label) {
+ return getNumUnreadConversations(getLabelId(label));
+ }
+
+ /** Returns the number of unread conversation with a given label. */
+ public int getNumUnreadConversations(long labelId) {
+ return getLabelIdValues(labelId).getAsInteger(LabelColumns.NUM_UNREAD_CONVERSATIONS);
+ }
+
+ /**
+ * @return the canonical name for a label
+ */
+ public String getCanonicalName(long labelId) {
+ return getLabelIdValues(labelId).getAsString(LabelColumns.CANONICAL_NAME);
+ }
+
+ /**
+ * @return the human name for a label
+ */
+ public String getName(long labelId) {
+ return getLabelIdValues(labelId).getAsString(LabelColumns.NAME);
+ }
+
+ /**
+ * @return whether a given label is known
+ */
+ public boolean hasLabel(long labelId) {
+ return mQueryMap.getRows().containsKey(Long.toString(labelId));
+ }
+
+ /**
+ * @return returns the id of a label given the canonical name
+ * @deprecated this is only needed because most of the UI uses label names instead of ids
+ */
+ public long getLabelId(String canonicalName) {
+ if (mCanonicalNameToId.containsKey(canonicalName)) {
+ return mCanonicalNameToId.get(canonicalName);
+ } else {
+ throw new IllegalArgumentException("Unknown canonical name: " + canonicalName);
+ }
+ }
+
+ private ContentValues getLabelIdValues(long labelId) {
+ final ContentValues values = mQueryMap.getValues(Long.toString(labelId));
+ if (values != null) {
+ return values;
+ } else {
+ return EMPTY_CONTENT_VALUES;
+ }
+ }
+
+ /** Force the map to requery. This should not be necessary outside tests. */
+ public void requery() {
+ mQueryMap.requery();
+ }
+
+ public void close() {
+ mQueryMap.close();
+ }
+ }
+
+ private Map<String, Gmail.LabelMap> mLabelMaps = Maps.newHashMap();
+
+ public LabelMap getLabelMap(String account) {
+ Gmail.LabelMap labelMap = mLabelMaps.get(account);
+ if (labelMap == null) {
+ labelMap = new Gmail.LabelMap(mContentResolver, account, true /* keepUpdated */);
+ mLabelMaps.put(account, labelMap);
+ }
+ return labelMap;
+ }
+
+ public enum PersonalLevel {
+ NOT_TO_ME(0),
+ TO_ME_AND_OTHERS(1),
+ ONLY_TO_ME(2);
+
+ private int mLevel;
+
+ PersonalLevel(int level) {
+ mLevel = level;
+ }
+
+ public int toInt() {
+ return mLevel;
+ }
+
+ public static PersonalLevel fromInt(int level) {
+ switch (level) {
+ case 0: return NOT_TO_ME;
+ case 1: return TO_ME_AND_OTHERS;
+ case 2: return ONLY_TO_ME;
+ default:
+ throw new IllegalArgumentException(
+ level + " is not a personal level");
+ }
+ }
+ }
+
+ /**
+ * Indicates a version of an attachment.
+ */
+ public enum AttachmentRendition {
+ /**
+ * The full version of an attachment if it can be handled on the device, otherwise the
+ * preview.
+ */
+ BEST,
+
+ /** A smaller or simpler version of the attachment, such as a scaled-down image or an HTML
+ * version of a document. Not always available.
+ */
+ SIMPLE,
+ }
+
+ /**
+ * The columns that can be requested when querying an attachment's download URI. See
+ * getAttachmentDownloadUri.
+ */
+ public static final class AttachmentColumns implements BaseColumns {
+
+ /** Contains a STATUS value from {@link android.provider.Downloads} */
+ public static final String STATUS = "status";
+
+ /**
+ * The name of the file to open (with ContentProvider.open). If this is empty then continue
+ * to use the attachment's URI.
+ *
+ * TODO: I'm not sure that we need this. See the note in CL 66853-p9.
+ */
+ public static final String FILENAME = "filename";
+ }
+
+ /**
+ * We track where an attachment came from so that we know how to download it and include it
+ * in new messages.
+ */
+ public enum AttachmentOrigin {
+ /** Extras are "<conversationId>-<messageId>-<partId>". */
+ SERVER_ATTACHMENT,
+ /** Extras are "<path>". */
+ LOCAL_FILE;
+
+ private static final String SERVER_EXTRAS_SEPARATOR = "_";
+
+ public static String serverExtras(
+ long conversationId, long messageId, String partId) {
+ return conversationId + SERVER_EXTRAS_SEPARATOR
+ + messageId + SERVER_EXTRAS_SEPARATOR + partId;
+ }
+
+ /**
+ * @param extras extras as returned by serverExtras
+ * @return an array of conversationId, messageId, partId (all as strings)
+ */
+ public static String[] splitServerExtras(String extras) {
+ return TextUtils.split(extras, SERVER_EXTRAS_SEPARATOR);
+ }
+
+ public static String localFileExtras(Uri path) {
+ return path.toString();
+ }
+ }
+
+ public static final class Attachment {
+ /** Identifies the attachment uniquely when combined wih a message id.*/
+ public String partId;
+
+ /** The intended filename of the attachment.*/
+ public String name;
+
+ /** The native content type.*/
+ public String contentType;
+
+ /** The size of the attachment in its native form.*/
+ public int size;
+
+ /**
+ * The content type of the simple version of the attachment. Blank if no simple version is
+ * available.
+ */
+ public String simpleContentType;
+
+ public AttachmentOrigin origin;
+
+ public String originExtras;
+
+ public String toJoinedString() {
+ return TextUtils.join(
+ "|", Lists.newArrayList(partId == null ? "" : partId,
+ name.replace("|", ""), contentType,
+ size, simpleContentType,
+ origin.toString(), originExtras));
+ }
+
+ public static Attachment parseJoinedString(String joinedString) {
+ String[] fragments = TextUtils.split(joinedString, "\\|");
+ int i = 0;
+ Attachment attachment = new Attachment();
+ attachment.partId = fragments[i++];
+ if (TextUtils.isEmpty(attachment.partId)) {
+ attachment.partId = null;
+ }
+ attachment.name = fragments[i++];
+ attachment.contentType = fragments[i++];
+ attachment.size = Integer.parseInt(fragments[i++]);
+ attachment.simpleContentType = fragments[i++];
+ attachment.origin = AttachmentOrigin.valueOf(fragments[i++]);
+ attachment.originExtras = fragments[i++];
+ return attachment;
+ }
+ }
+
+ /**
+ * Any given attachment can come in two different renditions (see
+ * {@link android.provider.Gmail.AttachmentRendition}) and can be saved to the sd card or to a
+ * cache. The gmail provider automatically syncs some attachments to the cache. Other
+ * attachments can be downloaded on demand. Attachments in the cache will be purged as needed to
+ * save space. Attachments on the SD card must be managed by the user or other software.
+ *
+ * @param account which account to use
+ * @param messageId the id of the mesage with the attachment
+ * @param attachment the attachment
+ * @param rendition the desired rendition
+ * @param saveToSd whether the attachment should be saved to (or loaded from) the sd card or
+ * @return the URI to ask the content provider to open in order to open an attachment.
+ */
+ public static Uri getAttachmentUri(
+ String account, long messageId, Attachment attachment,
+ AttachmentRendition rendition, boolean saveToSd) {
+ if (TextUtils.isEmpty(account)) {
+ throw new IllegalArgumentException("account is empty");
+ }
+ if (attachment.origin == AttachmentOrigin.LOCAL_FILE) {
+ return Uri.parse(attachment.originExtras);
+ } else {
+ return Uri.parse(
+ AUTHORITY_PLUS_MESSAGES).buildUpon()
+ .appendPath(account).appendPath(Long.toString(messageId))
+ .appendPath("attachments").appendPath(attachment.partId)
+ .appendPath(rendition.toString())
+ .appendPath(Boolean.toString(saveToSd))
+ .build();
+ }
+ }
+
+ /**
+ * Return the URI to query in order to find out whether an attachment is downloaded.
+ *
+ * <p>Querying this will also start a download if necessary. The cursor returned by querying
+ * this URI can contain the columns in {@link android.provider.Gmail.AttachmentColumns}.
+ *
+ * <p>Deleting this URI will cancel the download if it was not started automatically by the
+ * provider. It will also remove bookkeeping for saveToSd downloads.
+ *
+ * @param attachmentUri the attachment URI as returned by getAttachmentUri. The URI's authority
+ * Gmail.AUTHORITY. If it is not then you should open the file directly.
+ */
+ public static Uri getAttachmentDownloadUri(Uri attachmentUri) {
+ if (!"content".equals(attachmentUri.getScheme())) {
+ throw new IllegalArgumentException("Uri's scheme must be 'content': " + attachmentUri);
+ }
+ return attachmentUri.buildUpon().appendPath("download").build();
+ }
+
+ public enum CursorStatus {
+ LOADED,
+ LOADING,
+ ERROR, // A network error occurred.
+ }
+
+ /**
+ * A cursor over messages.
+ */
+ public static final class MessageCursor extends MailCursor {
+
+ private LabelMap mLabelMap;
+
+ private ContentResolver mContentResolver;
+
+ /**
+ * Only valid if mCursor == null, in which case we are inserting a new
+ * message.
+ */
+ long mInReplyToLocalMessageId;
+ boolean mPreserveAttachments;
+
+ private int mIdIndex;
+ private int mConversationIdIndex;
+ private int mSubjectIndex;
+ private int mSnippetIndex;
+ private int mFromIndex;
+ private int mToIndex;
+ private int mCcIndex;
+ private int mBccIndex;
+ private int mReplyToIndex;
+ private int mDateSentMsIndex;
+ private int mDateReceivedMsIndex;
+ private int mListInfoIndex;
+ private int mPersonalLevelIndex;
+ private int mBodyIndex;
+ private int mBodyEmbedsExternalResourcesIndex;
+ private int mLabelIdsIndex;
+ private int mJoinedAttachmentInfosIndex;
+ private int mErrorIndex;
+
+ private TextUtils.StringSplitter mLabelIdsSplitter = newMessageLabelIdsSplitter();
+
+ public MessageCursor(Gmail gmail, ContentResolver cr, String account, Cursor cursor) {
+ super(account, cursor);
+ mLabelMap = gmail.getLabelMap(account);
+ if (cursor == null) {
+ throw new IllegalArgumentException(
+ "null cursor passed to MessageCursor()");
+ }
+
+ mContentResolver = cr;
+
+ mIdIndex = mCursor.getColumnIndexOrThrow(MessageColumns.ID);
+ mConversationIdIndex =
+ mCursor.getColumnIndexOrThrow(MessageColumns.CONVERSATION_ID);
+ mSubjectIndex = mCursor.getColumnIndexOrThrow(MessageColumns.SUBJECT);
+ mSnippetIndex = mCursor.getColumnIndexOrThrow(MessageColumns.SNIPPET);
+ mFromIndex = mCursor.getColumnIndexOrThrow(MessageColumns.FROM);
+ mToIndex = mCursor.getColumnIndexOrThrow(MessageColumns.TO);
+ mCcIndex = mCursor.getColumnIndexOrThrow(MessageColumns.CC);
+ mBccIndex = mCursor.getColumnIndexOrThrow(MessageColumns.BCC);
+ mReplyToIndex = mCursor.getColumnIndexOrThrow(MessageColumns.REPLY_TO);
+ mDateSentMsIndex =
+ mCursor.getColumnIndexOrThrow(MessageColumns.DATE_SENT_MS);
+ mDateReceivedMsIndex =
+ mCursor.getColumnIndexOrThrow(MessageColumns.DATE_RECEIVED_MS);
+ mListInfoIndex = mCursor.getColumnIndexOrThrow(MessageColumns.LIST_INFO);
+ mPersonalLevelIndex =
+ mCursor.getColumnIndexOrThrow(MessageColumns.PERSONAL_LEVEL);
+ mBodyIndex = mCursor.getColumnIndexOrThrow(MessageColumns.BODY);
+ mBodyEmbedsExternalResourcesIndex =
+ mCursor.getColumnIndexOrThrow(MessageColumns.EMBEDS_EXTERNAL_RESOURCES);
+ mLabelIdsIndex = mCursor.getColumnIndexOrThrow(MessageColumns.LABEL_IDS);
+ mJoinedAttachmentInfosIndex =
+ mCursor.getColumnIndexOrThrow(MessageColumns.JOINED_ATTACHMENT_INFOS);
+ mErrorIndex = mCursor.getColumnIndexOrThrow(MessageColumns.ERROR);
+
+ mInReplyToLocalMessageId = 0;
+ mPreserveAttachments = false;
+ }
+
+ protected MessageCursor(ContentResolver cr, String account, long inReplyToMessageId,
+ boolean preserveAttachments) {
+ super(account, null);
+ mContentResolver = cr;
+ mInReplyToLocalMessageId = inReplyToMessageId;
+ mPreserveAttachments = preserveAttachments;
+ }
+
+ @Override
+ protected void onCursorPositionChanged() {
+ super.onCursorPositionChanged();
+ }
+
+ public CursorStatus getStatus() {
+ Bundle extras = mCursor.getExtras();
+ String stringStatus = extras.getString(EXTRA_STATUS);
+ return CursorStatus.valueOf(stringStatus);
+ }
+
+ /** Retry a network request after errors. */
+ public void retry() {
+ Bundle input = new Bundle();
+ input.putString(RESPOND_INPUT_COMMAND, COMMAND_RETRY);
+ Bundle output = mCursor.respond(input);
+ String response = output.getString(RESPOND_OUTPUT_COMMAND_RESPONSE);
+ assert COMMAND_RESPONSE_OK.equals(response);
+ }
+
+ /**
+ * Gets the message id of the current message. Note that this is an
+ * immutable local message (not, for example, GMail's message id, which
+ * is immutable).
+ *
+ * @return the message's id
+ */
+ public long getMessageId() {
+ checkCursor();
+ return mCursor.getLong(mIdIndex);
+ }
+
+ /**
+ * Gets the message's conversation id. This must be immutable. (For
+ * example, with GMail this should be the original conversation id
+ * rather than the default notion of converation id.)
+ *
+ * @return the message's conversation id
+ */
+ public long getConversationId() {
+ checkCursor();
+ return mCursor.getLong(mConversationIdIndex);
+ }
+
+ /**
+ * Gets the message's subject.
+ *
+ * @return the message's subject
+ */
+ public String getSubject() {
+ return getStringInColumn(mSubjectIndex);
+ }
+
+ /**
+ * Gets the message's snippet (the short piece of the body). The snippet
+ * is generated from the body and cannot be set directly.
+ *
+ * @return the message's snippet
+ */
+ public String getSnippet() {
+ return getStringInColumn(mSnippetIndex);
+ }
+
+ /**
+ * Gets the message's from address.
+ *
+ * @return the message's from address
+ */
+ public String getFromAddress() {
+ return getStringInColumn(mFromIndex);
+ }
+
+ /**
+ * Returns the addresses for the key, if it has been updated, or index otherwise.
+ */
+ private String[] getAddresses(String key, int index) {
+ ContentValues updated = getUpdateValues();
+ String addresses;
+ if (updated.containsKey(key)) {
+ addresses = (String)getUpdateValues().get(key);
+ } else {
+ addresses = getStringInColumn(index);
+ }
+
+ return TextUtils.split(addresses, EMAIL_SEPARATOR_PATTERN);
+ }
+
+ /**
+ * Gets the message's to addresses.
+ * @return the message's to addresses
+ */
+ public String[] getToAddresses() {
+ return getAddresses(MessageColumns.TO, mToIndex);
+ }
+
+ /**
+ * Gets the message's cc addresses.
+ * @return the message's cc addresses
+ */
+ public String[] getCcAddresses() {
+ return getAddresses(MessageColumns.CC, mCcIndex);
+ }
+
+ /**
+ * Gets the message's bcc addresses.
+ * @return the message's bcc addresses
+ */
+ public String[] getBccAddresses() {
+ return getAddresses(MessageColumns.BCC, mBccIndex);
+ }
+
+ /**
+ * Gets the message's replyTo address.
+ *
+ * @return the message's replyTo address
+ */
+ public String[] getReplyToAddress() {
+ return TextUtils.split(getStringInColumn(mReplyToIndex), EMAIL_SEPARATOR_PATTERN);
+ }
+
+ public long getDateSentMs() {
+ checkCursor();
+ return mCursor.getLong(mDateSentMsIndex);
+ }
+
+ public long getDateReceivedMs() {
+ checkCursor();
+ return mCursor.getLong(mDateReceivedMsIndex);
+ }
+
+ public String getListInfo() {
+ return getStringInColumn(mListInfoIndex);
+ }
+
+ public PersonalLevel getPersonalLevel() {
+ checkCursor();
+ int personalLevelInt = mCursor.getInt(mPersonalLevelIndex);
+ return PersonalLevel.fromInt(personalLevelInt);
+ }
+
+ /**
+ * @deprecated
+ */
+ public boolean getExpanded() {
+ return true;
+ }
+
+ /**
+ * Gets the message's body.
+ *
+ * @return the message's body
+ */
+ public String getBody() {
+ return getStringInColumn(mBodyIndex);
+ }
+
+ /**
+ * @return whether the message's body contains embedded references to external resources. In
+ * that case the resources should only be displayed if the user explicitly asks for them to
+ * be
+ */
+ public boolean getBodyEmbedsExternalResources() {
+ checkCursor();
+ return mCursor.getInt(mBodyEmbedsExternalResourcesIndex) != 0;
+ }
+
+ /**
+ * @return a copy of the set of label ids
+ */
+ public Set<Long> getLabelIds() {
+ String labelNames = mCursor.getString(mLabelIdsIndex);
+ mLabelIdsSplitter.setString(labelNames);
+ return getLabelIdsFromLabelIdsString(mLabelIdsSplitter);
+ }
+
+ /**
+ * @return a joined string of labels separated by spaces.
+ */
+ public String getRawLabelIds() {
+ return mCursor.getString(mLabelIdsIndex);
+ }
+
+ /**
+ * Adds a label to a message (if add is true) or removes it (if add is
+ * false).
+ *
+ * @param label the label to add or remove
+ * @param add whether to add or remove the label
+ * @throws NonexistentLabelException thrown if the named label does not
+ * exist
+ */
+ public void addOrRemoveLabel(String label, boolean add) throws NonexistentLabelException {
+ addOrRemoveLabelOnMessage(mContentResolver, mAccount, getConversationId(),
+ getMessageId(), label, add);
+ }
+
+ public ArrayList<Attachment> getAttachmentInfos() {
+ ArrayList<Attachment> attachments = Lists.newArrayList();
+
+ String joinedAttachmentInfos = mCursor.getString(mJoinedAttachmentInfosIndex);
+ if (joinedAttachmentInfos != null) {
+ for (String joinedAttachmentInfo :
+ TextUtils.split(joinedAttachmentInfos, ATTACHMENT_INFO_SEPARATOR_PATTERN)) {
+
+ Attachment attachment = Attachment.parseJoinedString(joinedAttachmentInfo);
+ attachments.add(attachment);
+ }
+ }
+ return attachments;
+ }
+
+ /**
+ * @return the error text for the message. Error text gets set if the server rejects a
+ * message that we try to save or send. If there is error text then the message is no longer
+ * scheduled to be saved or sent. Calling save() or send() will clear any error as well as
+ * scheduling another atempt to save or send the message.
+ */
+ public String getErrorText() {
+ return mCursor.getString(mErrorIndex);
+ }
+ }
+
+ /**
+ * A helper class for creating or updating messags. Use the putXxx methods to provide initial or
+ * new values for the message. Then save or send the message. To save or send an existing
+ * message without making other changes to it simply provide an emty ContentValues.
+ */
+ public static class MessageModification {
+
+ /**
+ * Sets the message's subject. Only valid for drafts.
+ *
+ * @param values the ContentValues that will be used to create or update the message
+ * @param subject the new subject
+ */
+ public static void putSubject(ContentValues values, String subject) {
+ values.put(MessageColumns.SUBJECT, subject);
+ }
+
+ /**
+ * Sets the message's to address. Only valid for drafts.
+ *
+ * @param values the ContentValues that will be used to create or update the message
+ * @param toAddresses the new to addresses
+ */
+ public static void putToAddresses(ContentValues values, String[] toAddresses) {
+ values.put(MessageColumns.TO, TextUtils.join(EMAIL_SEPARATOR, toAddresses));
+ }
+
+ /**
+ * Sets the message's cc address. Only valid for drafts.
+ *
+ * @param values the ContentValues that will be used to create or update the message
+ * @param ccAddresses the new cc addresses
+ */
+ public static void putCcAddresses(ContentValues values, String[] ccAddresses) {
+ values.put(MessageColumns.CC, TextUtils.join(EMAIL_SEPARATOR, ccAddresses));
+ }
+
+ /**
+ * Sets the message's bcc address. Only valid for drafts.
+ *
+ * @param values the ContentValues that will be used to create or update the message
+ * @param bccAddresses the new bcc addresses
+ */
+ public static void putBccAddresses(ContentValues values, String[] bccAddresses) {
+ values.put(MessageColumns.BCC, TextUtils.join(EMAIL_SEPARATOR, bccAddresses));
+ }
+
+ /**
+ * Saves a new body for the message. Only valid for drafts.
+ *
+ * @param values the ContentValues that will be used to create or update the message
+ * @param body the new body of the message
+ */
+ public static void putBody(ContentValues values, String body) {
+ values.put(MessageColumns.BODY, body);
+ }
+
+ /**
+ * Sets the attachments on a message. Only valid for drafts.
+ *
+ * @param values the ContentValues that will be used to create or update the message
+ * @param attachments
+ */
+ public static void putAttachments(ContentValues values, List<Attachment> attachments) {
+ values.put(
+ MessageColumns.JOINED_ATTACHMENT_INFOS, joinedAttachmentsString(attachments));
+ }
+
+ /**
+ * Create a new message and save it as a draft or send it.
+ *
+ * @param contentResolver the content resolver to use
+ * @param account the account to use
+ * @param values the values for the new message
+ * @param refMessageId the message that is being replied to or forwarded
+ * @param save whether to save or send the message
+ * @return the id of the new message
+ */
+ public static long sendOrSaveNewMessage(
+ ContentResolver contentResolver, String account,
+ ContentValues values, long refMessageId, boolean save) {
+ values.put(MessageColumns.FAKE_SAVE, save);
+ values.put(MessageColumns.FAKE_REF_MESSAGE_ID, refMessageId);
+ Uri uri = Uri.parse(AUTHORITY_PLUS_MESSAGES + account + "/");
+ Uri result = contentResolver.insert(uri, values);
+ return ContentUris.parseId(result);
+ }
+
+ /**
+ * Update an existing draft and save it as a new draft or send it.
+ *
+ * @param contentResolver the content resolver to use
+ * @param account the account to use
+ * @param messageId the id of the message to update
+ * @param updateValues the values to change. Unspecified fields will not be altered
+ * @param save whether to resave the message as a draft or send it
+ */
+ public static void sendOrSaveExistingMessage(
+ ContentResolver contentResolver, String account, long messageId,
+ ContentValues updateValues, boolean save) {
+ updateValues.put(MessageColumns.FAKE_SAVE, save);
+ updateValues.put(MessageColumns.FAKE_REF_MESSAGE_ID, 0);
+ Uri uri = Uri.parse(
+ AUTHORITY_PLUS_MESSAGES + account + "/" + messageId);
+ contentResolver.update(uri, updateValues, null, null);
+ }
+
+ /**
+ * The string produced here is parsed by Gmail.MessageCursor#getAttachmentInfos.
+ */
+ public static String joinedAttachmentsString(List<Gmail.Attachment> attachments) {
+ StringBuilder attachmentsSb = new StringBuilder();
+ for (Gmail.Attachment attachment : attachments) {
+ if (attachmentsSb.length() != 0) {
+ attachmentsSb.append(Gmail.ATTACHMENT_INFO_SEPARATOR);
+ }
+ attachmentsSb.append(attachment.toJoinedString());
+ }
+ return attachmentsSb.toString();
+ }
+
+ }
+
+ /**
+ * A cursor over conversations.
+ *
+ * "Conversation" refers to the information needed to populate a list of
+ * conversations, not all of the messages in a conversation.
+ */
+ public static final class ConversationCursor extends MailCursor {
+
+ private LabelMap mLabelMap;
+
+ private int mConversationIdIndex;
+ private int mSubjectIndex;
+ private int mSnippetIndex;
+ private int mFromIndex;
+ private int mDateIndex;
+ private int mPersonalLevelIndex;
+ private int mLabelIdsIndex;
+ private int mNumMessagesIndex;
+ private int mMaxMessageIdIndex;
+ private int mHasAttachmentsIndex;
+ private int mHasMessagesWithErrorsIndex;
+ private int mForceAllUnreadIndex;
+
+ private TextUtils.StringSplitter mLabelIdsSplitter = newConversationLabelIdsSplitter();
+
+ private ConversationCursor(Gmail gmail, String account, Cursor cursor) {
+ super(account, cursor);
+ mLabelMap = gmail.getLabelMap(account);
+
+ mConversationIdIndex =
+ mCursor.getColumnIndexOrThrow(ConversationColumns.ID);
+ mSubjectIndex = mCursor.getColumnIndexOrThrow(ConversationColumns.SUBJECT);
+ mSnippetIndex = mCursor.getColumnIndexOrThrow(ConversationColumns.SNIPPET);
+ mFromIndex = mCursor.getColumnIndexOrThrow(ConversationColumns.FROM);
+ mDateIndex = mCursor.getColumnIndexOrThrow(ConversationColumns.DATE);
+ mPersonalLevelIndex =
+ mCursor.getColumnIndexOrThrow(ConversationColumns.PERSONAL_LEVEL);
+ mLabelIdsIndex =
+ mCursor.getColumnIndexOrThrow(ConversationColumns.LABEL_IDS);
+ mNumMessagesIndex = mCursor.getColumnIndexOrThrow(ConversationColumns.NUM_MESSAGES);
+ mMaxMessageIdIndex = mCursor.getColumnIndexOrThrow(ConversationColumns.MAX_MESSAGE_ID);
+ mHasAttachmentsIndex =
+ mCursor.getColumnIndexOrThrow(ConversationColumns.HAS_ATTACHMENTS);
+ mHasMessagesWithErrorsIndex =
+ mCursor.getColumnIndexOrThrow(ConversationColumns.HAS_MESSAGES_WITH_ERRORS);
+ mForceAllUnreadIndex =
+ mCursor.getColumnIndexOrThrow(ConversationColumns.FORCE_ALL_UNREAD);
+ }
+
+ @Override
+ protected void onCursorPositionChanged() {
+ super.onCursorPositionChanged();
+ }
+
+ public CursorStatus getStatus() {
+ Bundle extras = mCursor.getExtras();
+ String stringStatus = extras.getString(EXTRA_STATUS);
+ return CursorStatus.valueOf(stringStatus);
+ }
+
+ /** Retry a network request after errors. */
+ public void retry() {
+ Bundle input = new Bundle();
+ input.putString(RESPOND_INPUT_COMMAND, COMMAND_RETRY);
+ Bundle output = mCursor.respond(input);
+ String response = output.getString(RESPOND_OUTPUT_COMMAND_RESPONSE);
+ assert COMMAND_RESPONSE_OK.equals(response);
+ }
+
+ /**
+ * When a conversation cursor is created it becomes the active network cursor, which means
+ * that it will fetch results from the network if it needs to in order to show all mail that
+ * matches its query. If you later want to requery an older cursor and would like that
+ * cursor to be the active cursor you need to call this method before requerying.
+ */
+ public void becomeActiveNetworkCursor() {
+ Bundle input = new Bundle();
+ input.putString(RESPOND_INPUT_COMMAND, COMMAND_ACTIVATE);
+ Bundle output = mCursor.respond(input);
+ String response = output.getString(RESPOND_OUTPUT_COMMAND_RESPONSE);
+ assert COMMAND_RESPONSE_OK.equals(response);
+ }
+
+ /**
+ * Gets the conversation id. This must be immutable. (For example, with
+ * GMail this should be the original conversation id rather than the
+ * default notion of converation id.)
+ *
+ * @return the conversation id
+ */
+ public long getConversationId() {
+ return mCursor.getLong(mConversationIdIndex);
+ }
+
+ /**
+ * Returns the instructions for building from snippets. Pass this to getFromSnippetHtml
+ * in order to actually build the snippets.
+ * @return snippet instructions for use by getFromSnippetHtml()
+ */
+ public String getFromSnippetInstructions() {
+ return getStringInColumn(mFromIndex);
+ }
+
+ /**
+ * Gets the conversation's subject.
+ *
+ * @return the subject
+ */
+ public String getSubject() {
+ return getStringInColumn(mSubjectIndex);
+ }
+
+ /**
+ * Gets the conversation's snippet.
+ *
+ * @return the snippet
+ */
+ public String getSnippet() {
+ return getStringInColumn(mSnippetIndex);
+ }
+
+ /**
+ * Get's the conversation's personal level.
+ *
+ * @return the personal level.
+ */
+ public PersonalLevel getPersonalLevel() {
+ int personalLevelInt = mCursor.getInt(mPersonalLevelIndex);
+ return PersonalLevel.fromInt(personalLevelInt);
+ }
+
+ /**
+ * @return a copy of the set of labels. To add or remove labels call
+ * MessageCursor.addOrRemoveLabel on each message in the conversation.
+ * @deprecated use getLabelIds
+ */
+ public Set<String> getLabels() {
+ return getLabels(getRawLabelIds(), mLabelMap);
+ }
+
+ /**
+ * @return a copy of the set of labels. To add or remove labels call
+ * MessageCursor.addOrRemoveLabel on each message in the conversation.
+ */
+ public Set<Long> getLabelIds() {
+ mLabelIdsSplitter.setString(getRawLabelIds());
+ return getLabelIdsFromLabelIdsString(mLabelIdsSplitter);
+ }
+
+ /**
+ * Returns the set of labels using the raw labels from a previous getRawLabels()
+ * as input.
+ * @return a copy of the set of labels. To add or remove labels call
+ * MessageCursor.addOrRemoveLabel on each message in the conversation.
+ */
+ public Set<String> getLabels(String rawLabelIds, LabelMap labelMap) {
+ mLabelIdsSplitter.setString(rawLabelIds);
+ return getCanonicalNamesFromLabelIdsString(labelMap, mLabelIdsSplitter);
+ }
+
+ /**
+ * @return a joined string of labels separated by spaces. Use
+ * getLabels(rawLabels) to convert this to a Set of labels.
+ */
+ public String getRawLabelIds() {
+ return mCursor.getString(mLabelIdsIndex);
+ }
+
+ /**
+ * @return the number of messages in the conversation
+ */
+ public int getNumMessages() {
+ return mCursor.getInt(mNumMessagesIndex);
+ }
+
+ /**
+ * @return the max message id in the conversation
+ */
+ public long getMaxMessageId() {
+ return mCursor.getLong(mMaxMessageIdIndex);
+ }
+
+ public long getDateMs() {
+ return mCursor.getLong(mDateIndex);
+ }
+
+ public boolean hasAttachments() {
+ return mCursor.getInt(mHasAttachmentsIndex) != 0;
+ }
+
+ public boolean hasMessagesWithErrors() {
+ return mCursor.getInt(mHasMessagesWithErrorsIndex) != 0;
+ }
+
+ public boolean getForceAllUnread() {
+ return !mCursor.isNull(mForceAllUnreadIndex)
+ && mCursor.getInt(mForceAllUnreadIndex) != 0;
+ }
+ }
+}
diff --git a/core/java/android/provider/Im.java b/core/java/android/provider/Im.java
new file mode 100644
index 0000000..8ca97e1
--- /dev/null
+++ b/core/java/android/provider/Im.java
@@ -0,0 +1,1937 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.provider;
+
+import android.content.ContentQueryMap;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Handler;
+
+import java.util.HashMap;
+
+/**
+ * The IM provider stores all information about roster contacts, chat messages, presence, etc.
+ *
+ * @hide
+ */
+public class Im {
+ /**
+ * no public constructor since this is a utility class
+ */
+ private Im() {}
+
+ /**
+ * The Columns for IM providers (i.e. AIM, Y!, GTalk)
+ */
+ public interface ProviderColumns {
+ /**
+ * The name of the IM provider
+ */
+ String NAME = "name";
+
+ /**
+ * The full name of the provider
+ */
+ String FULLNAME = "fullname";
+
+ /**
+ * The url users should visit to create a new account for this provider
+ */
+ String SIGNUP_URL = "signup_url";
+ }
+
+ /**
+ * Known names corresponding to the {@link ProviderColumns#NAME} column
+ */
+ public interface ProviderNames {
+ //
+ //NOTE: update Contacts.java with new providers when they're added.
+ //
+ String YAHOO = "Yahoo";
+ String GTALK = "GTalk";
+ String MSN = "MSN";
+ String ICQ = "ICQ";
+ String AIM = "AIM";
+ }
+
+ /**
+ * The ProviderCategories definitions are used for the Intent category for the Intent
+ *
+ * Intent intent = new Intent(Intent.ACTION_SENDTO,
+ * Uri.fromParts("im", data, null)).
+ * addCategory(category);
+ */
+ public interface ProviderCategories {
+ String GTALK = "com.android.im.category.GTALK";
+ String AIM = "com.android.im.category.AIM";
+ String MSN = "com.android.im.category.MSN";
+ String YAHOO = "com.android.im.category.YAHOO";
+ String ICQ = "com.android.im.category.ICQ";
+ }
+
+ /**
+ * This table contains the IM providers
+ */
+ public static final class Provider implements BaseColumns, ProviderColumns {
+ private Provider() {}
+
+ public static final long getProviderIdForName(ContentResolver cr, String providerName) {
+ String[] selectionArgs = new String[1];
+ selectionArgs[0] = providerName;
+
+ Cursor cursor = cr.query(CONTENT_URI,
+ PROVIDER_PROJECTION,
+ NAME+"=?",
+ selectionArgs, null);
+
+ long retVal = 0;
+ try {
+ if (cursor.moveToFirst()) {
+ retVal = cursor.getLong(cursor.getColumnIndexOrThrow(_ID));
+ }
+ } finally {
+ cursor.close();
+ }
+
+ return retVal;
+ }
+
+ public static final String getProviderNameForId(ContentResolver cr, long providerId) {
+ Cursor cursor = cr.query(CONTENT_URI,
+ PROVIDER_PROJECTION,
+ _ID + "=" + providerId,
+ null, null);
+
+ String retVal = null;
+ try {
+ if (cursor.moveToFirst()) {
+ retVal = cursor.getString(cursor.getColumnIndexOrThrow(NAME));
+ }
+ } finally {
+ cursor.close();
+ }
+
+ return retVal;
+ }
+
+ /**
+ * This returns the provider name given a provider category.
+ *
+ * @param providerCategory the provider category defined in {@link ProviderCategories}.
+ * @return the corresponding provider name defined in {@link ProviderNames}.
+ */
+ public static String getProviderNameForCategory(String providerCategory) {
+ if (providerCategory != null) {
+ if (providerCategory.equalsIgnoreCase(ProviderCategories.GTALK)) {
+ return ProviderNames.GTALK;
+ } else if (providerCategory.equalsIgnoreCase(ProviderCategories.AIM)) {
+ return ProviderNames.AIM;
+ } else if (providerCategory.equalsIgnoreCase(ProviderCategories.MSN)) {
+ return ProviderNames.MSN;
+ } else if (providerCategory.equalsIgnoreCase(ProviderCategories.YAHOO)) {
+ return ProviderNames.YAHOO;
+ } else if (providerCategory.equalsIgnoreCase(ProviderCategories.ICQ)) {
+ return ProviderNames.ICQ;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * This returns the provider category given a provider name.
+ *
+ * @param providername the provider name defined in {@link ProviderNames}.
+ * @return the provider category defined in {@link ProviderCategories}.
+ */
+ public static String getProviderCategoryFromName(String providername) {
+ if (providername != null) {
+ if (providername.equalsIgnoreCase(Im.ProviderNames.GTALK)) {
+ return Im.ProviderCategories.GTALK;
+ } else if (providername.equalsIgnoreCase(Im.ProviderNames.AIM)) {
+ return Im.ProviderCategories.AIM;
+ } else if (providername.equalsIgnoreCase(Im.ProviderNames.MSN)) {
+ return Im.ProviderCategories.MSN;
+ } else if (providername.equalsIgnoreCase(Im.ProviderNames.YAHOO)) {
+ return Im.ProviderCategories.YAHOO;
+ } else if (providername.equalsIgnoreCase(Im.ProviderNames.ICQ)) {
+ return Im.ProviderCategories.ICQ;
+ }
+ }
+
+ return null;
+ }
+
+ private static final String[] PROVIDER_PROJECTION = new String[] {
+ _ID,
+ NAME
+ };
+
+ public static final String ACTIVE_ACCOUNT_ID = "account_id";
+ public static final String ACTIVE_ACCOUNT_USERNAME = "account_username";
+ public static final String ACTIVE_ACCOUNT_PW = "account_pw";
+
+ /**
+ * The content:// style URL for this table
+ */
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://im/providers");
+
+ public static final Uri CONTENT_URI_WITH_ACCOUNT =
+ Uri.parse("content://im/providers/account");
+
+ /**
+ * The MIME type of {@link #CONTENT_URI} providing a directory of
+ * people.
+ */
+ public static final String CONTENT_TYPE =
+ "vnd.android.cursor.dir/im-providers";
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = "name ASC";
+ }
+
+ /**
+ * The columns for IM accounts. There can be more than one account for each IM provider.
+ */
+ public interface AccountColumns {
+ /**
+ * The name of the account
+ * <P>Type: TEXT</P>
+ */
+ String NAME = "name";
+
+ /**
+ * The IM provider for this account
+ * <P>Type: INTEGER</P>
+ */
+ String PROVIDER = "provider";
+
+ /**
+ * The username for this account
+ * <P>Type: TEXT</P>
+ */
+ String USERNAME = "username";
+
+ /**
+ * The password for this account
+ * <P>Type: TEXT</P>
+ */
+ String PASSWORD = "pw";
+
+ /**
+ * A boolean value indicates if the account is active.
+ * <P>Type: INTEGER</P>
+ */
+ String ACTIVE = "active";
+
+ /**
+ * A boolean value indicates if the account is locked (not editable)
+ * <P>Type: INTEGER</P>
+ */
+ String LOCKED = "locked";
+
+ /**
+ * A boolean value to indicate whether this account is kept signed in.
+ * <P>Type: INTEGER</P>
+ */
+ String KEEP_SIGNED_IN = "keep_signed_in";
+
+ /**
+ * A boolean value indiciating the last login state for this account
+ * <P>Type: INTEGER</P>
+ */
+ String LAST_LOGIN_STATE = "last_login_state";
+ }
+
+ /**
+ * This table contains the IM accounts.
+ */
+ public static final class Account implements BaseColumns, AccountColumns {
+ private Account() {}
+
+ public static final long getProviderIdForAccount(ContentResolver cr, long accountId) {
+ Cursor cursor = cr.query(CONTENT_URI,
+ PROVIDER_PROJECTION,
+ _ID + "=" + accountId,
+ null /* selection args */,
+ null /* sort order */);
+
+ long providerId = 0;
+
+ try {
+ if (cursor.moveToFirst()) {
+ providerId = cursor.getLong(PROVIDER_COLUMN);
+ }
+ } finally {
+ cursor.close();
+ }
+
+ return providerId;
+ }
+
+ private static final String[] PROVIDER_PROJECTION = new String[] { PROVIDER };
+ private static final int PROVIDER_COLUMN = 0;
+
+ /**
+ * The content:// style URL for this table
+ */
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://im/accounts");
+
+ /**
+ * The MIME type of {@link #CONTENT_URI} providing a directory of
+ * account.
+ */
+ public static final String CONTENT_TYPE =
+ "vnd.android.cursor.dir/im-accounts";
+
+ /**
+ * The MIME type of a {@link #CONTENT_URI} subdirectory of a single
+ * account.
+ */
+ public static final String CONTENT_ITEM_TYPE =
+ "vnd.android.cursor.item/im-accounts";
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = "name ASC";
+
+ }
+
+ /**
+ * Columns from the Contacts table.
+ */
+ public interface ContactsColumns {
+ /**
+ * The username
+ * <P>Type: TEXT</P>
+ */
+ String USERNAME = "username";
+
+ /**
+ * The nickname or display name
+ * <P>Type: TEXT</P>
+ */
+ String NICKNAME = "nickname";
+
+ /**
+ * The IM provider for this contact
+ * <P>Type: INTEGER</P>
+ */
+ String PROVIDER = "provider";
+
+ /**
+ * The account (within a IM provider) for this contact
+ * <P>Type: INTEGER</P>
+ */
+ String ACCOUNT = "account";
+
+ /**
+ * The contactList this contact belongs to
+ * <P>Type: INTEGER</P>
+ */
+ String CONTACTLIST = "contactList";
+
+ /**
+ * Contact type
+ * <P>Type: INTEGER</P>
+ */
+ String TYPE = "type";
+
+ /**
+ * normal IM contact
+ */
+ int TYPE_NORMAL = 0;
+ /**
+ * temporary contact, someone not in the list of contacts that we
+ * subscribe presence for. Usually created because of the user is
+ * having a chat session with this contact.
+ */
+ int TYPE_TEMPORARY = 1;
+ /**
+ * temporary contact created for group chat.
+ */
+ int TYPE_GROUP = 2;
+ /**
+ * blocked contact.
+ */
+ int TYPE_BLOCKED = 3;
+ /**
+ * the contact is hidden. The client should always display this contact to the user.
+ */
+ int TYPE_HIDDEN = 4;
+ /**
+ * the contact is pinned. The client should always display this contact to the user.
+ */
+ int TYPE_PINNED = 5;
+
+ /**
+ * Contact subscription status
+ * <P>Type: INTEGER</P>
+ */
+ String SUBSCRIPTION_STATUS = "subscriptionStatus";
+
+ /**
+ * no pending subscription
+ */
+ int SUBSCRIPTION_STATUS_NONE = 0;
+ /**
+ * requested to subscribe
+ */
+ int SUBSCRIPTION_STATUS_SUBSCRIBE_PENDING = 1;
+ /**
+ * requested to unsubscribe
+ */
+ int SUBSCRIPTION_STATUS_UNSUBSCRIBE_PENDING = 2;
+
+ /**
+ * Contact subscription type
+ * <P>Type: INTEGER </P>
+ */
+ String SUBSCRIPTION_TYPE = "subscriptionType";
+
+ /**
+ * The user and contact have no interest in each other's presence.
+ */
+ int SUBSCRIPTION_TYPE_NONE = 0;
+ /**
+ * The user wishes to stop receiving presence updates from the contact.
+ */
+ int SUBSCRIPTION_TYPE_REMOVE = 1;
+ /**
+ * The user is interested in receiving presence updates from the contact.
+ */
+ int SUBSCRIPTION_TYPE_TO = 2;
+ /**
+ * The contact is interested in receiving presence updates from the user.
+ */
+ int SUBSCRIPTION_TYPE_FROM = 3;
+ /**
+ * The user and contact have a mutual interest in each other's presence.
+ */
+ int SUBSCRIPTION_TYPE_BOTH = 4;
+ /**
+ * This is a special type reserved for pending subscription requests
+ */
+ int SUBSCRIPTION_TYPE_INVITATIONS = 5;
+
+ /**
+ * Quick Contact: derived from Google Contact Extension's "message_count" attribute.
+ * <P>Type: INTEGER</P>
+ */
+ String QUICK_CONTACT = "qc";
+
+ /**
+ * Google Contact Extension attribute
+ *
+ * Rejected: a boolean value indicating whether a subscription request from
+ * this client was ever rejected by the user. "true" indicates that it has.
+ * This is provided so that a client can block repeated subscription requests.
+ * <P>Type: INTEGER</P>
+ */
+ String REJECTED = "rejected";
+ }
+
+ /**
+ * This table contains contacts.
+ */
+ public static final class Contacts implements BaseColumns,
+ ContactsColumns, PresenceColumns, ChatsColumns {
+ /**
+ * no public constructor since this is a utility class
+ */
+ private Contacts() {}
+
+ /**
+ * The content:// style URL for this table
+ */
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://im/contacts");
+
+ /**
+ * The content:// style URL for contacts joined with presence
+ */
+ public static final Uri CONTENT_URI_WITH_PRESENCE =
+ Uri.parse("content://im/contactsWithPresence");
+
+ /**
+ * The content:// style URL for barebone contacts, not joined with any other table
+ */
+ public static final Uri CONTENT_URI_CONTACTS_BAREBONE =
+ Uri.parse("content://im/contactsBarebone");
+
+ /**
+ * The content:// style URL for contacts who have an open chat session
+ */
+ public static final Uri CONTENT_URI_CHAT_CONTACTS =
+ Uri.parse("content://im/contacts/chatting");
+
+ /**
+ * The content:// style URL for contacts who have been blocked
+ */
+ public static final Uri CONTENT_URI_BLOCKED_CONTACTS =
+ Uri.parse("content://im/contacts/blocked");
+
+ /**
+ * The content:// style URL for contacts by provider and account
+ */
+ public static final Uri CONTENT_URI_CONTACTS_BY =
+ Uri.parse("content://im/contacts");
+
+ /**
+ * The content:// style URL for contacts by provider and account,
+ * and who have an open chat session
+ */
+ public static final Uri CONTENT_URI_CHAT_CONTACTS_BY =
+ Uri.parse("content://im/contacts/chatting");
+
+ /**
+ * The content:// style URL for contacts by provider and account,
+ * and who are online
+ */
+ public static final Uri CONTENT_URI_ONLINE_CONTACTS_BY =
+ Uri.parse("content://im/contacts/online");
+
+ /**
+ * The content:// style URL for contacts by provider and account,
+ * and who are offline
+ */
+ public static final Uri CONTENT_URI_OFFLINE_CONTACTS_BY =
+ Uri.parse("content://im/contacts/offline");
+
+ /**
+ * The content:// style URL for operations on bulk contacts
+ */
+ public static final Uri BULK_CONTENT_URI =
+ Uri.parse("content://im/bulk_contacts");
+
+ /**
+ * The content:// style URL for the count of online contacts in each
+ * contact list by provider and account.
+ */
+ public static final Uri CONTENT_URI_ONLINE_COUNT =
+ Uri.parse("content://im/contacts/onlineCount");
+
+ /**
+ * The MIME type of {@link #CONTENT_URI} providing a directory of
+ * people.
+ */
+ public static final String CONTENT_TYPE = "vnd.android.cursor.dir/im-contacts";
+
+ /**
+ * The MIME type of a {@link #CONTENT_URI} subdirectory of a single
+ * person.
+ */
+ public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/im-contacts";
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER =
+ "subscriptionType DESC, last_message_date DESC," +
+ " mode DESC, nickname COLLATE UNICODE ASC";
+
+ public static final String CHATS_CONTACT = "chats_contact";
+
+ public static final String AVATAR_HASH = "avatars_hash";
+
+ public static final String AVATAR_DATA = "avatars_data";
+ }
+
+ /**
+ * Columns from the ContactList table.
+ */
+ public interface ContactListColumns {
+ String NAME = "name";
+ String PROVIDER = "provider";
+ String ACCOUNT = "account";
+ }
+
+ /**
+ * This table contains the contact lists.
+ */
+ public static final class ContactList implements BaseColumns,
+ ContactListColumns {
+ private ContactList() {}
+
+ /**
+ * The content:// style URL for this table
+ */
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://im/contactLists");
+
+ /**
+ * The MIME type of {@link #CONTENT_URI} providing a directory of
+ * people.
+ */
+ public static final String CONTENT_TYPE =
+ "vnd.android.cursor.dir/im-contactLists";
+
+ /**
+ * The MIME type of a {@link #CONTENT_URI} subdirectory of a single
+ * person.
+ */
+ public static final String CONTENT_ITEM_TYPE =
+ "vnd.android.cursor.item/im-contactLists";
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = "name COLLATE UNICODE ASC";
+
+ public static final String PROVIDER_NAME = "provider_name";
+
+ public static final String ACCOUNT_NAME = "account_name";
+ }
+
+ /**
+ * Columns from the BlockedList table.
+ */
+ public interface BlockedListColumns {
+ /**
+ * The username of the blocked contact.
+ * <P>Type: TEXT</P>
+ */
+ String USERNAME = "username";
+
+ /**
+ * The nickname of the blocked contact.
+ * <P>Type: TEXT</P>
+ */
+ String NICKNAME = "nickname";
+
+ /**
+ * The provider id of the blocked contact.
+ * <P>Type: INT</P>
+ */
+ String PROVIDER = "provider";
+
+ /**
+ * The account id of the blocked contact.
+ * <P>Type: INT</P>
+ */
+ String ACCOUNT = "account";
+ }
+
+ /**
+ * This table contains blocked lists
+ */
+ public static final class BlockedList implements BaseColumns, BlockedListColumns {
+ private BlockedList() {}
+
+ /**
+ * The content:// style URL for this table
+ */
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://im/blockedList");
+
+ /**
+ * The MIME type of {@link #CONTENT_URI} providing a directory of
+ * people.
+ */
+ public static final String CONTENT_TYPE =
+ "vnd.android.cursor.dir/im-blockedList";
+
+ /**
+ * The MIME type of a {@link #CONTENT_URI} subdirectory of a single
+ * person.
+ */
+ public static final String CONTENT_ITEM_TYPE =
+ "vnd.android.cursor.item/im-blockedList";
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = "nickname ASC";
+
+ public static final String PROVIDER_NAME = "provider_name";
+
+ public static final String ACCOUNT_NAME = "account_name";
+
+ public static final String AVATAR_DATA = "avatars_data";
+ }
+
+ /**
+ * Columns from the contactsEtag table
+ */
+ public interface ContactsEtagColumns {
+ /**
+ * The roster etag, computed by the server, stored on the client. There is one etag
+ * per account roster.
+ * <P>Type: TEXT</P>
+ */
+ String ETAG = "etag";
+
+ /**
+ * The account id for the etag.
+ * <P> Type: INTEGER </P>
+ */
+ String ACCOUNT = "account";
+ }
+
+ public static final class ContactsEtag implements BaseColumns, ContactsEtagColumns {
+ private ContactsEtag() {}
+
+ public static final Cursor query(ContentResolver cr,
+ String[] projection) {
+ return cr.query(CONTENT_URI, projection, null, null, null);
+ }
+
+ public static final Cursor query(ContentResolver cr,
+ String[] projection, String where, String orderBy) {
+ return cr.query(CONTENT_URI, projection, where,
+ null, orderBy == null ? null : orderBy);
+ }
+
+ public static final String getRosterEtag(ContentResolver resolver, long accountId) {
+ String retVal = null;
+
+ Cursor c = resolver.query(CONTENT_URI,
+ CONTACT_ETAG_PROJECTION,
+ ACCOUNT + "=" + accountId,
+ null /* selection args */,
+ null /* sort order */);
+
+ try {
+ if (c.moveToFirst()) {
+ retVal = c.getString(COLUMN_ETAG);
+ }
+ } finally {
+ c.close();
+ }
+
+ return retVal;
+ }
+
+ private static final String[] CONTACT_ETAG_PROJECTION = new String[] {
+ Im.ContactsEtag.ETAG // 0
+ };
+
+ private static int COLUMN_ETAG = 0;
+
+ /**
+ * The content:// style URL for this table
+ */
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://im/contactsEtag");
+
+ /**
+ * The MIME type of {@link #CONTENT_URI} providing a directory of
+ * people.
+ */
+ public static final String CONTENT_TYPE =
+ "vnd.android.cursor.dir/im-contactsEtag";
+
+ /**
+ * The MIME type of a {@link #CONTENT_URI} subdirectory of a single
+ * person.
+ */
+ public static final String CONTENT_ITEM_TYPE =
+ "vnd.android.cursor.item/im-contactsEtag";
+ }
+
+ /**
+ * Message type definition
+ */
+ public interface MessageType {
+ int OUTGOING = 0;
+ int INCOMING = 1;
+ int PRESENCE_AVAILABLE = 2;
+ int PRESENCE_AWAY = 3;
+ int PRESENCE_DND = 4;
+ int PRESENCE_UNAVAILABLE = 5;
+ int CONVERT_TO_GROUPCHAT = 6;
+ int STATUS = 7;
+ int POSTPONED = 8;
+ }
+
+ /**
+ * The common columns for both one-to-one chat messages or group chat messages.
+ */
+ public interface BaseMessageColumns {
+ /**
+ * The user this message belongs to
+ * <P>Type: TEXT</P>
+ */
+ String CONTACT = "contact";
+
+ /**
+ * The body
+ * <P>Type: TEXT</P>
+ */
+ String BODY = "body";
+
+ /**
+ * The date this message is sent or received
+ * <P>Type: INTEGER</P>
+ */
+ String DATE = "date";
+
+ /**
+ * Message Type, see {@link MessageType}
+ * <P>Type: INTEGER</P>
+ */
+ String TYPE = "type";
+
+ /**
+ * Error Code: 0 means no error.
+ * <P>Type: INTEGER </P>
+ */
+ String ERROR_CODE = "err_code";
+
+ /**
+ * Error Message
+ * <P>Type: TEXT</P>
+ */
+ String ERROR_MESSAGE = "err_msg";
+
+ /**
+ * Packet ID, auto assigned by the GTalkService for outgoing messages or the
+ * GTalk server for incoming messages. The packet id field is optional for messages,
+ * so it could be null.
+ * <P>Type: STRING</P>
+ */
+ String PACKET_ID = "packet_id";
+ }
+
+ /**
+ * Columns from the Messages table.
+ */
+ public interface MessagesColumns extends BaseMessageColumns{
+ /**
+ * The provider id
+ * <P> Type: INTEGER </P>
+ */
+ String PROVIDER = "provider";
+
+ /**
+ * The account id
+ * <P> Type: INTEGER </P>
+ */
+ String ACCOUNT = "account";
+ }
+
+ /**
+ * This table contains messages.
+ */
+ public static final class Messages implements BaseColumns, MessagesColumns {
+ /**
+ * no public constructor since this is a utility class
+ */
+ private Messages() {}
+
+ /**
+ * Gets the Uri to query messages by contact.
+ *
+ * @param providerId the provider id of the contact.
+ * @param accountId the account id of the contact.
+ * @param username the user name of the contact.
+ * @return the Uri
+ */
+ public static final Uri getContentUriByContact(long providerId,
+ long accountId, String username) {
+ Uri.Builder builder = CONTENT_URI_MESSAGES_BY.buildUpon();
+ ContentUris.appendId(builder, providerId);
+ ContentUris.appendId(builder, accountId);
+ builder.appendPath(username);
+ return builder.build();
+ }
+
+ /**
+ * The content:// style URL for this table
+ */
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://im/messages");
+
+ /**
+ * The content:// style URL for messages by provider and account
+ */
+ public static final Uri CONTENT_URI_MESSAGES_BY =
+ Uri.parse("content://im/messagesBy");
+
+ /**
+ * The MIME type of {@link #CONTENT_URI} providing a directory of
+ * people.
+ */
+ public static final String CONTENT_TYPE = "vnd.android.cursor.dir/im-messages";
+
+ /**
+ * The MIME type of a {@link #CONTENT_URI} subdirectory of a single
+ * person.
+ */
+ public static final String CONTENT_ITEM_TYPE =
+ "vnd.android.cursor.item/im-messages";
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = "date ASC";
+
+ }
+
+ /**
+ * Columns for the GroupMember table.
+ */
+ public interface GroupMemberColumns {
+ /**
+ * The id of the group this member belongs to.
+ * <p>Type: INTEGER</p>
+ */
+ String GROUP = "groupId";
+
+ /**
+ * The full name of this member.
+ * <p>Type: TEXT</p>
+ */
+ String USERNAME = "username";
+
+ /**
+ * The nick name of this member.
+ * <p>Type: TEXT</p>
+ */
+ String NICKNAME = "nickname";
+ }
+
+ public final static class GroupMembers implements GroupMemberColumns {
+ private GroupMembers(){}
+
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://im/groupMembers");
+
+ /**
+ * The MIME type of {@link #CONTENT_URI} providing a directory of
+ * group members.
+ */
+ public static final String CONTENT_TYPE =
+ "vnd.android.cursor.dir/im-groupMembers";
+
+ /**
+ * The MIME type of a {@link #CONTENT_URI} subdirectory of a single
+ * group member.
+ */
+ public static final String CONTENT_ITEM_TYPE =
+ "vnd.android.cursor.item/im-groupMembers";
+ }
+
+ /**
+ * Columns from the Invitation table.
+ */
+ public interface InvitationColumns {
+ /**
+ * The provider id.
+ * <p>Type: INTEGER</p>
+ */
+ String PROVIDER = "providerId";
+
+ /**
+ * The account id.
+ * <p>Type: INTEGER</p>
+ */
+ String ACCOUNT = "accountId";
+
+ /**
+ * The invitation id.
+ * <p>Type: TEXT</p>
+ */
+ String INVITE_ID = "inviteId";
+
+ /**
+ * The name of the sender of the invitation.
+ * <p>Type: TEXT</p>
+ */
+ String SENDER = "sender";
+
+ /**
+ * The name of the group which the sender invite you to join.
+ * <p>Type: TEXT</p>
+ */
+ String GROUP_NAME = "groupName";
+
+ /**
+ * A note
+ * <p>Type: TEXT</p>
+ */
+ String NOTE = "note";
+
+ /**
+ * The current status of the invitation.
+ * <p>Type: TEXT</p>
+ */
+ String STATUS = "status";
+
+ int STATUS_PENDING = 0;
+ int STATUS_ACCEPTED = 1;
+ int STATUS_REJECTED = 2;
+ }
+
+ /**
+ * This table contains the invitations received from others.
+ */
+ public final static class Invitation implements InvitationColumns,
+ BaseColumns {
+ private Invitation() {
+ }
+
+ /**
+ * The content:// style URL for this table
+ */
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://im/invitations");
+
+ /**
+ * The MIME type of {@link #CONTENT_URI} providing a directory of
+ * invitations.
+ */
+ public static final String CONTENT_TYPE =
+ "vnd.android.cursor.dir/im-invitations";
+
+ /**
+ * The MIME type of a {@link #CONTENT_URI} subdirectory of a single
+ * invitation.
+ */
+ public static final String CONTENT_ITEM_TYPE =
+ "vnd.android.cursor.item/im-invitations";
+ }
+
+ /**
+ * Columns from the GroupMessages table
+ */
+ public interface GroupMessageColumns extends BaseMessageColumns {
+ /**
+ * The group this message belongs to
+ * <p>Type: TEXT</p>
+ */
+ String GROUP = "groupId";
+ }
+
+ /**
+ * This table contains group messages.
+ */
+ public final static class GroupMessages implements BaseColumns,
+ GroupMessageColumns {
+ private GroupMessages() {}
+
+ /**
+ * Gets the Uri to query group messages by group.
+ *
+ * @param groupId the group id.
+ * @return the Uri
+ */
+ public static final Uri getContentUriByGroup(long groupId) {
+ Uri.Builder builder = CONTENT_URI_GROUP_MESSAGES_BY.buildUpon();
+ ContentUris.appendId(builder, groupId);
+ return builder.build();
+ }
+
+ /**
+ * The content:// style URL for this table
+ */
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://im/groupMessages");
+
+ /**
+ * The content:// style URL for group messages by provider and account
+ */
+ public static final Uri CONTENT_URI_GROUP_MESSAGES_BY =
+ Uri.parse("content://im/groupMessagesBy");
+
+ /**
+ * The MIME type of {@link #CONTENT_URI} providing a directory of
+ * group messages.
+ */
+ public static final String CONTENT_TYPE = "vnd.android.cursor.dir/im-groupMessages";
+
+ /**
+ * The MIME type of a {@link #CONTENT_URI} subdirectory of a single
+ * group message.
+ */
+ public static final String CONTENT_ITEM_TYPE =
+ "vnd.android.cursor.item/im-groupMessages";
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = "date ASC";
+ }
+
+ /**
+ * Columns from the Avatars table
+ */
+ public interface AvatarsColumns {
+ /**
+ * The contact this avatar belongs to
+ * <P>Type: TEXT</P>
+ */
+ String CONTACT = "contact";
+
+ String PROVIDER = "provider_id";
+
+ String ACCOUNT = "account_id";
+
+ /**
+ * The hash of the image data
+ * <P>Type: TEXT</P>
+ */
+ String HASH = "hash";
+
+ /**
+ * raw image data
+ * <P>Type: BLOB</P>
+ */
+ String DATA = "data";
+ }
+
+ /**
+ * This table contains avatars.
+ */
+ public static final class Avatars implements BaseColumns, AvatarsColumns {
+ /**
+ * no public constructor since this is a utility class
+ */
+ private Avatars() {}
+
+ /**
+ * The content:// style URL for this table
+ */
+ public static final Uri CONTENT_URI = Uri.parse("content://im/avatars");
+
+ /**
+ * The content:// style URL for avatars by provider, account and contact
+ */
+ public static final Uri CONTENT_URI_AVATARS_BY =
+ Uri.parse("content://im/avatarsBy");
+
+ /**
+ * The MIME type of {@link #CONTENT_URI} providing the avatars
+ */
+ public static final String CONTENT_TYPE = "vnd.android.cursor.dir/im-avatars";
+
+ /**
+ * The MIME type of a {@link #CONTENT_URI}
+ */
+ public static final String CONTENT_ITEM_TYPE =
+ "vnd.android.cursor.item/im-avatars";
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = "contact ASC";
+
+ }
+
+ /**
+ * Columns shared between the IM and contacts presence tables
+ */
+ interface CommonPresenceColumns {
+ /**
+ * The priority, an integer, used by XMPP presence
+ * <P>Type: INTEGER</P>
+ */
+ String PRIORITY = "priority";
+
+ /**
+ * The server defined status.
+ * <P>Type: INTEGER (one of the values below)</P>
+ */
+ String PRESENCE_STATUS = "mode";
+
+ /**
+ * Presence Status definition
+ */
+ int OFFLINE = 0;
+ int INVISIBLE = 1;
+ int AWAY = 2;
+ int IDLE = 3;
+ int DO_NOT_DISTURB = 4;
+ int AVAILABLE = 5;
+
+ /**
+ * The user defined status line.
+ * <P>Type: TEXT</P>
+ */
+ String PRESENCE_CUSTOM_STATUS = "status";
+ }
+
+ /**
+ * Columns from the Presence table.
+ */
+ public interface PresenceColumns extends CommonPresenceColumns {
+ /**
+ * The contact id
+ * <P>Type: INTEGER</P>
+ */
+ String CONTACT_ID = "contact_id";
+
+ /**
+ * The contact's JID resource, only relevant for XMPP contact
+ * <P>Type: TEXT</P>
+ */
+ String JID_RESOURCE = "jid_resource";
+
+ /**
+ * The contact's client type
+ */
+ String CLIENT_TYPE = "client_type";
+
+ /**
+ * client type definitions
+ */
+ int CLIENT_TYPE_DEFAULT = 0;
+ int CLIENT_TYPE_MOBILE = 1;
+ int CLIENT_TYPE_ANDROID = 2;
+ }
+
+ /**
+ * Contains presence infomation for contacts.
+ */
+ public static final class Presence implements BaseColumns, PresenceColumns {
+ /**
+ * The content:// style URL for this table
+ */
+ public static final Uri CONTENT_URI = Uri.parse("content://im/presence");
+
+ /**
+ * The content URL for IM presences for an account
+ */
+ public static final Uri CONTENT_URI_BY_ACCOUNT = Uri.parse("content://im/presence/account");
+
+ /**
+ * The content:// style URL for operations on bulk contacts
+ */
+ public static final Uri BULK_CONTENT_URI = Uri.parse("content://im/bulk_presence");
+
+ /**
+ * The content:// style URL for seeding presences for a given account id.
+ */
+ public static final Uri SEED_PRESENCE_BY_ACCOUNT_CONTENT_URI =
+ Uri.parse("content://im/seed_presence/account");
+
+ /**
+ * The MIME type of a {@link #CONTENT_URI} providing a directory of presence
+ */
+ public static final String CONTENT_TYPE = "vnd.android.cursor.dir/im-presence";
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = "mode DESC";
+ }
+
+ /**
+ * Columns from the Chats table.
+ */
+ public interface ChatsColumns {
+ /**
+ * The contact ID this chat belongs to. The value is a long.
+ * <P>Type: TEXT</P>
+ */
+ String CONTACT_ID = "contact_id";
+
+ /**
+ * The GTalk JID resource. The value is a string.
+ */
+ String JID_RESOURCE = "jid_resource";
+
+ /**
+ * Whether this is a groupchat or not.
+ */
+ // TODO: remove this column since we already have a tag in contacts
+ // table to indicate it's a group chat.
+ String GROUP_CHAT = "groupchat";
+
+ /**
+ * The last unread message. This both indicates that there is an
+ * unread message, and what the message is.
+ *
+ * <P>Type: TEXT</P>
+ */
+ String LAST_UNREAD_MESSAGE = "last_unread_message";
+
+ /**
+ * The last message timestamp
+ * <P>Type: INT</P>
+ */
+ String LAST_MESSAGE_DATE = "last_message_date";
+
+ /**
+ * A message that is being composed. This indicates that there was a
+ * message being composed when the chat screen was shutdown, and what the
+ * message is.
+ *
+ * <P>Type: TEXT</P>
+ */
+ String UNSENT_COMPOSED_MESSAGE = "unsent_composed_message";
+ }
+
+ /**
+ * Contains ongoing chat sessions.
+ */
+ public static final class Chats implements BaseColumns, ChatsColumns {
+ /**
+ * no public constructor since this is a utility class
+ */
+ private Chats() {}
+
+ /**
+ * The content:// style URL for this table
+ */
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://im/chats");
+
+ /**
+ * The content URL for all chats that belong to the account
+ */
+ public static final Uri CONTENT_URI_BY_ACCOUNT = Uri.parse("content://im/chats/account");
+
+ /**
+ * The MIME type of {@link #CONTENT_URI} providing a directory of chats.
+ */
+ public static final String CONTENT_TYPE = "vnd.android.cursor.dir/im-chats";
+
+ /**
+ * The MIME type of a {@link #CONTENT_URI} subdirectory of a single chat.
+ */
+ public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/im-chats";
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = "last_message_date ASC";
+ }
+
+ /**
+ * Columns from session cookies table. Used for IMPS.
+ */
+ public static interface SessionCookiesColumns {
+ String NAME = "name";
+ String VALUE = "value";
+ String PROVIDER = "provider";
+ String ACCOUNT = "account";
+ }
+
+ /**
+ * Contains IMPS session cookies.
+ */
+ public static class SessionCookies implements SessionCookiesColumns, BaseColumns {
+ private SessionCookies() {
+ }
+
+ /**
+ * The content:// style URI for this table
+ */
+ public static final Uri CONTENT_URI = Uri.parse("content://im/sessionCookies");
+
+ /**
+ * The content:// style URL for session cookies by provider and account
+ */
+ public static final Uri CONTENT_URI_SESSION_COOKIES_BY =
+ Uri.parse("content://im/sessionCookiesBy");
+
+ /**
+ * The MIME type of {@link #CONTENT_URI} providing a directory of
+ * people.
+ */
+ public static final String CONTENT_TYPE = "vnd.android-dir/im-sessionCookies";
+ }
+
+ /**
+ * Columns from ProviderSettings table
+ */
+ public static interface ProviderSettingsColumns {
+ /**
+ * The id in database of the related provider
+ *
+ * <P>Type: INT</P>
+ */
+ String PROVIDER = "provider";
+
+ /**
+ * The name of the setting
+ * <P>Type: TEXT</P>
+ */
+ String NAME = "name";
+
+ /**
+ * The value of the setting
+ * <P>Type: TEXT</P>
+ */
+ String VALUE = "value";
+ }
+
+ public static class ProviderSettings implements ProviderSettingsColumns {
+ private ProviderSettings() {
+ }
+
+ /**
+ * The content:// style URI for this table
+ */
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://im/providerSettings");
+
+ /**
+ * The MIME type of {@link #CONTENT_URI} providing provider settings
+ */
+ public static final String CONTENT_TYPE = "vnd.android-dir/im-providerSettings";
+
+ /**
+ * A boolean value to indicate whether this provider should show the offline contacts
+ */
+ public static final String SHOW_OFFLINE_CONTACTS = "show_offline_contacts";
+
+ /** controls whether or not the GTalk service automatically connect to server. */
+ public static final String SETTING_AUTOMATICALLY_CONNECT_GTALK = "gtalk_auto_connect";
+
+ /** controls whether or not the IM service will be automatically started after boot */
+ public static final String SETTING_AUTOMATICALLY_START_SERVICE = "auto_start_service";
+
+ /** controls whether or not the offline contacts will be hided */
+ public static final String SETTING_HIDE_OFFLINE_CONTACTS = "hide_offline_contacts";
+
+ /** controls whether or not enable the IM notification */
+ public static final String SETTING_ENABLE_NOTIFICATION = "enable_notification";
+
+ /** specifies whether or not to vibrate */
+ public static final String SETTING_VIBRATE = "vibrate";
+
+ /** specifies the Uri string of the ringtone */
+ public static final String SETTING_RINGTONE = "ringtone";
+
+ /** specifies the Uri of the default ringtone */
+ public static final String SETTING_RINGTONE_DEFAULT =
+ "content://settings/system/notification_sound";
+
+ /** specifies whether or not to show mobile indicator to friends */
+ public static final String SETTING_SHOW_MOBILE_INDICATOR = "mobile_indicator";
+
+ /**
+ * Used for reliable message queue (RMQ). This is for storing the last rmq id received
+ * from the GTalk server
+ */
+ public static final String LAST_RMQ_RECEIVED = "last_rmq_rec";
+
+ /**
+ * Query the settings of the provider specified by id
+ *
+ * @param cr
+ * the relative content resolver
+ * @param providerId
+ * the specified id of provider
+ * @return a HashMap which contains all the settings for the specified
+ * provider
+ */
+ public static HashMap<String, String> queryProviderSettings(ContentResolver cr,
+ long providerId) {
+ HashMap<String, String> settings = new HashMap<String, String>();
+
+ String[] projection = { NAME, VALUE };
+ Cursor c = cr.query(ContentUris.withAppendedId(CONTENT_URI, providerId), projection, null, null, null);
+ if (c == null) {
+ return null;
+ }
+
+ while(c.moveToNext()) {
+ settings.put(c.getString(0), c.getString(1));
+ }
+
+ c.close();
+
+ return settings;
+ }
+
+ /**
+ * Get the string value of setting which is specified by provider id and the setting name.
+ *
+ * @param cr The ContentResolver to use to access the settings table.
+ * @param providerId The id of the provider.
+ * @param settingName The name of the setting.
+ * @return The value of the setting if the setting exist, otherwise return null.
+ */
+ public static String getStringValue(ContentResolver cr, long providerId, String settingName) {
+ String ret = null;
+ Cursor c = getSettingValue(cr, providerId, settingName);
+ if (c != null) {
+ ret = c.getString(0);
+ c.close();
+ }
+
+ return ret;
+ }
+
+ /**
+ * Get the boolean value of setting which is specified by provider id and the setting name.
+ *
+ * @param cr The ContentResolver to use to access the settings table.
+ * @param providerId The id of the provider.
+ * @param settingName The name of the setting.
+ * @return The value of the setting if the setting exist, otherwise return false.
+ */
+ public static boolean getBooleanValue(ContentResolver cr, long providerId, String settingName) {
+ boolean ret = false;
+ Cursor c = getSettingValue(cr, providerId, settingName);
+ if (c != null) {
+ ret = c.getInt(0) != 0;
+ c.close();
+ }
+ return ret;
+ }
+
+ private static Cursor getSettingValue(ContentResolver cr, long providerId, String settingName) {
+ Cursor c = cr.query(ContentUris.withAppendedId(CONTENT_URI, providerId), new String[]{VALUE}, NAME + "=?",
+ new String[]{settingName}, null);
+ if (c != null) {
+ if (!c.moveToFirst()) {
+ c.close();
+ return null;
+ }
+ }
+ return c;
+ }
+
+ /**
+ * Save a long value of setting in the table providerSetting.
+ *
+ * @param cr The ContentProvider used to access the providerSetting table.
+ * @param providerId The id of the provider.
+ * @param name The name of the setting.
+ * @param value The value of the setting.
+ */
+ public static void putLongValue(ContentResolver cr, long providerId, String name,
+ long value) {
+ ContentValues v = new ContentValues(3);
+ v.put(PROVIDER, providerId);
+ v.put(NAME, name);
+ v.put(VALUE, value);
+
+ cr.insert(CONTENT_URI, v);
+ }
+
+ /**
+ * Save a boolean value of setting in the table providerSetting.
+ *
+ * @param cr The ContentProvider used to access the providerSetting table.
+ * @param providerId The id of the provider.
+ * @param name The name of the setting.
+ * @param value The value of the setting.
+ */
+ public static void putBooleanValue(ContentResolver cr, long providerId, String name,
+ boolean value) {
+ ContentValues v = new ContentValues(3);
+ v.put(PROVIDER, providerId);
+ v.put(NAME, name);
+ v.put(VALUE, Boolean.toString(value));
+
+ cr.insert(CONTENT_URI, v);
+ }
+
+ /**
+ * Save a string value of setting in the table providerSetting.
+ *
+ * @param cr The ContentProvider used to access the providerSetting table.
+ * @param providerId The id of the provider.
+ * @param name The name of the setting.
+ * @param value The value of the setting.
+ */
+ public static void putStringValue(ContentResolver cr, long providerId, String name,
+ String value) {
+ ContentValues v = new ContentValues(3);
+ v.put(PROVIDER, providerId);
+ v.put(NAME, name);
+ v.put(VALUE, value);
+
+ cr.insert(CONTENT_URI, v);
+ }
+
+ /**
+ * A convenience method to set whether or not the GTalk service should be started
+ * automatically.
+ *
+ * @param contentResolver The ContentResolver to use to access the settings table
+ * @param autoConnect Whether the GTalk service should be started automatically.
+ */
+ public static void setAutomaticallyConnectGTalk(ContentResolver contentResolver,
+ long providerId, boolean autoConnect) {
+ putBooleanValue(contentResolver, providerId, SETTING_AUTOMATICALLY_CONNECT_GTALK,
+ autoConnect);
+ }
+
+ /**
+ * A convenience method to set whether or not the offline contacts should be hided
+ *
+ * @param contentResolver The ContentResolver to use to access the setting table
+ * @param hideOfflineContacts Whether the offline contacts should be hided
+ */
+ public static void setHideOfflineContacts(ContentResolver contentResolver,
+ long providerId, boolean hideOfflineContacts) {
+ putBooleanValue(contentResolver, providerId, SETTING_HIDE_OFFLINE_CONTACTS,
+ hideOfflineContacts);
+ }
+
+ /**
+ * A convenience method to set whether or not enable the IM notification.
+ *
+ * @param contentResolver The ContentResolver to use to access the setting table.
+ * @param enable Whether enable the IM notification
+ */
+ public static void setEnableNotification(ContentResolver contentResolver, long providerId,
+ boolean enable) {
+ putBooleanValue(contentResolver, providerId, SETTING_ENABLE_NOTIFICATION, enable);
+ }
+
+ /**
+ * A convenience method to set whether or not to vibrate.
+ *
+ * @param contentResolver The ContentResolver to use to access the setting table.
+ * @param vibrate Whether or not to vibrate
+ */
+ public static void setVibrate(ContentResolver contentResolver, long providerId,
+ boolean vibrate) {
+ putBooleanValue(contentResolver, providerId, SETTING_VIBRATE, vibrate);
+ }
+
+ /**
+ * A convenience method to set the Uri String of the ringtone.
+ *
+ * @param contentResolver The ContentResolver to use to access the setting table.
+ * @param ringtoneUri The Uri String of the ringtone to be set.
+ */
+ public static void setRingtoneURI(ContentResolver contentResolver, long providerId,
+ String ringtoneUri) {
+ putStringValue(contentResolver, providerId, SETTING_RINGTONE, ringtoneUri);
+ }
+
+ /**
+ * A convenience method to set whether or not to show mobile indicator.
+ *
+ * @param contentResolver The ContentResolver to use to access the setting table.
+ * @param showMobileIndicator Whether or not to show mobile indicator.
+ */
+ public static void setShowMobileIndicator(ContentResolver contentResolver, long providerId,
+ boolean showMobileIndicator) {
+ putBooleanValue(contentResolver, providerId, SETTING_SHOW_MOBILE_INDICATOR,
+ showMobileIndicator);
+ }
+
+ public static class QueryMap extends ContentQueryMap {
+ private ContentResolver mContentResolver;
+ private long mProviderId;
+
+ public QueryMap(ContentResolver contentResolver, long providerId, boolean keepUpdated,
+ Handler handlerForUpdateNotifications) {
+ super(contentResolver.query(CONTENT_URI,
+ new String[] {NAME,VALUE},
+ PROVIDER + "=" + providerId,
+ null, // no selection args
+ null), // no sort order
+ NAME, keepUpdated, handlerForUpdateNotifications);
+ mContentResolver = contentResolver;
+ mProviderId = providerId;
+ }
+
+ /**
+ * Set if the GTalk service should automatically connect to server.
+ *
+ * @param autoConnect if the GTalk service should auto connect to server.
+ */
+ public void setAutomaticallyConnectToGTalkServer(boolean autoConnect) {
+ ProviderSettings.setAutomaticallyConnectGTalk(mContentResolver, mProviderId,
+ autoConnect);
+ }
+
+ /**
+ * Check if the GTalk service should automatically connect to server.
+ * @return if the GTalk service should automatically connect to server.
+ */
+ public boolean getAutomaticallyConnectToGTalkServer() {
+ return getBoolean(SETTING_AUTOMATICALLY_CONNECT_GTALK,
+ true /* default to automatically sign in */);
+ }
+
+ /**
+ * Set whether or not the offline contacts should be hided.
+ *
+ * @param hideOfflineContacts Whether or not the offline contacts should be hided.
+ */
+ public void setHideOfflineContacts(boolean hideOfflineContacts) {
+ ProviderSettings.setHideOfflineContacts(mContentResolver, mProviderId,
+ hideOfflineContacts);
+ }
+
+ /**
+ * Check if the offline contacts should be hided.
+ *
+ * @return Whether or not the offline contacts should be hided.
+ */
+ public boolean getHideOfflineContacts() {
+ return getBoolean(SETTING_HIDE_OFFLINE_CONTACTS,
+ false/* by default not hide the offline contacts*/);
+ }
+
+ /**
+ * Set whether or not enable the IM notification.
+ *
+ * @param enable Whether or not enable the IM notification.
+ */
+ public void setEnableNotification(boolean enable) {
+ ProviderSettings.setEnableNotification(mContentResolver, mProviderId, enable);
+ }
+
+ /**
+ * Check if the IM notification is enabled.
+ *
+ * @return Whether or not enable the IM notification.
+ */
+ public boolean getEnableNotification() {
+ return getBoolean(SETTING_ENABLE_NOTIFICATION,
+ true/* by default enable the notification */);
+ }
+
+ /**
+ * Set whether or not to vibrate on IM notification.
+ *
+ * @param vibrate Whether or not to vibrate.
+ */
+ public void setVibrate(boolean vibrate) {
+ ProviderSettings.setVibrate(mContentResolver, mProviderId, vibrate);
+ }
+
+ /**
+ * Gets whether or not to vibrate on IM notification.
+ *
+ * @return Whether or not to vibrate.
+ */
+ public boolean getVibrate() {
+ return getBoolean(SETTING_VIBRATE, false /* by default disable vibrate */);
+ }
+
+ /**
+ * Set the Uri for the ringtone.
+ *
+ * @param ringtoneUri The Uri of the ringtone to be set.
+ */
+ public void setRingtoneURI(String ringtoneUri) {
+ ProviderSettings.setRingtoneURI(mContentResolver, mProviderId, ringtoneUri);
+ }
+
+ /**
+ * Get the Uri String of the current ringtone.
+ *
+ * @return The Uri String of the current ringtone.
+ */
+ public String getRingtoneURI() {
+ return getString(SETTING_RINGTONE, SETTING_RINGTONE_DEFAULT);
+ }
+
+ /**
+ * Set whether or not to show mobile indicator to friends.
+ *
+ * @param showMobile whether or not to show mobile indicator.
+ */
+ public void setShowMobileIndicator(boolean showMobile) {
+ ProviderSettings.setShowMobileIndicator(mContentResolver, mProviderId, showMobile);
+ }
+
+ /**
+ * Gets whether or not to show mobile indicator.
+ *
+ * @return Whether or not to show mobile indicator.
+ */
+ public boolean getShowMobileIndicator() {
+ return getBoolean(SETTING_SHOW_MOBILE_INDICATOR,
+ true /* by default show mobile indicator */);
+ }
+
+ /**
+ * Convenience function for retrieving a single settings value
+ * as a boolean.
+ *
+ * @param name The name of the setting to retrieve.
+ * @param def Value to return if the setting is not defined.
+ * @return The setting's current value, or 'def' if it is not defined.
+ */
+ private boolean getBoolean(String name, boolean def) {
+ ContentValues values = getValues(name);
+ return values != null ? values.getAsBoolean(VALUE) : def;
+ }
+
+ /**
+ * Convenience function for retrieving a single settings value
+ * as a String.
+ *
+ * @param name The name of the setting to retrieve.
+ * @param def The value to return if the setting is not defined.
+ * @return The setting's current value or 'def' if it is not defined.
+ */
+ private String getString(String name, String def) {
+ ContentValues values = getValues(name);
+ return values != null ? values.getAsString(VALUE) : def;
+ }
+
+ /**
+ * Convenience function for retrieving a single settings value
+ * as an Integer.
+ *
+ * @param name The name of the setting to retrieve.
+ * @param def The value to return if the setting is not defined.
+ * @return The setting's current value or 'def' if it is not defined.
+ */
+ private int getInteger(String name, int def) {
+ ContentValues values = getValues(name);
+ return values != null ? values.getAsInteger(VALUE) : def;
+ }
+ }
+
+ }
+
+ /**
+ * Columns from OutgoingRmq table
+ */
+ public interface OutgoingRmqColumns {
+ String RMQ_ID = "rmq_id";
+ String TYPE = "type";
+ String TIMESTAMP = "ts";
+ String DATA = "data";
+ }
+
+ /**
+ * The table for storing outgoing rmq packets.
+ */
+ public static final class OutgoingRmq implements BaseColumns, OutgoingRmqColumns {
+ private static String[] RMQ_ID_PROJECTION = new String[] {
+ RMQ_ID,
+ };
+
+ /**
+ * queryHighestRmqId
+ *
+ * @param resolver the content resolver
+ * @return the highest rmq id assigned to the rmq packet, or 0 if there are no rmq packets
+ * in the OutgoingRmq table.
+ */
+ public static final long queryHighestRmqId(ContentResolver resolver) {
+ Cursor cursor = resolver.query(Im.OutgoingRmq.CONTENT_URI_FOR_HIGHEST_RMQ_ID,
+ RMQ_ID_PROJECTION,
+ null, // selection
+ null, // selection args
+ null // sort
+ );
+
+ long retVal = 0;
+ try {
+ //if (DBG) log("initializeRmqid: cursor.count= " + cursor.count());
+
+ if (cursor.moveToFirst()) {
+ retVal = cursor.getLong(cursor.getColumnIndexOrThrow(RMQ_ID));
+ }
+ } finally {
+ cursor.close();
+ }
+
+ return retVal;
+ }
+
+ /**
+ * The content:// style URL for this table.
+ */
+ public static final Uri CONTENT_URI = Uri.parse("content://im/outgoingRmqMessages");
+
+ /**
+ * The content:// style URL for the highest rmq id for the outgoing rmq messages
+ */
+ public static final Uri CONTENT_URI_FOR_HIGHEST_RMQ_ID =
+ Uri.parse("content://im/outgoingHighestRmqId");
+
+ /**
+ * The default sort order for this table.
+ */
+ public static final String DEFAULT_SORT_ORDER = "rmq_id ASC";
+ }
+
+ /**
+ * Columns for the LastRmqId table, which stores a single row for the last client rmq id
+ * sent to the server.
+ */
+ public interface LastRmqIdColumns {
+ String RMQ_ID = "rmq_id";
+ }
+
+ /**
+ * The table for storing the last client rmq id sent to the server.
+ */
+ public static final class LastRmqId implements BaseColumns, LastRmqIdColumns {
+ private static String[] PROJECTION = new String[] {
+ RMQ_ID,
+ };
+
+ /**
+ * queryLastRmqId
+ *
+ * queries the last rmq id saved in the LastRmqId table.
+ *
+ * @param resolver the content resolver.
+ * @return the last rmq id stored in the LastRmqId table, or 0 if not found.
+ */
+ public static final long queryLastRmqId(ContentResolver resolver) {
+ Cursor cursor = resolver.query(Im.LastRmqId.CONTENT_URI,
+ PROJECTION,
+ null, // selection
+ null, // selection args
+ null // sort
+ );
+
+ long retVal = 0;
+ try {
+ if (cursor.moveToFirst()) {
+ retVal = cursor.getLong(cursor.getColumnIndexOrThrow(RMQ_ID));
+ }
+ } finally {
+ cursor.close();
+ }
+
+ return retVal;
+ }
+
+ /**
+ * saveLastRmqId
+ *
+ * saves the rmqId to the lastRmqId table. This will override the existing row if any,
+ * as we only keep one row of data in this table.
+ *
+ * @param resolver the content resolver.
+ * @param rmqId the rmq id to be saved.
+ */
+ public static final void saveLastRmqId(ContentResolver resolver, long rmqId) {
+ ContentValues values = new ContentValues();
+
+ // always replace the first row.
+ values.put(_ID, 1);
+ values.put(RMQ_ID, rmqId);
+ resolver.insert(CONTENT_URI, values);
+ }
+
+ /**
+ * The content:// style URL for this table.
+ */
+ public static final Uri CONTENT_URI = Uri.parse("content://im/lastRmqId");
+ }
+}
diff --git a/core/java/android/provider/MediaStore.java b/core/java/android/provider/MediaStore.java
new file mode 100644
index 0000000..d99ad36
--- /dev/null
+++ b/core/java/android/provider/MediaStore.java
@@ -0,0 +1,1224 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.provider;
+
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.ContentUris;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Matrix;
+import android.net.Uri;
+import android.os.Environment;
+import android.util.Log;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.text.Collator;
+
+/**
+ * The Media provider contains meta data for all available media on both internal
+ * and external storage devices.
+ */
+public final class MediaStore
+{
+ private final static String TAG = "MediaStore";
+
+ public static final String AUTHORITY = "media";
+
+ private static final String CONTENT_AUTHORITY_SLASH = "content://" + AUTHORITY + "/";
+
+ /**
+ * Standard Intent action that can be sent to have the media application
+ * capture an image and return it. The image is returned as a Bitmap
+ * object in the extra field.
+ * @hide
+ */
+ public final static String ACTION_IMAGE_CAPTURE = "android.media.action.IMAGE_CAPTURE";
+
+ /**
+ * Common fields for most MediaProvider tables
+ */
+
+ public interface MediaColumns extends BaseColumns {
+ /**
+ * The data stream for the file
+ * <P>Type: DATA STREAM</P>
+ */
+ public static final String DATA = "_data";
+
+ /**
+ * The size of the file in bytes
+ * <P>Type: INTEGER (long)</P>
+ */
+ public static final String SIZE = "_size";
+
+ /**
+ * The display name of the file
+ * <P>Type: TEXT</P>
+ */
+ public static final String DISPLAY_NAME = "_display_name";
+
+ /**
+ * The title of the content
+ * <P>Type: TEXT</P>
+ */
+ public static final String TITLE = "title";
+
+ /**
+ * The time the file was added to the media provider
+ * Units are seconds since 1970.
+ * <P>Type: INTEGER (long)</P>
+ */
+ public static final String DATE_ADDED = "date_added";
+
+ /**
+ * The time the file was last modified
+ * Units are seconds since 1970.
+ * NOTE: This is for internal use by the media scanner. Do not modify this field.
+ * <P>Type: INTEGER (long)</P>
+ */
+ public static final String DATE_MODIFIED = "date_modified";
+
+ /**
+ * The MIME type of the file
+ * <P>Type: TEXT</P>
+ */
+ public static final String MIME_TYPE = "mime_type";
+ }
+
+ /**
+ * Contains meta data for all available images.
+ */
+ public static final class Images
+ {
+ public interface ImageColumns extends MediaColumns {
+ /**
+ * The description of the image
+ * <P>Type: TEXT</P>
+ */
+ public static final String DESCRIPTION = "description";
+
+ /**
+ * The picasa id of the image
+ * <P>Type: TEXT</P>
+ */
+ public static final String PICASA_ID = "picasa_id";
+
+ /**
+ * Whether the video should be published as public or private
+ * <P>Type: INTEGER</P>
+ */
+ public static final String IS_PRIVATE = "isprivate";
+
+ /**
+ * The latitude where the image was captured.
+ * <P>Type: DOUBLE</P>
+ */
+ public static final String LATITUDE = "latitude";
+
+ /**
+ * The longitude where the image was captured.
+ * <P>Type: DOUBLE</P>
+ */
+ public static final String LONGITUDE = "longitude";
+
+ /**
+ * The date & time that the image was taken in units
+ * of milliseconds since jan 1, 1970.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String DATE_TAKEN = "datetaken";
+
+ /**
+ * The orientation for the image expressed as degrees.
+ * Only degrees 0, 90, 180, 270 will work.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String ORIENTATION = "orientation";
+
+ /**
+ * The mini thumb id.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String MINI_THUMB_MAGIC = "mini_thumb_magic";
+
+ /**
+ * The bucket id of the image
+ * <P>Type: TEXT</P>
+ */
+ public static final String BUCKET_ID = "bucket_id";
+
+ /**
+ * The bucket display name of the image
+ * <P>Type: TEXT</P>
+ */
+ public static final String BUCKET_DISPLAY_NAME = "bucket_display_name";
+ }
+
+ public static final class Media implements ImageColumns {
+ public static final Cursor query(ContentResolver cr, Uri uri, String[] projection)
+ {
+ return cr.query(uri, projection, null, null, DEFAULT_SORT_ORDER);
+ }
+
+ public static final Cursor query(ContentResolver cr, Uri uri, String[] projection,
+ String where, String orderBy)
+ {
+ return cr.query(uri, projection, where,
+ null, orderBy == null ? DEFAULT_SORT_ORDER : orderBy);
+ }
+
+ public static final Cursor query(ContentResolver cr, Uri uri, String[] projection,
+ String selection, String [] selectionArgs, String orderBy)
+ {
+ return cr.query(uri, projection, selection,
+ selectionArgs, orderBy == null ? DEFAULT_SORT_ORDER : orderBy);
+ }
+
+ /**
+ * Retrieves an image for the given url as a {@link Bitmap}.
+ *
+ * @param cr The content resolver to use
+ * @param url The url of the image
+ * @throws FileNotFoundException
+ * @throws IOException
+ */
+ public static final Bitmap getBitmap(ContentResolver cr, Uri url)
+ throws FileNotFoundException, IOException
+ {
+ InputStream input = cr.openInputStream(url);
+ Bitmap bitmap = BitmapFactory.decodeStream(input);
+ input.close();
+ return bitmap;
+ }
+
+ /**
+ * Insert an image and create a thumbnail for it.
+ *
+ * @param cr The content resolver to use
+ * @param imagePath The path to the image to insert
+ * @param name The name of the image
+ * @param description The description of the image
+ * @return The URL to the newly created image
+ * @throws FileNotFoundException
+ */
+ public static final String insertImage(ContentResolver cr, String imagePath, String name,
+ String description) throws FileNotFoundException
+ {
+ // Check if file exists with a FileInputStream
+ FileInputStream stream = new FileInputStream(imagePath);
+ try {
+ return insertImage(cr, BitmapFactory.decodeFile(imagePath), name, description);
+ } finally {
+ try {
+ stream.close();
+ } catch (IOException e) {
+ }
+ }
+ }
+
+ private static final Bitmap StoreThumbnail(
+ ContentResolver cr,
+ Bitmap source,
+ long id,
+ float width, float height,
+ int kind) {
+ // create the matrix to scale it
+ Matrix matrix = new Matrix();
+
+ float scaleX = width / source.getWidth();
+ float scaleY = height / source.getHeight();
+
+ matrix.setScale(scaleX, scaleY);
+
+ Bitmap thumb = Bitmap.createBitmap(source, 0, 0,
+ source.getWidth(),
+ source.getHeight(), matrix,
+ true);
+
+ ContentValues values = new ContentValues(4);
+ values.put(Images.Thumbnails.KIND, kind);
+ values.put(Images.Thumbnails.IMAGE_ID, (int)id);
+ values.put(Images.Thumbnails.HEIGHT, thumb.getHeight());
+ values.put(Images.Thumbnails.WIDTH, thumb.getWidth());
+
+ Uri url = cr.insert(Images.Thumbnails.EXTERNAL_CONTENT_URI, values);
+
+ try {
+ OutputStream thumbOut = cr.openOutputStream(url);
+
+ thumb.compress(Bitmap.CompressFormat.JPEG, 100, thumbOut);
+ thumbOut.close();
+ return thumb;
+ }
+ catch (FileNotFoundException ex) {
+ return null;
+ }
+ catch (IOException ex) {
+ return null;
+ }
+ }
+
+ /**
+ * Insert an image and create a thumbnail for it.
+ *
+ * @param cr The content resolver to use
+ * @param source The stream to use for the image
+ * @param title The name of the image
+ * @param description The description of the image
+ * @return The URL to the newly created image, or <code>null</code> if the image failed to be stored
+ * for any reason.
+ */
+ public static final String insertImage(ContentResolver cr, Bitmap source,
+ String title, String description)
+ {
+ ContentValues values = new ContentValues();
+ values.put(Images.Media.TITLE, title);
+ values.put(Images.Media.DESCRIPTION, description);
+ values.put(Images.Media.MIME_TYPE, "image/jpeg");
+
+ Uri url = null;
+ String stringUrl = null; /* value to be returned */
+
+ try
+ {
+ url = cr.insert(EXTERNAL_CONTENT_URI, values);
+
+ if (source != null) {
+ OutputStream imageOut = cr.openOutputStream(url);
+ try {
+ source.compress(Bitmap.CompressFormat.JPEG, 50, imageOut);
+ } finally {
+ imageOut.close();
+ }
+
+ long id = ContentUris.parseId(url);
+ Bitmap miniThumb = StoreThumbnail(cr, source, id, 320F, 240F, Images.Thumbnails.MINI_KIND);
+ Bitmap microThumb = StoreThumbnail(cr, miniThumb, id, 50F, 50F, Images.Thumbnails.MICRO_KIND);
+ } else {
+ Log.e(TAG, "Failed to create thumbnail, removing original");
+ cr.delete(url, null, null);
+ url = null;
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to insert image", e);
+ if (url != null) {
+ cr.delete(url, null, null);
+ url = null;
+ }
+ }
+
+ if (url != null) {
+ stringUrl = url.toString();
+ }
+
+ return stringUrl;
+ }
+
+ /**
+ * Get the content:// style URI for the image media table on the
+ * given volume.
+ *
+ * @param volumeName the name of the volume to get the URI for
+ * @return the URI to the image media table on the given volume
+ */
+ public static Uri getContentUri(String volumeName) {
+ return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName +
+ "/images/media");
+ }
+
+ /**
+ * The content:// style URI for the internal storage.
+ */
+ public static final Uri INTERNAL_CONTENT_URI =
+ getContentUri("internal");
+
+ /**
+ * The content:// style URI for the "primary" external storage
+ * volume.
+ */
+ public static final Uri EXTERNAL_CONTENT_URI =
+ getContentUri("external");
+
+ /**
+ * The MIME type of of this directory of
+ * images. Note that each entry in this directory will have a standard
+ * image MIME type as appropriate -- for example, image/jpeg.
+ */
+ public static final String CONTENT_TYPE = "vnd.android.cursor.dir/image";
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = "name ASC";
+ }
+
+ public static class Thumbnails implements BaseColumns
+ {
+ public static final Cursor query(ContentResolver cr, Uri uri, String[] projection)
+ {
+ return cr.query(uri, projection, null, null, DEFAULT_SORT_ORDER);
+ }
+
+ public static final Cursor queryMiniThumbnails(ContentResolver cr, Uri uri, int kind, String[] projection)
+ {
+ return cr.query(uri, projection, "kind = " + kind, null, DEFAULT_SORT_ORDER);
+ }
+
+ public static final Cursor queryMiniThumbnail(ContentResolver cr, long origId, int kind, String[] projection)
+ {
+ return cr.query(EXTERNAL_CONTENT_URI, projection,
+ IMAGE_ID + " = " + origId + " AND " + KIND + " = " +
+ kind, null, null);
+ }
+
+ /**
+ * Get the content:// style URI for the image media table on the
+ * given volume.
+ *
+ * @param volumeName the name of the volume to get the URI for
+ * @return the URI to the image media table on the given volume
+ */
+ public static Uri getContentUri(String volumeName) {
+ return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName +
+ "/images/thumbnails");
+ }
+
+ /**
+ * The content:// style URI for the internal storage.
+ */
+ public static final Uri INTERNAL_CONTENT_URI =
+ getContentUri("internal");
+
+ /**
+ * The content:// style URI for the "primary" external storage
+ * volume.
+ */
+ public static final Uri EXTERNAL_CONTENT_URI =
+ getContentUri("external");
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = "image_id ASC";
+
+ /**
+ * The data stream for the thumbnail
+ * <P>Type: DATA STREAM</P>
+ */
+ public static final String DATA = "_data";
+
+ /**
+ * The original image for the thumbnal
+ * <P>Type: INTEGER (ID from Images table)</P>
+ */
+ public static final String IMAGE_ID = "image_id";
+
+ /**
+ * The kind of the thumbnail
+ * <P>Type: INTEGER (One of the values below)</P>
+ */
+ public static final String KIND = "kind";
+
+ public static final int MINI_KIND = 1;
+ public static final int FULL_SCREEN_KIND = 2;
+ public static final int MICRO_KIND = 3;
+
+ /**
+ * The width of the thumbnal
+ * <P>Type: INTEGER (long)</P>
+ */
+ public static final String WIDTH = "width";
+
+ /**
+ * The height of the thumbnail
+ * <P>Type: INTEGER (long)</P>
+ */
+ public static final String HEIGHT = "height";
+ }
+ }
+
+ /**
+ * Container for all audio content.
+ */
+ public static final class Audio {
+ /**
+ * Columns for audio file that show up in multiple tables.
+ */
+ public interface AudioColumns extends MediaColumns {
+
+ /**
+ * A non human readable key calculated from the TITLE, used for
+ * searching, sorting and grouping
+ * <P>Type: TEXT</P>
+ */
+ public static final String TITLE_KEY = "title_key";
+
+ /**
+ * The duration of the audio file, in ms
+ * <P>Type: INTEGER (long)</P>
+ */
+ public static final String DURATION = "duration";
+
+ /**
+ * The id of the artist who created the audio file, if any
+ * <P>Type: INTEGER (long)</P>
+ */
+ public static final String ARTIST_ID = "artist_id";
+
+ /**
+ * The artist who created the audio file, if any
+ * <P>Type: TEXT</P>
+ */
+ public static final String ARTIST = "artist";
+
+ /**
+ * A non human readable key calculated from the ARTIST, used for
+ * searching, sorting and grouping
+ * <P>Type: TEXT</P>
+ */
+ public static final String ARTIST_KEY = "artist_key";
+
+ /**
+ * The composer of the audio file, if any
+ * <P>Type: TEXT</P>
+ */
+ public static final String COMPOSER = "composer";
+
+ /**
+ * The id of the album the audio file is from, if any
+ * <P>Type: INTEGER (long)</P>
+ */
+ public static final String ALBUM_ID = "album_id";
+
+ /**
+ * The album the audio file is from, if any
+ * <P>Type: TEXT</P>
+ */
+ public static final String ALBUM = "album";
+
+ /**
+ * A non human readable key calculated from the ALBUM, used for
+ * searching, sorting and grouping
+ * <P>Type: TEXT</P>
+ */
+ public static final String ALBUM_KEY = "album_key";
+
+ /**
+ * A URI to the album art, if any
+ * <P>Type: TEXT</P>
+ */
+ public static final String ALBUM_ART = "album_art";
+
+ /**
+ * The track number of this song on the album, if any.
+ * This number encodes both the track number and the
+ * disc number. For multi-disc sets, this number will
+ * be 1xxx for tracks on the first disc, 2xxx for tracks
+ * on the second disc, etc.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String TRACK = "track";
+
+ /**
+ * The year the audio file was recorded, if any
+ * <P>Type: INTEGER</P>
+ */
+ public static final String YEAR = "year";
+
+ /**
+ * Non-zero if the audio file is music
+ * <P>Type: INTEGER (boolean)</P>
+ */
+ public static final String IS_MUSIC = "is_music";
+
+ /**
+ * Non-zero id the audio file may be a ringtone
+ * <P>Type: INTEGER (boolean)</P>
+ */
+ public static final String IS_RINGTONE = "is_ringtone";
+
+ /**
+ * Non-zero id the audio file may be an alarm
+ * <P>Type: INTEGER (boolean)</P>
+ */
+ public static final String IS_ALARM = "is_alarm";
+
+ /**
+ * Non-zero id the audio file may be a notification sound
+ * <P>Type: INTEGER (boolean)</P>
+ */
+ public static final String IS_NOTIFICATION = "is_notification";
+ }
+
+ /**
+ * Converts a name to a "key" that can be used for grouping, sorting
+ * and searching.
+ * The rules that govern this conversion are:
+ * - remove 'special' characters like ()[]'!?.,
+ * - remove leading/trailing spaces
+ * - convert everything to lowercase
+ * - remove leading "the ", "an " and "a "
+ * - remove trailing ", the|an|a"
+ * - remove accents. This step leaves us with CollationKey data,
+ * which is not human readable
+ *
+ * @param name The artist or album name to convert
+ * @return The "key" for the given name.
+ */
+ public static String keyFor(String name) {
+ if (name != null) {
+ if (name.equals(android.media.MediaFile.UNKNOWN_STRING)) {
+ return "\001";
+ }
+ name = name.trim().toLowerCase();
+ if (name.startsWith("the ")) {
+ name = name.substring(4);
+ }
+ if (name.startsWith("an ")) {
+ name = name.substring(3);
+ }
+ if (name.startsWith("a ")) {
+ name = name.substring(2);
+ }
+ if (name.endsWith(", the") || name.endsWith(",the") ||
+ name.endsWith(", an") || name.endsWith(",an") ||
+ name.endsWith(", a") || name.endsWith(",a")) {
+ name = name.substring(0, name.lastIndexOf(','));
+ }
+ name = name.replaceAll("[\\[\\]\\(\\)'.,?!]", "").trim();
+ if (name.length() > 0) {
+ // Insert a separator between the characters to avoid
+ // matches on a partial character. If we ever change
+ // to start-of-word-only matches, this can be removed.
+ StringBuilder b = new StringBuilder();
+ b.append('.');
+ int nl = name.length();
+ for (int i = 0; i < nl; i++) {
+ b.append(name.charAt(i));
+ b.append('.');
+ }
+ name = b.toString();
+ return DatabaseUtils.getCollationKey(name);
+ } else {
+ return "";
+ }
+ }
+ return null;
+ }
+
+ public static final class Media implements AudioColumns {
+ /**
+ * Get the content:// style URI for the audio media table on the
+ * given volume.
+ *
+ * @param volumeName the name of the volume to get the URI for
+ * @return the URI to the audio media table on the given volume
+ */
+ public static Uri getContentUri(String volumeName) {
+ return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName +
+ "/audio/media");
+ }
+
+ public static Uri getContentUriForPath(String path) {
+ return (path.startsWith(Environment.getExternalStorageDirectory().getPath()) ?
+ EXTERNAL_CONTENT_URI : INTERNAL_CONTENT_URI);
+ }
+
+ /**
+ * The content:// style URI for the internal storage.
+ */
+ public static final Uri INTERNAL_CONTENT_URI =
+ getContentUri("internal");
+
+ /**
+ * The content:// style URI for the "primary" external storage
+ * volume.
+ */
+ public static final Uri EXTERNAL_CONTENT_URI =
+ getContentUri("external");
+
+ /**
+ * The MIME type for this table.
+ */
+ public static final String CONTENT_TYPE = "vnd.android.cursor.dir/audio";
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = TITLE;
+
+ /**
+ * Activity Action: Start SoundRecorder application.
+ * <p>Input: nothing.
+ * <p>Output: An uri to the recorded sound stored in the Media Library
+ * if the recording was successful.
+ *
+ */
+ public static final String RECORD_SOUND_ACTION =
+ "android.provider.MediaStore.RECORD_SOUND";
+ }
+
+ /**
+ * Columns representing an audio genre
+ */
+ public interface GenresColumns {
+ /**
+ * The name of the genre
+ * <P>Type: TEXT</P>
+ */
+ public static final String NAME = "name";
+ }
+
+ /**
+ * Contains all genres for audio files
+ */
+ public static final class Genres implements BaseColumns, GenresColumns {
+ /**
+ * Get the content:// style URI for the audio genres table on the
+ * given volume.
+ *
+ * @param volumeName the name of the volume to get the URI for
+ * @return the URI to the audio genres table on the given volume
+ */
+ public static Uri getContentUri(String volumeName) {
+ return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName +
+ "/audio/genres");
+ }
+
+ /**
+ * The content:// style URI for the internal storage.
+ */
+ public static final Uri INTERNAL_CONTENT_URI =
+ getContentUri("internal");
+
+ /**
+ * The content:// style URI for the "primary" external storage
+ * volume.
+ */
+ public static final Uri EXTERNAL_CONTENT_URI =
+ getContentUri("external");
+
+ /**
+ * The MIME type for this table.
+ */
+ public static final String CONTENT_TYPE = "vnd.android.cursor.dir/genre";
+
+ /**
+ * The MIME type for entries in this table.
+ */
+ public static final String ENTRY_CONTENT_TYPE = "vnd.android.cursor.item/genre";
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = NAME;
+
+ /**
+ * Sub-directory of each genre containing all members.
+ */
+ public static final class Members implements AudioColumns {
+
+ public static final Uri getContentUri(String volumeName,
+ long genreId) {
+ return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName
+ + "/audio/genres/" + genreId + "/members");
+ }
+
+ /**
+ * A subdirectory of each genre containing all member audio files.
+ */
+ public static final String CONTENT_DIRECTORY = "members";
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = TITLE;
+
+ /**
+ * The ID of the audio file
+ * <P>Type: INTEGER (long)</P>
+ */
+ public static final String AUDIO_ID = "audio_id";
+
+ /**
+ * The ID of the genre
+ * <P>Type: INTEGER (long)</P>
+ */
+ public static final String GENRE_ID = "genre_id";
+ }
+ }
+
+ /**
+ * Columns representing a playlist
+ */
+ public interface PlaylistsColumns {
+ /**
+ * The name of the playlist
+ * <P>Type: TEXT</P>
+ */
+ public static final String NAME = "name";
+
+ /**
+ * The data stream for the playlist file
+ * <P>Type: DATA STREAM</P>
+ */
+ public static final String DATA = "_data";
+
+ /**
+ * The time the file was added to the media provider
+ * Units are seconds since 1970.
+ * <P>Type: INTEGER (long)</P>
+ */
+ public static final String DATE_ADDED = "date_added";
+
+ /**
+ * The time the file was last modified
+ * Units are seconds since 1970.
+ * NOTE: This is for internal use by the media scanner. Do not modify this field.
+ * <P>Type: INTEGER (long)</P>
+ */
+ public static final String DATE_MODIFIED = "date_modified";
+ }
+
+ /**
+ * Contains playlists for audio files
+ */
+ public static final class Playlists implements BaseColumns,
+ PlaylistsColumns {
+ /**
+ * Get the content:// style URI for the audio playlists table on the
+ * given volume.
+ *
+ * @param volumeName the name of the volume to get the URI for
+ * @return the URI to the audio playlists table on the given volume
+ */
+ public static Uri getContentUri(String volumeName) {
+ return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName +
+ "/audio/playlists");
+ }
+
+ /**
+ * The content:// style URI for the internal storage.
+ */
+ public static final Uri INTERNAL_CONTENT_URI =
+ getContentUri("internal");
+
+ /**
+ * The content:// style URI for the "primary" external storage
+ * volume.
+ */
+ public static final Uri EXTERNAL_CONTENT_URI =
+ getContentUri("external");
+
+ /**
+ * The MIME type for this table.
+ */
+ public static final String CONTENT_TYPE = "vnd.android.cursor.dir/playlist";
+
+ /**
+ * The MIME type for entries in this table.
+ */
+ public static final String ENTRY_CONTENT_TYPE = "vnd.android.cursor.item/playlist";
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = NAME;
+
+ /**
+ * Sub-directory of each playlist containing all members.
+ */
+ public static final class Members implements AudioColumns {
+ public static final Uri getContentUri(String volumeName,
+ long playlistId) {
+ return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName
+ + "/audio/playlists/" + playlistId + "/members");
+ }
+
+ /**
+ * The ID within the playlist.
+ */
+ public static final String _ID = "_id";
+
+ /**
+ * A subdirectory of each playlist containing all member audio
+ * files.
+ */
+ public static final String CONTENT_DIRECTORY = "members";
+
+ /**
+ * The ID of the audio file
+ * <P>Type: INTEGER (long)</P>
+ */
+ public static final String AUDIO_ID = "audio_id";
+
+ /**
+ * The ID of the playlist
+ * <P>Type: INTEGER (long)</P>
+ */
+ public static final String PLAYLIST_ID = "playlist_id";
+
+ /**
+ * The order of the songs in the playlist
+ * <P>Type: INTEGER (long)></P>
+ */
+ public static final String PLAY_ORDER = "play_order";
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = PLAY_ORDER;
+ }
+ }
+
+ /**
+ * Columns representing an artist
+ */
+ public interface ArtistColumns {
+ /**
+ * The artist who created the audio file, if any
+ * <P>Type: TEXT</P>
+ */
+ public static final String ARTIST = "artist";
+
+ /**
+ * A non human readable key calculated from the ARTIST, used for
+ * searching, sorting and grouping
+ * <P>Type: TEXT</P>
+ */
+ public static final String ARTIST_KEY = "artist_key";
+
+ /**
+ * The number of albums in the database for this artist
+ */
+ public static final String NUMBER_OF_ALBUMS = "number_of_albums";
+
+ /**
+ * The number of albums in the database for this artist
+ */
+ public static final String NUMBER_OF_TRACKS = "number_of_tracks";
+ }
+
+ /**
+ * Contains artists for audio files
+ */
+ public static final class Artists implements BaseColumns, ArtistColumns {
+ /**
+ * Get the content:// style URI for the artists table on the
+ * given volume.
+ *
+ * @param volumeName the name of the volume to get the URI for
+ * @return the URI to the audio artists table on the given volume
+ */
+ public static Uri getContentUri(String volumeName) {
+ return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName +
+ "/audio/artists");
+ }
+
+ /**
+ * The content:// style URI for the internal storage.
+ */
+ public static final Uri INTERNAL_CONTENT_URI =
+ getContentUri("internal");
+
+ /**
+ * The content:// style URI for the "primary" external storage
+ * volume.
+ */
+ public static final Uri EXTERNAL_CONTENT_URI =
+ getContentUri("external");
+
+ /**
+ * The MIME type for this table.
+ */
+ public static final String CONTENT_TYPE = "vnd.android.cursor.dir/artists";
+
+ /**
+ * The MIME type for entries in this table.
+ */
+ public static final String ENTRY_CONTENT_TYPE = "vnd.android.cursor.item/artist";
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = ARTIST_KEY;
+
+ /**
+ * Sub-directory of each artist containing all albums on which
+ * a song by the artist appears.
+ */
+ public static final class Albums implements AlbumColumns {
+ public static final Uri getContentUri(String volumeName,
+ long artistId) {
+ return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName
+ + "/audio/artists/" + artistId + "/albums");
+ }
+ }
+ }
+
+ /**
+ * Columns representing an album
+ */
+ public interface AlbumColumns {
+
+ /**
+ * The id for the album
+ * <P>Type: INTEGER</P>
+ */
+ public static final String ALBUM_ID = "album_id";
+
+ /**
+ * The album on which the audio file appears, if any
+ * <P>Type: TEXT</P>
+ */
+ public static final String ALBUM = "album";
+
+ /**
+ * The artist whose songs appear on this album
+ * <P>Type: TEXT</P>
+ */
+ public static final String ARTIST = "artist";
+
+ /**
+ * The number of songs on this album
+ * <P>Type: INTEGER</P>
+ */
+ public static final String NUMBER_OF_SONGS = "numsongs";
+
+ /**
+ * The year in which the earliest and latest songs
+ * on this album were released. These will often
+ * be the same, but for compilation albums they
+ * might differ.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String FIRST_YEAR = "minyear";
+ public static final String LAST_YEAR = "maxyear";
+
+ /**
+ * A non human readable key calculated from the ALBUM, used for
+ * searching, sorting and grouping
+ * <P>Type: TEXT</P>
+ */
+ public static final String ALBUM_KEY = "album_key";
+
+ /**
+ * Cached album art.
+ * <P>Type: TEXT</P>
+ */
+ public static final String ALBUM_ART = "album_art";
+ }
+
+ /**
+ * Contains artists for audio files
+ */
+ public static final class Albums implements BaseColumns, AlbumColumns {
+ /**
+ * Get the content:// style URI for the albums table on the
+ * given volume.
+ *
+ * @param volumeName the name of the volume to get the URI for
+ * @return the URI to the audio albums table on the given volume
+ */
+ public static Uri getContentUri(String volumeName) {
+ return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName +
+ "/audio/albums");
+ }
+
+ /**
+ * The content:// style URI for the internal storage.
+ */
+ public static final Uri INTERNAL_CONTENT_URI =
+ getContentUri("internal");
+
+ /**
+ * The content:// style URI for the "primary" external storage
+ * volume.
+ */
+ public static final Uri EXTERNAL_CONTENT_URI =
+ getContentUri("external");
+
+ /**
+ * The MIME type for this table.
+ */
+ public static final String CONTENT_TYPE = "vnd.android.cursor.dir/albums";
+
+ /**
+ * The MIME type for entries in this table.
+ */
+ public static final String ENTRY_CONTENT_TYPE = "vnd.android.cursor.item/album";
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = ALBUM_KEY;
+ }
+ }
+
+ public static final class Video {
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = "name ASC";
+
+ public static final Cursor query(ContentResolver cr, Uri uri, String[] projection)
+ {
+ return cr.query(uri, projection, null, null, DEFAULT_SORT_ORDER);
+ }
+
+ public interface VideoColumns extends MediaColumns {
+
+ /**
+ * The duration of the video file, in ms
+ * <P>Type: INTEGER (long)</P>
+ */
+ public static final String DURATION = "duration";
+
+ /**
+ * The artist who created the video file, if any
+ * <P>Type: TEXT</P>
+ */
+ public static final String ARTIST = "artist";
+
+ /**
+ * The album the video file is from, if any
+ * <P>Type: TEXT</P>
+ */
+ public static final String ALBUM = "album";
+
+ /**
+ * The resolution of the video file, formatted as "XxY"
+ * <P>Type: TEXT</P>
+ */
+ public static final String RESOLUTION = "resolution";
+
+ /**
+ * The description of the video recording
+ * <P>Type: TEXT</P>
+ */
+ public static final String DESCRIPTION = "description";
+
+ /**
+ * Whether the video should be published as public or private
+ * <P>Type: INTEGER</P>
+ */
+ public static final String IS_PRIVATE = "isprivate";
+
+ /**
+ * The user-added tags associated with a video
+ * <P>Type: TEXT</P>
+ */
+ public static final String TAGS = "tags";
+
+ /**
+ * The YouTube category of the video
+ * <P>Type: TEXT</P>
+ */
+ public static final String CATEGORY = "category";
+
+ /**
+ * The language of the video
+ * <P>Type: TEXT</P>
+ */
+ public static final String LANGUAGE = "language";
+
+ /**
+ * The latitude where the image was captured.
+ * <P>Type: DOUBLE</P>
+ */
+ public static final String LATITUDE = "latitude";
+
+ /**
+ * The longitude where the image was captured.
+ * <P>Type: DOUBLE</P>
+ */
+ public static final String LONGITUDE = "longitude";
+
+ /**
+ * The date & time that the image was taken in units
+ * of milliseconds since jan 1, 1970.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String DATE_TAKEN = "datetaken";
+
+ /**
+ * The mini thumb id.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String MINI_THUMB_MAGIC = "mini_thumb_magic";
+ }
+
+ public static final class Media implements VideoColumns {
+ /**
+ * Get the content:// style URI for the video media table on the
+ * given volume.
+ *
+ * @param volumeName the name of the volume to get the URI for
+ * @return the URI to the video media table on the given volume
+ */
+ public static Uri getContentUri(String volumeName) {
+ return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName +
+ "/video/media");
+ }
+
+ /**
+ * The content:// style URI for the internal storage.
+ */
+ public static final Uri INTERNAL_CONTENT_URI =
+ getContentUri("internal");
+
+ /**
+ * The content:// style URI for the "primary" external storage
+ * volume.
+ */
+ public static final Uri EXTERNAL_CONTENT_URI =
+ getContentUri("external");
+
+ /**
+ * The MIME type for this table.
+ */
+ public static final String CONTENT_TYPE = "vnd.android.cursor.dir/video";
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = TITLE;
+ }
+ }
+
+ /**
+ * Uri for querying the state of the media scanner.
+ */
+ public static Uri getMediaScannerUri() {
+ return Uri.parse(CONTENT_AUTHORITY_SLASH + "none/media_scanner");
+ }
+
+ /**
+ * Name of current volume being scanned by the media scanner.
+ */
+ public static final String MEDIA_SCANNER_VOLUME = "volume";
+}
diff --git a/core/java/android/provider/OpenableColumns.java b/core/java/android/provider/OpenableColumns.java
new file mode 100644
index 0000000..f548bae
--- /dev/null
+++ b/core/java/android/provider/OpenableColumns.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.provider;
+
+/**
+ * These are standard columns for openable URIs. (See
+ * {@link android.content.Intent#CATEGORY_OPENABLE}.) If possible providers that have openable URIs
+ * should support these columns. To find the content type of a URI use
+ * {@link android.content.ContentResolver#getType(android.net.Uri)} as normal.
+ */
+public interface OpenableColumns {
+
+ /**
+ * The human-friendly name of file. If this is not provided then the name should default to the
+ * the last segment of the file's URI.
+ */
+ public static final String DISPLAY_NAME = "_display_name";
+
+ /**
+ * The number of bytes in the file identified by the openable URI. Null if unknown.
+ */
+ public static final String SIZE = "_size";
+}
diff --git a/core/java/android/provider/SearchRecentSuggestions.java b/core/java/android/provider/SearchRecentSuggestions.java
new file mode 100644
index 0000000..1439b26
--- /dev/null
+++ b/core/java/android/provider/SearchRecentSuggestions.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.provider;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.SearchRecentSuggestionsProvider;
+import android.database.Cursor;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.Log;
+
+/**
+ * This is a utility class providing access to
+ * {@link android.content.SearchRecentSuggestionsProvider}.
+ *
+ * <p>Unlike some utility classes, this one must be instantiated and properly initialized, so that
+ * it can be configured to operate with the search suggestions provider that you have created.
+ *
+ * <p>Typically, you will do this in your searchable activity, each time you receive an incoming
+ * {@link android.content.Intent#ACTION_SEARCH ACTION_SEARCH} Intent. The code to record each
+ * incoming query is as follows:
+ * <pre class="prettyprint">
+ * SearchSuggestions suggestions = new SearchSuggestions(this,
+ * MySuggestionsProvider.AUTHORITY, MySuggestionsProvider.MODE);
+ * suggestions.saveRecentQuery(queryString, null);
+ * </pre>
+ *
+ * <p>For a working example, see SearchSuggestionSampleProvider and SearchQueryResults in
+ * samples/ApiDemos/app.
+ */
+public class SearchRecentSuggestions {
+ // debugging support
+ private static final String LOG_TAG = "SearchSuggestions";
+ // DELETE ME (eventually)
+ private static final int DBG_SUGGESTION_TIMESTAMPS = 0;
+
+ // This is a superset of all possible column names (need not all be in table)
+ private static class SuggestionColumns implements BaseColumns {
+ public static final String DISPLAY1 = "display1";
+ public static final String DISPLAY2 = "display2";
+ public static final String QUERY = "query";
+ public static final String DATE = "date";
+ }
+
+ /* if you change column order you must also change indices below */
+ /**
+ * This is the database projection that can be used to view saved queries, when
+ * configured for one-line operation.
+ */
+ public static final String[] QUERIES_PROJECTION_1LINE = new String[] {
+ SuggestionColumns._ID,
+ SuggestionColumns.DATE,
+ SuggestionColumns.QUERY,
+ SuggestionColumns.DISPLAY1,
+ };
+ /* if you change column order you must also change indices below */
+ /**
+ * This is the database projection that can be used to view saved queries, when
+ * configured for two-line operation.
+ */
+ public static final String[] QUERIES_PROJECTION_2LINE = new String[] {
+ SuggestionColumns._ID,
+ SuggestionColumns.DATE,
+ SuggestionColumns.QUERY,
+ SuggestionColumns.DISPLAY1,
+ SuggestionColumns.DISPLAY2,
+ };
+
+ /* these indices depend on QUERIES_PROJECTION_xxx */
+ /** Index into the provided query projections. For use with Cursor.update methods. */
+ public static final int QUERIES_PROJECTION_DATE_INDEX = 1;
+ /** Index into the provided query projections. For use with Cursor.update methods. */
+ public static final int QUERIES_PROJECTION_QUERY_INDEX = 2;
+ /** Index into the provided query projections. For use with Cursor.update methods. */
+ public static final int QUERIES_PROJECTION_DISPLAY1_INDEX = 3;
+ /** Index into the provided query projections. For use with Cursor.update methods. */
+ public static final int QUERIES_PROJECTION_DISPLAY2_INDEX = 4; // only when 2line active
+
+ /* columns needed to determine whether to truncate history */
+ private static final String[] TRUNCATE_HISTORY_PROJECTION = new String[] {
+ SuggestionColumns._ID, SuggestionColumns.DATE
+ };
+
+ /*
+ * Set a cap on the count of items in the suggestions table, to
+ * prevent db and layout operations from dragging to a crawl. Revisit this
+ * cap when/if db/layout performance improvements are made.
+ */
+ private static final int MAX_HISTORY_COUNT = 250;
+
+ // client-provided configuration values
+ private Context mContext;
+ private String mAuthority;
+ private boolean mTwoLineDisplay;
+ private Uri mSuggestionsUri;
+ private String[] mQueriesProjection;
+
+ /**
+ * Although provider utility classes are typically static, this one must be constructed
+ * because it needs to be initialized using the same values that you provided in your
+ * {@link android.content.SearchRecentSuggestionsProvider}.
+ *
+ * @param authority This must match the authority that you've declared in your manifest.
+ * @param mode You can use mode flags here to determine certain functional aspects of your
+ * database. Note, this value should not change from run to run, because when it does change,
+ * your suggestions database may be wiped.
+ *
+ * @see android.content.SearchRecentSuggestionsProvider
+ * @see android.content.SearchRecentSuggestionsProvider#setupSuggestions
+ */
+ public SearchRecentSuggestions(Context context, String authority, int mode) {
+ if (TextUtils.isEmpty(authority) ||
+ ((mode & SearchRecentSuggestionsProvider.DATABASE_MODE_QUERIES) == 0)) {
+ throw new IllegalArgumentException();
+ }
+ // unpack mode flags
+ mTwoLineDisplay = (0 != (mode & SearchRecentSuggestionsProvider.DATABASE_MODE_2LINES));
+
+ // saved values
+ mContext = context;
+ mAuthority = new String(authority);
+
+ // derived values
+ mSuggestionsUri = Uri.parse("content://" + mAuthority + "/suggestions");
+
+ if (mTwoLineDisplay) {
+ mQueriesProjection = QUERIES_PROJECTION_2LINE;
+ } else {
+ mQueriesProjection = QUERIES_PROJECTION_1LINE;
+ }
+ }
+
+ /**
+ * Add a query to the recent queries list.
+ *
+ * @param queryString The string as typed by the user. This string will be displayed as
+ * the suggestion, and if the user clicks on the suggestion, this string will be sent to your
+ * searchable activity (as a new search query).
+ * @param line2 If you have configured your recent suggestions provider with
+ * {@link android.content.SearchRecentSuggestionsProvider#DATABASE_MODE_2LINES}, you can
+ * pass a second line of text here. It will be shown in a smaller font, below the primary
+ * suggestion. When typing, matches in either line of text will be displayed in the list.
+ * If you did not configure two-line mode, or if a given suggestion does not have any
+ * additional text to display, you can pass null here.
+ */
+ public void saveRecentQuery(String queryString, String line2) {
+ if (TextUtils.isEmpty(queryString)) {
+ return;
+ }
+ if (!mTwoLineDisplay && !TextUtils.isEmpty(line2)) {
+ throw new IllegalArgumentException();
+ }
+
+ ContentResolver cr = mContext.getContentResolver();
+ long now = System.currentTimeMillis();
+
+ // Use content resolver (not cursor) to insert/update this query
+ try {
+ ContentValues values = new ContentValues();
+ values.put(SuggestionColumns.DISPLAY1, queryString);
+ if (mTwoLineDisplay) {
+ values.put(SuggestionColumns.DISPLAY2, line2);
+ }
+ values.put(SuggestionColumns.QUERY, queryString);
+ values.put(SuggestionColumns.DATE, now);
+ cr.insert(mSuggestionsUri, values);
+ } catch (RuntimeException e) {
+ Log.e(LOG_TAG, "saveRecentQuery", e);
+ }
+
+ // Shorten the list (if it has become too long)
+ truncateHistory(cr, MAX_HISTORY_COUNT);
+ }
+
+ /**
+ * Completely delete the history. Use this call to implement a "clear history" UI.
+ */
+ public void clearHistory() {
+ ContentResolver cr = mContext.getContentResolver();
+ truncateHistory(cr, 0);
+ }
+
+ /**
+ * Reduces the length of the history table, to prevent it from growing too large.
+ *
+ * @param cr Convenience copy of the content resolver.
+ * @param maxEntries Max entries to leave in the table. 0 means remove all entries.
+ */
+ protected void truncateHistory(ContentResolver cr, int maxEntries) {
+ if (maxEntries < 0) {
+ throw new IllegalArgumentException();
+ }
+
+ try {
+ // null means "delete all". otherwise "delete but leave n newest"
+ String selection = null;
+ if (maxEntries > 0) {
+ selection = "_id IN " +
+ "(SELECT _id FROM suggestions" +
+ " ORDER BY " + SuggestionColumns.DATE + " DESC" +
+ " LIMIT -1 OFFSET " + String.valueOf(maxEntries) + ")";
+ }
+ cr.delete(mSuggestionsUri, selection, null);
+ } catch (RuntimeException e) {
+ Log.e(LOG_TAG, "truncateHistory", e);
+ }
+ }
+}
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
new file mode 100644
index 0000000..6897bd5
--- /dev/null
+++ b/core/java/android/provider/Settings.java
@@ -0,0 +1,2073 @@
+/*
+ * 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.provider;
+
+import com.google.android.collect.Maps;
+
+import org.apache.commons.codec.binary.Base64;
+
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.content.ContentQueryMap;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.net.Uri;
+import android.os.*;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.AndroidException;
+import android.util.Log;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.HashMap;
+
+
+/**
+ * The Settings provider contains global system-level device preferences.
+ */
+public final class Settings {
+
+ // Intent actions for Settings
+
+ /**
+ * Activity Action: Show system settings.
+ * <p>
+ * Input: Nothing.
+ * <p>
+ * Output: nothing.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_SETTINGS = "android.settings.SETTINGS";
+
+ /**
+ * Activity Action: Show settings to allow configuration of APNs.
+ * <p>
+ * Input: Nothing.
+ * <p>
+ * Output: nothing.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_APN_SETTINGS = "android.settings.APN_SETTINGS";
+
+ /**
+ * Activity Action: Show settings to allow configuration of current location
+ * sources.
+ * <p>
+ * In some cases, a matching Activity may not exist, so ensure you
+ * safeguard against this.
+ * <p>
+ * Input: Nothing.
+ * <p>
+ * Output: Nothing.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_LOCATION_SOURCE_SETTINGS =
+ "android.settings.LOCATION_SOURCE_SETTINGS";
+
+ /**
+ * Activity Action: Show settings to allow configuration of wireless controls
+ * such as Wi-Fi, Bluetooth and Mobile networks.
+ * <p>
+ * In some cases, a matching Activity may not exist, so ensure you
+ * safeguard against this.
+ * <p>
+ * Input: Nothing.
+ * <p>
+ * Output: Nothing.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_WIRELESS_SETTINGS =
+ "android.settings.WIRELESS_SETTINGS";
+
+ /**
+ * Activity Action: Show settings to allow configuration of security and
+ * location privacy.
+ * <p>
+ * In some cases, a matching Activity may not exist, so ensure you
+ * safeguard against this.
+ * <p>
+ * Input: Nothing.
+ * <p>
+ * Output: Nothing.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_SECURITY_SETTINGS =
+ "android.settings.SECURITY_SETTINGS";
+
+ /**
+ * Activity Action: Show settings to allow configuration of Wi-Fi.
+ * <p>
+ * In some cases, a matching Activity may not exist, so ensure you
+ * safeguard against this.
+ * <p>
+ * Input: Nothing.
+ * <p>
+ * Output: Nothing.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_WIFI_SETTINGS =
+ "android.settings.WIFI_SETTINGS";
+
+ /**
+ * Activity Action: Show settings to allow configuration of Bluetooth.
+ * <p>
+ * In some cases, a matching Activity may not exist, so ensure you
+ * safeguard against this.
+ * <p>
+ * Input: Nothing.
+ * <p>
+ * Output: Nothing.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_BLUETOOTH_SETTINGS =
+ "android.settings.BLUETOOTH_SETTINGS";
+
+ /**
+ * Activity Action: Show settings to allow configuration of date and time.
+ * <p>
+ * In some cases, a matching Activity may not exist, so ensure you
+ * safeguard against this.
+ * <p>
+ * Input: Nothing.
+ * <p>
+ * Output: Nothing.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_DATE_SETTINGS =
+ "android.settings.DATE_SETTINGS";
+
+ /**
+ * Activity Action: Show settings to allow configuration of sound and volume.
+ * <p>
+ * In some cases, a matching Activity may not exist, so ensure you
+ * safeguard against this.
+ * <p>
+ * Input: Nothing.
+ * <p>
+ * Output: Nothing.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_SOUND_SETTINGS =
+ "android.settings.SOUND_SETTINGS";
+
+ /**
+ * Activity Action: Show settings to allow configuration of display.
+ * <p>
+ * In some cases, a matching Activity may not exist, so ensure you
+ * safeguard against this.
+ * <p>
+ * Input: Nothing.
+ * <p>
+ * Output: Nothing.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_DISPLAY_SETTINGS =
+ "android.settings.DISPLAY_SETTINGS";
+
+ /**
+ * Activity Action: Show settings to allow configuration of locale.
+ * <p>
+ * In some cases, a matching Activity may not exist, so ensure you
+ * safeguard against this.
+ * <p>
+ * Input: Nothing.
+ * <p>
+ * Output: Nothing.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_LOCALE_SETTINGS =
+ "android.settings.LOCALE_SETTINGS";
+
+ /**
+ * Activity Action: Show settings to allow configuration of application-related settings.
+ * <p>
+ * In some cases, a matching Activity may not exist, so ensure you
+ * safeguard against this.
+ * <p>
+ * Input: Nothing.
+ * <p>
+ * Output: Nothing.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_APPLICATION_SETTINGS =
+ "android.settings.APPLICATION_SETTINGS";
+
+ /**
+ * Activity Action: Show settings to allow configuration of sync settings.
+ * <p>
+ * In some cases, a matching Activity may not exist, so ensure you
+ * safeguard against this.
+ * <p>
+ * Input: Nothing.
+ * <p>
+ * Output: Nothing.
+ *
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_SYNC_SETTINGS =
+ "android.settings.SYNC_SETTINGS";
+
+ // End of Intent actions for Settings
+
+ private static final String JID_RESOURCE_PREFIX = "android";
+
+ public static final String AUTHORITY = "settings";
+
+ private static final String TAG = "Settings";
+
+ private static String sJidResource = null;
+
+ public static class SettingNotFoundException extends AndroidException {
+ public SettingNotFoundException(String msg) {
+ super(msg);
+ }
+ }
+
+ /**
+ * Common base for tables of name/value settings.
+ *
+ *
+ */
+ public static class NameValueTable implements BaseColumns {
+ public static final String NAME = "name";
+ public static final String VALUE = "value";
+
+ protected static boolean putString(ContentResolver resolver, Uri uri,
+ String name, String value) {
+ // The database will take care of replacing duplicates.
+ try {
+ ContentValues values = new ContentValues();
+ values.put(NAME, name);
+ values.put(VALUE, value);
+ resolver.insert(uri, values);
+ return true;
+ } catch (SQLException e) {
+ Log.e(TAG, "Can't set key " + name + " in " + uri, e);
+ return false;
+ }
+ }
+
+ public static Uri getUriFor(Uri uri, String name) {
+ return Uri.withAppendedPath(uri, name);
+ }
+ }
+
+ private static class NameValueCache {
+ private final String mVersionSystemProperty;
+ private final HashMap<String, String> mValues = Maps.newHashMap();
+ private long mValuesVersion = 0;
+ private final Uri mUri;
+
+ NameValueCache(String versionSystemProperty, Uri uri) {
+ mVersionSystemProperty = versionSystemProperty;
+ mUri = uri;
+ }
+
+ String getString(ContentResolver cr, String name) {
+ long newValuesVersion = SystemProperties.getLong(mVersionSystemProperty, 0);
+ if (mValuesVersion != newValuesVersion) {
+ mValues.clear();
+ mValuesVersion = newValuesVersion;
+ }
+ if (!mValues.containsKey(name)) {
+ String value = null;
+ Cursor c = null;
+ try {
+ c = cr.query(mUri, new String[] { Settings.NameValueTable.VALUE },
+ Settings.NameValueTable.NAME + "=?", new String[]{name}, null);
+ if (c.moveToNext()) value = c.getString(0);
+ mValues.put(name, value);
+ } catch (SQLException e) {
+ // SQL error: return null, but don't cache it.
+ Log.e(TAG, "Can't get key " + name + " from " + mUri, e);
+ } finally {
+ if (c != null) c.close();
+ }
+ return value;
+ } else {
+ return mValues.get(name);
+ }
+ }
+ }
+
+ /**
+ * System settings, containing miscellaneous system preferences. This
+ * table holds simple name/value pairs. There are convenience
+ * functions for accessing individual settings entries.
+ */
+ public static final class System extends NameValueTable {
+ public static final String SYS_PROP_SETTING_VERSION = "sys.settings_system_version";
+
+ private static volatile NameValueCache mNameValueCache = null;
+
+ /**
+ * Look up a name in the database.
+ * @param resolver to access the database with
+ * @param name to look up in the table
+ * @return the corresponding value, or null if not present
+ */
+ public synchronized static String getString(ContentResolver resolver, String name) {
+ if (mNameValueCache == null) {
+ mNameValueCache = new NameValueCache(SYS_PROP_SETTING_VERSION, CONTENT_URI);
+ }
+ return mNameValueCache.getString(resolver, name);
+ }
+
+ /**
+ * Store a name/value pair into the database.
+ * @param resolver to access the database with
+ * @param name to store
+ * @param value to associate with the name
+ * @return true if the value was set, false on database errors
+ */
+ public static boolean putString(ContentResolver resolver,
+ String name, String value) {
+ return putString(resolver, CONTENT_URI, name, value);
+ }
+
+ /**
+ * Construct the content URI for a particular name/value pair,
+ * useful for monitoring changes with a ContentObserver.
+ * @param name to look up in the table
+ * @return the corresponding content URI, or null if not present
+ */
+ public static Uri getUriFor(String name) {
+ return getUriFor(CONTENT_URI, name);
+ }
+
+ /**
+ * Convenience function for retrieving a single system settings value
+ * as an integer. Note that internally setting values are always
+ * stored as strings; this function converts the string to an integer
+ * for you. The default value will be returned if the setting is
+ * not defined or not an integer.
+ *
+ * @param cr The ContentResolver to access.
+ * @param name The name of the setting to retrieve.
+ * @param def Value to return if the setting is not defined.
+ *
+ * @return The setting's current value, or 'def' if it is not defined
+ * or not a valid integer.
+ */
+ public static int getInt(ContentResolver cr, String name, int def) {
+ String v = getString(cr, name);
+ try {
+ return v != null ? Integer.parseInt(v) : def;
+ } catch (NumberFormatException e) {
+ return def;
+ }
+ }
+
+ /**
+ * Convenience function for retrieving a single system settings value
+ * as an integer. Note that internally setting values are always
+ * stored as strings; this function converts the string to an integer
+ * for you.
+ * <p>
+ * This version does not take a default value. If the setting has not
+ * been set, or the string value is not a number,
+ * it throws {@link SettingNotFoundException}.
+ *
+ * @param cr The ContentResolver to access.
+ * @param name The name of the setting to retrieve.
+ *
+ * @throws SettingNotFoundException Thrown if a setting by the given
+ * name can't be found or the setting value is not an integer.
+ *
+ * @return The setting's current value.
+ */
+ public static int getInt(ContentResolver cr, String name)
+ throws SettingNotFoundException {
+ String v = getString(cr, name);
+ try {
+ return Integer.parseInt(v);
+ } catch (NumberFormatException e) {
+ throw new SettingNotFoundException(name);
+ }
+ }
+
+ /**
+ * Convenience function for updating a single settings value as an
+ * integer. This will either create a new entry in the table if the
+ * given name does not exist, or modify the value of the existing row
+ * with that name. Note that internally setting values are always
+ * stored as strings, so this function converts the given value to a
+ * string before storing it.
+ *
+ * @param cr The ContentResolver to access.
+ * @param name The name of the setting to modify.
+ * @param value The new value for the setting.
+ * @return true if the value was set, false on database errors
+ */
+ public static boolean putInt(ContentResolver cr, String name, int value) {
+ return putString(cr, name, Integer.toString(value));
+ }
+
+ /**
+ * Convenience function for retrieving a single system settings value
+ * as a floating point number. Note that internally setting values are
+ * always stored as strings; this function converts the string to an
+ * float for you. The default value will be returned if the setting
+ * is not defined or not a valid float.
+ *
+ * @param cr The ContentResolver to access.
+ * @param name The name of the setting to retrieve.
+ * @param def Value to return if the setting is not defined.
+ *
+ * @return The setting's current value, or 'def' if it is not defined
+ * or not a valid float.
+ */
+ public static float getFloat(ContentResolver cr, String name, float def) {
+ String v = getString(cr, name);
+ try {
+ return v != null ? Float.parseFloat(v) : def;
+ } catch (NumberFormatException e) {
+ return def;
+ }
+ }
+
+ /**
+ * Convenience function for retrieving a single system settings value
+ * as a float. Note that internally setting values are always
+ * stored as strings; this function converts the string to a float
+ * for you.
+ * <p>
+ * This version does not take a default value. If the setting has not
+ * been set, or the string value is not a number,
+ * it throws {@link SettingNotFoundException}.
+ *
+ * @param cr The ContentResolver to access.
+ * @param name The name of the setting to retrieve.
+ *
+ * @throws SettingNotFoundException Thrown if a setting by the given
+ * name can't be found or the setting value is not a float.
+ *
+ * @return The setting's current value.
+ */
+ public static float getFloat(ContentResolver cr, String name)
+ throws SettingNotFoundException {
+ String v = getString(cr, name);
+ try {
+ return Float.parseFloat(v);
+ } catch (NumberFormatException e) {
+ throw new SettingNotFoundException(name);
+ }
+ }
+
+ /**
+ * Convenience function for updating a single settings value as a
+ * floating point number. This will either create a new entry in the
+ * table if the given name does not exist, or modify the value of the
+ * existing row with that name. Note that internally setting values
+ * are always stored as strings, so this function converts the given
+ * value to a string before storing it.
+ *
+ * @param cr The ContentResolver to access.
+ * @param name The name of the setting to modify.
+ * @param value The new value for the setting.
+ * @return true if the value was set, false on database errors
+ */
+ public static boolean putFloat(ContentResolver cr, String name, float value) {
+ return putString(cr, name, Float.toString(value));
+ }
+
+ /**
+ * Convenience function to read all of the current
+ * configuration-related settings into a
+ * {@link Configuration} object.
+ *
+ * @param cr The ContentResolver to access.
+ * @param outConfig Where to place the configuration settings.
+ */
+ public static void getConfiguration(ContentResolver cr, Configuration outConfig) {
+ outConfig.fontScale = Settings.System.getFloat(
+ cr, FONT_SCALE, outConfig.fontScale);
+ if (outConfig.fontScale < 0) {
+ outConfig.fontScale = 1;
+ }
+ }
+
+ /**
+ * Convenience function to write a batch of configuration-related
+ * settings from a {@link Configuration} object.
+ *
+ * @param cr The ContentResolver to access.
+ * @param config The settings to write.
+ * @return true if the values were set, false on database errors
+ */
+ public static boolean putConfiguration(ContentResolver cr, Configuration config) {
+ return Settings.System.putFloat(cr, FONT_SCALE, config.fontScale);
+ }
+
+ public static boolean getShowGTalkServiceStatus(ContentResolver cr) {
+ return getInt(cr, SHOW_GTALK_SERVICE_STATUS, 0) != 0;
+ }
+
+ public static void setShowGTalkServiceStatus(ContentResolver cr, boolean flag) {
+ putInt(cr, SHOW_GTALK_SERVICE_STATUS, flag ? 1 : 0);
+ }
+
+ /**
+ * The content:// style URL for this table
+ */
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://" + AUTHORITY + "/system");
+
+ /**
+ * Whether we keep the device on while the device is plugged in.
+ * 0=no 1=yes
+ */
+ public static final String STAY_ON_WHILE_PLUGGED_IN = "stay_on_while_plugged_in";
+
+ /**
+ * What happens when the user presses the end call button if they're not
+ * on a call.<br/>
+ * <b>Values:</b><br/>
+ * 0 - The end button does nothing.<br/>
+ * 1 - The end button goes to the home screen.<br/>
+ * 2 - The end button puts the device to sleep and locks the keyguard.<br/>
+ * 3 - The end button goes to the home screen. If the user is already on the
+ * home screen, it puts the device to sleep.
+ */
+ public static final String END_BUTTON_BEHAVIOR = "end_button_behavior";
+
+ /**
+ * Whether Airplane Mode is on.
+ */
+ public static final String AIRPLANE_MODE_ON = "airplane_mode_on";
+
+ /**
+ * Constant for use in AIRPLANE_MODE_RADIOS to specify Bluetooth radio.
+ */
+ public static final String RADIO_BLUETOOTH = "bluetooth";
+
+ /**
+ * Constant for use in AIRPLANE_MODE_RADIOS to specify Wi-Fi radio.
+ */
+ public static final String RADIO_WIFI = "wifi";
+
+ /**
+ * Constant for use in AIRPLANE_MODE_RADIOS to specify Cellular radio.
+ */
+ public static final String RADIO_CELL = "cell";
+
+ /**
+ * A comma separated list of radios that need to be disabled when airplane mode
+ * is on. This overrides WIFI_ON and BLUETOOTH_ON, if Wi-Fi and bluetooth are
+ * included in the comma separated list.
+ */
+ public static final String AIRPLANE_MODE_RADIOS = "airplane_mode_radios";
+
+ /**
+ * Whether the Wi-Fi should be on. Only the Wi-Fi service should touch this.
+ */
+ public static final String WIFI_ON = "wifi_on";
+
+ /**
+ * Whether to notify the user of open networks.
+ * <p>
+ * If not connected and the scan results have an open network, we will
+ * put this notification up. If we attempt to connect to a network or
+ * the open network(s) disappear, we remove the notification. When we
+ * show the notification, we will not show it again for
+ * {@link #WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY} time.
+ */
+ public static final String WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON =
+ "wifi_networks_available_notification_on";
+
+ /**
+ * Delay (in seconds) before repeating the Wi-Fi networks available notification.
+ * Connecting to a network will reset the timer.
+ */
+ public static final String WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY =
+ "wifi_networks_available_repeat_delay";
+
+ /**
+ * When the number of open networks exceeds this number, the
+ * least-recently-used excess networks will be removed.
+ */
+ public static final String WIFI_NUM_OPEN_NETWORKS_KEPT = "wifi_num_open_networks_kept";
+
+ /**
+ * Whether the Wi-Fi watchdog is enabled.
+ */
+ public static final String WIFI_WATCHDOG_ON = "wifi_watchdog_on";
+
+ /**
+ * The number of access points required for a network in order for the
+ * watchdog to monitor it.
+ */
+ public static final String WIFI_WATCHDOG_AP_COUNT = "wifi_watchdog_ap_count";
+
+ /**
+ * The number of initial pings to perform that *may* be ignored if they
+ * fail. Again, if these fail, they will *not* be used in packet loss
+ * calculation. For example, one network always seemed to time out for
+ * the first couple pings, so this is set to 3 by default.
+ */
+ public static final String WIFI_WATCHDOG_INITIAL_IGNORED_PING_COUNT = "wifi_watchdog_initial_ignored_ping_count";
+
+ /**
+ * The number of pings to test if an access point is a good connection.
+ */
+ public static final String WIFI_WATCHDOG_PING_COUNT = "wifi_watchdog_ping_count";
+
+ /**
+ * The timeout per ping.
+ */
+ public static final String WIFI_WATCHDOG_PING_TIMEOUT_MS = "wifi_watchdog_ping_timeout_ms";
+
+ /**
+ * The delay between pings.
+ */
+ public static final String WIFI_WATCHDOG_PING_DELAY_MS = "wifi_watchdog_ping_delay_ms";
+
+ /**
+ * The acceptable packet loss percentage (range 0 - 100) before trying
+ * another AP on the same network.
+ */
+ public static final String WIFI_WATCHDOG_ACCEPTABLE_PACKET_LOSS_PERCENTAGE =
+ "wifi_watchdog_acceptable_packet_loss_percentage";
+
+ /**
+ * The maximum number of access points (per network) to attempt to test.
+ * If this number is reached, the watchdog will no longer monitor the
+ * initial connection state for the network. This is a safeguard for
+ * networks containing multiple APs whose DNS does not respond to pings.
+ */
+ public static final String WIFI_WATCHDOG_MAX_AP_CHECKS = "wifi_watchdog_max_ap_checks";
+
+ /**
+ * Whether the Wi-Fi watchdog is enabled for background checking even
+ * after it thinks the user has connected to a good access point.
+ */
+ public static final String WIFI_WATCHDOG_BACKGROUND_CHECK_ENABLED =
+ "wifi_watchdog_background_check_enabled";
+
+ /**
+ * The timeout for a background ping
+ */
+ public static final String WIFI_WATCHDOG_BACKGROUND_CHECK_TIMEOUT_MS =
+ "wifi_watchdog_background_check_timeout_ms";
+
+ /**
+ * The delay between background checks.
+ */
+ public static final String WIFI_WATCHDOG_BACKGROUND_CHECK_DELAY_MS =
+ "wifi_watchdog_background_check_delay_ms";
+
+ /**
+ * Whether to use static IP and other static network attributes.
+ * <p>
+ * Set to 1 for true and 0 for false.
+ */
+ public static final String WIFI_USE_STATIC_IP = "wifi_use_static_ip";
+
+ /**
+ * The static IP address.
+ * <p>
+ * Example: "192.168.1.51"
+ */
+ public static final String WIFI_STATIC_IP = "wifi_static_ip";
+
+ /**
+ * If using static IP, the gateway's IP address.
+ * <p>
+ * Example: "192.168.1.1"
+ */
+ public static final String WIFI_STATIC_GATEWAY = "wifi_static_gateway";
+
+ /**
+ * If using static IP, the net mask.
+ * <p>
+ * Example: "255.255.255.0"
+ */
+ public static final String WIFI_STATIC_NETMASK = "wifi_static_netmask";
+
+ /**
+ * If using static IP, the primary DNS's IP address.
+ * <p>
+ * Example: "192.168.1.1"
+ */
+ public static final String WIFI_STATIC_DNS1 = "wifi_static_dns1";
+
+ /**
+ * If using static IP, the secondary DNS's IP address.
+ * <p>
+ * Example: "192.168.1.2"
+ */
+ public static final String WIFI_STATIC_DNS2 = "wifi_static_dns2";
+
+ /**
+ * User preference for which network(s) should be used. Only the
+ * connectivity service should touch this.
+ */
+ public static final String NETWORK_PREFERENCE = "network_preference";
+
+ /**
+ * Whether bluetooth is enabled/disabled
+ * 0=disabled. 1=enabled.
+ */
+ public static final String BLUETOOTH_ON = "bluetooth_on";
+
+ /**
+ * Determines whether remote devices may discover and/or connect to
+ * this device.
+ * <P>Type: INT</P>
+ * 2 -- discoverable and connectable
+ * 1 -- connectable but not discoverable
+ * 0 -- neither connectable nor discoverable
+ */
+ public static final String BLUETOOTH_DISCOVERABILITY =
+ "bluetooth_discoverability";
+
+ /**
+ * Bluetooth discoverability timeout. If this value is nonzero, then
+ * Bluetooth becomes discoverable for a certain number of seconds,
+ * after which is becomes simply connectable. The value is in seconds.
+ */
+ public static final String BLUETOOTH_DISCOVERABILITY_TIMEOUT =
+ "bluetooth_discoverability_timeout";
+
+ /**
+ * Whether autolock is enabled (0 = false, 1 = true)
+ */
+ public static final String LOCK_PATTERN_ENABLED = "lock_pattern_autolock";
+
+ /**
+ * Whether the device has been provisioned (0 = false, 1 = true)
+ */
+ public static final String DEVICE_PROVISIONED = "device_provisioned";
+
+ /**
+ * Whether lock pattern is visible as user enters (0 = false, 1 = true)
+ */
+ public static final String LOCK_PATTERN_VISIBLE = "lock_pattern_visible_pattern";
+
+
+ /**
+ * A formatted string of the next alarm that is set, or the empty string
+ * if there is no alarm set.
+ */
+ public static final String NEXT_ALARM_FORMATTED = "next_alarm_formatted";
+
+ /**
+ * Comma-separated list of location providers that activities may access.
+ */
+ public static final String LOCATION_PROVIDERS_ALLOWED = "location_providers_allowed";
+
+ /**
+ * Whether or not data roaming is enabled. (0 = false, 1 = true)
+ */
+ public static final String DATA_ROAMING = "data_roaming";
+
+ /**
+ * Scaling factor for fonts, float.
+ */
+ public static final String FONT_SCALE = "font_scale";
+
+ /**
+ * Name of an application package to be debugged.
+ */
+ public static final String DEBUG_APP = "debug_app";
+
+ /**
+ * If 1, when launching DEBUG_APP it will wait for the debugger before
+ * starting user code. If 0, it will run normally.
+ */
+ public static final String WAIT_FOR_DEBUGGER = "wait_for_debugger";
+
+ /**
+ * Whether or not to dim the screen. 0=no 1=yes
+ */
+ public static final String DIM_SCREEN = "dim_screen";
+
+ /**
+ * The timeout before the screen turns off.
+ */
+ public static final String SCREEN_OFF_TIMEOUT = "screen_off_timeout";
+
+ /**
+ * The screen backlight brightness between 0 and 255.
+ */
+ public static final String SCREEN_BRIGHTNESS = "screen_brightness";
+
+ /**
+ * Control whether the process CPU usage meter should be shown.
+ */
+ public static final String SHOW_PROCESSES = "show_processes";
+
+ /**
+ * If 1, the activity manager will aggressively finish activities and
+ * processes as soon as they are no longer needed. If 0, the normal
+ * extended lifetime is used.
+ */
+ public static final String ALWAYS_FINISH_ACTIVITIES =
+ "always_finish_activities";
+
+
+ /**
+ * Ringer mode. This is used internally, changing this value will not
+ * change the ringer mode. See AudioManager.
+ */
+ public static final String MODE_RINGER = "mode_ringer";
+
+ /**
+ * Determines which streams are affected by ringer mode changes. The
+ * stream type's bit should be set to 1 if it should be muted when going
+ * into an inaudible ringer mode.
+ */
+ public static final String MODE_RINGER_STREAMS_AFFECTED = "mode_ringer_streams_affected";
+
+ /**
+ * Determines which streams are affected by mute. The
+ * stream type's bit should be set to 1 if it should be muted when a mute request
+ * is received.
+ */
+ public static final String MUTE_STREAMS_AFFECTED = "mute_streams_affected";
+
+ /**
+ * Whether vibrate is on for different events. This is used internally,
+ * changing this value will not change the vibrate. See AudioManager.
+ */
+ public static final String VIBRATE_ON = "vibrate_on";
+
+ /**
+ * Ringer volume. This is used internally, changing this value will not
+ * change the volume. See AudioManager.
+ */
+ public static final String VOLUME_RING = "volume_ring";
+
+ /**
+ * System/notifications volume. This is used internally, changing this
+ * value will not change the volume. See AudioManager.
+ */
+ public static final String VOLUME_SYSTEM = "volume_system";
+
+ /**
+ * Voice call volume. This is used internally, changing this value will
+ * not change the volume. See AudioManager.
+ */
+ public static final String VOLUME_VOICE = "volume_voice";
+
+ /**
+ * Music/media/gaming volume. This is used internally, changing this
+ * value will not change the volume. See AudioManager.
+ */
+ public static final String VOLUME_MUSIC = "volume_music";
+
+ /**
+ * Alarm volume. This is used internally, changing this
+ * value will not change the volume. See AudioManager.
+ */
+ public static final String VOLUME_ALARM = "volume_alarm";
+
+ /**
+ * The mapping of stream type (integer) to its setting.
+ */
+ public static final String[] VOLUME_SETTINGS = {
+ VOLUME_VOICE, VOLUME_SYSTEM, VOLUME_RING, VOLUME_MUSIC, VOLUME_ALARM
+ };
+
+ /**
+ * Appended to various volume related settings to record the previous
+ * values before they the settings were affected by a silent/vibrate
+ * ringer mode change.
+ */
+ public static final String APPEND_FOR_LAST_AUDIBLE = "_last_audible";
+
+ /**
+ * Persistent store for the system-wide default ringtone URI.
+ * <p>
+ * If you need to play the default ringtone at any given time, it is recommended
+ * you give {@link #DEFAULT_RINGTONE_URI} to the media player. It will resolve
+ * to the set default ringtone at the time of playing.
+ *
+ * @see #DEFAULT_RINGTONE_URI
+ */
+ public static final String RINGTONE = "ringtone";
+
+ /**
+ * A {@link Uri} that will point to the current default ringtone at any
+ * given time.
+ * <p>
+ * If the current default ringtone is in the DRM provider and the caller
+ * does not have permission, the exception will be a
+ * FileNotFoundException.
+ */
+ public static final Uri DEFAULT_RINGTONE_URI = getUriFor(RINGTONE);
+
+ /**
+ * Persistent store for the system-wide default notification sound.
+ *
+ * @see #RINGTONE
+ * @see #DEFAULT_NOTIFICATION_URI
+ */
+ public static final String NOTIFICATION_SOUND = "notification_sound";
+
+ /**
+ * A {@link Uri} that will point to the current default notification
+ * sound at any given time.
+ *
+ * @see #DEFAULT_RINGTONE_URI
+ */
+ public static final Uri DEFAULT_NOTIFICATION_URI = getUriFor(NOTIFICATION_SOUND);
+
+ /**
+ * Setting to enable Auto Replace (AutoText) in text editors. 1 = On, 0 = Off
+ */
+ public static final String TEXT_AUTO_REPLACE = "auto_replace";
+
+ /**
+ * Setting to enable Auto Caps in text editors. 1 = On, 0 = Off
+ */
+ public static final String TEXT_AUTO_CAPS = "auto_caps";
+
+ /**
+ * Setting to enable Auto Punctuate in text editors. 1 = On, 0 = Off. This
+ * feature converts two spaces to a "." and space.
+ */
+ public static final String TEXT_AUTO_PUNCTUATE = "auto_punctuate";
+
+ /**
+ * Setting to showing password characters in text editors. 1 = On, 0 = Off
+ */
+ public static final String TEXT_SHOW_PASSWORD = "show_password";
+ /**
+ * USB Mass Storage Enabled
+ */
+ public static final String USB_MASS_STORAGE_ENABLED =
+ "usb_mass_storage_enabled";
+
+ public static final String SHOW_GTALK_SERVICE_STATUS =
+ "SHOW_GTALK_SERVICE_STATUS";
+
+ /**
+ * Name of activity to use for wallpaper on the home screen.
+ */
+ public static final String WALLPAPER_ACTIVITY = "wallpaper_activity";
+
+ /**
+ * Host name and port for a user-selected proxy.
+ */
+ public static final String HTTP_PROXY = "http_proxy";
+
+ /**
+ * Value to specify if the user prefers the date, time and time zone
+ * to be automatically fetched from the network (NITZ). 1=yes, 0=no
+ */
+ public static final String AUTO_TIME = "auto_time";
+
+ /**
+ * Display times as 12 or 24 hours
+ * 12
+ * 24
+ */
+ public static final String TIME_12_24 = "time_12_24";
+
+ /**
+ * Date format string
+ * mm/dd/yyyy
+ * dd/mm/yyyy
+ * yyyy/mm/dd
+ */
+ public static final String DATE_FORMAT = "date_format";
+
+ /**
+ * Settings classname to launch when Settings is clicked from All
+ * Applications. Needed because of user testing between the old
+ * and new Settings apps. TODO: 881807
+ */
+ public static final String SETTINGS_CLASSNAME = "settings_classname";
+
+ /**
+ * Whether the setup wizard has been run before (on first boot), or if
+ * it still needs to be run.
+ *
+ * nonzero = it has been run in the past
+ * 0 = it has not been run in the past
+ */
+ public static final String SETUP_WIZARD_HAS_RUN = "setup_wizard_has_run";
+
+ /**
+ * The Android ID (a unique 64-bit value) as a hex string.
+ * Identical to that obtained by calling
+ * GoogleLoginService.getAndroidId(); it is also placed here
+ * so you can get it without binding to a service.
+ */
+ public static final String ANDROID_ID = "android_id";
+
+ /**
+ * The Logging ID (a unique 64-bit value) as a hex string.
+ * Used as a pseudonymous identifier for logging.
+ */
+ public static final String LOGGING_ID = "logging_id";
+
+ /**
+ * If this setting is set (to anything), then all references
+ * to Gmail on the device must change to Google Mail.
+ */
+ public static final String USE_GOOGLE_MAIL = "use_google_mail";
+
+ /**
+ * Whether the package installer should allow installation of apps downloaded from
+ * sources other than the Android Market (vending machine).
+ *
+ * 1 = allow installing from other sources
+ * 0 = only allow installing from the Android Market
+ */
+ public static final String INSTALL_NON_MARKET_APPS = "install_non_market_apps";
+
+ /**
+ * Scaling factor for normal window animations. Setting to 0 will disable window
+ * animations.
+ */
+ public static final String WINDOW_ANIMATION_SCALE = "window_animation_scale";
+
+ /**
+ * Scaling factor for activity transition animations. Setting to 0 will disable window
+ * animations.
+ */
+ public static final String TRANSITION_ANIMATION_SCALE = "transition_animation_scale";
+
+ public static final String PARENTAL_CONTROL_ENABLED =
+ "parental_control_enabled";
+
+ public static final String PARENTAL_CONTROL_REDIRECT_URL =
+ "parental_control_redirect_url";
+
+ public static final String PARENTAL_CONTROL_LAST_UPDATE =
+ "parental_control_last_update";
+
+ /**
+ * Whether ADB is enabled.
+ */
+ public static final String ADB_ENABLED = "adb_enabled";
+
+ /**
+ * Whether the audible DTMF tones are played by the dialer when dialing. The value is
+ * boolean (1 or 0).
+ */
+ public static final String DTMF_TONE_WHEN_DIALING = "dtmf_tone";
+
+ /**
+ * Whether the sounds effects (key clicks, lid open ...) are enabled. The value is
+ * boolean (1 or 0).
+ */
+ public static final String SOUND_EFFECTS_ENABLED = "sound_effects_enabled";
+ }
+
+
+ /**
+ * Gservices settings, containing the network names for Google's
+ * various services. This table holds simple name/addr pairs.
+ * Addresses can be accessed through the getString() method.
+ * @hide
+ */
+ public static final class Gservices extends NameValueTable {
+ public static final String SYS_PROP_SETTING_VERSION = "sys.settings_gservices_version";
+
+ private static volatile NameValueCache mNameValueCache = null;
+ private static final Object mNameValueCacheLock = new Object();
+
+ /**
+ * Look up a name in the database.
+ * @param resolver to access the database with
+ * @param name to look up in the table
+ * @return the corresponding value, or null if not present
+ */
+ public static String getString(ContentResolver resolver, String name) {
+ synchronized (mNameValueCacheLock) {
+ if (mNameValueCache == null) {
+ mNameValueCache = new NameValueCache(SYS_PROP_SETTING_VERSION, CONTENT_URI);
+ }
+ return mNameValueCache.getString(resolver, name);
+ }
+ }
+
+ /**
+ * Store a name/value pair into the database.
+ * @param resolver to access the database with
+ * @param name to store
+ * @param value to associate with the name
+ * @return true if the value was set, false on database errors
+ */
+ public static boolean putString(ContentResolver resolver,
+ String name, String value) {
+ return putString(resolver, CONTENT_URI, name, value);
+ }
+
+ /**
+ * Look up the value for name in the database, convert it to an int using Integer.parseInt
+ * and return it. If it is null or if a NumberFormatException is caught during the
+ * conversion then return defValue.
+ */
+ public static int getInt(ContentResolver resolver, String name, int defValue) {
+ String valString = getString(resolver, name);
+ int value;
+ try {
+ value = valString != null ? Integer.parseInt(valString) : defValue;
+ } catch (NumberFormatException e) {
+ value = defValue;
+ }
+ return value;
+ }
+
+ /**
+ * Look up the value for name in the database, convert it to a long using Long.parseLong
+ * and return it. If it is null or if a NumberFormatException is caught during the
+ * conversion then return defValue.
+ */
+ public static long getLong(ContentResolver resolver, String name, long defValue) {
+ String valString = getString(resolver, name);
+ long value;
+ try {
+ value = valString != null ? Long.parseLong(valString) : defValue;
+ } catch (NumberFormatException e) {
+ value = defValue;
+ }
+ return value;
+ }
+
+ /**
+ * Construct the content URI for a particular name/value pair,
+ * useful for monitoring changes with a ContentObserver.
+ * @param name to look up in the table
+ * @return the corresponding content URI, or null if not present
+ */
+ public static Uri getUriFor(String name) {
+ return getUriFor(CONTENT_URI, name);
+ }
+
+ /**
+ * The content:// style URL for this table
+ */
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://" + AUTHORITY + "/gservices");
+
+ /**
+ * MMS - URL to use for HTTP "x-wap-profile" header
+ */
+ public static final String MMS_X_WAP_PROFILE_URL
+ = "mms_x_wap_profile_url";
+
+ /**
+ * YouTube - "most viewed" url
+ */
+ public static final String YOUTUBE_MOST_VIEWED_URL
+ = "youtube_most_viewed_url";
+
+ /**
+ * YouTube - "most recent" url
+ */
+ public static final String YOUTUBE_MOST_RECENT_URL
+ = "youtube_most_recent_url";
+
+ /**
+ * YouTube - "top favorites" url
+ */
+ public static final String YOUTUBE_TOP_FAVORITES_URL
+ = "youtube_top_favorites_url";
+
+ /**
+ * YouTube - "most discussed" url
+ */
+ public static final String YOUTUBE_MOST_DISCUSSED_URL
+ = "youtube_most_discussed_url";
+
+ /**
+ * YouTube - "most responded" url
+ */
+ public static final String YOUTUBE_MOST_RESPONDED_URL
+ = "youtube_most_responded_url";
+
+ /**
+ * YouTube - "most linked" url
+ */
+ public static final String YOUTUBE_MOST_LINKED_URL
+ = "youtube_most_linked_url";
+
+ /**
+ * YouTube - "top rated" url
+ */
+ public static final String YOUTUBE_TOP_RATED_URL
+ = "youtube_top_rated_url";
+
+ /**
+ * YouTube - "recently featured" url
+ */
+ public static final String YOUTUBE_RECENTLY_FEATURED_URL
+ = "youtube_recently_featured_url";
+
+ /**
+ * YouTube - my uploaded videos
+ */
+ public static final String YOUTUBE_MY_VIDEOS_URL
+ = "youtube_my_videos_url";
+
+ /**
+ * YouTube - "my favorite" videos url
+ */
+ public static final String YOUTUBE_MY_FAVORITES_URL
+ = "youtube_my_favorites_url";
+
+ /**
+ * YouTube - "by author" videos url -- used for My videos
+ */
+ public static final String YOUTUBE_BY_AUTHOR_URL
+ = "youtube_by_author_url";
+
+ /**
+ * YouTube - save a video to favorite videos url
+ */
+ public static final String YOUTUBE_SAVE_TO_FAVORITES_URL
+ = "youtube_save_to_favorites_url";
+
+ /**
+ * YouTube - "mobile" videos url
+ */
+ public static final String YOUTUBE_MOBILE_VIDEOS_URL
+ = "youtube_mobile_videos_url";
+
+ /**
+ * YouTube - search videos url
+ */
+ public static final String YOUTUBE_SEARCH_URL
+ = "youtube_search_url";
+
+ /**
+ * YouTube - category search videos url
+ */
+ public static final String YOUTUBE_CATEGORY_SEARCH_URL
+ = "youtube_category_search_url";
+
+ /**
+ * YouTube - url to get the list of categories
+ */
+ public static final String YOUTUBE_CATEGORY_LIST_URL
+ = "youtube_category_list_url";
+
+ /**
+ * YouTube - related videos url
+ */
+ public static final String YOUTUBE_RELATED_VIDEOS_URL
+ = "youtube_related_videos_url";
+
+ /**
+ * YouTube - individual video url
+ */
+ public static final String YOUTUBE_INDIVIDUAL_VIDEO_URL
+ = "youtube_individual_video_url";
+
+ /**
+ * YouTube - user's playlist url
+ */
+ public static final String YOUTUBE_MY_PLAYLISTS_URL
+ = "youtube_my_playlists_url";
+
+ /**
+ * YouTube - user's subscriptions url
+ */
+ public static final String YOUTUBE_MY_SUBSCRIPTIONS_URL
+ = "youtube_my_subscriptions_url";
+
+ /**
+ * YouTube - the url we use to contact YouTube to get a device id
+ */
+ public static final String YOUTUBE_REGISTER_DEVICE_URL
+ = "youtube_register_device_url";
+
+ /**
+ * YouTube - the flag to indicate whether to use proxy
+ */
+ public static final String YOUTUBE_USE_PROXY
+ = "youtube_use_proxy";
+
+ /**
+ * Event tags from the kernel event log to upload during checkin.
+ */
+ public static final String CHECKIN_EVENTS = "checkin_events";
+
+ /**
+ * The interval (in seconds) between periodic checkin attempts.
+ */
+ public static final String CHECKIN_INTERVAL = "checkin_interval";
+
+ /**
+ * How frequently (in seconds) to check the memory status of the
+ * device.
+ */
+ public static final String MEMCHECK_INTERVAL = "memcheck_interval";
+
+ /**
+ * Max frequency (in seconds) to log memory check stats, in realtime
+ * seconds. This allows for throttling of logs when the device is
+ * running for large amounts of time.
+ */
+ public static final String MEMCHECK_LOG_REALTIME_INTERVAL = "memcheck_log_realtime_interval";
+
+ /**
+ * Boolean indicating whether rebooting due to system memory checks
+ * is enabled.
+ */
+ public static final String MEMCHECK_SYSTEM_ENABLED = "memcheck_system_enabled";
+
+ /**
+ * How many bytes the system process must be below to avoid scheduling
+ * a soft reboot. This reboot will happen when it is next determined
+ * to be a good time.
+ */
+ public static final String MEMCHECK_SYSTEM_SOFT_THRESHOLD = "memcheck_system_soft";
+
+ /**
+ * How many bytes the system process must be below to avoid scheduling
+ * a hard reboot. This reboot will happen immediately.
+ */
+ public static final String MEMCHECK_SYSTEM_HARD_THRESHOLD = "memcheck_system_hard";
+
+ /**
+ * How many bytes the phone process must be below to avoid scheduling
+ * a soft restart. This restart will happen when it is next determined
+ * to be a good time.
+ */
+ public static final String MEMCHECK_PHONE_SOFT_THRESHOLD = "memcheck_phone_soft";
+
+ /**
+ * How many bytes the phone process must be below to avoid scheduling
+ * a hard restart. This restart will happen immediately.
+ */
+ public static final String MEMCHECK_PHONE_HARD_THRESHOLD = "memcheck_phone_hard";
+
+ /**
+ * Boolean indicating whether restarting the phone process due to
+ * memory checks is enabled.
+ */
+ public static final String MEMCHECK_PHONE_ENABLED = "memcheck_phone_enabled";
+
+ /**
+ * First time during the day it is okay to kill processes
+ * or reboot the device due to low memory situations. This number is
+ * in seconds since midnight.
+ */
+ public static final String MEMCHECK_EXEC_START_TIME = "memcheck_exec_start_time";
+
+ /**
+ * Last time during the day it is okay to kill processes
+ * or reboot the device due to low memory situations. This number is
+ * in seconds since midnight.
+ */
+ public static final String MEMCHECK_EXEC_END_TIME = "memcheck_exec_end_time";
+
+ /**
+ * How long the screen must have been off in order to kill processes
+ * or reboot. This number is in seconds. A value of -1 means to
+ * entirely disregard whether the screen is on.
+ */
+ public static final String MEMCHECK_MIN_SCREEN_OFF = "memcheck_min_screen_off";
+
+ /**
+ * How much time there must be until the next alarm in order to kill processes
+ * or reboot. This number is in seconds. Note: this value must be
+ * smaller than {@link #MEMCHECK_RECHECK_INTERVAL} or else it will
+ * always see an alarm scheduled within its time.
+ */
+ public static final String MEMCHECK_MIN_ALARM = "memcheck_min_alarm";
+
+ /**
+ * How frequently to check whether it is a good time to restart things,
+ * if the device is in a bad state. This number is in seconds. Note:
+ * this value must be larger than {@link #MEMCHECK_MIN_ALARM} or else
+ * the alarm to schedule the recheck will always appear within the
+ * minimum "do not execute now" time.
+ */
+ public static final String MEMCHECK_RECHECK_INTERVAL = "memcheck_recheck_interval";
+
+ /**
+ * How frequently (in DAYS) to reboot the device. If 0, no reboots
+ * will occur.
+ */
+ public static final String REBOOT_INTERVAL = "reboot_interval";
+
+ /**
+ * First time during the day it is okay to force a reboot of the
+ * device (if REBOOT_INTERVAL is set). This number is
+ * in seconds since midnight.
+ */
+ public static final String REBOOT_START_TIME = "reboot_start_time";
+
+ /**
+ * The window of time (in seconds) after each REBOOT_INTERVAL in which
+ * a reboot can be executed. If 0, a reboot will always be executed at
+ * exactly the given time. Otherwise, it will only be executed if
+ * the device is idle within the window.
+ */
+ public static final String REBOOT_WINDOW = "reboot_window";
+
+ /**
+ * The minimum version of the server that is required in order for the device to accept
+ * the server's recommendations about the initial sync settings to use. When this is unset,
+ * blank or can't be interpreted as an integer then we will not ask the server for a
+ * recommendation.
+ */
+ public static final String GMAIL_CONFIG_INFO_MIN_SERVER_VERSION =
+ "gmail_config_info_min_server_version";
+
+ /**
+ * Controls whether Gmail offers a preview button for images.
+ */
+ public static final String GMAIL_DISALLOW_IMAGE_PREVIEWS = "gmail_disallow_image_previews";
+
+ /**
+ * The timeout in milliseconds that Gmail uses when opening a connection and reading
+ * from it. A missing value or a value of -1 instructs Gmail to use the defaults provided
+ * by GoogleHttpClient.
+ */
+ public static final String GMAIL_TIMEOUT_MS = "gmail_timeout_ms";
+
+ /**
+ * Hostname of the GTalk server.
+ */
+ public static final String GTALK_SERVICE_HOSTNAME = "gtalk_hostname";
+
+ /**
+ * Secure port of the GTalk server.
+ */
+ public static final String GTALK_SERVICE_SECURE_PORT = "gtalk_secure_port";
+
+ /**
+ * The server configurable RMQ acking interval
+ */
+ public static final String GTALK_SERVICE_RMQ_ACK_INTERVAL = "gtalk_rmq_ack_interval";
+
+ /**
+ * The minimum reconnect delay for short network outages or when the network is suspended
+ * due to phone use.
+ */
+ public static final String GTALK_SERVICE_MIN_RECONNECT_DELAY_SHORT =
+ "gtalk_min_reconnect_delay_short";
+
+ /**
+ * The reconnect variant range for short network outages or when the network is suspended
+ * due to phone use. A random number between 0 and this constant is computed and
+ * added to {@link #GTALK_SERVICE_MIN_RECONNECT_DELAY_SHORT} to form the initial reconnect
+ * delay.
+ */
+ public static final String GTALK_SERVICE_RECONNECT_VARIANT_SHORT =
+ "gtalk_reconnect_variant_short";
+
+ /**
+ * The minimum reconnect delay for long network outages
+ */
+ public static final String GTALK_SERVICE_MIN_RECONNECT_DELAY_LONG =
+ "gtalk_min_reconnect_delay_long";
+
+ /**
+ * The reconnect variant range for long network outages. A random number between 0 and this
+ * constant is computed and added to {@link #GTALK_SERVICE_MIN_RECONNECT_DELAY_LONG} to
+ * form the initial reconnect delay.
+ */
+ public static final String GTALK_SERVICE_RECONNECT_VARIANT_LONG =
+ "gtalk_reconnect_variant_long";
+
+ /**
+ * The maximum reconnect delay time, in milliseconds.
+ */
+ public static final String GTALK_SERVICE_MAX_RECONNECT_DELAY =
+ "gtalk_max_reconnect_delay";
+
+ /**
+ * The network downtime that is considered "short" for the above calculations,
+ * in milliseconds.
+ */
+ public static final String GTALK_SERVICE_SHORT_NETWORK_DOWNTIME =
+ "gtalk_short_network_downtime";
+
+ /**
+ * How frequently we send heartbeat pings to the GTalk server. Receiving a server packet
+ * will reset the heartbeat timer. The away heartbeat should be used when the user is
+ * logged into the GTalk app, but not actively using it.
+ */
+ public static final String GTALK_SERVICE_AWAY_HEARTBEAT_INTERVAL_MS =
+ "gtalk_heartbeat_ping_interval_ms"; // keep the string backward compatible
+
+ /**
+ * How frequently we send heartbeat pings to the GTalk server. Receiving a server packet
+ * will reset the heartbeat timer. The active heartbeat should be used when the user is
+ * actively using the GTalk app.
+ */
+ public static final String GTALK_SERVICE_ACTIVE_HEARTBEAT_INTERVAL_MS =
+ "gtalk_active_heartbeat_ping_interval_ms";
+
+ /**
+ * How frequently we send heartbeat pings to the GTalk server. Receiving a server packet
+ * will reset the heartbeat timer. The sync heartbeat should be used when the user isn't
+ * logged into the GTalk app, but auto-sync is enabled.
+ */
+ public static final String GTALK_SERVICE_SYNC_HEARTBEAT_INTERVAL_MS =
+ "gtalk_sync_heartbeat_ping_interval_ms";
+
+ /**
+ * How frequently we send heartbeat pings to the GTalk server. Receiving a server packet
+ * will reset the heartbeat timer. The no sync heartbeat should be used when the user isn't
+ * logged into the GTalk app, and auto-sync is not enabled.
+ */
+ public static final String GTALK_SERVICE_NOSYNC_HEARTBEAT_INTERVAL_MS =
+ "gtalk_nosync_heartbeat_ping_interval_ms";
+
+ /**
+ * How long we wait to receive a heartbeat ping acknowledgement (or another packet)
+ * from the GTalk server, before deeming the connection dead.
+ */
+ public static final String GTALK_SERVICE_HEARTBEAT_ACK_TIMEOUT_MS =
+ "gtalk_heartbeat_ack_timeout_ms";
+
+ /**
+ * How long after screen is turned off before we consider the user to be idle.
+ */
+ public static final String GTALK_SERVICE_IDLE_TIMEOUT_MS =
+ "gtalk_idle_timeout_ms";
+
+ /**
+ * By default, GTalkService will always connect to the server regardless of the auto-sync
+ * setting. However, if this parameter is true, then GTalkService will only connect
+ * if auto-sync is enabled. Using the GTalk app will trigger the connection too.
+ */
+ public static final String GTALK_SERVICE_CONNECT_ON_AUTO_SYNC =
+ "gtalk_connect_on_auto_sync";
+
+ /**
+ * GTalkService holds a wakelock while broadcasting the intent for data message received.
+ * It then automatically release the wakelock after a timeout. This setting controls what
+ * the timeout should be.
+ */
+ public static final String GTALK_DATA_MESSAGE_WAKELOCK_MS =
+ "gtalk_data_message_wakelock_ms";
+
+ /**
+ * The socket read timeout used to control how long ssl handshake wait for reads before
+ * timing out. This is needed so the ssl handshake doesn't hang for a long time in some
+ * circumstances.
+ */
+ public static final String GTALK_SSL_HANDSHAKE_TIMEOUT_MS =
+ "gtalk_ssl_handshake_timeout_ms";
+
+ /**
+ * How many bytes long a message has to be, in order to be gzipped.
+ */
+ public static final String SYNC_MIN_GZIP_BYTES =
+ "sync_min_gzip_bytes";
+
+ /**
+ * The hash value of the current provisioning settings
+ */
+ public static final String PROVISIONING_DIGEST = "digest";
+
+ /**
+ * Provisioning keys to block from server update
+ */
+ public static final String PROVISIONING_OVERRIDE = "override";
+
+ /**
+ * "Generic" service name for authentication requests.
+ */
+ public static final String GOOGLE_LOGIN_GENERIC_AUTH_SERVICE
+ = "google_login_generic_auth_service";
+
+ /**
+ * Frequency in milliseconds at which we should sync the locally installed Vending Machine
+ * content with the server.
+ */
+ public static final String VENDING_SYNC_FREQUENCY_MS = "vending_sync_frequency_ms";
+
+ /**
+ * Support URL that is opened in a browser when user clicks on 'Help and Info' in Vending
+ * Machine.
+ */
+ public static final String VENDING_SUPPORT_URL = "vending_support_url";
+
+ /**
+ * Indicates if Vending Machine requires a SIM to be in the phone to allow a purchase.
+ *
+ * true = SIM is required
+ * false = SIM is not required
+ */
+ public static final String VENDING_REQUIRE_SIM_FOR_PURCHASE =
+ "vending_require_sim_for_purchase";
+
+ /**
+ * The current version id of the Vending Machine terms of service.
+ */
+ public static final String VENDING_TOS_VERSION = "vending_tos_version";
+
+ /**
+ * URL that points to the terms of service for Vending Machine.
+ */
+ public static final String VENDING_TOS_URL = "vending_tos_url";
+
+ /**
+ * Whether to use sierraqa instead of sierra tokens for the purchase flow in
+ * Vending Machine.
+ *
+ * true = use sierraqa
+ * false = use sierra (default)
+ */
+ public static final String VENDING_USE_CHECKOUT_QA_SERVICE =
+ "vending_use_checkout_qa_service";
+
+ /**
+ * URL that points to the legal terms of service to display in Settings.
+ * <p>
+ * This should be a https URL. For a pretty user-friendly URL, use
+ * {@link #SETTINGS_TOS_PRETTY_URL}.
+ */
+ public static final String SETTINGS_TOS_URL = "settings_tos_url";
+
+ /**
+ * URL that points to the legal terms of service to display in Settings.
+ * <p>
+ * This should be a pretty http URL. For the URL the device will access
+ * via Settings, use {@link #SETTINGS_TOS_URL}.
+ */
+ public static final String SETTINGS_TOS_PRETTY_URL = "settings_tos_pretty_url";
+
+ /**
+ * URL that points to the contributors to display in Settings.
+ * <p>
+ * This should be a https URL. For a pretty user-friendly URL, use
+ * {@link #SETTINGS_CONTRIBUTORS_PRETTY_URL}.
+ */
+ public static final String SETTINGS_CONTRIBUTORS_URL = "settings_contributors_url";
+
+ /**
+ * URL that points to the contributors to display in Settings.
+ * <p>
+ * This should be a pretty http URL. For the URL the device will access
+ * via Settings, use {@link #SETTINGS_CONTRIBUTORS_URL}.
+ */
+ public static final String SETTINGS_CONTRIBUTORS_PRETTY_URL =
+ "settings_contributors_pretty_url";
+
+ /**
+ * Request an MSISDN token for various Google services.
+ */
+ public static final String USE_MSISDN_TOKEN = "use_msisdn_token";
+
+ /**
+ * RSA public key used to encrypt passwords stored in the database.
+ */
+ public static final String GLS_PUBLIC_KEY = "google_login_public_key";
+
+ /**
+ * Only check parental control status if this is set to "true".
+ */
+ public static final String PARENTAL_CONTROL_CHECK_ENABLED =
+ "parental_control_check_enabled";
+
+
+ /**
+ * Duration in which parental control status is valid.
+ */
+ public static final String PARENTAL_CONTROL_TIMEOUT_IN_MS =
+ "parental_control_timeout_in_ms";
+
+ /**
+ * When parental control is off, we expect to get this string from the
+ * litmus url.
+ */
+ public static final String PARENTAL_CONTROL_EXPECTED_RESPONSE =
+ "parental_control_expected_response";
+
+ /**
+ * When the litmus url returns a 302, declare parental control to be on
+ * only if the redirect url matches this regular expression.
+ */
+ public static final String PARENTAL_CONTROL_REDIRECT_REGEX =
+ "parental_control_redirect_regex";
+
+ /**
+ * Threshold for the amount of change in disk free space required to report the amount of
+ * free space. Used to prevent spamming the logs when the disk free space isn't changing
+ * frequently.
+ */
+ public static final String DISK_FREE_CHANGE_REPORTING_THRESHOLD =
+ "disk_free_change_reporting_threshold";
+
+ /**
+ * Prefix for new Google services published by the checkin
+ * server.
+ */
+ public static final String GOOGLE_SERVICES_PREFIX
+ = "google_services:";
+
+ /**
+ * The maximum reconnect delay for short network outages or when the network is suspended
+ * due to phone use.
+ */
+ public static final String SYNC_MAX_RETRY_DELAY_IN_SECONDS =
+ "sync_max_retry_delay_in_seconds";
+
+ /**
+ * Minimum percentage of free storage on the device that is used to determine if
+ * the device is running low on storage.
+ * Say this value is set to 10, the device is considered running low on storage
+ * if 90% or more of the device storage is filled up.
+ */
+ public static final String SYS_STORAGE_THRESHOLD_PERCENTAGE =
+ "sys_storage_threshold_percentage";
+
+ /**
+ * The interval in minutes after which the amount of free storage left on the
+ * device is logged to the event log
+ */
+ public static final String SYS_FREE_STORAGE_LOG_INTERVAL =
+ "sys_free_storage_log_interval";
+
+ /**
+ * The interval in milliseconds at which to check packet counts on the
+ * mobile data interface when screen is on, to detect possible data
+ * connection problems.
+ */
+ public static final String PDP_WATCHDOG_POLL_INTERVAL_MS =
+ "pdp_watchdog_poll_interval_ms";
+
+ /**
+ * The interval in milliseconds at which to check packet counts on the
+ * mobile data interface when screen is off, to detect possible data
+ * connection problems.
+ */
+ public static final String PDP_WATCHDOG_LONG_POLL_INTERVAL_MS =
+ "pdp_watchdog_long_poll_interval_ms";
+
+ /**
+ * The interval in milliseconds at which to check packet counts on the
+ * mobile data interface after {@link #PDP_WATCHDOG_TRIGGER_PACKET_COUNT}
+ * outgoing packets has been reached without incoming packets.
+ */
+ public static final String PDP_WATCHDOG_ERROR_POLL_INTERVAL_MS =
+ "pdp_watchdog_error_poll_interval_ms";
+
+ /**
+ * The number of outgoing packets sent without seeing an incoming packet
+ * that triggers a countdown (of {@link #PDP_WATCHDOG_ERROR_POLL_COUNT}
+ * device is logged to the event log
+ */
+ public static final String PDP_WATCHDOG_TRIGGER_PACKET_COUNT =
+ "pdp_watchdog_trigger_packet_count";
+
+ /**
+ * The number of polls to perform (at {@link #PDP_WATCHDOG_ERROR_POLL_INTERVAL_MS})
+ * after hitting {@link #PDP_WATCHDOG_TRIGGER_PACKET_COUNT} before
+ * attempting data connection recovery.
+ */
+ public static final String PDP_WATCHDOG_ERROR_POLL_COUNT =
+ "pdp_watchdog_error_poll_count";
+
+ /**
+ * The number of failed PDP reset attempts before moving to something more
+ * drastic: re-registering to the network.
+ */
+ public static final String PDP_WATCHDOG_MAX_PDP_RESET_FAIL_COUNT =
+ "pdp_watchdog_max_pdp_reset_fail_count";
+
+ /**
+ * Address to ping as a last sanity check before attempting any recovery.
+ * Unset or set to "0.0.0.0" to skip this check.
+ */
+ public static final String PDP_WATCHDOG_PING_ADDRESS =
+ "pdp_watchdog_ping_address";
+
+ /**
+ * The "-w deadline" parameter for the ping, ie, the max time in
+ * seconds to spend pinging.
+ */
+ public static final String PDP_WATCHDOG_PING_DEADLINE =
+ "pdp_watchdog_ping_deadline";
+
+ /**
+ * The interval in milliseconds after which Wi-Fi is considered idle.
+ * When idle, it is possible for the device to be switched from Wi-Fi to
+ * the mobile data network.
+ */
+ public static final String WIFI_IDLE_MS = "wifi_idle_ms";
+
+ /**
+ * The interval in milliseconds at which we forcefully release the
+ * transition-to-mobile-data wake lock.
+ */
+ public static final String WIFI_MOBILE_DATA_TRANSITION_WAKELOCK_TIMEOUT_MS =
+ "wifi_mobile_data_transition_wakelock_timeout_ms";
+
+ /**
+ * The maximum number of times we will retry a connection to an access
+ * point for which we have failed in acquiring an IP address from DHCP.
+ * A value of N means that we will make N+1 connection attempts in all.
+ */
+ public static final String WIFI_MAX_DHCP_RETRY_COUNT = "wifi_max_dhcp_retry_count";
+
+ /**
+ * @deprecated
+ * @hide
+ */
+ @Deprecated // Obviated by NameValueCache: just fetch the value directly.
+ public static class QueryMap extends ContentQueryMap {
+
+ public QueryMap(ContentResolver contentResolver, Cursor cursor, boolean keepUpdated,
+ Handler handlerForUpdateNotifications) {
+ super(cursor, NAME, keepUpdated, handlerForUpdateNotifications);
+ }
+
+ public QueryMap(ContentResolver contentResolver, boolean keepUpdated,
+ Handler handlerForUpdateNotifications) {
+ this(contentResolver,
+ contentResolver.query(CONTENT_URI, null, null, null, null),
+ keepUpdated, handlerForUpdateNotifications);
+ }
+
+ public String getString(String name) {
+ ContentValues cv = getValues(name);
+ if (cv == null) return null;
+ return cv.getAsString(VALUE);
+ }
+ }
+
+ }
+
+ /**
+ * User-defined bookmarks and shortcuts. The target of each bookmark is an
+ * Intent URL, allowing it to be either a web page or a particular
+ * application activity.
+ *
+ * @hide
+ */
+ public static final class Bookmarks implements BaseColumns
+ {
+ private static final String TAG = "Bookmarks";
+
+ /**
+ * The content:// style URL for this table
+ */
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://" + AUTHORITY + "/bookmarks");
+
+ /**
+ * The row ID.
+ * <p>Type: INTEGER</p>
+ */
+ public static final String ID = "_id";
+
+ /**
+ * Descriptive name of the bookmark that can be displayed to the user.
+ * <P>Type: TEXT</P>
+ */
+ public static final String TITLE = "title";
+
+ /**
+ * Arbitrary string (displayed to the user) that allows bookmarks to be
+ * organized into categories. There are some special names for
+ * standard folders, which all start with '@'. The label displayed for
+ * the folder changes with the locale (via {@link #labelForFolder}) but
+ * the folder name does not change so you can consistently query for
+ * the folder regardless of the current locale.
+ *
+ * <P>Type: TEXT</P>
+ *
+ */
+ public static final String FOLDER = "folder";
+
+ /**
+ * The Intent URL of the bookmark, describing what it points to. This
+ * value is given to {@link android.content.Intent#getIntent} to create
+ * an Intent that can be launched.
+ * <P>Type: TEXT</P>
+ */
+ public static final String INTENT = "intent";
+
+ /**
+ * Optional shortcut character associated with this bookmark.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String SHORTCUT = "shortcut";
+
+ /**
+ * The order in which the bookmark should be displayed
+ * <P>Type: INTEGER</P>
+ */
+ public static final String ORDERING = "ordering";
+
+ private static final String[] sIntentProjection = { INTENT };
+ private static final String[] sShortcutProjection = { ID, SHORTCUT };
+ private static final String sShortcutSelection = SHORTCUT + "=?";
+
+ /**
+ * Convenience function to retrieve the bookmarked Intent for a
+ * particular shortcut key.
+ *
+ * @param cr The ContentResolver to query.
+ * @param shortcut The shortcut key.
+ *
+ * @return Intent The bookmarked URL, or null if there is no bookmark
+ * matching the given shortcut.
+ */
+ public static Intent getIntentForShortcut(ContentResolver cr, char shortcut)
+ {
+ Intent intent = null;
+
+ Cursor c = cr.query(CONTENT_URI,
+ sIntentProjection, sShortcutSelection,
+ new String[] { String.valueOf((int) shortcut) }, ORDERING);
+ // Keep trying until we find a valid shortcut
+ try {
+ while (intent == null && c.moveToNext()) {
+ try {
+ String intentURI = c.getString(c.getColumnIndexOrThrow(INTENT));
+ intent = Intent.getIntent(intentURI);
+ } catch (java.net.URISyntaxException e) {
+ // The stored URL is bad... ignore it.
+ } catch (IllegalArgumentException e) {
+ // Column not found
+ Log.e(TAG, "Intent column not found", e);
+ }
+ }
+ } finally {
+ if (c != null) c.close();
+ }
+
+ return intent;
+ }
+
+ /**
+ * Add a new bookmark to the system.
+ *
+ * @param cr The ContentResolver to query.
+ * @param intent The desired target of the bookmark.
+ * @param title Bookmark title that is shown to the user; null if none.
+ * @param folder Folder in which to place the bookmark; null if none.
+ * @param shortcut Shortcut that will invoke the bookmark; 0 if none.
+ * If this is non-zero and there is an existing
+ * bookmark entry with this same shortcut, then that
+ * existing shortcut is cleared (the bookmark is not
+ * removed).
+ *
+ * @return The unique content URL for the new bookmark entry.
+ */
+ public static Uri add(ContentResolver cr,
+ Intent intent,
+ String title,
+ String folder,
+ char shortcut,
+ int ordering)
+ {
+ // If a shortcut is supplied, and it is already defined for
+ // another bookmark, then remove the old definition.
+ if (shortcut != 0) {
+ Cursor c = cr.query(CONTENT_URI,
+ sShortcutProjection, sShortcutSelection,
+ new String[] { String.valueOf((int) shortcut) }, null);
+ try {
+ if (c.moveToFirst()) {
+ while (c.getCount() > 0) {
+ if (!c.deleteRow()) {
+ Log.w(TAG, "Could not delete existing shortcut row");
+ }
+ }
+ }
+ } finally {
+ if (c != null) c.close();
+ }
+ }
+
+ ContentValues values = new ContentValues();
+ if (title != null) values.put(TITLE, title);
+ if (folder != null) values.put(FOLDER, folder);
+ values.put(INTENT, intent.toURI());
+ if (shortcut != 0) values.put(SHORTCUT, (int) shortcut);
+ values.put(ORDERING, ordering);
+ return cr.insert(CONTENT_URI, values);
+ }
+
+ /**
+ * Return the folder name as it should be displayed to the user. This
+ * takes care of localizing special folders.
+ *
+ * @param r Resources object for current locale; only need access to
+ * system resources.
+ * @param folder The value found in the {@link #FOLDER} column.
+ *
+ * @return CharSequence The label for this folder that should be shown
+ * to the user.
+ */
+ public static CharSequence labelForFolder(Resources r, String folder) {
+ return folder;
+ }
+ }
+
+ /**
+ * Returns the GTalk JID resource associated with this device.
+ *
+ * @return String the JID resource of the device. It uses the device IMEI in the computation
+ * of the JID resource. If IMEI is not ready (i.e. telephony module not ready), we'll return
+ * an empty string.
+ * @hide
+ */
+ // TODO: we shouldn't not have a permenant Jid resource, as that's an easy target for
+ // spams. We should change it once a while, like when we resubscribe to the subscription feeds
+ // server.
+ // (also, should this live in GTalkService?)
+ public static synchronized String getJidResource() {
+ if (sJidResource != null) {
+ return sJidResource;
+ }
+
+ MessageDigest digest;
+ try {
+ digest = MessageDigest.getInstance("SHA-1");
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException("this should never happen");
+ }
+
+ String imei = TelephonyManager.getDefault().getDeviceId();
+ if (TextUtils.isEmpty(imei)) {
+ return "";
+ }
+
+ byte[] hashedImei = digest.digest(imei.getBytes());
+ String id = new String(Base64.encodeBase64(hashedImei), 0, 12);
+ id = id.replaceAll("/", "_");
+ sJidResource = JID_RESOURCE_PREFIX + id;
+ return sJidResource;
+ }
+
+ /**
+ * Returns the device ID that we should use when connecting to the mobile gtalk server.
+ * This is a string like "android-0x1242", where the hex string is the Android ID obtained
+ * from the GoogleLoginService.
+ *
+ * @param androidId The Android ID for this device.
+ * @return The device ID that should be used when connecting to the mobile gtalk server.
+ * @hide
+ */
+ public static String getGTalkDeviceId(long androidId) {
+ return "android-" + Long.toHexString(androidId);
+ }
+}
diff --git a/core/java/android/provider/SubscribedFeeds.java b/core/java/android/provider/SubscribedFeeds.java
new file mode 100644
index 0000000..4d430d5
--- /dev/null
+++ b/core/java/android/provider/SubscribedFeeds.java
@@ -0,0 +1,204 @@
+/*
+ * 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.provider;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+
+/**
+ * The SubscribedFeeds provider stores all information about subscribed feeds.
+ *
+ * @hide
+ */
+public class SubscribedFeeds {
+ private SubscribedFeeds() {}
+
+ /**
+ * Columns from the Feed table that other tables join into themselves.
+ */
+ public interface FeedColumns {
+ /**
+ * The feed url.
+ * <P>Type: TEXT</P>
+ */
+ public static final String FEED = "feed";
+
+ /**
+ * The authority that cares about the feed.
+ * <P>Type: TEXT</P>
+ */
+ public static final String AUTHORITY = "authority";
+
+ /**
+ * The gaia service this feed is for (used for authentication).
+ * <P>Type: TEXT</P>
+ */
+ public static final String SERVICE = "service";
+ }
+
+ /**
+ * Provides constants to access the Feeds table and some utility methods
+ * to ease using the Feeds content provider.
+ */
+ public static final class Feeds implements BaseColumns, SyncConstValue,
+ FeedColumns {
+ private Feeds() {}
+
+ public static Cursor query(ContentResolver cr, String[] projection) {
+ return cr.query(CONTENT_URI, projection, null, null, DEFAULT_SORT_ORDER);
+ }
+
+ public static Cursor query(ContentResolver cr, String[] projection,
+ String where, String[] whereArgs, String orderBy) {
+ return cr.query(CONTENT_URI, projection, where,
+ whereArgs, (orderBy == null) ? DEFAULT_SORT_ORDER : orderBy);
+ }
+
+ /**
+ * The content:// style URL for this table
+ */
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://subscribedfeeds/feeds");
+
+ /**
+ * The content:// style URL for this table
+ */
+ public static final Uri DELETED_CONTENT_URI =
+ Uri.parse("content://subscribedfeeds/deleted_feeds");
+
+ /**
+ * The MIME type of {@link #CONTENT_URI} providing a directory of
+ * subscribed feeds.
+ */
+ public static final String CONTENT_TYPE =
+ "vnd.android.cursor.dir/subscribedfeeds";
+
+ /**
+ * The MIME type of a {@link #CONTENT_URI} subdirectory of a single
+ * subscribed feed.
+ */
+ public static final String CONTENT_ITEM_TYPE =
+ "vnd.android.cursor.item/subscribedfeed";
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = "_SYNC_ACCOUNT ASC";
+ }
+
+ /**
+ * A convenience method to add a feed to the SubscribedFeeds
+ * content provider. The user specifies the values of the FEED,
+ * _SYNC_ACCOUNT, AUTHORITY. SERVICE, and ROUTING_INFO.
+ * @param resolver used to access the underlying content provider
+ * @param feed corresponds to the FEED column
+ * @param account corresponds to the _SYNC_ACCOUNT column
+ * @param authority corresponds to the AUTHORITY column
+ * @param service corresponds to the SERVICE column
+ * @return the Uri of the feed that was added
+ */
+ public static Uri addFeed(ContentResolver resolver,
+ String feed, String account,
+ String authority, String service) {
+ ContentValues values = new ContentValues();
+ values.put(SubscribedFeeds.Feeds.FEED, feed);
+ values.put(SubscribedFeeds.Feeds._SYNC_ACCOUNT, account);
+ values.put(SubscribedFeeds.Feeds.AUTHORITY, authority);
+ values.put(SubscribedFeeds.Feeds.SERVICE, service);
+ return resolver.insert(SubscribedFeeds.Feeds.CONTENT_URI, values);
+ }
+
+ public static int deleteFeed(ContentResolver resolver,
+ String feed, String account, String authority) {
+ StringBuilder where = new StringBuilder();
+ where.append(SubscribedFeeds.Feeds._SYNC_ACCOUNT + "=?");
+ where.append(" AND " + SubscribedFeeds.Feeds.FEED + "=?");
+ where.append(" AND " + SubscribedFeeds.Feeds.AUTHORITY + "=?");
+ return resolver.delete(SubscribedFeeds.Feeds.CONTENT_URI,
+ where.toString(), new String[] {account, feed, authority});
+ }
+
+ public static int deleteFeeds(ContentResolver resolver,
+ String account, String authority) {
+ StringBuilder where = new StringBuilder();
+ where.append(SubscribedFeeds.Feeds._SYNC_ACCOUNT + "=?");
+ where.append(" AND " + SubscribedFeeds.Feeds.AUTHORITY + "=?");
+ return resolver.delete(SubscribedFeeds.Feeds.CONTENT_URI,
+ where.toString(), new String[] {account, authority});
+ }
+
+ public static String gtalkServiceRoutingInfoFromAccountAndResource(
+ String account, String res) {
+ return Uri.parse("gtalk://" + account + "/" + res).toString();
+ }
+
+ /**
+ * Columns from the Accounts table.
+ */
+ public interface AccountColumns {
+ /**
+ * The account.
+ * <P>Type: TEXT</P>
+ */
+ public static final String _SYNC_ACCOUNT = SyncConstValue._SYNC_ACCOUNT;
+ }
+
+ /**
+ * Provides constants to access the Accounts table and some utility methods
+ * to ease using it.
+ */
+ public static final class Accounts implements BaseColumns, AccountColumns {
+ private Accounts() {}
+
+ public static Cursor query(ContentResolver cr, String[] projection) {
+ return cr.query(CONTENT_URI, projection, null, null, DEFAULT_SORT_ORDER);
+ }
+
+ public static Cursor query(ContentResolver cr, String[] projection,
+ String where, String orderBy) {
+ return cr.query(CONTENT_URI, projection, where,
+ null, (orderBy == null) ? DEFAULT_SORT_ORDER : orderBy);
+ }
+
+ /**
+ * The content:// style URL for this table
+ */
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://subscribedfeeds/accounts");
+
+ /**
+ * The MIME type of {@link #CONTENT_URI} providing a directory of
+ * accounts that have subscribed feeds.
+ */
+ public static final String CONTENT_TYPE =
+ "vnd.android.cursor.dir/subscribedfeedaccounts";
+
+ /**
+ * The MIME type of a {@link #CONTENT_URI} subdirectory of a single
+ * account in the subscribed feeds.
+ */
+ public static final String CONTENT_ITEM_TYPE =
+ "vnd.android.cursor.item/subscribedfeedaccount";
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = "_SYNC_ACCOUNT ASC";
+ }
+}
diff --git a/core/java/android/provider/Sync.java b/core/java/android/provider/Sync.java
new file mode 100644
index 0000000..b889293
--- /dev/null
+++ b/core/java/android/provider/Sync.java
@@ -0,0 +1,603 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.provider;
+
+import android.content.ContentQueryMap;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Handler;
+
+import java.util.Map;
+
+
+/**
+ * The Sync provider stores information used in managing the syncing of the device,
+ * including the history and pending syncs.
+ *
+ * @hide
+ */
+public final class Sync {
+ // utility class
+ private Sync() {}
+
+ /**
+ * The content url for this provider.
+ */
+ public static final Uri CONTENT_URI = Uri.parse("content://sync");
+
+ /**
+ * Columns from the stats table.
+ */
+ public interface StatsColumns {
+ /**
+ * The sync account.
+ * <P>Type: TEXT</P>
+ */
+ public static final String ACCOUNT = "account";
+
+ /**
+ * The content authority (contacts, calendar, etc.).
+ * <P>Type: TEXT</P>
+ */
+ public static final String AUTHORITY = "authority";
+ }
+
+ /**
+ * Provides constants and utility methods to access and use the stats table.
+ */
+ public static final class Stats implements BaseColumns, StatsColumns {
+
+ // utility class
+ private Stats() {}
+
+ /**
+ * The content url for this table.
+ */
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://sync/stats");
+
+ /** Projection for the _id column in the stats table. */
+ public static final String[] SYNC_STATS_PROJECTION = {_ID};
+ }
+
+ /**
+ * Columns from the history table.
+ */
+ public interface HistoryColumns {
+ /**
+ * The ID of the stats row corresponding to this event.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String STATS_ID = "stats_id";
+
+ /**
+ * The source of the sync event (LOCAL, POLL, USER, SERVER).
+ * <P>Type: INTEGER</P>
+ */
+ public static final String SOURCE = "source";
+
+ /**
+ * The type of sync event (START, STOP).
+ * <P>Type: INTEGER</P>
+ */
+ public static final String EVENT = "event";
+
+ /**
+ * The time of the event.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String EVENT_TIME = "eventTime";
+
+ /**
+ * How long this event took. This is only valid if the EVENT is EVENT_STOP.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String ELAPSED_TIME = "elapsedTime";
+
+ /**
+ * Any additional message associated with this event.
+ * <P>Type: TEXT</P>
+ */
+ public static final String MESG = "mesg";
+
+ /**
+ * How much activity was performed sending data to the server. This is sync adapter
+ * specific, but usually is something like how many record update/insert/delete attempts
+ * were carried out. This is only valid if the EVENT is EVENT_STOP.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String UPSTREAM_ACTIVITY = "upstreamActivity";
+
+ /**
+ * How much activity was performed while receiving data from the server.
+ * This is sync adapter specific, but usually is something like how many
+ * records were received from the server. This is only valid if the
+ * EVENT is EVENT_STOP.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String DOWNSTREAM_ACTIVITY = "downstreamActivity";
+ }
+
+ /**
+ * Columns from the history table.
+ */
+ public interface StatusColumns {
+ /**
+ * How many syncs were completed for this account and authority.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String NUM_SYNCS = "numSyncs";
+
+ /**
+ * How long all the events for this account and authority took.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String TOTAL_ELAPSED_TIME = "totalElapsedTime";
+
+ /**
+ * The number of syncs with SOURCE_POLL.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String NUM_SOURCE_POLL = "numSourcePoll";
+
+ /**
+ * The number of syncs with SOURCE_SERVER.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String NUM_SOURCE_SERVER = "numSourceServer";
+
+ /**
+ * The number of syncs with SOURCE_LOCAL.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String NUM_SOURCE_LOCAL = "numSourceLocal";
+
+ /**
+ * The number of syncs with SOURCE_USER.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String NUM_SOURCE_USER = "numSourceUser";
+
+ /**
+ * The time in ms that the last successful sync ended. Will be null if
+ * there are no successful syncs. A successful sync is defined as one having
+ * MESG=MESG_SUCCESS.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String LAST_SUCCESS_TIME = "lastSuccessTime";
+
+ /**
+ * The SOURCE of the last successful sync. Will be null if
+ * there are no successful syncs. A successful sync is defined
+ * as one having MESG=MESG_SUCCESS.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String LAST_SUCCESS_SOURCE = "lastSuccessSource";
+
+ /**
+ * The end time in ms of the last sync that failed since the last successful sync.
+ * Will be null if there are no syncs or if the last one succeeded. A failed
+ * sync is defined as one where MESG isn't MESG_SUCCESS or MESG_CANCELED.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String LAST_FAILURE_TIME = "lastFailureTime";
+
+ /**
+ * The SOURCE of the last sync that failed since the last successful sync.
+ * Will be null if there are no syncs or if the last one succeeded. A failed
+ * sync is defined as one where MESG isn't MESG_SUCCESS or MESG_CANCELED.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String LAST_FAILURE_SOURCE = "lastFailureSource";
+
+ /**
+ * The MESG of the last sync that failed since the last successful sync.
+ * Will be null if there are no syncs or if the last one succeeded. A failed
+ * sync is defined as one where MESG isn't MESG_SUCCESS or MESG_CANCELED.
+ * <P>Type: STRING</P>
+ */
+ public static final String LAST_FAILURE_MESG = "lastFailureMesg";
+
+ /**
+ * Is set to 1 if a sync is pending, 0 if not.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String PENDING = "pending";
+ }
+
+ /**
+ * Provides constants and utility methods to access and use the history
+ * table.
+ */
+ public static class History implements BaseColumns,
+ StatsColumns,
+ HistoryColumns {
+
+ /**
+ * The content url for this table.
+ */
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://sync/history");
+
+ /** Enum value for a sync start event. */
+ public static final int EVENT_START = 0;
+
+ /** Enum value for a sync stop event. */
+ public static final int EVENT_STOP = 1;
+
+ // TODO: i18n -- grab these out of resources.
+ /** String names for the sync event types. */
+ public static final String[] EVENTS = { "START", "STOP" };
+
+ /** Enum value for a server-initiated sync. */
+ public static final int SOURCE_SERVER = 0;
+
+ /** Enum value for a local-initiated sync. */
+ public static final int SOURCE_LOCAL = 1;
+ /**
+ * Enum value for a poll-based sync (e.g., upon connection to
+ * network)
+ */
+ public static final int SOURCE_POLL = 2;
+
+ /** Enum value for a user-initiated sync. */
+ public static final int SOURCE_USER = 3;
+
+ // TODO: i18n -- grab these out of resources.
+ /** String names for the sync source types. */
+ public static final String[] SOURCES = { "SERVER",
+ "LOCAL",
+ "POLL",
+ "USER" };
+
+ // Error types
+ public static final int ERROR_SYNC_ALREADY_IN_PROGRESS = 1;
+ public static final int ERROR_AUTHENTICATION = 2;
+ public static final int ERROR_IO = 3;
+ public static final int ERROR_PARSE = 4;
+ public static final int ERROR_CONFLICT = 5;
+ public static final int ERROR_TOO_MANY_DELETIONS = 6;
+ public static final int ERROR_TOO_MANY_RETRIES = 7;
+
+ // The MESG column will contain one of these or one of the Error types.
+ public static final String MESG_SUCCESS = "success";
+ public static final String MESG_CANCELED = "canceled";
+
+ private static final String FINISHED_SINCE_WHERE_CLAUSE = EVENT + "=" + EVENT_STOP
+ + " AND " + EVENT_TIME + ">? AND " + ACCOUNT + "=? AND " + AUTHORITY + "=?";
+
+ public static String mesgToString(String mesg) {
+ if (MESG_SUCCESS.equals(mesg)) return mesg;
+ if (MESG_CANCELED.equals(mesg)) return mesg;
+ switch (Integer.parseInt(mesg)) {
+ case ERROR_SYNC_ALREADY_IN_PROGRESS: return "already in progress";
+ case ERROR_AUTHENTICATION: return "bad authentication";
+ case ERROR_IO: return "network error";
+ case ERROR_PARSE: return "parse error";
+ case ERROR_CONFLICT: return "conflict detected";
+ case ERROR_TOO_MANY_DELETIONS: return "too many deletions";
+ case ERROR_TOO_MANY_RETRIES: return "too many retries";
+ default: return "unknown error";
+ }
+ }
+
+ // utility class
+ private History() {}
+
+ /**
+ * returns a cursor that queries the sync history in descending event time order
+ * @param contentResolver the ContentResolver to use for the query
+ * @return the cursor on the History table
+ */
+ public static Cursor query(ContentResolver contentResolver) {
+ return contentResolver.query(CONTENT_URI, null, null, null, EVENT_TIME + " desc");
+ }
+
+ public static boolean hasNewerSyncFinished(ContentResolver contentResolver,
+ String account, String authority, long when) {
+ Cursor c = contentResolver.query(CONTENT_URI, new String[]{_ID},
+ FINISHED_SINCE_WHERE_CLAUSE,
+ new String[]{Long.toString(when), account, authority}, null);
+ try {
+ return c.getCount() > 0;
+ } finally {
+ c.close();
+ }
+ }
+ }
+
+ /**
+ * Provides constants and utility methods to access and use the authority history
+ * table, which contains information about syncs aggregated by account and authority.
+ * All the HistoryColumns except for EVENT are present, plus the AuthorityHistoryColumns.
+ */
+ public static class Status extends History implements StatusColumns {
+
+ /**
+ * The content url for this table.
+ */
+ public static final Uri CONTENT_URI = Uri.parse("content://sync/status");
+
+ // utility class
+ private Status() {}
+
+ /**
+ * returns a cursor that queries the authority sync history in descending event order of
+ * ACCOUNT, AUTHORITY
+ * @param contentResolver the ContentResolver to use for the query
+ * @return the cursor on the AuthorityHistory table
+ */
+ public static Cursor query(ContentResolver contentResolver) {
+ return contentResolver.query(CONTENT_URI, null, null, null, ACCOUNT + ", " + AUTHORITY);
+ }
+
+ public static class QueryMap extends ContentQueryMap {
+ public QueryMap(ContentResolver contentResolver,
+ boolean keepUpdated,
+ Handler handlerForUpdateNotifications) {
+ super(contentResolver.query(CONTENT_URI, null, null, null, null),
+ _ID, keepUpdated, handlerForUpdateNotifications);
+ }
+
+ public ContentValues get(String account, String authority) {
+ Map<String, ContentValues> rows = getRows();
+ for (ContentValues values : rows.values()) {
+ if (values.getAsString(ACCOUNT).equals(account)
+ && values.getAsString(AUTHORITY).equals(authority)) {
+ return values;
+ }
+ }
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Provides constants and utility methods to access and use the pending syncs table
+ */
+ public static final class Pending implements BaseColumns,
+ StatsColumns {
+
+ /**
+ * The content url for this table.
+ */
+ public static final Uri CONTENT_URI = Uri.parse("content://sync/pending");
+
+ // utility class
+ private Pending() {}
+
+ public static class QueryMap extends ContentQueryMap {
+ public QueryMap(ContentResolver contentResolver, boolean keepUpdated,
+ Handler handlerForUpdateNotifications) {
+ super(contentResolver.query(CONTENT_URI, null, null, null, null), _ID, keepUpdated,
+ handlerForUpdateNotifications);
+ }
+
+ public boolean isPending(String account, String authority) {
+ Map<String, ContentValues> rows = getRows();
+ for (ContentValues values : rows.values()) {
+ if (values.getAsString(ACCOUNT).equals(account)
+ && values.getAsString(AUTHORITY).equals(authority)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+ }
+
+ /**
+ * Columns from the history table.
+ */
+ public interface ActiveColumns {
+ /**
+ * The wallclock time of when the active sync started.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String START_TIME = "startTime";
+ }
+
+ /**
+ * Provides constants and utility methods to access and use the pending syncs table
+ */
+ public static final class Active implements BaseColumns,
+ StatsColumns,
+ ActiveColumns {
+
+ /**
+ * The content url for this table.
+ */
+ public static final Uri CONTENT_URI = Uri.parse("content://sync/active");
+
+ // utility class
+ private Active() {}
+
+ public static class QueryMap extends ContentQueryMap {
+ public QueryMap(ContentResolver contentResolver, boolean keepUpdated,
+ Handler handlerForUpdateNotifications) {
+ super(contentResolver.query(CONTENT_URI, null, null, null, null), _ID, keepUpdated,
+ handlerForUpdateNotifications);
+ }
+
+ public ContentValues getActiveSyncInfo() {
+ Map<String, ContentValues> rows = getRows();
+ for (ContentValues values : rows.values()) {
+ return values;
+ }
+ return null;
+ }
+
+ public String getSyncingAccount() {
+ ContentValues values = getActiveSyncInfo();
+ return (values == null) ? null : values.getAsString(ACCOUNT);
+ }
+
+ public String getSyncingAuthority() {
+ ContentValues values = getActiveSyncInfo();
+ return (values == null) ? null : values.getAsString(AUTHORITY);
+ }
+
+ public long getSyncStartTime() {
+ ContentValues values = getActiveSyncInfo();
+ return (values == null) ? -1 : values.getAsLong(START_TIME);
+ }
+ }
+ }
+
+ /**
+ * Columns in the settings table, which holds key/value pairs of settings.
+ */
+ public interface SettingsColumns {
+ /**
+ * The key of the setting
+ * <P>Type: TEXT</P>
+ */
+ public static final String KEY = "name";
+
+ /**
+ * The value of the settings
+ * <P>Type: TEXT</P>
+ */
+ public static final String VALUE = "value";
+ }
+
+ /**
+ * Provides constants and utility methods to access and use the settings
+ * table.
+ */
+ public static final class Settings implements BaseColumns, SettingsColumns {
+ /**
+ * The Uri of the settings table. This table behaves a little differently than
+ * normal tables. Updates are not allowed, only inserts, and inserts cause a replace
+ * to be performed, which first deletes the row if it is already present.
+ */
+ public static final Uri CONTENT_URI = Uri.parse("content://sync/settings");
+
+ /** controls whether or not the devices listens for sync tickles */
+ public static final String SETTING_LISTEN_FOR_TICKLES = "listen_for_tickles";
+
+ /** controls whether or not the individual provider is synced when tickles are received */
+ public static final String SETTING_SYNC_PROVIDER_PREFIX = "sync_provider_";
+
+ /**
+ * Convenience function for updating a single settings value as a
+ * boolean. This will either create a new entry in the table if the
+ * given name does not exist, or modify the value of the existing row
+ * with that name. Note that internally setting values are always
+ * stored as strings, so this function converts the given value to a
+ * string before storing it.
+ *
+ * @param contentResolver the ContentResolver to use to access the settings table
+ * @param name The name of the setting to modify.
+ * @param val The new value for the setting.
+ */
+ static private void putBoolean(ContentResolver contentResolver, String name, boolean val) {
+ ContentValues values = new ContentValues();
+ values.put(KEY, name);
+ values.put(VALUE, Boolean.toString(val));
+ // this insert is translated into an update by the underlying Sync provider
+ contentResolver.insert(CONTENT_URI, values);
+ }
+
+ /**
+ * A convenience method to set whether or not the provider is synced when
+ * it receives a network tickle.
+ *
+ * @param contentResolver the ContentResolver to use to access the settings table
+ * @param providerName the provider whose behavior is being controlled
+ * @param sync true if the provider should be synced when tickles are received for it
+ */
+ static public void setSyncProviderAutomatically(ContentResolver contentResolver,
+ String providerName, boolean sync) {
+ putBoolean(contentResolver, SETTING_SYNC_PROVIDER_PREFIX + providerName, sync);
+ }
+
+ /**
+ * A convenience method to set whether or not the tickle xmpp connection
+ * should be established.
+ *
+ * @param contentResolver the ContentResolver to use to access the settings table
+ * @param flag true if the tickle xmpp connection should be established
+ */
+ static public void setListenForNetworkTickles(ContentResolver contentResolver,
+ boolean flag) {
+ putBoolean(contentResolver, SETTING_LISTEN_FOR_TICKLES, flag);
+ }
+
+ public static class QueryMap extends ContentQueryMap {
+ private ContentResolver mContentResolver;
+
+ public QueryMap(ContentResolver contentResolver, boolean keepUpdated,
+ Handler handlerForUpdateNotifications) {
+ super(contentResolver.query(CONTENT_URI, null, null, null, null), KEY, keepUpdated,
+ handlerForUpdateNotifications);
+ mContentResolver = contentResolver;
+ }
+
+ /**
+ * Check if the provider should be synced when a network tickle is received
+ * @param providerName the provider whose setting we are querying
+ * @return true of the provider should be synced when a network tickle is received
+ */
+ public boolean getSyncProviderAutomatically(String providerName) {
+ return getBoolean(SETTING_SYNC_PROVIDER_PREFIX + providerName, true);
+ }
+
+ /**
+ * Set whether or not the provider is synced when it receives a network tickle.
+ *
+ * @param providerName the provider whose behavior is being controlled
+ * @param sync true if the provider should be synced when tickles are received for it
+ */
+ public void setSyncProviderAutomatically(String providerName, boolean sync) {
+ Settings.setSyncProviderAutomatically(mContentResolver, providerName, sync);
+ }
+
+ /**
+ * Set whether or not the tickle xmpp connection should be established.
+ *
+ * @param flag true if the tickle xmpp connection should be established
+ */
+ public void setListenForNetworkTickles(boolean flag) {
+ Settings.setListenForNetworkTickles(mContentResolver, flag);
+ }
+
+ /**
+ * Check if the tickle xmpp connection should be established
+ * @return true if it should be stablished
+ */
+ public boolean getListenForNetworkTickles() {
+ return getBoolean(SETTING_LISTEN_FOR_TICKLES, true);
+ }
+
+ /**
+ * Convenience function for retrieving a single settings value
+ * as a boolean.
+ *
+ * @param name The name of the setting to retrieve.
+ * @param def Value to return if the setting is not defined.
+ * @return The setting's current value, or 'def' if it is not defined.
+ */
+ private boolean getBoolean(String name, boolean def) {
+ ContentValues values = getValues(name);
+ return values != null ? values.getAsBoolean(VALUE) : def;
+ }
+ }
+ }
+}
diff --git a/core/java/android/provider/SyncConstValue.java b/core/java/android/provider/SyncConstValue.java
new file mode 100644
index 0000000..6eb4398
--- /dev/null
+++ b/core/java/android/provider/SyncConstValue.java
@@ -0,0 +1,71 @@
+/*
+ * 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.provider;
+
+/**
+ * Columns for tables that are synced to a server.
+ * @hide
+ */
+public interface SyncConstValue
+{
+ /**
+ * The account that was used to sync the entry to the device.
+ * <P>Type: TEXT</P>
+ */
+ public static final String _SYNC_ACCOUNT = "_sync_account";
+
+ /**
+ * The unique ID for a row assigned by the sync source. NULL if the row has never been synced.
+ * <P>Type: TEXT</P>
+ */
+ public static final String _SYNC_ID = "_sync_id";
+
+ /**
+ * The last time, from the sync source's point of view, that this row has been synchronized.
+ * <P>Type: INTEGER (long)</P>
+ */
+ public static final String _SYNC_TIME = "_sync_time";
+
+ /**
+ * The version of the row, as assigned by the server.
+ * <P>Type: TEXT</P>
+ */
+ public static final String _SYNC_VERSION = "_sync_version";
+
+ /**
+ * Used in temporary provider while syncing, always NULL for rows in persistent providers.
+ * <P>Type: INTEGER (long)</P>
+ */
+ public static final String _SYNC_LOCAL_ID = "_sync_local_id";
+
+ /**
+ * Used only in persistent providers, and only during merging.
+ * <P>Type: INTEGER (long)</P>
+ */
+ public static final String _SYNC_MARK = "_sync_mark";
+
+ /**
+ * Used to indicate that local, unsynced, changes are present.
+ * <P>Type: INTEGER (long)</P>
+ */
+ public static final String _SYNC_DIRTY = "_sync_dirty";
+
+ /**
+ * Used to indicate that this account is not synced
+ */
+ public static final String NON_SYNCABLE_ACCOUNT = "non_syncable";
+}
diff --git a/core/java/android/provider/Telephony.java b/core/java/android/provider/Telephony.java
new file mode 100644
index 0000000..776a266
--- /dev/null
+++ b/core/java/android/provider/Telephony.java
@@ -0,0 +1,1694 @@
+/*
+ * 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.provider;
+
+import com.android.internal.telephony.CallerInfo;
+import com.google.android.mms.util.SqliteWrapper;
+
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.TelephonyManager;
+import android.telephony.gsm.SmsMessage;
+import android.text.TextUtils;
+import android.text.util.Regex;
+import android.util.Config;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * The Telephony provider contains data related to phone operation.
+ *
+ * @hide
+ */
+public final class Telephony {
+ private static final String TAG = "Telephony";
+ private static final boolean DEBUG = false;
+ private static final boolean LOCAL_LOGV = DEBUG ? Config.LOGD : Config.LOGV;
+
+ /**
+ * Base columns for tables that contain text based SMSs.
+ */
+ public interface TextBasedSmsColumns {
+ /**
+ * The type of the message
+ * <P>Type: INTEGER</P>
+ */
+ public static final String TYPE = "type";
+
+ public static final int MESSAGE_TYPE_ALL = 0;
+ public static final int MESSAGE_TYPE_INBOX = 1;
+ public static final int MESSAGE_TYPE_SENT = 2;
+ public static final int MESSAGE_TYPE_DRAFT = 3;
+ public static final int MESSAGE_TYPE_OUTBOX = 4;
+ public static final int MESSAGE_TYPE_FAILED = 5; // for failed outgoing messages
+ public static final int MESSAGE_TYPE_QUEUED = 6; // for messages to send later
+
+
+ /**
+ * The thread ID of the message
+ * <P>Type: INTEGER</P>
+ */
+ public static final String THREAD_ID = "thread_id";
+
+ /**
+ * The address of the other party
+ * <P>Type: TEXT</P>
+ */
+ public static final String ADDRESS = "address";
+
+ /**
+ * The person ID of the sender
+ * <P>Type: INTEGER (long)</P>
+ */
+ public static final String PERSON_ID = "person";
+
+ /**
+ * The date the message was sent
+ * <P>Type: INTEGER (long)</P>
+ */
+ public static final String DATE = "date";
+
+ /**
+ * Has the message been read
+ * <P>Type: INTEGER (boolean)</P>
+ */
+ public static final String READ = "read";
+
+ /**
+ * The TP-Status value for the message, or -1 if no status has
+ * been received
+ */
+ public static final String STATUS = "status";
+
+ public static final int STATUS_NONE = -1;
+ public static final int STATUS_COMPLETE = 0;
+ public static final int STATUS_PENDING = 64;
+ public static final int STATUS_FAILED = 128;
+
+ /**
+ * The subject of the message, if present
+ * <P>Type: TEXT</P>
+ */
+ public static final String SUBJECT = "subject";
+
+ /**
+ * The body of the message
+ * <P>Type: TEXT</P>
+ */
+ public static final String BODY = "body";
+
+ /**
+ * The id of the sender of the conversation, if present
+ * <P>Type: INTEGER (reference to item in content://contacts/people)</P>
+ */
+ public static final String PERSON = "person";
+
+ /**
+ * The protocol identifier code
+ * <P>Type: INTEGER</P>
+ */
+ public static final String PROTOCOL = "protocol";
+
+ /**
+ * Whether the <code>TP-Reply-Path</code> bit was set on this message
+ * <P>Type: BOOLEAN</P>
+ */
+ public static final String REPLY_PATH_PRESENT = "reply_path_present";
+
+ /**
+ * The service center (SC) through which to send the message, if present
+ * <P>Type: TEXT</P>
+ */
+ public static final String SERVICE_CENTER = "service_center";
+ }
+
+ /**
+ * Contains all text based SMS messages.
+ */
+ public static final class Sms implements BaseColumns, TextBasedSmsColumns {
+ public static final Cursor query(ContentResolver cr, String[] projection) {
+ return cr.query(CONTENT_URI, projection, null, null, DEFAULT_SORT_ORDER);
+ }
+
+ public static final Cursor query(ContentResolver cr, String[] projection,
+ String where, String orderBy) {
+ return cr.query(CONTENT_URI, projection, where,
+ null, orderBy == null ? DEFAULT_SORT_ORDER : orderBy);
+ }
+
+ /**
+ * The content:// style URL for this table
+ */
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://sms");
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = "date DESC";
+
+ /**
+ * Add an SMS to the given URI.
+ *
+ * @param resolver the content resolver to use
+ * @param uri the URI to add the message to
+ * @param address the address of the sender
+ * @param body the body of the message
+ * @param subject the psuedo-subject of the message
+ * @param date the timestamp for the message
+ * @param read true if the message has been read, false if not
+ * @param deliveryReport true if a delivery report was requested, false if not
+ * @return the URI for the new message
+ */
+ public static Uri addMessageToUri(ContentResolver resolver,
+ Uri uri, String address, String body, String subject,
+ Long date, boolean read, boolean deliveryReport) {
+ return addMessageToUri(resolver, uri, address, body, subject,
+ date, read, deliveryReport, -1L);
+ }
+
+ /**
+ * Add an SMS to the given URI with thread_id specified.
+ *
+ * @param resolver the content resolver to use
+ * @param uri the URI to add the message to
+ * @param address the address of the sender
+ * @param body the body of the message
+ * @param subject the psuedo-subject of the message
+ * @param date the timestamp for the message
+ * @param read true if the message has been read, false if not
+ * @param deliveryReport true if a delivery report was requested, false if not
+ * @param threadId the thread_id of the message
+ * @return the URI for the new message
+ */
+ public static Uri addMessageToUri(ContentResolver resolver,
+ Uri uri, String address, String body, String subject,
+ Long date, boolean read, boolean deliveryReport, long threadId) {
+ ContentValues values = new ContentValues(7);
+
+ values.put(ADDRESS, address);
+ if (date != null) {
+ values.put(DATE, date);
+ }
+ values.put(READ, read ? Integer.valueOf(1) : Integer.valueOf(0));
+ values.put(SUBJECT, subject);
+ values.put(BODY, body);
+ if (deliveryReport) {
+ values.put(STATUS, STATUS_PENDING);
+ }
+ if (threadId != -1L) {
+ values.put(THREAD_ID, threadId);
+ }
+ return resolver.insert(uri, values);
+ }
+
+ /**
+ * Move a message to the given folder.
+ *
+ * @param context the context to use
+ * @param uri the message to move
+ * @param folder the folder to move to
+ * @return true if the operation succeeded
+ */
+ public static boolean moveMessageToFolder(Context context,
+ Uri uri, int folder) {
+ if ((uri == null) || ((folder != MESSAGE_TYPE_INBOX)
+ && (folder != MESSAGE_TYPE_OUTBOX)
+ && (folder != MESSAGE_TYPE_SENT)
+ && (folder != MESSAGE_TYPE_DRAFT)
+ && (folder != MESSAGE_TYPE_FAILED)
+ && (folder != MESSAGE_TYPE_QUEUED))) {
+ return false;
+ }
+
+ ContentValues values = new ContentValues(1);
+
+ values.put(TYPE, folder);
+ return 1 == SqliteWrapper.update(context, context.getContentResolver(),
+ uri, values, null, null);
+ }
+
+ /**
+ * Returns true iff the folder (message type) identifies an
+ * outgoing message.
+ */
+ public static boolean isOutgoingFolder(int messageType) {
+ return (messageType == MESSAGE_TYPE_FAILED)
+ || (messageType == MESSAGE_TYPE_OUTBOX)
+ || (messageType == MESSAGE_TYPE_SENT)
+ || (messageType == MESSAGE_TYPE_QUEUED);
+ }
+
+ /**
+ * Returns true if the address is an email address
+ *
+ * @param address the input address to be tested
+ * @return true if address is an email address
+ */
+ public static boolean isEmailAddress(String address) {
+ /*
+ * The '@' char isn't a valid char in phone numbers. However, in SMS
+ * messages sent by carrier, the originating-address can contain
+ * non-dialable alphanumeric chars. For the purpose of thread id
+ * grouping, we don't care about those. We only care about the
+ * legitmate/dialable phone numbers (which we use the special phone
+ * number comparison) and email addresses (which we do straight up
+ * string comparison).
+ */
+ return (address != null) && (address.indexOf('@') != -1);
+ }
+
+ /**
+ * Formats an address for displaying, doing a phone number lookup in the
+ * Address Book, etc.
+ *
+ * @param context the context to use
+ * @param address the address to format
+ * @return a nicely formatted version of the sender to display
+ */
+ public static String getDisplayAddress(Context context, String address) {
+ String result;
+ int index;
+ if (isEmailAddress(address)) {
+ index = address.indexOf('@');
+ if (index != -1) {
+ result = address.substring(0, index);
+ } else {
+ result = address;
+ }
+ } else {
+ result = CallerInfo.getCallerId(context, address);
+ }
+ return result;
+ }
+
+ /**
+ * Contains all text based SMS messages in the SMS app's inbox.
+ */
+ public static final class Inbox implements BaseColumns, TextBasedSmsColumns {
+ /**
+ * The content:// style URL for this table
+ */
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://sms/inbox");
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = "date DESC";
+
+ /**
+ * Add an SMS to the Draft box.
+ *
+ * @param resolver the content resolver to use
+ * @param address the address of the sender
+ * @param body the body of the message
+ * @param subject the psuedo-subject of the message
+ * @param date the timestamp for the message
+ * @param read true if the message has been read, false if not
+ * @return the URI for the new message
+ */
+ public static Uri addMessage(ContentResolver resolver,
+ String address, String body, String subject, Long date,
+ boolean read) {
+ return addMessageToUri(resolver, CONTENT_URI, address, body,
+ subject, date, read, false);
+ }
+ }
+
+ /**
+ * Contains all sent text based SMS messages in the SMS app's.
+ */
+ public static final class Sent implements BaseColumns, TextBasedSmsColumns {
+ /**
+ * The content:// style URL for this table
+ */
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://sms/sent");
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = "date DESC";
+
+ /**
+ * Add an SMS to the Draft box.
+ *
+ * @param resolver the content resolver to use
+ * @param address the address of the sender
+ * @param body the body of the message
+ * @param subject the psuedo-subject of the message
+ * @param date the timestamp for the message
+ * @return the URI for the new message
+ */
+ public static Uri addMessage(ContentResolver resolver,
+ String address, String body, String subject, Long date) {
+ return addMessageToUri(resolver, CONTENT_URI, address, body,
+ subject, date, true, false);
+ }
+ }
+
+ /**
+ * Contains all sent text based SMS messages in the SMS app's.
+ */
+ public static final class Draft implements BaseColumns, TextBasedSmsColumns {
+ /**
+ * The content:// style URL for this table
+ */
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://sms/draft");
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = "date DESC";
+
+ /**
+ * Add an SMS to the Draft box.
+ *
+ * @param resolver the content resolver to use
+ * @param address the address of the sender
+ * @param body the body of the message
+ * @param subject the psuedo-subject of the message
+ * @param date the timestamp for the message
+ * @return the URI for the new message
+ */
+ public static Uri addMessage(ContentResolver resolver,
+ String address, String body, String subject, Long date) {
+ return addMessageToUri(resolver, CONTENT_URI, address, body,
+ subject, date, true, false);
+ }
+
+ /**
+ * Save over an existing draft message.
+ *
+ * @param resolver the content resolver to use
+ * @param uri of existing message
+ * @param body the new body for the draft message
+ * @return true is successful, false otherwise
+ */
+ public static boolean saveMessage(ContentResolver resolver,
+ Uri uri, String body) {
+ ContentValues values = new ContentValues(2);
+ values.put(BODY, body);
+ values.put(DATE, System.currentTimeMillis());
+ return resolver.update(uri, values, null, null) == 1;
+ }
+ }
+
+ /**
+ * Contains all pending outgoing text based SMS messages.
+ */
+ public static final class Outbox implements BaseColumns, TextBasedSmsColumns {
+ /**
+ * The content:// style URL for this table
+ */
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://sms/outbox");
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = "date DESC";
+
+ /**
+ * Add an SMS to the Out box.
+ *
+ * @param resolver the content resolver to use
+ * @param address the address of the sender
+ * @param body the body of the message
+ * @param subject the psuedo-subject of the message
+ * @param date the timestamp for the message
+ * @param deliveryReport whether a delivery report was requested for the message
+ * @return the URI for the new message
+ */
+ public static Uri addMessage(ContentResolver resolver,
+ String address, String body, String subject, Long date,
+ boolean deliveryReport, long threadId) {
+ return addMessageToUri(resolver, CONTENT_URI, address, body,
+ subject, date, true, deliveryReport, threadId);
+ }
+ }
+
+ /**
+ * Contains all sent text-based SMS messages in the SMS app's.
+ */
+ public static final class Conversations
+ implements BaseColumns, TextBasedSmsColumns {
+ /**
+ * The content:// style URL for this table
+ */
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://sms/conversations");
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = "date DESC";
+
+ /**
+ * The first 45 characters of the body of the message
+ * <P>Type: TEXT</P>
+ */
+ public static final String SNIPPET = "snippet";
+
+ /**
+ * The number of messages in the conversation
+ * <P>Type: INTEGER</P>
+ */
+ public static final String MESSAGE_COUNT = "msg_count";
+ }
+
+ /**
+ * Contains info about SMS related Intents that are broadcast.
+ */
+ public static final class Intents {
+ /**
+ * Broadcast Action: A new text based SMS message has been received
+ * by the device. The intent will have the following extra
+ * values:</p>
+ *
+ * <ul>
+ * <li><em>pdus</em> - An Object[] od byte[]s containing the PDUs
+ * that make up the message.</li>
+ * </ul>
+ *
+ * <p>The extra values can be extracted using
+ * {@link #getMessagesFromIntent(Intent)}</p>
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String SMS_RECEIVED_ACTION =
+ "android.provider.Telephony.SMS_RECEIVED";
+
+ /**
+ * Broadcast Action: A new data based SMS message has been received
+ * by the device. The intent will have the following extra
+ * values:</p>
+ *
+ * <ul>
+ * <li><em>pdus</em> - An Object[] od byte[]s containing the PDUs
+ * that make up the message.</li>
+ * </ul>
+ *
+ * <p>The extra values can be extracted using
+ * {@link #getMessagesFromIntent(Intent)}</p>
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String DATA_SMS_RECEIVED_ACTION =
+ "android.intent.action.DATA_SMS_RECEIVED";
+
+ /**
+ * Broadcast Action: A new WAP PUSH message has been received by the
+ * device. The intent will have the following extra
+ * values:</p>
+ *
+ * <ul>
+ * <li><em>transactionId (Integer)</em> - The WAP transaction
+ * ID</li>
+ * <li><em>pduType (Integer)</em> - The WAP PDU type</li>
+ * <li><em>data</em> - The data payload of the message</li>
+ * </ul>
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String WAP_PUSH_RECEIVED_ACTION =
+ "android.provider.Telephony.WAP_PUSH_RECEIVED";
+
+ /**
+ * Broadcast Action: The SIM storage for SMS messages is full. If
+ * space is not freed, messages targeted for the SIM (class 2) may
+ * not be saved.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String SIM_FULL_ACTION =
+ "android.provider.Telephony.SIM_FULL";
+
+ /**
+ * Read the PDUs out of an {@link #SMS_RECEIVED_ACTION} or a
+ * {@link #DATA_SMS_RECEIVED_ACTION} intent.
+ *
+ * @param intent the intent to read from
+ * @return an array of SmsMessages for the PDUs
+ */
+ public static final SmsMessage[] getMessagesFromIntent(
+ Intent intent) {
+ Object[] messages = (Object[]) intent.getSerializableExtra("pdus");
+ byte[][] pduObjs = new byte[messages.length][];
+
+ for (int i = 0; i < messages.length; i++) {
+ pduObjs[i] = (byte[]) messages[i];
+ }
+ byte[][] pdus = new byte[pduObjs.length][];
+ int pduCount = pdus.length;
+ SmsMessage[] msgs = new SmsMessage[pduCount];
+ for (int i = 0; i < pduCount; i++) {
+ pdus[i] = pduObjs[i];
+ msgs[i] = SmsMessage.createFromPdu(pdus[i]);
+ }
+ return msgs;
+ }
+ }
+ }
+
+ /**
+ * Base columns for tables that contain MMSs.
+ */
+ public interface BaseMmsColumns extends BaseColumns {
+
+ public static final int MESSAGE_BOX_ALL = 0;
+ public static final int MESSAGE_BOX_INBOX = 1;
+ public static final int MESSAGE_BOX_SENT = 2;
+ public static final int MESSAGE_BOX_DRAFTS = 3;
+ public static final int MESSAGE_BOX_OUTBOX = 4;
+
+ /**
+ * The date the message was sent.
+ * <P>Type: INTEGER (long)</P>
+ */
+ public static final String DATE = "date";
+
+ /**
+ * The box which the message belong to, for example, MESSAGE_BOX_INBOX.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String MESSAGE_BOX = "msg_box";
+
+ /**
+ * Has the message been read.
+ * <P>Type: INTEGER (boolean)</P>
+ */
+ public static final String READ = "read";
+
+ /**
+ * The Message-ID of the message.
+ * <P>Type: TEXT</P>
+ */
+ public static final String MESSAGE_ID = "m_id";
+
+ /**
+ * The subject of the message, if present.
+ * <P>Type: TEXT</P>
+ */
+ public static final String SUBJECT = "sub";
+
+ /**
+ * The character set of the subject, if present.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String SUBJECT_CHARSET = "sub_cs";
+
+ /**
+ * The Content-Type of the message.
+ * <P>Type: TEXT</P>
+ */
+ public static final String CONTENT_TYPE = "ct_t";
+
+ /**
+ * The Content-Location of the message.
+ * <P>Type: TEXT</P>
+ */
+ public static final String CONTENT_LOCATION = "ct_l";
+
+ /**
+ * The address of the sender.
+ * <P>Type: TEXT</P>
+ */
+ public static final String FROM = "from";
+
+ /**
+ * The address of the recipients.
+ * <P>Type: TEXT</P>
+ */
+ public static final String TO = "to";
+
+ /**
+ * The address of the cc. recipients.
+ * <P>Type: TEXT</P>
+ */
+ public static final String CC = "cc";
+
+ /**
+ * The address of the bcc. recipients.
+ * <P>Type: TEXT</P>
+ */
+ public static final String BCC = "bcc";
+
+ /**
+ * The expiry time of the message.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String EXPIRY = "exp";
+
+ /**
+ * The class of the message.
+ * <P>Type: TEXT</P>
+ */
+ public static final String MESSAGE_CLASS = "m_cls";
+
+ /**
+ * The type of the message defined by MMS spec.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String MESSAGE_TYPE = "m_type";
+
+ /**
+ * The version of specification that this message conform.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String MMS_VERSION = "v";
+
+ /**
+ * The size of the message.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String MESSAGE_SIZE = "m_size";
+
+ /**
+ * The priority of the message.
+ * <P>Type: TEXT</P>
+ */
+ public static final String PRIORITY = "pri";
+
+ /**
+ * The read-report of the message.
+ * <P>Type: TEXT</P>
+ */
+ public static final String READ_REPORT = "rr";
+
+ /**
+ * Whether the report is allowed.
+ * <P>Type: TEXT</P>
+ */
+ public static final String REPORT_ALLOWED = "rpt_a";
+
+ /**
+ * The response-status of the message.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String RESPONSE_STATUS = "resp_st";
+
+ /**
+ * The status of the message.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String STATUS = "st";
+
+ /**
+ * The transaction-id of the message.
+ * <P>Type: TEXT</P>
+ */
+ public static final String TRANSACTION_ID = "tr_id";
+
+ /**
+ * The retrieve-status of the message.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String RETRIEVE_STATUS = "retr_st";
+
+ /**
+ * The retrieve-text of the message.
+ * <P>Type: TEXT</P>
+ */
+ public static final String RETRIEVE_TEXT = "retr_txt";
+
+ /**
+ * The character set of the retrieve-text.
+ * <P>Type: TEXT</P>
+ */
+ public static final String RETRIEVE_TEXT_CHARSET = "retr_txt_cs";
+
+ /**
+ * The read-status of the message.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String READ_STATUS = "read_status";
+
+ /**
+ * The content-class of the message.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String CONTENT_CLASS = "ct_cls";
+
+ /**
+ * The delivery-report of the message.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String DELIVERY_REPORT = "d_rpt";
+
+ /**
+ * The delivery-time-token of the message.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String DELIVERY_TIME_TOKEN = "d_tm_tok";
+
+ /**
+ * The delivery-time of the message.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String DELIVERY_TIME = "d_tm";
+
+ /**
+ * The response-text of the message.
+ * <P>Type: TEXT</P>
+ */
+ public static final String RESPONSE_TEXT = "resp_txt";
+
+ /**
+ * The sender-visibility of the message.
+ * <P>Type: TEXT</P>
+ */
+ public static final String SENDER_VISIBILITY = "s_vis";
+
+ /**
+ * The reply-charging of the message.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String REPLY_CHARGING = "r_chg";
+
+ /**
+ * The reply-charging-deadline-token of the message.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String REPLY_CHARGING_DEADLINE_TOKEN = "r_chg_dl_tok";
+
+ /**
+ * The reply-charging-deadline of the message.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String REPLY_CHARGING_DEADLINE = "r_chg_dl";
+
+ /**
+ * The reply-charging-id of the message.
+ * <P>Type: TEXT</P>
+ */
+ public static final String REPLY_CHARGING_ID = "r_chg_id";
+
+ /**
+ * The reply-charging-size of the message.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String REPLY_CHARGING_SIZE = "r_chg_sz";
+
+ /**
+ * The previously-sent-by of the message.
+ * <P>Type: TEXT</P>
+ */
+ public static final String PREVIOUSLY_SENT_BY = "p_s_by";
+
+ /**
+ * The previously-sent-date of the message.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String PREVIOUSLY_SENT_DATE = "p_s_d";
+
+ /**
+ * The store of the message.
+ * <P>Type: TEXT</P>
+ */
+ public static final String STORE = "store";
+
+ /**
+ * The mm-state of the message.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String MM_STATE = "mm_st";
+
+ /**
+ * The mm-flags-token of the message.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String MM_FLAGS_TOKEN = "mm_flg_tok";
+
+ /**
+ * The mm-flags of the message.
+ * <P>Type: TEXT</P>
+ */
+ public static final String MM_FLAGS = "mm_flg";
+
+ /**
+ * The store-status of the message.
+ * <P>Type: TEXT</P>
+ */
+ public static final String STORE_STATUS = "store_st";
+
+ /**
+ * The store-status-text of the message.
+ * <P>Type: TEXT</P>
+ */
+ public static final String STORE_STATUS_TEXT = "store_st_txt";
+
+ /**
+ * The stored of the message.
+ * <P>Type: TEXT</P>
+ */
+ public static final String STORED = "stored";
+
+ /**
+ * The totals of the message.
+ * <P>Type: TEXT</P>
+ */
+ public static final String TOTALS = "totals";
+
+ /**
+ * The mbox-totals of the message.
+ * <P>Type: TEXT</P>
+ */
+ public static final String MBOX_TOTALS = "mb_t";
+
+ /**
+ * The mbox-totals-token of the message.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String MBOX_TOTALS_TOKEN = "mb_t_tok";
+
+ /**
+ * The quotas of the message.
+ * <P>Type: TEXT</P>
+ */
+ public static final String QUOTAS = "qt";
+
+ /**
+ * The mbox-quotas of the message.
+ * <P>Type: TEXT</P>
+ */
+ public static final String MBOX_QUOTAS = "mb_qt";
+
+ /**
+ * The mbox-quotas-token of the message.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String MBOX_QUOTAS_TOKEN = "mb_qt_tok";
+
+ /**
+ * The message-count of the message.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String MESSAGE_COUNT = "m_cnt";
+
+ /**
+ * The start of the message.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String START = "start";
+
+ /**
+ * The distribution-indicator of the message.
+ * <P>Type: TEXT</P>
+ */
+ public static final String DISTRIBUTION_INDICATOR = "d_ind";
+
+ /**
+ * The element-descriptor of the message.
+ * <P>Type: TEXT</P>
+ */
+ public static final String ELEMENT_DESCRIPTOR = "e_des";
+
+ /**
+ * The limit of the message.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String LIMIT = "limit";
+
+ /**
+ * The recommended-retrieval-mode of the message.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String RECOMMENDED_RETRIEVAL_MODE = "r_r_mod";
+
+ /**
+ * The recommended-retrieval-mode-text of the message.
+ * <P>Type: TEXT</P>
+ */
+ public static final String RECOMMENDED_RETRIEVAL_MODE_TEXT = "r_r_mod_txt";
+
+ /**
+ * The status-text of the message.
+ * <P>Type: TEXT</P>
+ */
+ public static final String STATUS_TEXT = "st_txt";
+
+ /**
+ * The applic-id of the message.
+ * <P>Type: TEXT</P>
+ */
+ public static final String APPLIC_ID = "apl_id";
+
+ /**
+ * The reply-applic-id of the message.
+ * <P>Type: TEXT</P>
+ */
+ public static final String REPLY_APPLIC_ID = "r_apl_id";
+
+ /**
+ * The aux-applic-id of the message.
+ * <P>Type: TEXT</P>
+ */
+ public static final String AUX_APPLIC_ID = "aux_apl_id";
+
+ /**
+ * The drm-content of the message.
+ * <P>Type: TEXT</P>
+ */
+ public static final String DRM_CONTENT = "drm_c";
+
+ /**
+ * The adaptation-allowed of the message.
+ * <P>Type: TEXT</P>
+ */
+ public static final String ADAPTATION_ALLOWED = "adp_a";
+
+ /**
+ * The replace-id of the message.
+ * <P>Type: TEXT</P>
+ */
+ public static final String REPLACE_ID = "repl_id";
+
+ /**
+ * The cancel-id of the message.
+ * <P>Type: TEXT</P>
+ */
+ public static final String CANCEL_ID = "cl_id";
+
+ /**
+ * The cancel-status of the message.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String CANCEL_STATUS = "cl_st";
+
+ /**
+ * The thread ID of the message
+ * <P>Type: INTEGER</P>
+ */
+ public static final String THREAD_ID = "thread_id";
+ }
+
+ /**
+ * Columns for the "canonical_addresses" table used by MMS and
+ * SMS."
+ */
+ public interface CanonicalAddressesColumns extends BaseColumns {
+ /**
+ * An address used in MMS or SMS. Email addresses are
+ * converted to lower case and are compared by string
+ * equality. Other addresses are compared using
+ * PHONE_NUMBERS_EQUAL.
+ * <P>Type: TEXT</P>
+ */
+ public static final String ADDRESS = "address";
+ }
+
+ /**
+ * Columns for the "threads" table used by MMS and SMS.
+ */
+ public interface ThreadsColumns extends BaseColumns {
+ /**
+ * The date at which the thread was created.
+ *
+ * <P>Type: INTEGER (long)</P>
+ */
+ public static final String DATE = "date";
+
+ /**
+ * A string encoding of the recipient IDs of the recipients of
+ * the message, in numerical order and separated by spaces.
+ * <P>Type: TEXT</P>
+ */
+ public static final String RECIPIENT_IDS = "recipient_ids";
+
+ /**
+ * The message count of the thread.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String MESSAGE_COUNT = "message_count";
+ /**
+ * Indicates whether all messages of the thread have been read.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String READ = "read";
+ /**
+ * The snippet of the latest message in the thread.
+ * <P>Type: TEXT</P>
+ */
+ public static final String SNIPPET = "snippet";
+ /**
+ * The charset of the snippet.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String SNIPPET_CHARSET = "snippet_cs";
+ /**
+ * Type of the thread, either Threads.COMMON_THREAD or
+ * Threads.BROADCAST_THREAD.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String TYPE = "type";
+ /**
+ * Indicates whether there is a transmission error in the thread.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String ERROR = "error";
+ }
+
+ /**
+ * Helper functions for the "threads" table used by MMS and SMS.
+ */
+ public static final class Threads implements ThreadsColumns {
+ private static final String[] ID_PROJECTION = { BaseColumns._ID };
+ private static final String STANDARD_ENCODING = "UTF-8";
+ private static final Uri THREAD_ID_CONTENT_URI = Uri.parse(
+ "content://mms-sms/threadID");
+ public static final Uri CONTENT_URI = Uri.withAppendedPath(
+ MmsSms.CONTENT_URI, "conversations");
+ public static final Uri OBSOLETE_THREADS_URI = Uri.withAppendedPath(
+ CONTENT_URI, "obsolete");
+
+ public static final int COMMON_THREAD = 0;
+ public static final int BROADCAST_THREAD = 1;
+
+ // No one should construct an instance of this class.
+ private Threads() {
+ }
+
+ /**
+ * This is a single-recipient version of
+ * getOrCreateThreadId. It's convenient for use with SMS
+ * messages.
+ */
+ public static long getOrCreateThreadId(Context context, String recipient) {
+ Set<String> recipients = new HashSet<String>();
+
+ recipients.add(recipient);
+ return getOrCreateThreadId(context, recipients);
+ }
+
+ /**
+ * Given the recipients list and subject of an unsaved message,
+ * return its thread ID. If the message starts a new thread,
+ * allocate a new thread ID. Otherwise, use the appropriate
+ * existing thread ID.
+ *
+ * Find the thread ID of the same set of recipients (in
+ * any order, without any additions). If one
+ * is found, return it. Otherwise, return a unique thread ID.
+ */
+ public static long getOrCreateThreadId(
+ Context context, Set<String> recipients) {
+ Uri.Builder uriBuilder = THREAD_ID_CONTENT_URI.buildUpon();
+
+ for (String recipient : recipients) {
+ if (Mms.isEmailAddress(recipient)) {
+ recipient = Mms.extractAddrSpec(recipient);
+ }
+
+ uriBuilder.appendQueryParameter("recipient", recipient);
+ }
+
+ Cursor cursor = SqliteWrapper.query(context, context.getContentResolver(),
+ uriBuilder.build(), ID_PROJECTION, null, null, null);
+ if (cursor != null) {
+ try {
+ if (cursor.moveToFirst()) {
+ return cursor.getLong(0);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ throw new IllegalArgumentException("Unable to find or allocate a thread ID.");
+ }
+ }
+
+ /**
+ * Contains all MMS messages.
+ */
+ public static final class Mms implements BaseMmsColumns {
+ /**
+ * The content:// style URL for this table
+ */
+ public static final Uri CONTENT_URI = Uri.parse("content://mms");
+
+ public static final Uri REPORT_REQUEST_URI = Uri.withAppendedPath(
+ CONTENT_URI, "report-request");
+
+ public static final Uri REPORT_STATUS_URI = Uri.withAppendedPath(
+ CONTENT_URI, "report-status");
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = "date DESC";
+
+ /**
+ * mailbox = name-addr
+ * name-addr = [display-name] angle-addr
+ * angle-addr = [CFWS] "<" addr-spec ">" [CFWS]
+ */
+ private static final Pattern NAME_ADDR_EMAIL_PATTERN =
+ Pattern.compile("\\s*(\"[^\"]*\"|[^<>\"]+)\\s*<([^<>]+)>\\s*");
+
+ /**
+ * quoted-string = [CFWS]
+ * DQUOTE *([FWS] qcontent) [FWS] DQUOTE
+ * [CFWS]
+ */
+ private static final Pattern QUOTED_STRING_PATTERN =
+ Pattern.compile("\\s*\"([^\"]*)\"\\s*");
+
+ public static final Cursor query(
+ ContentResolver cr, String[] projection) {
+ return cr.query(CONTENT_URI, projection, null, null, DEFAULT_SORT_ORDER);
+ }
+
+ public static final Cursor query(
+ ContentResolver cr, String[] projection,
+ String where, String orderBy) {
+ return cr.query(CONTENT_URI, projection,
+ where, null, orderBy == null ? DEFAULT_SORT_ORDER : orderBy);
+ }
+
+ public static final String getMessageBoxName(int msgBox) {
+ switch (msgBox) {
+ case MESSAGE_BOX_ALL:
+ return "all";
+ case MESSAGE_BOX_INBOX:
+ return "inbox";
+ case MESSAGE_BOX_SENT:
+ return "sent";
+ case MESSAGE_BOX_DRAFTS:
+ return "drafts";
+ case MESSAGE_BOX_OUTBOX:
+ return "outbox";
+ default:
+ throw new IllegalArgumentException("Invalid message box: " + msgBox);
+ }
+ }
+
+ public static String extractAddrSpec(String address) {
+ Matcher match = NAME_ADDR_EMAIL_PATTERN.matcher(address);
+
+ if (match.matches()) {
+ return match.group(2);
+ }
+ return address;
+ }
+
+ /**
+ * Returns true if the address is an email address
+ *
+ * @param address the input address to be tested
+ * @return true if address is an email address
+ */
+ public static boolean isEmailAddress(String address) {
+ if (TextUtils.isEmpty(address)) {
+ return false;
+ }
+
+ String s = extractAddrSpec(address);
+ Matcher match = Regex.EMAIL_ADDRESS_PATTERN.matcher(s);
+ return match.matches();
+ }
+
+ /**
+ * Formats an address for displaying, doing a phone number lookup in the
+ * Address Book, etc.
+ *
+ * @param context the context to use
+ * @param address the address to format
+ * @return a nicely formatted version of the sender to display
+ */
+ public static String getDisplayAddress(Context context, String address) {
+ if (address == null) {
+ return "";
+ }
+
+ String localNumber = TelephonyManager.getDefault().getLine1Number();
+ String[] values = address.split(";");
+ String result = "";
+ for (int i = 0; i < values.length; i++) {
+ if (values[i].length() > 0) {
+ if (PhoneNumberUtils.compare(values[i], localNumber)) {
+ result = result + ";"
+ + context.getString(com.android.internal.R.string.me);
+ } else if (isEmailAddress(values[i])) {
+ result = result + ";" + getDisplayName(context, values[i]);
+ } else {
+ result = result + ";" + CallerInfo.getCallerId(context, values[i]);
+ }
+ }
+ }
+
+ if (result.length() > 0) {
+ // Skip the first ';'
+ return result.substring(1);
+ }
+ return result;
+ }
+
+ private static String getEmailDisplayName(String displayString) {
+ Matcher match = QUOTED_STRING_PATTERN.matcher(displayString);
+ if (match.matches()) {
+ return match.group(1);
+ }
+
+ return displayString;
+ }
+
+ private static String getDisplayName(Context context, String email) {
+ Matcher match = NAME_ADDR_EMAIL_PATTERN.matcher(email);
+ if (match.matches()) {
+ // email has display name
+ return getEmailDisplayName(match.group(1));
+ }
+
+ Cursor cursor = SqliteWrapper.query(context, context.getContentResolver(),
+ Contacts.ContactMethods.CONTENT_EMAIL_URI,
+ new String[] { Contacts.ContactMethods.NAME },
+ Contacts.ContactMethods.DATA + " = \'" + email + "\'",
+ null, null);
+
+ if (cursor != null) {
+ try {
+ int columnIndex = cursor.getColumnIndexOrThrow(
+ Contacts.ContactMethods.NAME);
+ while (cursor.moveToNext()) {
+ String name = cursor.getString(columnIndex);
+ if (!TextUtils.isEmpty(name)) {
+ return name;
+ }
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+ return email;
+ }
+
+ /**
+ * Contains all MMS messages in the MMS app's inbox.
+ */
+ public static final class Inbox implements BaseMmsColumns {
+ /**
+ * The content:// style URL for this table
+ */
+ public static final Uri
+ CONTENT_URI = Uri.parse("content://mms/inbox");
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = "date DESC";
+ }
+
+ /**
+ * Contains all MMS messages in the MMS app's sent box.
+ */
+ public static final class Sent implements BaseMmsColumns {
+ /**
+ * The content:// style URL for this table
+ */
+ public static final Uri
+ CONTENT_URI = Uri.parse("content://mms/sent");
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = "date DESC";
+ }
+
+ /**
+ * Contains all MMS messages in the MMS app's drafts box.
+ */
+ public static final class Draft implements BaseMmsColumns {
+ /**
+ * The content:// style URL for this table
+ */
+ public static final Uri
+ CONTENT_URI = Uri.parse("content://mms/drafts");
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = "date DESC";
+ }
+
+ /**
+ * Contains all MMS messages in the MMS app's outbox.
+ */
+ public static final class Outbox implements BaseMmsColumns {
+ /**
+ * The content:// style URL for this table
+ */
+ public static final Uri
+ CONTENT_URI = Uri.parse("content://mms/outbox");
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = "date DESC";
+ }
+
+ public static final class Addr implements BaseColumns {
+ /**
+ * The ID of MM which this address entry belongs to.
+ */
+ public static final String MSG_ID = "msg_id";
+
+ /**
+ * The ID of contact entry in Phone Book.
+ */
+ public static final String CONTACT_ID = "contact_id";
+
+ /**
+ * The address text.
+ */
+ public static final String ADDRESS = "address";
+
+ /**
+ * Type of address, must be one of PduHeaders.BCC,
+ * PduHeaders.CC, PduHeaders.FROM, PduHeaders.TO.
+ */
+ public static final String TYPE = "type";
+
+ /**
+ * Character set of this entry.
+ */
+ public static final String CHARSET = "charset";
+ }
+
+ public static final class Part implements BaseColumns {
+ /**
+ * The identifier of the message which this part belongs to.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String MSG_ID = "mid";
+
+ /**
+ * The order of the part.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String SEQ = "seq";
+
+ /**
+ * The content type of the part.
+ * <P>Type: TEXT</P>
+ */
+ public static final String CONTENT_TYPE = "ct";
+
+ /**
+ * The name of the part.
+ * <P>Type: TEXT</P>
+ */
+ public static final String NAME = "name";
+
+ /**
+ * The charset of the part.
+ * <P>Type: TEXT</P>
+ */
+ public static final String CHARSET = "chset";
+
+ /**
+ * The file name of the part.
+ * <P>Type: TEXT</P>
+ */
+ public static final String FILENAME = "fn";
+
+ /**
+ * The content disposition of the part.
+ * <P>Type: TEXT</P>
+ */
+ public static final String CONTENT_DISPOSITION = "cd";
+
+ /**
+ * The content ID of the part.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String CONTENT_ID = "cid";
+
+ /**
+ * The content location of the part.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String CONTENT_LOCATION = "cl";
+
+ /**
+ * The start of content-type of the message.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String CT_START = "ctt_s";
+
+ /**
+ * The type of content-type of the message.
+ * <P>Type: TEXT</P>
+ */
+ public static final String CT_TYPE = "ctt_t";
+
+ /**
+ * The location(on filesystem) of the binary data of the part.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String _DATA = "_data";
+
+ }
+
+ public static final class Rate {
+ public static final Uri CONTENT_URI = Uri.withAppendedPath(
+ Mms.CONTENT_URI, "rate");
+ /**
+ * When a message was successfully sent.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String SENT_TIME = "sent_time";
+ }
+
+ public static final class Intents {
+ private Intents() {
+ // Non-instantiatable.
+ }
+
+ /**
+ * The extra field to store the contents of the Intent,
+ * which should be an array of Uri.
+ */
+ public static final String EXTRA_CONTENTS = "contents";
+ /**
+ * The extra field to store the type of the contents,
+ * which should be an array of String.
+ */
+ public static final String EXTRA_TYPES = "types";
+ /**
+ * The extra field to store the 'Cc' addresses.
+ */
+ public static final String EXTRA_CC = "cc";
+ /**
+ * The extra field to store the 'Bcc' addresses;
+ */
+ public static final String EXTRA_BCC = "bcc";
+ /**
+ * The extra field to store the 'Subject'.
+ */
+ public static final String EXTRA_SUBJECT = "subject";
+ /**
+ * Indicates that the contents of specified URIs were changed.
+ * The application which is showing or caching these contents
+ * should be updated.
+ */
+ public static final String
+ CONTENT_CHANGED_ACTION = "android.intent.action.CONTENT_CHANGED";
+ /**
+ * An extra field which stores the URI of deleted contents.
+ */
+ public static final String DELETED_CONTENTS = "deleted_contents";
+ }
+ }
+
+ /**
+ * Contains all MMS and SMS messages.
+ */
+ public static final class MmsSms implements BaseColumns {
+ /**
+ * The column to distinguish SMS &amp; MMS messages in query results.
+ */
+ public static final String TYPE_DISCRIMINATOR_COLUMN =
+ "transport_type";
+
+ public static final Uri CONTENT_URI = Uri.parse("content://mms-sms/");
+
+ public static final Uri CONTENT_CONVERSATIONS_URI = Uri.parse(
+ "content://mms-sms/conversations");
+
+ public static final Uri CONTENT_FILTER_BYPHONE_URI = Uri.parse(
+ "content://mms-sms/messages/byphone");
+
+ public static final Uri CONTENT_UNDELIVERED_URI = Uri.parse(
+ "content://mms-sms/undelivered");
+
+ public static final Uri CONTENT_DRAFT_URI = Uri.parse(
+ "content://mms-sms/draft");
+
+ // Constants for message protocol types.
+ public static final int SMS_PROTO = 0;
+ public static final int MMS_PROTO = 1;
+
+ // Constants for error types of pending messages.
+ public static final int NO_ERROR = 0;
+ public static final int ERR_TYPE_GENERIC = 1;
+ public static final int ERR_TYPE_SMS_PROTO_TRANSIENT = 2;
+ public static final int ERR_TYPE_MMS_PROTO_TRANSIENT = 3;
+ public static final int ERR_TYPE_TRANSPORT_FAILURE = 4;
+ public static final int ERR_TYPE_GENERIC_PERMANENT = 10;
+ public static final int ERR_TYPE_SMS_PROTO_PERMANENT = 11;
+ public static final int ERR_TYPE_MMS_PROTO_PERMANENT = 12;
+
+ public static final class PendingMessages implements BaseColumns {
+ public static final Uri CONTENT_URI = Uri.withAppendedPath(
+ MmsSms.CONTENT_URI, "pending");
+ /**
+ * The type of transport protocol(MMS or SMS).
+ * <P>Type: INTEGER</P>
+ */
+ public static final String PROTO_TYPE = "proto_type";
+ /**
+ * The ID of the message to be sent or downloaded.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String MSG_ID = "msg_id";
+ /**
+ * The type of the message to be sent or downloaded.
+ * This field is only valid for MM. For SM, its value is always
+ * set to 0.
+ */
+ public static final String MSG_TYPE = "msg_type";
+ /**
+ * The type of the error code.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String ERROR_TYPE = "err_type";
+ /**
+ * The error code of sending/retrieving process.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String ERROR_CODE = "err_code";
+ /**
+ * How many times we tried to send or download the message.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String RETRY_INDEX = "retry_index";
+ /**
+ * The time to do next retry.
+ */
+ public static final String DUE_TIME = "due_time";
+ /**
+ * The time we last tried to send or download the message.
+ */
+ public static final String LAST_TRY = "last_try";
+ }
+ }
+
+ public static final class Carriers implements BaseColumns {
+ /**
+ * The content:// style URL for this table
+ */
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://telephony/carriers");
+
+ /**
+ * The default sort order for this table
+ */
+ public static final String DEFAULT_SORT_ORDER = "name ASC";
+
+ public static final String NAME = "name";
+
+ public static final String APN = "apn";
+
+ public static final String PROXY = "proxy";
+
+ public static final String PORT = "port";
+
+ public static final String MMSPROXY = "mmsproxy";
+
+ public static final String MMSPORT = "mmsport";
+
+ public static final String SERVER = "server";
+
+ public static final String USER = "user";
+
+ public static final String PASSWORD = "password";
+
+ public static final String MMSC = "mmsc";
+
+ public static final String MCC = "mcc";
+
+ public static final String MNC = "mnc";
+
+ public static final String NUMERIC = "numeric";
+
+ public static final String TYPE = "type";
+
+ }
+
+ public static final class Intents {
+ private Intents() {
+ // Not instantiable
+ }
+
+ /**
+ * Broadcast Action: A "secret code" has been entered in the dialer. Secret codes are
+ * of the form *#*#<code>#*#*. The intent will have the data URI:</p>
+ *
+ * <p><code>android_secret_code://&lt;code&gt;</code></p>
+ */
+ public static final String SECRET_CODE_ACTION =
+ "android.provider.Telephony.SECRET_CODE";
+
+ /**
+ * Broadcast Action: The Service Provider string(s) have been updated. Activities or
+ * services that use these strings should update their display.
+ * The intent will have the following extra values:</p>
+ * <ul>
+ * <li><em>showPlmn</em> - Boolean that indicates whether the PLMN should be shown.</li>
+ * <li><em>plmn</em> - The operator name of the registered network, as a string.</li>
+ * <li><em>showSpn</em> - Boolean that indicates whether the SPN should be shown.</li>
+ * <li><em>spn</em> - The service provider name, as a string.</li>
+ * </ul>
+ * Note that <em>showPlmn</em> may indicate that <em>plmn</em> should be displayed, even
+ * though the value for <em>plmn</em> is null. This can happen, for example, if the phone
+ * has not registered to a network yet. In this case the receiver may substitute an
+ * appropriate placeholder string (eg, "No service").
+ *
+ * It is recommended to display <em>plmn</em> before / above <em>spn</em> if
+ * both are displayed.
+ */
+ public static final String SPN_STRINGS_UPDATED_ACTION =
+ "android.provider.Telephony.SPN_STRINGS_UPDATED";
+
+ public static final String EXTRA_SHOW_PLMN = "showPlmn";
+ public static final String EXTRA_PLMN = "plmn";
+ public static final String EXTRA_SHOW_SPN = "showSpn";
+ public static final String EXTRA_SPN = "spn";
+ }
+}
+
+
diff --git a/core/java/android/provider/package.html b/core/java/android/provider/package.html
new file mode 100644
index 0000000..a553592
--- /dev/null
+++ b/core/java/android/provider/package.html
@@ -0,0 +1,11 @@
+<HTML>
+<BODY>
+Provides convenience classes to access the content providers supplied by
+Android.
+<p>Android ships with a number of content providers that store common data such
+as contact informations, calendar information, and media files. These classes
+provide simplified methods of adding or retrieving data from these content
+providers. For information about how to use a content provider, see <a
+href="{@docRoot}devel/data.html">Reading and Writing Persistent Data</a>.
+</BODY>
+</HTML>
diff --git a/core/java/android/security/Md5MessageDigest.java b/core/java/android/security/Md5MessageDigest.java
new file mode 100644
index 0000000..a7221ae
--- /dev/null
+++ b/core/java/android/security/Md5MessageDigest.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security;
+
+/**
+ * This is a temporary class to provide SHA-1 hash.
+ * It's not meant to be correct, and eventually doesn't belong in java.security
+ */
+public class Md5MessageDigest extends MessageDigest
+{
+ // ptr to native context
+ private int mNativeMd5Context;
+
+ public Md5MessageDigest()
+ {
+ init();
+ }
+
+ public byte[] digest(byte[] input)
+ {
+ update(input);
+ return digest();
+ }
+
+ private native void init();
+ public native void update(byte[] input);
+ public native byte[] digest();
+ native public void reset();
+}
diff --git a/core/java/android/security/MessageDigest.java b/core/java/android/security/MessageDigest.java
new file mode 100644
index 0000000..93040b9
--- /dev/null
+++ b/core/java/android/security/MessageDigest.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security;
+
+import java.security.NoSuchAlgorithmException;
+
+public abstract class MessageDigest
+{
+ public static MessageDigest getInstance(String algorithm)
+ throws NoSuchAlgorithmException
+ {
+ if (algorithm == null) {
+ return null;
+ }
+
+ if (algorithm.equals("SHA-1")) {
+ return new Sha1MessageDigest();
+ }
+ else if (algorithm.equals("MD5")) {
+ return new Md5MessageDigest();
+ }
+
+ throw new NoSuchAlgorithmException();
+ }
+
+ public abstract void update(byte[] input);
+ public abstract byte[] digest();
+ public abstract byte[] digest(byte[] input);
+}
diff --git a/core/java/android/security/Sha1MessageDigest.java b/core/java/android/security/Sha1MessageDigest.java
new file mode 100644
index 0000000..3b3fd6a
--- /dev/null
+++ b/core/java/android/security/Sha1MessageDigest.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security;
+
+/**
+ * This is a temporary class to provide SHA-1 hash.
+ * It's not meant to be correct, and eventually doesn't belong in java.security
+ */
+public class Sha1MessageDigest extends MessageDigest
+{
+ // ptr to native context
+ private int mNativeSha1Context;
+
+ public Sha1MessageDigest()
+ {
+ init();
+ }
+
+ public byte[] digest(byte[] input)
+ {
+ update(input);
+ return digest();
+ }
+
+ private native void init();
+ public native void update(byte[] input);
+ public native byte[] digest();
+ native public void reset();
+}
diff --git a/core/java/android/security/package.html b/core/java/android/security/package.html
new file mode 100644
index 0000000..26b8a32
--- /dev/null
+++ b/core/java/android/security/package.html
@@ -0,0 +1,5 @@
+<HTML>
+<BODY>
+{@hide}
+</BODY>
+</HTML> \ No newline at end of file
diff --git a/core/java/android/server/BluetoothDeviceService.java b/core/java/android/server/BluetoothDeviceService.java
new file mode 100644
index 0000000..10f9f7c
--- /dev/null
+++ b/core/java/android/server/BluetoothDeviceService.java
@@ -0,0 +1,1175 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * TODO: Move this to
+ * java/services/com/android/server/BluetoothDeviceService.java
+ * and make the contructor package private again.
+ *
+ * @hide
+ */
+
+package android.server;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHeadset; // just for dump()
+import android.bluetooth.BluetoothIntent;
+import android.bluetooth.IBluetoothDevice;
+import android.bluetooth.IBluetoothDeviceCallback;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.os.RemoteException;
+import android.provider.Settings;
+import android.util.Log;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.Message;
+import android.os.SystemService;
+
+import java.io.IOException;
+import java.io.FileDescriptor;
+import java.io.FileNotFoundException;
+import java.io.FileWriter;
+import java.io.PrintWriter;
+import java.io.UnsupportedEncodingException;
+import java.util.HashMap;
+
+public class BluetoothDeviceService extends IBluetoothDevice.Stub {
+ private static final String TAG = "BluetoothDeviceService";
+ private int mNativeData;
+ private Context mContext;
+ private BluetoothEventLoop mEventLoop;
+ private IntentFilter mIntentFilter;
+ private boolean mIsAirplaneSensitive;
+ private volatile boolean mIsEnabled; // local cache of isEnabledNative()
+ private boolean mIsDiscovering;
+
+ static {
+ classInitNative();
+ }
+ private native static void classInitNative();
+
+ public BluetoothDeviceService(Context context) {
+ mContext = context;
+ }
+
+ /** Must be called after construction, and before any other method.
+ */
+ public synchronized void init() {
+ initializeNativeDataNative();
+ mIsEnabled = (isEnabledNative() == 1);
+ mIsDiscovering = false;
+ mEventLoop = new BluetoothEventLoop(mContext, this);
+ registerForAirplaneMode();
+
+ disableEsco(); // TODO: enable eSCO support once its fully supported
+ }
+ private native void initializeNativeDataNative();
+
+ @Override
+ protected void finalize() throws Throwable {
+ if (mIsAirplaneSensitive) {
+ mContext.unregisterReceiver(mReceiver);
+ }
+ try {
+ cleanupNativeDataNative();
+ } finally {
+ super.finalize();
+ }
+ }
+ private native void cleanupNativeDataNative();
+
+ public boolean isEnabled() {
+ checkPermissionBluetooth();
+ return mIsEnabled;
+ }
+ private native int isEnabledNative();
+
+ /**
+ * Disable bluetooth. Returns true on success.
+ */
+ public synchronized boolean disable() {
+ checkPermissionBluetoothAdmin();
+
+ if (mEnableThread != null && mEnableThread.isAlive()) {
+ return false;
+ }
+ if (!mIsEnabled) {
+ return true;
+ }
+ mEventLoop.stop();
+ disableNative();
+ mIsEnabled = false;
+ mIsDiscovering = false;
+ Intent intent = new Intent(BluetoothIntent.DISABLED_ACTION);
+ mContext.sendBroadcast(intent);
+ return true;
+ }
+
+ /**
+ * Enable this Bluetooth device, asynchronously.
+ * This turns on/off the underlying hardware.
+ *
+ * @return True on success (so far), guarenteeing the callback with be
+ * notified when complete.
+ */
+ public synchronized boolean enable(IBluetoothDeviceCallback callback) {
+ checkPermissionBluetoothAdmin();
+
+ // Airplane mode can prevent Bluetooth radio from being turned on.
+ if (mIsAirplaneSensitive && isAirplaneModeOn()) {
+ return false;
+ }
+ if (mIsEnabled) {
+ return false;
+ }
+ if (mEnableThread != null && mEnableThread.isAlive()) {
+ return false;
+ }
+ mEnableThread = new EnableThread(callback);
+ mEnableThread.start();
+ return true;
+ }
+
+ private static final int REGISTER_SDP_RECORDS = 1;
+ private final Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case REGISTER_SDP_RECORDS:
+ //TODO: Don't assume HSP/HFP is running, don't use sdptool,
+ if (isEnabled()) {
+ SystemService.start("hsag");
+ SystemService.start("hfag");
+ }
+ }
+ }
+ };
+
+ private EnableThread mEnableThread;
+ private class EnableThread extends Thread {
+ private final IBluetoothDeviceCallback mEnableCallback;
+ public EnableThread(IBluetoothDeviceCallback callback) {
+ mEnableCallback = callback;
+ }
+ public void run() {
+ boolean res = (enableNative() == 0);
+ if (res) {
+ mEventLoop.start();
+ }
+
+ if (mEnableCallback != null) {
+ try {
+ mEnableCallback.onEnableResult(res ?
+ BluetoothDevice.RESULT_SUCCESS :
+ BluetoothDevice.RESULT_FAILURE);
+ } catch (RemoteException e) {}
+ }
+
+ if (res) {
+ mIsEnabled = true;
+ mIsDiscovering = false;
+ Intent intent = new Intent(BluetoothIntent.ENABLED_ACTION);
+ mContext.sendBroadcast(intent);
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(REGISTER_SDP_RECORDS), 3000);
+ }
+ mEnableThread = null;
+ }
+ };
+
+ private native int enableNative();
+ private native int disableNative();
+
+ public synchronized String getAddress() {
+ checkPermissionBluetooth();
+ return getAddressNative();
+ }
+ private native String getAddressNative();
+
+ public synchronized String getName() {
+ checkPermissionBluetooth();
+ return getNameNative();
+ }
+ private native String getNameNative();
+
+ public synchronized boolean setName(String name) {
+ checkPermissionBluetoothAdmin();
+ if (name == null) {
+ return false;
+ }
+ // hcid handles persistance of the bluetooth name
+ return setNameNative(name);
+ }
+ private native boolean setNameNative(String name);
+
+ public synchronized String[] listBondings() {
+ checkPermissionBluetooth();
+ return listBondingsNative();
+ }
+ private native String[] listBondingsNative();
+
+ public synchronized String getMajorClass() {
+ checkPermissionBluetooth();
+ return getMajorClassNative();
+ }
+ private native String getMajorClassNative();
+
+ public synchronized String getMinorClass() {
+ checkPermissionBluetooth();
+ return getMinorClassNative();
+ }
+ private native String getMinorClassNative();
+
+ /**
+ * Returns the user-friendly name of a remote device. This value is
+ * retrned from our local cache, which is updated during device discovery.
+ * Do not expect to retrieve the updated remote name immediately after
+ * changing the name on the remote device.
+ *
+ * @param address Bluetooth address of remote device.
+ *
+ * @return The user-friendly name of the specified remote device.
+ */
+ public synchronized String getRemoteName(String address) {
+ checkPermissionBluetooth();
+ if (!BluetoothDevice.checkBluetoothAddress(address)) {
+ return null;
+ }
+ return getRemoteNameNative(address);
+ }
+ private native String getRemoteNameNative(String address);
+
+ /* pacakge */ native String getAdapterPathNative();
+
+ /**
+ * Initiate a remote-device-discovery procedure. This procedure may be
+ * canceled by calling {@link #stopDiscovery}. Remote-device discoveries
+ * are returned as intents
+ * <p>
+ * Typically, when a remote device is found, your
+ * android.bluetooth.DiscoveryEventNotifier#notifyRemoteDeviceFound
+ * method will be invoked, and subsequently, your
+ * android.bluetooth.RemoteDeviceEventNotifier#notifyRemoteNameUpdated
+ * will tell you the user-friendly name of the remote device. However,
+ * it is possible that the name update may fail for various reasons, so you
+ * should display the device's Bluetooth address as soon as you get a
+ * notifyRemoteDeviceFound event, and update the name when you get the
+ * remote name.
+ *
+ * @return true if discovery has started,
+ * false otherwise.
+ */
+ public synchronized boolean startDiscovery(boolean resolveNames) {
+ checkPermissionBluetoothAdmin();
+ return startDiscoveryNative(resolveNames);
+ }
+ private native boolean startDiscoveryNative(boolean resolveNames);
+
+ /**
+ * Cancel a remote-device discovery.
+ *
+ * Note: you may safely call this method even when discovery has not been
+ * started.
+ */
+ public synchronized boolean cancelDiscovery() {
+ checkPermissionBluetoothAdmin();
+ return cancelDiscoveryNative();
+ }
+ private native boolean cancelDiscoveryNative();
+
+ public synchronized boolean isDiscovering() {
+ checkPermissionBluetooth();
+ return mIsDiscovering;
+ }
+
+ /* package */ void setIsDiscovering(boolean isDiscovering) {
+ mIsDiscovering = isDiscovering;
+ }
+
+ public synchronized boolean startPeriodicDiscovery() {
+ checkPermissionBluetoothAdmin();
+ return startPeriodicDiscoveryNative();
+ }
+ private native boolean startPeriodicDiscoveryNative();
+
+ public synchronized boolean stopPeriodicDiscovery() {
+ checkPermissionBluetoothAdmin();
+ return stopPeriodicDiscoveryNative();
+ }
+ private native boolean stopPeriodicDiscoveryNative();
+
+ public synchronized boolean isPeriodicDiscovery() {
+ checkPermissionBluetooth();
+ return isPeriodicDiscoveryNative();
+ }
+ private native boolean isPeriodicDiscoveryNative();
+
+ /**
+ * Set the discoverability window for the device. A timeout of zero
+ * makes the device permanently discoverable (if the device is
+ * discoverable). Setting the timeout to a nonzero value does not make
+ * a device discoverable; you need to call setMode() to make the device
+ * explicitly discoverable.
+ *
+ * @param timeout_s The discoverable timeout in seconds.
+ */
+ public synchronized boolean setDiscoverableTimeout(int timeout) {
+ checkPermissionBluetoothAdmin();
+ return setDiscoverableTimeoutNative(timeout);
+ }
+ private native boolean setDiscoverableTimeoutNative(int timeout_s);
+
+ /**
+ * Get the discoverability window for the device. A timeout of zero
+ * means that the device is permanently discoverable (if the device is
+ * in the discoverable mode).
+ *
+ * @return The discoverability window of the device, in seconds. A negative
+ * value indicates an error.
+ */
+ public synchronized int getDiscoverableTimeout() {
+ checkPermissionBluetooth();
+ return getDiscoverableTimeoutNative();
+ }
+ private native int getDiscoverableTimeoutNative();
+
+ public synchronized boolean isAclConnected(String address) {
+ checkPermissionBluetooth();
+ if (!BluetoothDevice.checkBluetoothAddress(address)) {
+ return false;
+ }
+ return isConnectedNative(address);
+ }
+ private native boolean isConnectedNative(String address);
+
+ /**
+ * Detetermines whether this device is connectable (that is, whether remote
+ * devices can connect to it.)
+ * <p>
+ * Note: A Bluetooth adapter has separate connectable and discoverable
+ * states, and you could have any combination of those. Although
+ * any combination is possible (such as discoverable but not
+ * connectable), we restrict the possible combinations to one of
+ * three possibilities: discoverable and connectable, connectable
+ * but not discoverable, and neither connectable nor discoverable.
+ *
+ * @return true if this adapter is connectable
+ * false otherwise
+ *
+ * @see #isDiscoverable
+ * @see #getMode
+ * @see #setMode
+ */
+ public synchronized boolean isConnectable() {
+ checkPermissionBluetooth();
+ return isConnectableNative();
+ }
+ private native boolean isConnectableNative();
+
+ /**
+ * Detetermines whether this device is discoverable.
+ *
+ * Note: a Bluetooth adapter has separate connectable and discoverable
+ * states, and you could have any combination of those. Although
+ * any combination is possible (such as discoverable but not
+ * connectable), we restrict the possible combinations to one of
+ * three possibilities: discoverable and connectable, connectable
+ * but not discoverable, and neither connectable nor discoverable.
+ *
+ * @return true if this adapter is discoverable
+ * false otherwise
+ *
+ * @see #isConnectable
+ * @see #getMode
+ * @see #setMode
+ */
+ public synchronized boolean isDiscoverable() {
+ checkPermissionBluetooth();
+ return isDiscoverableNative();
+ }
+ private native boolean isDiscoverableNative();
+
+ /**
+ * Determines which one of three modes this adapter is in: discoverable and
+ * connectable, not discoverable but connectable, or neither.
+ *
+ * @return Mode enumeration containing the current mode.
+ *
+ * @see #setMode
+ */
+ public synchronized int getMode() {
+ checkPermissionBluetooth();
+ String mode = getModeNative();
+ if (mode == null) {
+ return BluetoothDevice.MODE_UNKNOWN;
+ }
+ if (mode.equalsIgnoreCase("off")) {
+ return BluetoothDevice.MODE_OFF;
+ }
+ else if (mode.equalsIgnoreCase("connectable")) {
+ return BluetoothDevice.MODE_CONNECTABLE;
+ }
+ else if (mode.equalsIgnoreCase("discoverable")) {
+ return BluetoothDevice.MODE_DISCOVERABLE;
+ }
+ else {
+ return BluetoothDevice.MODE_UNKNOWN;
+ }
+ }
+ private native String getModeNative();
+
+ /**
+ * Set the discoverability and connectability mode of this adapter. The
+ * possibilities are discoverable and connectable (MODE_DISCOVERABLE),
+ * connectable but not discoverable (MODE_CONNECTABLE), and neither
+ * (MODE_OFF).
+ *
+ * Note: MODE_OFF does not mean that the adapter is physically off. It
+ * may be neither discoverable nor connectable, but it could still
+ * initiate outgoing connections, or could participate in a
+ * connection initiated by a remote device before its mode was set
+ * to MODE_OFF.
+ *
+ * @param mode the new mode
+ * @see #getMode
+ */
+ public synchronized boolean setMode(int mode) {
+ checkPermissionBluetoothAdmin();
+ switch (mode) {
+ case BluetoothDevice.MODE_OFF:
+ return setModeNative("off");
+ case BluetoothDevice.MODE_CONNECTABLE:
+ return setModeNative("connectable");
+ case BluetoothDevice.MODE_DISCOVERABLE:
+ return setModeNative("discoverable");
+ }
+ return false;
+ }
+ private native boolean setModeNative(String mode);
+
+ /**
+ * Retrieves the alias of a remote device. The alias is a local feature,
+ * and allows us to associate a name with a remote device that is different
+ * from that remote device's user-friendly name. The remote device knows
+ * nothing about this. The alias can be changed with
+ * {@link #setRemoteAlias}, and it may be removed with
+ * {@link #clearRemoteAlias}
+ *
+ * @param address Bluetooth address of remote device.
+ *
+ * @return The alias of the remote device.
+ */
+ public synchronized String getRemoteAlias(String address) {
+ checkPermissionBluetooth();
+ if (!BluetoothDevice.checkBluetoothAddress(address)) {
+ return null;
+ }
+ return getRemoteAliasNative(address);
+ }
+ private native String getRemoteAliasNative(String address);
+
+ /**
+ * Changes the alias of a remote device. The alias is a local feature,
+ * from that remote device's user-friendly name. The remote device knows
+ * nothing about this. The alias can be retrieved with
+ * {@link #getRemoteAlias}, and it may be removed with
+ * {@link #clearRemoteAlias}.
+ *
+ * @param address Bluetooth address of remote device
+ * @param alias Alias for the remote device
+ */
+ public synchronized boolean setRemoteAlias(String address, String alias) {
+ checkPermissionBluetoothAdmin();
+ if (alias == null || !BluetoothDevice.checkBluetoothAddress(address)) {
+ return false;
+ }
+ return setRemoteAliasNative(address, alias);
+ }
+ private native boolean setRemoteAliasNative(String address, String alias);
+
+ /**
+ * Removes the alias of a remote device. The alias is a local feature,
+ * from that remote device's user-friendly name. The remote device knows
+ * nothing about this. The alias can be retrieved with
+ * {@link #getRemoteAlias}.
+ *
+ * @param address Bluetooth address of remote device
+ */
+ public synchronized boolean clearRemoteAlias(String address) {
+ checkPermissionBluetoothAdmin();
+ if (!BluetoothDevice.checkBluetoothAddress(address)) {
+ return false;
+ }
+ return clearRemoteAliasNative(address);
+ }
+ private native boolean clearRemoteAliasNative(String address);
+
+ public synchronized boolean disconnectRemoteDeviceAcl(String address) {
+ checkPermissionBluetoothAdmin();
+ if (!BluetoothDevice.checkBluetoothAddress(address)) {
+ return false;
+ }
+ return disconnectRemoteDeviceNative(address);
+ }
+ private native boolean disconnectRemoteDeviceNative(String address);
+
+ private static final int MAX_OUTSTANDING_ASYNC = 32;
+ /**
+ * This method initiates a Bonding request to a remote device.
+ *
+ *
+ * @param address The Bluetooth address of the remote device
+ *
+ * @see #createBonding
+ * @see #cancelBondingProcess
+ * @see #removeBonding
+ * @see #hasBonding
+ * @see #listBondings
+ *
+ * @see android.bluetooth.PasskeyAgent
+ */
+ public synchronized boolean createBonding(String address, IBluetoothDeviceCallback callback) {
+ checkPermissionBluetoothAdmin();
+ if (!BluetoothDevice.checkBluetoothAddress(address)) {
+ return false;
+ }
+
+ HashMap<String, IBluetoothDeviceCallback> callbacks = mEventLoop.getCreateBondingCallbacks();
+ if (callbacks.containsKey(address)) {
+ Log.w(TAG, "createBonding() already in progress for " + address);
+ return false;
+ }
+
+ // Protect from malicious clients - limit number of outstanding requests
+ if (callbacks.size() > MAX_OUTSTANDING_ASYNC) {
+ Log.w(TAG, "Too many outstanding bonding requests, dropping request for " + address);
+ return false;
+ }
+
+ callbacks.put(address, callback);
+ if (!createBondingNative(address, 60000 /* 1 minute */)) {
+ callbacks.remove(address);
+ return false;
+ }
+ return true;
+ }
+ private native boolean createBondingNative(String address, int timeout_ms);
+
+ /**
+ * This method cancels a pending bonding request.
+ *
+ * @param address The Bluetooth address of the remote device to which a
+ * bonding request has been initiated.
+ *
+ * Note: When a request is canceled, method
+ * {@link CreateBondingResultNotifier#notifyAuthenticationFailed}
+ * will be called on the object passed to method
+ * {@link #createBonding}.
+ *
+ * Note: it is safe to call this method when there is no outstanding
+ * bonding request.
+ *
+ * @see #createBonding
+ * @see #cancelBondingProcess
+ * @see #removeBonding
+ * @see #hasBonding
+ * @see #listBondings
+ */
+ public synchronized boolean cancelBondingProcess(String address) {
+ checkPermissionBluetoothAdmin();
+ if (!BluetoothDevice.checkBluetoothAddress(address)) {
+ return false;
+ }
+ return cancelBondingProcessNative(address);
+ }
+ private native boolean cancelBondingProcessNative(String address);
+
+ /**
+ * This method removes a bonding to a remote device. This is a local
+ * operation only, resulting in this adapter "forgetting" the bonding
+ * information about the specified remote device. The other device itself
+ * does not know what the bonding has been torn down. The next time either
+ * device attemps to connect to the other, the connection will fail, and
+ * the pairing procedure will have to be re-initiated.
+ *
+ * @param address The Bluetooth address of the remote device.
+ *
+ * @see #createBonding
+ * @see #cancelBondingProcess
+ * @see #removeBonding
+ * @see #hasBonding
+ * @see #listBondings
+ */
+ public synchronized boolean removeBonding(String address) {
+ checkPermissionBluetoothAdmin();
+ if (!BluetoothDevice.checkBluetoothAddress(address)) {
+ return false;
+ }
+ return removeBondingNative(address);
+ }
+ private native boolean removeBondingNative(String address);
+
+ public synchronized boolean hasBonding(String address) {
+ checkPermissionBluetooth();
+ if (!BluetoothDevice.checkBluetoothAddress(address)) {
+ return false;
+ }
+ return hasBondingNative(address);
+ }
+ private native boolean hasBondingNative(String address);
+
+ public synchronized String[] listAclConnections() {
+ checkPermissionBluetooth();
+ return listConnectionsNative();
+ }
+ private native String[] listConnectionsNative();
+
+ /**
+ * This method lists all remote devices that this adapter is aware of.
+ * This is a list not only of all most-recently discovered devices, but of
+ * all devices discovered by this adapter up to some point in the past.
+ * Note that many of these devices may not be in the neighborhood anymore,
+ * and attempting to connect to them will result in an error.
+ *
+ * @return An array of strings representing the Bluetooth addresses of all
+ * remote devices that this adapter is aware of.
+ */
+ public synchronized String[] listRemoteDevices() {
+ checkPermissionBluetooth();
+ return listRemoteDevicesNative();
+ }
+ private native String[] listRemoteDevicesNative();
+
+ /**
+ * Returns the version of the Bluetooth chip. This version is compiled from
+ * the LMP version. In case of EDR the features attribute must be checked.
+ * Example: "Bluetooth 2.0 + EDR".
+ *
+ * @return a String representation of the this Adapter's underlying
+ * Bluetooth-chip version.
+ */
+ public synchronized String getVersion() {
+ checkPermissionBluetooth();
+ return getVersionNative();
+ }
+ private native String getVersionNative();
+
+ /**
+ * Returns the revision of the Bluetooth chip. This is a vendor-specific
+ * value and in most cases it represents the firmware version. This might
+ * derive from the HCI revision and LMP subversion values or via extra
+ * vendord specific commands.
+ * In case the revision of a chip is not available. This method should
+ * return the LMP subversion value as a string.
+ * Example: "HCI 19.2"
+ *
+ * @return The HCI revision of this adapter.
+ */
+ public synchronized String getRevision() {
+ checkPermissionBluetooth();
+ return getRevisionNative();
+ }
+ private native String getRevisionNative();
+
+ /**
+ * Returns the manufacturer of the Bluetooth chip. If the company id is not
+ * known the sting "Company ID %d" where %d should be replaced with the
+ * numeric value from the manufacturer field.
+ * Example: "Cambridge Silicon Radio"
+ *
+ * @return Manufacturer name.
+ */
+ public synchronized String getManufacturer() {
+ checkPermissionBluetooth();
+ return getManufacturerNative();
+ }
+ private native String getManufacturerNative();
+
+ /**
+ * Returns the company name from the OUI database of the Bluetooth device
+ * address. This function will need a valid and up-to-date oui.txt from
+ * the IEEE. This value will be different from the manufacturer string in
+ * the most cases.
+ * If the oui.txt file is not present or the OUI part of the Bluetooth
+ * address is not listed, it should return the string "OUI %s" where %s is
+ * the actual OUI.
+ *
+ * Example: "Apple Computer"
+ *
+ * @return company name
+ */
+ public synchronized String getCompany() {
+ checkPermissionBluetooth();
+ return getCompanyNative();
+ }
+ private native String getCompanyNative();
+
+ /**
+ * Like getVersion(), but for a remote device.
+ *
+ * @param address The Bluetooth address of the remote device.
+ *
+ * @return remote-device Bluetooth version
+ *
+ * @see #getVersion
+ */
+ public synchronized String getRemoteVersion(String address) {
+ checkPermissionBluetooth();
+ if (!BluetoothDevice.checkBluetoothAddress(address)) {
+ return null;
+ }
+ return getRemoteVersionNative(address);
+ }
+ private native String getRemoteVersionNative(String address);
+
+ /**
+ * Like getRevision(), but for a remote device.
+ *
+ * @param address The Bluetooth address of the remote device.
+ *
+ * @return remote-device HCI revision
+ *
+ * @see #getRevision
+ */
+ public synchronized String getRemoteRevision(String address) {
+ checkPermissionBluetooth();
+ if (!BluetoothDevice.checkBluetoothAddress(address)) {
+ return null;
+ }
+ return getRemoteRevisionNative(address);
+ }
+ private native String getRemoteRevisionNative(String address);
+
+ /**
+ * Like getManufacturer(), but for a remote device.
+ *
+ * @param address The Bluetooth address of the remote device.
+ *
+ * @return remote-device Bluetooth chip manufacturer
+ *
+ * @see #getManufacturer
+ */
+ public synchronized String getRemoteManufacturer(String address) {
+ checkPermissionBluetooth();
+ if (!BluetoothDevice.checkBluetoothAddress(address)) {
+ return null;
+ }
+ return getRemoteManufacturerNative(address);
+ }
+ private native String getRemoteManufacturerNative(String address);
+
+ /**
+ * Like getCompany(), but for a remote device.
+ *
+ * @param address The Bluetooth address of the remote device.
+ *
+ * @return remote-device company
+ *
+ * @see #getCompany
+ */
+ public synchronized String getRemoteCompany(String address) {
+ checkPermissionBluetooth();
+ if (!BluetoothDevice.checkBluetoothAddress(address)) {
+ return null;
+ }
+ return getRemoteCompanyNative(address);
+ }
+ private native String getRemoteCompanyNative(String address);
+
+ /**
+ * Returns the date and time when the specified remote device has been seen
+ * by a discover procedure.
+ * Example: "2006-02-08 12:00:00 GMT"
+ *
+ * @return a String with the timestamp.
+ */
+ public synchronized String lastSeen(String address) {
+ checkPermissionBluetooth();
+ if (!BluetoothDevice.checkBluetoothAddress(address)) {
+ return null;
+ }
+ return lastSeenNative(address);
+ }
+ private native String lastSeenNative(String address);
+
+ /**
+ * Returns the date and time when the specified remote device has last been
+ * connected to
+ * Example: "2006-02-08 12:00:00 GMT"
+ *
+ * @return a String with the timestamp.
+ */
+ public synchronized String lastUsed(String address) {
+ checkPermissionBluetooth();
+ if (!BluetoothDevice.checkBluetoothAddress(address)) {
+ return null;
+ }
+ return lastUsedNative(address);
+ }
+ private native String lastUsedNative(String address);
+
+ /**
+ * Gets the major device class of the specified device.
+ * Example: "computer"
+ *
+ * Note: This is simply a string desciption of the major class of the
+ * device-class information, which is returned as a 32-bit value
+ * during device discovery.
+ *
+ * @param address The Bluetooth address of the remote device.
+ *
+ * @return remote-device major class
+ *
+ * @see #getRemoteClass
+ */
+ public synchronized String getRemoteMajorClass(String address) {
+ if (!BluetoothDevice.checkBluetoothAddress(address)) {
+ checkPermissionBluetooth();
+ return null;
+ }
+ return getRemoteMajorClassNative(address);
+ }
+ private native String getRemoteMajorClassNative(String address);
+
+ /**
+ * Gets the minor device class of the specified device.
+ * Example: "laptop"
+ *
+ * Note: This is simply a string desciption of the minor class of the
+ * device-class information, which is returned as a 32-bit value
+ * during device discovery.
+ *
+ * @param address The Bluetooth address of the remote device.
+ *
+ * @return remote-device minor class
+ *
+ * @see #getRemoteClass
+ */
+ public synchronized String getRemoteMinorClass(String address) {
+ checkPermissionBluetooth();
+ if (!BluetoothDevice.checkBluetoothAddress(address)) {
+ return null;
+ }
+ return getRemoteMinorClassNative(address);
+ }
+ private native String getRemoteMinorClassNative(String address);
+
+ /**
+ * Gets the service classes of the specified device.
+ * Example: ["networking", "object transfer"]
+ *
+ * @return a String array with the descriptions of the service classes.
+ *
+ * @see #getRemoteClass
+ */
+ public synchronized String[] getRemoteServiceClasses(String address) {
+ checkPermissionBluetooth();
+ if (!BluetoothDevice.checkBluetoothAddress(address)) {
+ return null;
+ }
+ return getRemoteServiceClassesNative(address);
+ }
+ private native String[] getRemoteServiceClassesNative(String address);
+
+ /**
+ * Gets the remote major, minor, and service classes encoded as a 32-bit
+ * integer.
+ *
+ * Note: this value is retrieved from cache, because we get it during
+ * remote-device discovery.
+ *
+ * @return 32-bit integer encoding the remote major, minor, and service
+ * classes.
+ *
+ * @see #getRemoteMajorClass
+ * @see #getRemoteMinorClass
+ * @see #getRemoteServiceClasses
+ */
+ public synchronized int getRemoteClass(String address) {
+ if (!BluetoothDevice.checkBluetoothAddress(address)) {
+ checkPermissionBluetooth();
+ return -1;
+ }
+ return getRemoteClassNative(address);
+ }
+ private native int getRemoteClassNative(String address);
+
+ /**
+ * Gets the remote features encoded as bit mask.
+ *
+ * Note: This method may be obsoleted soon.
+ *
+ * @return byte array of features.
+ */
+ public synchronized byte[] getRemoteFeatures(String address) {
+ checkPermissionBluetooth();
+ if (!BluetoothDevice.checkBluetoothAddress(address)) {
+ return null;
+ }
+ return getRemoteFeaturesNative(address);
+ }
+ private native byte[] getRemoteFeaturesNative(String address);
+
+ /**
+ * This method and {@link #getRemoteServiceRecord} query the SDP service
+ * on a remote device. They do not interpret the data, but simply return
+ * it raw to the user. To read more about SDP service handles and records,
+ * consult the Bluetooth core documentation (www.bluetooth.com).
+ *
+ * @param address Bluetooth address of remote device.
+ * @param match a String match to narrow down the service-handle search.
+ * The only supported value currently is "hsp" for the headset
+ * profile. To retrieve all service handles, simply pass an empty
+ * match string.
+ *
+ * @return all service handles corresponding to the string match.
+ *
+ * @see #getRemoteServiceRecord
+ */
+ public synchronized int[] getRemoteServiceHandles(String address, String match) {
+ checkPermissionBluetooth();
+ if (!BluetoothDevice.checkBluetoothAddress(address)) {
+ return null;
+ }
+ if (match == null) {
+ match = "";
+ }
+ return getRemoteServiceHandlesNative(address, match);
+ }
+ private native int[] getRemoteServiceHandlesNative(String address, String match);
+
+ /**
+ * This method retrieves the service records corresponding to a given
+ * service handle (method {@link #getRemoteServiceHandles} retrieves the
+ * service handles.)
+ *
+ * This method and {@link #getRemoteServiceHandles} do not interpret their
+ * data, but simply return it raw to the user. To read more about SDP
+ * service handles and records, consult the Bluetooth core documentation
+ * (www.bluetooth.com).
+ *
+ * @param address Bluetooth address of remote device.
+ * @param handle Service handle returned by {@link #getRemoteServiceHandles}
+ *
+ * @return a byte array of all service records corresponding to the
+ * specified service handle.
+ *
+ * @see #getRemoteServiceHandles
+ */
+ public synchronized byte[] getRemoteServiceRecord(String address, int handle) {
+ checkPermissionBluetooth();
+ if (!BluetoothDevice.checkBluetoothAddress(address)) {
+ return null;
+ }
+ return getRemoteServiceRecordNative(address, handle);
+ }
+ private native byte[] getRemoteServiceRecordNative(String address, int handle);
+
+ // AIDL does not yet support short's
+ public synchronized boolean getRemoteServiceChannel(String address, int uuid16,
+ IBluetoothDeviceCallback callback) {
+ checkPermissionBluetooth();
+ if (!BluetoothDevice.checkBluetoothAddress(address)) {
+ return false;
+ }
+ HashMap<String, IBluetoothDeviceCallback> callbacks =
+ mEventLoop.getRemoteServiceChannelCallbacks();
+ if (callbacks.containsKey(address)) {
+ Log.w(TAG, "SDP request already in progress for " + address);
+ return false;
+ }
+ // Protect from malicious clients - only allow 32 bonding requests per minute.
+ if (callbacks.size() > MAX_OUTSTANDING_ASYNC) {
+ Log.w(TAG, "Too many outstanding SDP requests, dropping request for " + address);
+ return false;
+ }
+ callbacks.put(address, callback);
+
+ if (!getRemoteServiceChannelNative(address, (short)uuid16)) {
+ callbacks.remove(address);
+ return false;
+ }
+ return true;
+ }
+ private native boolean getRemoteServiceChannelNative(String address, short uuid16);
+
+ public synchronized boolean setPin(String address, byte[] pin) {
+ checkPermissionBluetoothAdmin();
+ if (pin == null || pin.length <= 0 || pin.length > 16 ||
+ !BluetoothDevice.checkBluetoothAddress(address)) {
+ return false;
+ }
+ Integer data = mEventLoop.getPasskeyAgentRequestData().remove(address);
+ if (data == null) {
+ Log.w(TAG, "setPin(" + address + ") called but no native data available, " +
+ "ignoring. Maybe the PasskeyAgent Request was cancelled by the remote device" +
+ " or by bluez.\n");
+ return false;
+ }
+ // bluez API wants pin as a string
+ String pinString;
+ try {
+ pinString = new String(pin, "UTF8");
+ } catch (UnsupportedEncodingException uee) {
+ Log.e(TAG, "UTF8 not supported?!?");
+ return false;
+ }
+ return setPinNative(address, pinString, data.intValue());
+ }
+ private native boolean setPinNative(String address, String pin, int nativeData);
+
+ public synchronized boolean cancelPin(String address) {
+ checkPermissionBluetoothAdmin();
+ if (!BluetoothDevice.checkBluetoothAddress(address)) {
+ return false;
+ }
+ Integer data = mEventLoop.getPasskeyAgentRequestData().remove(address);
+ if (data == null) {
+ Log.w(TAG, "cancelPin(" + address + ") called but no native data available, " +
+ "ignoring. Maybe the PasskeyAgent Request was already cancelled by the remote " +
+ "or by bluez.\n");
+ return false;
+ }
+ return cancelPinNative(address, data.intValue());
+ }
+ private native boolean cancelPinNative(String address, int natveiData);
+
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (action.equals(Intent.ACTION_AIRPLANE_MODE_CHANGED)) {
+ ContentResolver resolver = context.getContentResolver();
+ // Query the airplane mode from Settings.System just to make sure that
+ // some random app is not sending this intent and disabling bluetooth
+ boolean enabled = !isAirplaneModeOn();
+ // If bluetooth is currently expected to be on, then enable or disable bluetooth
+ if (Settings.System.getInt(resolver, Settings.System.BLUETOOTH_ON, 0) > 0) {
+ if (enabled) {
+ enable(null);
+ } else {
+ disable();
+ }
+ }
+ }
+ }
+ };
+
+ private void registerForAirplaneMode() {
+ String airplaneModeRadios = Settings.System.getString(mContext.getContentResolver(),
+ Settings.System.AIRPLANE_MODE_RADIOS);
+ mIsAirplaneSensitive = airplaneModeRadios == null
+ ? true : airplaneModeRadios.contains(Settings.System.RADIO_BLUETOOTH);
+ if (mIsAirplaneSensitive) {
+ mIntentFilter = new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+ mContext.registerReceiver(mReceiver, mIntentFilter);
+ }
+ }
+
+ /* Returns true if airplane mode is currently on */
+ private final boolean isAirplaneModeOn() {
+ return Settings.System.getInt(mContext.getContentResolver(),
+ Settings.System.AIRPLANE_MODE_ON, 0) == 1;
+ }
+
+ private static final String BLUETOOTH_ADMIN = android.Manifest.permission.BLUETOOTH_ADMIN;
+ private static final String BLUETOOTH = android.Manifest.permission.BLUETOOTH;
+
+ private void checkPermissionBluetoothAdmin() {
+ if (mContext.checkCallingOrSelfPermission(BLUETOOTH_ADMIN) !=
+ PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("Requires BLUETOOTH_ADMIN permission");
+ }
+ }
+
+ private void checkPermissionBluetooth() {
+ if (mContext.checkCallingOrSelfPermission(BLUETOOTH_ADMIN) !=
+ PackageManager.PERMISSION_GRANTED &&
+ mContext.checkCallingOrSelfPermission(BLUETOOTH) !=
+ PackageManager.PERMISSION_GRANTED ) {
+ throw new SecurityException("Requires BLUETOOTH or BLUETOOTH_ADMIN permission");
+ }
+ }
+
+ private static final String DISABLE_ESCO_PATH = "/sys/module/sco/parameters/disable_esco";
+ private static void disableEsco() {
+ try {
+ FileWriter file = new FileWriter(DISABLE_ESCO_PATH);
+ file.write("Y");
+ file.close();
+ } catch (FileNotFoundException e) {
+ } catch (IOException e) {}
+ }
+
+ @Override
+ protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ if (mIsEnabled) {
+ pw.println("\nBluetooth ENABLED: " + getAddress() + " (" + getName() + ")");
+ pw.println("\nisDiscovering() = " + isDiscovering());
+
+ BluetoothHeadset headset = new BluetoothHeadset(mContext);
+
+ pw.println("\n--Bondings--");
+ String[] addresses = listBondings();
+ for (String address : addresses) {
+ String name = getRemoteName(address);
+ pw.println(address + " (" + name + ")");
+ }
+
+ pw.println("\n--Current ACL Connections--");
+ addresses = listAclConnections();
+ for (String address : addresses) {
+ String name = getRemoteName(address);
+ pw.println(address + " (" + name + ")");
+ }
+
+ pw.println("\n--Known Devices--");
+ addresses = listRemoteDevices();
+ for (String address : addresses) {
+ String name = getRemoteName(address);
+ pw.println(address + " (" + name + ")");
+ }
+
+ // Rather not do this from here, but no-where else and I need this
+ // dump
+ pw.println("\n--Headset Service--");
+ switch (headset.getState()) {
+ case BluetoothHeadset.STATE_DISCONNECTED:
+ pw.println("getState() = STATE_DISCONNECTED");
+ break;
+ case BluetoothHeadset.STATE_CONNECTING:
+ pw.println("getState() = STATE_CONNECTING");
+ break;
+ case BluetoothHeadset.STATE_CONNECTED:
+ pw.println("getState() = STATE_CONNECTED");
+ break;
+ case BluetoothHeadset.STATE_ERROR:
+ pw.println("getState() = STATE_ERROR");
+ break;
+ }
+ pw.println("getHeadsetAddress() = " + headset.getHeadsetAddress());
+ headset.close();
+
+ } else {
+ pw.println("\nBluetooth DISABLED");
+ }
+ pw.println("\nmIsAirplaneSensitive = " + mIsAirplaneSensitive);
+ }
+}
diff --git a/core/java/android/server/BluetoothEventLoop.java b/core/java/android/server/BluetoothEventLoop.java
new file mode 100644
index 0000000..5722f51
--- /dev/null
+++ b/core/java/android/server/BluetoothEventLoop.java
@@ -0,0 +1,289 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.server;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothIntent;
+import android.bluetooth.IBluetoothDeviceCallback;
+import android.content.Context;
+import android.content.Intent;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.io.IOException;
+import java.lang.Thread;
+import java.util.HashMap;
+
+/**
+ * TODO: Move this to
+ * java/services/com/android/server/BluetoothEventLoop.java
+ * and make the contructor package private again.
+ *
+ * @hide
+ */
+class BluetoothEventLoop {
+ private static final String TAG = "BluetoothEventLoop";
+ private static final boolean DBG = false;
+
+ private int mNativeData;
+ private Thread mThread;
+ private boolean mInterrupted;
+ private HashMap<String, IBluetoothDeviceCallback> mCreateBondingCallbacks;
+ private HashMap<String, Integer> mPasskeyAgentRequestData;
+ private HashMap<String, IBluetoothDeviceCallback> mGetRemoteServiceChannelCallbacks;
+ private BluetoothDeviceService mBluetoothService;
+
+ private Context mContext;
+
+ static { classInitNative(); }
+ private static native void classInitNative();
+
+ /* pacakge */ BluetoothEventLoop(Context context, BluetoothDeviceService bluetoothService) {
+ mBluetoothService = bluetoothService;
+ mContext = context;
+ mCreateBondingCallbacks = new HashMap();
+ mPasskeyAgentRequestData = new HashMap();
+ mGetRemoteServiceChannelCallbacks = new HashMap();
+ initializeNativeDataNative();
+ }
+ private native void initializeNativeDataNative();
+
+ protected void finalize() throws Throwable {
+ try {
+ cleanupNativeDataNative();
+ } finally {
+ super.finalize();
+ }
+ }
+ private native void cleanupNativeDataNative();
+
+ /* pacakge */ HashMap<String, IBluetoothDeviceCallback> getCreateBondingCallbacks() {
+ return mCreateBondingCallbacks;
+ }
+ /* pacakge */ HashMap<String, IBluetoothDeviceCallback> getRemoteServiceChannelCallbacks() {
+ return mGetRemoteServiceChannelCallbacks;
+ }
+
+ /* pacakge */ HashMap<String, Integer> getPasskeyAgentRequestData() {
+ return mPasskeyAgentRequestData;
+ }
+
+ private synchronized boolean waitForAndDispatchEvent(int timeout_ms) {
+ return waitForAndDispatchEventNative(timeout_ms);
+ }
+ private native boolean waitForAndDispatchEventNative(int timeout_ms);
+
+ /* package */ synchronized void start() {
+
+ if (mThread != null) {
+ // Already running.
+ return;
+ }
+ mThread = new Thread("Bluetooth Event Loop") {
+ @Override
+ public void run() {
+ try {
+ if (setUpEventLoopNative()) {
+ while (!mInterrupted) {
+ waitForAndDispatchEvent(0);
+ sleep(500);
+ }
+ tearDownEventLoopNative();
+ }
+ } catch (InterruptedException e) { }
+ if (DBG) log("Event Loop thread finished");
+ }
+ };
+ if (DBG) log("Starting Event Loop thread");
+ mInterrupted = false;
+ mThread.start();
+ }
+ private native boolean setUpEventLoopNative();
+ private native void tearDownEventLoopNative();
+
+ public synchronized void stop() {
+ if (mThread != null) {
+
+ mInterrupted = true;
+
+ try {
+ mThread.join();
+ mThread = null;
+ } catch (InterruptedException e) {
+ Log.i(TAG, "Interrupted waiting for Event Loop thread to join");
+ }
+ }
+ }
+
+ public synchronized boolean isEventLoopRunning() {
+ return mThread != null;
+ }
+
+ public void onModeChanged(String mode) {
+ Intent intent = new Intent(BluetoothIntent.MODE_CHANGED_ACTION);
+ int intMode = BluetoothDevice.MODE_UNKNOWN;
+ if (mode.equalsIgnoreCase("off")) {
+ intMode = BluetoothDevice.MODE_OFF;
+ }
+ else if (mode.equalsIgnoreCase("connectable")) {
+ intMode = BluetoothDevice.MODE_CONNECTABLE;
+ }
+ else if (mode.equalsIgnoreCase("discoverable")) {
+ intMode = BluetoothDevice.MODE_DISCOVERABLE;
+ }
+ intent.putExtra(BluetoothIntent.MODE, intMode);
+ mContext.sendBroadcast(intent);
+ }
+
+ public void onDiscoveryStarted() {
+ mBluetoothService.setIsDiscovering(true);
+ Intent intent = new Intent(BluetoothIntent.DISCOVERY_STARTED_ACTION);
+ mContext.sendBroadcast(intent);
+ }
+ public void onDiscoveryCompleted() {
+ mBluetoothService.setIsDiscovering(false);
+ Intent intent = new Intent(BluetoothIntent.DISCOVERY_COMPLETED_ACTION);
+ mContext.sendBroadcast(intent);
+ }
+
+ public void onPairingRequest() {
+ Intent intent = new Intent(BluetoothIntent.PAIRING_REQUEST_ACTION);
+ mContext.sendBroadcast(intent);
+ }
+ public void onPairingCancel() {
+ Intent intent = new Intent(BluetoothIntent.PAIRING_CANCEL_ACTION);
+ mContext.sendBroadcast(intent);
+ }
+
+ public void onRemoteDeviceFound(String address, int deviceClass, short rssi) {
+ Intent intent = new Intent(BluetoothIntent.REMOTE_DEVICE_FOUND_ACTION);
+ intent.putExtra(BluetoothIntent.ADDRESS, address);
+ intent.putExtra(BluetoothIntent.CLASS, deviceClass);
+ intent.putExtra(BluetoothIntent.RSSI, rssi);
+ mContext.sendBroadcast(intent);
+ }
+ public void onRemoteDeviceDisappeared(String address) {
+ Intent intent = new Intent(BluetoothIntent.REMOTE_DEVICE_DISAPPEARED_ACTION);
+ intent.putExtra(BluetoothIntent.ADDRESS, address);
+ mContext.sendBroadcast(intent);
+ }
+ public void onRemoteClassUpdated(String address, int deviceClass) {
+ Intent intent = new Intent(BluetoothIntent.REMOTE_DEVICE_CLASS_UPDATED_ACTION);
+ intent.putExtra(BluetoothIntent.ADDRESS, address);
+ intent.putExtra(BluetoothIntent.CLASS, deviceClass);
+ mContext.sendBroadcast(intent);
+ }
+ public void onRemoteDeviceConnected(String address) {
+ Intent intent = new Intent(BluetoothIntent.REMOTE_DEVICE_CONNECTED_ACTION);
+ intent.putExtra(BluetoothIntent.ADDRESS, address);
+ mContext.sendBroadcast(intent);
+ }
+ public void onRemoteDeviceDisconnectRequested(String address) {
+ Intent intent = new Intent(BluetoothIntent.REMOTE_DEVICE_DISCONNECT_REQUESTED_ACTION);
+ intent.putExtra(BluetoothIntent.ADDRESS, address);
+ mContext.sendBroadcast(intent);
+ }
+ public void onRemoteDeviceDisconnected(String address) {
+ Intent intent = new Intent(BluetoothIntent.REMOTE_DEVICE_DISCONNECTED_ACTION);
+ intent.putExtra(BluetoothIntent.ADDRESS, address);
+ mContext.sendBroadcast(intent);
+ }
+ public void onRemoteNameUpdated(String address, String name) {
+ Intent intent = new Intent(BluetoothIntent.REMOTE_NAME_UPDATED_ACTION);
+ intent.putExtra(BluetoothIntent.ADDRESS, address);
+ intent.putExtra(BluetoothIntent.NAME, name);
+ mContext.sendBroadcast(intent);
+ }
+ public void onRemoteNameFailed(String address) {
+ Intent intent = new Intent(BluetoothIntent.REMOTE_NAME_FAILED_ACTION);
+ intent.putExtra(BluetoothIntent.ADDRESS, address);
+ mContext.sendBroadcast(intent);
+ }
+ public void onRemoteNameChanged(String address, String name) {
+ Intent intent = new Intent(BluetoothIntent.REMOTE_NAME_UPDATED_ACTION);
+ intent.putExtra(BluetoothIntent.ADDRESS, address);
+ intent.putExtra(BluetoothIntent.NAME, name);
+ mContext.sendBroadcast(intent);
+ }
+ public void onRemoteAliasChanged(String address, String alias) {
+ Intent intent = new Intent(BluetoothIntent.REMOTE_ALIAS_CHANGED_ACTION);
+ intent.putExtra(BluetoothIntent.ADDRESS, address);
+ intent.putExtra(BluetoothIntent.ALIAS, alias);
+ mContext.sendBroadcast(intent);
+ }
+ public void onRemoteAliasCleared(String address) {
+ Intent intent = new Intent(BluetoothIntent.REMOTE_ALIAS_CLEARED_ACTION);
+ intent.putExtra(BluetoothIntent.ADDRESS, address);
+ mContext.sendBroadcast(intent);
+ }
+
+ private void onCreateBondingResult(String address, boolean result) {
+ IBluetoothDeviceCallback callback = mCreateBondingCallbacks.get(address);
+ if (callback != null) {
+ try {
+ callback.onCreateBondingResult(address,
+ result ? BluetoothDevice.RESULT_SUCCESS :
+ BluetoothDevice.RESULT_FAILURE);
+ } catch (RemoteException e) {}
+ mCreateBondingCallbacks.remove(address);
+ }
+ }
+ public void onBondingCreated(String address) {
+ Intent intent = new Intent(BluetoothIntent.BONDING_CREATED_ACTION);
+ intent.putExtra(BluetoothIntent.ADDRESS, address);
+ mContext.sendBroadcast(intent);
+ }
+ public void onBondingRemoved(String address) {
+ Intent intent = new Intent(BluetoothIntent.BONDING_REMOVED_ACTION);
+ intent.putExtra(BluetoothIntent.ADDRESS, address);
+ mContext.sendBroadcast(intent);
+ }
+
+ public void onNameChanged(String name) {
+ Intent intent = new Intent(BluetoothIntent.NAME_CHANGED_ACTION);
+ intent.putExtra(BluetoothIntent.NAME, name);
+ mContext.sendBroadcast(intent);
+ }
+
+ public void onPasskeyAgentRequest(String address, int nativeData) {
+ mPasskeyAgentRequestData.put(address, new Integer(nativeData));
+
+ Intent intent = new Intent(BluetoothIntent.PAIRING_REQUEST_ACTION);
+ intent.putExtra(BluetoothIntent.ADDRESS, address);
+ mContext.sendBroadcast(intent);
+ }
+ public void onPasskeyAgentCancel(String address) {
+ mPasskeyAgentRequestData.remove(address);
+
+ Intent intent = new Intent(BluetoothIntent.PAIRING_CANCEL_ACTION);
+ intent.putExtra(BluetoothIntent.ADDRESS, address);
+ mContext.sendBroadcast(intent);
+ }
+ private void onGetRemoteServiceChannelResult(String address, int channel) {
+ IBluetoothDeviceCallback callback = mGetRemoteServiceChannelCallbacks.get(address);
+ if (callback != null) {
+ try {
+ callback.onGetRemoteServiceChannelResult(address, channel);
+ } catch (RemoteException e) {}
+ mGetRemoteServiceChannelCallbacks.remove(address);
+ }
+ }
+
+ private static void log(String msg) {
+ Log.d(TAG, msg);
+ }
+}
diff --git a/core/java/android/server/checkin/CheckinProvider.java b/core/java/android/server/checkin/CheckinProvider.java
new file mode 100644
index 0000000..86ece4a
--- /dev/null
+++ b/core/java/android/server/checkin/CheckinProvider.java
@@ -0,0 +1,388 @@
+/*
+ * 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.server.checkin;
+
+import android.content.ContentProvider;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.net.Uri;
+import android.os.Environment;
+import android.provider.BaseColumns;
+import android.provider.Checkin;
+import android.util.Log;
+
+import java.io.File;
+
+/**
+ * Content provider for the database used to store events and statistics
+ * while they wait to be uploaded by the checkin service.
+ */
+public class CheckinProvider extends ContentProvider {
+ /** Class identifier for logging. */
+ private static final String TAG = "CheckinProvider";
+
+ /** Filename of database (in /data directory). */
+ private static final String DATABASE_FILENAME = "checkin.db";
+
+ /** Version of database schema. */
+ private static final int DATABASE_VERSION = 1;
+
+ /** Maximum number of events recorded. */
+ private static final int EVENT_LIMIT = 1000;
+
+ /** Maximum size of individual event data. */
+ private static final int EVENT_SIZE = 8192;
+
+ /** Maximum number of crashes recorded. */
+ private static final int CRASH_LIMIT = 25;
+
+ /** Maximum size of individual crashes recorded. */
+ private static final int CRASH_SIZE = 16384;
+
+ /** Permission required for access to the 'properties' database. */
+ private static final String PROPERTIES_PERMISSION =
+ "android.permission.ACCESS_CHECKIN_PROPERTIES";
+
+ /** Lock for stats read-modify-write update cycle (see {@link #insert}). */
+ private final Object mStatsLock = new Object();
+
+ /** The underlying SQLite database. */
+ private SQLiteOpenHelper mOpenHelper;
+
+ private static class OpenHelper extends SQLiteOpenHelper {
+ public OpenHelper(Context context) {
+ super(context, DATABASE_FILENAME, null, DATABASE_VERSION);
+
+ // The database used to live in /data/checkin.db.
+ File oldLocation = Environment.getDataDirectory();
+ File old = new File(oldLocation, DATABASE_FILENAME);
+ File file = context.getDatabasePath(DATABASE_FILENAME);
+
+ // Try to move the file to the new location.
+ // TODO: Remove this code before shipping.
+ if (old.exists() && !file.exists() && !old.renameTo(file)) {
+ Log.e(TAG, "Can't rename " + old + " to " + file);
+ }
+ if (old.exists() && !old.delete()) {
+ // Clean up the old data file in any case.
+ Log.e(TAG, "Can't remove " + old);
+ }
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ db.execSQL("CREATE TABLE " + Checkin.Events.TABLE_NAME + " (" +
+ BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+ Checkin.Events.TAG + " TEXT NOT NULL," +
+ Checkin.Events.VALUE + " TEXT DEFAULT \"\"," +
+ Checkin.Events.DATE + " INTEGER NOT NULL)");
+
+ db.execSQL("CREATE INDEX events_index ON " +
+ Checkin.Events.TABLE_NAME + " (" +
+ Checkin.Events.TAG + ")");
+
+ db.execSQL("CREATE TABLE " + Checkin.Stats.TABLE_NAME + " (" +
+ BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+ Checkin.Stats.TAG + " TEXT UNIQUE," +
+ Checkin.Stats.COUNT + " INTEGER DEFAULT 0," +
+ Checkin.Stats.SUM + " REAL DEFAULT 0.0)");
+
+ db.execSQL("CREATE TABLE " + Checkin.Crashes.TABLE_NAME + " (" +
+ BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+ Checkin.Crashes.DATA + " TEXT NOT NULL," +
+ Checkin.Crashes.LOGS + " TEXT)");
+
+ db.execSQL("CREATE TABLE " + Checkin.Properties.TABLE_NAME + " (" +
+ BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+ Checkin.Properties.TAG + " TEXT UNIQUE ON CONFLICT REPLACE,"
+ + Checkin.Properties.VALUE + " TEXT DEFAULT \"\")");
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int old, int version) {
+ db.execSQL("DROP TABLE IF EXISTS " + Checkin.Events.TABLE_NAME);
+ db.execSQL("DROP TABLE IF EXISTS " + Checkin.Stats.TABLE_NAME);
+ db.execSQL("DROP TABLE IF EXISTS " + Checkin.Crashes.TABLE_NAME);
+ db.execSQL("DROP TABLE IF EXISTS " + Checkin.Properties.TABLE_NAME);
+ onCreate(db);
+ }
+ }
+
+ @Override public boolean onCreate() {
+ mOpenHelper = new OpenHelper(getContext());
+ return true;
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] select,
+ String where, String[] args, String sort) {
+ checkPermissions(uri);
+ SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+ qb.setTables(uri.getPathSegments().get(0));
+ if (uri.getPathSegments().size() == 2) {
+ qb.appendWhere("_id=" + ContentUris.parseId(uri));
+ } else if (uri.getPathSegments().size() != 1) {
+ throw new IllegalArgumentException("Invalid query URI: " + uri);
+ }
+
+ SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+ Cursor cursor = qb.query(db, select, where, args, null, null, sort);
+ cursor.setNotificationUri(getContext().getContentResolver(), uri);
+ return cursor;
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ checkPermissions(uri);
+ if (uri.getPathSegments().size() != 1) {
+ throw new IllegalArgumentException("Invalid insert URI: " + uri);
+ }
+
+ long id;
+ String table = uri.getPathSegments().get(0);
+ if (Checkin.Events.TABLE_NAME.equals(table)) {
+ id = insertEvent(values);
+ } else if (Checkin.Stats.TABLE_NAME.equals(table)) {
+ id = insertStats(values);
+ } else if (Checkin.Crashes.TABLE_NAME.equals(table)) {
+ id = insertCrash(values);
+ } else {
+ id = mOpenHelper.getWritableDatabase().insert(table, null, values);
+ }
+
+ if (id < 0) {
+ return null;
+ } else {
+ uri = ContentUris.withAppendedId(uri, id);
+ getContext().getContentResolver().notifyChange(uri, null);
+ return uri;
+ }
+ }
+
+ /**
+ * Insert an entry into the events table.
+ * Trims old events from the table to keep the size bounded.
+ * @param values to insert
+ * @return the row ID of the new entry
+ */
+ private long insertEvent(ContentValues values) {
+ String value = values.getAsString(Checkin.Events.VALUE);
+ if (value != null && value.length() > EVENT_SIZE) {
+ // Event values are readable text, so they can be truncated.
+ value = value.substring(0, EVENT_SIZE - 3) + "...";
+ values.put(Checkin.Events.VALUE, value);
+ }
+
+ if (!values.containsKey(Checkin.Events.DATE)) {
+ values.put(Checkin.Events.DATE, System.currentTimeMillis());
+ }
+
+ // TODO: Make this more efficient; don't do it on every insert.
+ // Also, consider keeping the most recent instance of every tag,
+ // and possibly update a counter when events are deleted.
+
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ db.execSQL("DELETE FROM " +
+ Checkin.Events.TABLE_NAME + " WHERE " +
+ Checkin.Events._ID + " IN (SELECT " +
+ Checkin.Events._ID + " FROM " +
+ Checkin.Events.TABLE_NAME + " ORDER BY " +
+ Checkin.Events.DATE + " DESC LIMIT -1 OFFSET " +
+ (EVENT_LIMIT - 1) + ")");
+ return db.insert(Checkin.Events.TABLE_NAME, null, values);
+ }
+
+ /**
+ * Add an entry into the stats table.
+ * For statistics, instead of just inserting a row into the database,
+ * we add the count and sum values to the existing values (if any)
+ * for the specified tag. This must be done with a lock held,
+ * to avoid a race condition during the read-modify-write update.
+ * @param values to insert
+ * @return the row ID of the modified entry
+ */
+ private long insertStats(ContentValues values) {
+ synchronized (mStatsLock) {
+ String tag = values.getAsString(Checkin.Stats.TAG);
+ if (tag == null) {
+ throw new IllegalArgumentException("Tag required:" + values);
+ }
+
+ // Look for existing values with this tag.
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ Cursor cursor = db.query(false,
+ Checkin.Stats.TABLE_NAME,
+ new String[] {
+ Checkin.Stats._ID,
+ Checkin.Stats.COUNT,
+ Checkin.Stats.SUM
+ },
+ Checkin.Stats.TAG + "=?",
+ new String[] { tag },
+ null, null, null, null /* limit */);
+
+ try {
+ if (cursor == null || !cursor.moveToNext()) {
+ // This is a new statistic, insert it directly.
+ return db.insert(Checkin.Stats.TABLE_NAME, null, values);
+ } else {
+ // Depend on SELECT column order to avoid getColumnIndex()
+ long id = cursor.getLong(0);
+ int count = cursor.getInt(1);
+ double sum = cursor.getDouble(2);
+
+ Integer countAdd = values.getAsInteger(Checkin.Stats.COUNT);
+ if (countAdd != null) count += countAdd.intValue();
+
+ Double sumAdd = values.getAsDouble(Checkin.Stats.SUM);
+ if (sumAdd != null) sum += sumAdd.doubleValue();
+
+ if (count <= 0 && sum == 0.0) {
+ // Updated to nothing: delete the row!
+ cursor.deleteRow();
+ getContext().getContentResolver().notifyChange(
+ ContentUris.withAppendedId(Checkin.Stats.CONTENT_URI, id), null);
+ return -1;
+ } else {
+ if (countAdd != null) cursor.updateInt(1, count);
+ if (sumAdd != null) cursor.updateDouble(2, sum);
+ cursor.commitUpdates();
+ return id;
+ }
+ }
+ } finally {
+ // Always clean up the cursor.
+ if (cursor != null) cursor.close();
+ }
+ }
+ }
+
+ /**
+ * Add an entry into the crashes table.
+ * @param values to insert
+ * @return the row ID of the modified entry
+ */
+ private long insertCrash(ContentValues values) {
+ try {
+ int crashSize = values.getAsString(Checkin.Crashes.DATA).length();
+ if (crashSize > CRASH_SIZE) {
+ // The crash is too big. Don't report it, but do log a stat.
+ Checkin.updateStats(getContext().getContentResolver(),
+ Checkin.Stats.Tag.CRASHES_TRUNCATED, 1, 0.0);
+ throw new IllegalArgumentException("Too big: " + crashSize);
+ }
+
+ // Count the number of crashes reported, even if they roll over.
+ Checkin.updateStats(getContext().getContentResolver(),
+ Checkin.Stats.Tag.CRASHES_REPORTED, 1, 0.0);
+
+ // Trim the crashes database, if needed.
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ db.execSQL("DELETE FROM " +
+ Checkin.Crashes.TABLE_NAME + " WHERE " +
+ Checkin.Crashes._ID + " IN (SELECT " +
+ Checkin.Crashes._ID + " FROM " +
+ Checkin.Crashes.TABLE_NAME + " ORDER BY " +
+ Checkin.Crashes._ID + " DESC LIMIT -1 OFFSET " +
+ (CRASH_LIMIT - 1) + ")");
+
+ return db.insert(Checkin.Crashes.TABLE_NAME, null, values);
+ } catch (Throwable t) {
+ // To avoid an infinite crash-reporting loop, swallow the error.
+ Log.e("CheckinProvider", "Error inserting crash: " + t);
+ return -1;
+ }
+ }
+
+ // TODO: optimize bulkInsert, especially for stats?
+
+ @Override
+ public int update(Uri uri, ContentValues values,
+ String where, String[] args) {
+ checkPermissions(uri);
+ if (uri.getPathSegments().size() == 2) {
+ if (where != null && where.length() > 0) {
+ throw new UnsupportedOperationException(
+ "WHERE clause not supported for update: " + uri);
+ }
+ where = "_id=" + ContentUris.parseId(uri);
+ args = null;
+ } else if (uri.getPathSegments().size() != 1) {
+ throw new IllegalArgumentException("Invalid update URI: " + uri);
+ }
+
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ int count = db.update(uri.getPathSegments().get(0), values, where, args);
+ getContext().getContentResolver().notifyChange(uri, null);
+ return count;
+ }
+
+ @Override
+ public int delete(Uri uri, String where, String[] args) {
+ checkPermissions(uri);
+ if (uri.getPathSegments().size() == 2) {
+ if (where != null && where.length() > 0) {
+ throw new UnsupportedOperationException(
+ "WHERE clause not supported for delete: " + uri);
+ }
+ where = "_id=" + ContentUris.parseId(uri);
+ args = null;
+ } else if (uri.getPathSegments().size() != 1) {
+ throw new IllegalArgumentException("Invalid delete URI: " + uri);
+ }
+
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ int count = db.delete(uri.getPathSegments().get(0), where, args);
+ getContext().getContentResolver().notifyChange(uri, null);
+ return count;
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ if (uri.getPathSegments().size() == 1) {
+ return "vnd.android.cursor.dir/" + uri.getPathSegments().get(0);
+ } else if (uri.getPathSegments().size() == 2) {
+ return "vnd.android.cursor.item/" + uri.getPathSegments().get(0);
+ } else {
+ throw new IllegalArgumentException("Invalid URI: " + uri);
+ }
+ }
+
+ /**
+ * Make sure the caller has permission to the database.
+ * @param uri the caller is requesting access to
+ * @throws SecurityException if the caller is forbidden.
+ */
+ private void checkPermissions(Uri uri) {
+ if (uri.getPathSegments().size() < 1) {
+ throw new IllegalArgumentException("Invalid query URI: " + uri);
+ }
+
+ String table = uri.getPathSegments().get(0);
+ if (table.equals(Checkin.Properties.TABLE_NAME) &&
+ getContext().checkCallingOrSelfPermission(PROPERTIES_PERMISSION) !=
+ PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("Cannot access checkin properties");
+ }
+ }
+}
diff --git a/core/java/android/server/checkin/FallbackCheckinService.java b/core/java/android/server/checkin/FallbackCheckinService.java
new file mode 100644
index 0000000..b450913
--- /dev/null
+++ b/core/java/android/server/checkin/FallbackCheckinService.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.server.checkin;
+
+import android.os.ICheckinService;
+import android.os.RemoteException;
+import android.os.IParentalControlCallback;
+import com.google.android.net.ParentalControlState;
+
+/**
+ * @hide
+ */
+public final class FallbackCheckinService extends ICheckinService.Stub {
+ public FallbackCheckinService() {
+ }
+
+ public void reportCrashSync(byte[] crashData) throws RemoteException {
+ }
+
+ public void reportCrashAsync(byte[] crashData) throws RemoteException {
+ }
+
+ public void masterClear() throws RemoteException {
+ }
+
+ public void getParentalControlState(IParentalControlCallback p) throws RemoteException {
+ ParentalControlState state = new ParentalControlState();
+ state.isEnabled = false;
+ p.onResult(state);
+ }
+}
diff --git a/core/java/android/server/checkin/package.html b/core/java/android/server/checkin/package.html
new file mode 100644
index 0000000..1c9bf9d
--- /dev/null
+++ b/core/java/android/server/checkin/package.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+ {@hide}
+</body>
+</html>
diff --git a/core/java/android/server/data/BuildData.java b/core/java/android/server/data/BuildData.java
new file mode 100644
index 0000000..53ffa3f
--- /dev/null
+++ b/core/java/android/server/data/BuildData.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.server.data;
+
+import android.os.Build;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+
+import static com.android.internal.util.Objects.nonNull;
+
+/**
+ * Build data transfer object. Keep in sync. with the server side version.
+ */
+public class BuildData {
+
+ /** The version of the data returned by write() and understood by the constructor. */
+ private static final int VERSION = 0;
+
+ private final String fingerprint;
+ private final String incrementalVersion;
+ private final long time; // in *seconds* since the epoch (not msec!)
+
+ public BuildData() {
+ this.fingerprint = "android:" + Build.FINGERPRINT;
+ this.incrementalVersion = Build.VERSION.INCREMENTAL;
+ this.time = Build.TIME / 1000; // msec -> sec
+ }
+
+ public BuildData(String fingerprint, String incrementalVersion, long time) {
+ this.fingerprint = nonNull(fingerprint);
+ this.incrementalVersion = incrementalVersion;
+ this.time = time;
+ }
+
+ /*package*/ BuildData(DataInput in) throws IOException {
+ int dataVersion = in.readInt();
+ if (dataVersion != VERSION) {
+ throw new IOException("Expected " + VERSION + ". Got: " + dataVersion);
+ }
+
+ this.fingerprint = in.readUTF();
+ this.incrementalVersion = Long.toString(in.readLong());
+ this.time = in.readLong();
+ }
+
+ /*package*/ void write(DataOutput out) throws IOException {
+ out.writeInt(VERSION);
+ out.writeUTF(fingerprint);
+
+ // TODO: change the format/version to expect a string for this field.
+ // Version 0, still used by the server side, expects a long.
+ long changelist;
+ try {
+ changelist = Long.parseLong(incrementalVersion);
+ } catch (NumberFormatException ex) {
+ changelist = -1;
+ }
+ out.writeLong(changelist);
+ out.writeLong(time);
+ }
+
+ public String getFingerprint() {
+ return fingerprint;
+ }
+
+ public String getIncrementalVersion() {
+ return incrementalVersion;
+ }
+
+ public long getTime() {
+ return time;
+ }
+}
diff --git a/core/java/android/server/data/CrashData.java b/core/java/android/server/data/CrashData.java
new file mode 100644
index 0000000..d652bb3
--- /dev/null
+++ b/core/java/android/server/data/CrashData.java
@@ -0,0 +1,145 @@
+/*
+ * 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.server.data;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+
+import static com.android.internal.util.Objects.nonNull;
+
+/**
+ * Crash data transfer object. Keep in sync. with the server side version.
+ */
+public class CrashData {
+
+ final String id;
+ final String activity;
+ final long time;
+ final BuildData buildData;
+ final ThrowableData throwableData;
+ final byte[] state;
+
+ public CrashData(String id, String activity, BuildData buildData,
+ ThrowableData throwableData) {
+ this.id = nonNull(id);
+ this.activity = nonNull(activity);
+ this.buildData = nonNull(buildData);
+ this.throwableData = nonNull(throwableData);
+ this.time = System.currentTimeMillis();
+ this.state = null;
+ }
+
+ public CrashData(String id, String activity, BuildData buildData,
+ ThrowableData throwableData, byte[] state) {
+ this.id = nonNull(id);
+ this.activity = nonNull(activity);
+ this.buildData = nonNull(buildData);
+ this.throwableData = nonNull(throwableData);
+ this.time = System.currentTimeMillis();
+ this.state = state;
+ }
+
+ public CrashData(DataInput in) throws IOException {
+ int dataVersion = in.readInt();
+ if (dataVersion != 0 && dataVersion != 1) {
+ throw new IOException("Expected 0 or 1. Got: " + dataVersion);
+ }
+
+ this.id = in.readUTF();
+ this.activity = in.readUTF();
+ this.time = in.readLong();
+ this.buildData = new BuildData(in);
+ this.throwableData = new ThrowableData(in);
+ if (dataVersion == 1) {
+ int len = in.readInt();
+ if (len == 0) {
+ this.state = null;
+ } else {
+ this.state = new byte[len];
+ in.readFully(this.state, 0, len);
+ }
+ } else {
+ this.state = null;
+ }
+ }
+
+ public CrashData(String tag, Throwable throwable) {
+ id = "";
+ activity = tag;
+ buildData = new BuildData();
+ throwableData = new ThrowableData(throwable);
+ time = System.currentTimeMillis();
+ state = null;
+ }
+
+ public void write(DataOutput out) throws IOException {
+ // version
+ if (this.state == null) {
+ out.writeInt(0);
+ } else {
+ out.writeInt(1);
+ }
+
+ out.writeUTF(this.id);
+ out.writeUTF(this.activity);
+ out.writeLong(this.time);
+ buildData.write(out);
+ throwableData.write(out);
+ if (this.state != null) {
+ out.writeInt(this.state.length);
+ out.write(this.state, 0, this.state.length);
+ }
+ }
+
+ public BuildData getBuildData() {
+ return buildData;
+ }
+
+ public ThrowableData getThrowableData() {
+ return throwableData;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getActivity() {
+ return activity;
+ }
+
+ public long getTime() {
+ return time;
+ }
+
+ public byte[] getState() {
+ return state;
+ }
+
+ /**
+ * Return a brief description of this CrashData record. The details of the
+ * representation are subject to change.
+ *
+ * @return Returns a String representing the contents of the object.
+ */
+ @Override
+ public String toString() {
+ return "[CrashData: id=" + id + " activity=" + activity + " time=" + time +
+ " buildData=" + buildData.toString() +
+ " throwableData=" + throwableData.toString() + "]";
+ }
+}
diff --git a/core/java/android/server/data/StackTraceElementData.java b/core/java/android/server/data/StackTraceElementData.java
new file mode 100644
index 0000000..07185a0
--- /dev/null
+++ b/core/java/android/server/data/StackTraceElementData.java
@@ -0,0 +1,80 @@
+/*
+ * 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.server.data;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+
+/**
+ * Stack trace element data transfer object. Keep in sync. with the server side
+ * version.
+ */
+public class StackTraceElementData {
+
+ final String className;
+ final String fileName;
+ final String methodName;
+ final int lineNumber;
+
+ public StackTraceElementData(StackTraceElement element) {
+ this.className = element.getClassName();
+
+ String fileName = element.getFileName();
+ this.fileName = fileName == null ? "[unknown source]" : fileName;
+
+ this.methodName = element.getMethodName();
+ this.lineNumber = element.getLineNumber();
+ }
+
+ public StackTraceElementData(DataInput in) throws IOException {
+ int dataVersion = in.readInt();
+ if (dataVersion != 0) {
+ throw new IOException("Expected 0. Got: " + dataVersion);
+ }
+
+ this.className = in.readUTF();
+ this.fileName = in.readUTF();
+ this.methodName = in.readUTF();
+ this.lineNumber = in.readInt();
+ }
+
+ void write(DataOutput out) throws IOException {
+ out.writeInt(0); // version
+
+ out.writeUTF(className);
+ out.writeUTF(fileName);
+ out.writeUTF(methodName);
+ out.writeInt(lineNumber);
+ }
+
+ public String getClassName() {
+ return className;
+ }
+
+ public String getFileName() {
+ return fileName;
+ }
+
+ public String getMethodName() {
+ return methodName;
+ }
+
+ public int getLineNumber() {
+ return lineNumber;
+ }
+}
diff --git a/core/java/android/server/data/ThrowableData.java b/core/java/android/server/data/ThrowableData.java
new file mode 100644
index 0000000..e500aca
--- /dev/null
+++ b/core/java/android/server/data/ThrowableData.java
@@ -0,0 +1,138 @@
+/*
+ * 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.server.data;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+
+/**
+ * Throwable data transfer object. Keep in sync. with the server side version.
+ */
+public class ThrowableData {
+
+ final String message;
+ final String type;
+ final StackTraceElementData[] stackTrace;
+ final ThrowableData cause;
+
+ public ThrowableData(Throwable throwable) {
+ this.type = throwable.getClass().getName();
+ String message = throwable.getMessage();
+ this.message = message == null ? "" : message;
+
+ StackTraceElement[] elements = throwable.getStackTrace();
+ this.stackTrace = new StackTraceElementData[elements.length];
+ for (int i = 0; i < elements.length; i++) {
+ this.stackTrace[i] = new StackTraceElementData(elements[i]);
+ }
+
+ Throwable cause = throwable.getCause();
+ this.cause = cause == null ? null : new ThrowableData(cause);
+ }
+
+ public ThrowableData(DataInput in) throws IOException {
+ int dataVersion = in.readInt();
+ if (dataVersion != 0) {
+ throw new IOException("Expected 0. Got: " + dataVersion);
+ }
+
+ this.message = in.readUTF();
+ this.type = in.readUTF();
+
+ int count = in.readInt();
+ this.stackTrace = new StackTraceElementData[count];
+ for (int i = 0; i < count; i++) {
+ this.stackTrace[i] = new StackTraceElementData(in);
+ }
+
+ this.cause = in.readBoolean() ? new ThrowableData(in) : null;
+ }
+
+ public void write(DataOutput out) throws IOException {
+ out.writeInt(0); // version
+
+ out.writeUTF(message);
+ out.writeUTF(type);
+
+ out.writeInt(stackTrace.length);
+ for (StackTraceElementData elementData : stackTrace) {
+ elementData.write(out);
+ }
+
+ out.writeBoolean(cause != null);
+ if (cause != null) {
+ cause.write(out);
+ }
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public StackTraceElementData[] getStackTrace() {
+ return stackTrace;
+ }
+
+ public ThrowableData getCause() {
+ return cause;
+ }
+
+
+ public String toString() {
+ return toString(null);
+ }
+
+ public String toString(String prefix) {
+ StringBuilder builder = new StringBuilder();
+ append(prefix, builder, this);
+ return builder.toString();
+ }
+
+ private static void append(String prefix, StringBuilder builder,
+ ThrowableData throwableData) {
+ if (prefix != null) builder.append(prefix);
+ builder.append(throwableData.getType())
+ .append(": ")
+ .append(throwableData.getMessage())
+ .append('\n');
+ for (StackTraceElementData element : throwableData.getStackTrace()) {
+ if (prefix != null ) builder.append(prefix);
+ builder.append(" at ")
+ .append(element.getClassName())
+ .append('.')
+ .append(element.getMethodName())
+ .append("(")
+ .append(element.getFileName())
+ .append(':')
+ .append(element.getLineNumber())
+ .append(")\n");
+
+ }
+
+ ThrowableData cause = throwableData.getCause();
+ if (cause != null) {
+ if (prefix != null ) builder.append(prefix);
+ builder.append("Caused by: ");
+ append(prefix, builder, cause);
+ }
+ }
+}
diff --git a/core/java/android/server/data/package.html b/core/java/android/server/data/package.html
new file mode 100755
index 0000000..1c9bf9d
--- /dev/null
+++ b/core/java/android/server/data/package.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+ {@hide}
+</body>
+</html>
diff --git a/core/java/android/server/package.html b/core/java/android/server/package.html
new file mode 100755
index 0000000..c9f96a6
--- /dev/null
+++ b/core/java/android/server/package.html
@@ -0,0 +1,5 @@
+<body>
+
+{@hide}
+
+</body>
diff --git a/core/java/android/server/search/SearchManagerService.java b/core/java/android/server/search/SearchManagerService.java
new file mode 100644
index 0000000..fe15553
--- /dev/null
+++ b/core/java/android/server/search/SearchManagerService.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.server.search;
+
+import android.app.ISearchManager;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Handler;
+import android.util.Config;
+
+/**
+ * This is a simplified version of the Search Manager service. It no longer handles
+ * presentation (UI). Its function is to maintain the map & list of "searchable"
+ * items, which provides a mapping from individual activities (where a user might have
+ * invoked search) to specific searchable activities (where the search will be dispatched).
+ */
+public class SearchManagerService extends ISearchManager.Stub
+{
+ // general debugging support
+ private static final String TAG = "SearchManagerService";
+ private static final boolean DEBUG = false;
+ private static final boolean localLOGV = DEBUG ? Config.LOGD : Config.LOGV;
+
+ // configuration choices
+ private static final boolean IMMEDIATE_SEARCHABLES_UPDATE = true;
+
+ // class maintenance and general shared data
+ private final Context mContext;
+ private final Handler mHandler;
+ private boolean mSearchablesDirty;
+
+ /**
+ * Initialize the Search Manager service in the provided system context.
+ * Only one instance of this object should be created!
+ *
+ * @param context to use for accessing DB, window manager, etc.
+ */
+ public SearchManagerService(Context context) {
+ mContext = context;
+ mHandler = new Handler();
+
+ // Setup the infrastructure for updating and maintaining the list
+ // of searchable activities.
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_PACKAGE_ADDED);
+ filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+ filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
+ filter.addDataScheme("package");
+ mContext.registerReceiver(mIntentReceiver, filter, null, mHandler);
+ mSearchablesDirty = true;
+
+ // After startup settles down, preload the searchables list,
+ // which will reduce the delay when the search UI is invoked.
+ if (IMMEDIATE_SEARCHABLES_UPDATE) {
+ mHandler.post(mRunUpdateSearchable);
+ }
+ }
+
+ /**
+ * Listens for intent broadcasts.
+ *
+ * The primary purpose here is to refresh the "searchables" list
+ * if packages are added/removed.
+ */
+ private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+
+ // First, test for intents that matter at any time
+ if (action.equals(Intent.ACTION_PACKAGE_ADDED) ||
+ action.equals(Intent.ACTION_PACKAGE_REMOVED) ||
+ action.equals(Intent.ACTION_PACKAGE_CHANGED)) {
+ mSearchablesDirty = true;
+ if (IMMEDIATE_SEARCHABLES_UPDATE) {
+ mHandler.post(mRunUpdateSearchable);
+ }
+ return;
+ }
+ }
+ };
+
+ /**
+ * This runnable (for the main handler / UI thread) will update the searchables list.
+ */
+ private Runnable mRunUpdateSearchable = new Runnable() {
+ public void run() {
+ if (mSearchablesDirty) {
+ updateSearchables();
+ }
+ }
+ };
+
+ /**
+ * Update the list of searchables, either at startup or in response to
+ * a package add/remove broadcast message.
+ */
+ private void updateSearchables() {
+ SearchableInfo.buildSearchableList(mContext);
+ mSearchablesDirty = false;
+
+ // TODO This is a hack. This shouldn't be hardcoded here, it's probably
+ // a policy.
+// ComponentName defaultSearch = new ComponentName(
+// "com.android.contacts",
+// "com.android.contacts.ContactsListActivity" );
+ ComponentName defaultSearch = new ComponentName(
+ "com.android.googlesearch",
+ "com.android.googlesearch.GoogleSearch" );
+ SearchableInfo.setDefaultSearchable(mContext, defaultSearch);
+ }
+
+ /**
+ * Return the searchableinfo for a given activity
+ *
+ * @param launchActivity The activity from which we're launching this search.
+ * @return Returns a SearchableInfo record describing the parameters of the search,
+ * or null if no searchable metadata was available.
+ * @param globalSearch If false, this will only launch the search that has been specifically
+ * defined by the application (which is usually defined as a local search). If no default
+ * search is defined in the current application or activity, no search will be launched.
+ * If true, this will always launch a platform-global (e.g. web-based) search instead.
+ */
+ public SearchableInfo getSearchableInfo(ComponentName launchActivity, boolean globalSearch) {
+ // final check. however we should try to avoid this, because
+ // it slows down the entry into the UI.
+ if (mSearchablesDirty) {
+ updateSearchables();
+ }
+ SearchableInfo si = null;
+ if (globalSearch) {
+ si = SearchableInfo.getDefaultSearchable();
+ } else {
+ si = SearchableInfo.getSearchableInfo(mContext, launchActivity);
+ }
+
+ return si;
+ }
+}
diff --git a/core/java/android/server/search/SearchableInfo.aidl b/core/java/android/server/search/SearchableInfo.aidl
new file mode 100644
index 0000000..9576c2b
--- /dev/null
+++ b/core/java/android/server/search/SearchableInfo.aidl
@@ -0,0 +1,19 @@
+/**
+ * Copyright (c) 2008, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.server.search;
+
+parcelable SearchableInfo;
diff --git a/core/java/android/server/search/SearchableInfo.java b/core/java/android/server/search/SearchableInfo.java
new file mode 100644
index 0000000..5b9942e
--- /dev/null
+++ b/core/java/android/server/search/SearchableInfo.java
@@ -0,0 +1,747 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.server.search;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ProviderInfo;
+import android.content.pm.ResolveInfo;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.Xml;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+public final class SearchableInfo implements Parcelable {
+
+ // general debugging support
+ final static String LOG_TAG = "SearchableInfo";
+
+ // set this flag to 1 to prevent any apps from providing suggestions
+ final static int DBG_INHIBIT_SUGGESTIONS = 0;
+
+ // static strings used for XML lookups, etc.
+ // TODO how should these be documented for the developer, in a more structured way than
+ // the current long wordy javadoc in SearchManager.java ?
+ private static final String MD_LABEL_DEFAULT_SEARCHABLE = "android.app.default_searchable";
+ private static final String MD_LABEL_SEARCHABLE = "android.app.searchable";
+ private static final String MD_SEARCHABLE_SYSTEM_SEARCH = "*";
+ private static final String MD_XML_ELEMENT_SEARCHABLE = "searchable";
+ private static final String MD_XML_ELEMENT_SEARCHABLE_ACTION_KEY = "actionkey";
+
+ // class maintenance and general shared data
+ private static HashMap<ComponentName, SearchableInfo> sSearchablesMap = null;
+ private static ArrayList<SearchableInfo> sSearchablesList = null;
+ private static SearchableInfo sDefaultSearchable = null;
+
+ // true member variables - what we know about the searchability
+ // TO-DO replace public with getters
+ public boolean mSearchable = false;
+ private int mLabelId = 0;
+ public ComponentName mSearchActivity = null;
+ private int mHintId = 0;
+ private int mSearchMode = 0;
+ public boolean mBadgeLabel = false;
+ public boolean mBadgeIcon = false;
+ public boolean mQueryRewriteFromData = false;
+ public boolean mQueryRewriteFromText = false;
+ private int mIconId = 0;
+ private int mSearchButtonText = 0;
+ private String mSuggestAuthority = null;
+ private String mSuggestPath = null;
+ private String mSuggestSelection = null;
+ private String mSuggestIntentAction = null;
+ private String mSuggestIntentData = null;
+ private ActionKeyInfo mActionKeyList = null;
+ private String mSuggestProviderPackage = null;
+ private Context mCacheActivityContext = null; // use during setup only - don't hold memory!
+
+ /**
+ * Set the default searchable activity (when none is specified).
+ */
+ public static void setDefaultSearchable(Context context,
+ ComponentName activity) {
+ synchronized (SearchableInfo.class) {
+ SearchableInfo si = null;
+ if (activity != null) {
+ si = getSearchableInfo(context, activity);
+ if (si != null) {
+ // move to front of list
+ sSearchablesList.remove(si);
+ sSearchablesList.add(0, si);
+ }
+ }
+ sDefaultSearchable = si;
+ }
+ }
+
+ /**
+ * Provides the system-default search activity, which you can use
+ * whenever getSearchableInfo() returns null;
+ *
+ * @return Returns the system-default search activity, null if never defined
+ */
+ public static SearchableInfo getDefaultSearchable() {
+ synchronized (SearchableInfo.class) {
+ return sDefaultSearchable;
+ }
+ }
+
+ /**
+ * Retrieve the authority for obtaining search suggestions.
+ *
+ * @return Returns a string containing the suggestions authority.
+ */
+ public String getSuggestAuthority() {
+ return mSuggestAuthority;
+ }
+
+ /**
+ * Retrieve the path for obtaining search suggestions.
+ *
+ * @return Returns a string containing the suggestions path, or null if not provided.
+ */
+ public String getSuggestPath() {
+ return mSuggestPath;
+ }
+
+ /**
+ * Retrieve the selection pattern for obtaining search suggestions. This must
+ * include a single ? which will be used for the user-typed characters.
+ *
+ * @return Returns a string containing the suggestions authority.
+ */
+ public String getSuggestSelection() {
+ return mSuggestSelection;
+ }
+
+ /**
+ * Retrieve the (optional) intent action for use with these suggestions. This is
+ * useful if all intents will have the same action (e.g. "android.intent.action.VIEW").
+ *
+ * Can be overriden in any given suggestion via the AUTOSUGGEST_COLUMN_INTENT_ACTION column.
+ *
+ * @return Returns a string containing the default intent action.
+ */
+ public String getSuggestIntentAction() {
+ return mSuggestIntentAction;
+ }
+
+ /**
+ * Retrieve the (optional) intent data for use with these suggestions. This is
+ * useful if all intents will have similar data URIs (e.g. "android.intent.action.VIEW"),
+ * but you'll likely need to provide a specific ID as well via the column
+ * AUTOSUGGEST_COLUMN_INTENT_DATA_ID, which will be appended to the intent data URI.
+ *
+ * Can be overriden in any given suggestion via the AUTOSUGGEST_COLUMN_INTENT_DATA column.
+ *
+ * @return Returns a string containing the default intent data.
+ */
+ public String getSuggestIntentData() {
+ return mSuggestIntentData;
+ }
+
+ /**
+ * Get the context for the searchable activity.
+ *
+ * This is fairly expensive so do it on the original scan, or when an app is
+ * selected, but don't hang on to the result forever.
+ *
+ * @param context You need to supply a context to start with
+ * @return Returns a context related to the searchable activity
+ */
+ public Context getActivityContext(Context context) {
+ Context theirContext = null;
+ try {
+ theirContext = context.createPackageContext(mSearchActivity.getPackageName(), 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ // unexpected, but we deal with this by null-checking theirContext
+ } catch (java.lang.SecurityException e) {
+ // unexpected, but we deal with this by null-checking theirContext
+ }
+
+ return theirContext;
+ }
+
+ /**
+ * Get the context for the suggestions provider.
+ *
+ * This is fairly expensive so do it on the original scan, or when an app is
+ * selected, but don't hang on to the result forever.
+ *
+ * @param context You need to supply a context to start with
+ * @param activityContext If we can determine that the provider and the activity are the
+ * same, we'll just return this one.
+ * @return Returns a context related to the context provider
+ */
+ public Context getProviderContext(Context context, Context activityContext) {
+ Context theirContext = null;
+ if (mSearchActivity.getPackageName().equals(mSuggestProviderPackage)) {
+ return activityContext;
+ }
+ if (mSuggestProviderPackage != null)
+ try {
+ theirContext = context.createPackageContext(mSuggestProviderPackage, 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ // unexpected, but we deal with this by null-checking theirContext
+ } catch (java.lang.SecurityException e) {
+ // unexpected, but we deal with this by null-checking theirContext
+ }
+
+ return theirContext;
+ }
+
+ /**
+ * Factory. Look up, or construct, based on the activity.
+ *
+ * The activities fall into three cases, based on meta-data found in
+ * the manifest entry:
+ * <ol>
+ * <li>The activity itself implements search. This is indicated by the
+ * presence of a "android.app.searchable" meta-data attribute.
+ * The value is a reference to an XML file containing search information.</li>
+ * <li>A related activity implements search. This is indicated by the
+ * presence of a "android.app.default_searchable" meta-data attribute.
+ * The value is a string naming the activity implementing search. In this
+ * case the factory will "redirect" and return the searchable data.</li>
+ * <li>No searchability data is provided. We return null here and other
+ * code will insert the "default" (e.g. contacts) search.
+ *
+ * TODO: cache the result in the map, and check the map first.
+ * TODO: it might make sense to implement the searchable reference as
+ * an application meta-data entry. This way we don't have to pepper each
+ * and every activity.
+ * TODO: can we skip the constructor step if it's a non-searchable?
+ * TODO: does it make sense to plug the default into a slot here for
+ * automatic return? Probably not, but it's one way to do it.
+ *
+ * @param activity The name of the current activity, or null if the
+ * activity does not define any explicit searchable metadata.
+ */
+ public static SearchableInfo getSearchableInfo(Context context,
+ ComponentName activity) {
+ // Step 1. Is the result already hashed? (case 1)
+ SearchableInfo result;
+ synchronized (SearchableInfo.class) {
+ result = sSearchablesMap.get(activity);
+ if (result != null) return result;
+ }
+
+ // Step 2. See if the current activity references a searchable.
+ // Note: Conceptually, this could be a while(true) loop, but there's
+ // no point in implementing reference chaining here and risking a loop.
+ // References must point directly to searchable activities.
+
+ ActivityInfo ai = null;
+ XmlPullParser xml = null;
+ try {
+ ai = context.getPackageManager().
+ getActivityInfo(activity, PackageManager.GET_META_DATA );
+ String refActivityName = null;
+
+ // First look for activity-specific reference
+ Bundle md = ai.metaData;
+ if (md != null) {
+ refActivityName = md.getString(MD_LABEL_DEFAULT_SEARCHABLE);
+ }
+ // If not found, try for app-wide reference
+ if (refActivityName == null) {
+ md = ai.applicationInfo.metaData;
+ if (md != null) {
+ refActivityName = md.getString(MD_LABEL_DEFAULT_SEARCHABLE);
+ }
+ }
+
+ // Irrespective of source, if a reference was found, follow it.
+ if (refActivityName != null)
+ {
+ // An app or activity can declare that we should simply launch
+ // "system default search" if search is invoked.
+ if (refActivityName.equals(MD_SEARCHABLE_SYSTEM_SEARCH)) {
+ return getDefaultSearchable();
+ }
+ String pkg = activity.getPackageName();
+ ComponentName referredActivity;
+ if (refActivityName.charAt(0) == '.') {
+ referredActivity = new ComponentName(pkg, pkg + refActivityName);
+ } else {
+ referredActivity = new ComponentName(pkg, refActivityName);
+ }
+
+ // Now try the referred activity, and if found, cache
+ // it against the original name so we can skip the check
+ synchronized (SearchableInfo.class) {
+ result = sSearchablesMap.get(referredActivity);
+ if (result != null) {
+ sSearchablesMap.put(activity, result);
+ return result;
+ }
+ }
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ // case 3: no metadata
+ }
+
+ // Step 3. None found. Return null.
+ return null;
+
+ }
+
+ /**
+ * Super-factory. Builds an entire list (suitable for display) of
+ * activities that are searchable, by iterating the entire set of
+ * ACTION_SEARCH intents.
+ *
+ * Also clears the hash of all activities -> searches which will
+ * refill as the user clicks "search".
+ *
+ * This should only be done at startup and again if we know that the
+ * list has changed.
+ *
+ * TODO: every activity that provides a ACTION_SEARCH intent should
+ * also provide searchability meta-data. There are a bunch of checks here
+ * that, if data is not found, silently skip to the next activity. This
+ * won't help a developer trying to figure out why their activity isn't
+ * showing up in the list, but an exception here is too rough. I would
+ * like to find a better notification mechanism.
+ *
+ * TODO: sort the list somehow? UI choice.
+ *
+ * @param context a context we can use during this work
+ */
+ public static void buildSearchableList(Context context) {
+
+ // create empty hash & list
+ HashMap<ComponentName, SearchableInfo> newSearchablesMap
+ = new HashMap<ComponentName, SearchableInfo>();
+ ArrayList<SearchableInfo> newSearchablesList
+ = new ArrayList<SearchableInfo>();
+
+ // use intent resolver to generate list of ACTION_SEARCH receivers
+ final PackageManager pm = context.getPackageManager();
+ List<ResolveInfo> infoList;
+ final Intent intent = new Intent(Intent.ACTION_SEARCH);
+ infoList = pm.queryIntentActivities(intent, PackageManager.GET_META_DATA);
+
+ // analyze each one, generate a Searchables record, and record
+ if (infoList != null) {
+ int count = infoList.size();
+ for (int ii = 0; ii < count; ii++) {
+ // for each component, try to find metadata
+ ResolveInfo info = infoList.get(ii);
+ ActivityInfo ai = info.activityInfo;
+ XmlResourceParser xml = ai.loadXmlMetaData(context.getPackageManager(),
+ MD_LABEL_SEARCHABLE);
+ if (xml == null) {
+ continue;
+ }
+ ComponentName cName = new ComponentName(
+ info.activityInfo.packageName,
+ info.activityInfo.name);
+
+ SearchableInfo searchable = getActivityMetaData(context, xml, cName);
+ xml.close();
+
+ if (searchable != null) {
+ // no need to keep the context any longer. setup time is over.
+ searchable.mCacheActivityContext = null;
+
+ newSearchablesList.add(searchable);
+ newSearchablesMap.put(cName, searchable);
+ }
+ }
+ }
+
+ // record the final values as a coherent pair
+ synchronized (SearchableInfo.class) {
+ sSearchablesList = newSearchablesList;
+ sSearchablesMap = newSearchablesMap;
+ }
+ }
+
+ /**
+ * Constructor
+ *
+ * Given a ComponentName, get the searchability info
+ * and build a local copy of it. Use the factory, not this.
+ *
+ * @param context runtime context
+ * @param attr The attribute set we found in the XML file, contains the values that are used to
+ * construct the object.
+ * @param cName The component name of the searchable activity
+ */
+ private SearchableInfo(Context context, AttributeSet attr, final ComponentName cName) {
+ // initialize as an "unsearchable" object
+ mSearchable = false;
+ mSearchActivity = cName;
+
+ // to access another activity's resources, I need its context.
+ // BE SURE to release the cache sometime after construction - it's a large object to hold
+ mCacheActivityContext = getActivityContext(context);
+ if (mCacheActivityContext != null) {
+ TypedArray a = mCacheActivityContext.obtainStyledAttributes(attr,
+ com.android.internal.R.styleable.Searchable);
+ mSearchMode = a.getInt(com.android.internal.R.styleable.Searchable_searchMode, 0);
+ mLabelId = a.getResourceId(com.android.internal.R.styleable.Searchable_label, 0);
+ mHintId = a.getResourceId(com.android.internal.R.styleable.Searchable_hint, 0);
+ mIconId = a.getResourceId(com.android.internal.R.styleable.Searchable_icon, 0);
+ mSearchButtonText = a.getResourceId(
+ com.android.internal.R.styleable.Searchable_searchButtonText, 0);
+ setSearchModeFlags();
+ if (DBG_INHIBIT_SUGGESTIONS == 0) {
+ mSuggestAuthority = a.getString(
+ com.android.internal.R.styleable.Searchable_searchSuggestAuthority);
+ mSuggestPath = a.getString(
+ com.android.internal.R.styleable.Searchable_searchSuggestPath);
+ mSuggestSelection = a.getString(
+ com.android.internal.R.styleable.Searchable_searchSuggestSelection);
+ mSuggestIntentAction = a.getString(
+ com.android.internal.R.styleable.Searchable_searchSuggestIntentAction);
+ mSuggestIntentData = a.getString(
+ com.android.internal.R.styleable.Searchable_searchSuggestIntentData);
+ }
+ a.recycle();
+
+ // get package info for suggestions provider (if any)
+ if (mSuggestAuthority != null) {
+ ProviderInfo pi =
+ context.getPackageManager().resolveContentProvider(mSuggestAuthority,
+ 0);
+ if (pi != null) {
+ mSuggestProviderPackage = pi.packageName;
+ }
+ }
+ }
+
+ // for now, implement some form of rules - minimal data
+ if (mLabelId != 0) {
+ mSearchable = true;
+ } else {
+ // Provide some help for developers instead of just silently discarding
+ Log.w(LOG_TAG, "Insufficient metadata to configure searchability for " +
+ cName.flattenToShortString());
+ }
+ }
+
+ /**
+ * Convert searchmode to flags.
+ */
+ private void setSearchModeFlags() {
+ // decompose searchMode attribute
+ // TODO How do I reconcile these hardcoded values with the flag bits defined in
+ // in attrs.xml? e.g. android.R.id.filterMode = 0x010200a4 instead of just "1"
+ /* mFilterMode = (0 != (mSearchMode & 1)); */
+ /* mQuickStart = (0 != (mSearchMode & 2)); */
+ mBadgeLabel = (0 != (mSearchMode & 4));
+ mBadgeIcon = (0 != (mSearchMode & 8)) && (mIconId != 0);
+ mQueryRewriteFromData = (0 != (mSearchMode & 0x10));
+ mQueryRewriteFromText = (0 != (mSearchMode & 0x20));
+ }
+
+ /**
+ * Private class used to hold the "action key" configuration
+ */
+ public class ActionKeyInfo implements Parcelable {
+
+ public int mKeyCode = 0;
+ public String mQueryActionMsg;
+ public String mSuggestActionMsg;
+ public String mSuggestActionMsgColumn;
+ private ActionKeyInfo mNext;
+
+ /**
+ * Create one object using attributeset as input data.
+ * @param context runtime context
+ * @param attr The attribute set we found in the XML file, contains the values that are used to
+ * construct the object.
+ * @param next We'll build these up using a simple linked list (since there are usually
+ * just zero or one).
+ */
+ public ActionKeyInfo(Context context, AttributeSet attr, ActionKeyInfo next) {
+ TypedArray a = mCacheActivityContext.obtainStyledAttributes(attr,
+ com.android.internal.R.styleable.SearchableActionKey);
+
+ mKeyCode = a.getInt(
+ com.android.internal.R.styleable.SearchableActionKey_keycode, 0);
+ mQueryActionMsg = a.getString(
+ com.android.internal.R.styleable.SearchableActionKey_queryActionMsg);
+ if (DBG_INHIBIT_SUGGESTIONS == 0) {
+ mSuggestActionMsg = a.getString(
+ com.android.internal.R.styleable.SearchableActionKey_suggestActionMsg);
+ mSuggestActionMsgColumn = a.getString(
+ com.android.internal.R.styleable.SearchableActionKey_suggestActionMsgColumn);
+ }
+ a.recycle();
+
+ // initialize any other fields
+ mNext = next;
+
+ // sanity check. must have at least one action message, or invalidate the object.
+ if ((mQueryActionMsg == null) &&
+ (mSuggestActionMsg == null) &&
+ (mSuggestActionMsgColumn == null)) {
+ mKeyCode = 0;
+ }
+ }
+
+ /**
+ * Instantiate a new ActionKeyInfo from the data in a Parcel that was
+ * previously written with {@link #writeToParcel(Parcel, int)}.
+ *
+ * @param in The Parcel containing the previously written ActionKeyInfo,
+ * positioned at the location in the buffer where it was written.
+ * @param next The value to place in mNext, creating a linked list
+ */
+ public ActionKeyInfo(Parcel in, ActionKeyInfo next) {
+ mKeyCode = in.readInt();
+ mQueryActionMsg = in.readString();
+ mSuggestActionMsg = in.readString();
+ mSuggestActionMsgColumn = in.readString();
+ mNext = next;
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mKeyCode);
+ dest.writeString(mQueryActionMsg);
+ dest.writeString(mSuggestActionMsg);
+ dest.writeString(mSuggestActionMsgColumn);
+ }
+ }
+
+ /**
+ * If any action keys were defined for this searchable activity, look up and return.
+ *
+ * @param keyCode The key that was pressed
+ * @return Returns the ActionKeyInfo record, or null if none defined
+ */
+ public ActionKeyInfo findActionKey(int keyCode) {
+ ActionKeyInfo info = mActionKeyList;
+ while (info != null) {
+ if (info.mKeyCode == keyCode) {
+ return info;
+ }
+ info = info.mNext;
+ }
+ return null;
+ }
+
+ /**
+ * Get the metadata for a given activity
+ *
+ * TODO: clean up where we return null vs. where we throw exceptions.
+ *
+ * @param context runtime context
+ * @param xml XML parser for reading attributes
+ * @param cName The component name of the searchable activity
+ *
+ * @result A completely constructed SearchableInfo, or null if insufficient XML data for it
+ */
+ private static SearchableInfo getActivityMetaData(Context context, XmlPullParser xml,
+ final ComponentName cName) {
+ SearchableInfo result = null;
+
+ // in order to use the attributes mechanism, we have to walk the parser
+ // forward through the file until it's reading the tag of interest.
+ try {
+ int tagType = xml.next();
+ while (tagType != XmlPullParser.END_DOCUMENT) {
+ if (tagType == XmlPullParser.START_TAG) {
+ if (xml.getName().equals(MD_XML_ELEMENT_SEARCHABLE)) {
+ AttributeSet attr = Xml.asAttributeSet(xml);
+ if (attr != null) {
+ result = new SearchableInfo(context, attr, cName);
+ // if the constructor returned a bad object, exit now.
+ if (! result.mSearchable) {
+ return null;
+ }
+ }
+ } else if (xml.getName().equals(MD_XML_ELEMENT_SEARCHABLE_ACTION_KEY)) {
+ if (result == null) {
+ // Can't process an embedded element if we haven't seen the enclosing
+ return null;
+ }
+ AttributeSet attr = Xml.asAttributeSet(xml);
+ if (attr != null) {
+ ActionKeyInfo keyInfo = result.new ActionKeyInfo(context, attr,
+ result.mActionKeyList);
+ // only add to list if it is was useable
+ if (keyInfo.mKeyCode != 0) {
+ result.mActionKeyList = keyInfo;
+ }
+ }
+ }
+ }
+ tagType = xml.next();
+ }
+ } catch (XmlPullParserException e) {
+ throw new RuntimeException(e);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return result;
+ }
+
+ /**
+ * Return the "label" (user-visible name) of this searchable context. This must be
+ * accessed using the target (searchable) Activity's resources, not simply the context of the
+ * caller.
+ *
+ * @return Returns the resource Id
+ */
+ public int getLabelId() {
+ return mLabelId;
+ }
+
+ /**
+ * Return the resource Id of the hint text. This must be
+ * accessed using the target (searchable) Activity's resources, not simply the context of the
+ * caller.
+ *
+ * @return Returns the resource Id, or 0 if not specified by this package.
+ */
+ public int getHintId() {
+ return mHintId;
+ }
+
+ /**
+ * Return the icon Id specified by the Searchable_icon meta-data entry. This must be
+ * accessed using the target (searchable) Activity's resources, not simply the context of the
+ * caller.
+ *
+ * @return Returns the resource id.
+ */
+ public int getIconId() {
+ return mIconId;
+ }
+
+ /**
+ * Return the resource Id of replacement text for the "Search" button.
+ *
+ * @return Returns the resource Id, or 0 if not specified by this package.
+ */
+ public int getSearchButtonText() {
+ return mSearchButtonText;
+ }
+
+ /**
+ * Return the list of searchable activities, for use in the drop-down.
+ */
+ public static ArrayList<SearchableInfo> getSearchablesList() {
+ synchronized (SearchableInfo.class) {
+ ArrayList<SearchableInfo> result = new ArrayList<SearchableInfo>(sSearchablesList);
+ return result;
+ }
+ }
+
+ /**
+ * Support for parcelable and aidl operations.
+ */
+ public static final Parcelable.Creator<SearchableInfo> CREATOR
+ = new Parcelable.Creator<SearchableInfo>() {
+ public SearchableInfo createFromParcel(Parcel in) {
+ return new SearchableInfo(in);
+ }
+
+ public SearchableInfo[] newArray(int size) {
+ return new SearchableInfo[size];
+ }
+ };
+
+ /**
+ * Instantiate a new SearchableInfo from the data in a Parcel that was
+ * previously written with {@link #writeToParcel(Parcel, int)}.
+ *
+ * @param in The Parcel containing the previously written SearchableInfo,
+ * positioned at the location in the buffer where it was written.
+ */
+ public SearchableInfo(Parcel in) {
+ mLabelId = in.readInt();
+ mSearchActivity = ComponentName.readFromParcel(in);
+ mHintId = in.readInt();
+ mSearchMode = in.readInt();
+ mIconId = in.readInt();
+ mSearchButtonText = in.readInt();
+ setSearchModeFlags();
+
+ mSuggestAuthority = in.readString();
+ mSuggestPath = in.readString();
+ mSuggestSelection = in.readString();
+ mSuggestIntentAction = in.readString();
+ mSuggestIntentData = in.readString();
+
+ mActionKeyList = null;
+ int count = in.readInt();
+ while (count-- > 0) {
+ mActionKeyList = new ActionKeyInfo(in, mActionKeyList);
+ }
+
+ mSuggestProviderPackage = in.readString();
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mLabelId);
+ mSearchActivity.writeToParcel(dest, flags);
+ dest.writeInt(mHintId);
+ dest.writeInt(mSearchMode);
+ dest.writeInt(mIconId);
+ dest.writeInt(mSearchButtonText);
+
+ dest.writeString(mSuggestAuthority);
+ dest.writeString(mSuggestPath);
+ dest.writeString(mSuggestSelection);
+ dest.writeString(mSuggestIntentAction);
+ dest.writeString(mSuggestIntentData);
+
+ // This is usually a very short linked list so we'll just pre-count it
+ ActionKeyInfo nextKeyInfo = mActionKeyList;
+ int count = 0;
+ while (nextKeyInfo != null) {
+ ++count;
+ nextKeyInfo = nextKeyInfo.mNext;
+ }
+ dest.writeInt(count);
+ // Now write count of 'em
+ nextKeyInfo = mActionKeyList;
+ while (count-- > 0) {
+ nextKeyInfo.writeToParcel(dest, flags);
+ }
+
+ dest.writeString(mSuggestProviderPackage);
+ }
+}
diff --git a/core/java/android/server/search/package.html b/core/java/android/server/search/package.html
new file mode 100644
index 0000000..c9f96a6
--- /dev/null
+++ b/core/java/android/server/search/package.html
@@ -0,0 +1,5 @@
+<body>
+
+{@hide}
+
+</body>
diff --git a/core/java/android/speech/recognition/AbstractEmbeddedGrammarListener.java b/core/java/android/speech/recognition/AbstractEmbeddedGrammarListener.java
new file mode 100644
index 0000000..c25a7e3
--- /dev/null
+++ b/core/java/android/speech/recognition/AbstractEmbeddedGrammarListener.java
@@ -0,0 +1,51 @@
+/*---------------------------------------------------------------------------*
+ * AbstractEmbeddedGrammarListener.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition;
+
+/**
+ * An EmbeddedGrammarListener whose methods are empty. This class exists as
+ * convenience for creating listener objects.
+ */
+public abstract class AbstractEmbeddedGrammarListener implements EmbeddedGrammarListener
+{
+ public void onCompileAllSlots()
+ {
+ }
+
+ public void onError(Exception e)
+ {
+ }
+
+ public void onLoaded()
+ {
+ }
+
+ public void onResetAllSlots()
+ {
+ }
+
+ public void onSaved(String path)
+ {
+ }
+
+ public void onUnloaded()
+ {
+ }
+}
diff --git a/core/java/android/speech/recognition/AbstractGrammarListener.java b/core/java/android/speech/recognition/AbstractGrammarListener.java
new file mode 100644
index 0000000..fe62290
--- /dev/null
+++ b/core/java/android/speech/recognition/AbstractGrammarListener.java
@@ -0,0 +1,39 @@
+/*---------------------------------------------------------------------------*
+ * AbstractGrammarListener.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition;
+
+/**
+ * A GrammarListener whose methods are empty. This class exists as convenience
+ * for creating listener objects.
+ */
+public abstract class AbstractGrammarListener implements GrammarListener
+{
+ public void onError(Exception e)
+ {
+ }
+
+ public void onLoaded()
+ {
+ }
+
+ public void onUnloaded()
+ {
+ }
+}
diff --git a/core/java/android/speech/recognition/AbstractRecognizerListener.java b/core/java/android/speech/recognition/AbstractRecognizerListener.java
new file mode 100644
index 0000000..ee2b8d1
--- /dev/null
+++ b/core/java/android/speech/recognition/AbstractRecognizerListener.java
@@ -0,0 +1,83 @@
+/*---------------------------------------------------------------------------*
+ * AbstractRecognizerListener.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition;
+
+import java.util.Hashtable;
+import java.util.Vector;
+
+/**
+ * A RecognizerListener whose methods are empty. This class exists as
+ * convenience for creating listener objects.
+ */
+public abstract class AbstractRecognizerListener implements RecognizerListener
+{
+ public void onBeginningOfSpeech()
+ {
+ }
+
+ public void onEndOfSpeech()
+ {
+ }
+
+ public void onRecognitionSuccess(RecognitionResult result)
+ {
+ }
+
+ public void onRecognitionFailure(FailureReason reason)
+ {
+ }
+
+ public void onError(Exception e)
+ {
+ }
+
+ public void onParametersGetError(Vector<String> parameters, Exception e)
+ {
+ }
+
+ public void onParametersSetError(Hashtable<String, String> parameters,
+ Exception e)
+ {
+ }
+
+ public void onParametersGet(Hashtable<String, String> parameters)
+ {
+ }
+
+ public void onParametersSet(Hashtable<String, String> parameters)
+ {
+ }
+
+ public void onStartOfSpeechTimeout()
+ {
+ }
+
+ public void onAcousticStateReset()
+ {
+ }
+
+ public void onStarted()
+ {
+ }
+
+ public void onStopped()
+ {
+ }
+}
diff --git a/core/java/android/speech/recognition/AbstractSrecGrammarListener.java b/core/java/android/speech/recognition/AbstractSrecGrammarListener.java
new file mode 100644
index 0000000..e62e4ba
--- /dev/null
+++ b/core/java/android/speech/recognition/AbstractSrecGrammarListener.java
@@ -0,0 +1,59 @@
+/*---------------------------------------------------------------------------*
+ * AbstractSrecGrammarListener.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition;
+
+/**
+ * An SrecGrammarListener whose methods are empty. This class exists as
+ * convenience for creating listener objects.
+ */
+public abstract class AbstractSrecGrammarListener implements SrecGrammarListener
+{
+ public void onCompileAllSlots()
+ {
+ }
+
+ public void onError(Exception e)
+ {
+ }
+
+ public void onLoaded()
+ {
+ }
+
+ public void onResetAllSlots()
+ {
+ }
+
+ public void onSaved(String path)
+ {
+ }
+
+ public void onUnloaded()
+ {
+ }
+
+ public void onAddItemList()
+ {
+ }
+
+ public void onAddItemListFailure(int index, Exception e)
+ {
+ }
+}
diff --git a/core/java/android/speech/recognition/AudioAlreadyInUseException.java b/core/java/android/speech/recognition/AudioAlreadyInUseException.java
new file mode 100644
index 0000000..90698a7
--- /dev/null
+++ b/core/java/android/speech/recognition/AudioAlreadyInUseException.java
@@ -0,0 +1,34 @@
+/*---------------------------------------------------------------------------*
+ * AudioAlreadyInUseException.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition;
+
+/**
+ * Thrown when an AudioStream is passed into a component when another component
+ * is already using it.
+ */
+public class AudioAlreadyInUseException extends IllegalArgumentException
+{
+ private static final long serialVersionUID = 0L;
+
+ public AudioAlreadyInUseException(String msg)
+ {
+ super(msg);
+ }
+}
diff --git a/core/java/android/speech/recognition/AudioDriverErrorException.java b/core/java/android/speech/recognition/AudioDriverErrorException.java
new file mode 100644
index 0000000..a755e7f
--- /dev/null
+++ b/core/java/android/speech/recognition/AudioDriverErrorException.java
@@ -0,0 +1,33 @@
+/*---------------------------------------------------------------------------*
+ * AudioDriverErrorException.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition;
+
+/**
+ * Thrown if an error occurs in the audio driver.
+ */
+public class AudioDriverErrorException extends Exception
+{
+ private static final long serialVersionUID = 0L;
+
+ public AudioDriverErrorException(String msg)
+ {
+ super(msg);
+ }
+}
diff --git a/core/java/android/speech/recognition/AudioSource.java b/core/java/android/speech/recognition/AudioSource.java
new file mode 100644
index 0000000..c4cd802
--- /dev/null
+++ b/core/java/android/speech/recognition/AudioSource.java
@@ -0,0 +1,45 @@
+/*---------------------------------------------------------------------------*
+ * AudioSource.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition;
+
+/**
+ * Generates audio data.
+ */
+public interface AudioSource
+{
+ /**
+ * Returns an object that contains the audio samples. This object
+ * is passed to other components that consumes it, such a Recognizer
+ * or a DeviceSpeaker.
+ *
+ * @return an AudioStream instance
+ */
+ AudioStream createAudio();
+
+ /**
+ * Tells the audio source to start collecting audio samples.
+ */
+ void start();
+
+ /**
+ * Tells the audio source to stop collecting audio samples.
+ */
+ void stop();
+}
diff --git a/core/java/android/speech/recognition/AudioSourceListener.java b/core/java/android/speech/recognition/AudioSourceListener.java
new file mode 100644
index 0000000..42e8ebe
--- /dev/null
+++ b/core/java/android/speech/recognition/AudioSourceListener.java
@@ -0,0 +1,44 @@
+/*---------------------------------------------------------------------------*
+ * AudioSourceListener.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition;
+
+/**
+ * Listens for Microphone events.
+ */
+public interface AudioSourceListener
+{
+ /**
+ * Invoked after the microphone starts recording.
+ */
+ void onStarted();
+
+ /**
+ * Invoked after the microphone stops recording.
+ */
+ void onStopped();
+
+ /**
+ * Invoked when an unexpected error occurs. This is normally followed by
+ * onStopped() if the component shuts down successfully.
+ *
+ * @param e the cause of the failure
+ */
+ void onError(Exception e);
+}
diff --git a/core/java/android/speech/recognition/AudioStream.java b/core/java/android/speech/recognition/AudioStream.java
new file mode 100644
index 0000000..36afe21
--- /dev/null
+++ b/core/java/android/speech/recognition/AudioStream.java
@@ -0,0 +1,35 @@
+/*---------------------------------------------------------------------------*
+ * AudioStream.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition;
+
+/**
+ * Stream used to read audio data.
+ */
+public interface AudioStream
+{
+ /**
+ * Releases resources associated with the object.
+ *
+ * @deprecated this method is deprecated and has no replacement. It will be
+ * removed in a future release of the API.
+ */
+ @Deprecated
+ void dispose();
+}
diff --git a/core/java/android/speech/recognition/Codec.java b/core/java/android/speech/recognition/Codec.java
new file mode 100644
index 0000000..18d9e15
--- /dev/null
+++ b/core/java/android/speech/recognition/Codec.java
@@ -0,0 +1,126 @@
+/*---------------------------------------------------------------------------*
+ * Codec.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition;
+
+/**
+ * Audio formats.
+ */
+public abstract class Codec
+{
+ /**
+ * PCM, 16 bits, 8KHz.
+ */
+ public static final Codec PCM_16BIT_8K = new Codec("PCM/16bit/8KHz")
+ {
+ @Override
+ public byte getBitsPerSample()
+ {
+ return 16;
+ }
+
+ @Override
+ public int getSampleRate()
+ {
+ return 8000;
+ }
+ };
+ /**
+ * PCM, 16 bits, 11KHz.
+ */
+ public static final Codec PCM_16BIT_11K = new Codec("PCM/16bit/11KHz")
+ {
+ @Override
+ public byte getBitsPerSample()
+ {
+ return 16;
+ }
+
+ @Override
+ public int getSampleRate()
+ {
+ return 11025;
+ }
+ };
+ /**
+ * PCM, 16 bits, 22KHz.
+ */
+ public static final Codec PCM_16BIT_22K = new Codec("PCM/16bit/22KHz")
+ {
+ @Override
+ public byte getBitsPerSample()
+ {
+ return 16;
+ }
+
+ @Override
+ public int getSampleRate()
+ {
+ return 22050;
+ }
+ };
+ /**
+ * ULAW, 8 bits, 8KHz.
+ */
+ public static final Codec ULAW_8BIT_8K = new Codec("ULAW/8bit/8KHz")
+ {
+ @Override
+ public byte getBitsPerSample()
+ {
+ return 8;
+ }
+
+ @Override
+ public int getSampleRate()
+ {
+ return 8000;
+ }
+ };
+ private final String message;
+
+ /**
+ * Creates a new Codec.
+ *
+ * @param message the message to associate with the codec
+ */
+ private Codec(String message)
+ {
+ this.message = message;
+ }
+
+ @Override
+ public String toString()
+ {
+ return message;
+ }
+
+ /**
+ * Returns the codec sample-rate.
+ *
+ * @return the codec sample-rate
+ */
+ public abstract int getSampleRate();
+
+ /**
+ * Returns the codec bitrate.
+ *
+ * @return the codec bitrate
+ */
+ public abstract byte getBitsPerSample();
+}
diff --git a/core/java/android/speech/recognition/DeviceSpeaker.java b/core/java/android/speech/recognition/DeviceSpeaker.java
new file mode 100644
index 0000000..bd18687
--- /dev/null
+++ b/core/java/android/speech/recognition/DeviceSpeaker.java
@@ -0,0 +1,77 @@
+/*---------------------------------------------------------------------------*
+ * DeviceSpeaker.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition;
+
+import android.speech.recognition.impl.DeviceSpeakerImpl;
+
+/**
+ * A device for transforming electric signals into audible sound, most
+ * frequently used to reproduce speech and music.
+ */
+public abstract class DeviceSpeaker
+{
+ private static DeviceSpeaker instance;
+
+ /**
+ * Returns the device speaker instance.
+ *
+ * @return an instance of a DeviceSpeaker class.
+ */
+ public static DeviceSpeaker getInstance()
+ {
+ instance = DeviceSpeakerImpl.getInstance();
+ return instance;
+ }
+
+ /**
+ * Starts the audio playback.
+ *
+ * @param source the audio to play
+ * @throws IllegalStateException if the component is already started
+ * @throws IllegalArgumentException if source audio is null, in-use by
+ * another component or is empty.
+ *
+ */
+ public abstract void start(AudioStream source) throws IllegalStateException,
+ IllegalArgumentException;
+
+ /**
+ * Stops audio playback.
+ */
+ public abstract void stop();
+
+ /**
+ * Sets the playback codec. This must be called before start() is called.
+ *
+ * @param playbackCodec the codec to use for the playback operation.
+ * @throws IllegalStateException if the component is already stopped
+ * @throws IllegalArgumentException if the specified codec is not supported
+ */
+ public abstract void setCodec(Codec playbackCodec) throws IllegalStateException,
+ IllegalArgumentException;
+
+ /**
+ * Sets the microphone listener.
+ *
+ * @param listener the device speaker listener.
+ * @throws IllegalStateException if the component is started
+ */
+ public abstract void setListener(DeviceSpeakerListener listener) throws IllegalStateException;
+}
diff --git a/core/java/android/speech/recognition/DeviceSpeakerListener.java b/core/java/android/speech/recognition/DeviceSpeakerListener.java
new file mode 100644
index 0000000..e2baa2e
--- /dev/null
+++ b/core/java/android/speech/recognition/DeviceSpeakerListener.java
@@ -0,0 +1,44 @@
+/*---------------------------------------------------------------------------*
+ * DeviceSpeakerListener.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition;
+
+/**
+ * Listens for DeviceSpeaker events.
+ */
+public interface DeviceSpeakerListener
+{
+ /**
+ * Invoked after playback begins.
+ */
+ void onStarted();
+
+ /**
+ * Invoked after playback terminates.
+ */
+ void onStopped();
+
+ /**
+ * Invoked when an unexpected error occurs. This is normally followed by
+ * onStopped() if the component shuts down successfully.
+ *
+ * @param e the cause of the failure
+ */
+ void onError(Exception e);
+}
diff --git a/core/java/android/speech/recognition/EmbeddedGrammar.java b/core/java/android/speech/recognition/EmbeddedGrammar.java
new file mode 100644
index 0000000..c6f037b
--- /dev/null
+++ b/core/java/android/speech/recognition/EmbeddedGrammar.java
@@ -0,0 +1,43 @@
+/*---------------------------------------------------------------------------*
+ * EmbeddedGrammar.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition;
+
+/**
+ * Grammar on an embedded recognizer.
+ */
+public interface EmbeddedGrammar extends Grammar
+{
+ /**
+ * Compiles items that were added to any of the grammar slots.
+ */
+ void compileAllSlots();
+
+ /**
+ * Removes all words added to all slots.
+ */
+ void resetAllSlots();
+
+ /**
+ * Saves the compiled grammar.
+ *
+ * @param url the url to save the grammar to
+ */
+ void save(String url);
+}
diff --git a/core/java/android/speech/recognition/EmbeddedGrammarListener.java b/core/java/android/speech/recognition/EmbeddedGrammarListener.java
new file mode 100644
index 0000000..5b8c1a4
--- /dev/null
+++ b/core/java/android/speech/recognition/EmbeddedGrammarListener.java
@@ -0,0 +1,58 @@
+/*---------------------------------------------------------------------------*
+ * EmbeddedGrammarListener.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition;
+
+/**
+ * Listens for EmbeddedGrammar events.
+ */
+public interface EmbeddedGrammarListener extends GrammarListener
+{
+ /**
+ * Invoked after the grammar is saved.
+ *
+ * @param path the path the grammar was saved to
+ */
+ void onSaved(String path);
+
+ /**
+ * Invoked when a grammar operation fails.
+ *
+ * @param e the cause of the failure.<br/>
+ * {@link GrammarOverflowException} if the grammar slot is full and no
+ * further items may be added to it.<br/>
+ * {@link java.lang.UnsupportedOperationException} if different words with
+ * the same pronunciation are added.<br/>
+ * {@link java.lang.IllegalStateException} if reseting or compiling the
+ * slots fails.<br/>
+ * {@link java.io.IOException} if the grammar could not be loaded or
+ * saved.</p>
+ */
+ void onError(Exception e);
+
+ /**
+ * Invokes after all grammar slots have been compiled.
+ */
+ void onCompileAllSlots();
+
+ /**
+ * Invokes after all grammar slots have been reset.
+ */
+ void onResetAllSlots();
+}
diff --git a/core/java/android/speech/recognition/EmbeddedRecognizer.java b/core/java/android/speech/recognition/EmbeddedRecognizer.java
new file mode 100644
index 0000000..cd79edc
--- /dev/null
+++ b/core/java/android/speech/recognition/EmbeddedRecognizer.java
@@ -0,0 +1,66 @@
+/*---------------------------------------------------------------------------*
+ * EmbeddedRecognizer.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import android.speech.recognition.impl.EmbeddedRecognizerImpl;
+
+/**
+ * Embedded recognizer.
+ */
+public abstract class EmbeddedRecognizer implements Recognizer
+{
+ private static EmbeddedRecognizer instance;
+
+ /**
+ * Returns the embedded recognizer.
+ *
+ * @return the embedded recognizer
+ */
+ public static EmbeddedRecognizer getInstance()
+ {
+ instance = EmbeddedRecognizerImpl.getInstance();
+ return instance;
+ }
+
+ /**
+ * Configures the recognizer.
+ *
+ * @param config recognizer configuration file
+ * @throws IllegalArgumentException if config is null or an empty string
+ * @throws FileNotFoundException if the specified file could not be found
+ * @throws IOException if the specified file could not be opened
+ * @throws UnsatisfiedLinkError if the recognizer plugin could not be loaded
+ * @throws ClassNotFoundException if the recognizer plugin could not be found
+ */
+ public abstract void configure(String config) throws IllegalArgumentException,
+ FileNotFoundException, IOException, UnsatisfiedLinkError,
+ ClassNotFoundException;
+
+ /**
+ * The recognition accuracy improves over time as the recognizer adapts to
+ * the surrounding environment. This method enables developers to reset the
+ * adaptation when the environment is known to have changed.
+ *
+ * @throws IllegalArgumentException if recognizer instance is null
+ */
+ public abstract void resetAcousticState() throws IllegalArgumentException;
+}
diff --git a/core/java/android/speech/recognition/Grammar.java b/core/java/android/speech/recognition/Grammar.java
new file mode 100644
index 0000000..9f1b624
--- /dev/null
+++ b/core/java/android/speech/recognition/Grammar.java
@@ -0,0 +1,43 @@
+/*---------------------------------------------------------------------------*
+ * Grammar.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition;
+
+/**
+ * Speech recognition grammar.
+ */
+public interface Grammar {
+ /**
+ * Load the grammar sets the grammar state to active, indicating that can be used in a recognition process.
+ * Multiple grammars can be loaded, but only one at a time can be used by the recognizer.
+ *
+ */
+ void load();
+
+ /**
+ * Unload the grammar sets the grammar state to inactive (inactive grammars can not be used as a parameter of a recognition).
+ */
+ void unload();
+
+ /**
+ * (Optional operation) Releases resources associated with the object. The
+ * grammar may not be used past this point.
+ */
+ void dispose();
+}
diff --git a/core/java/android/speech/recognition/GrammarErrorException.java b/core/java/android/speech/recognition/GrammarErrorException.java
new file mode 100644
index 0000000..6070758
--- /dev/null
+++ b/core/java/android/speech/recognition/GrammarErrorException.java
@@ -0,0 +1,33 @@
+/*---------------------------------------------------------------------------*
+ * GrammarErrorException.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition;
+
+/**
+ * Thrown if an error occurs in the audio driver.
+ */
+public class GrammarErrorException extends Exception
+{
+ private static final long serialVersionUID = 0L;
+
+ public GrammarErrorException(String msg)
+ {
+ super(msg);
+ }
+}
diff --git a/core/java/android/speech/recognition/GrammarListener.java b/core/java/android/speech/recognition/GrammarListener.java
new file mode 100644
index 0000000..871cbcb
--- /dev/null
+++ b/core/java/android/speech/recognition/GrammarListener.java
@@ -0,0 +1,45 @@
+/*---------------------------------------------------------------------------*
+ * GrammarListener.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition;
+
+/**
+ * Listens for Grammar events.
+ */
+public interface GrammarListener
+{
+ /**
+ * Invoked after the Grammar is loaded.
+ */
+ void onLoaded();
+
+ /**
+ * Invoked after the Grammar is unloaded.
+ */
+ void onUnloaded();
+
+ /**
+ * Invoked when a grammar operation fails.
+ *
+ * @param e the cause of the failure.<br/>
+ * {@link java.io.IOException} if the grammar could not be loaded or
+ * saved.</p>
+ */
+ void onError(Exception e);
+}
diff --git a/core/java/android/speech/recognition/GrammarOverflowException.java b/core/java/android/speech/recognition/GrammarOverflowException.java
new file mode 100644
index 0000000..227820b
--- /dev/null
+++ b/core/java/android/speech/recognition/GrammarOverflowException.java
@@ -0,0 +1,33 @@
+/*---------------------------------------------------------------------------*
+ * GrammarOverflowException.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition;
+
+/**
+ * Thrown if a SlotItem is added into a grammar slot that is filled to capacity.
+ */
+public class GrammarOverflowException extends Exception
+{
+ private static final long serialVersionUID = 0L;
+
+ public GrammarOverflowException(String message)
+ {
+ super(message);
+ }
+}
diff --git a/core/java/android/speech/recognition/InvalidURLException.java b/core/java/android/speech/recognition/InvalidURLException.java
new file mode 100644
index 0000000..fec9411
--- /dev/null
+++ b/core/java/android/speech/recognition/InvalidURLException.java
@@ -0,0 +1,34 @@
+/*---------------------------------------------------------------------------*
+ * InvalidURLException.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition;
+
+/**
+ */
+public class InvalidURLException extends Exception {
+
+ private static final long serialVersionUID = 0L;
+
+ /** Creates a new instance of InvalidURLException */
+ public InvalidURLException(String msg)
+ {
+ super(msg);
+ }
+
+}
diff --git a/core/java/android/speech/recognition/Logger.java b/core/java/android/speech/recognition/Logger.java
new file mode 100644
index 0000000..8a09cb3
--- /dev/null
+++ b/core/java/android/speech/recognition/Logger.java
@@ -0,0 +1,127 @@
+/*---------------------------------------------------------------------------*
+ * Logger.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition;
+
+import android.speech.recognition.impl.LoggerImpl;
+
+/**
+ * Logs debugging information.
+ */
+public abstract class Logger
+{
+ /**
+ * Logging level
+ */
+ public static class LogLevel
+ {
+ /**
+ * Does not log.
+ */
+ public static LogLevel LEVEL_NONE = new LogLevel("Do not log");
+ /**
+ * Logs fatal issues. This level only logs ERROR.
+ */
+ public static LogLevel LEVEL_ERROR = new LogLevel("log UAPI_ERROR logs");
+ /**
+ * Logs non-fatal issues. This level also logs ERROR.
+ */
+ public static LogLevel LEVEL_WARN =
+ new LogLevel("log UAPI_ERROR, UAPI_WARN logs");
+ /**
+ * Logs debugging information, such as the values of variables. This level also logs ERROR, WARN.
+ */
+ public static LogLevel LEVEL_INFO =
+ new LogLevel("log UAPI_ERROR, UAPI_WARN, UAPI_INFO logs");
+ /**
+ * Logs when loggers are created or destroyed. This level also logs INFO, WARN, ERROR.
+ */
+ public static LogLevel LEVEL_TRACE =
+ new LogLevel("log UAPI_ERROR, UAPI_WARN, UAPI_INFO, UAPI_TRACE logs");
+ private String message;
+
+ /**
+ * Creates a new LogLevel.
+ *
+ * @param message the message associated with the LogLevel.
+ */
+ private LogLevel(String message)
+ {
+ this.message = message;
+ }
+
+ @Override
+ public String toString()
+ {
+ return message;
+ }
+ }
+
+ /**
+ * Returns the singleton instance.
+ *
+ * @return the singleton instance
+ */
+ public static Logger getInstance()
+ {
+ return LoggerImpl.getInstance();
+ }
+
+ /**
+ * Sets the logging level.
+ *
+ * @param level the logging level
+ */
+ public abstract void setLoggingLevel(LogLevel level);
+
+ /**
+ * Sets the log path.
+ *
+ * @param path the path of the log file
+ */
+ public abstract void setPath(String path);
+
+ /**
+ * Logs an error message.
+ *
+ * @param message the message to log
+ */
+ public abstract void error(String message);
+
+ /**
+ * Logs a warning message.
+ *
+ * @param message the message to log
+ */
+ public abstract void warn(String message);
+
+ /**
+ * Logs an informational message.
+ *
+ * @param message the message to log
+ */
+ public abstract void info(String message);
+
+ /**
+ * Logs a method tracing message.
+ *
+ * @param message the message to log
+ */
+ public abstract void trace(String message);
+}
diff --git a/core/java/android/speech/recognition/MediaFileReader.java b/core/java/android/speech/recognition/MediaFileReader.java
new file mode 100644
index 0000000..216511f
--- /dev/null
+++ b/core/java/android/speech/recognition/MediaFileReader.java
@@ -0,0 +1,90 @@
+/*---------------------------------------------------------------------------*
+ * MediaFileReader.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition;
+
+import android.speech.recognition.impl.MediaFileReaderImpl;
+
+/**
+ * Reads audio from a file.
+ */
+public abstract class MediaFileReader implements AudioSource
+{
+ /**
+ * Reading mode
+ */
+ public static class Mode
+ {
+ /**
+ * Read the file in "real time".
+ */
+ public static Mode REAL_TIME = new Mode("real-time");
+ /**
+ * Read the file all at once.
+ */
+ public static Mode ALL_AT_ONCE = new Mode("all at once");
+ private String message;
+
+ /**
+ * Creates a new Mode.
+ *
+ * @param message the message associated with the reading mode.
+ */
+ private Mode(String message)
+ {
+ this.message = message;
+ }
+ }
+
+ /**
+ * Creates a new MediaFileReader to read audio samples from a file.
+ *
+ * @param filename the name of the file to read from Note: The file MUST be of type Microsoft WAVE RIFF
+ * format (PCM 16 bits 8000 Hz or PCM 16 bits 11025 Hz).
+ * @param listener listens for MediaFileReader events
+ * @return a new MediaFileReader
+ * @throws IllegalArgumentException if filename is null or is an empty string. Or if offset > file length. Or if codec is null or invalid
+ */
+ public static MediaFileReader create(String filename, AudioSourceListener listener) throws IllegalArgumentException
+ {
+ return new MediaFileReaderImpl(filename, listener);
+ }
+
+ /**
+ * Sets the reading mode.
+ *
+ * @param mode the reading mode
+ */
+ public abstract void setMode(Mode mode);
+
+ /**
+ * Creates an audio source.
+ */
+ public abstract AudioStream createAudio();
+
+ /**
+ * Starts collecting audio samples.
+ */
+ public abstract void start();
+
+ /**
+ * Stops collecting audio samples.
+ */
+ public abstract void stop();
+}
diff --git a/core/java/android/speech/recognition/MediaFileReaderListener.java b/core/java/android/speech/recognition/MediaFileReaderListener.java
new file mode 100644
index 0000000..f76e65f
--- /dev/null
+++ b/core/java/android/speech/recognition/MediaFileReaderListener.java
@@ -0,0 +1,29 @@
+/*---------------------------------------------------------------------------*
+ * MediaFileReaderListener.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition;
+
+import android.speech.recognition.AudioSourceListener;
+
+/**
+ * Listens for MediaFileReader events.
+ */
+public interface MediaFileReaderListener extends AudioSourceListener
+{
+}
diff --git a/core/java/android/speech/recognition/MediaFileWriter.java b/core/java/android/speech/recognition/MediaFileWriter.java
new file mode 100644
index 0000000..b2d627c
--- /dev/null
+++ b/core/java/android/speech/recognition/MediaFileWriter.java
@@ -0,0 +1,49 @@
+/*---------------------------------------------------------------------------*
+ * MediaFileWriter.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition;
+
+import android.speech.recognition.impl.MediaFileWriterImpl;
+
+/**
+ * Writes audio to a file.
+ */
+public abstract class MediaFileWriter
+{
+ /**
+ * Creates a new MediaFileWriter to write audio samples into a file.
+ *
+ * @param listener listens for MediaFileWriter events
+ * @return a new MediaFileWriter
+ */
+ public static MediaFileWriter create(MediaFileWriterListener listener)
+ {
+ return new MediaFileWriterImpl(listener);
+ }
+
+ /**
+ * Saves audio to a file.
+ *
+ * @param source the audio stream to write
+ * @param filename the file to write to
+ * @throws IllegalArgumentException if source is null, in-use by another
+ * component or contains no data. Or if filename is null or is empty.
+ */
+ public abstract void save(AudioStream source, String filename) throws IllegalArgumentException;
+}
diff --git a/core/java/android/speech/recognition/MediaFileWriterListener.java b/core/java/android/speech/recognition/MediaFileWriterListener.java
new file mode 100644
index 0000000..e2104c8
--- /dev/null
+++ b/core/java/android/speech/recognition/MediaFileWriterListener.java
@@ -0,0 +1,40 @@
+/*---------------------------------------------------------------------------*
+ * MediaFileWriterListener.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition;
+
+/**
+ * Listens for MediaFileWriter events.
+ */
+public interface MediaFileWriterListener
+{
+ /**
+ * Invoked after the save() operation terminates
+ */
+ void onStopped();
+
+ /**
+ * Invoked when an unexpected error occurs. This is normally followed by
+ * onStopped() if the component shuts down successfully.
+ *
+ * @param e the cause of the failure.<br/>
+ * {@link java.io.IOException} if an error occured opening or writing to the file
+ */
+ void onError(Exception e);
+}
diff --git a/core/java/android/speech/recognition/Microphone.java b/core/java/android/speech/recognition/Microphone.java
new file mode 100644
index 0000000..1b713f5
--- /dev/null
+++ b/core/java/android/speech/recognition/Microphone.java
@@ -0,0 +1,76 @@
+/*---------------------------------------------------------------------------*
+ * Microphone.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition;
+
+import android.speech.recognition.impl.MicrophoneImpl;
+
+/**
+ * Records live audio.
+ */
+public abstract class Microphone implements AudioSource
+{
+ private static Microphone instance;
+
+ /**
+ * Returns the microphone instance
+ *
+ * @return an instance of a Microphone class.
+ */
+ public static Microphone getInstance()
+ {
+ instance = MicrophoneImpl.getInstance();
+ return instance;
+ }
+
+ /**
+ * Sets the recording codec. This must be called before start() is called.
+ *
+ * @param recordingCodec the codec in which the samples will be recorded.
+ * @throws IllegalStateException if Microphone is started
+ * @throws IllegalArgumentException if codec is not supported
+ */
+ public abstract void setCodec(Codec recordingCodec) throws IllegalStateException,
+ IllegalArgumentException;
+
+ /**
+ * Sets the microphone listener.
+ *
+ * @param listener the microphone listener.
+ * @throws IllegalStateException if Microphone is started
+ */
+ public abstract void setListener(AudioSourceListener listener) throws IllegalStateException;
+
+ /**
+ * Creates an audio source
+ */
+ public abstract AudioStream createAudio();
+
+ /**
+ * Start recording audio.
+ *
+ * @throws IllegalStateException if Microphone is already started
+ */
+ public abstract void start() throws IllegalStateException;
+
+ /**
+ * Stops recording audio.
+ */
+ public abstract void stop();
+}
diff --git a/core/java/android/speech/recognition/MicrophoneListener.java b/core/java/android/speech/recognition/MicrophoneListener.java
new file mode 100644
index 0000000..f43eff9
--- /dev/null
+++ b/core/java/android/speech/recognition/MicrophoneListener.java
@@ -0,0 +1,29 @@
+/*---------------------------------------------------------------------------*
+ * MicrophoneListener.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition;
+
+import android.speech.recognition.AudioSourceListener;
+
+/**
+ * Listens for Microphone events.
+ */
+public interface MicrophoneListener extends AudioSourceListener
+{
+}
diff --git a/core/java/android/speech/recognition/NBestRecognitionResult.java b/core/java/android/speech/recognition/NBestRecognitionResult.java
new file mode 100644
index 0000000..e679c19
--- /dev/null
+++ b/core/java/android/speech/recognition/NBestRecognitionResult.java
@@ -0,0 +1,113 @@
+/*---------------------------------------------------------------------------*
+ * NBestRecognitionResult.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition;
+
+import java.util.Enumeration;
+
+/**
+ * N-Best recognition results. Entries are sorted in decreasing order according
+ * to their probability, from the most probable result to the least probable
+ * result.
+ */
+public interface NBestRecognitionResult extends RecognitionResult
+{
+ /**
+ * Recognition result entry
+ */
+ public static interface Entry
+ {
+ /**
+ * Returns the semantic meaning of a recognition result (i.e.&nbsp;the application-specific value
+ * associated with what the user said). In an example where a person's name is mapped
+ * to a phone-number, the phone-number is the semantic meaning.
+ *
+ * @return the semantic meaning of a recognition result.
+ * @throws IllegalStateException if the object has been disposed
+ */
+ String getSemanticMeaning() throws IllegalStateException;
+
+ /**
+ * The confidence score of a recognition result. Values range from 0 to 100
+ * (inclusive).
+ *
+ * @return the confidence score of a recognition result.
+ * @throws IllegalStateException if the object has been disposed
+ */
+ byte getConfidenceScore() throws IllegalStateException;
+
+ /**
+ * Returns the literal meaning of a recognition result (i.e.&nbsp;literally
+ * what the user said). In an example where a person's name is mapped to a
+ * phone-number, the person's name is the literal meaning.
+ *
+ * @return the literal meaning of a recognition result.
+ * @throws IllegalStateException if the object has been disposed
+ */
+ String getLiteralMeaning() throws IllegalStateException;
+
+ /**
+ * Returns the value associated with the specified key.
+ *
+ * @param key the key to look up
+ * @return the associated value or null if this entry does not contain
+ * any mapping for the key
+ */
+ String get(String key);
+
+ /**
+ * Returns an enumeration of the keys in this Entry.
+ *
+ * @return an enumeration of the keys in this Entry.
+ */
+ Enumeration keys();
+ }
+
+ /**
+ * Returns the number of entries in the n-best list.
+ *
+ * @return the number of entries in the n-best list
+ */
+ int getSize();
+
+ /**
+ * Returns the n-best entry that contains key-value pairs associated with the
+ * recognition result.
+ *
+ * @param index the index of the n-best entry
+ * @return null if all active GrammarConfiguration.grammarToMeaning() return
+ * null
+ * @throws ArrayIndexOutOfBoundsException if index is greater than size of
+ * entries
+ */
+ Entry getEntry(int index) throws ArrayIndexOutOfBoundsException;
+
+ /**
+ * Creates a new VoicetagItem if the last recognition was an enrollment
+ * operation.
+ *
+ * @param VoicetagId string voicetag unique id value.
+ * @param listener listens for Voicetag events
+ * @return the resulting VoicetagItem
+ * @throws IllegalArgumentException if VoicetagId is null or an empty string.
+ * @throws IllegalStateException if the last recognition was not an
+ * enrollment operation
+ */
+ VoicetagItem createVoicetagItem(String VoicetagId, VoicetagItemListener listener) throws IllegalArgumentException,IllegalStateException;
+}
diff --git a/core/java/android/speech/recognition/ParameterErrorException.java b/core/java/android/speech/recognition/ParameterErrorException.java
new file mode 100644
index 0000000..042ed31
--- /dev/null
+++ b/core/java/android/speech/recognition/ParameterErrorException.java
@@ -0,0 +1,33 @@
+/*---------------------------------------------------------------------------*
+ * ParameterErrorException.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition;
+
+/**
+ * Thrown if an error occurs in the audio driver.
+ */
+public class ParameterErrorException extends Exception
+{
+ private static final long serialVersionUID = 0L;
+
+ public ParameterErrorException(String msg)
+ {
+ super(msg);
+ }
+}
diff --git a/core/java/android/speech/recognition/ParametersListener.java b/core/java/android/speech/recognition/ParametersListener.java
new file mode 100644
index 0000000..bdb551e
--- /dev/null
+++ b/core/java/android/speech/recognition/ParametersListener.java
@@ -0,0 +1,63 @@
+/*---------------------------------------------------------------------------*
+ * ParametersListener.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition;
+
+import java.util.Hashtable;
+import java.util.Vector;
+
+/**
+ * Listens for parameter events.
+ */
+public interface ParametersListener
+{
+ /**
+ * Invoked if retrieving parameters has failed.
+ *
+ * @param parameters the parameters that could not be retrieved
+ * @param e the failure reason
+ */
+ void onParametersGetError(Vector<String> parameters, Exception e);
+
+ /**
+ * Invoked if setting parameters has failed.
+ *
+ * @param parameters the parameters that could not be set
+ * @param e the failure reason
+ */
+ void onParametersSetError(Hashtable<String, String> parameters, Exception e);
+
+ /**
+ * This method is called when the parameters specified in setParameters have
+ * successfully been set. This method is guaranteed to be invoked after
+ * onParametersSetError, even if count==0.
+ *
+ * @param parameters the set parameters
+ */
+ void onParametersSet(Hashtable<String, String> parameters);
+
+ /**
+ * This method is called when the parameters specified in getParameters have
+ * successfully been retrieved. This method is guaranteed to be invoked after
+ * onParametersGetError, even if count==0.
+ *
+ * @param parameters the retrieved parameters
+ */
+ void onParametersGet(Hashtable<String, String> parameters);
+}
diff --git a/core/java/android/speech/recognition/ParseErrorException.java b/core/java/android/speech/recognition/ParseErrorException.java
new file mode 100644
index 0000000..2288a90
--- /dev/null
+++ b/core/java/android/speech/recognition/ParseErrorException.java
@@ -0,0 +1,33 @@
+/*---------------------------------------------------------------------------*
+ * ParseErrorException.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition;
+
+/**
+ * Thrown if an error occurs in the audio driver.
+ */
+public class ParseErrorException extends Exception
+{
+ private static final long serialVersionUID = 0L;
+
+ public ParseErrorException(String msg)
+ {
+ super(msg);
+ }
+}
diff --git a/core/java/android/speech/recognition/RecognitionResult.java b/core/java/android/speech/recognition/RecognitionResult.java
new file mode 100644
index 0000000..cbbc938
--- /dev/null
+++ b/core/java/android/speech/recognition/RecognitionResult.java
@@ -0,0 +1,27 @@
+/*---------------------------------------------------------------------------*
+ * RecognitionResult.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition;
+
+/**
+ * Recognition result interface.
+ */
+public interface RecognitionResult
+{
+}
diff --git a/core/java/android/speech/recognition/Recognizer.java b/core/java/android/speech/recognition/Recognizer.java
new file mode 100644
index 0000000..ab7f8f4
--- /dev/null
+++ b/core/java/android/speech/recognition/Recognizer.java
@@ -0,0 +1,102 @@
+/*---------------------------------------------------------------------------*
+ * Recognizer.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition;
+
+import java.util.Hashtable;
+import java.util.Vector;
+
+/**
+ * Speech recognizer interface.
+ */
+public interface Recognizer
+{
+ /**
+ * Sets the recognizer event listener.
+ *
+ * @param listener listens for recognizer events
+ */
+ void setListener(RecognizerListener listener);
+
+ /**
+ * Creates an embedded grammar.
+ *
+ * @param value value of that grammarType. Could be a URL or an inline grammar.
+ * @return a grammar
+ * @throws IllegalArgumentException if value is null or listener is not of type
+ * GrammarListener.
+ */
+ Grammar createGrammar(String value, GrammarListener listener) throws IllegalArgumentException;
+
+ /**
+ * Begins speech recognition.
+ *
+ * @param audio the audio stream to recognizer
+ * @param grammars a collection of grammar sets to recognize against
+ * @see #recognize(AudioStream, Grammar)
+ * @throws IllegalStateException if any of the grammars are not loaded
+ * @throws IllegalArgumentException if audio is null, in-use by another
+ * component or empty. Or if grammars is null or grammars count is less than
+ * one. Or if the audio codec differs from recognizer codec.
+ * @throws UnsupportedOperationException if the recognizer does not support
+ * the number of grammars specified.
+ */
+ void recognize(AudioStream audio,
+ Vector<Grammar> grammars) throws IllegalStateException,
+ IllegalArgumentException, UnsupportedOperationException;
+
+ /**
+ * This convenience method is equivalent to invoking
+ * recognize(audio, grammars) with a single grammar.
+ *
+ * @param audio the audio to recognizer
+ * @param grammar a grammar to recognize against
+ * @see #recognize(AudioStream, Vector)
+ * @throws IllegalStateException if grammar is not loaded
+ * @throws IllegalArgumentException if audio is null, in-use by another
+ * component or is empty. Or if grammar is null or if the audio codec differs
+ * from the recognizer codec.
+ */
+ void recognize(AudioStream audio, Grammar grammar) throws IllegalStateException,
+ IllegalArgumentException;
+
+ /**
+ * Terminates a recognition if one is in-progress.
+ * This must not be called until the recognize method
+ * returns; otherwise the result is not defined.
+ *
+ * @see RecognizerListener#onStopped
+ */
+ void stop();
+
+ /**
+ * Sets the values of recognition parameters.
+ *
+ * @param parameters the parameter key-value pairs to set
+ */
+ void setParameters(Hashtable<String, String> parameters);
+
+ /**
+ * Retrieves the values of recognition parameters.
+ *
+ * @param parameters the names of the parameters to retrieve
+ */
+ void getParameters(Vector<String> parameters);
+
+}
diff --git a/core/java/android/speech/recognition/RecognizerListener.java b/core/java/android/speech/recognition/RecognizerListener.java
new file mode 100644
index 0000000..d7bbda9
--- /dev/null
+++ b/core/java/android/speech/recognition/RecognizerListener.java
@@ -0,0 +1,142 @@
+/*---------------------------------------------------------------------------*
+ * RecognizerListener.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition;
+
+/**
+ * Listens for recognizer events.
+ */
+public interface RecognizerListener extends ParametersListener
+{
+ /**
+ * Recognition failure.
+ */
+ public static class FailureReason
+ {
+ /**
+ * The audio did not generate any results.
+ */
+ public static FailureReason NO_MATCH =
+ new FailureReason("The audio did not generate any results");
+ /**
+ * Beginning of speech occured too soon.
+ */
+ public static FailureReason SPOKE_TOO_SOON =
+ new FailureReason("Beginning of speech occurred too soon");
+ /**
+ * A timeout occured before the beginning of speech.
+ */
+ public static FailureReason BEGINNING_OF_SPEECH_TIMEOUT =
+ new FailureReason("A timeout occurred before the beginning of " + "speech");
+ /**
+ * A timeout occured before the recognition could complete.
+ */
+ public static FailureReason RECOGNITION_TIMEOUT =
+ new FailureReason("A timeout occurred before the recognition " +
+ "could complete");
+ /**
+ * The recognizer encountered more audio than was acceptable according to
+ * its configuration.
+ */
+ public static FailureReason TOO_MUCH_SPEECH =
+ new FailureReason("The " +
+ "recognizer encountered more audio than was acceptable according to " +
+ "its configuration");
+
+ public static FailureReason UNKNOWN =
+ new FailureReason("unknown failure reason");
+
+ private final String message;
+
+ private FailureReason(String message)
+ {
+ this.message = message;
+ }
+
+ @Override
+ public String toString()
+ {
+ return message;
+ }
+ }
+
+ /**
+ * Invoked after recognition begins.
+ */
+ void onStarted();
+
+ /**
+ * Invoked if the recognizer detects the beginning of speech.
+ */
+ void onBeginningOfSpeech();
+
+ /**
+ * Invoked if the recognizer detects the end of speech.
+ */
+ void onEndOfSpeech();
+
+ /**
+ * Invoked if the recognizer does not detect speech within the configured
+ * timeout period.
+ */
+ void onStartOfSpeechTimeout();
+
+ /**
+ * Invoked when the recognizer acoustic state is reset.
+ *
+ * @see android.speech.recognition.EmbeddedRecognizer#resetAcousticState()
+ */
+ void onAcousticStateReset();
+
+ /**
+ * Invoked when a recognition result is generated.
+ *
+ * @param result the recognition result. The result object can not be
+ * used outside of the scope of the onRecognitionSuccess() callback method.
+ * To be able to do so, copy it's contents to an user-defined object.<BR>
+ * An example of this object could be a vector of string arrays; where the
+ * vector represents a list of recognition result entries and each entry
+ * is an array of strings to hold the entry's values (the semantic
+ * meaning, confidence score and literal meaning).
+ */
+ void onRecognitionSuccess(RecognitionResult result);
+
+ /**
+ * Invoked when a recognition failure occurs.
+ *
+ * @param reason the failure reason
+ */
+ void onRecognitionFailure(FailureReason reason);
+
+ /**
+ * Invoked when an unexpected error occurs. This is normally followed by
+ * onStopped() if the component shuts down successfully.
+ *
+ * @param e the cause of the failure
+ */
+ void onError(Exception e);
+
+ /**
+ * Invoked when the recognizer stops (due to normal termination or an error).
+ *
+ * Invoking stop() on a recognizer that is already stopped will not result
+ * in a onStopped() event.
+ */
+ void onStopped();
+}
diff --git a/core/java/android/speech/recognition/SlotItem.java b/core/java/android/speech/recognition/SlotItem.java
new file mode 100644
index 0000000..3abd27a
--- /dev/null
+++ b/core/java/android/speech/recognition/SlotItem.java
@@ -0,0 +1,27 @@
+/*---------------------------------------------------------------------------*
+ * SlotItem.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition;
+
+/**
+ * Item that may be inserted into an embedded grammar slot.
+ */
+public interface SlotItem
+{
+}
diff --git a/core/java/android/speech/recognition/SrecGrammar.java b/core/java/android/speech/recognition/SrecGrammar.java
new file mode 100644
index 0000000..c591e05
--- /dev/null
+++ b/core/java/android/speech/recognition/SrecGrammar.java
@@ -0,0 +1,81 @@
+/*---------------------------------------------------------------------------*
+ * SrecGrammar.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition;
+import java.util.Vector;
+
+/**
+ * Grammar on an SREC recognizer.
+ */
+public interface SrecGrammar extends EmbeddedGrammar
+{
+ /**
+ * SrecGrammar Item
+ */
+ public class Item
+ {
+ public SlotItem _item;
+ public int _weight;
+ public String _semanticMeaning;
+
+ /**
+ * Creates a grammar item.
+ *
+ * @param item the Slotitem.
+ * @param weight the weight of the item. Smaller values are more likely to get recognized. This should be >= 0.
+ * @param semanticMeaning the value that will be returned if this item is recognized.
+ * @throws IllegalArgumentException if item or semanticMeaning are null; if semanticMeaning is empty."
+ */
+ public Item(SlotItem item, int weight, String semanticMeaning)
+ throws IllegalArgumentException
+ {
+ if (item == null)
+ throw new IllegalArgumentException("Item(): item can't be null.");
+ if (semanticMeaning == null || semanticMeaning.length()==0)
+ throw new IllegalArgumentException("Item(): semanticMeaning is null or empty.");
+ _item = item;
+ _weight = weight;
+ _semanticMeaning = semanticMeaning;
+
+ }
+ }
+
+ /**
+ * Adds an item to a slot.
+ *
+ * @param slotName the name of the slot
+ * @param item the item to add to the slot.
+ * @param weight the weight of the item. Smaller values are more likely to get recognized. This should be >= 0.
+ * @param semanticMeaning the value that will be returned if this item is recognized.
+ * @throws IllegalArgumentException if slotName, item or semanticMeaning are null; if semanticMeaning is not of the format "V=&#039;Jen_Parker&#039;"
+ */
+ public void addItem(String slotName, SlotItem item, int weight,
+ String semanticMeaning) throws IllegalArgumentException;
+
+ /**
+ * Add a list of item to a slot.
+ *
+ * @param slotName the name of the slot
+ * @param items the vector of SrecGrammar.Item to add to the slot.
+ * @throws IllegalArgumentException if slotName,items are null or any element in the items(_item, _semanticMeaning) is null; if any semanticMeaning of the list is not of the format "key=&#039;value&#039"
+ */
+ public void addItemList(String slotName, Vector<Item> items)
+ throws IllegalArgumentException;
+
+}
diff --git a/core/java/android/speech/recognition/SrecGrammarListener.java b/core/java/android/speech/recognition/SrecGrammarListener.java
new file mode 100644
index 0000000..e1f7d3f
--- /dev/null
+++ b/core/java/android/speech/recognition/SrecGrammarListener.java
@@ -0,0 +1,58 @@
+/*---------------------------------------------------------------------------*
+ * SrecGrammarListener.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition;
+
+/**
+ * Listens for SrecGrammar events.
+ */
+public interface SrecGrammarListener extends EmbeddedGrammarListener {
+
+ /**
+ * Invokes after all items of the list have been added.
+ */
+ void onAddItemList();
+
+ /**
+ * Invoked when adding a SlotItem from a list fails.
+ * This callback will be trigger for each element in the list that fails to be
+ * add in the slot, unless there is a grammar fail operation, which will be
+ * reported in the onError callback.
+ * @param index of the list that could not be added to the slot
+ * @param e the cause of the failure.
+ */
+ void onAddItemListFailure(int index, Exception e);
+
+
+ /**
+ * Invoked when a grammar related operation fails.
+ *
+ * @param e the cause of the failure.<br/>
+ * {@link GrammarOverflowException} if the grammar slot is full and no
+ * further items may be added to it.<br/>
+ * {@link java.lang.UnsupportedOperationException} if different words with
+ * the same pronunciation are added.<br/>
+ * {@link java.lang.IllegalStateException} if reseting or compiling the
+ * slots fails.<br/>
+ * {@link java.io.IOException} if the grammar could not be loaded or
+ * saved.</p>
+ */
+ void onError(Exception e);
+
+}
diff --git a/core/java/android/speech/recognition/VoicetagItem.java b/core/java/android/speech/recognition/VoicetagItem.java
new file mode 100644
index 0000000..0b89639
--- /dev/null
+++ b/core/java/android/speech/recognition/VoicetagItem.java
@@ -0,0 +1,82 @@
+/*---------------------------------------------------------------------------*
+ * VoicetagItem.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition;
+
+import android.speech.recognition.impl.VoicetagItemImpl;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+/**
+ * Voicetag that may be inserted into an embedded grammar slot.
+ */
+public abstract class VoicetagItem implements SlotItem
+{
+ /**
+ * Creates a VoicetagItem from a file
+ *
+ * @param filename filename for Voicetag
+ * @param listener listens for Voicetag events
+ * @return the resulting VoicetagItem
+ * @throws IllegalArgumentException if filename is null or an empty string.
+ * @throws FileNotFoundException if the specified filename could not be found
+ * @throws IOException if the specified filename could not be opened
+ */
+ public static VoicetagItem create(String filename, VoicetagItemListener listener) throws IllegalArgumentException,FileNotFoundException,IOException
+ {
+ return VoicetagItemImpl.create(filename,listener);
+ }
+ /**
+ * Returns the audio used to construct the VoicetagItem.
+ * The audio is in PCM format and is start-pointed and end-pointed. The audio
+ * is only generated if the enableGetWaveform recognition parameter
+ * is set prior to recognition.
+ *
+ * @throws IllegalStateException if the recognition parameter 'enableGetWaveform' is not set
+ * @return the audio used to construct the VoicetagItem.
+ */
+ public abstract byte[] getAudio() throws IllegalStateException;
+
+ /**
+ * Sets the audio used to construct the Voicetag. The
+ * audio is in PCM format and is start-pointed and end-pointed. The audio is
+ * only generated if the enableGetWaveform recognition parameter is set
+ * prior to recognition.
+ *
+ * @param waveform the endpointed waveform
+ * @throws IllegalArgumentException if waveform is null or empty.
+ * @throws IllegalStateException if the recognition parameter 'enableGetWaveform' is not set
+ */
+ public abstract void setAudio(byte[] waveform) throws IllegalArgumentException,IllegalStateException;
+
+ /**
+ * Save the Voicetag Item.
+ *
+ * @param path where the Voicetag will be saved. We strongly recommend to set the filename with the same value of the VoicetagId.
+ * @throws IllegalArgumentException if path is null or an empty string.
+ */
+ public abstract void save(String path) throws IllegalArgumentException,IllegalStateException;
+
+ /**
+ * Load a Voicetag Item.
+ *
+ * @throws IllegalStateException if voicetag has not been created from a file.
+ */
+ public abstract void load() throws IllegalStateException;
+
+}
diff --git a/core/java/android/speech/recognition/VoicetagItemListener.java b/core/java/android/speech/recognition/VoicetagItemListener.java
new file mode 100644
index 0000000..610d1c7
--- /dev/null
+++ b/core/java/android/speech/recognition/VoicetagItemListener.java
@@ -0,0 +1,49 @@
+/*---------------------------------------------------------------------------*
+ * VoicetagItemListener.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition;
+
+/**
+ * Listens for VoicetagItem events.
+ */
+public interface VoicetagItemListener
+{
+ /**
+ * Invoked after the Voicetag is saved.
+ *
+ * @param path the path the Voicetag was saved to
+ */
+ void onSaved(String path);
+
+ /**
+ * Invoked after the Voicetag is loaded.
+ */
+ void onLoaded();
+
+ /**
+ * Invoked when a grammar operation fails.
+ *
+ * @param e the cause of the failure.<br/>
+ * {@link java.io.IOException} if the Voicetag could not be loaded or
+ * saved.</p>
+ * {@link java.io.FileNotFoundException} if the specified file could not be found
+ */
+ void onError(Exception e);
+
+}
diff --git a/core/java/android/speech/recognition/WordItem.java b/core/java/android/speech/recognition/WordItem.java
new file mode 100644
index 0000000..5c21c98
--- /dev/null
+++ b/core/java/android/speech/recognition/WordItem.java
@@ -0,0 +1,58 @@
+/*---------------------------------------------------------------------------*
+ * WordItem.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition;
+
+import android.speech.recognition.impl.WordItemImpl;
+
+/**
+ * Word that may be inserted into an embedded grammar slot.
+ */
+public abstract class WordItem implements SlotItem
+{
+ /**
+ * Creates a new WordItem.
+ *
+ * @param word the word to insert
+ * @param pronunciations the pronunciations to associated with the item. If the list is
+ * is empty (example:new String[0]) the recognizer will attempt to guess the pronunciations.
+ * @return the WordItem
+ * @throws IllegalArgumentException if word is null or if pronunciations is
+ * null or pronunciations contains an element equal to null or empty string.
+ */
+ public static WordItem valueOf(String word, String[] pronunciations) throws IllegalArgumentException
+ {
+ return WordItemImpl.valueOf(word, pronunciations);
+ }
+
+ /**
+ * Creates a new WordItem.
+ *
+ * @param word the word to insert
+ * @param pronunciation the pronunciation to associate with the item. If it
+ * is null the recognizer will attempt to guess the pronunciations.
+ * @return the WordItem
+ * @throws IllegalArgumentException if word is null or if pronunciation is
+ * an empty string
+ */
+ public static WordItem valueOf(String word, String pronunciation) throws IllegalArgumentException
+ {
+ return WordItemImpl.valueOf(word, pronunciation);
+ }
+}
diff --git a/core/java/android/speech/recognition/impl/AudioStreamImpl.java b/core/java/android/speech/recognition/impl/AudioStreamImpl.java
new file mode 100644
index 0000000..730e2d9
--- /dev/null
+++ b/core/java/android/speech/recognition/impl/AudioStreamImpl.java
@@ -0,0 +1,84 @@
+/*---------------------------------------------------------------------------*
+ * AudioStreamImpl.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition.impl;
+
+import android.speech.recognition.AudioStream;
+
+/**
+ */
+public class AudioStreamImpl implements AudioStream, Runnable
+{
+ /**
+ * Reference to the native object.
+ */
+ private long nativeObject;
+
+ /**
+ * Creates a new AudioStreamImpl.
+ *
+ * @param nativeObj a reference to the native object
+ */
+ public AudioStreamImpl(long nativeObj)
+ {
+ nativeObject = nativeObj;
+ }
+
+ public synchronized void run()
+ {
+ dispose();
+ }
+
+ public long getNativeObject() {
+ synchronized (AudioStreamImpl.class)
+ {
+ return nativeObject;
+ }
+ }
+
+ /**
+ * Releases the native resources associated with the object.
+ */
+ @SuppressWarnings("deprecation")
+ public void dispose()
+ {
+ synchronized (AudioStreamImpl.class)
+ {
+ if (nativeObject != 0)
+ {
+ deleteNativeObject(nativeObject);
+ nativeObject = 0;
+ }
+ }
+ }
+
+ @Override
+ protected void finalize() throws Throwable
+ {
+ dispose();
+ super.finalize();
+ }
+
+ /**
+ * Deletes a native object.
+ *
+ * @param nativeObject pointer to the native object
+ */
+ private native void deleteNativeObject(long nativeObject);
+}
diff --git a/core/java/android/speech/recognition/impl/DeviceSpeakerImpl.java b/core/java/android/speech/recognition/impl/DeviceSpeakerImpl.java
new file mode 100644
index 0000000..5d72110
--- /dev/null
+++ b/core/java/android/speech/recognition/impl/DeviceSpeakerImpl.java
@@ -0,0 +1,164 @@
+/*---------------------------------------------------------------------------*
+ * DeviceSpeakerImpl.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition.impl;
+
+import android.speech.recognition.AudioStream;
+import android.speech.recognition.Codec;
+import android.speech.recognition.DeviceSpeaker;
+import android.speech.recognition.DeviceSpeakerListener;
+
+/**
+ */
+public class DeviceSpeakerImpl extends DeviceSpeaker implements Runnable
+{
+ private static DeviceSpeakerImpl instance;
+ /**
+ * Reference to the native object.
+ */
+ private long nativeObject;
+ private DeviceSpeakerListener locallistener;
+
+ /**
+ * Private constructor
+ */
+ private DeviceSpeakerImpl()
+ {
+ System system = System.getInstance();
+ nativeObject = initNativeObject();
+ if (nativeObject != 0)
+ system.register(this);
+ }
+
+ public void run()
+ {
+ dispose();
+ }
+
+ /**
+ * Returns the singleton instance.
+ *
+ * @return the singleton instance
+ */
+ public static DeviceSpeakerImpl getInstance()
+ {
+ synchronized (DeviceSpeakerImpl.class)
+ {
+ if (instance == null)
+ instance = new DeviceSpeakerImpl();
+ return instance;
+ }
+ }
+
+ /**
+ * Start audio playback.
+ *
+ * @param source the audio to play
+ */
+ public void start(AudioStream source)
+ {
+ synchronized (DeviceSpeakerImpl.class)
+ {
+ if (nativeObject == 0)
+ throw new IllegalStateException("Object was destroyed.");
+ AudioStreamImpl src = (AudioStreamImpl)source;
+ startProxy(nativeObject,src.getNativeObject());
+ src = null;
+ }
+ }
+
+ /**
+ * Stops audio playback.
+ */
+ public void stop()
+ {
+ synchronized (DeviceSpeakerImpl.class)
+ {
+ if (nativeObject == 0)
+ throw new IllegalStateException("Object was destroyed.");
+ stopProxy(nativeObject);
+ }
+ }
+
+ /**
+ * Set the playback codec. This must be called before start is called.
+ * @param playbackCodec the codec to use for the playback operation.
+ */
+ public void setCodec(Codec playbackCodec)
+ {
+ synchronized (DeviceSpeakerImpl.class)
+ {
+ if (nativeObject == 0)
+ throw new IllegalStateException("Object was destroyed.");
+ setCodecProxy(nativeObject,playbackCodec);
+ }
+ }
+
+ /**
+ * set the microphone listener.
+ * @param listener the device speaker listener.
+ */
+ public void setListener(DeviceSpeakerListener listener)
+ {
+ synchronized (DeviceSpeakerImpl.class)
+ {
+ if (nativeObject == 0)
+ throw new IllegalStateException("Object was destroyed.");
+ locallistener = listener;
+ setListenerProxy(nativeObject,listener);
+ }
+ }
+
+ /**
+ * Releases the native resources associated with the object.
+ */
+ private void dispose()
+ {
+ synchronized (DeviceSpeakerImpl.class)
+ {
+ if (nativeObject != 0)
+ {
+ deleteNativeObject(nativeObject);
+ nativeObject = 0;
+ instance = null;
+ locallistener = null;
+ System.getInstance().unregister(this);
+ }
+ }
+ }
+
+ @Override
+ protected void finalize() throws Throwable
+ {
+ dispose();
+ super.finalize();
+ }
+
+ private native long initNativeObject();
+
+ private native void startProxy(long nativeObject, long audioNativeObject);
+
+ private native void stopProxy(long nativeObject);
+
+ private native void setCodecProxy(long nativeObject,Codec playbackCodec);
+
+ private native void setListenerProxy(long nativeObject,DeviceSpeakerListener listener);
+
+ private native void deleteNativeObject(long nativeObject);
+}
diff --git a/core/java/android/speech/recognition/impl/EmbeddedGrammarImpl.java b/core/java/android/speech/recognition/impl/EmbeddedGrammarImpl.java
new file mode 100644
index 0000000..0b88cb2
--- /dev/null
+++ b/core/java/android/speech/recognition/impl/EmbeddedGrammarImpl.java
@@ -0,0 +1,73 @@
+/*---------------------------------------------------------------------------*
+ * EmbeddedGrammarImpl.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition.impl;
+
+import android.speech.recognition.EmbeddedGrammar;
+
+/**
+ */
+public class EmbeddedGrammarImpl extends GrammarImpl implements EmbeddedGrammar
+{
+ /**
+ * Creates a new EmbeddedGrammarImpl.
+ *
+ * @param nativeObject a reference to the native object
+ */
+ public EmbeddedGrammarImpl(long nativeObject)
+ {
+ super(nativeObject);
+ }
+
+ public void compileAllSlots()
+ {
+ synchronized (GrammarImpl.class)
+ {
+ if (nativeObject == 0)
+ throw new IllegalStateException("Object was destroyed.");
+ compileAllSlotsProxy(nativeObject);
+ }
+ }
+
+ public void resetAllSlots()
+ {
+ synchronized (GrammarImpl.class)
+ {
+ if (nativeObject == 0)
+ throw new IllegalStateException("Object was destroyed.");
+ resetAllSlotsProxy(nativeObject);
+ }
+ }
+
+ public void save(String url)
+ {
+ synchronized (GrammarImpl.class)
+ {
+ if (nativeObject == 0)
+ throw new IllegalStateException("Object was destroyed.");
+ saveProxy(nativeObject, url.toString());
+ }
+ }
+
+ private native void compileAllSlotsProxy(long nativeObject);
+
+ private native void resetAllSlotsProxy(long nativeObject);
+
+ private native void saveProxy(long nativeObject, String url);
+}
diff --git a/core/java/android/speech/recognition/impl/EmbeddedRecognizerImpl.java b/core/java/android/speech/recognition/impl/EmbeddedRecognizerImpl.java
new file mode 100644
index 0000000..f04bfe4
--- /dev/null
+++ b/core/java/android/speech/recognition/impl/EmbeddedRecognizerImpl.java
@@ -0,0 +1,246 @@
+/*---------------------------------------------------------------------------*
+ * EmbeddedRecognizerImpl.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition.impl;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.Hashtable;
+import java.util.Vector;
+import android.speech.recognition.EmbeddedRecognizer;
+import android.speech.recognition.Grammar;
+import android.speech.recognition.AudioStream;
+import android.speech.recognition.Grammar;
+import android.speech.recognition.RecognizerListener;
+import android.speech.recognition.GrammarListener;
+
+/**
+ */
+public class EmbeddedRecognizerImpl extends EmbeddedRecognizer implements Runnable
+{
+ /**
+ * Reference to the native object.
+ */
+ private long nativeObject;
+ /**
+ * The singleton instance.
+ */
+ private static EmbeddedRecognizerImpl instance;
+
+ /**
+ * Creates a new instance.
+ */
+ EmbeddedRecognizerImpl()
+ {
+ System system = System.getInstance();
+ nativeObject = getInstanceProxy();
+ if (nativeObject != 0)
+ system.register(this);
+ }
+
+ /**
+ * Returns the singleton instance.
+ *
+ * @return the singleton instance
+ */
+ public synchronized static EmbeddedRecognizerImpl getInstance()
+ {
+ synchronized (EmbeddedRecognizerImpl.class)
+ {
+ if (instance == null)
+ instance = new EmbeddedRecognizerImpl();
+ return instance;
+ }
+ }
+
+ public void run()
+ {
+ dispose();
+ }
+
+ /**
+ * Releases the native resources associated with the object.
+ */
+ private void dispose()
+ {
+ synchronized (EmbeddedRecognizerImpl.class)
+ {
+ if (instance != null)
+ {
+ deleteNativeObject(nativeObject);
+ nativeObject = 0;
+ instance = null;
+ System.getInstance().unregister(this);
+ }
+ }
+ }
+
+ public void configure(String config) throws IllegalArgumentException,
+ FileNotFoundException, IOException, UnsatisfiedLinkError,
+ ClassNotFoundException
+ {
+ synchronized (EmbeddedRecognizerImpl.class)
+ {
+ if (nativeObject == 0)
+ throw new IllegalStateException("Object was destroyed.");
+ if (config == null)
+ throw new IllegalArgumentException("Configuration Is Null.");
+ configureProxy(nativeObject,config);
+ }
+ }
+
+ public void setListener(RecognizerListener listener)
+ {
+ synchronized (EmbeddedRecognizerImpl.class)
+ {
+ if (nativeObject == 0)
+ throw new IllegalStateException("Object was destroyed.");
+ setListenerProxy(nativeObject,listener);
+ }
+ }
+
+ public Grammar createGrammar(String value, GrammarListener listener)
+ throws IllegalArgumentException
+ {
+ synchronized (EmbeddedRecognizerImpl.class)
+ {
+ if (nativeObject == 0)
+ throw new IllegalStateException("Object was destroyed.");
+ long nativeGrammar = createEmbeddedGrammarProxy(nativeObject,value.toString(), listener);
+ return new SrecGrammarImpl(nativeGrammar);
+ }
+ }
+
+ public void resetAcousticState()
+ {
+ synchronized (EmbeddedRecognizerImpl.class)
+ {
+ if (nativeObject == 0)
+ throw new IllegalStateException("Object was destroyed.");
+ resetAcousticStateProxy(nativeObject);
+ }
+ }
+
+ public void recognize(AudioStream audio,
+ Vector<Grammar> grammars)
+ {
+ synchronized (EmbeddedRecognizerImpl.class)
+ {
+ if (nativeObject == 0)
+ throw new IllegalStateException("Object was destroyed.");
+
+ if (audio == null)
+ throw new IllegalArgumentException("AudioStream cannot be null.");
+
+ if (grammars == null || grammars.isEmpty() == true)
+ throw new IllegalArgumentException("Grammars are null or empty.");
+ int grammarCount = grammars.size();
+
+ long[] nativeGrammars = new long[grammarCount];
+
+ for (int i = 0; i < grammarCount; ++i)
+ nativeGrammars[i] = ((GrammarImpl) grammars.get(i)).getNativeObject();
+
+ recognizeProxy(nativeObject,((AudioStreamImpl)audio).getNativeObject(), nativeGrammars);
+ }
+ }
+
+ public void recognize(AudioStream audio,
+ Grammar grammar)
+ {
+ synchronized (EmbeddedRecognizerImpl.class)
+ {
+ if (nativeObject == 0)
+ throw new IllegalStateException("Object was destroyed.");
+ }
+ Vector<Grammar> grammars = new Vector<Grammar>();
+ grammars.add(grammar);
+ recognize(audio, grammars);
+ }
+
+ public void stop()
+ {
+ synchronized (EmbeddedRecognizerImpl.class)
+ {
+ if (nativeObject == 0)
+ throw new IllegalStateException("Object was destroyed.");
+ stopProxy(nativeObject);
+ }
+ }
+
+ public void setParameters(Hashtable<String, String> params)
+ {
+ synchronized (EmbeddedRecognizerImpl.class)
+ {
+ if (nativeObject == 0)
+ throw new IllegalStateException("Object was destroyed.");
+ setParametersProxy(nativeObject,params);
+ }
+ }
+
+ public void getParameters(Vector<String> params)
+ {
+ synchronized (EmbeddedRecognizerImpl.class)
+ {
+ if (nativeObject == 0)
+ throw new IllegalStateException("Object was destroyed.");
+ getParametersProxy(nativeObject,params);
+ }
+ }
+
+ /**
+ * Returns the native EmbeddedRecognizer.
+ *
+ * @return a reference to the native object
+ */
+ private native long getInstanceProxy();
+
+ /**
+ * Configures the recognizer instance.
+ *
+ * @param config the recognizer configuration file
+ */
+ private native void configureProxy(long nativeObject, String config) throws IllegalArgumentException,
+ FileNotFoundException, IOException, UnsatisfiedLinkError,
+ ClassNotFoundException;
+
+ /**
+ * Sets the recognizer listener.
+ *
+ * @param listener listens for recognizer events
+ */
+ private native void setListenerProxy(long nativeObject, RecognizerListener listener);
+
+ private native void recognizeProxy(long nativeObject, long audioNativeObject,
+ long[] pGrammars);
+
+ private native long createEmbeddedGrammarProxy(long nativeObject, String url,
+ GrammarListener listener);
+
+ private native void stopProxy(long nativeObject);
+
+ private native void deleteNativeObject(long nativeObject);
+
+ private native void setParametersProxy(long nativeObject, Hashtable<String, String> params);
+
+ private native void getParametersProxy(long nativeObject, Vector<String> params);
+
+ private native void resetAcousticStateProxy(long nativeObject);
+
+}
diff --git a/core/java/android/speech/recognition/impl/EntryImpl.java b/core/java/android/speech/recognition/impl/EntryImpl.java
new file mode 100644
index 0000000..91b2b78
--- /dev/null
+++ b/core/java/android/speech/recognition/impl/EntryImpl.java
@@ -0,0 +1,147 @@
+/*---------------------------------------------------------------------------*
+ * EntryImpl.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition.impl;
+
+import android.speech.recognition.NBestRecognitionResult;
+import java.util.Enumeration;
+
+/**
+ */
+public class EntryImpl implements NBestRecognitionResult.Entry, Runnable
+{
+ private long nativeObject;
+
+ /**
+ * This implementation is a work-around to solve Q bug with
+ * nested classes.
+ *
+ * @param nativeObject the native NBestRecognitionResult.Entry object
+ */
+ public EntryImpl(long nativeObject)
+ {
+ this.nativeObject = nativeObject;
+ }
+
+ public void run()
+ {
+ dispose();
+ }
+
+ public byte getConfidenceScore() throws IllegalStateException
+ {
+ synchronized (EntryImpl.class)
+ {
+ if (nativeObject == 0)
+ throw new IllegalStateException("Object has been disposed");
+ return getConfidenceScoreProxy(nativeObject);
+ }
+ }
+
+ public String getLiteralMeaning() throws IllegalStateException
+ {
+ synchronized (EntryImpl.class)
+ {
+ if (nativeObject == 0)
+ throw new IllegalStateException("Object has been disposed");
+ return getLiteralMeaningProxy(nativeObject);
+ }
+ }
+
+ public String getSemanticMeaning() throws IllegalStateException
+ {
+ synchronized (EntryImpl.class)
+ {
+ if (nativeObject == 0)
+ throw new IllegalStateException("Object has been disposed");
+ return getSemanticMeaningProxy(nativeObject);
+ }
+ }
+
+ public String get(String key)
+ {
+ synchronized (EntryImpl.class)
+ {
+ if (nativeObject == 0)
+ throw new IllegalStateException("Object has been disposed");
+ return getProxy(nativeObject,key);
+ }
+ }
+
+ public Enumeration keys()
+ {
+ synchronized (EntryImpl.class)
+ {
+ if (nativeObject == 0)
+ throw new IllegalStateException("Object has been disposed");
+
+ return new Enumeration()
+ {
+ private String[] keys = keysProxy(nativeObject);
+ private int indexOfNextRead = 0;
+
+ public boolean hasMoreElements()
+ {
+ return indexOfNextRead <= keys.length-1;
+ }
+
+ public Object nextElement()
+ {
+ return keys[indexOfNextRead++];
+ }
+ };
+ }
+ }
+
+
+ /**
+ * Releases the native resources associated with the object.
+ */
+ private void dispose()
+ {
+ synchronized (EntryImpl.class)
+ {
+ if (nativeObject != 0)
+ {
+ deleteNativeObject(nativeObject);
+ nativeObject = 0;
+ }
+ }
+ }
+
+ @Override
+ protected void finalize() throws Throwable
+ {
+ dispose();
+ super.finalize();
+ }
+
+ private native void deleteNativeObject(long nativeObject);
+
+ private native String getLiteralMeaningProxy(long nativeObject);
+
+ private native String getSemanticMeaningProxy(long nativeObject);
+
+ private native byte getConfidenceScoreProxy(long nativeObject);
+
+ private native String getProxy(long nativeObject,String key);
+
+ private native String[] keysProxy(long nativeObject);
+
+}
diff --git a/core/java/android/speech/recognition/impl/GrammarImpl.java b/core/java/android/speech/recognition/impl/GrammarImpl.java
new file mode 100644
index 0000000..563d5d9
--- /dev/null
+++ b/core/java/android/speech/recognition/impl/GrammarImpl.java
@@ -0,0 +1,114 @@
+/*---------------------------------------------------------------------------*
+ * GrammarImpl.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition.impl;
+
+import android.speech.recognition.Grammar;
+
+/**
+ */
+public class GrammarImpl implements Grammar, Runnable
+{
+ /**
+ * Reference to the native object.
+ */
+ protected long nativeObject;
+
+ /**
+ * Creates a new GrammarImpl.
+ *
+ * @param nativeObj a reference to the native object
+ */
+ public GrammarImpl(long nativeObj)
+ {
+ nativeObject = nativeObj;
+ }
+
+ public void run()
+ {
+ dispose();
+ }
+
+ public long getNativeObject()
+ {
+ synchronized (GrammarImpl.class)
+ {
+ return nativeObject;
+ }
+ }
+
+ /**
+ * Indicates that the grammar will be used in the near future.
+ */
+ public void load()
+ {
+ synchronized (GrammarImpl.class)
+ {
+ if (nativeObject == 0)
+ throw new IllegalStateException("Object has been disposed");
+ loadProxy(nativeObject);
+ }
+ }
+
+ /**
+ * The grammar will be removed from use.
+ */
+ public void unload()
+ {
+ synchronized (GrammarImpl.class)
+ {
+ if (nativeObject == 0)
+ throw new IllegalStateException("Object has been disposed");
+ unloadProxy(nativeObject);
+ }
+ }
+
+ /**
+ * Releases the native resources associated with the object.
+ */
+ public void dispose()
+ {
+ synchronized (GrammarImpl.class)
+ {
+ if (nativeObject != 0)
+ {
+ deleteNativeObject(nativeObject);
+ nativeObject = 0;
+ }
+ }
+ }
+
+ @Override
+ protected void finalize() throws Throwable
+ {
+ dispose();
+ super.finalize();
+ }
+
+ /**
+ * Deletes a native object.
+ *
+ * @param nativeObject pointer to the native object
+ */
+ private native void deleteNativeObject(long nativeObject);
+
+ private native void loadProxy(long nativeObject);
+
+ private native void unloadProxy(long nativeObject);
+}
diff --git a/core/java/android/speech/recognition/impl/LoggerImpl.java b/core/java/android/speech/recognition/impl/LoggerImpl.java
new file mode 100644
index 0000000..9933c56
--- /dev/null
+++ b/core/java/android/speech/recognition/impl/LoggerImpl.java
@@ -0,0 +1,166 @@
+/*---------------------------------------------------------------------------*
+ * LoggerImpl.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition.impl;
+
+import android.speech.recognition.Logger;
+
+/**
+ */
+public class LoggerImpl extends Logger implements Runnable
+{
+ private static LoggerImpl instance;
+ /**
+ * Reference to the native object.
+ */
+ private long nativeObject;
+
+ /**
+ * Creates a new instance of LoggerImpl.
+ *
+ * @param function the name of the enclosing function
+ */
+ private LoggerImpl()
+ {
+ System system = System.getInstance();
+ nativeObject = initNativeObject();
+ if (nativeObject!=0)
+ system.register(this);
+ }
+
+ public void run()
+ {
+ dispose();
+ }
+
+ /**
+ * Returns the singleton instance.
+ *
+ * @return the singleton instance
+ */
+ public static LoggerImpl getInstance()
+ {
+ synchronized (LoggerImpl.class)
+ {
+ if (instance == null)
+ instance = new LoggerImpl();
+ return instance;
+ }
+ }
+
+ public void setLoggingLevel(LogLevel level)
+ {
+ synchronized (LoggerImpl.class)
+ {
+ if (nativeObject == 0)
+ throw new IllegalStateException("Object has been disposed");
+ setLoggingLevelProxy(nativeObject,level);
+ }
+ }
+
+ public void setPath(String path)
+ {
+ synchronized (LoggerImpl.class)
+ {
+ if (nativeObject == 0)
+ throw new IllegalStateException("Object has been disposed");
+ setPathProxy(nativeObject,path);
+ }
+ }
+
+ public void error(String message)
+ {
+ synchronized (LoggerImpl.class)
+ {
+ if (nativeObject == 0)
+ throw new IllegalStateException("Object has been disposed");
+ errorProxy(nativeObject,message);
+ }
+ }
+
+ public void warn(String message)
+ {
+ synchronized (LoggerImpl.class)
+ {
+ if (nativeObject == 0)
+ throw new IllegalStateException("Object has been disposed");
+ warnProxy(nativeObject,message);
+ }
+ }
+
+ public void info(String message)
+ {
+ synchronized (LoggerImpl.class)
+ {
+ if (nativeObject == 0)
+ throw new IllegalStateException("Object has been disposed");
+ infoProxy(nativeObject,message);
+ }
+ }
+
+ public void trace(String message)
+ {
+ synchronized (LoggerImpl.class)
+ {
+ if (nativeObject == 0)
+ throw new IllegalStateException("Object has been disposed");
+ traceProxy(nativeObject,message);
+ }
+ }
+
+ /**
+ * Releases the native resources associated with the object.
+ */
+ private void dispose()
+ {
+ synchronized (LoggerImpl.class)
+ {
+ if (nativeObject!=0)
+ {
+ deleteNativeObject(nativeObject);
+ System.getInstance().unregister(this);
+ }
+ nativeObject = 0;
+ instance = null;
+ }
+ }
+
+ @Override
+ protected void finalize() throws Throwable
+ {
+ dispose();
+ super.finalize();
+ }
+
+ private native long initNativeObject();
+
+ private native void setLoggingLevelProxy(long nativeObject, LogLevel level);
+
+ private native void setPathProxy(long nativeObject, String filename);
+
+ private native void errorProxy(long nativeObject, String message);
+
+ private native void warnProxy(long nativeObject, String message);
+
+ private native void infoProxy(long nativeObject, String message);
+
+ private native void traceProxy(long nativeObject,String message);
+
+ private native void deleteNativeObject(long nativeObject);
+}
diff --git a/core/java/android/speech/recognition/impl/MediaFileReaderImpl.java b/core/java/android/speech/recognition/impl/MediaFileReaderImpl.java
new file mode 100644
index 0000000..8ce643d
--- /dev/null
+++ b/core/java/android/speech/recognition/impl/MediaFileReaderImpl.java
@@ -0,0 +1,156 @@
+/*---------------------------------------------------------------------------*
+ * MediaFileReaderImpl.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition.impl;
+
+import android.speech.recognition.MediaFileReader;
+import android.speech.recognition.AudioStream;
+import android.speech.recognition.Codec;
+import android.speech.recognition.AudioSourceListener;
+
+/**
+ */
+public class MediaFileReaderImpl extends MediaFileReader implements Runnable
+{
+ /**
+ * Reference to the native object.
+ */
+ private long nativeObject;
+
+ /**
+ * Creates a new MediaFileReaderImpl.
+ *
+ * @param filename the name of the file to read from
+ * @param listener listens for MediaFileReader events
+ */
+ public MediaFileReaderImpl(String filename, AudioSourceListener listener)
+ {
+ System system = System.getInstance();
+ nativeObject =
+ createMediaFileReaderProxy(filename, listener);
+ if (nativeObject != 0)
+ system.register(this);
+ }
+
+ public void run()
+ {
+ dispose();
+ }
+
+ /**
+ * Set the reading mode
+ */
+ public void setMode(Mode mode)
+ {
+ synchronized (MediaFileReaderImpl.class)
+ {
+ if (nativeObject == 0)
+ throw new IllegalStateException("Object has been disposed");
+ setModeProxy(nativeObject,mode);
+ }
+ }
+
+ /**
+ * Creates an audioStream source
+ */
+ public AudioStream createAudio()
+ {
+ synchronized (MediaFileReaderImpl.class)
+ {
+ if (nativeObject == 0)
+ throw new IllegalStateException("Object has been disposed");
+ return new AudioStreamImpl(createAudioProxy(nativeObject));
+ }
+ }
+
+ /**
+ * Tells the audio source to start collecting audio samples.
+ */
+ public void start()
+ {
+ synchronized (MediaFileReaderImpl.class)
+ {
+ if (nativeObject == 0)
+ throw new IllegalStateException("Object has been disposed");
+ startProxy(nativeObject);
+ }
+ }
+
+ /**
+ * Stops this source from collecting audio samples.
+ */
+ public void stop()
+ {
+ synchronized (MediaFileReaderImpl.class)
+ {
+ if (nativeObject == 0)
+ throw new IllegalStateException("Object has been disposed");
+ stopProxy(nativeObject);
+ }
+ }
+
+ /**
+ * Releases the native resources associated with the object.
+ */
+ public void dispose()
+ {
+ synchronized (MediaFileReaderImpl.class)
+ {
+ if (nativeObject != 0)
+ {
+ deleteNativeObject(nativeObject);
+ nativeObject = 0;
+ System.getInstance().unregister(this);
+ }
+ }
+ }
+
+ @Override
+ protected void finalize() throws Throwable
+ {
+ dispose();
+ super.finalize();
+ }
+
+ /**
+ * Deletes a native object.
+ *
+ * @param nativeObject pointer to the native object
+ */
+ private native void deleteNativeObject(long nativeObject);
+
+ /**
+ * Creates a native MediaFileReader.
+ *
+ * @param filename the name of the file to read from
+ * @param offset the offset to begin reading from
+ * @param codec the file audio format
+ * @param listener listens for MediaFileReader events
+ * @return a reference to the native object
+ */
+ private native long createMediaFileReaderProxy(String filename, AudioSourceListener listener);
+
+ private native void setModeProxy(long nativeObject,Mode mode);
+
+ private native long createAudioProxy(long nativeObject);
+
+ private native void startProxy(long nativeObject);
+
+ private native void stopProxy(long nativeObject);
+}
diff --git a/core/java/android/speech/recognition/impl/MediaFileWriterImpl.java b/core/java/android/speech/recognition/impl/MediaFileWriterImpl.java
new file mode 100644
index 0000000..c4bd836
--- /dev/null
+++ b/core/java/android/speech/recognition/impl/MediaFileWriterImpl.java
@@ -0,0 +1,102 @@
+/*---------------------------------------------------------------------------*
+ * MediaFileWriterImpl.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition.impl;
+
+import android.speech.recognition.AudioStream;
+import android.speech.recognition.MediaFileWriter;
+import android.speech.recognition.MediaFileWriterListener;
+
+/**
+ */
+public class MediaFileWriterImpl extends MediaFileWriter implements Runnable
+{
+ /**
+ * Reference to the native object.
+ */
+ private long nativeObject;
+
+ /**
+ * Creates a new MediaFileWriterImpl.
+ *
+ * @param listener listens for MediaFileWriter events
+ */
+ public MediaFileWriterImpl(MediaFileWriterListener listener)
+ {
+ System system = System.getInstance();
+ nativeObject = createMediaFileWriterProxy(listener);
+ if (nativeObject != 0)
+ system.register(this);
+ }
+
+ public void run()
+ {
+ dispose();
+ }
+
+ public void save(AudioStream source, String filename)
+ {
+ synchronized (MediaFileWriterImpl.class)
+ {
+ if (nativeObject == 0)
+ throw new IllegalStateException("Object has been disposed");
+ saveProxy(nativeObject,((AudioStreamImpl)source).getNativeObject(), filename);
+ }
+ }
+
+ /**
+ * Releases the native resources associated with the object.
+ */
+ public synchronized void dispose()
+ {
+ synchronized (MediaFileWriterImpl.class)
+ {
+ if (nativeObject != 0)
+ {
+ deleteNativeObject(nativeObject);
+ nativeObject = 0;
+ System.getInstance().unregister(this);
+ }
+ }
+ }
+
+ @Override
+ protected void finalize() throws Throwable
+ {
+ dispose();
+ super.finalize();
+ }
+
+ /**
+ * Creates a native MediaFileWriter.
+ *
+ * @param listener listens for MediaFileReader events
+ * @return a reference to the native object
+ */
+ private native long createMediaFileWriterProxy(MediaFileWriterListener listener);
+
+ /**
+ * Deletes a native object.
+ *
+ * @param nativeObject pointer to the native object
+ */
+ private native void deleteNativeObject(long nativeObject);
+
+ private native void saveProxy(long nativeObject, long audioNativeObject, String filename);
+}
diff --git a/core/java/android/speech/recognition/impl/MicrophoneImpl.java b/core/java/android/speech/recognition/impl/MicrophoneImpl.java
new file mode 100644
index 0000000..a915484
--- /dev/null
+++ b/core/java/android/speech/recognition/impl/MicrophoneImpl.java
@@ -0,0 +1,165 @@
+/*---------------------------------------------------------------------------*
+ * MicrophoneImpl.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition.impl;
+
+import android.speech.recognition.AudioStream;
+import android.speech.recognition.Codec;
+import android.speech.recognition.Microphone;
+import android.speech.recognition.AudioSourceListener;
+
+/**
+ */
+public class MicrophoneImpl extends Microphone implements Runnable
+{
+ private static MicrophoneImpl instance;
+ /**
+ * Reference to the native object.
+ */
+ private long nativeObject;
+
+ /**
+ * Creates a new MicrophoneImpl.
+ *
+ * @param nativeObj a reference to the native object
+ */
+ private MicrophoneImpl()
+ {
+ System system = System.getInstance();
+ nativeObject = initNativeObject();
+ if (nativeObject != 0)
+ system.register(this);
+ }
+
+ public void run()
+ {
+ dispose();
+ }
+
+ /**
+ * Returns the singleton instance.
+ *
+ * @return the singleton instance
+ */
+ public static MicrophoneImpl getInstance()
+ {
+ synchronized (MicrophoneImpl.class)
+ {
+ if (instance == null)
+ instance = new MicrophoneImpl();
+ return instance;
+ }
+ }
+
+ /**
+ * set the recording codec. This must be called before Start is called.
+ * @param recordingCodec the codec in which the samples will be recorded.
+ */
+ public void setCodec(Codec recordingCodec)
+ {
+ synchronized (MicrophoneImpl.class)
+ {
+ if (nativeObject == 0)
+ throw new IllegalStateException("Object has been disposed");
+ setCodecProxy(nativeObject,recordingCodec);
+ }
+ }
+
+ /**
+ * set the microphone listener.
+ * @param listener the microphone listener.
+ */
+ public void setListener(AudioSourceListener listener)
+ {
+ synchronized (MicrophoneImpl.class)
+ {
+ if (nativeObject == 0)
+ throw new IllegalStateException("Object has been disposed");
+ setListenerProxy(nativeObject,listener);
+ }
+ }
+
+ public AudioStream createAudio()
+ {
+ synchronized (MicrophoneImpl.class)
+ {
+ if (nativeObject == 0)
+ throw new IllegalStateException("Object has been disposed");
+ return new AudioStreamImpl(createAudioProxy(nativeObject));
+ }
+ }
+
+ public void start()
+ {
+ synchronized (MicrophoneImpl.class)
+ {
+ if (nativeObject == 0)
+ throw new IllegalStateException("Object has been disposed");
+ startProxy(nativeObject);
+ }
+ }
+
+ public void stop()
+ {
+ synchronized (MicrophoneImpl.class)
+ {
+ if (nativeObject == 0)
+ throw new IllegalStateException("Object has been disposed");
+ stopProxy(nativeObject);
+ }
+ }
+
+ /**
+ * Releases the native resources associated with the object.
+ */
+ private void dispose()
+ {
+ synchronized (MicrophoneImpl.class)
+ {
+ if (nativeObject != 0)
+ {
+ deleteNativeObject(nativeObject);
+ nativeObject = 0;
+ instance = null;
+ System.getInstance().unregister(this);
+ }
+ }
+ }
+
+ @Override
+ protected void finalize() throws Throwable
+ {
+ dispose();
+ super.finalize();
+ }
+
+ private native long initNativeObject();
+
+ private native void setCodecProxy(long nativeObject,Codec recordingCodec);
+
+ private native void setListenerProxy(long nativeObject, AudioSourceListener listener);
+
+ private native long createAudioProxy(long nativeObject);
+
+ private native void startProxy(long nativeObject);
+
+ private native void stopProxy(long nativeObject);
+
+ private native void deleteNativeObject(long nativeObject);
+}
diff --git a/core/java/android/speech/recognition/impl/NBestRecognitionResultImpl.java b/core/java/android/speech/recognition/impl/NBestRecognitionResultImpl.java
new file mode 100644
index 0000000..4d2e00a
--- /dev/null
+++ b/core/java/android/speech/recognition/impl/NBestRecognitionResultImpl.java
@@ -0,0 +1,106 @@
+/*---------------------------------------------------------------------------*
+ * NBestRecognitionResultImpl.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition.impl;
+
+import android.speech.recognition.NBestRecognitionResult;
+import android.speech.recognition.VoicetagItem;
+import android.speech.recognition.VoicetagItemListener;
+/**
+ */
+public class NBestRecognitionResultImpl implements NBestRecognitionResult
+{
+ /**
+ * Reference to the native object.
+ */
+ private long nativeObject;
+
+ /**
+ * Creates a new NBestRecognitionResultImpl.
+ *
+ * @param nativeObject a reference to the native object
+ */
+ public NBestRecognitionResultImpl(long nativeObject)
+ {
+ this.nativeObject = nativeObject;
+ }
+
+ public int getSize()
+ {
+ synchronized (NBestRecognitionResultImpl.class)
+ {
+ if (nativeObject == 0)
+ throw new IllegalStateException("Object has been disposed");
+ return getSizeProxy(nativeObject);
+ }
+ }
+
+ public Entry getEntry(int index)
+ {
+ synchronized (NBestRecognitionResultImpl.class)
+ {
+ if (nativeObject == 0)
+ throw new IllegalStateException("Object has been disposed");
+ long nativeEntryObject = getEntryProxy(nativeObject,index);
+ if (nativeEntryObject==0)
+ return null;
+ else
+ return new EntryImpl(nativeEntryObject);
+ }
+ }
+
+ public VoicetagItem createVoicetagItem(String VoicetagId, VoicetagItemListener listener)
+ {
+ synchronized (NBestRecognitionResultImpl.class)
+ {
+ if (nativeObject == 0)
+ throw new IllegalStateException("Object has been disposed");
+ if ((VoicetagId == null) || (VoicetagId.length() == 0))
+ throw new IllegalArgumentException("VoicetagId may not be null or empty string.");
+ return new VoicetagItemImpl(createVoicetagItemProxy(nativeObject,VoicetagId,listener),false);
+ }
+ }
+
+ /**
+ * Releases the native resources associated with the object.
+ */
+ private void dispose()
+ {
+ synchronized (NBestRecognitionResultImpl.class)
+ {
+ nativeObject = 0;
+ }
+ }
+
+ @Override
+ protected void finalize() throws Throwable
+ {
+ dispose();
+ super.finalize();
+ }
+
+ /**
+ * Returns a reference to the native VoicetagItem.
+ */
+ private native long createVoicetagItemProxy(long nativeObject, String VoicetagId, VoicetagItemListener listener);
+
+ private native long getEntryProxy(long nativeObject, int index);
+
+ private native int getSizeProxy(long nativeObject);
+}
diff --git a/core/java/android/speech/recognition/impl/SrecGrammarImpl.java b/core/java/android/speech/recognition/impl/SrecGrammarImpl.java
new file mode 100644
index 0000000..cb6f4c6
--- /dev/null
+++ b/core/java/android/speech/recognition/impl/SrecGrammarImpl.java
@@ -0,0 +1,120 @@
+/*---------------------------------------------------------------------------*
+ * SrecGrammarImpl.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition.impl;
+
+import android.speech.recognition.SrecGrammar;
+import android.speech.recognition.SlotItem;
+import android.speech.recognition.VoicetagItem;
+import android.speech.recognition.WordItem;
+
+import java.util.Vector;
+
+/**
+ */
+public class SrecGrammarImpl extends EmbeddedGrammarImpl implements SrecGrammar
+{
+ /**
+ * Creates a new SrecGrammarImpl.
+ *
+ * @param nativeObject the native object
+ */
+ public SrecGrammarImpl(long nativeObject)
+ {
+ super(nativeObject);
+ }
+
+ public void addItem(String slotName, SlotItem item, int weight,
+ String semanticValue)
+ {
+ synchronized (GrammarImpl.class)
+ {
+ if (nativeObject == 0)
+ throw new IllegalStateException("Object has been disposed");
+
+ if (slotName == null || slotName.length()==0)
+ throw new IllegalArgumentException("addItem() - Slot name is null or empty.");
+ if (item == null)
+ throw new IllegalArgumentException("addItem() - item can't be null.");
+ if (semanticValue == null || semanticValue.length()==0)
+ throw new IllegalArgumentException("addItem() - semanticValue is null or empty.");
+
+ long itemNativeObject = 0;
+ if (item instanceof VoicetagItem)
+ itemNativeObject = ((VoicetagItemImpl)item).getNativeObject();
+ else if (item instanceof WordItem)
+ itemNativeObject = ((WordItemImpl)item).getNativeObject();
+ else
+ throw new IllegalArgumentException("SlotItem - should be a WordItem or a VoicetagItem object.");
+
+ addItemProxy(nativeObject, slotName, itemNativeObject, weight, semanticValue);
+ }
+ }
+
+ public void addItemList(String slotName, Vector<Item> items)
+ {
+ synchronized (GrammarImpl.class)
+ {
+ if (nativeObject == 0)
+ throw new IllegalStateException("Object has been disposed");
+
+ if (slotName == null || slotName.length()==0)
+ throw new IllegalArgumentException("addItemList - Slot name is null or empty.");
+ if (items == null || items.isEmpty() == true)
+ throw new IllegalArgumentException("addItemList - Items is null or empty.");
+
+ int itemsCount = items.size();
+
+ long[] nativeSlots = new long[itemsCount];
+ int[] nativeWeights = new int[itemsCount];
+ String[] nativeSemantic = new String[itemsCount];
+
+ Item element = null;
+ long itemNativeObject = 0;
+ SlotItem item = null;
+ for (int i = 0; i < itemsCount; ++i)
+ {
+ element = items.get(i);
+
+ item = element._item;
+ if (item instanceof VoicetagItem)
+ itemNativeObject = ((VoicetagItemImpl)item).getNativeObject();
+ else if (item instanceof WordItem)
+ itemNativeObject = ((WordItemImpl)item).getNativeObject();
+ else
+ {
+ throw new IllegalArgumentException("SlotItem ["+i+"] - should be a WordItem or a VoicetagItem object.");
+ }
+ nativeSlots[i] = itemNativeObject;
+ nativeWeights[i] = element._weight;
+ nativeSemantic[i]= element._semanticMeaning;
+ itemNativeObject = 0;
+ item = null;
+ }
+ addItemListProxy(nativeObject, slotName,nativeSlots,nativeWeights,nativeSemantic);
+ }
+ }
+
+ private native void addItemProxy(long nativeObject, String slotName, long item, int weight,
+ String semanticValue);
+
+ private native void addItemListProxy(long nativeObject, String slotName, long[] items,
+ int[] weights, String[] semanticValues);
+
+}
diff --git a/core/java/android/speech/recognition/impl/System.java b/core/java/android/speech/recognition/impl/System.java
new file mode 100644
index 0000000..23418fe
--- /dev/null
+++ b/core/java/android/speech/recognition/impl/System.java
@@ -0,0 +1,179 @@
+/*---------------------------------------------------------------------------*
+ * System.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition.impl;
+
+import java.lang.ref.WeakReference;
+import java.util.WeakHashMap;
+
+
+/**
+ */
+public class System
+{
+ private static boolean libraryLoaded;
+ private static System instance;
+ private static WeakHashMap<Object, WeakReference> registerMap;
+ /**
+ * Reference to the native object.
+ */
+ private long nativeObject;
+ private boolean shutdownRequested;
+
+ /**
+ * Creates a new instance of System
+ */
+ private System()
+ {
+ shutdownRequested = false;
+ registerMap =
+ new WeakHashMap<Object, WeakReference>();
+ initLibrary();
+ nativeObject = initNativeObject();
+ Runtime.getRuntime().
+ addShutdownHook(new Thread()
+ {
+ @Override
+ public void run()
+ {
+ try
+ {
+ dispose();
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+ });
+
+ }
+
+ /**
+ * Returns the singleton instance.
+ *
+ * @return the singleton instance
+ */
+ public static System getInstance()
+ {
+ synchronized (System.class)
+ {
+ if (instance == null)
+ instance = new System();
+ return instance;
+ }
+ }
+
+ /**
+ * Loads the native library if necessary.
+ */
+ private void initLibrary()
+ {
+ if (!libraryLoaded)
+ {
+ java.lang.System.loadLibrary("UAPI_jni");
+ libraryLoaded = true;
+ }
+ }
+
+ /**
+ * Registers an object for shutdown when System.dispose() is invoked.
+ *
+ * @param r the code to run on shutdown
+ * @throws IllegalStateException if the System is shutting down
+ */
+ public void register(Runnable r) throws IllegalStateException
+ {
+ synchronized (System.class)
+ {
+ if (shutdownRequested)
+ throw new IllegalStateException("System is shutting down");
+ registerMap.put(r,
+ new WeakReference<Runnable>(r));
+ }
+ }
+
+ /**
+ * Registers an object for shutdown when System.dispose() is invoked.
+ *
+ * @param r the code to run on shutdown
+ */
+ public void unregister(Runnable r)
+ {
+ synchronized (System.class)
+ {
+ if (shutdownRequested)
+ {
+ // System.dispose() will end up removing all entries
+ return;
+ }
+ if (r!=null) registerMap.remove(r);
+ }
+ }
+
+ /**
+ * Releases the native resources associated with the object.
+ *
+ * @throws java.util.concurrent.TimeoutException if the operation timeouts
+ * @throws IllegalThreadStateException if a native thread error occurs
+ */
+ public void dispose() throws java.util.concurrent.TimeoutException,
+ IllegalThreadStateException
+ {
+ synchronized (System.class)
+ {
+ if (nativeObject == 0)
+ return;
+ shutdownRequested = true;
+ }
+
+ // Traverse the list of WeakReferences
+ // cast to a Runnable object if the weakrerefence is not null
+ // then call the run method.
+ for (Object o: registerMap.keySet())
+ {
+ WeakReference weakReference = registerMap.get(o);
+ Runnable r = (Runnable) weakReference.get();
+ if (r != null)
+ r.run();
+ }
+ registerMap.clear();
+
+ // Call the native dispose method
+ disposeProxy();
+ synchronized (System.class)
+ {
+ nativeObject = 0;
+ instance = null;
+ }
+ }
+
+ @Override
+ protected void finalize() throws Throwable
+ {
+ dispose();
+ super.finalize();
+ }
+
+ public static native String getAPIVersion();
+
+ private static native long initNativeObject();
+
+ private static native void disposeProxy();
+}
diff --git a/core/java/android/speech/recognition/impl/VoicetagItemImpl.java b/core/java/android/speech/recognition/impl/VoicetagItemImpl.java
new file mode 100644
index 0000000..f9db399
--- /dev/null
+++ b/core/java/android/speech/recognition/impl/VoicetagItemImpl.java
@@ -0,0 +1,206 @@
+/*---------------------------------------------------------------------------*
+ * VoicetagItemImpl.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition.impl;
+
+import android.speech.recognition.VoicetagItem;
+import android.speech.recognition.VoicetagItemListener;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+/**
+ */
+public class VoicetagItemImpl extends VoicetagItem implements Runnable
+{
+ /**
+ * Reference to the native object.
+ */
+ private long nativeObject;
+ /**
+ * Voicetag has a filename need to be loaded before use it.
+ */
+ private boolean needToBeLoaded;
+
+ /**
+ * Creates a new VoicetagItemImpl.
+ *
+ * @param nativeObject the pointer to the native object
+ */
+ public VoicetagItemImpl(long nativeObject, boolean fromfile)
+ {
+ this.nativeObject = nativeObject;
+ needToBeLoaded = fromfile;
+ }
+
+ public void run()
+ {
+ dispose();
+ }
+
+ /**
+ * Creates a VoicetagItem from a file
+ *
+ * @param filename filename for Voicetag
+ * @param listener listens for Voicetag events
+ * @return the resulting VoicetagItem
+ * @throws IllegalArgumentException if filename is null or an empty string.
+ * @throws FileNotFoundException if the specified filename could not be found
+ * @throws IOException if the specified filename could not be opened
+ */
+ public static VoicetagItem create(String filename, VoicetagItemListener listener) throws IllegalArgumentException,FileNotFoundException,IOException
+ {
+ if ((filename == null) || (filename.length() == 0))
+ throw new IllegalArgumentException("Filename may not be null or empty string.");
+
+ VoicetagItemImpl voicetag = null;
+ long nativeVoicetag = createVoicetagProxy(filename,listener);
+ if (nativeVoicetag!=0)
+ {
+ voicetag = new VoicetagItemImpl(nativeVoicetag,true);
+ }
+ return voicetag;
+ }
+ /**
+ * Returns the audio used to construct the VoicetagItem.
+ */
+ public byte[] getAudio() throws IllegalStateException
+ {
+ synchronized (VoicetagItem.class)
+ {
+ if (nativeObject == 0)
+ throw new IllegalStateException("Object was destroyed.");
+
+ return getAudioProxy(nativeObject);
+ }
+ }
+
+ /**
+ * Sets the audio used to construct the Voicetag.
+ */
+ public void setAudio(byte[] waveform) throws IllegalArgumentException,IllegalStateException
+ {
+ synchronized (VoicetagItem.class)
+ {
+ if (nativeObject == 0)
+ throw new IllegalStateException("Object was destroyed.");
+
+ if ((waveform == null) || (waveform.length == 0))
+ throw new IllegalArgumentException("Waveform may not be null or empty.");
+ setAudioProxy(nativeObject,waveform);
+ }
+ }
+
+ /**
+ * Save the Voicetag.
+ */
+ public void save(String path) throws IllegalArgumentException
+ {
+ synchronized (VoicetagItem.class)
+ {
+ if (nativeObject == 0)
+ throw new IllegalStateException("Object was destroyed.");
+ if ((path == null) || (path.length() == 0))
+ throw new IllegalArgumentException("Path may not be null or empty string.");
+ saveVoicetagProxy(nativeObject,path);
+ }
+ }
+
+ /**
+ * Load a Voicetag.
+ */
+ public void load() throws IllegalStateException
+ {
+ synchronized (VoicetagItem.class)
+ {
+ if (nativeObject == 0)
+ throw new IllegalStateException("Object was destroyed.");
+ if (!needToBeLoaded)
+ throw new IllegalStateException("This Voicetag was not created from a file, does not need to be loaded.");
+ loadVoicetagProxy(nativeObject);
+ }
+ }
+
+ public long getNativeObject()
+ {
+ synchronized (VoicetagItem.class)
+ {
+ return nativeObject;
+ }
+ }
+
+ /**
+ * Releases the native resources associated with the object.
+ */
+ private void dispose()
+ {
+ synchronized (VoicetagItem.class)
+ {
+ if (nativeObject != 0)
+ {
+ deleteNativeObject(nativeObject);
+ nativeObject = 0;
+ }
+ }
+ }
+
+ @Override
+ protected void finalize() throws Throwable
+ {
+ dispose();
+ super.finalize();
+ }
+
+
+ private static native long createVoicetagProxy(String filename, VoicetagItemListener listener);
+ /**
+ * (Optional operation) Returns the audio used to construct the Voicetag. The
+ * audio is in PCM format and is start-pointed and end-pointed. The audio is
+ * only generated if the enableGetWaveform recognition parameter is set
+ * prior to recognition.
+ *
+ * @see RecognizerParameters.enableGetWaveform
+ */
+ private native byte[] getAudioProxy(long nativeObject);
+
+ /**
+ * (Optional operation) Sets the audio used to construct the Voicetag. The
+ * audio is in PCM format and is start-pointed and end-pointed. The audio is
+ * only generated if the enableGetWaveform recognition parameter is set
+ * prior to recognition.
+ *
+ * @param waveform the endpointed waveform
+ */
+ private native void setAudioProxy(long nativeObject, byte[] waveform);
+
+ /**
+ * Save the Voicetag Item.
+ */
+ private native void saveVoicetagProxy(long nativeObject, String path);
+
+ /**
+ * Load a Voicetag Item.
+ */
+ private native void loadVoicetagProxy(long nativeObject);
+
+ /**
+ * Deletes a native object.
+ *
+ * @param nativeObject pointer to the native object
+ */
+ private native void deleteNativeObject(long nativeObject);
+}
diff --git a/core/java/android/speech/recognition/impl/WordItemImpl.java b/core/java/android/speech/recognition/impl/WordItemImpl.java
new file mode 100644
index 0000000..f0daa34
--- /dev/null
+++ b/core/java/android/speech/recognition/impl/WordItemImpl.java
@@ -0,0 +1,157 @@
+/*---------------------------------------------------------------------------*
+ * WordItemImpl.java *
+ * *
+ * Copyright 2007, 2008 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.recognition.impl;
+
+import android.speech.recognition.WordItem;
+
+/**
+ */
+public class WordItemImpl extends WordItem implements Runnable
+{
+ /**
+ * Empty array that gets reused whenever the code requests that the underlying
+ * recognizer guess the pronunciations.
+ */
+ private static final String[] guessPronunciations = new String[0];
+ /**
+ * Reference to the native object.
+ */
+ private long nativeObject;
+
+ /**
+ * Creates a new WordItem.
+ *
+ * @param word the word to insert
+ * @throws IllegalArgumentException if word or pronunciations are null
+ */
+ private WordItemImpl(String word, String[] pronunciations) throws IllegalArgumentException
+ {
+ initNativeObject(word, pronunciations);
+ }
+
+ public void run()
+ {
+ dispose();
+ }
+
+ /**
+ * Creates a new WordItem.
+ *
+ * @param word the word to insert
+ * @param pronunciations the pronunciations to associated with the item. If the list is
+ * is empty (example:new String[0]) the recognizer will attempt to guess the pronunciations.
+ * @return the WordItem
+ * @throws IllegalArgumentException if word is null or if pronunciations is
+ * null or pronunciations contains an element equal to null or empty string.
+ */
+ public static WordItemImpl valueOf(String word, String[] pronunciations)
+ throws IllegalArgumentException
+ {
+ if (word == null)
+ throw new IllegalArgumentException("Word may not be null");
+ else if (pronunciations == null)
+ throw new IllegalArgumentException("Pronunciations may not be null");
+ for (int i = 0, size = pronunciations.length; i < size; ++i)
+ {
+ if (pronunciations[i]==null)
+ {
+ throw new IllegalArgumentException(
+ "Pronunciations element may not be null");
+ }
+ else
+ {
+ if (pronunciations[i].trim().equals(""))
+ throw new IllegalArgumentException(
+ "Pronunciations may not contain empty strings");
+ }
+ }
+ return new WordItemImpl(word, pronunciations);
+ }
+
+ /**
+ * Creates a new WordItem.
+ *
+ * @param word the word to insert
+ * @param pronunciation the pronunciation to associate with the item. If it
+ * is null the recognizer will attempt to guess the pronunciations.
+ * @return the WordItem
+ * @throws IllegalArgumentException if word is null or if pronunciation is
+ * an empty string
+ */
+ public static WordItemImpl valueOf(String word, String pronunciation)
+ throws IllegalArgumentException
+ {
+ String[] pronunciations;
+ if (word == null)
+ throw new IllegalArgumentException("Word may not be null");
+ else if (pronunciation == null)
+ pronunciations = guessPronunciations;
+ else if (pronunciation.trim().equals(""))
+ throw new IllegalArgumentException(
+ "Pronunciation may not be an empty string");
+ else
+ pronunciations = new String[]{pronunciation};
+ return new WordItemImpl(word, pronunciations);
+ }
+
+ /**
+ * Allocates a reference to the native object.
+ *
+ * @param word the word to insert
+ */
+ private native void initNativeObject(String word, String[] pronunciations);
+
+ public long getNativeObject()
+ {
+ synchronized (WordItemImpl.class)
+ {
+ return nativeObject;
+ }
+ }
+
+ /**
+ * Releases the native resources associated with the object.
+ */
+ private void dispose()
+ {
+ synchronized (WordItemImpl.class)
+ {
+ if (nativeObject != 0)
+ {
+ deleteNativeObject(nativeObject);
+ nativeObject = 0;
+ }
+ }
+ }
+
+ @Override
+ protected void finalize() throws Throwable
+ {
+ dispose();
+ super.finalize();
+ }
+
+ /**
+ * Deletes a native object.
+ *
+ * @param nativeObject pointer to the native object
+ */
+ private native void deleteNativeObject(long nativeObject);
+}
diff --git a/core/java/android/speech/recognition/impl/package.html b/core/java/android/speech/recognition/impl/package.html
new file mode 100755
index 0000000..1c9bf9d
--- /dev/null
+++ b/core/java/android/speech/recognition/impl/package.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+ {@hide}
+</body>
+</html>
diff --git a/core/java/android/speech/recognition/package.html b/core/java/android/speech/recognition/package.html
new file mode 100644
index 0000000..3c59962
--- /dev/null
+++ b/core/java/android/speech/recognition/package.html
@@ -0,0 +1,6 @@
+<HTML>
+<BODY>
+{@hide}
+Provides classes for speech recogntion.
+</BODY>
+</HTML>
diff --git a/core/java/android/speech/srec/Srec.java b/core/java/android/speech/srec/Srec.java
new file mode 100644
index 0000000..a629214
--- /dev/null
+++ b/core/java/android/speech/srec/Srec.java
@@ -0,0 +1,162 @@
+/*---------------------------------------------------------------------------*
+ * EmbeddedRecognizerImpl.java *
+ * *
+ * Copyright 2007 Nuance Communciations, Inc. *
+ * *
+ * Licensed under the Apache License, Version 2.0 (the 'License'); *
+ * you may not use this file except in compliance with the License. *
+ * *
+ * You may obtain a copy of the License at *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, software *
+ * distributed under the License is distributed on an 'AS IS' BASIS, *
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
+ * See the License for the specific language governing permissions and *
+ * limitations under the License. *
+ * *
+ *---------------------------------------------------------------------------*/
+
+package android.speech.srec;
+
+import java.io.IOException;
+
+/**
+ * Simple, synchronous speech recognizer, using the SREC package.
+ *
+ * @hide
+ */
+public class Srec
+{
+ private int mNative;
+
+
+ /**
+ * Create an instance of a SREC speech recognizer.
+ * @param configFile pathname of the baseline*.par configuration file.
+ * @throws IOException
+ */
+ Srec(String configFile) throws IOException {
+
+ }
+
+ /**
+ * Creates a Srec recognizer.
+ * @param g2gFileName pathname of a g2g grammar file.
+ * @return
+ * @throws IOException
+ */
+ public Grammar loadGrammar(String g2gFileName) throws IOException {
+ return null;
+ }
+
+ /**
+ * Represents a grammar loaded into the recognizer.
+ */
+ public class Grammar {
+ private int mId = -1;
+
+ /**
+ * Add a word to a slot
+ * @param slot slot name
+ * @param word word
+ * @param pron pronunciation, or null to derive from word
+ * @param weight weight to give the word
+ * @param meaning meaning string
+ */
+ public void addToSlot(String slot, String word, String pron, int weight, String meaning) {
+
+ }
+
+ /**
+ * Compile all slots.
+ */
+ public void compileSlots() {
+
+ }
+
+ /**
+ * Reset all slots.
+ */
+ public void resetAllSlots() {
+
+ }
+
+ /**
+ * Save grammar to g2g file.
+ * @param g2gFileName
+ * @throws IOException
+ */
+ public void save(String g2gFileName) throws IOException {
+
+ }
+
+ /**
+ * Release resources associated with this grammar.
+ */
+ public void unload() {
+
+ }
+ }
+
+ /**
+ * Start recognition
+ */
+ public void start() {
+
+ }
+
+ /**
+ * Process some audio and return the next state.
+ * @return true if complete
+ */
+ public boolean process() {
+ return false;
+ }
+
+ /**
+ * Get the number of recognition results.
+ * @return
+ */
+ public int getResultCount() {
+ return 0;
+ }
+
+ /**
+ * Get a set of keys for the result.
+ * @param index index of result.
+ * @return array of keys.
+ */
+ public String[] getResultKeys(int index) {
+ return null;
+ }
+
+ /**
+ * Get a result value
+ * @param index index of the result.
+ * @param key key of the result.
+ * @return the result.
+ */
+ public String getResult(int index, String key) {
+ return null;
+ }
+
+ /**
+ * Reset the recognizer to the idle state.
+ */
+ public void reset() {
+
+ }
+
+ /**
+ * Clean up resources.
+ */
+ public void dispose() {
+
+ }
+
+ protected void finalize() {
+
+ }
+
+}
diff --git a/core/java/android/syncml/package.html b/core/java/android/syncml/package.html
new file mode 100644
index 0000000..cb4ca46
--- /dev/null
+++ b/core/java/android/syncml/package.html
@@ -0,0 +1,6 @@
+<HTML>
+<BODY>
+Support classes for SyncML.
+{@hide}
+</BODY>
+</HTML> \ No newline at end of file
diff --git a/core/java/android/syncml/pim/PropertyNode.java b/core/java/android/syncml/pim/PropertyNode.java
new file mode 100644
index 0000000..13d4930
--- /dev/null
+++ b/core/java/android/syncml/pim/PropertyNode.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.syncml.pim;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+import android.content.ContentValues;
+
+public class PropertyNode {
+
+ public String propName;
+
+ public String propValue = "";
+
+ public Collection<String> propValue_vector;
+
+ /** Store value as byte[],after decode. */
+ public byte[] propValue_byts;
+
+ /** param store: key=paramType, value=paramValue */
+ public ContentValues paraMap = new ContentValues();
+
+ /** Only for TYPE=??? param store. */
+ public ArrayList<String> paraMap_TYPE = new ArrayList<String>();
+}
diff --git a/core/java/android/syncml/pim/VBuilder.java b/core/java/android/syncml/pim/VBuilder.java
new file mode 100644
index 0000000..822c2ce
--- /dev/null
+++ b/core/java/android/syncml/pim/VBuilder.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.syncml.pim;
+
+import java.util.Collection;
+
+public interface VBuilder {
+ void start();
+
+ void end();
+
+ /**
+ * @param type
+ * VXX <br>
+ * BEGIN:VXX
+ */
+ void startRecord(String type);
+
+ /** END:VXX */
+ void endRecord();
+
+ void startProperty();
+
+ void endProperty();
+
+ /**
+ * @param name
+ * a.N <br>
+ * a.N
+ */
+ void propertyName(String name);
+
+ /**
+ * @param type
+ * LANGUAGE \ ENCODING <br>
+ * ;LANGUage= \ ;ENCODING=
+ */
+ void propertyParamType(String type);
+
+ /**
+ * @param value
+ * FR-EN \ GBK <br>
+ * FR-EN \ GBK
+ */
+ void propertyParamValue(String value);
+
+ void propertyValues(Collection<String> values);
+}
diff --git a/core/java/android/syncml/pim/VDataBuilder.java b/core/java/android/syncml/pim/VDataBuilder.java
new file mode 100644
index 0000000..f0a0cb9
--- /dev/null
+++ b/core/java/android/syncml/pim/VDataBuilder.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.syncml.pim;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.codec.net.QuotedPrintableCodec;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+/**
+ * Store the parse result to custom datastruct: VNode, PropertyNode
+ * Maybe several vcard instance, so use vNodeList to store.
+ * VNode: standy by a vcard instance.
+ * PropertyNode: standy by a property line of a card.
+ */
+public class VDataBuilder implements VBuilder {
+
+ /** type=VNode */
+ public ArrayList<VNode> vNodeList = new ArrayList<VNode>();
+ int nodeListPos = 0;
+ VNode curVNode;
+ PropertyNode curPropNode;
+ String curParamType;
+
+ public void start() {
+ }
+
+ public void end() {
+ }
+
+ public void startRecord(String type) {
+ VNode vnode = new VNode();
+ vnode.parseStatus = 1;
+ vnode.VName = type;
+ vNodeList.add(vnode);
+ nodeListPos = vNodeList.size()-1;
+ curVNode = vNodeList.get(nodeListPos);
+ }
+
+ public void endRecord() {
+ VNode endNode = vNodeList.get(nodeListPos);
+ endNode.parseStatus = 0;
+ while(nodeListPos > 0){
+ nodeListPos--;
+ if((vNodeList.get(nodeListPos)).parseStatus == 1)
+ break;
+ }
+ curVNode = vNodeList.get(nodeListPos);
+ }
+
+ public void startProperty() {
+ // System.out.println("+ startProperty. ");
+ }
+
+ public void endProperty() {
+ // System.out.println("- endProperty. ");
+ }
+
+ public void propertyName(String name) {
+ curPropNode = new PropertyNode();
+ curPropNode.propName = name;
+ }
+
+ public void propertyParamType(String type) {
+ curParamType = type;
+ }
+
+ public void propertyParamValue(String value) {
+ if(curParamType == null)
+ curPropNode.paraMap_TYPE.add(value);
+ else if(curParamType.equalsIgnoreCase("TYPE"))
+ curPropNode.paraMap_TYPE.add(value);
+ else
+ curPropNode.paraMap.put(curParamType, value);
+
+ curParamType = null;
+ }
+
+ public void propertyValues(Collection<String> values) {
+ curPropNode.propValue_vector = values;
+ curPropNode.propValue = listToString(values);
+ //decode value string to propValue_byts
+ if(curPropNode.paraMap.containsKey("ENCODING")){
+ if(curPropNode.paraMap.getAsString("ENCODING").
+ equalsIgnoreCase("BASE64")){
+ curPropNode.propValue_byts =
+ Base64.decodeBase64(curPropNode.propValue.
+ replaceAll(" ","").replaceAll("\t","").
+ replaceAll("\r\n","").
+ getBytes());
+ }
+ if(curPropNode.paraMap.getAsString("ENCODING").
+ equalsIgnoreCase("QUOTED-PRINTABLE")){
+ try{
+ curPropNode.propValue_byts =
+ QuotedPrintableCodec.decodeQuotedPrintable(
+ curPropNode.propValue.
+ replaceAll("= ", " ").replaceAll("=\t", "\t").
+ getBytes() );
+ curPropNode.propValue =
+ new String(curPropNode.propValue_byts);
+ }catch(Exception e){
+ System.out.println("=Decode quoted-printable exception.");
+ e.printStackTrace();
+ }
+ }
+ }
+ curVNode.propList.add(curPropNode);
+ }
+
+ private String listToString(Collection<String> list){
+ StringBuilder typeListB = new StringBuilder();
+ for (String type : list) {
+ typeListB.append(type).append(";");
+ }
+ int len = typeListB.length();
+ if (len > 0 && typeListB.charAt(len - 1) == ';') {
+ return typeListB.substring(0, len - 1);
+ }
+ return typeListB.toString();
+ }
+
+ public String getResult(){
+ return null;
+ }
+}
+
diff --git a/core/java/android/syncml/pim/VNode.java b/core/java/android/syncml/pim/VNode.java
new file mode 100644
index 0000000..9015415
--- /dev/null
+++ b/core/java/android/syncml/pim/VNode.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.syncml.pim;
+
+import java.util.ArrayList;
+
+public class VNode {
+
+ public String VName;
+
+ public ArrayList<PropertyNode> propList = new ArrayList<PropertyNode>();
+
+ /** 0:parse over. 1:parsing. */
+ public int parseStatus = 1;
+}
diff --git a/core/java/android/syncml/pim/VParser.java b/core/java/android/syncml/pim/VParser.java
new file mode 100644
index 0000000..df93f38
--- /dev/null
+++ b/core/java/android/syncml/pim/VParser.java
@@ -0,0 +1,723 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.syncml.pim;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
+
+/**
+ * This interface is used to parse the V format files, such as VCard & VCal
+ *
+ */
+abstract public class VParser {
+
+ /**
+ * The buffer used to store input stream
+ */
+ protected String mBuffer = null;
+
+ /** The builder to build parsed data */
+ protected VBuilder mBuilder = null;
+
+ /** The encoding type */
+ protected String mEncoding = null;
+
+ protected final int PARSE_ERROR = -1;
+
+ protected final String mDefaultEncoding = "8BIT";
+
+ /**
+ * If offset reach '\r\n' return 2. Else return PARSE_ERROR.
+ */
+ protected int parseCrlf(int offset) {
+ if (offset >= mBuffer.length())
+ return PARSE_ERROR;
+ char ch = mBuffer.charAt(offset);
+ if (ch == '\r') {
+ offset++;
+ ch = mBuffer.charAt(offset);
+ if (ch == '\n') {
+ return 2;
+ }
+ }
+ return PARSE_ERROR;
+ }
+
+ /**
+ * Parse the given stream
+ *
+ * @param is
+ * The source to parse.
+ * @param encoding
+ * The encoding type.
+ * @param builder
+ * The v builder which used to construct data.
+ * @return Return true for success, otherwise false.
+ * @throws IOException
+ */
+ public boolean parse(InputStream is, String encoding, VBuilder builder)
+ throws IOException {
+ setInputStream(is, encoding);
+ mBuilder = builder;
+ int ret = 0, offset = 0, sum = 0;
+
+ if (mBuilder != null) {
+ mBuilder.start();
+ }
+ for (;;) {
+ ret = parseVFile(offset); // for next property length
+ if (PARSE_ERROR == ret) {
+ break;
+ } else {
+ offset += ret;
+ sum += ret;
+ }
+ }
+ if (mBuilder != null) {
+ mBuilder.end();
+ }
+ return (mBuffer.length() == sum);
+ }
+
+ /**
+ * Copy the content of input stream and filter the "folding"
+ */
+ protected void setInputStream(InputStream is, String encoding)
+ throws UnsupportedEncodingException {
+ InputStreamReader reader = new InputStreamReader(is, encoding);
+ StringBuilder b = new StringBuilder();
+
+ int ch;
+ try {
+ while ((ch = reader.read()) != -1) {
+ if (ch == '\r') {
+ ch = reader.read();
+ if (ch == '\n') {
+ ch = reader.read();
+ if (ch == ' ' || ch == '\t') {
+ b.append((char) ch);
+ continue;
+ }
+ b.append("\r\n");
+ if (ch == -1) {
+ break;
+ }
+ } else {
+ b.append("\r");
+ }
+ }
+ b.append((char) ch);
+ }
+ mBuffer = b.toString();
+ } catch (Exception e) {
+ return;
+ }
+ return;
+ }
+
+ /**
+ * abstract function, waiting implement.<br>
+ * analyse from offset, return the length of consumed property.
+ */
+ abstract protected int parseVFile(int offset);
+
+ /**
+ * From offset, jump ' ', '\t', '\r\n' sequence, return the length of jump.<br>
+ * 1 * (SPACE / HTAB / CRLF)
+ */
+ protected int parseWsls(int offset) {
+ int ret = 0, sum = 0;
+
+ try {
+ char ch = mBuffer.charAt(offset);
+ if (ch == ' ' || ch == '\t') {
+ sum++;
+ offset++;
+ } else if ((ret = parseCrlf(offset)) != PARSE_ERROR) {
+ offset += ret;
+ sum += ret;
+ } else {
+ return PARSE_ERROR;
+ }
+ for (;;) {
+ ch = mBuffer.charAt(offset);
+ if (ch == ' ' || ch == '\t') {
+ sum++;
+ offset++;
+ } else if ((ret = parseCrlf(offset)) != PARSE_ERROR) {
+ offset += ret;
+ sum += ret;
+ } else {
+ break;
+ }
+ }
+ } catch (IndexOutOfBoundsException e) {
+ ;
+ }
+ if (sum > 0)
+ return sum;
+ return PARSE_ERROR;
+ }
+
+ /**
+ * To determine if the given string equals to the start of the current
+ * string.
+ *
+ * @param offset
+ * The offset in buffer of current string
+ * @param tar
+ * The given string.
+ * @param ignoreCase
+ * To determine case sensitive or not.
+ * @return The consumed characters, otherwise return PARSE_ERROR.
+ */
+ protected int parseString(int offset, final String tar, boolean ignoreCase) {
+ int sum = 0;
+ if (tar == null) {
+ return PARSE_ERROR;
+ }
+
+ if (ignoreCase) {
+ int len = tar.length();
+ try {
+ if (mBuffer.substring(offset, offset + len).equalsIgnoreCase(
+ tar)) {
+ sum = len;
+ } else {
+ return PARSE_ERROR;
+ }
+ } catch (IndexOutOfBoundsException e) {
+ return PARSE_ERROR;
+ }
+
+ } else { /* case sensitive */
+ if (mBuffer.startsWith(tar, offset)) {
+ sum = tar.length();
+ } else {
+ return PARSE_ERROR;
+ }
+ }
+ return sum;
+ }
+
+ /**
+ * Skip the white space in string.
+ */
+ protected int removeWs(int offset) {
+ if (offset >= mBuffer.length())
+ return PARSE_ERROR;
+ int sum = 0;
+ char ch;
+ while ((ch = mBuffer.charAt(offset)) == ' ' || ch == '\t') {
+ offset++;
+ sum++;
+ }
+ return sum;
+ }
+
+ /**
+ * "X-" word, and its value. Return consumed length.
+ */
+ protected int parseXWord(int offset) {
+ int ret = 0, sum = 0;
+ ret = parseString(offset, "X-", true);
+ if (PARSE_ERROR == ret)
+ return PARSE_ERROR;
+ offset += ret;
+ sum += ret;
+
+ ret = parseWord(offset);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ sum += ret;
+ return sum;
+ }
+
+ /**
+ * From offset, parse as :mEncoding ?= 7bit / 8bit / quoted-printable /
+ * base64
+ */
+ protected int parseValue(int offset) {
+ int ret = 0;
+
+ if (mEncoding == null || mEncoding.equalsIgnoreCase("7BIT")
+ || mEncoding.equalsIgnoreCase("8BIT")
+ || mEncoding.toUpperCase().startsWith("X-")) {
+ ret = parse8bit(offset);
+ if (ret != PARSE_ERROR) {
+ return ret;
+ }
+ return PARSE_ERROR;
+ }
+
+ if (mEncoding.equalsIgnoreCase("QUOTED-PRINTABLE")) {
+ ret = parseQuotedPrintable(offset);
+ if (ret != PARSE_ERROR) {
+ return ret;
+ }
+ return PARSE_ERROR;
+ }
+
+ if (mEncoding.equalsIgnoreCase("BASE64")) {
+ ret = parseBase64(offset);
+ if (ret != PARSE_ERROR) {
+ return ret;
+ }
+ return PARSE_ERROR;
+ }
+ return PARSE_ERROR;
+ }
+
+ /**
+ * Refer to RFC 1521, 8bit text
+ */
+ protected int parse8bit(int offset) {
+ int index = 0;
+
+ index = mBuffer.substring(offset).indexOf("\r\n");
+
+ if (index == -1)
+ return PARSE_ERROR;
+ else
+ return index;
+
+ }
+
+ /**
+ * Refer to RFC 1521, quoted printable text ([*(ptext / SPACE / TAB) ptext]
+ * ["="] CRLF)
+ */
+ protected int parseQuotedPrintable(int offset) {
+ int ret = 0, sum = 0;
+
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ for (;;) {
+ ret = parsePtext(offset);
+ if (PARSE_ERROR == ret)
+ break;
+ offset += ret;
+ sum += ret;
+
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+ }
+
+ ret = parseString(offset, "=", false);
+ if (ret != PARSE_ERROR) {
+ // offset += ret;
+ sum += ret;
+ }
+
+ return sum;
+ }
+
+ /**
+ * return 1 or 3 <any ASCII character except "=", SPACE, or TAB>
+ */
+ protected int parsePtext(int offset) {
+ int ret = 0;
+
+ try {
+ char ch = mBuffer.charAt(offset);
+ if (isPrintable(ch) && ch != '=' && ch != ' ' && ch != '\t') {
+ return 1;
+ }
+ } catch (IndexOutOfBoundsException e) {
+ return PARSE_ERROR;
+ }
+
+ ret = parseOctet(offset);
+ if (ret != PARSE_ERROR) {
+ return ret;
+ }
+ return PARSE_ERROR;
+ }
+
+ /**
+ * start with "=" two of (DIGIT / "A" / "B" / "C" / "D" / "E" / "F") <br>
+ * So maybe return 3.
+ */
+ protected int parseOctet(int offset) {
+ int ret = 0, sum = 0;
+
+ ret = parseString(offset, "=", false);
+ if (PARSE_ERROR == ret)
+ return PARSE_ERROR;
+ offset += ret;
+ sum += ret;
+
+ try {
+ int ch = mBuffer.charAt(offset);
+ if (ch == ' ' || ch == '\t')
+ return ++sum;
+ if ((ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'F')) {
+ offset++;
+ sum++;
+ ch = mBuffer.charAt(offset);
+ if ((ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'F')) {
+ sum++;
+ return sum;
+ }
+ }
+ } catch (IndexOutOfBoundsException e) {
+ ;
+ }
+ return PARSE_ERROR;
+ }
+
+ /**
+ * Refer to RFC 1521, base64 text The end of the text is marked with two
+ * CRLF sequences
+ */
+ protected int parseBase64(int offset) {
+ int sum = 0;
+ try {
+ for (;;) {
+ char ch;
+ ch = mBuffer.charAt(offset);
+
+ if (ch == '\r') {
+ int ret = parseString(offset, "\r\n\r\n", false);
+ sum += ret;
+ break;
+ } else {
+ /* ignore none base64 character */
+ sum++;
+ offset++;
+ }
+ }
+ } catch (IndexOutOfBoundsException e) {
+ return PARSE_ERROR;
+ }
+ sum -= 2;/* leave one CRLF to parse the end of this property */
+ return sum;
+ }
+
+ /**
+ * Any printable ASCII sequence except [ ]=:.,;
+ */
+ protected int parseWord(int offset) {
+ int sum = 0;
+ try {
+ for (;;) {
+ char ch = mBuffer.charAt(offset);
+ if (!isPrintable(ch))
+ break;
+ if (ch == ' ' || ch == '=' || ch == ':' || ch == '.'
+ || ch == ',' || ch == ';')
+ break;
+ if (ch == '\\') {
+ ch = mBuffer.charAt(offset + 1);
+ if (ch == ';') {
+ offset++;
+ sum++;
+ }
+ }
+ offset++;
+ sum++;
+ }
+ } catch (IndexOutOfBoundsException e) {
+ ;
+ }
+ if (sum == 0)
+ return PARSE_ERROR;
+ return sum;
+ }
+
+ /**
+ * If it is a letter or digit.
+ */
+ protected boolean isLetterOrDigit(char ch) {
+ if (ch >= '0' && ch <= '9')
+ return true;
+ if (ch >= 'a' && ch <= 'z')
+ return true;
+ if (ch >= 'A' && ch <= 'Z')
+ return true;
+ return false;
+ }
+
+ /**
+ * If it is printable in ASCII
+ */
+ protected boolean isPrintable(char ch) {
+ if (ch >= ' ' && ch <= '~')
+ return true;
+ return false;
+ }
+
+ /**
+ * If it is a letter.
+ */
+ protected boolean isLetter(char ch) {
+ if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Get a word from current position.
+ */
+ protected String getWord(int offset) {
+ StringBuilder word = new StringBuilder();
+ try {
+ for (;;) {
+ char ch = mBuffer.charAt(offset);
+ if (isLetterOrDigit(ch) || ch == '-') {
+ word.append(ch);
+ offset++;
+ } else {
+ break;
+ }
+ }
+ } catch (IndexOutOfBoundsException e) {
+ ;
+ }
+ return word.toString();
+ }
+
+ /**
+ * If is: "INLINE" / "URL" / "CONTENT-ID" / "CID" / "X-" word
+ */
+ protected int parsePValueVal(int offset) {
+ int ret = 0, sum = 0;
+
+ ret = parseString(offset, "INLINE", true);
+ if (ret != PARSE_ERROR) {
+ sum += ret;
+ return sum;
+ }
+
+ ret = parseString(offset, "URL", true);
+ if (ret != PARSE_ERROR) {
+ sum += ret;
+ return sum;
+ }
+
+ ret = parseString(offset, "CONTENT-ID", true);
+ if (ret != PARSE_ERROR) {
+ sum += ret;
+ return sum;
+ }
+
+ ret = parseString(offset, "CID", true);
+ if (ret != PARSE_ERROR) {
+ sum += ret;
+ return sum;
+ }
+
+ ret = parseString(offset, "INLINE", true);
+ if (ret != PARSE_ERROR) {
+ sum += ret;
+ return sum;
+ }
+
+ ret = parseXWord(offset);
+ if (ret != PARSE_ERROR) {
+ sum += ret;
+ return sum;
+ }
+
+ return PARSE_ERROR;
+ }
+
+ /**
+ * If is: "7BIT" / "8BIT" / "QUOTED-PRINTABLE" / "BASE64" / "X-" word and
+ * set mEncoding.
+ */
+ protected int parsePEncodingVal(int offset) {
+ int ret = 0, sum = 0;
+
+ ret = parseString(offset, "7BIT", true);
+ if (ret != PARSE_ERROR) {
+ mEncoding = "7BIT";
+ sum += ret;
+ return sum;
+ }
+
+ ret = parseString(offset, "8BIT", true);
+ if (ret != PARSE_ERROR) {
+ mEncoding = "8BIT";
+ sum += ret;
+ return sum;
+ }
+
+ ret = parseString(offset, "QUOTED-PRINTABLE", true);
+ if (ret != PARSE_ERROR) {
+ mEncoding = "QUOTED-PRINTABLE";
+ sum += ret;
+ return sum;
+ }
+
+ ret = parseString(offset, "BASE64", true);
+ if (ret != PARSE_ERROR) {
+ mEncoding = "BASE64";
+ sum += ret;
+ return sum;
+ }
+
+ ret = parseXWord(offset);
+ if (ret != PARSE_ERROR) {
+ mEncoding = mBuffer.substring(offset).substring(0, ret);
+ sum += ret;
+ return sum;
+ }
+
+ return PARSE_ERROR;
+ }
+
+ /**
+ * Refer to RFC1521, section 7.1<br>
+ * If is: "us-ascii" / "iso-8859-xxx" / "X-" word
+ */
+ protected int parseCharsetVal(int offset) {
+ int ret = 0, sum = 0;
+
+ ret = parseString(offset, "us-ascii", true);
+ if (ret != PARSE_ERROR) {
+ sum += ret;
+ return sum;
+ }
+
+ ret = parseString(offset, "iso-8859-1", true);
+ if (ret != PARSE_ERROR) {
+ sum += ret;
+ return sum;
+ }
+
+ ret = parseString(offset, "iso-8859-2", true);
+ if (ret != PARSE_ERROR) {
+ sum += ret;
+ return sum;
+ }
+
+ ret = parseString(offset, "iso-8859-3", true);
+ if (ret != PARSE_ERROR) {
+ sum += ret;
+ return sum;
+ }
+
+ ret = parseString(offset, "iso-8859-4", true);
+ if (ret != PARSE_ERROR) {
+ sum += ret;
+ return sum;
+ }
+
+ ret = parseString(offset, "iso-8859-5", true);
+ if (ret != PARSE_ERROR) {
+ sum += ret;
+ return sum;
+ }
+
+ ret = parseString(offset, "iso-8859-6", true);
+ if (ret != PARSE_ERROR) {
+ sum += ret;
+ return sum;
+ }
+
+ ret = parseString(offset, "iso-8859-7", true);
+ if (ret != PARSE_ERROR) {
+ sum += ret;
+ return sum;
+ }
+
+ ret = parseString(offset, "iso-8859-8", true);
+ if (ret != PARSE_ERROR) {
+ sum += ret;
+ return sum;
+ }
+
+ ret = parseString(offset, "iso-8859-9", true);
+ if (ret != PARSE_ERROR) {
+ sum += ret;
+ return sum;
+ }
+
+ ret = parseXWord(offset);
+ if (ret != PARSE_ERROR) {
+ sum += ret;
+ return sum;
+ }
+
+ return PARSE_ERROR;
+ }
+
+ /**
+ * Refer to RFC 1766<br>
+ * like: XXX(sequence letters)-XXX(sequence letters)
+ */
+ protected int parseLangVal(int offset) {
+ int ret = 0, sum = 0;
+
+ ret = parseTag(offset);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+
+ for (;;) {
+ ret = parseString(offset, "-", false);
+ if (PARSE_ERROR == ret) {
+ break;
+ }
+ offset += ret;
+ sum += ret;
+
+ ret = parseTag(offset);
+ if (PARSE_ERROR == ret) {
+ break;
+ }
+ offset += ret;
+ sum += ret;
+ }
+ return sum;
+ }
+
+ /**
+ * From first 8 position, is sequence LETTER.
+ */
+ protected int parseTag(int offset) {
+ int sum = 0, i = 0;
+
+ try {
+ for (i = 0; i < 8; i++) {
+ char ch = mBuffer.charAt(offset);
+ if (!isLetter(ch)) {
+ break;
+ }
+ sum++;
+ offset++;
+ }
+ } catch (IndexOutOfBoundsException e) {
+ ;
+ }
+ if (i == 0) {
+ return PARSE_ERROR;
+ }
+ return sum;
+ }
+
+}
diff --git a/core/java/android/syncml/pim/package.html b/core/java/android/syncml/pim/package.html
new file mode 100644
index 0000000..cb4ca46
--- /dev/null
+++ b/core/java/android/syncml/pim/package.html
@@ -0,0 +1,6 @@
+<HTML>
+<BODY>
+Support classes for SyncML.
+{@hide}
+</BODY>
+</HTML> \ No newline at end of file
diff --git a/core/java/android/syncml/pim/vcalendar/CalendarStruct.java b/core/java/android/syncml/pim/vcalendar/CalendarStruct.java
new file mode 100644
index 0000000..3388ada
--- /dev/null
+++ b/core/java/android/syncml/pim/vcalendar/CalendarStruct.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.syncml.pim.vcalendar;
+
+import java.util.List;
+import java.util.ArrayList;
+
+/**
+ * Same comment as ContactStruct.
+ */
+public class CalendarStruct{
+
+ public static class EventStruct{
+ public String description;
+ public String dtend;
+ public String dtstart;
+ public String duration;
+ public String has_alarm;
+ public String last_date;
+ public String rrule;
+ public String status;
+ public String title;
+ public String event_location;
+ public String uid;
+ public List<String> reminderList;
+
+ public void addReminderList(String method){
+ if(reminderList == null)
+ reminderList = new ArrayList<String>();
+ reminderList.add(method);
+ }
+ }
+
+ public String timezone;
+ public List<EventStruct> eventList;
+
+ public void addEventList(EventStruct stru){
+ if(eventList == null)
+ eventList = new ArrayList<EventStruct>();
+ eventList.add(stru);
+ }
+}
diff --git a/core/java/android/syncml/pim/vcalendar/VCalComposer.java b/core/java/android/syncml/pim/vcalendar/VCalComposer.java
new file mode 100644
index 0000000..18b6719
--- /dev/null
+++ b/core/java/android/syncml/pim/vcalendar/VCalComposer.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.syncml.pim.vcalendar;
+
+/**
+ * vCalendar string composer class
+ */
+public class VCalComposer {
+
+ public final static String VERSION_VCALENDAR10 = "vcalendar1.0";
+ public final static String VERSION_VCALENDAR20 = "vcalendar2.0";
+
+ public final static int VERSION_VCAL10_INT = 1;
+ public final static int VERSION_VCAL20_INT = 2;
+
+ private static String mNewLine = "\r\n";
+ private String mVersion = null;
+
+ public VCalComposer() {
+ }
+
+ /**
+ * Create a vCalendar String.
+ * @param struct see more from CalendarStruct class
+ * @param vcalversion MUST be VERSION_VCAL10 /VERSION_VCAL20
+ * @return vCalendar string
+ * @throws VcalException if version is invalid or create failed
+ */
+ public String createVCal(CalendarStruct struct, int vcalversion)
+ throws VCalException{
+
+ StringBuilder returnStr = new StringBuilder();
+
+ //Version check
+ if(vcalversion != 1 && vcalversion != 2)
+ throw new VCalException("version not match 1.0 or 2.0.");
+ if (vcalversion == 1)
+ mVersion = VERSION_VCALENDAR10;
+ else
+ mVersion = VERSION_VCALENDAR20;
+
+ //Build vCalendar:
+ returnStr.append("BEGIN:VCALENDAR").append(mNewLine);
+
+ if(vcalversion == VERSION_VCAL10_INT)
+ returnStr.append("VERSION:1.0").append(mNewLine);
+ else
+ returnStr.append("VERSION:2.0").append(mNewLine);
+
+ returnStr.append("PRODID:vCal ID default").append(mNewLine);
+
+ if(!isNull(struct.timezone)){
+ if(vcalversion == VERSION_VCAL10_INT)
+ returnStr.append("TZ:").append(struct.timezone).append(mNewLine);
+ else//down here MUST have
+ returnStr.append("BEGIN:VTIMEZONE").append(mNewLine).
+ append("TZID:vCal default").append(mNewLine).
+ append("BEGIN:STANDARD").append(mNewLine).
+ append("DTSTART:16010101T000000").append(mNewLine).
+ append("TZOFFSETFROM:").append(struct.timezone).append(mNewLine).
+ append("TZOFFSETTO:").append(struct.timezone).append(mNewLine).
+ append("END:STANDARD").append(mNewLine).
+ append("END:VTIMEZONE").append(mNewLine);
+ }
+ //Build VEVNET
+ for(int i = 0; i < struct.eventList.size(); i++){
+ String str = buildEventStr( struct.eventList.get(i) );
+ returnStr.append(str);
+ }
+
+ //Build VTODO
+ //TODO
+
+ returnStr.append("END:VCALENDAR").append(mNewLine).append(mNewLine);
+
+ return returnStr.toString();
+ }
+
+ private String buildEventStr(CalendarStruct.EventStruct stru){
+
+ StringBuilder strbuf = new StringBuilder();
+
+ strbuf.append("BEGIN:VEVENT").append(mNewLine);
+
+ if(!isNull(stru.uid))
+ strbuf.append("UID:").append(stru.uid).append(mNewLine);
+
+ if(!isNull(stru.description))
+ strbuf.append("DESCRIPTION:").
+ append(foldingString(stru.description)).append(mNewLine);
+
+ if(!isNull(stru.dtend))
+ strbuf.append("DTEND:").append(stru.dtend).append(mNewLine);
+
+ if(!isNull(stru.dtstart))
+ strbuf.append("DTSTART:").append(stru.dtstart).append(mNewLine);
+
+ if(!isNull(stru.duration))
+ strbuf.append("DUE:").append(stru.duration).append(mNewLine);
+
+ if(!isNull(stru.event_location))
+ strbuf.append("LOCATION:").append(stru.event_location).append(mNewLine);
+
+ if(!isNull(stru.last_date))
+ strbuf.append("COMPLETED:").append(stru.last_date).append(mNewLine);
+
+ if(!isNull(stru.rrule))
+ strbuf.append("RRULE:").append(stru.rrule).append(mNewLine);
+
+ if(!isNull(stru.title))
+ strbuf.append("SUMMARY:").append(stru.title).append(mNewLine);
+
+ if(!isNull(stru.status)){
+ String stat = "TENTATIVE";
+ switch (Integer.parseInt(stru.status)){
+ case 0://Calendar.Calendars.STATUS_TENTATIVE
+ stat = "TENTATIVE";
+ break;
+ case 1://Calendar.Calendars.STATUS_CONFIRMED
+ stat = "CONFIRMED";
+ break;
+ case 2://Calendar.Calendars.STATUS_CANCELED
+ stat = "CANCELLED";
+ break;
+ }
+ strbuf.append("STATUS:").append(stat).append(mNewLine);
+ }
+ //Alarm
+ if(!isNull(stru.has_alarm)
+ && stru.reminderList != null
+ && stru.reminderList.size() > 0){
+
+ if (mVersion.equals(VERSION_VCALENDAR10)){
+ String prefix = "";
+ for(String method : stru.reminderList){
+ switch (Integer.parseInt(method)){
+ case 0:
+ prefix = "DALARM";
+ break;
+ case 1:
+ prefix = "AALARM";
+ break;
+ case 2:
+ prefix = "MALARM";
+ break;
+ case 3:
+ default:
+ prefix = "DALARM";
+ break;
+ }
+ strbuf.append(prefix).append(":default").append(mNewLine);
+ }
+ }else {//version 2.0 only support audio-method now.
+ strbuf.append("BEGIN:VALARM").append(mNewLine).
+ append("ACTION:AUDIO").append(mNewLine).
+ append("TRIGGER:-PT10M").append(mNewLine).
+ append("END:VALARM").append(mNewLine);
+ }
+ }
+ strbuf.append("END:VEVENT").append(mNewLine);
+ return strbuf.toString();
+ }
+
+ /** Alter str to folding supported format. */
+ private String foldingString(String str){
+ return str.replaceAll("\r\n", "\n").replaceAll("\n", "\r\n ");
+ }
+
+ /** is null */
+ private boolean isNull(String str){
+ if(str == null || str.trim().equals(""))
+ return true;
+ return false;
+ }
+}
diff --git a/core/java/android/syncml/pim/vcalendar/VCalException.java b/core/java/android/syncml/pim/vcalendar/VCalException.java
new file mode 100644
index 0000000..48ea134
--- /dev/null
+++ b/core/java/android/syncml/pim/vcalendar/VCalException.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.syncml.pim.vcalendar;
+
+public class VCalException extends java.lang.Exception{
+ // constructors
+
+ /**
+ * Constructs a VCalException object
+ */
+
+ public VCalException()
+ {
+ }
+
+ /**
+ * Constructs a VCalException object
+ *
+ * @param message the error message
+ */
+
+ public VCalException( String message )
+ {
+ super( message );
+ }
+
+}
diff --git a/core/java/android/syncml/pim/vcalendar/VCalParser.java b/core/java/android/syncml/pim/vcalendar/VCalParser.java
new file mode 100644
index 0000000..bc2d598
--- /dev/null
+++ b/core/java/android/syncml/pim/vcalendar/VCalParser.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.syncml.pim.vcalendar;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import android.util.Config;
+import android.util.Log;
+
+import android.syncml.pim.VDataBuilder;
+import android.syncml.pim.VParser;
+
+public class VCalParser{
+
+ private final static String TAG = "VCalParser";
+
+ public final static String VERSION_VCALENDAR10 = "vcalendar1.0";
+ public final static String VERSION_VCALENDAR20 = "vcalendar2.0";
+
+ private VParser mParser = null;
+ private String mVersion = null;
+
+ public VCalParser() {
+ }
+
+ public boolean parse(String vcalendarStr, VDataBuilder builder)
+ throws VCalException {
+
+ vcalendarStr = verifyVCal(vcalendarStr);
+ try{
+ boolean isSuccess = mParser.parse(
+ new ByteArrayInputStream(vcalendarStr.getBytes()),
+ "US-ASCII", builder);
+
+ if (!isSuccess) {
+ if (mVersion.equals(VERSION_VCALENDAR10)) {
+ if(Config.LOGD)
+ Log.d(TAG, "Parse failed for vCal 1.0 parser."
+ + " Try to use 2.0 parser.");
+ mVersion = VERSION_VCALENDAR20;
+ return parse(vcalendarStr, builder);
+ }else
+ throw new VCalException("parse failed.(even use 2.0 parser)");
+ }
+ }catch (IOException e){
+ throw new VCalException(e.getMessage());
+ }
+ return true;
+ }
+
+ /**
+ * Verify vCalendar string, and initialize mVersion according to it.
+ * */
+ private String verifyVCal(String vcalStr) {
+
+ //Version check
+ judgeVersion(vcalStr);
+
+ vcalStr = vcalStr.replaceAll("\r\n", "\n");
+ String[] strlist = vcalStr.split("\n");
+
+ StringBuilder replacedStr = new StringBuilder();
+
+ for (int i = 0; i < strlist.length; i++) {
+ if (strlist[i].indexOf(":") < 0) {
+ if (strlist[i].length() == 0 && strlist[i + 1].indexOf(":") > 0)
+ replacedStr.append(strlist[i]).append("\r\n");
+ else
+ replacedStr.append(" ").append(strlist[i]).append("\r\n");
+ } else
+ replacedStr.append(strlist[i]).append("\r\n");
+ }
+ if(Config.LOGD)Log.d(TAG, "After verify:\r\n" + replacedStr.toString());
+
+ return replacedStr.toString();
+ }
+
+ /**
+ * If version not given. Search from vcal string of the VERSION property.
+ * Then instance mParser to appropriate parser.
+ */
+ private void judgeVersion(String vcalStr) {
+
+ if (mVersion == null) {
+ int versionIdx = vcalStr.indexOf("\nVERSION:");
+
+ mVersion = VERSION_VCALENDAR10;
+
+ if (versionIdx != -1){
+ String versionStr = vcalStr.substring(
+ versionIdx, vcalStr.indexOf("\n", versionIdx + 1));
+ if (versionStr.indexOf("2.0") > 0)
+ mVersion = VERSION_VCALENDAR20;
+ }
+ }
+ if (mVersion.equals(VERSION_VCALENDAR10))
+ mParser = new VCalParser_V10();
+ if (mVersion.equals(VERSION_VCALENDAR20))
+ mParser = new VCalParser_V20();
+ }
+}
+
diff --git a/core/java/android/syncml/pim/vcalendar/VCalParser_V10.java b/core/java/android/syncml/pim/vcalendar/VCalParser_V10.java
new file mode 100644
index 0000000..1b251f3
--- /dev/null
+++ b/core/java/android/syncml/pim/vcalendar/VCalParser_V10.java
@@ -0,0 +1,1628 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.syncml.pim.vcalendar;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import android.syncml.pim.VParser;
+
+public class VCalParser_V10 extends VParser {
+
+ /*
+ * The names of the properties whose value are not separated by ";"
+ */
+ private static final HashSet<String> mEvtPropNameGroup1 = new HashSet<String>(
+ Arrays.asList("ATTACH", "ATTENDEE", "DCREATED", "COMPLETED",
+ "DESCRIPTION", "DUE", "DTEND", "EXRULE", "LAST-MODIFIED",
+ "LOCATION", "RNUM", "PRIORITY", "RELATED-TO", "RRULE",
+ "SEQUENCE", "DTSTART", "SUMMARY", "TRANSP", "URL", "UID",
+ // above belong to simprop
+ "CLASS", "STATUS"));
+
+ /*
+ * The names of properties whose value are separated by ";"
+ */
+ private static final HashSet<String> mEvtPropNameGroup2 = new HashSet<String>(
+ Arrays.asList("AALARM", "CATEGORIES", "DALARM", "EXDATE", "MALARM",
+ "PALARM", "RDATE", "RESOURCES"));
+
+ private static final HashSet<String> mValueCAT = new HashSet<String>(Arrays
+ .asList("APPOINTMENT", "BUSINESS", "EDUCATION", "HOLIDAY",
+ "MEETING", "MISCELLANEOUS", "PERSONAL", "PHONE CALL",
+ "SICK DAY", "SPECIAL OCCASION", "TRAVEL", "VACATION"));
+
+ private static final HashSet<String> mValueCLASS = new HashSet<String>(Arrays
+ .asList("PUBLIC", "PRIVATE", "CONFIDENTIAL"));
+
+ private static final HashSet<String> mValueRES = new HashSet<String>(Arrays
+ .asList("CATERING", "CHAIRS", "EASEL", "PROJECTOR", "VCR",
+ "VEHICLE"));
+
+ private static final HashSet<String> mValueSTAT = new HashSet<String>(Arrays
+ .asList("ACCEPTED", "NEEDS ACTION", "SENT", "TENTATIVE",
+ "CONFIRMED", "DECLINED", "COMPLETED", "DELEGATED"));
+
+ /*
+ * The names of properties whose value can contain escape characters
+ */
+ private static final HashSet<String> mEscAllowedProps = new HashSet<String>(
+ Arrays.asList("DESCRIPTION", "SUMMARY", "AALARM", "DALARM",
+ "MALARM", "PALARM"));
+
+ private static final HashMap<String, HashSet<String>> mSpecialValueSetMap =
+ new HashMap<String, HashSet<String>>();
+
+ static {
+ mSpecialValueSetMap.put("CATEGORIES", mValueCAT);
+ mSpecialValueSetMap.put("CLASS", mValueCLASS);
+ mSpecialValueSetMap.put("RESOURCES", mValueRES);
+ mSpecialValueSetMap.put("STATUS", mValueSTAT);
+ }
+
+ public VCalParser_V10() {
+ }
+
+ protected int parseVFile(int offset) {
+ return parseVCalFile(offset);
+ }
+
+ private int parseVCalFile(int offset) {
+ int ret = 0, sum = 0;
+
+ /* remove wsls */
+ while (PARSE_ERROR != (ret = parseWsls(offset))) {
+ offset += ret;
+ sum += ret;
+ }
+
+ ret = parseVCal(offset); // BEGIN:VCAL ... END:VCAL
+ if (PARSE_ERROR != ret) {
+ offset += ret;
+ sum += ret;
+ } else {
+ return PARSE_ERROR;
+ }
+
+ /* remove wsls */
+ while (PARSE_ERROR != (ret = parseWsls(offset))) {
+ offset += ret;
+ sum += ret;
+ }
+ return sum;
+ }
+
+ /**
+ * "BEGIN" [ws] ":" [ws] "VCALENDAR" [ws] 1*crlf calprop calentities [ws]
+ * *crlf "END" [ws] ":" [ws] "VCALENDAR" [ws] 1*CRLF
+ */
+ private int parseVCal(int offset) {
+ int ret = 0, sum = 0;
+
+ /* BEGIN */
+ ret = parseString(offset, "BEGIN", false);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+
+ // [ws]
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ // ":"
+ ret = parseString(offset, ":", false);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+
+ // [ws]
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ // "VCALENDAR
+ ret = parseString(offset, "VCALENDAR", false);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ mBuilder.startRecord("VCALENDAR");
+ }
+
+ /* [ws] */
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ // 1*CRLF
+ ret = parseCrlf(offset);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ while (PARSE_ERROR != (ret = parseCrlf(offset))) {
+ offset += ret;
+ sum += ret;
+ }
+
+ // calprop
+ ret = parseCalprops(offset);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+
+ // calentities
+ ret = parseCalentities(offset);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+
+ // [ws]
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ // *CRLF
+ while (PARSE_ERROR != (ret = parseCrlf(offset))) {
+ offset += ret;
+ sum += ret;
+ }
+
+ // "END"
+ ret = parseString(offset, "END", false);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+
+ // [ws]
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ // ":"
+ // ":"
+ ret = parseString(offset, ":", false);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+
+ // [ws]
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ // "VCALENDAR"
+ ret = parseString(offset, "VCALENDAR", false);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ mBuilder.endRecord();
+ }
+
+ // [ws]
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ // 1 * CRLF
+ ret = parseCrlf(offset);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ while (PARSE_ERROR != (ret = parseCrlf(offset))) {
+ offset += ret;
+ sum += ret;
+ }
+
+ return sum;
+ }
+
+ /**
+ * calprops * CRLF calprop / calprop
+ */
+ private int parseCalprops(int offset) {
+ int ret = 0, sum = 0;
+
+ if (mBuilder != null) {
+ mBuilder.startProperty();
+ }
+ ret = parseCalprop(offset);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ mBuilder.endProperty();
+ }
+
+ for (;;) {
+ /* *CRLF */
+ while (PARSE_ERROR != (ret = parseCrlf(offset))) {
+ offset += ret;
+ sum += ret;
+ }
+ // follow VEVENT ,it wont reach endProperty
+ if (mBuilder != null) {
+ mBuilder.startProperty();
+ }
+ ret = parseCalprop(offset);
+ if (PARSE_ERROR == ret) {
+ break;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ mBuilder.endProperty();
+ }
+ }
+
+ return sum;
+ }
+
+ /**
+ * calentities *CRLF calentity / calentity
+ */
+ private int parseCalentities(int offset) {
+ int ret = 0, sum = 0;
+
+ ret = parseCalentity(offset);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+
+ for (;;) {
+ /* *CRLF */
+ while (PARSE_ERROR != (ret = parseCrlf(offset))) {
+ offset += ret;
+ sum += ret;
+ }
+
+ ret = parseCalentity(offset);
+ if (PARSE_ERROR == ret) {
+ break;
+ }
+ offset += ret;
+ sum += ret;
+ }
+
+ return sum;
+ }
+
+ /**
+ * calprop = DAYLIGHT/ GEO/ PRODID/ TZ/ VERSION
+ */
+ private int parseCalprop(int offset) {
+ int ret = 0;
+
+ ret = parseCalprop0(offset, "DAYLIGHT");
+ if (PARSE_ERROR != ret) {
+ return ret;
+ }
+
+ ret = parseCalprop0(offset, "GEO");
+ if (PARSE_ERROR != ret) {
+ return ret;
+ }
+
+ ret = parseCalprop0(offset, "PRODID");
+ if (PARSE_ERROR != ret) {
+ return ret;
+ }
+
+ ret = parseCalprop0(offset, "TZ");
+ if (PARSE_ERROR != ret) {
+ return ret;
+ }
+
+ ret = parseCalprop1(offset);
+ if (PARSE_ERROR != ret) {
+ return ret;
+ }
+ return PARSE_ERROR;
+ }
+
+ /**
+ * evententity / todoentity
+ */
+ private int parseCalentity(int offset) {
+ int ret = 0;
+
+ ret = parseEvententity(offset);
+ if (PARSE_ERROR != ret) {
+ return ret;
+ }
+
+ ret = parseTodoentity(offset);
+ if (PARSE_ERROR != ret) {
+ return ret;
+ }
+ return PARSE_ERROR;
+
+ }
+
+ /**
+ * propName [params] ":" value CRLF
+ */
+ private int parseCalprop0(int offset, String propName) {
+ int ret = 0, sum = 0, start = 0;
+
+ ret = parseString(offset, propName, true);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ mBuilder.propertyName(propName);
+ }
+
+ ret = parseParams(offset);
+ if (PARSE_ERROR != ret) {
+ offset += ret;
+ sum += ret;
+ }
+
+ ret = parseString(offset, ":", true);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+
+ start = offset;
+ ret = parseValue(offset);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ ArrayList<String> v = new ArrayList<String>();
+ v.add(mBuffer.substring(start, offset));
+ mBuilder.propertyValues(v);
+ }
+
+ ret = parseCrlf(offset);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ sum += ret;
+
+ return sum;
+ }
+
+ /**
+ * "VERSION" [params] ":" "1.0" CRLF
+ */
+ private int parseCalprop1(int offset) {
+ int ret = 0, sum = 0;
+
+ ret = parseString(offset, "VERSION", true);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ mBuilder.propertyName("VERSION");
+ }
+
+ ret = parseParams(offset);
+ if (PARSE_ERROR != ret) {
+ offset += ret;
+ sum += ret;
+ }
+
+ ret = parseString(offset, ":", true);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+
+ ret = parseString(offset, "1.0", true);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ ArrayList<String> v = new ArrayList<String>();
+ v.add("1.0");
+ mBuilder.propertyValues(v);
+ }
+ ret = parseCrlf(offset);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ sum += ret;
+
+ return sum;
+ }
+
+ /**
+ * "BEGIN" [ws] ":" [ws] "VEVENT" [ws] 1*CRLF entprops [ws] *CRLF "END" [ws]
+ * ":" [ws] "VEVENT" [ws] 1*CRLF
+ */
+ private int parseEvententity(int offset) {
+ int ret = 0, sum = 0;
+
+ ret = parseString(offset, "BEGIN", false);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+
+ // [ws]
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ // ":"
+ ret = parseString(offset, ":", false);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+
+ // [ws]
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ // "VEVNET"
+ ret = parseString(offset, "VEVENT", false);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ mBuilder.startRecord("VEVENT");
+ }
+
+ /* [ws] */
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ // 1*CRLF
+ ret = parseCrlf(offset);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ while (PARSE_ERROR != (ret = parseCrlf(offset))) {
+ offset += ret;
+ sum += ret;
+ }
+
+ ret = parseEntprops(offset);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+
+ // [ws]
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ // *CRLF
+ while (PARSE_ERROR != (ret = parseCrlf(offset))) {
+ offset += ret;
+ sum += ret;
+ }
+
+ // "END"
+ ret = parseString(offset, "END", false);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+
+ // [ws]
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ // ":"
+ ret = parseString(offset, ":", false);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+
+ // [ws]
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ // "VEVENT"
+ ret = parseString(offset, "VEVENT", false);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ mBuilder.endRecord();
+ }
+
+ // [ws]
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ // 1 * CRLF
+ ret = parseCrlf(offset);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ while (PARSE_ERROR != (ret = parseCrlf(offset))) {
+ offset += ret;
+ sum += ret;
+ }
+
+ return sum;
+ }
+
+ /**
+ * "BEGIN" [ws] ":" [ws] "VTODO" [ws] 1*CRLF entprops [ws] *CRLF "END" [ws]
+ * ":" [ws] "VTODO" [ws] 1*CRLF
+ */
+ private int parseTodoentity(int offset) {
+ int ret = 0, sum = 0;
+
+ ret = parseString(offset, "BEGIN", false);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+
+ // [ws]
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ // ":"
+ ret = parseString(offset, ":", false);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+
+ // [ws]
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ // "VTODO"
+ ret = parseString(offset, "VTODO", false);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ mBuilder.startRecord("VTODO");
+ }
+
+ // 1*CRLF
+ ret = parseCrlf(offset);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ while (PARSE_ERROR != (ret = parseCrlf(offset))) {
+ offset += ret;
+ sum += ret;
+ }
+
+ ret = parseEntprops(offset);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+
+ // [ws]
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ // *CRLF
+ while (PARSE_ERROR != (ret = parseCrlf(offset))) {
+ offset += ret;
+ sum += ret;
+ }
+
+ // "END"
+ ret = parseString(offset, "END", false);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+
+ // [ws]
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ // ":"
+ ret = parseString(offset, ":", false);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+
+ // [ws]
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ // "VTODO"
+ ret = parseString(offset, "VTODO", false);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ mBuilder.endRecord();
+ }
+
+ // [ws]
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ // 1 * CRLF
+ ret = parseCrlf(offset);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ while (PARSE_ERROR != (ret = parseCrlf(offset))) {
+ offset += ret;
+ sum += ret;
+ }
+
+ return sum;
+ }
+
+ /**
+ * entprops *CRLF entprop / entprop
+ */
+ private int parseEntprops(int offset) {
+ int ret = 0, sum = 0;
+ if (mBuilder != null) {
+ mBuilder.startProperty();
+ }
+
+ ret = parseEntprop(offset);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ mBuilder.endProperty();
+ }
+
+ for (;;) {
+ while (PARSE_ERROR != (ret = parseCrlf(offset))) {
+ offset += ret;
+ sum += ret;
+ }
+ if (mBuilder != null) {
+ mBuilder.startProperty();
+ }
+
+ ret = parseEntprop(offset);
+ if (PARSE_ERROR == ret) {
+ break;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ mBuilder.endProperty();
+ }
+ }
+ return sum;
+ }
+
+ /**
+ * for VEVENT,VTODO prop. entprop0 / entprop1
+ */
+ private int parseEntprop(int offset) {
+ int ret = 0;
+ ret = parseEntprop0(offset);
+ if (PARSE_ERROR != ret) {
+ return ret;
+ }
+
+ ret = parseEntprop1(offset);
+ if (PARSE_ERROR != ret) {
+ return ret;
+ }
+ return PARSE_ERROR;
+ }
+
+ /**
+ * Same with card. ";" [ws] paramlist
+ */
+ private int parseParams(int offset) {
+ int ret = 0, sum = 0;
+
+ ret = parseString(offset, ";", true);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ ret = parseParamlist(offset);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ sum += ret;
+
+ return sum;
+ }
+
+ /**
+ * Same with card. paramlist [ws] ";" [ws] param / param
+ */
+ private int parseParamlist(int offset) {
+ int ret = 0, sum = 0;
+
+ ret = parseParam(offset);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+
+ int offsetTemp = offset;
+ int sumTemp = sum;
+ for (;;) {
+ ret = removeWs(offsetTemp);
+ offsetTemp += ret;
+ sumTemp += ret;
+
+ ret = parseString(offsetTemp, ";", false);
+ if (PARSE_ERROR == ret) {
+ return sum;
+ }
+ offsetTemp += ret;
+ sumTemp += ret;
+
+ ret = removeWs(offsetTemp);
+ offsetTemp += ret;
+ sumTemp += ret;
+
+ ret = parseParam(offsetTemp);
+ if (PARSE_ERROR == ret) {
+ break;
+ }
+ offsetTemp += ret;
+ sumTemp += ret;
+
+ // offset = offsetTemp;
+ sum = sumTemp;
+ }
+ return sum;
+ }
+
+ /**
+ * param0 - param7 / knowntype
+ */
+ private int parseParam(int offset) {
+ int ret = 0;
+
+ ret = parseParam0(offset);
+ if (PARSE_ERROR != ret) {
+ return ret;
+ }
+
+ ret = parseParam1(offset);
+ if (PARSE_ERROR != ret) {
+ return ret;
+ }
+
+ ret = parseParam2(offset);
+ if (PARSE_ERROR != ret) {
+ return ret;
+ }
+
+ ret = parseParam3(offset);
+ if (PARSE_ERROR != ret) {
+ return ret;
+ }
+
+ ret = parseParam4(offset);
+ if (PARSE_ERROR != ret) {
+ return ret;
+ }
+
+ ret = parseParam5(offset);
+ if (PARSE_ERROR != ret) {
+ return ret;
+ }
+
+ ret = parseParam6(offset);
+ if (PARSE_ERROR != ret) {
+ return ret;
+ }
+
+ ret = parseParam7(offset);
+ if (PARSE_ERROR != ret) {
+ return ret;
+ }
+
+ int start = offset;
+ ret = parseKnownType(offset);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ if (mBuilder != null) {
+ mBuilder.propertyParamType(null);
+ mBuilder.propertyParamValue(mBuffer.substring(start, offset));
+ }
+
+ return ret;
+ }
+
+ /**
+ * simprop AND "CLASS" AND "STATUS" The value of these properties are not
+ * seperated by ";"
+ *
+ * [ws] simprop [params] ":" value CRLF
+ */
+ private int parseEntprop0(int offset) {
+ int ret = 0, sum = 0, start = 0;
+
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ String propName = getWord(offset).toUpperCase();
+ if (!mEvtPropNameGroup1.contains(propName)) {
+ if (PARSE_ERROR == parseXWord(offset))
+ return PARSE_ERROR;
+ }
+ ret = propName.length();
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ mBuilder.propertyName(propName);
+ }
+
+ ret = parseParams(offset);
+ if (PARSE_ERROR != ret) {
+ offset += ret;
+ sum += ret;
+ }
+
+ ret = parseString(offset, ":", false);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+
+ start = offset;
+ ret = parseValue(offset);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ ArrayList<String> v = new ArrayList<String>();
+ v.add(exportEntpropValue(propName, mBuffer.substring(start,
+ offset)));
+ mBuilder.propertyValues(v);
+ // Filter value,match string, REFER:RFC
+ if (PARSE_ERROR == valueFilter(propName, v))
+ return PARSE_ERROR;
+ }
+
+ ret = parseCrlf(offset);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ sum += ret;
+ return sum;
+ }
+
+ /**
+ * other event prop names except simprop AND "CLASS" AND "STATUS" The value
+ * of these properties are seperated by ";" [ws] proper name [params] ":"
+ * value CRLF
+ */
+ private int parseEntprop1(int offset) {
+ int ret = 0, sum = 0;
+
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ String propName = getWord(offset).toUpperCase();
+ if (!mEvtPropNameGroup2.contains(propName)) {
+ return PARSE_ERROR;
+ }
+ ret = propName.length();
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ mBuilder.propertyName(propName);
+ }
+
+ ret = parseParams(offset);
+ if (PARSE_ERROR != ret) {
+ offset += ret;
+ sum += ret;
+ }
+
+ ret = parseString(offset, ":", false);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+
+ int start = offset;
+ ret = parseValue(offset);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ // mutil-values
+ if (mBuilder != null) {
+ int end = 0;
+ ArrayList<String> v = new ArrayList<String>();
+ Pattern p = Pattern
+ .compile("([^;\\\\]*(\\\\[\\\\;:,])*[^;\\\\]*)(;?)");
+ Matcher m = p.matcher(mBuffer.substring(start, offset));
+ while (m.find()) {
+ String s = exportEntpropValue(propName, m.group(1));
+ v.add(s);
+ end = m.end();
+ if (offset == start + end) {
+ String endValue = m.group(3);
+ if (";".equals(endValue)) {
+ v.add("");
+ }
+ break;
+ }
+ }
+ mBuilder.propertyValues(v);
+ // Filter value,match string, REFER:RFC
+ if (PARSE_ERROR == valueFilter(propName, v))
+ return PARSE_ERROR;
+ }
+
+ ret = parseCrlf(offset);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ sum += ret;
+ return sum;
+ }
+
+ /**
+ * "TYPE" [ws] = [ws] ptypeval
+ */
+ private int parseParam0(int offset) {
+ int ret = 0, sum = 0, start = offset;
+
+ ret = parseString(offset, "TYPE", true);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ mBuilder.propertyParamType(mBuffer.substring(start, offset));
+ }
+
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ ret = parseString(offset, "=", false);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ start = offset;
+ ret = parsePtypeval(offset);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ mBuilder.propertyParamValue(mBuffer.substring(start, offset));
+ }
+ return sum;
+ }
+
+ /**
+ * ["VALUE" [ws] "=" [ws]] pvalueval
+ */
+ private int parseParam1(int offset) {
+ int ret = 0, sum = 0, start = offset;
+ boolean flag = false;
+
+ ret = parseString(offset, "VALUE", true);
+ if (PARSE_ERROR != ret) {
+ offset += ret;
+ sum += ret;
+ flag = true;
+ }
+ if (flag == true && mBuilder != null) {
+ mBuilder.propertyParamType(mBuffer.substring(start, offset));
+ }
+
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ ret = parseString(offset, "=", true);
+ if (PARSE_ERROR != ret) {
+ if (flag == false) { // "VALUE" does not exist
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ } else {
+ if (flag == true) { // "VALUE" exists
+ return PARSE_ERROR;
+ }
+ }
+
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ start = offset;
+ ret = parsePValueVal(offset);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ mBuilder.propertyParamValue(mBuffer.substring(start, offset));
+ }
+
+ return sum;
+ }
+
+ /** ["ENCODING" [ws] "=" [ws]] pencodingval */
+ private int parseParam2(int offset) {
+ int ret = 0, sum = 0, start = offset;
+ boolean flag = false;
+
+ ret = parseString(offset, "ENCODING", true);
+ if (PARSE_ERROR != ret) {
+ offset += ret;
+ sum += ret;
+ flag = true;
+ }
+ if (flag == true && mBuilder != null) {
+ mBuilder.propertyParamType(mBuffer.substring(start, offset));
+ }
+
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ ret = parseString(offset, "=", true);
+ if (PARSE_ERROR != ret) {
+ if (flag == false) { // "VALUE" does not exist
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ } else {
+ if (flag == true) { // "VALUE" exists
+ return PARSE_ERROR;
+ }
+ }
+
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ start = offset;
+ ret = parsePEncodingVal(offset);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ mBuilder.propertyParamValue(mBuffer.substring(start, offset));
+ }
+
+ return sum;
+ }
+
+ /**
+ * "CHARSET" [WS] "=" [WS] charsetval
+ */
+ private int parseParam3(int offset) {
+ int ret = 0, sum = 0, start = offset;
+
+ ret = parseString(offset, "CHARSET", true);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ mBuilder.propertyParamType(mBuffer.substring(start, offset));
+ }
+
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ ret = parseString(offset, "=", true);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ start = offset;
+ ret = parseCharsetVal(offset);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ mBuilder.propertyParamValue(mBuffer.substring(start, offset));
+ }
+
+ return sum;
+ }
+
+ /**
+ * "LANGUAGE" [ws] "=" [ws] langval
+ */
+ private int parseParam4(int offset) {
+ int ret = 0, sum = 0, start = offset;
+
+ ret = parseString(offset, "LANGUAGE", true);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ mBuilder.propertyParamType(mBuffer.substring(start, offset));
+ }
+
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ ret = parseString(offset, "=", true);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ start = offset;
+ ret = parseLangVal(offset);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ mBuilder.propertyParamValue(mBuffer.substring(start, offset));
+ }
+
+ return sum;
+ }
+
+ /**
+ * "ROLE" [ws] "=" [ws] roleval
+ */
+ private int parseParam5(int offset) {
+ int ret = 0, sum = 0, start = offset;
+
+ ret = parseString(offset, "ROLE", true);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ mBuilder.propertyParamType(mBuffer.substring(start, offset));
+ }
+
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ ret = parseString(offset, "=", true);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ start = offset;
+ ret = parseRoleVal(offset);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ mBuilder.propertyParamValue(mBuffer.substring(start, offset));
+ }
+
+ return sum;
+ }
+
+ /**
+ * "STATUS" [ws] = [ws] statuval
+ */
+ private int parseParam6(int offset) {
+ int ret = 0, sum = 0, start = offset;
+
+ ret = parseString(offset, "STATUS", true);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ mBuilder.propertyParamType(mBuffer.substring(start, offset));
+ }
+
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ ret = parseString(offset, "=", true);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ start = offset;
+ ret = parseStatuVal(offset);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ mBuilder.propertyParamValue(mBuffer.substring(start, offset));
+ }
+
+ return sum;
+
+ }
+
+ /**
+ * XWord [ws] "=" [ws] word
+ */
+ private int parseParam7(int offset) {
+ int ret = 0, sum = 0, start = offset;
+
+ ret = parseXWord(offset);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ mBuilder.propertyParamType(mBuffer.substring(start, offset));
+ }
+
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ ret = parseString(offset, "=", true);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ start = offset;
+ ret = parseWord(offset);
+ if (PARSE_ERROR == ret) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ mBuilder.propertyParamValue(mBuffer.substring(start, offset));
+ }
+
+ return sum;
+
+ }
+
+ /*
+ * "WAVE" / "PCM" / "VCARD" / XWORD
+ */
+ private int parseKnownType(int offset) {
+ int ret = 0;
+
+ ret = parseString(offset, "WAVE", true);
+ if (PARSE_ERROR != ret) {
+ return ret;
+ }
+
+ ret = parseString(offset, "PCM", true);
+ if (PARSE_ERROR != ret) {
+ return ret;
+ }
+
+ ret = parseString(offset, "VCARD", true);
+ if (PARSE_ERROR != ret) {
+ return ret;
+ }
+
+ ret = parseXWord(offset);
+ if (PARSE_ERROR != ret) {
+ return ret;
+ }
+
+ return PARSE_ERROR;
+ }
+
+ /*
+ * knowntype / Xword
+ */
+ private int parsePtypeval(int offset) {
+ int ret = 0;
+
+ ret = parseKnownType(offset);
+ if (PARSE_ERROR != ret) {
+ return ret;
+ }
+
+ ret = parseXWord(offset);
+ if (PARSE_ERROR != ret) {
+ return ret;
+ }
+
+ return PARSE_ERROR;
+ }
+
+ /**
+ * "ATTENDEE" / "ORGANIZER" / "OWNER" / XWORD
+ */
+ private int parseRoleVal(int offset) {
+ int ret = 0;
+
+ ret = parseString(offset, "ATTENDEE", true);
+ if (PARSE_ERROR != ret) {
+ return ret;
+ }
+
+ ret = parseString(offset, "ORGANIZER", true);
+ if (PARSE_ERROR != ret) {
+ return ret;
+ }
+
+ ret = parseString(offset, "OWNER", true);
+ if (PARSE_ERROR != ret) {
+ return ret;
+ }
+
+ ret = parseXWord(offset);
+ if (PARSE_ERROR != ret) {
+ return ret;
+ }
+
+ return PARSE_ERROR;
+ }
+
+ /**
+ * "ACCEPTED" / "NEED ACTION" / "SENT" / "TENTATIVE" / "CONFIRMED" /
+ * "DECLINED" / "COMPLETED" / "DELEGATED / XWORD
+ */
+ private int parseStatuVal(int offset) {
+ int ret = 0;
+
+ ret = parseString(offset, "ACCEPTED", true);
+ if (PARSE_ERROR != ret) {
+ return ret;
+ }
+
+ ret = parseString(offset, "NEED ACTION", true);
+ if (PARSE_ERROR != ret) {
+ return ret;
+ }
+
+ ret = parseString(offset, "TENTATIVE", true);
+ if (PARSE_ERROR != ret) {
+ return ret;
+ }
+ ret = parseString(offset, "CONFIRMED", true);
+ if (PARSE_ERROR != ret) {
+ return ret;
+ }
+ ret = parseString(offset, "DECLINED", true);
+ if (PARSE_ERROR != ret) {
+ return ret;
+ }
+ ret = parseString(offset, "COMPLETED", true);
+ if (PARSE_ERROR != ret) {
+ return ret;
+ }
+ ret = parseString(offset, "DELEGATED", true);
+ if (PARSE_ERROR != ret) {
+ return ret;
+ }
+
+ ret = parseXWord(offset);
+ if (PARSE_ERROR != ret) {
+ return ret;
+ }
+
+ return PARSE_ERROR;
+ }
+
+ /**
+ * Check 4 special propName and it's value to match Hash.
+ *
+ * @return PARSE_ERROR:value not match. 1:go on,like nothing happen.
+ */
+ private int valueFilter(String propName, ArrayList<String> values) {
+ if (propName == null || propName.equals("") || values == null
+ || values.isEmpty())
+ return 1; // go on, like nothing happen.
+
+ if (mSpecialValueSetMap.containsKey(propName)) {
+ for (String value : values) {
+ if (!mSpecialValueSetMap.get(propName).contains(value)) {
+ if (!value.startsWith("X-"))
+ return PARSE_ERROR;
+ }
+ }
+ }
+
+ return 1;
+ }
+
+ /**
+ *
+ * Translate escape characters("\\", "\;") which define in vcalendar1.0
+ * spec. But for fault tolerance, we will translate "\:" and "\,", which
+ * isn't define in vcalendar1.0 explicitly, as the same behavior as other
+ * client.
+ *
+ * Though vcalendar1.0 spec does not defined the value of property
+ * "description", "summary", "aalarm", "dalarm", "malarm" and "palarm" could
+ * contain escape characters, we do support escape characters in these
+ * properties.
+ *
+ * @param str:
+ * the value string will be translated.
+ * @return the string which do not contain any escape character in
+ * vcalendar1.0
+ */
+ private String exportEntpropValue(String propName, String str) {
+ if (null == propName || null == str)
+ return null;
+ if ("".equals(propName) || "".equals(str))
+ return "";
+
+ if (!mEscAllowedProps.contains(propName))
+ return str;
+
+ String tmp = str.replace("\\\\", "\n\r\n");
+ tmp = tmp.replace("\\;", ";");
+ tmp = tmp.replace("\\:", ":");
+ tmp = tmp.replace("\\,", ",");
+ tmp = tmp.replace("\n\r\n", "\\");
+ return tmp;
+ }
+}
diff --git a/core/java/android/syncml/pim/vcalendar/VCalParser_V20.java b/core/java/android/syncml/pim/vcalendar/VCalParser_V20.java
new file mode 100644
index 0000000..5748379
--- /dev/null
+++ b/core/java/android/syncml/pim/vcalendar/VCalParser_V20.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.syncml.pim.vcalendar;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.HashSet;
+
+import android.syncml.pim.VBuilder;
+
+public class VCalParser_V20 extends VCalParser_V10 {
+ private static final String V10LINEBREAKER = "\r\n";
+
+ private static final HashSet<String> acceptableComponents = new HashSet<String>(
+ Arrays.asList("VEVENT", "VTODO", "VALARM", "VTIMEZONE"));
+
+ private static final HashSet<String> acceptableV20Props = new HashSet<String>(
+ Arrays.asList("DESCRIPTION", "DTEND", "DTSTART", "DUE",
+ "COMPLETED", "RRULE", "STATUS", "SUMMARY", "LOCATION"));
+
+ private boolean hasTZ = false; // MUST only have one TZ property
+
+ private String[] lines;
+
+ private int index;
+
+ @Override
+ public boolean parse(InputStream is, String encoding, VBuilder builder)
+ throws IOException {
+ // get useful info for android calendar, and alter to vcal 1.0
+ byte[] bytes = new byte[is.available()];
+ is.read(bytes);
+ String scStr = new String(bytes);
+ StringBuilder v10str = new StringBuilder("");
+
+ lines = splitProperty(scStr);
+ index = 0;
+
+ if ("BEGIN:VCALENDAR".equals(lines[index]))
+ v10str.append("BEGIN:VCALENDAR" + V10LINEBREAKER);
+ else
+ return false;
+ index++;
+ if (false == parseV20Calbody(lines, v10str)
+ || index > lines.length - 1)
+ return false;
+
+ if (lines.length - 1 == index && "END:VCALENDAR".equals(lines[index]))
+ v10str.append("END:VCALENDAR" + V10LINEBREAKER);
+ else
+ return false;
+
+ return super.parse(
+ // use vCal 1.0 parser
+ new ByteArrayInputStream(v10str.toString().getBytes()),
+ encoding, builder);
+ }
+
+ /**
+ * Parse and pick acceptable iCalendar body and translate it to
+ * calendarV1.0 format.
+ * @param lines iCalendar components/properties line list.
+ * @param buffer calendarV10 format string buffer
+ * @return true for success, or false
+ */
+ private boolean parseV20Calbody(String[] lines, StringBuilder buffer) {
+ try {
+ while (!"VERSION:2.0".equals(lines[index]))
+ index++;
+ buffer.append("VERSION:1.0" + V10LINEBREAKER);
+
+ index++;
+ for (; index < lines.length - 1; index++) {
+ String[] keyAndValue = lines[index].split(":", 2);
+ String key = keyAndValue[0];
+ String value = keyAndValue[1];
+
+ if ("BEGIN".equals(key.trim())) {
+ if (!key.equals(key.trim()))
+ return false; // MUST be "BEGIN:componentname"
+ index++;
+ if (false == parseV20Component(value, buffer))
+ return false;
+ }
+ }
+ } catch (ArrayIndexOutOfBoundsException e) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Parse and pick acceptable calendar V2.0's component and translate it to
+ * V1.0 format.
+ * @param compName component name
+ * @param buffer calendarV10 format string buffer
+ * @return true for success, or false
+ * @throws ArrayIndexOutOfBoundsException
+ */
+ private boolean parseV20Component(String compName, StringBuilder buffer)
+ throws ArrayIndexOutOfBoundsException {
+ String endTag = "END:" + compName;
+ String[] propAndValue;
+ String propName, value;
+
+ if (acceptableComponents.contains(compName)) {
+ if ("VEVENT".equals(compName) || "VTODO".equals(compName)) {
+ buffer.append("BEGIN:" + compName + V10LINEBREAKER);
+ while (!endTag.equals(lines[index])) {
+ propAndValue = lines[index].split(":", 2);
+ propName = propAndValue[0].split(";", 2)[0];
+ value = propAndValue[1];
+
+ if ("".equals(lines[index]))
+ buffer.append(V10LINEBREAKER);
+ else if (acceptableV20Props.contains(propName)) {
+ buffer.append(propName + ":" + value + V10LINEBREAKER);
+ } else if ("BEGIN".equals(propName.trim())) {
+ // MUST be BEGIN:VALARM
+ if (propName.equals(propName.trim())
+ && "VALARM".equals(value)) {
+ buffer.append("AALARM:default" + V10LINEBREAKER);
+ while (!"END:VALARM".equals(lines[index]))
+ index++;
+ } else
+ return false;
+ }
+ index++;
+ } // end while
+ buffer.append(endTag + V10LINEBREAKER);
+ } else if ("VALARM".equals(compName)) { // VALARM component MUST
+ // only appear within either VEVENT or VTODO
+ return false;
+ } else if ("VTIMEZONE".equals(compName)) {
+ do {
+ if (false == hasTZ) {// MUST only have 1 time TZ property
+ propAndValue = lines[index].split(":", 2);
+ propName = propAndValue[0].split(";", 2)[0];
+
+ if ("TZOFFSETFROM".equals(propName)) {
+ value = propAndValue[1];
+ buffer.append("TZ" + ":" + value + V10LINEBREAKER);
+ hasTZ = true;
+ }
+ }
+ index++;
+ } while (!endTag.equals(lines[index]));
+ } else
+ return false;
+ } else {
+ while (!endTag.equals(lines[index]))
+ index++;
+ }
+
+ return true;
+ }
+
+ /** split ever property line to String[], not split folding line. */
+ private String[] splitProperty(String scStr) {
+ /*
+ * Property splitted by \n, and unfold folding lines by removing
+ * CRLF+LWSP-char
+ */
+ scStr = scStr.replaceAll("\r\n", "\n").replaceAll("\n ", "")
+ .replaceAll("\n\t", "");
+ String[] strs = scStr.split("\n");
+ return strs;
+ }
+}
diff --git a/core/java/android/syncml/pim/vcalendar/package.html b/core/java/android/syncml/pim/vcalendar/package.html
new file mode 100644
index 0000000..cb4ca46
--- /dev/null
+++ b/core/java/android/syncml/pim/vcalendar/package.html
@@ -0,0 +1,6 @@
+<HTML>
+<BODY>
+Support classes for SyncML.
+{@hide}
+</BODY>
+</HTML> \ No newline at end of file
diff --git a/core/java/android/syncml/pim/vcard/ContactStruct.java b/core/java/android/syncml/pim/vcard/ContactStruct.java
new file mode 100644
index 0000000..8d9b7fa
--- /dev/null
+++ b/core/java/android/syncml/pim/vcard/ContactStruct.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.syncml.pim.vcard;
+
+import java.util.List;
+import java.util.ArrayList;
+
+/**
+ * The parameter class of VCardCreator.
+ * This class standy by the person-contact in
+ * Android system, we must use this class instance as parameter to transmit to
+ * VCardCreator so that create vCard string.
+ */
+// TODO: rename the class name, next step
+public class ContactStruct {
+ public String company;
+ /** MUST exist */
+ public String name;
+ /** maybe folding */
+ public String notes;
+ /** maybe folding */
+ public String title;
+ /** binary bytes of pic. */
+ public byte[] photoBytes;
+ /** mime_type col of images table */
+ public String photoType;
+ /** Only for GET. Use addPhoneList() to PUT. */
+ public List<PhoneData> phoneList;
+ /** Only for GET. Use addContactmethodList() to PUT. */
+ public List<ContactMethod> contactmethodList;
+
+ public static class PhoneData{
+ /** maybe folding */
+ public String data;
+ public String type;
+ public String label;
+ }
+
+ public static class ContactMethod{
+ public String kind;
+ public String type;
+ public String data;
+ public String label;
+ }
+
+ /**
+ * Add a phone info to phoneList.
+ * @param data phone number
+ * @param type type col of content://contacts/phones
+ * @param label lable col of content://contacts/phones
+ */
+ public void addPhone(String data, String type, String label){
+ if(phoneList == null)
+ phoneList = new ArrayList<PhoneData>();
+ PhoneData st = new PhoneData();
+ st.data = data;
+ st.type = type;
+ st.label = label;
+ phoneList.add(st);
+ }
+ /**
+ * Add a contactmethod info to contactmethodList.
+ * @param data contact data
+ * @param type type col of content://contacts/contact_methods
+ */
+ public void addContactmethod(String kind, String data, String type,
+ String label){
+ if(contactmethodList == null)
+ contactmethodList = new ArrayList<ContactMethod>();
+ ContactMethod st = new ContactMethod();
+ st.kind = kind;
+ st.data = data;
+ st.type = type;
+ st.label = label;
+ contactmethodList.add(st);
+ }
+}
diff --git a/core/java/android/syncml/pim/vcard/VCardComposer.java b/core/java/android/syncml/pim/vcard/VCardComposer.java
new file mode 100644
index 0000000..05e8f40
--- /dev/null
+++ b/core/java/android/syncml/pim/vcard/VCardComposer.java
@@ -0,0 +1,350 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.syncml.pim.vcard;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.codec.binary.Base64;
+
+import android.provider.Contacts;
+import android.syncml.pim.vcard.ContactStruct.PhoneData;
+
+/**
+ * Compose VCard string
+ */
+public class VCardComposer {
+ final public static int VERSION_VCARD21_INT = 1;
+
+ final public static int VERSION_VCARD30_INT = 2;
+
+ /**
+ * A new line
+ */
+ private String mNewline;
+
+ /**
+ * The composed string
+ */
+ private StringBuilder mResult;
+
+ /**
+ * The email's type
+ */
+ static final private HashSet<String> emailTypes = new HashSet<String>(
+ Arrays.asList("CELL", "AOL", "APPLELINK", "ATTMAIL", "CIS",
+ "EWORLD", "INTERNET", "IBMMAIL", "MCIMAIL", "POWERSHARE",
+ "PRODIGY", "TLX", "X400"));
+
+ static final private HashSet<String> phoneTypes = new HashSet<String>(
+ Arrays.asList("PREF", "WORK", "HOME", "VOICE", "FAX", "MSG",
+ "CELL", "PAGER", "BBS", "MODEM", "CAR", "ISDN", "VIDEO"));
+
+ static final private String TAG = "VCardComposer";
+
+ public VCardComposer() {
+ }
+
+ private static final HashMap<Integer, String> phoneTypeMap = new HashMap<Integer, String>();
+
+ private static final HashMap<Integer, String> emailTypeMap = new HashMap<Integer, String>();
+
+ static {
+ phoneTypeMap.put(Contacts.Phones.TYPE_HOME, "HOME");
+ phoneTypeMap.put(Contacts.Phones.TYPE_MOBILE, "CELL");
+ phoneTypeMap.put(Contacts.Phones.TYPE_WORK, "WORK");
+ // FAX_WORK not exist in vcard spec. The approximate is the combine of
+ // WORK and FAX, here only map to FAX
+ phoneTypeMap.put(Contacts.Phones.TYPE_FAX_WORK, "WORK;FAX");
+ phoneTypeMap.put(Contacts.Phones.TYPE_FAX_HOME, "HOME;FAX");
+ phoneTypeMap.put(Contacts.Phones.TYPE_PAGER, "PAGER");
+ phoneTypeMap.put(Contacts.Phones.TYPE_OTHER, "X-OTHER");
+ emailTypeMap.put(Contacts.ContactMethods.TYPE_HOME, "HOME");
+ emailTypeMap.put(Contacts.ContactMethods.TYPE_WORK, "WORK");
+ }
+
+ /**
+ * Create a vCard String.
+ *
+ * @param struct
+ * see more from ContactStruct class
+ * @param vcardversion
+ * MUST be VERSION_VCARD21 /VERSION_VCARD30
+ * @return vCard string
+ * @throws VCardException
+ * struct.name is null /vcardversion not match
+ */
+ public String createVCard(ContactStruct struct, int vcardversion)
+ throws VCardException {
+
+ mResult = new StringBuilder();
+ // check exception:
+ if (struct.name == null || struct.name.trim().equals("")) {
+ throw new VCardException(" struct.name MUST have value.");
+ }
+ if (vcardversion == VERSION_VCARD21_INT) {
+ mNewline = "\r\n";
+ } else if (vcardversion == VERSION_VCARD30_INT) {
+ mNewline = "\n";
+ } else {
+ throw new VCardException(
+ " version not match VERSION_VCARD21 or VERSION_VCARD30.");
+ }
+ // build vcard:
+ mResult.append("BEGIN:VCARD").append(mNewline);
+
+ if (vcardversion == VERSION_VCARD21_INT) {
+ mResult.append("VERSION:2.1").append(mNewline);
+ } else {
+ mResult.append("VERSION:3.0").append(mNewline);
+ }
+
+ if (!isNull(struct.name)) {
+ appendNameStr(struct.name);
+ }
+
+ if (!isNull(struct.company)) {
+ mResult.append("ORG:").append(struct.company).append(mNewline);
+ }
+
+ if (!isNull(struct.notes)) {
+ mResult.append("NOTE:").append(
+ foldingString(struct.notes, vcardversion)).append(mNewline);
+ }
+
+ if (!isNull(struct.title)) {
+ mResult.append("TITLE:").append(
+ foldingString(struct.title, vcardversion)).append(mNewline);
+ }
+
+ if (struct.photoBytes != null) {
+ appendPhotoStr(struct.photoBytes, struct.photoType, vcardversion);
+ }
+
+ if (struct.phoneList != null) {
+ appendPhoneStr(struct.phoneList, vcardversion);
+ }
+
+ if (struct.contactmethodList != null) {
+ appendContactMethodStr(struct.contactmethodList, vcardversion);
+ }
+
+ mResult.append("END:VCARD").append(mNewline);
+ return mResult.toString();
+ }
+
+ /**
+ * Alter str to folding supported format.
+ *
+ * @param str
+ * the string to be folded
+ * @param version
+ * the vcard version
+ * @return the folded string
+ */
+ private String foldingString(String str, int version) {
+ if (str.endsWith("\r\n")) {
+ str = str.substring(0, str.length() - 2);
+ } else if (str.endsWith("\n")) {
+ str = str.substring(0, str.length() - 1);
+ } else {
+ return null;
+ }
+
+ str = str.replaceAll("\r\n", "\n");
+ if (version == VERSION_VCARD21_INT) {
+ return str.replaceAll("\n", "\r\n ");
+ } else if (version == VERSION_VCARD30_INT) {
+ return str.replaceAll("\n", "\n ");
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Build LOGO property. format LOGO's param and encode value as base64.
+ *
+ * @param bytes
+ * the binary string to be converted
+ * @param type
+ * the type of the content
+ * @param version
+ * the version of vcard
+ */
+ private void appendPhotoStr(byte[] bytes, String type, int version)
+ throws VCardException {
+ String value, apptype, encodingStr;
+ try {
+ value = foldingString(new String(Base64.encodeBase64(bytes, true)),
+ version);
+ } catch (Exception e) {
+ throw new VCardException(e.getMessage());
+ }
+
+ if (isNull(type)) {
+ type = "image/jpeg";
+ }
+ if (type.indexOf("jpeg") > 0) {
+ apptype = "JPEG";
+ } else if (type.indexOf("gif") > 0) {
+ apptype = "GIF";
+ } else if (type.indexOf("bmp") > 0) {
+ apptype = "BMP";
+ } else {
+ apptype = type.substring(type.indexOf("/")).toUpperCase();
+ }
+
+ mResult.append("LOGO;TYPE=").append(apptype);
+ if (version == VERSION_VCARD21_INT) {
+ encodingStr = ";ENCODING=BASE64:";
+ value = value + mNewline;
+ } else if (version == VERSION_VCARD30_INT) {
+ encodingStr = ";ENCODING=b:";
+ } else {
+ return;
+ }
+ mResult.append(encodingStr).append(value).append(mNewline);
+ }
+
+ private boolean isNull(String str) {
+ if (str == null || str.trim().equals("")) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Build FN and N property. format N's value.
+ *
+ * @param name
+ * the name of the contact
+ */
+ private void appendNameStr(String name) {
+ mResult.append("FN:").append(name).append(mNewline);
+ mResult.append("N:").append(name).append(mNewline);
+ /*
+ * if(name.indexOf(";") > 0)
+ * mResult.append("N:").append(name).append(mNewline); else
+ * if(name.indexOf(" ") > 0) mResult.append("N:").append(name.replace(' ',
+ * ';')). append(mNewline); else
+ * mResult.append("N:").append(name).append("; ").append(mNewline);
+ */
+ }
+
+ /** Loop append TEL property. */
+ private void appendPhoneStr(List<ContactStruct.PhoneData> phoneList,
+ int version) {
+ HashMap<String, String> numMap = new HashMap<String, String>();
+ String joinMark = version == VERSION_VCARD21_INT ? ";" : ",";
+
+ for (ContactStruct.PhoneData phone : phoneList) {
+ String type;
+ if (!isNull(phone.data)) {
+ type = getPhoneTypeStr(phone);
+ if (version == VERSION_VCARD30_INT && type.indexOf(";") != -1) {
+ type = type.replace(";", ",");
+ }
+ if (numMap.containsKey(phone.data)) {
+ type = numMap.get(phone.data) + joinMark + type;
+ }
+ numMap.put(phone.data, type);
+ }
+ }
+
+ for (Map.Entry<String, String> num : numMap.entrySet()) {
+ if (version == VERSION_VCARD21_INT) {
+ mResult.append("TEL;");
+ } else { // vcard3.0
+ mResult.append("TEL;TYPE=");
+ }
+ mResult.append(num.getValue()).append(":").append(num.getKey())
+ .append(mNewline);
+ }
+ }
+
+ private String getPhoneTypeStr(PhoneData phone) {
+
+ int phoneType = Integer.parseInt(phone.type);
+ String typeStr, label;
+
+ if (phoneTypeMap.containsKey(phoneType)) {
+ typeStr = phoneTypeMap.get(phoneType);
+ } else if (phoneType == Contacts.Phones.TYPE_CUSTOM) {
+ label = phone.label.toUpperCase();
+ if (phoneTypes.contains(label) || label.startsWith("X-")) {
+ typeStr = label;
+ } else {
+ typeStr = "X-CUSTOM-" + label;
+ }
+ } else {
+ // TODO: need be updated with the provider's future changes
+ typeStr = "VOICE"; // the default type is VOICE in spec.
+ }
+ return typeStr;
+ }
+
+ /** Loop append ADR / EMAIL property. */
+ private void appendContactMethodStr(
+ List<ContactStruct.ContactMethod> contactMList, int version) {
+
+ HashMap<String, String> emailMap = new HashMap<String, String>();
+ String joinMark = version == VERSION_VCARD21_INT ? ";" : ",";
+ for (ContactStruct.ContactMethod contactMethod : contactMList) {
+ // same with v2.1 and v3.0
+ switch (Integer.parseInt(contactMethod.kind)) {
+ case Contacts.KIND_EMAIL:
+ String mailType = "INTERNET";
+ if (!isNull(contactMethod.data)) {
+ int methodType = new Integer(contactMethod.type).intValue();
+ if (emailTypeMap.containsKey(methodType)) {
+ mailType = emailTypeMap.get(methodType);
+ } else if (emailTypes.contains(contactMethod.label
+ .toUpperCase())) {
+ mailType = contactMethod.label.toUpperCase();
+ }
+ if (emailMap.containsKey(contactMethod.data)) {
+ mailType = emailMap.get(contactMethod.data) + joinMark
+ + mailType;
+ }
+ emailMap.put(contactMethod.data, mailType);
+ }
+ break;
+ case Contacts.KIND_POSTAL:
+ if (!isNull(contactMethod.data)) {
+ mResult.append("ADR;TYPE=POSTAL:").append(
+ foldingString(contactMethod.data, version)).append(
+ mNewline);
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ for (Map.Entry<String, String> email : emailMap.entrySet()) {
+ if (version == VERSION_VCARD21_INT) {
+ mResult.append("EMAIL;");
+ } else {
+ mResult.append("EMAIL;TYPE=");
+ }
+ mResult.append(email.getValue()).append(":").append(email.getKey())
+ .append(mNewline);
+ }
+ }
+}
diff --git a/core/java/android/syncml/pim/vcard/VCardException.java b/core/java/android/syncml/pim/vcard/VCardException.java
new file mode 100644
index 0000000..35b31ec
--- /dev/null
+++ b/core/java/android/syncml/pim/vcard/VCardException.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.syncml.pim.vcard;
+
+public class VCardException extends java.lang.Exception{
+ // constructors
+
+ /**
+ * Constructs a VCardException object
+ */
+
+ public VCardException()
+ {
+ }
+
+ /**
+ * Constructs a VCardException object
+ *
+ * @param message the error message
+ */
+
+ public VCardException( String message )
+ {
+ super( message );
+ }
+
+}
diff --git a/core/java/android/syncml/pim/vcard/VCardParser.java b/core/java/android/syncml/pim/vcard/VCardParser.java
new file mode 100644
index 0000000..3926243
--- /dev/null
+++ b/core/java/android/syncml/pim/vcard/VCardParser.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.syncml.pim.vcard;
+
+import android.syncml.pim.VDataBuilder;
+import android.syncml.pim.VParser;
+import android.util.Config;
+import android.util.Log;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+
+public class VCardParser {
+
+ VParser mParser = null;
+
+ public final static String VERSION_VCARD21 = "vcard2.1";
+
+ public final static String VERSION_VCARD30 = "vcard3.0";
+
+ final public static int VERSION_VCARD21_INT = 1;
+
+ final public static int VERSION_VCARD30_INT = 2;
+
+ String mVersion = null;
+
+ static final private String TAG = "VCardParser";
+
+ public VCardParser() {
+ }
+
+ /**
+ * If version not given. Search from vcard string of the VERSION property.
+ * Then instance mParser to appropriate parser.
+ *
+ * @param vcardStr
+ * the content of vcard data
+ */
+ private void judgeVersion(String vcardStr) {
+ if (mVersion == null) {// auto judge
+ int verIdx = vcardStr.indexOf("\nVERSION:");
+ if (verIdx == -1) // if not have VERSION, v2.1 default
+ mVersion = VERSION_VCARD21;
+ else {
+ String verStr = vcardStr.substring(verIdx, vcardStr.indexOf(
+ "\n", verIdx + 1));
+ if (verStr.indexOf("2.1") > 0)
+ mVersion = VERSION_VCARD21;
+ else if (verStr.indexOf("3.0") > 0)
+ mVersion = VERSION_VCARD30;
+ else
+ mVersion = VERSION_VCARD21;
+ }
+ }
+ if (mVersion.equals(VERSION_VCARD21))
+ mParser = new VCardParser_V21();
+ if (mVersion.equals(VERSION_VCARD30))
+ mParser = new VCardParser_V30();
+ }
+
+ /**
+ * To make sure the vcard string has proper wrap character
+ *
+ * @param vcardStr
+ * the string to be checked
+ * @return string after verified
+ */
+ private String verifyVCard(String vcardStr) {
+ this.judgeVersion(vcardStr);
+ // -- indent line:
+ vcardStr = vcardStr.replaceAll("\r\n", "\n");
+ String[] strlist = vcardStr.split("\n");
+ StringBuilder v21str = new StringBuilder("");
+ for (int i = 0; i < strlist.length; i++) {
+ if (strlist[i].indexOf(":") < 0) {
+ if (strlist[i].length() == 0 && strlist[i + 1].indexOf(":") > 0)
+ v21str.append(strlist[i]).append("\r\n");
+ else
+ v21str.append(" ").append(strlist[i]).append("\r\n");
+ } else
+ v21str.append(strlist[i]).append("\r\n");
+ }
+ return v21str.toString();
+ }
+
+ /**
+ * Set current version
+ *
+ * @param version
+ * the new version
+ */
+ private void setVersion(String version) {
+ this.mVersion = version;
+ }
+
+ /**
+ * Parse the given vcard string
+ *
+ * @param vcardStr
+ * to content to be parsed
+ * @param builder
+ * the data builder to hold data
+ * @return true if the string is successfully parsed, else return false
+ * @throws VCardException
+ * @throws IOException
+ */
+ public boolean parse(String vcardStr, VDataBuilder builder)
+ throws VCardException, IOException {
+
+ vcardStr = this.verifyVCard(vcardStr);
+
+ boolean isSuccess = mParser.parse(new ByteArrayInputStream(vcardStr
+ .getBytes()), "US-ASCII", builder);
+ if (!isSuccess) {
+ if (mVersion.equals(VERSION_VCARD21)) {
+ if (Config.LOGD)
+ Log.d(TAG, "Parse failed for vCard 2.1 parser."
+ + " Try to use 3.0 parser.");
+
+ this.setVersion(VERSION_VCARD30);
+
+ return this.parse(vcardStr, builder);
+ }
+ throw new VCardException("parse failed.(even use 3.0 parser)");
+ }
+ return true;
+ }
+}
diff --git a/core/java/android/syncml/pim/vcard/VCardParser_V21.java b/core/java/android/syncml/pim/vcard/VCardParser_V21.java
new file mode 100644
index 0000000..b6fa032
--- /dev/null
+++ b/core/java/android/syncml/pim/vcard/VCardParser_V21.java
@@ -0,0 +1,970 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.syncml.pim.vcard;
+
+import android.syncml.pim.VParser;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * This class is used to parse vcard. Please refer to vCard Specification 2.1
+ */
+public class VCardParser_V21 extends VParser {
+
+ /** Store the known-type */
+ private static final HashSet<String> mKnownTypeSet = new HashSet<String>(
+ Arrays.asList("DOM", "INTL", "POSTAL", "PARCEL", "HOME", "WORK",
+ "PREF", "VOICE", "FAX", "MSG", "CELL", "PAGER", "BBS",
+ "MODEM", "CAR", "ISDN", "VIDEO", "AOL", "APPLELINK",
+ "ATTMAIL", "CIS", "EWORLD", "INTERNET", "IBMMAIL",
+ "MCIMAIL", "POWERSHARE", "PRODIGY", "TLX", "X400", "GIF",
+ "CGM", "WMF", "BMP", "MET", "PMB", "DIB", "PICT", "TIFF",
+ "PDF", "PS", "JPEG", "QTIME", "MPEG", "MPEG2", "AVI",
+ "WAVE", "AIFF", "PCM", "X509", "PGP"));
+
+ /** Store the name */
+ private static final HashSet<String> mName = new HashSet<String>(Arrays
+ .asList("LOGO", "PHOTO", "LABEL", "FN", "TITLE", "SOUND",
+ "VERSION", "TEL", "EMAIL", "TZ", "GEO", "NOTE", "URL",
+ "BDAY", "ROLE", "REV", "UID", "KEY", "MAILER"));
+
+ /**
+ * Create a new VCard parser.
+ */
+ public VCardParser_V21() {
+ super();
+ }
+
+ /**
+ * Parse the file at the given position
+ *
+ * @param offset
+ * the given position to parse
+ * @return vcard length
+ */
+ protected int parseVFile(int offset) {
+ return parseVCardFile(offset);
+ }
+
+ /**
+ * [wsls] vcard [wsls]
+ */
+ int parseVCardFile(int offset) {
+ int ret = 0, sum = 0;
+
+ /* remove \t \r\n */
+ while ((ret = parseWsls(offset)) != PARSE_ERROR) {
+ offset += ret;
+ sum += ret;
+ }
+
+ ret = parseVCard(offset); // BEGIN:VCARD ... END:VCARD
+ if (ret != PARSE_ERROR) {
+ offset += ret;
+ sum += ret;
+ } else {
+ return PARSE_ERROR;
+ }
+
+ /* remove \t \r\n */
+ while ((ret = parseWsls(offset)) != PARSE_ERROR) {
+ offset += ret;
+ sum += ret;
+ }
+ return sum;
+ }
+
+ /**
+ * "BEGIN" [ws] ":" [ws] "VCARD" [ws] 1*CRLF items *CRLF "END" [ws] ":"
+ * "VCARD"
+ */
+ private int parseVCard(int offset) {
+ int ret = 0, sum = 0;
+
+ /* BEGIN */
+ ret = parseString(offset, "BEGIN", false);
+ if (ret == PARSE_ERROR) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+
+ /* [ws] */
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ /* colon */
+ ret = parseString(offset, ":", false);
+ if (ret == PARSE_ERROR) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+
+ /* [ws] */
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ /* VCARD */
+ ret = parseString(offset, "VCARD", false);
+ if (ret == PARSE_ERROR) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ mBuilder.startRecord("VCARD");
+ }
+
+ /* [ws] */
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ /* 1*CRLF */
+ ret = parseCrlf(offset);
+ if (ret == PARSE_ERROR) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ while ((ret = parseCrlf(offset)) != PARSE_ERROR) {
+ offset += ret;
+ sum += ret;
+ }
+
+ ret = parseItems(offset);
+ if (ret == PARSE_ERROR) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+
+ /* *CRLF */
+ while ((ret = parseCrlf(offset)) != PARSE_ERROR) {
+ offset += ret;
+ sum += ret;
+ }
+
+ /* END */
+ ret = parseString(offset, "END", false);
+ if (ret == PARSE_ERROR) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+
+ /* [ws] */
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ /* colon */
+ ret = parseString(offset, ":", false);
+ if (ret == PARSE_ERROR) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+
+ /* [ws] */
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ /* VCARD */
+ ret = parseString(offset, "VCARD", false);
+ if (ret == PARSE_ERROR) {
+ return PARSE_ERROR;
+ }
+ // offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ mBuilder.endRecord();
+ }
+
+ return sum;
+ }
+
+ /**
+ * items *CRLF item / item
+ */
+ private int parseItems(int offset) {
+ /* items *CRLF item / item */
+ int ret = 0, sum = 0;
+
+ if (mBuilder != null) {
+ mBuilder.startProperty();
+ }
+ ret = parseItem(offset);
+ if (ret == PARSE_ERROR) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ mBuilder.endProperty();
+ }
+
+ for (;;) {
+ while ((ret = parseCrlf(offset)) != PARSE_ERROR) {
+ offset += ret;
+ sum += ret;
+ }
+ // follow VCARD ,it wont reach endProperty
+ if (mBuilder != null) {
+ mBuilder.startProperty();
+ }
+
+ ret = parseItem(offset);
+ if (ret == PARSE_ERROR) {
+ break;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ mBuilder.endProperty();
+ }
+ }
+
+ return sum;
+ }
+
+ /**
+ * item0 / item1 / item2
+ */
+ private int parseItem(int offset) {
+ int ret = 0, sum = 0;
+ mEncoding = mDefaultEncoding;
+
+ ret = parseItem0(offset);
+ if (ret != PARSE_ERROR) {
+ sum += ret;
+ return sum;
+ }
+
+ ret = parseItem1(offset);
+ if (ret != PARSE_ERROR) {
+ sum += ret;
+ return sum;
+ }
+
+ ret = parseItem2(offset);
+ if (ret != PARSE_ERROR) {
+ sum += ret;
+ return sum;
+ }
+
+ return PARSE_ERROR;
+ }
+
+ /** [groups "."] name [params] ":" value CRLF */
+ private int parseItem0(int offset) {
+ int ret = 0, sum = 0, start = offset;
+ String proName = "", proValue = "";
+
+ ret = parseGroupsWithDot(offset);
+ if (ret != PARSE_ERROR) {
+ offset += ret;
+ sum += ret;
+ }
+
+ ret = parseName(offset);
+ if (ret == PARSE_ERROR) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ proName = mBuffer.substring(start, offset).trim();
+ mBuilder.propertyName(proName);
+ }
+
+ ret = parseParams(offset);
+ if (ret != PARSE_ERROR) {
+ offset += ret;
+ sum += ret;
+ }
+
+ ret = parseString(offset, ":", false);
+ if (ret == PARSE_ERROR) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+
+ start = offset;
+ ret = parseValue(offset);
+ if (ret == PARSE_ERROR) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ proValue = mBuffer.substring(start, offset);
+ if (proName.equals("VERSION") && !proValue.equals("2.1")) {
+ return PARSE_ERROR;
+ }
+ if (mBuilder != null) {
+ ArrayList<String> v = new ArrayList<String>();
+ v.add(proValue);
+ mBuilder.propertyValues(v);
+ }
+
+ ret = parseCrlf(offset);
+ if (ret == PARSE_ERROR) {
+ return PARSE_ERROR;
+ }
+ sum += ret;
+
+ return sum;
+ }
+
+ /** "ADR" "ORG" "N" with semi-colon separated content */
+ private int parseItem1(int offset) {
+ int ret = 0, sum = 0, start = offset;
+
+ ret = parseGroupsWithDot(offset);
+ if (ret != PARSE_ERROR) {
+ offset += ret;
+ sum += ret;
+ }
+
+ if ((ret = parseString(offset, "ADR", true)) == PARSE_ERROR
+ && (ret = parseString(offset, "ORG", true)) == PARSE_ERROR
+ && (ret = parseString(offset, "N", true)) == PARSE_ERROR) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ mBuilder.propertyName(mBuffer.substring(start, offset).trim());
+ }
+
+ ret = parseParams(offset);
+ if (ret != PARSE_ERROR) {
+ offset += ret;
+ sum += ret;
+ }
+
+ ret = parseString(offset, ":", false);
+ if (ret == PARSE_ERROR) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+
+ start = offset;
+ ret = parseValue(offset);
+ if (ret == PARSE_ERROR) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ int end = 0;
+ ArrayList<String> v = new ArrayList<String>();
+ Pattern p = Pattern
+ .compile("([^;\\\\]*(\\\\[\\\\;:,])*[^;\\\\]*)(;?)");
+ Matcher m = p.matcher(mBuffer.substring(start, offset));
+ while (m.find()) {
+ String s = escapeTranslator(m.group(1));
+ v.add(s);
+ end = m.end();
+ if (offset == start + end) {
+ String endValue = m.group(3);
+ if (";".equals(endValue)) {
+ v.add("");
+ }
+ break;
+ }
+ }
+ mBuilder.propertyValues(v);
+ }
+
+ ret = parseCrlf(offset);
+ if (ret == PARSE_ERROR) {
+ return PARSE_ERROR;
+ }
+ sum += ret;
+
+ return sum;
+ }
+
+ /** [groups] "." "AGENT" [params] ":" vcard CRLF */
+ private int parseItem2(int offset) {
+ int ret = 0, sum = 0, start = offset;
+
+ ret = parseGroupsWithDot(offset);
+ if (ret != PARSE_ERROR) {
+ offset += ret;
+ sum += ret;
+ }
+
+ ret = parseString(offset, "AGENT", true);
+ if (ret == PARSE_ERROR) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ mBuilder.propertyName(mBuffer.substring(start, offset));
+ }
+
+ ret = parseParams(offset);
+ if (ret != PARSE_ERROR) {
+ offset += ret;
+ sum += ret;
+ }
+
+ ret = parseString(offset, ":", false);
+ if (ret == PARSE_ERROR) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+
+ ret = parseCrlf(offset);
+ if (ret != PARSE_ERROR) {
+ offset += ret;
+ sum += ret;
+ }
+
+ ret = parseVCard(offset);
+ if (ret == PARSE_ERROR) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ mBuilder.propertyValues(new ArrayList<String>());
+ }
+
+ ret = parseCrlf(offset);
+ if (ret == PARSE_ERROR) {
+ return PARSE_ERROR;
+ }
+ sum += ret;
+
+ return sum;
+ }
+
+ private int parseGroupsWithDot(int offset) {
+ int ret = 0, sum = 0;
+ /* [groups "."] */
+ ret = parseGroups(offset);
+ if (ret == PARSE_ERROR) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+
+ ret = parseString(offset, ".", false);
+ if (ret == PARSE_ERROR) {
+ return PARSE_ERROR;
+ }
+ sum += ret;
+
+ return sum;
+ }
+
+ /** ";" [ws] paramlist */
+ private int parseParams(int offset) {
+ int ret = 0, sum = 0;
+
+ ret = parseString(offset, ";", false);
+ if (ret == PARSE_ERROR) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ ret = parseParamList(offset);
+ if (ret == PARSE_ERROR) {
+ return PARSE_ERROR;
+ }
+ sum += ret;
+
+ return sum;
+ }
+
+ /**
+ * paramlist [ws] ";" [ws] param / param
+ */
+ private int parseParamList(int offset) {
+ int ret = 0, sum = 0;
+
+ ret = parseParam(offset);
+ if (ret == PARSE_ERROR) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+
+ int offsetTemp = offset;
+ int sumTemp = sum;
+ for (;;) {
+ ret = removeWs(offsetTemp);
+ offsetTemp += ret;
+ sumTemp += ret;
+
+ ret = parseString(offsetTemp, ";", false);
+ if (ret == PARSE_ERROR) {
+ return sum;
+ }
+ offsetTemp += ret;
+ sumTemp += ret;
+
+ ret = removeWs(offsetTemp);
+ offsetTemp += ret;
+ sumTemp += ret;
+
+ ret = parseParam(offsetTemp);
+ if (ret == PARSE_ERROR) {
+ break;
+ }
+ offsetTemp += ret;
+ sumTemp += ret;
+
+ // offset = offsetTemp;
+ sum = sumTemp;
+ }
+ return sum;
+ }
+
+ /**
+ * param0 / param1 / param2 / param3 / param4 / param5 / knowntype<BR>
+ * TYPE / VALUE / ENDCODING / CHARSET / LANGUAGE ...
+ */
+ private int parseParam(int offset) {
+ int ret = 0, sum = 0;
+
+ ret = parseParam0(offset);
+ if (ret != PARSE_ERROR) {
+ sum += ret;
+ return sum;
+ }
+
+ ret = parseParam1(offset);
+ if (ret != PARSE_ERROR) {
+ sum += ret;
+ return sum;
+ }
+
+ ret = parseParam2(offset);
+ if (ret != PARSE_ERROR) {
+ sum += ret;
+ return sum;
+ }
+
+ ret = parseParam3(offset);
+ if (ret != PARSE_ERROR) {
+ sum += ret;
+ return sum;
+ }
+
+ ret = parseParam4(offset);
+ if (ret != PARSE_ERROR) {
+ sum += ret;
+ return sum;
+ }
+
+ ret = parseParam5(offset);
+ if (ret != PARSE_ERROR) {
+ sum += ret;
+ return sum;
+ }
+
+ int start = offset;
+ ret = parseKnownType(offset);
+ if (ret == PARSE_ERROR) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ mBuilder.propertyParamType(null);
+ mBuilder.propertyParamValue(mBuffer.substring(start, offset));
+ }
+
+ return sum;
+ }
+
+ /** "TYPE" [ws] "=" [ws] ptypeval */
+ private int parseParam0(int offset) {
+ int ret = 0, sum = 0, start = offset;
+
+ ret = parseString(offset, "TYPE", true);
+ if (ret == PARSE_ERROR) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ mBuilder.propertyParamType(mBuffer.substring(start, offset));
+ }
+
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ ret = parseString(offset, "=", false);
+ if (ret == PARSE_ERROR) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ start = offset;
+ ret = parsePTypeVal(offset);
+ if (ret == PARSE_ERROR) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ mBuilder.propertyParamValue(mBuffer.substring(start, offset));
+ }
+
+ return sum;
+
+ }
+
+ /** "VALUE" [ws] "=" [ws] pvalueval */
+ private int parseParam1(int offset) {
+ int ret = 0, sum = 0, start = offset;
+
+ ret = parseString(offset, "VALUE", true);
+ if (ret == PARSE_ERROR) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ mBuilder.propertyParamType(mBuffer.substring(start, offset));
+ }
+
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ ret = parseString(offset, "=", false);
+ if (ret == PARSE_ERROR) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ start = offset;
+ ret = parsePValueVal(offset);
+ if (ret == PARSE_ERROR) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ mBuilder.propertyParamValue(mBuffer.substring(start, offset));
+ }
+
+ return sum;
+ }
+
+ /** "ENCODING" [ws] "=" [ws] pencodingval */
+ private int parseParam2(int offset) {
+ int ret = 0, sum = 0, start = offset;
+
+ ret = parseString(offset, "ENCODING", true);
+ if (ret == PARSE_ERROR) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ mBuilder.propertyParamType(mBuffer.substring(start, offset));
+ }
+
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ ret = parseString(offset, "=", false);
+ if (ret == PARSE_ERROR) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ start = offset;
+ ret = parsePEncodingVal(offset);
+ if (ret == PARSE_ERROR) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ mBuilder.propertyParamValue(mBuffer.substring(start, offset));
+ }
+
+ return sum;
+
+ }
+
+ /** "CHARSET" [ws] "=" [ws] charsetval */
+ private int parseParam3(int offset) {
+ int ret = 0, sum = 0, start = offset;
+
+ ret = parseString(offset, "CHARSET", true);
+ if (ret == PARSE_ERROR) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ mBuilder.propertyParamType(mBuffer.substring(start, offset));
+ }
+
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ ret = parseString(offset, "=", false);
+ if (ret == PARSE_ERROR) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ start = offset;
+ ret = parseCharsetVal(offset);
+ if (ret == PARSE_ERROR) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ mBuilder.propertyParamValue(mBuffer.substring(start, offset));
+ }
+
+ return sum;
+ }
+
+ /** "LANGUAGE" [ws] "=" [ws] langval */
+ private int parseParam4(int offset) {
+ int ret = 0, sum = 0, start = offset;
+
+ ret = parseString(offset, "LANGUAGE", true);
+ if (ret == PARSE_ERROR) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ mBuilder.propertyParamType(mBuffer.substring(start, offset));
+ }
+
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ ret = parseString(offset, "=", false);
+ if (ret == PARSE_ERROR) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ start = offset;
+ ret = parseLangVal(offset);
+ if (ret == PARSE_ERROR) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ mBuilder.propertyParamValue(mBuffer.substring(start, offset));
+ }
+
+ return sum;
+
+ }
+
+ /** "X-" word [ws] "=" [ws] word */
+ private int parseParam5(int offset) {
+ int ret = 0, sum = 0, start = offset;
+
+ ret = parseXWord(offset);
+ if (ret == PARSE_ERROR) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ mBuilder.propertyParamType(mBuffer.substring(start, offset));
+ }
+
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ ret = parseString(offset, "=", false);
+ if (ret == PARSE_ERROR) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+
+ ret = removeWs(offset);
+ offset += ret;
+ sum += ret;
+
+ start = offset;
+ ret = parseWord(offset);
+ if (ret == PARSE_ERROR) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+ if (mBuilder != null) {
+ mBuilder.propertyParamValue(mBuffer.substring(start, offset));
+ }
+
+ return sum;
+ }
+
+ /**
+ * knowntype: "DOM" / "INTL" / ...
+ */
+ private int parseKnownType(int offset) {
+ String word = getWord(offset);
+
+ if (mKnownTypeSet.contains(word.toUpperCase())) {
+ return word.length();
+ }
+ return PARSE_ERROR;
+ }
+
+ /** knowntype / "X-" word */
+ private int parsePTypeVal(int offset) {
+ int ret = 0, sum = 0;
+
+ ret = parseKnownType(offset);
+ if (ret != PARSE_ERROR) {
+ sum += ret;
+ return sum;
+ }
+
+ ret = parseXWord(offset);
+ if (ret != PARSE_ERROR) {
+ sum += ret;
+ return sum;
+ }
+ sum += ret;
+
+ return sum;
+ }
+
+ /** "LOGO" /.../ XWord, case insensitive */
+ private int parseName(int offset) {
+ int ret = 0;
+ ret = parseXWord(offset);
+ if (ret != PARSE_ERROR) {
+ return ret;
+ }
+ String word = getWord(offset).toUpperCase();
+ if (mName.contains(word)) {
+ return word.length();
+ }
+ return PARSE_ERROR;
+ }
+
+ /** groups "." word / word */
+ private int parseGroups(int offset) {
+ int ret = 0, sum = 0;
+
+ ret = parseWord(offset);
+ if (ret == PARSE_ERROR) {
+ return PARSE_ERROR;
+ }
+ offset += ret;
+ sum += ret;
+
+ for (;;) {
+ ret = parseString(offset, ".", false);
+ if (ret == PARSE_ERROR) {
+ break;
+ }
+
+ int ret1 = parseWord(offset);
+ if (ret1 == PARSE_ERROR) {
+ break;
+ }
+ offset += ret + ret1;
+ sum += ret + ret1;
+ }
+ return sum;
+ }
+
+ /**
+ * Translate escape characters("\\", "\;") which define in vcard2.1 spec.
+ * But for fault tolerance, we will translate "\:" and "\,", which isn't
+ * define in vcard2.1 explicitly, as the same behavior as other client.
+ *
+ * @param str:
+ * the string will be translated.
+ * @return the string which do not contain any escape character in vcard2.1
+ */
+ private String escapeTranslator(String str) {
+ if (null == str)
+ return null;
+
+ String tmp = str.replace("\\\\", "\n\r\n");
+ tmp = tmp.replace("\\;", ";");
+ tmp = tmp.replace("\\:", ":");
+ tmp = tmp.replace("\\,", ",");
+ tmp = tmp.replace("\n\r\n", "\\");
+ return tmp;
+ }
+}
diff --git a/core/java/android/syncml/pim/vcard/VCardParser_V30.java b/core/java/android/syncml/pim/vcard/VCardParser_V30.java
new file mode 100644
index 0000000..c56cfed
--- /dev/null
+++ b/core/java/android/syncml/pim/vcard/VCardParser_V30.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.syncml.pim.vcard;
+
+import android.syncml.pim.VBuilder;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+
+/**
+ * This class is used to parse vcard3.0. <br>
+ * It get useful data refer from android contact, and alter to vCard 2.1 format.
+ * Then reuse vcard 2.1 parser to analyze the result.<br>
+ * Please refer to vCard Specification 3.0
+ */
+public class VCardParser_V30 extends VCardParser_V21 {
+ private static final String V21LINEBREAKER = "\r\n";
+
+ private static final HashSet<String> acceptablePropsWithParam = new HashSet<String>(
+ Arrays.asList("PHOTO", "LOGO", "TEL", "EMAIL", "ADR"));
+
+ private static final HashSet<String> acceptablePropsWithoutParam = new HashSet<String>(
+ Arrays.asList("ORG", "NOTE", "TITLE", "FN", "N"));
+
+ private static final HashMap<String, String> propV30ToV21Map = new HashMap<String, String>();
+
+ static {
+ propV30ToV21Map.put("PHOTO", "PHOTO");
+ propV30ToV21Map.put("LOGO", "PHOTO");
+ }
+
+ @Override
+ public boolean parse(InputStream is, String encoding, VBuilder builder)
+ throws IOException {
+ // get useful info for android contact, and alter to vCard 2.1
+ byte[] bytes = new byte[is.available()];
+ is.read(bytes);
+ String scStr = new String(bytes);
+ StringBuilder v21str = new StringBuilder("");
+
+ String[] strlist = splitProperty(scStr);
+
+ if ("BEGIN:vCard".equals(strlist[0])
+ || "BEGIN:VCARD".equals(strlist[0])) {
+ v21str.append("BEGIN:VCARD" + V21LINEBREAKER);
+ } else {
+ return false;
+ }
+
+ for (int i = 1; i < strlist.length - 1; i++) {// for ever property
+ // line
+ String propName;
+ String params;
+ String value;
+
+ String line = strlist[i];
+ if ("".equals(line)) { // line breaker is useful in encoding string
+ v21str.append(V21LINEBREAKER);
+ continue;
+ }
+
+ String[] contentline = line.split(":", 2);
+ String propNameAndParam = contentline[0];
+ value = (contentline.length > 1) ? contentline[1] : "";
+ if (propNameAndParam.length() > 0) {
+ String[] nameAndParams = propNameAndParam.split(";", 2);
+ propName = nameAndParams[0];
+ params = (nameAndParams.length > 1) ? nameAndParams[1] : "";
+
+ if (acceptablePropsWithParam.contains(propName)
+ || acceptablePropsWithoutParam.contains(propName)) {
+ v21str.append(mapContentlineV30ToV21(propName, params,
+ value));
+ }
+ }
+ }// end for
+
+ if ("END:vCard".equals(strlist[strlist.length - 1])
+ || "END:VCARD".equals(strlist[strlist.length - 1])) {
+ v21str.append("END:VCARD" + V21LINEBREAKER);
+ } else {
+ return false;
+ }
+
+ return super.parse(
+ // use vCard 2.1 parser
+ new ByteArrayInputStream(v21str.toString().getBytes()),
+ encoding, builder);
+ }
+
+ /**
+ * Convert V30 string to V21 string
+ *
+ * @param propName
+ * The name of property
+ * @param params
+ * parameter of property
+ * @param value
+ * value of property
+ * @return the converted string
+ */
+ private String mapContentlineV30ToV21(String propName, String params,
+ String value) {
+ String result;
+
+ if (propV30ToV21Map.containsKey(propName)) {
+ result = propV30ToV21Map.get(propName);
+ } else {
+ result = propName;
+ }
+ // Alter parameter part of property to vCard 2.1 format
+ if (acceptablePropsWithParam.contains(propName) && params.length() > 0)
+ result = result
+ + ";"
+ + params.replaceAll(",", ";").replaceAll("ENCODING=B",
+ "ENCODING=BASE64").replaceAll("ENCODING=b",
+ "ENCODING=BASE64");
+
+ return result + ":" + value + V21LINEBREAKER;
+ }
+
+ /**
+ * Split ever property line to Stringp[], not split folding line.
+ *
+ * @param scStr
+ * the string to be splitted
+ * @return a list of splitted string
+ */
+ private String[] splitProperty(String scStr) {
+ /*
+ * Property splitted by \n, and unfold folding lines by removing
+ * CRLF+LWSP-char
+ */
+ scStr = scStr.replaceAll("\r\n", "\n").replaceAll("\n ", "")
+ .replaceAll("\n\t", "");
+ String[] strs = scStr.split("\n");
+ return strs;
+ }
+}
diff --git a/core/java/android/syncml/pim/vcard/package.html b/core/java/android/syncml/pim/vcard/package.html
new file mode 100644
index 0000000..cb4ca46
--- /dev/null
+++ b/core/java/android/syncml/pim/vcard/package.html
@@ -0,0 +1,6 @@
+<HTML>
+<BODY>
+Support classes for SyncML.
+{@hide}
+</BODY>
+</HTML> \ No newline at end of file
diff --git a/core/java/android/test/AndroidTestCase.java b/core/java/android/test/AndroidTestCase.java
new file mode 100644
index 0000000..9bafa32
--- /dev/null
+++ b/core/java/android/test/AndroidTestCase.java
@@ -0,0 +1,86 @@
+/*
+ * 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.test;
+
+import android.content.Context;
+
+import java.lang.reflect.Field;
+
+import junit.framework.TestCase;
+
+/**
+ * Extend this if you need to access Resources or other things that depend on Activity Context.
+ */
+public class AndroidTestCase extends TestCase {
+
+ protected Context mContext;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ public void testAndroidTestCaseSetupProperly() {
+ assertNotNull("Context is null. setContext should be called before tests are run",
+ mContext);
+ }
+
+ public void setContext(Context context) {
+ mContext = context;
+ }
+
+ public Context getContext() {
+ return mContext;
+ }
+
+ /**
+ * This function is called by various TestCase implementations, at tearDown() time, in order
+ * to scrub out any class variables. This protects against memory leaks in the case where a
+ * test case creates a non-static inner class (thus referencing the test case) and gives it to
+ * someone else to hold onto.
+ *
+ * @param testCaseClass The class of the derived TestCase implementation.
+ *
+ * @throws IllegalAccessException
+ */
+ protected void scrubClass(final Class<?> testCaseClass)
+ throws IllegalAccessException {
+ final Field[] fields = getClass().getDeclaredFields();
+ for (Field field : fields) {
+ final Class<?> fieldClass = field.getDeclaringClass();
+ if (testCaseClass.isAssignableFrom(fieldClass) && !field.getType().isPrimitive()) {
+ try {
+ field.setAccessible(true);
+ field.set(this, null);
+ } catch (Exception e) {
+ android.util.Log.d("TestCase", "Error: Could not nullify field!");
+ }
+
+ if (field.get(this) != null) {
+ android.util.Log.d("TestCase", "Error: Could not nullify field!");
+ }
+ }
+ }
+ }
+
+
+}
diff --git a/core/java/android/test/FlakyTest.java b/core/java/android/test/FlakyTest.java
new file mode 100644
index 0000000..919767f
--- /dev/null
+++ b/core/java/android/test/FlakyTest.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.test;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.annotation.ElementType;
+
+/**
+ * This annotation can be used on an {@link android.test.InstrumentationTestCase}'s
+ * test methods. When the annotation is present, the test method is re-executed if
+ * the test fails. The total number of executions is specified by the tolerance and
+ * defaults to 1.
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface FlakyTest {
+ /**
+ * Indicates how many times a test can run and fail before being reported
+ * as a failed test. If the tolerance factor is less than 1, the test runs
+ * only once.
+ *
+ * @return The total number of allowed run, the default is 1.
+ */
+ int tolerance() default 1;
+}
diff --git a/core/java/android/test/InstrumentationTestCase.java b/core/java/android/test/InstrumentationTestCase.java
new file mode 100644
index 0000000..08a8ad1
--- /dev/null
+++ b/core/java/android/test/InstrumentationTestCase.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.test;
+
+import junit.framework.TestCase;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.KeyEvent;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.InvocationTargetException;
+
+/**
+ * A test case that has access to {@link Instrumentation}. See
+ * <code>InstrumentationTestRunner</code>.
+ */
+public class InstrumentationTestCase extends TestCase {
+
+ private Instrumentation mInstrumentation;
+
+ /**
+ * Injects instrumentation into this test case. This method is
+ * called by the test runner during test setup.
+ *
+ * @param instrumentation the instrumentation to use with this instance
+ */
+ public void injectInsrumentation(Instrumentation instrumentation) {
+ mInstrumentation = instrumentation;
+ }
+
+ /**
+ * Inheritors can access the instrumentation using this.
+ * @return instrumentation
+ */
+ public Instrumentation getInstrumentation() {
+ return mInstrumentation;
+ }
+
+ /**
+ * Utility method for launching an activity.
+ * @param pkg The package hosting the activity to be launched.
+ * @param activityCls The activity class to launch.
+ * @param extras Optional extra stuff to pass to the activity.
+ * @return The activity, or null if non launched.
+ */
+ @SuppressWarnings("unchecked")
+ public final <T extends Activity> T launchActivity(
+ String pkg,
+ Class<T> activityCls,
+ Bundle extras) {
+ Intent intent = new Intent(Intent.ACTION_MAIN);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.setClassName(pkg, activityCls.getName());
+ if (extras != null) {
+ intent.putExtras(extras);
+ }
+ T activity = (T) getInstrumentation().startActivitySync(intent);
+ getInstrumentation().waitForIdleSync();
+ return activity;
+ }
+
+ /**
+ * Runs the current unit test. If the unit test is annotated with
+ * {@link android.test.UiThreadTest}, the test is run on the UI thread.
+ */
+ protected void runTest() throws Throwable {
+ String fName = getName();
+ assertNotNull(fName);
+ Method method = null;
+ try {
+ // use getMethod to get all public inherited
+ // methods. getDeclaredMethods returns all
+ // methods of this class but excludes the
+ // inherited ones.
+ method = getClass().getMethod(fName, (Class[]) null);
+ } catch (NoSuchMethodException e) {
+ fail("Method \""+fName+"\" not found");
+ }
+
+ if (!Modifier.isPublic(method.getModifiers())) {
+ fail("Method \""+fName+"\" should be public");
+ }
+
+ int runCount = 1;
+ if (method.isAnnotationPresent(FlakyTest.class)) {
+ runCount = method.getAnnotation(FlakyTest.class).tolerance();
+ }
+
+ if (method.isAnnotationPresent(UiThreadTest.class)) {
+ final int tolerance = runCount;
+ final Method testMethod = method;
+ final Throwable[] exceptions = new Throwable[1];
+ getInstrumentation().runOnMainSync(new Runnable() {
+ public void run() {
+ try {
+ runMethod(testMethod, tolerance);
+ } catch (Throwable throwable) {
+ exceptions[0] = throwable;
+ }
+ }
+ });
+ if (exceptions[0] != null) {
+ throw exceptions[0];
+ }
+ } else {
+ runMethod(method, runCount);
+ }
+ }
+
+ private void runMethod(Method runMethod, int tolerance) throws Throwable {
+ Throwable exception = null;
+
+ int runCount = 0;
+ do {
+ try {
+ runMethod.invoke(this, (Object[]) null);
+ exception = null;
+ } catch (InvocationTargetException e) {
+ e.fillInStackTrace();
+ exception = e.getTargetException();
+ } catch (IllegalAccessException e) {
+ e.fillInStackTrace();
+ exception = e;
+ } finally {
+ runCount++;
+ }
+ } while ((runCount < tolerance) && (exception != null));
+
+ if (exception != null) {
+ throw exception;
+ }
+ }
+
+ /**
+ * Sends a series of key events through instrumentation and waits for idle. The sequence
+ * of keys is a string containing the key names as specified in KeyEvent, without the
+ * KEYCODE_ prefix. For instance: sendKeys("DPAD_LEFT A B C DPAD_CENTER"). Each key can
+ * be repeated by using the N* prefix. For instance, to send two KEYCODE_DPAD_LEFT, use
+ * the following: sendKeys("2*DPAD_LEFT").
+ *
+ * @param keysSequence The sequence of keys.
+ */
+ public void sendKeys(String keysSequence) {
+ final String[] keys = keysSequence.split(" ");
+ final int count = keys.length;
+
+ final Instrumentation instrumentation = getInstrumentation();
+
+ for (int i = 0; i < count; i++) {
+ String key = keys[i];
+ int repeater = key.indexOf('*');
+
+ int keyCount;
+ try {
+ keyCount = repeater == -1 ? 1 : Integer.parseInt(key.substring(0, repeater));
+ } catch (NumberFormatException e) {
+ Log.w("ActivityTestCase", "Invalid repeat count: " + key);
+ continue;
+ }
+
+ if (repeater != -1) {
+ key = key.substring(repeater + 1);
+ }
+
+ for (int j = 0; j < keyCount; j++) {
+ try {
+ final Field keyCodeField = KeyEvent.class.getField("KEYCODE_" + key);
+ final int keyCode = keyCodeField.getInt(null);
+ instrumentation.sendKeyDownUpSync(keyCode);
+ } catch (NoSuchFieldException e) {
+ Log.w("ActivityTestCase", "Unknown keycode: KEYCODE_" + key);
+ break;
+ } catch (IllegalAccessException e) {
+ Log.w("ActivityTestCase", "Unknown keycode: KEYCODE_" + key);
+ break;
+ }
+ }
+ }
+
+ instrumentation.waitForIdleSync();
+ }
+
+ /**
+ * Sends a series of key events through instrumentation and waits for idle. For instance:
+ * sendKeys(KEYCODE_DPAD_LEFT, KEYCODE_DPAD_CENTER).
+ *
+ * @param keys The series of key codes to send through instrumentation.
+ */
+ public void sendKeys(int... keys) {
+ final int count = keys.length;
+ final Instrumentation instrumentation = getInstrumentation();
+
+ for (int i = 0; i < count; i++) {
+ instrumentation.sendKeyDownUpSync(keys[i]);
+ }
+
+ instrumentation.waitForIdleSync();
+ }
+
+ /**
+ * Sends a series of key events through instrumentation and waits for idle. Each key code
+ * must be preceded by the number of times the key code must be sent. For instance:
+ * sendRepeatedKeys(1, KEYCODE_DPAD_CENTER, 2, KEYCODE_DPAD_LEFT).
+ *
+ * @param keys The series of key repeats and codes to send through instrumentation.
+ */
+ public void sendRepeatedKeys(int... keys) {
+ final int count = keys.length;
+ if ((count & 0x1) == 0x1) {
+ throw new IllegalArgumentException("The size of the keys array must "
+ + "be a multiple of 2");
+ }
+
+ final Instrumentation instrumentation = getInstrumentation();
+
+ for (int i = 0; i < count; i += 2) {
+ final int keyCount = keys[i];
+ final int keyCode = keys[i + 1];
+ for (int j = 0; j < keyCount; j++) {
+ instrumentation.sendKeyDownUpSync(keyCode);
+ }
+ }
+
+ instrumentation.waitForIdleSync();
+ }
+
+ /**
+ * Make sure all resources are cleaned up and garbage collected before moving on to the next
+ * test. Subclasses that override this method should make sure they call super.tearDown()
+ * at the end of the overriding method.
+ *
+ * @throws Exception
+ */
+ protected void tearDown() throws Exception {
+ Runtime.getRuntime().gc();
+ Runtime.getRuntime().runFinalization();
+ Runtime.getRuntime().gc();
+ super.tearDown();
+ }
+}
diff --git a/core/java/android/test/InstrumentationTestSuite.java b/core/java/android/test/InstrumentationTestSuite.java
new file mode 100644
index 0000000..2ab949e
--- /dev/null
+++ b/core/java/android/test/InstrumentationTestSuite.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.test;
+
+import android.app.Instrumentation;
+
+import junit.framework.TestSuite;
+import junit.framework.Test;
+import junit.framework.TestResult;
+
+/**
+ * A {@link junit.framework.TestSuite} that injects {@link android.app.Instrumentation} into
+ * {@link InstrumentationTestCase} before running them.
+ */
+public class InstrumentationTestSuite extends TestSuite {
+
+ private final Instrumentation mInstrumentation;
+
+ /**
+ * @param instr The instrumentation that will be injected into each
+ * test before running it.
+ */
+ public InstrumentationTestSuite(Instrumentation instr) {
+ mInstrumentation = instr;
+ }
+
+
+ public InstrumentationTestSuite(String name, Instrumentation instr) {
+ super(name);
+ mInstrumentation = instr;
+ }
+
+ /**
+ * @param theClass Inspected for methods starting with 'test'
+ * @param instr The instrumentation to inject into each test before
+ * running.
+ */
+ public InstrumentationTestSuite(final Class theClass, Instrumentation instr) {
+ super(theClass);
+ mInstrumentation = instr;
+ }
+
+
+ @Override
+ public void addTestSuite(Class testClass) {
+ addTest(new InstrumentationTestSuite(testClass, mInstrumentation));
+ }
+
+
+ @Override
+ public void runTest(Test test, TestResult result) {
+
+ if (test instanceof InstrumentationTestCase) {
+ ((InstrumentationTestCase) test).injectInsrumentation(mInstrumentation);
+ }
+
+ // run the test as usual
+ super.runTest(test, result);
+ }
+}
diff --git a/core/java/android/test/PerformanceTestCase.java b/core/java/android/test/PerformanceTestCase.java
new file mode 100644
index 0000000..679ad40
--- /dev/null
+++ b/core/java/android/test/PerformanceTestCase.java
@@ -0,0 +1,65 @@
+/*
+ * 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.test;
+
+/**
+ * More complex interface performance for test cases.
+ *
+ * If you want your test to be used as a performance test, you must
+ * implement this interface.
+ */
+public interface PerformanceTestCase
+{
+ /**
+ * Callbacks for {@link PerformanceTestCase}.
+ */
+ public interface Intermediates
+ {
+ void setInternalIterations(int count);
+ void startTiming(boolean realTime);
+ void addIntermediate(String name);
+ void addIntermediate(String name, long timeInNS);
+ void finishTiming(boolean realTime);
+ }
+
+ /**
+ * Set up to begin performance tests. The 'intermediates' is a
+ * communication channel to send back intermediate performance numbers --
+ * if you use it, you will probably want to ensure your test is only
+ * executed once by returning 1. Otherwise, return 0 to allow the test
+ * harness to decide the number of iterations.
+ *
+ * <p>If you return a non-zero iteration count, you should call
+ * {@link Intermediates#startTiming intermediates.startTiming} and
+ * {@link Intermediates#finishTiming intermediates.endTiming} to report the
+ * duration of the test whose performance should actually be measured.
+ *
+ * @param intermediates Callback for sending intermediate results.
+ *
+ * @return int Maximum number of iterations to run, or 0 to let the caller
+ * decide.
+ */
+ int startPerformance(Intermediates intermediates);
+
+ /**
+ * This method is used to determine what modes this test case can run in.
+ *
+ * @return true if this test case can only be run in performance mode.
+ */
+ boolean isPerformanceOnly();
+}
+
diff --git a/core/java/android/test/UiThreadTest.java b/core/java/android/test/UiThreadTest.java
new file mode 100644
index 0000000..cd92231
--- /dev/null
+++ b/core/java/android/test/UiThreadTest.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.test;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.annotation.ElementType;
+
+/**
+ * This annotation can be used on an {@link InstrumentationTestCase}'s test methods.
+ * When the annotation is present, the test method is executed on the application's
+ * main thread (or UI thread.) Note that instrumentation methods may not be used
+ * when this annotation is present.
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface UiThreadTest {
+}
diff --git a/core/java/android/test/package.html b/core/java/android/test/package.html
new file mode 100644
index 0000000..1972bed
--- /dev/null
+++ b/core/java/android/test/package.html
@@ -0,0 +1,5 @@
+<HTML>
+<BODY>
+A framework for writing Android test cases and suites.
+</BODY>
+</HTML> \ No newline at end of file
diff --git a/core/java/android/test/suitebuilder/annotation/Smoke.java b/core/java/android/test/suitebuilder/annotation/Smoke.java
new file mode 100644
index 0000000..237e033
--- /dev/null
+++ b/core/java/android/test/suitebuilder/annotation/Smoke.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.test.suitebuilder.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marks a test that should run as part of the smoke tests.
+ * The <code>android.test.suitebuilder.SmokeTestSuiteBuilder</code>
+ * will run all tests with this annotation.
+ *
+ * @see android.test.suitebuilder.SmokeTestSuiteBuilder
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD, ElementType.TYPE})
+public @interface Smoke {
+}
diff --git a/core/java/android/test/suitebuilder/annotation/Suppress.java b/core/java/android/test/suitebuilder/annotation/Suppress.java
new file mode 100644
index 0000000..f16c8fa
--- /dev/null
+++ b/core/java/android/test/suitebuilder/annotation/Suppress.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.test.suitebuilder.annotation;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.annotation.ElementType;
+
+/**
+ * Use this annotation on test classes or test methods that should not be included in a test
+ * suite. If the annotation appears on the class then no tests in that class will be included. If
+ * the annotation appears only on a test method then only that method will be excluded.
+ *
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD, ElementType.TYPE})
+public @interface Suppress {
+}
diff --git a/core/java/android/text/AlteredCharSequence.java b/core/java/android/text/AlteredCharSequence.java
new file mode 100644
index 0000000..4cc71fd
--- /dev/null
+++ b/core/java/android/text/AlteredCharSequence.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text;
+
+// XXX should this really be in the public API at all?
+/**
+ * An AlteredCharSequence is a CharSequence that is largely mirrored from
+ * another CharSequence, except that a specified range of characters are
+ * mirrored from a different char array instead.
+ */
+public class AlteredCharSequence
+implements CharSequence, GetChars
+{
+ /**
+ * Create an AlteredCharSequence whose text (and possibly spans)
+ * are mirrored from <code>source</code>, except that the range of
+ * offsets <code>substart</code> inclusive to <code>subend</code> exclusive
+ * are mirrored instead from <code>sub</code>, beginning at offset 0.
+ */
+ public static AlteredCharSequence make(CharSequence source, char[] sub,
+ int substart, int subend) {
+ if (source instanceof Spanned)
+ return new AlteredSpanned(source, sub, substart, subend);
+ else
+ return new AlteredCharSequence(source, sub, substart, subend);
+ }
+
+ private AlteredCharSequence(CharSequence source, char[] sub,
+ int substart, int subend) {
+ mSource = source;
+ mChars = sub;
+ mStart = substart;
+ mEnd = subend;
+ }
+
+ /* package */ void update(char[] sub, int substart, int subend) {
+ mChars = sub;
+ mStart = substart;
+ mEnd = subend;
+ }
+
+ private static class AlteredSpanned
+ extends AlteredCharSequence
+ implements Spanned
+ {
+ private AlteredSpanned(CharSequence source, char[] sub,
+ int substart, int subend) {
+ super(source, sub, substart, subend);
+ mSpanned = (Spanned) source;
+ }
+
+ public <T> T[] getSpans(int start, int end, Class<T> kind) {
+ return mSpanned.getSpans(start, end, kind);
+ }
+
+ public int getSpanStart(Object span) {
+ return mSpanned.getSpanStart(span);
+ }
+
+ public int getSpanEnd(Object span) {
+ return mSpanned.getSpanEnd(span);
+ }
+
+ public int getSpanFlags(Object span) {
+ return mSpanned.getSpanFlags(span);
+ }
+
+ public int nextSpanTransition(int start, int end, Class kind) {
+ return mSpanned.nextSpanTransition(start, end, kind);
+ }
+
+ private Spanned mSpanned;
+ }
+
+ public char charAt(int off) {
+ if (off >= mStart && off < mEnd)
+ return mChars[off - mStart];
+ else
+ return mSource.charAt(off);
+ }
+
+ public int length() {
+ return mSource.length();
+ }
+
+ public CharSequence subSequence(int start, int end) {
+ return AlteredCharSequence.make(mSource.subSequence(start, end),
+ mChars, mStart - start, mEnd - start);
+ }
+
+ public void getChars(int start, int end, char[] dest, int off) {
+ TextUtils.getChars(mSource, start, end, dest, off);
+
+ start = Math.max(mStart, start);
+ end = Math.min(mEnd, end);
+
+ if (start > end)
+ System.arraycopy(mChars, start - mStart, dest, off, end - start);
+ }
+
+ public String toString() {
+ int len = length();
+
+ char[] ret = new char[len];
+ getChars(0, len, ret, 0);
+ return String.valueOf(ret);
+ }
+
+ private int mStart;
+ private int mEnd;
+ private char[] mChars;
+ private CharSequence mSource;
+}
diff --git a/core/java/android/text/AndroidCharacter.java b/core/java/android/text/AndroidCharacter.java
new file mode 100644
index 0000000..6dfd64d
--- /dev/null
+++ b/core/java/android/text/AndroidCharacter.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text;
+
+/**
+ * AndroidCharacter exposes some character properties that are not
+ * easily accessed from java.lang.Character.
+ */
+public class AndroidCharacter
+{
+ /**
+ * Fill in the first <code>count</code> bytes of <code>dest</code> with the
+ * directionalities from the first <code>count</code> chars of <code>src</code>.
+ * This is just like Character.getDirectionality() except it is a
+ * batch operation.
+ */
+ public native static void getDirectionalities(char[] src, byte[] dest,
+ int count);
+ /**
+ * Replace the specified slice of <code>text</code> with the chars'
+ * right-to-left mirrors (if any), returning true if any
+ * replacements were made.
+ */
+ public native static boolean mirror(char[] text, int start, int count);
+
+ /**
+ * Return the right-to-left mirror (or the original char if none)
+ * of the specified char.
+ */
+ public native static char getMirror(char ch);
+}
diff --git a/core/java/android/text/Annotation.java b/core/java/android/text/Annotation.java
new file mode 100644
index 0000000..a3812a8
--- /dev/null
+++ b/core/java/android/text/Annotation.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text;
+
+/**
+ * Annotations are simple key-value pairs that are preserved across
+ * TextView save/restore cycles and can be used to keep application-specific
+ * data that needs to be maintained for regions of text.
+ */
+public class Annotation {
+ private String mKey;
+ private String mValue;
+
+ public Annotation(String key, String value) {
+ mKey = key;
+ mValue = value;
+ }
+
+ public String getKey() {
+ return mKey;
+ }
+
+ public String getValue() {
+ return mValue;
+ }
+}
diff --git a/core/java/android/text/AutoText.java b/core/java/android/text/AutoText.java
new file mode 100644
index 0000000..508d740
--- /dev/null
+++ b/core/java/android/text/AutoText.java
@@ -0,0 +1,246 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text;
+
+import android.content.res.Resources;
+import android.content.res.XmlResourceParser;
+import com.android.internal.util.XmlUtils;
+import android.view.View;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.Locale;
+
+/**
+ * This class accesses a dictionary of corrections to frequent misspellings.
+ */
+public class AutoText {
+ // struct trie {
+ // char c;
+ // int off;
+ // struct trie *child;
+ // struct trie *next;
+ // };
+
+ private static final int TRIE_C = 0;
+ private static final int TRIE_OFF = 1;
+ private static final int TRIE_CHILD = 2;
+ private static final int TRIE_NEXT = 3;
+
+ private static final int TRIE_SIZEOF = 4;
+ private static final char TRIE_NULL = (char) -1;
+ private static final int TRIE_ROOT = 0;
+
+ private static final int INCREMENT = 1024;
+
+ private static final int DEFAULT = 14337; // Size of the Trie 13 Aug 2007
+
+ private static final int RIGHT = 9300; // Size of 'right' 13 Aug 2007
+
+ private static AutoText sInstance = new AutoText(Resources.getSystem());
+ private static Object sLock = new Object();
+
+ // TODO:
+ //
+ // Note the assumption that the destination strings total less than
+ // 64K characters and that the trie for the source side totals less
+ // than 64K chars/offsets/child pointers/next pointers.
+ //
+ // This seems very safe for English (currently 7K of destination,
+ // 14K of trie) but may need to be revisited.
+
+ private char[] mTrie;
+ private char mTrieUsed;
+ private String mText;
+ private Locale mLocale;
+
+ private AutoText(Resources resources) {
+ mLocale = resources.getConfiguration().locale;
+ init(resources);
+ }
+
+ /**
+ * Retrieves a possible spelling correction for the specified range
+ * of text. Returns null if no correction can be found.
+ * The View is used to get the current Locale and Resources.
+ */
+ public static String get(CharSequence src, final int start, final int end,
+ View view) {
+ Resources res = view.getContext().getResources();
+ Locale locale = res.getConfiguration().locale;
+ AutoText instance;
+
+ synchronized (sLock) {
+ instance = sInstance;
+
+ if (!locale.equals(instance.mLocale)) {
+ instance = new AutoText(res);
+ sInstance = instance;
+ }
+ }
+
+ return instance.lookup(src, start, end);
+ }
+
+ private String lookup(CharSequence src, final int start, final int end) {
+ int here = mTrie[TRIE_ROOT];
+
+ for (int i = start; i < end; i++) {
+ char c = src.charAt(i);
+
+ for (; here != TRIE_NULL; here = mTrie[here + TRIE_NEXT]) {
+ if (c == mTrie[here + TRIE_C]) {
+ if ((i == end - 1)
+ && (mTrie[here + TRIE_OFF] != TRIE_NULL)) {
+ int off = mTrie[here + TRIE_OFF];
+ int len = mText.charAt(off);
+
+ return mText.substring(off + 1, off + 1 + len);
+ }
+
+ here = mTrie[here + TRIE_CHILD];
+ break;
+ }
+ }
+
+ if (here == TRIE_NULL) {
+ return null;
+ }
+ }
+
+ return null;
+ }
+
+ private void init(Resources r) {
+ XmlResourceParser parser = r.getXml(com.android.internal.R.xml.autotext);
+
+ StringBuilder right = new StringBuilder(RIGHT);
+ mTrie = new char[DEFAULT];
+ mTrie[TRIE_ROOT] = TRIE_NULL;
+ mTrieUsed = TRIE_ROOT + 1;
+
+ try {
+ XmlUtils.beginDocument(parser, "words");
+ String odest = "";
+ char ooff = 0;
+
+ while (true) {
+ XmlUtils.nextElement(parser);
+
+ String element = parser.getName();
+ if (element == null || !(element.equals("word"))) {
+ break;
+ }
+
+ String src = parser.getAttributeValue(null, "src");
+ if (parser.next() == XmlPullParser.TEXT) {
+ String dest = parser.getText();
+ char off;
+
+ if (dest.equals(odest)) {
+ off = ooff;
+ } else {
+ off = (char) right.length();
+ right.append((char) dest.length());
+ right.append(dest);
+ }
+
+ add(src, off);
+ }
+ }
+
+ // Don't let Resources cache a copy of all these strings.
+ r.flushLayoutCache();
+ } catch (XmlPullParserException e) {
+ throw new RuntimeException(e);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ } finally {
+ parser.close();
+ }
+
+ mText = right.toString();
+ }
+
+ private void add(String src, char off) {
+ int slen = src.length();
+ int herep = TRIE_ROOT;
+
+ for (int i = 0; i < slen; i++) {
+ char c = src.charAt(i);
+ boolean found = false;
+
+ for (; mTrie[herep] != TRIE_NULL;
+ herep = mTrie[herep] + TRIE_NEXT) {
+ if (c == mTrie[mTrie[herep] + TRIE_C]) {
+ // There is a node for this letter, and this is the
+ // end, so fill in the right hand side fields.
+
+ if (i == slen - 1) {
+ mTrie[mTrie[herep] + TRIE_OFF] = off;
+ return;
+ }
+
+ // There is a node for this letter, and we need
+ // to go deeper into it to fill in the rest.
+
+ herep = mTrie[herep] + TRIE_CHILD;
+ found = true;
+ break;
+ }
+ }
+
+ if (!found) {
+ // No node for this letter yet. Make one.
+
+ char node = newTrieNode();
+ mTrie[herep] = node;
+
+ mTrie[mTrie[herep] + TRIE_C] = c;
+ mTrie[mTrie[herep] + TRIE_OFF] = TRIE_NULL;
+ mTrie[mTrie[herep] + TRIE_NEXT] = TRIE_NULL;
+ mTrie[mTrie[herep] + TRIE_CHILD] = TRIE_NULL;
+
+ // If this is the end of the word, fill in the offset.
+
+ if (i == slen - 1) {
+ mTrie[mTrie[herep] + TRIE_OFF] = off;
+ return;
+ }
+
+ // Otherwise, step in deeper and go to the next letter.
+
+ herep = mTrie[herep] + TRIE_CHILD;
+ }
+ }
+ }
+
+ private char newTrieNode() {
+ if (mTrieUsed + TRIE_SIZEOF > mTrie.length) {
+ char[] copy = new char[mTrie.length + INCREMENT];
+ System.arraycopy(mTrie, 0, copy, 0, mTrie.length);
+ mTrie = copy;
+ }
+
+ char ret = mTrieUsed;
+ mTrieUsed += TRIE_SIZEOF;
+
+ return ret;
+ }
+}
diff --git a/core/java/android/text/BoringLayout.java b/core/java/android/text/BoringLayout.java
new file mode 100644
index 0000000..2ee4f62
--- /dev/null
+++ b/core/java/android/text/BoringLayout.java
@@ -0,0 +1,388 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.util.FloatMath;
+
+/**
+ * A BoringLayout is a very simple Layout implementation for text that
+ * fits on a single line and is all left-to-right characters.
+ * You will probably never want to make one of these yourself;
+ * if you do, be sure to call {@link #isBoring} first to make sure
+ * the text meets the criteria.
+ * <p>This class is used by widgets to control text layout. You should not need
+ * to use this class directly unless you are implementing your own widget
+ * or custom display object, in which case
+ * you are encouraged to use a Layout instead of calling
+ * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, float, float, android.graphics.Paint)
+ * Canvas.drawText()} directly.</p>
+ */
+public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback {
+ public static BoringLayout make(CharSequence source,
+ TextPaint paint, int outerwidth,
+ Alignment align,
+ float spacingmult, float spacingadd,
+ BoringLayout.Metrics metrics, boolean includepad) {
+ return new BoringLayout(source, paint, outerwidth, align,
+ spacingmult, spacingadd, metrics,
+ includepad);
+ }
+
+ public static BoringLayout make(CharSequence source,
+ TextPaint paint, int outerwidth,
+ Alignment align,
+ float spacingmult, float spacingadd,
+ BoringLayout.Metrics metrics, boolean includepad,
+ TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
+ return new BoringLayout(source, paint, outerwidth, align,
+ spacingmult, spacingadd, metrics,
+ includepad, ellipsize, ellipsizedWidth);
+ }
+
+ /**
+ * Returns a BoringLayout for the specified text, potentially reusing
+ * this one if it is already suitable. The caller must make sure that
+ * no one is still using this Layout.
+ */
+ public BoringLayout replaceOrMake(CharSequence source, TextPaint paint,
+ int outerwidth, Alignment align,
+ float spacingmult, float spacingadd,
+ BoringLayout.Metrics metrics,
+ boolean includepad) {
+ replaceWith(source, paint, outerwidth, align, spacingmult,
+ spacingadd);
+
+ mEllipsizedWidth = outerwidth;
+ mEllipsizedStart = 0;
+ mEllipsizedCount = 0;
+
+ init(source, paint, outerwidth, align, spacingmult, spacingadd,
+ metrics, includepad, true);
+ return this;
+ }
+
+ /**
+ * Returns a BoringLayout for the specified text, potentially reusing
+ * this one if it is already suitable. The caller must make sure that
+ * no one is still using this Layout.
+ */
+ public BoringLayout replaceOrMake(CharSequence source, TextPaint paint,
+ int outerwidth, Alignment align,
+ float spacingmult, float spacingadd,
+ BoringLayout.Metrics metrics,
+ boolean includepad,
+ TextUtils.TruncateAt ellipsize,
+ int ellipsizedWidth) {
+ boolean trust;
+
+ if (ellipsize == null) {
+ replaceWith(source, paint, outerwidth, align, spacingmult,
+ spacingadd);
+
+ mEllipsizedWidth = outerwidth;
+ mEllipsizedStart = 0;
+ mEllipsizedCount = 0;
+ trust = true;
+ } else {
+ replaceWith(TextUtils.ellipsize(source, paint, ellipsizedWidth,
+ ellipsize, true, this),
+ paint, outerwidth, align, spacingmult,
+ spacingadd);
+
+ mEllipsizedWidth = ellipsizedWidth;
+ trust = false;
+ }
+
+ init(getText(), paint, outerwidth, align, spacingmult, spacingadd,
+ metrics, includepad, trust);
+ return this;
+ }
+
+ public BoringLayout(CharSequence source,
+ TextPaint paint, int outerwidth,
+ Alignment align,
+ float spacingmult, float spacingadd,
+ BoringLayout.Metrics metrics, boolean includepad) {
+ super(source, paint, outerwidth, align, spacingmult, spacingadd);
+
+ mEllipsizedWidth = outerwidth;
+ mEllipsizedStart = 0;
+ mEllipsizedCount = 0;
+
+ init(source, paint, outerwidth, align, spacingmult, spacingadd,
+ metrics, includepad, true);
+ }
+
+ public BoringLayout(CharSequence source,
+ TextPaint paint, int outerwidth,
+ Alignment align,
+ float spacingmult, float spacingadd,
+ BoringLayout.Metrics metrics, boolean includepad,
+ TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
+ /*
+ * It is silly to have to call super() and then replaceWith(),
+ * but we can't use "this" for the callback until the call to
+ * super() finishes.
+ */
+ super(source, paint, outerwidth, align, spacingmult, spacingadd);
+
+ boolean trust;
+
+ if (ellipsize == null) {
+ mEllipsizedWidth = outerwidth;
+ mEllipsizedStart = 0;
+ mEllipsizedCount = 0;
+ trust = true;
+ } else {
+ replaceWith(TextUtils.ellipsize(source, paint, ellipsizedWidth,
+ ellipsize, true, this),
+ paint, outerwidth, align, spacingmult,
+ spacingadd);
+
+
+ mEllipsizedWidth = ellipsizedWidth;
+ trust = false;
+ }
+
+ init(getText(), paint, outerwidth, align, spacingmult, spacingadd,
+ metrics, includepad, trust);
+ }
+
+ /* package */ void init(CharSequence source,
+ TextPaint paint, int outerwidth,
+ Alignment align,
+ float spacingmult, float spacingadd,
+ BoringLayout.Metrics metrics, boolean includepad,
+ boolean trustWidth) {
+ int spacing;
+
+ if (source instanceof String && align == Layout.Alignment.ALIGN_NORMAL) {
+ mDirect = source.toString();
+ } else {
+ mDirect = null;
+ }
+
+ mPaint = paint;
+
+ if (includepad) {
+ spacing = metrics.bottom - metrics.top;
+ } else {
+ spacing = metrics.descent - metrics.ascent;
+ }
+
+ if (spacingmult != 1 || spacingadd != 0) {
+ spacing = (int)(spacing * spacingmult + spacingadd + 0.5f);
+ }
+
+ mBottom = spacing;
+
+ if (includepad) {
+ mDesc = spacing + metrics.top;
+ } else {
+ mDesc = spacing + metrics.ascent;
+ }
+
+ if (trustWidth) {
+ mMax = metrics.width;
+ } else {
+ /*
+ * If we have ellipsized, we have to actually calculate the
+ * width because the width that was passed in was for the
+ * full text, not the ellipsized form.
+ */
+ synchronized (sTemp) {
+ mMax = (int) (FloatMath.ceil(Styled.measureText(paint, sTemp,
+ source, 0, source.length(),
+ null)));
+ }
+ }
+
+ if (includepad) {
+ mTopPadding = metrics.top - metrics.ascent;
+ mBottomPadding = metrics.bottom - metrics.descent;
+ }
+ }
+
+ /**
+ * Returns null if not boring; the width, ascent, and descent if boring.
+ */
+ public static Metrics isBoring(CharSequence text,
+ TextPaint paint) {
+ return isBoring(text, paint, null);
+ }
+
+ /**
+ * Returns null if not boring; the width, ascent, and descent in the
+ * provided Metrics object (or a new one if the provided one was null)
+ * if boring.
+ */
+ public static Metrics isBoring(CharSequence text, TextPaint paint,
+ Metrics metrics) {
+ char[] temp = TextUtils.obtain(500);
+ int len = text.length();
+ boolean boring = true;
+
+ outer:
+ for (int i = 0; i < len; i += 500) {
+ int j = i + 500;
+
+ if (j > len)
+ j = len;
+
+ TextUtils.getChars(text, i, j, temp, 0);
+
+ int n = j - i;
+
+ for (int a = 0; a < n; a++) {
+ char c = temp[a];
+
+ if (c == '\n' || c == '\t' || c >= FIRST_RIGHT_TO_LEFT) {
+ boring = false;
+ break outer;
+ }
+ }
+ }
+
+ TextUtils.recycle(temp);
+
+ if (boring) {
+ Metrics fm = metrics;
+ if (fm == null) {
+ fm = new Metrics();
+ }
+
+ int wid;
+
+ synchronized (sTemp) {
+ wid = (int) (FloatMath.ceil(Styled.measureText(paint, sTemp,
+ text, 0, text.length(), fm)));
+ }
+ fm.width = wid;
+ return fm;
+ } else {
+ return null;
+ }
+ }
+
+ @Override public int getHeight() {
+ return mBottom;
+ }
+
+ @Override public int getLineCount() {
+ return 1;
+ }
+
+ @Override public int getLineTop(int line) {
+ if (line == 0)
+ return 0;
+ else
+ return mBottom;
+ }
+
+ @Override public int getLineDescent(int line) {
+ return mDesc;
+ }
+
+ @Override public int getLineStart(int line) {
+ if (line == 0)
+ return 0;
+ else
+ return getText().length();
+ }
+
+ @Override public int getParagraphDirection(int line) {
+ return DIR_LEFT_TO_RIGHT;
+ }
+
+ @Override public boolean getLineContainsTab(int line) {
+ return false;
+ }
+
+ @Override public float getLineMax(int line) {
+ return mMax;
+ }
+
+ @Override public final Directions getLineDirections(int line) {
+ return Layout.DIRS_ALL_LEFT_TO_RIGHT;
+ }
+
+ public int getTopPadding() {
+ return mTopPadding;
+ }
+
+ public int getBottomPadding() {
+ return mBottomPadding;
+ }
+
+ @Override
+ public int getEllipsisCount(int line) {
+ return mEllipsizedCount;
+ }
+
+ @Override
+ public int getEllipsisStart(int line) {
+ return mEllipsizedStart;
+ }
+
+ @Override
+ public int getEllipsizedWidth() {
+ return mEllipsizedWidth;
+ }
+
+ // Override draw so it will be faster.
+ @Override
+ public void draw(Canvas c, Path highlight, Paint highlightpaint,
+ int cursorOffset) {
+ if (mDirect != null && highlight == null) {
+ c.drawText(mDirect, 0, mBottom - mDesc, mPaint);
+ } else {
+ super.draw(c, highlight, highlightpaint, cursorOffset);
+ }
+ }
+
+ /**
+ * Callback for the ellipsizer to report what region it ellipsized.
+ */
+ public void ellipsized(int start, int end) {
+ mEllipsizedStart = start;
+ mEllipsizedCount = end - start;
+ }
+
+ private static final char FIRST_RIGHT_TO_LEFT = '\u0590';
+
+ private String mDirect;
+ private Paint mPaint;
+
+ /* package */ int mBottom, mDesc; // for Direct
+ private int mTopPadding, mBottomPadding;
+ private float mMax;
+ private int mEllipsizedWidth, mEllipsizedStart, mEllipsizedCount;
+
+ private static final TextPaint sTemp =
+ new TextPaint();
+
+ public static class Metrics extends Paint.FontMetricsInt {
+ public int width;
+
+ @Override public String toString() {
+ return super.toString() + " width=" + width;
+ }
+ }
+}
diff --git a/core/java/android/text/ClipboardManager.java b/core/java/android/text/ClipboardManager.java
new file mode 100644
index 0000000..52039af
--- /dev/null
+++ b/core/java/android/text/ClipboardManager.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text;
+
+import android.content.Context;
+import android.os.RemoteException;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.ServiceManager;
+import android.util.Log;
+
+/**
+ * Interface to the clipboard service, for placing and retrieving text in
+ * the global clipboard.
+ *
+ * <p>
+ * You do not instantiate this class directly; instead, retrieve it through
+ * {@link android.content.Context#getSystemService}.
+ *
+ * @see android.content.Context#getSystemService
+ */
+public class ClipboardManager {
+ private static IClipboard sService;
+
+ private Context mContext;
+
+ static private IClipboard getService() {
+ if (sService != null) {
+ return sService;
+ }
+ IBinder b = ServiceManager.getService("clipboard");
+ sService = IClipboard.Stub.asInterface(b);
+ return sService;
+ }
+
+ /** {@hide} */
+ public ClipboardManager(Context context, Handler handler) {
+ mContext = context;
+ }
+
+ /**
+ * Returns the text on the clipboard. It will eventually be possible
+ * to store types other than text too, in which case this will return
+ * null if the type cannot be coerced to text.
+ */
+ public CharSequence getText() {
+ try {
+ return getService().getClipboardText();
+ } catch (RemoteException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Sets the contents of the clipboard to the specified text.
+ */
+ public void setText(CharSequence text) {
+ try {
+ getService().setClipboardText(text);
+ } catch (RemoteException e) {
+ }
+ }
+
+ /**
+ * Returns true if the clipboard contains text; false otherwise.
+ */
+ public boolean hasText() {
+ try {
+ return getService().hasClipboardText();
+ } catch (RemoteException e) {
+ return false;
+ }
+ }
+}
diff --git a/core/java/android/text/DynamicLayout.java b/core/java/android/text/DynamicLayout.java
new file mode 100644
index 0000000..14e5655
--- /dev/null
+++ b/core/java/android/text/DynamicLayout.java
@@ -0,0 +1,503 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text;
+
+import android.graphics.Paint;
+import android.text.style.UpdateLayout;
+import android.text.style.WrapTogetherSpan;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * DynamicLayout is a text layout that updates itself as the text is edited.
+ * <p>This is used by widgets to control text layout. You should not need
+ * to use this class directly unless you are implementing your own widget
+ * or custom display object, or need to call
+ * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, float, float, android.graphics.Paint)
+ * Canvas.drawText()} directly.</p>
+ */
+public class DynamicLayout
+extends Layout
+{
+ private static final int PRIORITY = 128;
+
+ /**
+ * Make a layout for the specified text that will be updated as
+ * the text is changed.
+ */
+ public DynamicLayout(CharSequence base,
+ TextPaint paint,
+ int width, Alignment align,
+ float spacingmult, float spacingadd,
+ boolean includepad) {
+ this(base, base, paint, width, align, spacingmult, spacingadd,
+ includepad);
+ }
+
+ /**
+ * Make a layout for the transformed text (password transformation
+ * being the primary example of a transformation)
+ * that will be updated as the base text is changed.
+ */
+ public DynamicLayout(CharSequence base, CharSequence display,
+ TextPaint paint,
+ int width, Alignment align,
+ float spacingmult, float spacingadd,
+ boolean includepad) {
+ this(base, display, paint, width, align, spacingmult, spacingadd,
+ includepad, null, 0);
+ }
+
+ /**
+ * Make a layout for the transformed text (password transformation
+ * being the primary example of a transformation)
+ * that will be updated as the base text is changed.
+ * If ellipsize is non-null, the Layout will ellipsize the text
+ * down to ellipsizedWidth.
+ */
+ public DynamicLayout(CharSequence base, CharSequence display,
+ TextPaint paint,
+ int width, Alignment align,
+ float spacingmult, float spacingadd,
+ boolean includepad,
+ TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
+ super((ellipsize == null)
+ ? display
+ : (display instanceof Spanned)
+ ? new SpannedEllipsizer(display)
+ : new Ellipsizer(display),
+ paint, width, align, spacingmult, spacingadd);
+
+ mBase = base;
+ mDisplay = display;
+
+ if (ellipsize != null) {
+ mInts = new PackedIntVector(COLUMNS_ELLIPSIZE);
+ mEllipsizedWidth = ellipsizedWidth;
+ mEllipsizeAt = ellipsize;
+ } else {
+ mInts = new PackedIntVector(COLUMNS_NORMAL);
+ mEllipsizedWidth = width;
+ mEllipsizeAt = ellipsize;
+ }
+
+ mObjects = new PackedObjectVector<Directions>(1);
+
+ mIncludePad = includepad;
+
+ /*
+ * This is annoying, but we can't refer to the layout until
+ * superclass construction is finished, and the superclass
+ * constructor wants the reference to the display text.
+ *
+ * This will break if the superclass constructor ever actually
+ * cares about the content instead of just holding the reference.
+ */
+ if (ellipsize != null) {
+ Ellipsizer e = (Ellipsizer) getText();
+
+ e.mLayout = this;
+ e.mWidth = ellipsizedWidth;
+ e.mMethod = ellipsize;
+ mEllipsize = true;
+ }
+
+ // Initial state is a single line with 0 characters (0 to 0),
+ // with top at 0 and bottom at whatever is natural, and
+ // undefined ellipsis.
+
+ int[] start;
+
+ if (ellipsize != null) {
+ start = new int[COLUMNS_ELLIPSIZE];
+ start[ELLIPSIS_START] = ELLIPSIS_UNDEFINED;
+ } else {
+ start = new int[COLUMNS_NORMAL];
+ }
+
+ Directions[] dirs = new Directions[] { DIRS_ALL_LEFT_TO_RIGHT };
+
+ Paint.FontMetricsInt fm = paint.getFontMetricsInt();
+ int asc = fm.ascent;
+ int desc = fm.descent;
+
+ start[DIR] = DIR_LEFT_TO_RIGHT << DIR_SHIFT;
+ start[TOP] = 0;
+ start[DESCENT] = desc;
+ mInts.insertAt(0, start);
+
+ start[TOP] = desc - asc;
+ mInts.insertAt(1, start);
+
+ mObjects.insertAt(0, dirs);
+
+ // Update from 0 characters to whatever the real text is
+
+ reflow(base, 0, 0, base.length());
+
+ if (base instanceof Spannable) {
+ if (mWatcher == null)
+ mWatcher = new ChangeWatcher(this);
+
+ // Strip out any watchers for other DynamicLayouts.
+ Spannable sp = (Spannable) base;
+ ChangeWatcher[] spans = sp.getSpans(0, sp.length(), ChangeWatcher.class);
+ for (int i = 0; i < spans.length; i++)
+ sp.removeSpan(spans[i]);
+
+ sp.setSpan(mWatcher, 0, base.length(),
+ Spannable.SPAN_INCLUSIVE_INCLUSIVE |
+ (PRIORITY << Spannable.SPAN_PRIORITY_SHIFT));
+ }
+ }
+
+ private void reflow(CharSequence s, int where, int before, int after) {
+ if (s != mBase)
+ return;
+
+ CharSequence text = mDisplay;
+ int len = text.length();
+
+ // seek back to the start of the paragraph
+
+ int find = TextUtils.lastIndexOf(text, '\n', where - 1);
+ if (find < 0)
+ find = 0;
+ else
+ find = find + 1;
+
+ {
+ int diff = where - find;
+ before += diff;
+ after += diff;
+ where -= diff;
+ }
+
+ // seek forward to the end of the paragraph
+
+ int look = TextUtils.indexOf(text, '\n', where + after);
+ if (look < 0)
+ look = len;
+ else
+ look++; // we want the index after the \n
+
+ int change = look - (where + after);
+ before += change;
+ after += change;
+
+ // seek further out to cover anything that is forced to wrap together
+
+ if (text instanceof Spanned) {
+ Spanned sp = (Spanned) text;
+ boolean again;
+
+ do {
+ again = false;
+
+ Object[] force = sp.getSpans(where, where + after,
+ WrapTogetherSpan.class);
+
+ for (int i = 0; i < force.length; i++) {
+ int st = sp.getSpanStart(force[i]);
+ int en = sp.getSpanEnd(force[i]);
+
+ if (st < where) {
+ again = true;
+
+ int diff = where - st;
+ before += diff;
+ after += diff;
+ where -= diff;
+ }
+
+ if (en > where + after) {
+ again = true;
+
+ int diff = en - (where + after);
+ before += diff;
+ after += diff;
+ }
+ }
+ } while (again);
+ }
+
+ // find affected region of old layout
+
+ int startline = getLineForOffset(where);
+ int startv = getLineTop(startline);
+
+ int endline = getLineForOffset(where + before);
+ if (where + after == len)
+ endline = getLineCount();
+ int endv = getLineTop(endline);
+ boolean islast = (endline == getLineCount());
+
+ // generate new layout for affected text
+
+ StaticLayout reflowed;
+
+ synchronized (sLock) {
+ reflowed = sStaticLayout;
+ sStaticLayout = null;
+ }
+
+ if (reflowed == null)
+ reflowed = new StaticLayout(true);
+
+ reflowed.generate(text, where, where + after,
+ getPaint(), getWidth(), getAlignment(),
+ getSpacingMultiplier(), getSpacingAdd(),
+ false, true, mEllipsize,
+ mEllipsizedWidth, mEllipsizeAt);
+ int n = reflowed.getLineCount();
+
+ // If the new layout has a blank line at the end, but it is not
+ // the very end of the buffer, then we already have a line that
+ // starts there, so disregard the blank line.
+
+ if (where + after != len &&
+ reflowed.getLineStart(n - 1) == where + after)
+ n--;
+
+ // remove affected lines from old layout
+
+ mInts.deleteAt(startline, endline - startline);
+ mObjects.deleteAt(startline, endline - startline);
+
+ // adjust offsets in layout for new height and offsets
+
+ int ht = reflowed.getLineTop(n);
+ int toppad = 0, botpad = 0;
+
+ if (mIncludePad && startline == 0) {
+ toppad = reflowed.getTopPadding();
+ mTopPadding = toppad;
+ ht -= toppad;
+ }
+ if (mIncludePad && islast) {
+ botpad = reflowed.getBottomPadding();
+ mBottomPadding = botpad;
+ ht += botpad;
+ }
+
+ mInts.adjustValuesBelow(startline, START, after - before);
+ mInts.adjustValuesBelow(startline, TOP, startv - endv + ht);
+
+ // insert new layout
+
+ int[] ints;
+
+ if (mEllipsize) {
+ ints = new int[COLUMNS_ELLIPSIZE];
+ ints[ELLIPSIS_START] = ELLIPSIS_UNDEFINED;
+ } else {
+ ints = new int[COLUMNS_NORMAL];
+ }
+
+ Directions[] objects = new Directions[1];
+
+
+ for (int i = 0; i < n; i++) {
+ ints[START] = reflowed.getLineStart(i) |
+ (reflowed.getParagraphDirection(i) << DIR_SHIFT) |
+ (reflowed.getLineContainsTab(i) ? TAB_MASK : 0);
+
+ int top = reflowed.getLineTop(i) + startv;
+ if (i > 0)
+ top -= toppad;
+ ints[TOP] = top;
+
+ int desc = reflowed.getLineDescent(i);
+ if (i == n - 1)
+ desc += botpad;
+
+ ints[DESCENT] = desc;
+ objects[0] = reflowed.getLineDirections(i);
+
+ if (mEllipsize) {
+ ints[ELLIPSIS_START] = reflowed.getEllipsisStart(i);
+ ints[ELLIPSIS_COUNT] = reflowed.getEllipsisCount(i);
+ }
+
+ mInts.insertAt(startline + i, ints);
+ mObjects.insertAt(startline + i, objects);
+ }
+
+ synchronized (sLock) {
+ sStaticLayout = reflowed;
+ }
+ }
+
+ private void dump(boolean show) {
+ int n = getLineCount();
+
+ for (int i = 0; i < n; i++) {
+ System.out.print("line " + i + ": " + getLineStart(i) + " to " + getLineEnd(i) + " ");
+
+ if (show) {
+ System.out.print(getText().subSequence(getLineStart(i),
+ getLineEnd(i)));
+ }
+
+ System.out.println("");
+ }
+
+ System.out.println("");
+ }
+
+ public int getLineCount() {
+ return mInts.size() - 1;
+ }
+
+ public int getLineTop(int line) {
+ return mInts.getValue(line, TOP);
+ }
+
+ public int getLineDescent(int line) {
+ return mInts.getValue(line, DESCENT);
+ }
+
+ public int getLineStart(int line) {
+ return mInts.getValue(line, START) & START_MASK;
+ }
+
+ public boolean getLineContainsTab(int line) {
+ return (mInts.getValue(line, TAB) & TAB_MASK) != 0;
+ }
+
+ public int getParagraphDirection(int line) {
+ return mInts.getValue(line, DIR) >> DIR_SHIFT;
+ }
+
+ public final Directions getLineDirections(int line) {
+ return mObjects.getValue(line, 0);
+ }
+
+ public int getTopPadding() {
+ return mTopPadding;
+ }
+
+ public int getBottomPadding() {
+ return mBottomPadding;
+ }
+
+ @Override
+ public int getEllipsizedWidth() {
+ return mEllipsizedWidth;
+ }
+
+ private static class ChangeWatcher
+ implements TextWatcher, SpanWatcher
+ {
+ public ChangeWatcher(DynamicLayout layout) {
+ mLayout = new WeakReference(layout);
+ }
+
+ private void reflow(CharSequence s, int where, int before, int after) {
+ DynamicLayout ml = (DynamicLayout) mLayout.get();
+
+ if (ml != null)
+ ml.reflow(s, where, before, after);
+ else if (s instanceof Spannable)
+ ((Spannable) s).removeSpan(this);
+ }
+
+ public void beforeTextChanged(CharSequence s,
+ int where, int before, int after) {
+ ;
+ }
+
+ public void onTextChanged(CharSequence s,
+ int where, int before, int after) {
+ reflow(s, where, before, after);
+ }
+
+ public void afterTextChanged(Editable s) {
+ ;
+ }
+
+ public void onSpanAdded(Spannable s, Object o, int start, int end) {
+ if (o instanceof UpdateLayout)
+ reflow(s, start, end - start, end - start);
+ }
+
+ public void onSpanRemoved(Spannable s, Object o, int start, int end) {
+ if (o instanceof UpdateLayout)
+ reflow(s, start, end - start, end - start);
+ }
+
+ public void onSpanChanged(Spannable s, Object o, int start, int end,
+ int nstart, int nend) {
+ if (o instanceof UpdateLayout) {
+ reflow(s, start, end - start, end - start);
+ reflow(s, nstart, nend - nstart, nend - nstart);
+ }
+ }
+
+ private WeakReference mLayout;
+ }
+
+ public int getEllipsisStart(int line) {
+ if (mEllipsizeAt == null) {
+ return 0;
+ }
+
+ return mInts.getValue(line, ELLIPSIS_START);
+ }
+
+ public int getEllipsisCount(int line) {
+ if (mEllipsizeAt == null) {
+ return 0;
+ }
+
+ return mInts.getValue(line, ELLIPSIS_COUNT);
+ }
+
+ private CharSequence mBase;
+ private CharSequence mDisplay;
+ private ChangeWatcher mWatcher;
+ private boolean mIncludePad;
+ private boolean mEllipsize;
+ private int mEllipsizedWidth;
+ private TextUtils.TruncateAt mEllipsizeAt;
+
+ private PackedIntVector mInts;
+ private PackedObjectVector<Directions> mObjects;
+
+ private int mTopPadding, mBottomPadding;
+
+ private static StaticLayout sStaticLayout = new StaticLayout(true);
+ private static Object sLock = new Object();
+
+ private static final int START = 0;
+ private static final int DIR = START;
+ private static final int TAB = START;
+ private static final int TOP = 1;
+ private static final int DESCENT = 2;
+ private static final int COLUMNS_NORMAL = 3;
+
+ private static final int ELLIPSIS_START = 3;
+ private static final int ELLIPSIS_COUNT = 4;
+ private static final int COLUMNS_ELLIPSIZE = 5;
+
+ private static final int START_MASK = 0x1FFFFFFF;
+ private static final int DIR_MASK = 0xC0000000;
+ private static final int DIR_SHIFT = 30;
+ private static final int TAB_MASK = 0x20000000;
+
+ private static final int ELLIPSIS_UNDEFINED = 0x80000000;
+}
diff --git a/core/java/android/text/Editable.java b/core/java/android/text/Editable.java
new file mode 100644
index 0000000..a284a00
--- /dev/null
+++ b/core/java/android/text/Editable.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text;
+
+/**
+ * This is the interface for text whose content and markup
+ * can be changed (as opposed
+ * to immutable text like Strings). If you make a {@link DynamicLayout}
+ * of an Editable, the layout will be reflowed as the text is changed.
+ */
+public interface Editable
+extends CharSequence, GetChars, Spannable, Appendable
+{
+ /**
+ * Replaces the specified range (<code>st&hellip;en</code>) of text in this
+ * Editable with a copy of the slice <code>start&hellip;end</code> from
+ * <code>source</code>. The destination slice may be empty, in which case
+ * the operation is an insertion, or the source slice may be empty,
+ * in which case the operation is a deletion.
+ * <p>
+ * Before the change is committed, each filter that was set with
+ * {@link #setFilters} is given the opportunity to modify the
+ * <code>source</code> text.
+ * <p>
+ * If <code>source</code>
+ * is Spanned, the spans from it are preserved into the Editable.
+ * Existing spans within the Editable that entirely cover the replaced
+ * range are retained, but any that were strictly within the range
+ * that was replaced are removed. As a special case, the cursor
+ * position is preserved even when the entire range where it is
+ * located is replaced.
+ * @return a reference to this object.
+ */
+ public Editable replace(int st, int en, CharSequence source, int start, int end);
+
+ /**
+ * Convenience for replace(st, en, text, 0, text.length())
+ * @see #replace(int, int, CharSequence, int, int)
+ */
+ public Editable replace(int st, int en, CharSequence text);
+
+ /**
+ * Convenience for replace(where, where, text, start, end)
+ * @see #replace(int, int, CharSequence, int, int)
+ */
+ public Editable insert(int where, CharSequence text, int start, int end);
+
+ /**
+ * Convenience for replace(where, where, text, 0, text.length());
+ * @see #replace(int, int, CharSequence, int, int)
+ */
+ public Editable insert(int where, CharSequence text);
+
+ /**
+ * Convenience for replace(st, en, "", 0, 0)
+ * @see #replace(int, int, CharSequence, int, int)
+ */
+ public Editable delete(int st, int en);
+
+ /**
+ * Convenience for replace(length(), length(), text, 0, text.length())
+ * @see #replace(int, int, CharSequence, int, int)
+ */
+ public Editable append(CharSequence text);
+
+ /**
+ * Convenience for replace(length(), length(), text, start, end)
+ * @see #replace(int, int, CharSequence, int, int)
+ */
+ public Editable append(CharSequence text, int start, int end);
+
+ /**
+ * Convenience for append(String.valueOf(text)).
+ * @see #replace(int, int, CharSequence, int, int)
+ */
+ public Editable append(char text);
+
+ /**
+ * Convenience for replace(0, length(), "", 0, 0)
+ * @see #replace(int, int, CharSequence, int, int)
+ * Note that this clears the text, not the spans;
+ * use {@link #clearSpans} if you need that.
+ */
+ public void clear();
+
+ /**
+ * Removes all spans from the Editable, as if by calling
+ * {@link #removeSpan} on each of them.
+ */
+ public void clearSpans();
+
+ /**
+ * Sets the series of filters that will be called in succession
+ * whenever the text of this Editable is changed, each of which has
+ * the opportunity to limit or transform the text that is being inserted.
+ */
+ public void setFilters(InputFilter[] filters);
+
+ /**
+ * Returns the array of input filters that are currently applied
+ * to changes to this Editable.
+ */
+ public InputFilter[] getFilters();
+
+ /**
+ * Factory used by TextView to create new Editables. You can subclass
+ * it to provide something other than SpannableStringBuilder.
+ */
+ public static class Factory {
+ private static Editable.Factory sInstance = new Editable.Factory();
+
+ /**
+ * Returns the standard Editable Factory.
+ */
+ public static Editable.Factory getInstance() {
+ return sInstance;
+ }
+
+ /**
+ * Returns a new SpannedStringBuilder from the specified
+ * CharSequence. You can override this to provide
+ * a different kind of Spanned.
+ */
+ public Editable newEditable(CharSequence source) {
+ return new SpannableStringBuilder(source);
+ }
+ }
+}
diff --git a/core/java/android/text/GetChars.java b/core/java/android/text/GetChars.java
new file mode 100644
index 0000000..348a911
--- /dev/null
+++ b/core/java/android/text/GetChars.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text;
+
+/**
+ * Please implement this interface if your CharSequence has a
+ * getChars() method like the one in String that is faster than
+ * calling charAt() multiple times.
+ */
+public interface GetChars
+extends CharSequence
+{
+ /**
+ * Exactly like String.getChars(): copy chars <code>start</code>
+ * through <code>end - 1</code> from this CharSequence into <code>dest</code>
+ * beginning at offset <code>destoff</code>.
+ */
+ public void getChars(int start, int end, char[] dest, int destoff);
+}
diff --git a/core/java/android/text/GraphicsOperations.java b/core/java/android/text/GraphicsOperations.java
new file mode 100644
index 0000000..c3bd0ae
--- /dev/null
+++ b/core/java/android/text/GraphicsOperations.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+
+/**
+ * Please implement this interface if your CharSequence can do quick
+ * draw/measure/widths calculations from an internal array.
+ * {@hide}
+ */
+public interface GraphicsOperations
+extends CharSequence
+{
+ /**
+ * Just like {@link Canvas#drawText}.
+ */
+ void drawText(Canvas c, int start, int end,
+ float x, float y, Paint p);
+
+ /**
+ * Just like {@link Paint#measureText}.
+ */
+ float measureText(int start, int end, Paint p);
+
+
+ /**
+ * Just like {@link Paint#getTextWidths}.
+ */
+ public int getTextWidths(int start, int end, float[] widths, Paint p);
+}
diff --git a/core/java/android/text/Html.java b/core/java/android/text/Html.java
new file mode 100644
index 0000000..90f5e4c
--- /dev/null
+++ b/core/java/android/text/Html.java
@@ -0,0 +1,750 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text;
+
+import org.ccil.cowan.tagsoup.HTMLSchema;
+import org.ccil.cowan.tagsoup.Parser;
+import org.xml.sax.Attributes;
+import org.xml.sax.ContentHandler;
+import org.xml.sax.InputSource;
+import org.xml.sax.Locator;
+import org.xml.sax.SAXException;
+import org.xml.sax.XMLReader;
+
+import android.content.res.Resources;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import android.text.style.CharacterStyle;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.ImageSpan;
+import android.text.style.ParagraphStyle;
+import android.text.style.QuoteSpan;
+import android.text.style.RelativeSizeSpan;
+import android.text.style.StrikethroughSpan;
+import android.text.style.StyleSpan;
+import android.text.style.SubscriptSpan;
+import android.text.style.SuperscriptSpan;
+import android.text.style.TypefaceSpan;
+import android.text.style.URLSpan;
+import android.text.style.UnderlineSpan;
+import com.android.internal.util.XmlUtils;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.nio.CharBuffer;
+
+/**
+ * This class processes HTML strings into displayable styled text.
+ * Not all HTML tags are supported.
+ */
+public class Html {
+ /**
+ * Retrieves images for HTML &lt;img&gt; tags.
+ */
+ public static interface ImageGetter {
+ /**
+ * This methos is called when the HTML parser encounters an
+ * &lt;img&gt; tag. The <code>source</code> argument is the
+ * string from the "src" attribute; the return value should be
+ * a Drawable representation of the image or <code>null</code>
+ * for a generic replacement image. Make sure you call
+ * setBounds() on your Drawable if it doesn't already have
+ * its bounds set.
+ */
+ public Drawable getDrawable(String source);
+ }
+
+ /**
+ * Is notified when HTML tags are encountered that the parser does
+ * not know how to interpret.
+ */
+ public static interface TagHandler {
+ /**
+ * This method will be called whenn the HTML parser encounters
+ * a tag that it does not know how to interpret.
+ */
+ public void handleTag(boolean opening, String tag,
+ Editable output, XMLReader xmlReader);
+ }
+
+ private Html() { }
+
+ /**
+ * Returns displayable styled text from the provided HTML string.
+ * Any &lt;img&gt; tags in the HTML will display as a generic
+ * replacement image which your program can then go through and
+ * replace with real images.
+ *
+ * <p>This uses TagSoup to handle real HTML, including all of the brokenness found in the wild.
+ */
+ public static Spanned fromHtml(String source) {
+ return fromHtml(source, null, null);
+ }
+
+ /**
+ * Lazy initialization holder for HTML parser. This class will
+ * a) be preloaded by the zygote, or b) not loaded until absolutely
+ * necessary.
+ */
+ private static class HtmlParser {
+ private static final HTMLSchema schema = new HTMLSchema();
+ }
+
+ /**
+ * Returns displayable styled text from the provided HTML string.
+ * Any &lt;img&gt; tags in the HTML will use the specified ImageGetter
+ * to request a representation of the image (use null if you don't
+ * want this) and the specified TagHandler to handle unknown tags
+ * (specify null if you don't want this).
+ *
+ * <p>This uses TagSoup to handle real HTML, including all of the brokenness found in the wild.
+ */
+ public static Spanned fromHtml(String source, ImageGetter imageGetter,
+ TagHandler tagHandler) {
+ Parser parser = new Parser();
+ try {
+ parser.setProperty(Parser.schemaProperty, HtmlParser.schema);
+ } catch (org.xml.sax.SAXNotRecognizedException e) {
+ // Should not happen.
+ throw new RuntimeException(e);
+ } catch (org.xml.sax.SAXNotSupportedException e) {
+ // Should not happen.
+ throw new RuntimeException(e);
+ }
+
+ HtmlToSpannedConverter converter =
+ new HtmlToSpannedConverter(source, imageGetter, tagHandler,
+ parser);
+ return converter.convert();
+ }
+
+ /**
+ * Returns an HTML representation of the provided Spanned text.
+ */
+ public static String toHtml(Spanned text) {
+ StringBuilder out = new StringBuilder();
+ int len = text.length();
+
+ int next;
+ for (int i = 0; i < text.length(); i = next) {
+ next = text.nextSpanTransition(i, len, QuoteSpan.class);
+ QuoteSpan[] quotes = text.getSpans(i, next, QuoteSpan.class);
+
+ for (QuoteSpan quote: quotes) {
+ out.append("<blockquote>");
+ }
+
+ withinBlockquote(out, text, i, next);
+
+ for (QuoteSpan quote: quotes) {
+ out.append("</blockquote>\n");
+ }
+ }
+
+ return out.toString();
+ }
+
+ private static void withinBlockquote(StringBuilder out, Spanned text,
+ int start, int end) {
+ out.append("<p>");
+
+ int next;
+ for (int i = start; i < end; i = next) {
+ next = TextUtils.indexOf(text, '\n', i, end);
+ if (next < 0) {
+ next = end;
+ }
+
+ int nl = 0;
+
+ while (next < end && text.charAt(next) == '\n') {
+ nl++;
+ next++;
+ }
+
+ withinParagraph(out, text, i, next - nl, nl, next == end);
+ }
+
+ out.append("</p>\n");
+ }
+
+ private static void withinParagraph(StringBuilder out, Spanned text,
+ int start, int end, int nl,
+ boolean last) {
+ int next;
+ for (int i = start; i < end; i = next) {
+ next = text.nextSpanTransition(i, end, CharacterStyle.class);
+ CharacterStyle[] style = text.getSpans(i, next,
+ CharacterStyle.class);
+
+ for (int j = 0; j < style.length; j++) {
+ if (style[j] instanceof StyleSpan) {
+ int s = ((StyleSpan) style[j]).getStyle();
+
+ if ((s & Typeface.BOLD) != 0) {
+ out.append("<b>");
+ }
+ if ((s & Typeface.ITALIC) != 0) {
+ out.append("<i>");
+ }
+ }
+ if (style[j] instanceof TypefaceSpan) {
+ String s = ((TypefaceSpan) style[j]).getFamily();
+
+ if (s.equals("monospace")) {
+ out.append("<tt>");
+ }
+ }
+ if (style[j] instanceof SuperscriptSpan) {
+ out.append("<sup>");
+ }
+ if (style[j] instanceof SubscriptSpan) {
+ out.append("<sub>");
+ }
+ if (style[j] instanceof UnderlineSpan) {
+ out.append("<u>");
+ }
+ if (style[j] instanceof StrikethroughSpan) {
+ out.append("<strike>");
+ }
+ if (style[j] instanceof URLSpan) {
+ out.append("<a href=\"");
+ out.append(((URLSpan) style[j]).getURL());
+ out.append("\">");
+ }
+ if (style[j] instanceof ImageSpan) {
+ out.append("<img src=\"");
+ out.append(((ImageSpan) style[j]).getSource());
+ out.append("\">");
+
+ // Don't output the dummy character underlying the image.
+ i = next;
+ }
+ }
+
+ withinStyle(out, text, i, next);
+
+ for (int j = style.length - 1; j >= 0; j--) {
+ if (style[j] instanceof URLSpan) {
+ out.append("</a>");
+ }
+ if (style[j] instanceof StrikethroughSpan) {
+ out.append("</strike>");
+ }
+ if (style[j] instanceof UnderlineSpan) {
+ out.append("</u>");
+ }
+ if (style[j] instanceof SubscriptSpan) {
+ out.append("</sub>");
+ }
+ if (style[j] instanceof SuperscriptSpan) {
+ out.append("</sup>");
+ }
+ if (style[j] instanceof TypefaceSpan) {
+ String s = ((TypefaceSpan) style[j]).getFamily();
+
+ if (s.equals("monospace")) {
+ out.append("</tt>");
+ }
+ }
+ if (style[j] instanceof StyleSpan) {
+ int s = ((StyleSpan) style[j]).getStyle();
+
+ if ((s & Typeface.BOLD) != 0) {
+ out.append("</b>");
+ }
+ if ((s & Typeface.ITALIC) != 0) {
+ out.append("</i>");
+ }
+ }
+ }
+ }
+
+ String p = last ? "" : "</p>\n<p>";
+
+ if (nl == 1) {
+ out.append("<br>\n");
+ } else if (nl == 2) {
+ out.append(p);
+ } else {
+ for (int i = 2; i < nl; i++) {
+ out.append("<br>");
+ }
+
+ out.append(p);
+ }
+ }
+
+ private static void withinStyle(StringBuilder out, Spanned text,
+ int start, int end) {
+ for (int i = start; i < end; i++) {
+ char c = text.charAt(i);
+
+ if (c == '<') {
+ out.append("&lt;");
+ } else if (c == '>') {
+ out.append("&gt;");
+ } else if (c == '&') {
+ out.append("&amp;");
+ } else if (c > 0x7E || c < ' ') {
+ out.append("&#" + ((int) c) + ";");
+ } else if (c == ' ') {
+ while (i + 1 < end && text.charAt(i + 1) == ' ') {
+ out.append("&nbsp;");
+ i++;
+ }
+
+ out.append(' ');
+ } else {
+ out.append(c);
+ }
+ }
+ }
+}
+
+class HtmlToSpannedConverter implements ContentHandler {
+
+ private static final float[] HEADER_SIZES = {
+ 1.5f, 1.4f, 1.3f, 1.2f, 1.1f, 1f,
+ };
+
+ private String mSource;
+ private XMLReader mReader;
+ private SpannableStringBuilder mSpannableStringBuilder;
+ private Html.ImageGetter mImageGetter;
+ private Html.TagHandler mTagHandler;
+
+ public HtmlToSpannedConverter(
+ String source, Html.ImageGetter imageGetter, Html.TagHandler tagHandler,
+ Parser parser) {
+ mSource = source;
+ mSpannableStringBuilder = new SpannableStringBuilder();
+ mImageGetter = imageGetter;
+ mTagHandler = tagHandler;
+ mReader = parser;
+ }
+
+ public Spanned convert() {
+
+ mReader.setContentHandler(this);
+ try {
+ mReader.parse(new InputSource(new StringReader(mSource)));
+ } catch (IOException e) {
+ // We are reading from a string. There should not be IO problems.
+ throw new RuntimeException(e);
+ } catch (SAXException e) {
+ // TagSoup doesn't throw parse exceptions.
+ throw new RuntimeException(e);
+ }
+
+ // Fix flags and range for paragraph-type markup.
+ Object[] obj = mSpannableStringBuilder.getSpans(0, mSpannableStringBuilder.length(), ParagraphStyle.class);
+ for (int i = 0; i < obj.length; i++) {
+ int start = mSpannableStringBuilder.getSpanStart(obj[i]);
+ int end = mSpannableStringBuilder.getSpanEnd(obj[i]);
+
+ // If the last line of the range is blank, back off by one.
+ if (end - 2 >= 0) {
+ if (mSpannableStringBuilder.charAt(end - 1) == '\n' &&
+ mSpannableStringBuilder.charAt(end - 2) == '\n') {
+ end--;
+ }
+ }
+
+ if (end == start) {
+ mSpannableStringBuilder.removeSpan(obj[i]);
+ } else {
+ mSpannableStringBuilder.setSpan(obj[i], start, end, Spannable.SPAN_PARAGRAPH);
+ }
+ }
+
+ return mSpannableStringBuilder;
+ }
+
+ private void handleStartTag(String tag, Attributes attributes) {
+ if (tag.equalsIgnoreCase("br")) {
+ // We don't need to handle this. TagSoup will ensure that there's a </br> for each <br>
+ // so we can safely emite the linebreaks when we handle the close tag.
+ } else if (tag.equalsIgnoreCase("p")) {
+ handleP(mSpannableStringBuilder);
+ } else if (tag.equalsIgnoreCase("div")) {
+ handleP(mSpannableStringBuilder);
+ } else if (tag.equalsIgnoreCase("em")) {
+ start(mSpannableStringBuilder, new Bold());
+ } else if (tag.equalsIgnoreCase("b")) {
+ start(mSpannableStringBuilder, new Bold());
+ } else if (tag.equalsIgnoreCase("strong")) {
+ start(mSpannableStringBuilder, new Italic());
+ } else if (tag.equalsIgnoreCase("cite")) {
+ start(mSpannableStringBuilder, new Italic());
+ } else if (tag.equalsIgnoreCase("dfn")) {
+ start(mSpannableStringBuilder, new Italic());
+ } else if (tag.equalsIgnoreCase("i")) {
+ start(mSpannableStringBuilder, new Italic());
+ } else if (tag.equalsIgnoreCase("big")) {
+ start(mSpannableStringBuilder, new Big());
+ } else if (tag.equalsIgnoreCase("small")) {
+ start(mSpannableStringBuilder, new Small());
+ } else if (tag.equalsIgnoreCase("font")) {
+ startFont(mSpannableStringBuilder, attributes);
+ } else if (tag.equalsIgnoreCase("blockquote")) {
+ handleP(mSpannableStringBuilder);
+ start(mSpannableStringBuilder, new Blockquote());
+ } else if (tag.equalsIgnoreCase("tt")) {
+ start(mSpannableStringBuilder, new Monospace());
+ } else if (tag.equalsIgnoreCase("a")) {
+ startA(mSpannableStringBuilder, attributes);
+ } else if (tag.equalsIgnoreCase("u")) {
+ start(mSpannableStringBuilder, new Underline());
+ } else if (tag.equalsIgnoreCase("sup")) {
+ start(mSpannableStringBuilder, new Super());
+ } else if (tag.equalsIgnoreCase("sub")) {
+ start(mSpannableStringBuilder, new Sub());
+ } else if (tag.length() == 2 &&
+ Character.toLowerCase(tag.charAt(0)) == 'h' &&
+ tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
+ handleP(mSpannableStringBuilder);
+ start(mSpannableStringBuilder, new Header(tag.charAt(1) - '1'));
+ } else if (tag.equalsIgnoreCase("img")) {
+ startImg(mSpannableStringBuilder, attributes, mImageGetter);
+ } else if (mTagHandler != null) {
+ mTagHandler.handleTag(true, tag, mSpannableStringBuilder, mReader);
+ }
+ }
+
+ private void handleEndTag(String tag) {
+ if (tag.equalsIgnoreCase("br")) {
+ handleBr(mSpannableStringBuilder);
+ } else if (tag.equalsIgnoreCase("p")) {
+ handleP(mSpannableStringBuilder);
+ } else if (tag.equalsIgnoreCase("div")) {
+ handleP(mSpannableStringBuilder);
+ } else if (tag.equalsIgnoreCase("em")) {
+ end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD));
+ } else if (tag.equalsIgnoreCase("b")) {
+ end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD));
+ } else if (tag.equalsIgnoreCase("strong")) {
+ end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
+ } else if (tag.equalsIgnoreCase("cite")) {
+ end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
+ } else if (tag.equalsIgnoreCase("dfn")) {
+ end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
+ } else if (tag.equalsIgnoreCase("i")) {
+ end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
+ } else if (tag.equalsIgnoreCase("big")) {
+ end(mSpannableStringBuilder, Big.class, new RelativeSizeSpan(1.25f));
+ } else if (tag.equalsIgnoreCase("small")) {
+ end(mSpannableStringBuilder, Small.class, new RelativeSizeSpan(0.8f));
+ } else if (tag.equalsIgnoreCase("font")) {
+ endFont(mSpannableStringBuilder);
+ } else if (tag.equalsIgnoreCase("blockquote")) {
+ handleP(mSpannableStringBuilder);
+ end(mSpannableStringBuilder, Blockquote.class, new QuoteSpan());
+ } else if (tag.equalsIgnoreCase("tt")) {
+ end(mSpannableStringBuilder, Monospace.class,
+ new TypefaceSpan("monospace"));
+ } else if (tag.equalsIgnoreCase("a")) {
+ endA(mSpannableStringBuilder);
+ } else if (tag.equalsIgnoreCase("u")) {
+ end(mSpannableStringBuilder, Underline.class, new UnderlineSpan());
+ } else if (tag.equalsIgnoreCase("sup")) {
+ end(mSpannableStringBuilder, Super.class, new SuperscriptSpan());
+ } else if (tag.equalsIgnoreCase("sub")) {
+ end(mSpannableStringBuilder, Sub.class, new SubscriptSpan());
+ } else if (tag.length() == 2 &&
+ Character.toLowerCase(tag.charAt(0)) == 'h' &&
+ tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
+ handleP(mSpannableStringBuilder);
+ endHeader(mSpannableStringBuilder);
+ } else if (mTagHandler != null) {
+ mTagHandler.handleTag(false, tag, mSpannableStringBuilder, mReader);
+ }
+ }
+
+ private static void handleP(SpannableStringBuilder text) {
+ int len = text.length();
+
+ if (len >= 1 && text.charAt(len - 1) == '\n') {
+ if (len >= 2 && text.charAt(len - 2) == '\n') {
+ return;
+ }
+
+ text.append("\n");
+ return;
+ }
+
+ if (len != 0) {
+ text.append("\n\n");
+ }
+ }
+
+ private static void handleBr(SpannableStringBuilder text) {
+ text.append("\n");
+ }
+
+ private static Object getLast(Spanned text, Class kind) {
+ /*
+ * This knows that the last returned object from getSpans()
+ * will be the most recently added.
+ */
+ Object[] objs = text.getSpans(0, text.length(), kind);
+
+ if (objs.length == 0) {
+ return null;
+ } else {
+ return objs[objs.length - 1];
+ }
+ }
+
+ private static void start(SpannableStringBuilder text, Object mark) {
+ int len = text.length();
+ text.setSpan(mark, len, len, Spannable.SPAN_MARK_MARK);
+ }
+
+ private static void end(SpannableStringBuilder text, Class kind,
+ Object repl) {
+ int len = text.length();
+ Object obj = getLast(text, kind);
+ int where = text.getSpanStart(obj);
+
+ text.removeSpan(obj);
+
+ if (where != len) {
+ text.setSpan(repl, where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
+ return;
+ }
+
+ private static void startImg(SpannableStringBuilder text,
+ Attributes attributes, Html.ImageGetter img) {
+ String src = attributes.getValue("", "src");
+ Drawable d = null;
+
+ if (img != null) {
+ d = img.getDrawable(src);
+ }
+
+ if (d == null) {
+ d = Resources.getSystem().
+ getDrawable(com.android.internal.R.drawable.unknown_image);
+ d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
+ }
+
+ int len = text.length();
+ text.append("\uFFFC");
+
+ text.setSpan(new ImageSpan(d, src), len, text.length(),
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
+ private static void startFont(SpannableStringBuilder text,
+ Attributes attributes) {
+ String color = attributes.getValue("", "color");
+ String face = attributes.getValue("", "face");
+
+ int len = text.length();
+ text.setSpan(new Font(color, face), len, len, Spannable.SPAN_MARK_MARK);
+ }
+
+ private static void endFont(SpannableStringBuilder text) {
+ int len = text.length();
+ Object obj = getLast(text, Font.class);
+ int where = text.getSpanStart(obj);
+
+ text.removeSpan(obj);
+
+ if (where != len) {
+ Font f = (Font) obj;
+
+ if (f.mColor != null) {
+ int c = -1;
+
+ if (f.mColor.equalsIgnoreCase("aqua")) {
+ c = 0x00FFFF;
+ } else if (f.mColor.equalsIgnoreCase("black")) {
+ c = 0x000000;
+ } else if (f.mColor.equalsIgnoreCase("blue")) {
+ c = 0x0000FF;
+ } else if (f.mColor.equalsIgnoreCase("fuchsia")) {
+ c = 0xFF00FF;
+ } else if (f.mColor.equalsIgnoreCase("green")) {
+ c = 0x008000;
+ } else if (f.mColor.equalsIgnoreCase("grey")) {
+ c = 0x808080;
+ } else if (f.mColor.equalsIgnoreCase("lime")) {
+ c = 0x00FF00;
+ } else if (f.mColor.equalsIgnoreCase("maroon")) {
+ c = 0x800000;
+ } else if (f.mColor.equalsIgnoreCase("navy")) {
+ c = 0x000080;
+ } else if (f.mColor.equalsIgnoreCase("olive")) {
+ c = 0x808000;
+ } else if (f.mColor.equalsIgnoreCase("purple")) {
+ c = 0x800080;
+ } else if (f.mColor.equalsIgnoreCase("red")) {
+ c = 0xFF0000;
+ } else if (f.mColor.equalsIgnoreCase("silver")) {
+ c = 0xC0C0C0;
+ } else if (f.mColor.equalsIgnoreCase("teal")) {
+ c = 0x008080;
+ } else if (f.mColor.equalsIgnoreCase("white")) {
+ c = 0xFFFFFF;
+ } else if (f.mColor.equalsIgnoreCase("yellow")) {
+ c = 0xFFFF00;
+ } else {
+ try {
+ c = XmlUtils.convertValueToInt(f.mColor, -1);
+ } catch (NumberFormatException nfe) {
+ // Can't understand the color, so just drop it.
+ }
+ }
+
+ if (c != -1) {
+ text.setSpan(new ForegroundColorSpan(c | 0xFF000000),
+ where, len,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+
+ if (f.mFace != null) {
+ text.setSpan(new TypefaceSpan(f.mFace), where, len,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+ }
+
+ private static void startA(SpannableStringBuilder text, Attributes attributes) {
+ String href = attributes.getValue("", "href");
+
+ int len = text.length();
+ text.setSpan(new Href(href), len, len, Spannable.SPAN_MARK_MARK);
+ }
+
+ private static void endA(SpannableStringBuilder text) {
+ int len = text.length();
+ Object obj = getLast(text, Href.class);
+ int where = text.getSpanStart(obj);
+
+ text.removeSpan(obj);
+
+ if (where != len) {
+ Href h = (Href) obj;
+
+ if (h.mHref != null) {
+ text.setSpan(new URLSpan(h.mHref), where, len,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+ }
+
+ private static void endHeader(SpannableStringBuilder text) {
+ int len = text.length();
+ Object obj = getLast(text, Header.class);
+
+ int where = text.getSpanStart(obj);
+
+ text.removeSpan(obj);
+
+ // Back off not to change only the text, not the blank line.
+ while (len > where && text.charAt(len - 1) == '\n') {
+ len--;
+ }
+
+ if (where != len) {
+ Header h = (Header) obj;
+
+ text.setSpan(new RelativeSizeSpan(HEADER_SIZES[h.mLevel]),
+ where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ text.setSpan(new StyleSpan(Typeface.BOLD),
+ where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+
+ public void setDocumentLocator(Locator locator) {
+ }
+
+ public void startDocument() throws SAXException {
+ }
+
+ public void endDocument() throws SAXException {
+ }
+
+ public void startPrefixMapping(String prefix, String uri) throws SAXException {
+ }
+
+ public void endPrefixMapping(String prefix) throws SAXException {
+ }
+
+ public void startElement(String uri, String localName, String qName, Attributes attributes)
+ throws SAXException {
+ handleStartTag(localName, attributes);
+ }
+
+ public void endElement(String uri, String localName, String qName) throws SAXException {
+ handleEndTag(localName);
+ }
+
+ public void characters(char ch[], int start, int length) throws SAXException {
+ mSpannableStringBuilder.append(CharBuffer.wrap(ch, start, length));
+ }
+
+ public void ignorableWhitespace(char ch[], int start, int length) throws SAXException {
+ }
+
+ public void processingInstruction(String target, String data) throws SAXException {
+ }
+
+ public void skippedEntity(String name) throws SAXException {
+ }
+
+ private static class Bold { }
+ private static class Italic { }
+ private static class Underline { }
+ private static class Big { }
+ private static class Small { }
+ private static class Monospace { }
+ private static class Blockquote { }
+ private static class Super { }
+ private static class Sub { }
+
+ private static class Font {
+ public String mColor;
+ public String mFace;
+
+ public Font(String color, String face) {
+ mColor = color;
+ mFace = face;
+ }
+ }
+
+ private static class Href {
+ public String mHref;
+
+ public Href(String href) {
+ mHref = href;
+ }
+ }
+
+ private static class Header {
+ private int mLevel;
+
+ public Header(int level) {
+ mLevel = level;
+ }
+ }
+}
diff --git a/core/java/android/text/IClipboard.aidl b/core/java/android/text/IClipboard.aidl
new file mode 100644
index 0000000..4deb5c8
--- /dev/null
+++ b/core/java/android/text/IClipboard.aidl
@@ -0,0 +1,42 @@
+/**
+ * Copyright (c) 2008, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text;
+
+/**
+ * Programming interface to the clipboard, which allows copying and pasting
+ * between applications.
+ * {@hide}
+ */
+interface IClipboard {
+ /**
+ * Returns the text on the clipboard. It will eventually be possible
+ * to store types other than text too, in which case this will return
+ * null if the type cannot be coerced to text.
+ */
+ CharSequence getClipboardText();
+
+ /**
+ * Sets the contents of the clipboard to the specified text.
+ */
+ void setClipboardText(CharSequence text);
+
+ /**
+ * Returns true if the clipboard contains text; false otherwise.
+ */
+ boolean hasClipboardText();
+}
+
diff --git a/core/java/android/text/InputFilter.java b/core/java/android/text/InputFilter.java
new file mode 100644
index 0000000..e1563ae
--- /dev/null
+++ b/core/java/android/text/InputFilter.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text;
+
+/**
+ * InputFilters can be attached to {@link Editable}s to constrain the
+ * changes that can be made to them.
+ */
+public interface InputFilter
+{
+ /**
+ * This method is called when the buffer is going to replace the
+ * range <code>dstart &hellip; dend</code> of <code>dest</code>
+ * with the new text from the range <code>start &hellip; end</code>
+ * of <code>source</code>. Return the CharSequence that you would
+ * like to have placed there instead, including an empty string
+ * if appropriate, or <code>null</code> to accept the original
+ * replacement. Be careful to not to reject 0-length replacements,
+ * as this is what happens when you delete text. Also beware that
+ * you should not attempt to make any changes to <code>dest</code>
+ * from this method; you may only examine it for context.
+ */
+ public CharSequence filter(CharSequence source, int start, int end,
+ Spanned dest, int dstart, int dend);
+
+ /**
+ * This filter will capitalize all the lower case letters that are added
+ * through edits.
+ */
+ public static class AllCaps implements InputFilter {
+ public CharSequence filter(CharSequence source, int start, int end,
+ Spanned dest, int dstart, int dend) {
+ for (int i = start; i < end; i++) {
+ if (Character.isLowerCase(source.charAt(i))) {
+ char[] v = new char[end - start];
+ TextUtils.getChars(source, start, end, v, 0);
+ String s = new String(v).toUpperCase();
+
+ if (source instanceof Spanned) {
+ SpannableString sp = new SpannableString(s);
+ TextUtils.copySpansFrom((Spanned) source,
+ start, end, null, sp, 0);
+ return sp;
+ } else {
+ return s;
+ }
+ }
+ }
+
+ return null; // keep original
+ }
+ }
+
+ /**
+ * This filter will constrain edits not to make the length of the text
+ * greater than the specified length.
+ */
+ public static class LengthFilter implements InputFilter {
+ public LengthFilter(int max) {
+ mMax = max;
+ }
+
+ public CharSequence filter(CharSequence source, int start, int end,
+ Spanned dest, int dstart, int dend) {
+ int keep = mMax - (dest.length() - (dend - dstart));
+
+ if (keep <= 0) {
+ return "";
+ } else if (keep >= end - start) {
+ return null; // keep original
+ } else {
+ return source.subSequence(start, start + keep);
+ }
+ }
+
+ private int mMax;
+ }
+}
diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java
new file mode 100644
index 0000000..346db49
--- /dev/null
+++ b/core/java/android/text/Layout.java
@@ -0,0 +1,1745 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.Path;
+import com.android.internal.util.ArrayUtils;
+import android.util.Config;
+
+import junit.framework.Assert;
+import android.text.style.*;
+import android.text.method.TextKeyListener;
+import android.view.KeyEvent;
+
+/**
+ * A base class that manages text layout in visual elements on
+ * the screen.
+ * <p>For text that will be edited, use a {@link DynamicLayout},
+ * which will be updated as the text changes.
+ * For text that will not change, use a {@link StaticLayout}.
+ */
+public abstract class Layout {
+ /**
+ * Return how wide a layout would be necessary to display the
+ * specified text with one line per paragraph.
+ */
+ public static float getDesiredWidth(CharSequence source,
+ TextPaint paint) {
+ return getDesiredWidth(source, 0, source.length(), paint);
+ }
+
+ /**
+ * Return how wide a layout would be necessary to display the
+ * specified text slice with one line per paragraph.
+ */
+ public static float getDesiredWidth(CharSequence source,
+ int start, int end,
+ TextPaint paint) {
+ float need = 0;
+ TextPaint workPaint = new TextPaint();
+
+ int next;
+ for (int i = start; i <= end; i = next) {
+ next = TextUtils.indexOf(source, '\n', i, end);
+
+ if (next < 0)
+ next = end;
+
+ float w = measureText(paint, workPaint,
+ source, i, next, null, true, null);
+
+ if (w > need)
+ need = w;
+
+ next++;
+ }
+
+ return need;
+ }
+
+ /**
+ * Subclasses of Layout use this constructor to set the display text,
+ * width, and other standard properties.
+ */
+ protected Layout(CharSequence text, TextPaint paint,
+ int width, Alignment align,
+ float spacingmult, float spacingadd) {
+ if (width < 0)
+ throw new IllegalArgumentException("Layout: " + width + " < 0");
+
+ mText = text;
+ mPaint = paint;
+ mWorkPaint = new TextPaint();
+ mWidth = width;
+ mAlignment = align;
+ mSpacingMult = spacingmult;
+ mSpacingAdd = spacingadd;
+ mSpannedText = text instanceof Spanned;
+ }
+
+ /**
+ * Replace constructor properties of this Layout with new ones. Be careful.
+ */
+ /* package */ void replaceWith(CharSequence text, TextPaint paint,
+ int width, Alignment align,
+ float spacingmult, float spacingadd) {
+ if (width < 0) {
+ throw new IllegalArgumentException("Layout: " + width + " < 0");
+ }
+
+ mText = text;
+ mPaint = paint;
+ mWidth = width;
+ mAlignment = align;
+ mSpacingMult = spacingmult;
+ mSpacingAdd = spacingadd;
+ mSpannedText = text instanceof Spanned;
+ }
+
+ /**
+ * Draw this Layout on the specified Canvas.
+ */
+ public void draw(Canvas c) {
+ draw(c, null, null, 0);
+ }
+
+ /**
+ * Draw the specified rectangle from this Layout on the specified Canvas,
+ * with the specified path drawn between the background and the text.
+ */
+ public void draw(Canvas c, Path highlight, Paint highlightpaint,
+ int cursorOffsetVertical) {
+ int dtop, dbottom;
+
+ synchronized (sTempRect) {
+ if (!c.getClipBounds(sTempRect)) {
+ return;
+ }
+
+ dtop = sTempRect.top;
+ dbottom = sTempRect.bottom;
+ }
+
+ TextPaint paint = mPaint;
+
+ int top = 0;
+ // getLineBottom(getLineCount() -1) just calls getLineTop(getLineCount)
+ int bottom = getLineTop(getLineCount());
+
+
+ if (dtop > top) {
+ top = dtop;
+ }
+ if (dbottom < bottom) {
+ bottom = dbottom;
+ }
+
+ int first = getLineForVertical(top);
+ int last = getLineForVertical(bottom);
+
+ int previousLineBottom = getLineTop(first);
+ int previousLineEnd = getLineStart(first);
+
+ CharSequence buf = mText;
+
+ ParagraphStyle[] nospans = ArrayUtils.emptyArray(ParagraphStyle.class);
+ ParagraphStyle[] spans = nospans;
+ int spanend = 0;
+ int textLength = 0;
+ boolean spannedText = mSpannedText;
+
+ if (spannedText) {
+ spanend = 0;
+ textLength = buf.length();
+ for (int i = first; i <= last; i++) {
+ int start = previousLineEnd;
+ int end = getLineStart(i+1);
+ previousLineEnd = end;
+
+ int ltop = previousLineBottom;
+ int lbottom = getLineTop(i+1);
+ previousLineBottom = lbottom;
+ int lbaseline = lbottom - getLineDescent(i);
+
+ if (start >= spanend) {
+ Spanned sp = (Spanned) buf;
+ spanend = sp.nextSpanTransition(start, textLength,
+ LineBackgroundSpan.class);
+ spans = sp.getSpans(start, spanend,
+ LineBackgroundSpan.class);
+ }
+
+ for (int n = 0; n < spans.length; n++) {
+ LineBackgroundSpan back = (LineBackgroundSpan) spans[n];
+
+ back.drawBackground(c, paint, 0, mWidth,
+ ltop, lbaseline, lbottom,
+ buf, start, end,
+ i);
+ }
+ }
+ // reset to their original values
+ spanend = 0;
+ previousLineBottom = getLineTop(first);
+ previousLineEnd = getLineStart(first);
+ spans = nospans;
+ }
+
+ // There can be a highlight even without spans if we are drawing
+ // a non-spanned transformation of a spanned editing buffer.
+ if (highlight != null) {
+ if (cursorOffsetVertical != 0) {
+ c.translate(0, cursorOffsetVertical);
+ }
+
+ c.drawPath(highlight, highlightpaint);
+
+ if (cursorOffsetVertical != 0) {
+ c.translate(0, -cursorOffsetVertical);
+ }
+ }
+
+ Alignment align = mAlignment;
+
+ for (int i = first; i <= last; i++) {
+ int start = previousLineEnd;
+
+ previousLineEnd = getLineStart(i+1);
+ int end = getLineVisibleEnd(i, start, previousLineEnd);
+
+ int ltop = previousLineBottom;
+ int lbottom = getLineTop(i+1);
+ previousLineBottom = lbottom;
+ int lbaseline = lbottom - getLineDescent(i);
+
+ boolean par = false;
+ if (spannedText) {
+ if (start == 0 || buf.charAt(start - 1) == '\n') {
+ par = true;
+ }
+ if (start >= spanend) {
+
+ Spanned sp = (Spanned) buf;
+
+ spanend = sp.nextSpanTransition(start, textLength,
+ ParagraphStyle.class);
+ spans = sp.getSpans(start, spanend, ParagraphStyle.class);
+
+ align = mAlignment;
+
+ for (int n = spans.length-1; n >= 0; n--) {
+ if (spans[n] instanceof AlignmentSpan) {
+ align = ((AlignmentSpan) spans[n]).getAlignment();
+ break;
+ }
+ }
+ }
+ }
+
+ int dir = getParagraphDirection(i);
+ int left = 0;
+ int right = mWidth;
+
+ if (spannedText) {
+ final int length = spans.length;
+ for (int n = 0; n < length; n++) {
+ if (spans[n] instanceof LeadingMarginSpan) {
+ LeadingMarginSpan margin = (LeadingMarginSpan) spans[n];
+
+ if (dir == DIR_RIGHT_TO_LEFT) {
+ margin.drawLeadingMargin(c, paint, right, dir, ltop,
+ lbaseline, lbottom, buf,
+ start, end, par, this);
+
+ right -= margin.getLeadingMargin(par);
+ } else {
+ margin.drawLeadingMargin(c, paint, left, dir, ltop,
+ lbaseline, lbottom, buf,
+ start, end, par, this);
+
+ left += margin.getLeadingMargin(par);
+ }
+ }
+ }
+ }
+
+ int x;
+ if (align == Alignment.ALIGN_NORMAL) {
+ if (dir == DIR_LEFT_TO_RIGHT) {
+ x = left;
+ } else {
+ x = right;
+ }
+ } else {
+ int max = (int)getLineMax(i, spans, false);
+ if (align == Alignment.ALIGN_OPPOSITE) {
+ if (dir == DIR_RIGHT_TO_LEFT) {
+ x = left + max;
+ } else {
+ x = right - max;
+ }
+ } else {
+ // Alignment.ALIGN_CENTER
+ max = max & ~1;
+ int half = (right - left - max) >> 1;
+ if (dir == DIR_RIGHT_TO_LEFT) {
+ x = right - half;
+ } else {
+ x = left + half;
+ }
+ }
+ }
+
+ Directions directions = getLineDirections(i);
+ boolean hasTab = getLineContainsTab(i);
+ if (directions == DIRS_ALL_LEFT_TO_RIGHT &&
+ !spannedText && !hasTab) {
+ if (Config.DEBUG) {
+ Assert.assertTrue(dir == DIR_LEFT_TO_RIGHT);
+ Assert.assertNotNull(c);
+ }
+ c.drawText(buf, start, end, x, lbaseline, paint);
+ } else {
+ drawText(c, buf, start, end, dir, directions,
+ x, ltop, lbaseline, lbottom, paint, mWorkPaint,
+ hasTab, spans);
+ }
+ }
+ }
+
+ /**
+ * Return the text that is displayed by this Layout.
+ */
+ public final CharSequence getText() {
+ return mText;
+ }
+
+ /**
+ * Return the base Paint properties for this layout.
+ * Do NOT change the paint, which may result in funny
+ * drawing for this layout.
+ */
+ public final TextPaint getPaint() {
+ return mPaint;
+ }
+
+ /**
+ * Return the width of this layout.
+ */
+ public final int getWidth() {
+ return mWidth;
+ }
+
+ /**
+ * Return the width to which this Layout is ellipsizing, or
+ * {@link #getWidth} if it is not doing anything special.
+ */
+ public int getEllipsizedWidth() {
+ return mWidth;
+ }
+
+ /**
+ * Increase the width of this layout to the specified width.
+ * Be careful to use this only when you know it is appropriate --
+ * it does not cause the text to reflow to use the full new width.
+ */
+ public final void increaseWidthTo(int wid) {
+ if (wid < mWidth) {
+ throw new RuntimeException("attempted to reduce Layout width");
+ }
+
+ mWidth = wid;
+ }
+
+ /**
+ * Return the total height of this layout.
+ */
+ public int getHeight() {
+ return getLineTop(getLineCount()); // same as getLineBottom(getLineCount() - 1);
+ }
+
+ /**
+ * Return the base alignment of this layout.
+ */
+ public final Alignment getAlignment() {
+ return mAlignment;
+ }
+
+ /**
+ * Return what the text height is multiplied by to get the line height.
+ */
+ public final float getSpacingMultiplier() {
+ return mSpacingMult;
+ }
+
+ /**
+ * Return the number of units of leading that are added to each line.
+ */
+ public final float getSpacingAdd() {
+ return mSpacingAdd;
+ }
+
+ /**
+ * Return the number of lines of text in this layout.
+ */
+ public abstract int getLineCount();
+
+ /**
+ * Return the baseline for the specified line (0&hellip;getLineCount() - 1)
+ * If bounds is not null, return the top, left, right, bottom extents
+ * of the specified line in it.
+ * @param line which line to examine (0..getLineCount() - 1)
+ * @param bounds Optional. If not null, it returns the extent of the line
+ * @return the Y-coordinate of the baseline
+ */
+ public int getLineBounds(int line, Rect bounds) {
+ if (bounds != null) {
+ bounds.left = 0; // ???
+ bounds.top = getLineTop(line);
+ bounds.right = mWidth; // ???
+ bounds.bottom = getLineBottom(line);
+ }
+ return getLineBaseline(line);
+ }
+
+ /**
+ * Return the vertical position of the top of the specified line.
+ * If the specified line is one beyond the last line, returns the
+ * bottom of the last line.
+ */
+ public abstract int getLineTop(int line);
+
+ /**
+ * Return the descent of the specified line.
+ */
+ public abstract int getLineDescent(int line);
+
+ /**
+ * Return the text offset of the beginning of the specified line.
+ * If the specified line is one beyond the last line, returns the
+ * end of the last line.
+ */
+ public abstract int getLineStart(int line);
+
+ /**
+ * Returns the primary directionality of the paragraph containing
+ * the specified line.
+ */
+ public abstract int getParagraphDirection(int line);
+
+ /**
+ * Returns whether the specified line contains one or more tabs.
+ */
+ public abstract boolean getLineContainsTab(int line);
+
+ /**
+ * Returns an array of directionalities for the specified line.
+ * The array alternates counts of characters in left-to-right
+ * and right-to-left segments of the line.
+ */
+ public abstract Directions getLineDirections(int line);
+
+ /**
+ * Returns the (negative) number of extra pixels of ascent padding in the
+ * top line of the Layout.
+ */
+ public abstract int getTopPadding();
+
+ /**
+ * Returns the number of extra pixels of descent padding in the
+ * bottom line of the Layout.
+ */
+ public abstract int getBottomPadding();
+
+ /**
+ * Get the primary horizontal position for the specified text offset.
+ * This is the location where a new character would be inserted in
+ * the paragraph's primary direction.
+ */
+ public float getPrimaryHorizontal(int offset) {
+ return getHorizontal(offset, false, true);
+ }
+
+ /**
+ * Get the secondary horizontal position for the specified text offset.
+ * This is the location where a new character would be inserted in
+ * the direction other than the paragraph's primary direction.
+ */
+ public float getSecondaryHorizontal(int offset) {
+ return getHorizontal(offset, true, true);
+ }
+
+ private float getHorizontal(int offset, boolean trailing, boolean alt) {
+ int line = getLineForOffset(offset);
+
+ return getHorizontal(offset, trailing, alt, line);
+ }
+
+ private float getHorizontal(int offset, boolean trailing, boolean alt,
+ int line) {
+ int start = getLineStart(line);
+ int end = getLineVisibleEnd(line);
+ int dir = getParagraphDirection(line);
+ boolean tab = getLineContainsTab(line);
+ Directions directions = getLineDirections(line);
+
+ TabStopSpan[] tabs = null;
+ if (tab && mText instanceof Spanned) {
+ tabs = ((Spanned) mText).getSpans(start, end, TabStopSpan.class);
+ }
+
+ float wid = measureText(mPaint, mWorkPaint, mText, start, offset, end,
+ dir, directions, trailing, alt, tab, tabs);
+
+ if (offset > end) {
+ if (dir == DIR_RIGHT_TO_LEFT)
+ wid -= measureText(mPaint, mWorkPaint,
+ mText, end, offset, null, tab, tabs);
+ else
+ wid += measureText(mPaint, mWorkPaint,
+ mText, end, offset, null, tab, tabs);
+ }
+
+ Alignment align = getParagraphAlignment(line);
+ int left = getParagraphLeft(line);
+ int right = getParagraphRight(line);
+
+ if (align == Alignment.ALIGN_NORMAL) {
+ if (dir == DIR_RIGHT_TO_LEFT)
+ return right + wid;
+ else
+ return left + wid;
+ }
+
+ float max = getLineMax(line);
+
+ if (align == Alignment.ALIGN_OPPOSITE) {
+ if (dir == DIR_RIGHT_TO_LEFT)
+ return left + max + wid;
+ else
+ return right - max + wid;
+ } else { /* align == Alignment.ALIGN_CENTER */
+ int imax = ((int) max) & ~1;
+
+ if (dir == DIR_RIGHT_TO_LEFT)
+ return right - (((right - left) - imax) / 2) + wid;
+ else
+ return left + ((right - left) - imax) / 2 + wid;
+ }
+ }
+
+ /**
+ * Get the leftmost position that should be exposed for horizontal
+ * scrolling on the specified line.
+ */
+ public float getLineLeft(int line) {
+ int dir = getParagraphDirection(line);
+ Alignment align = getParagraphAlignment(line);
+
+ if (align == Alignment.ALIGN_NORMAL) {
+ if (dir == DIR_RIGHT_TO_LEFT)
+ return getParagraphRight(line) - getLineMax(line);
+ else
+ return 0;
+ } else if (align == Alignment.ALIGN_OPPOSITE) {
+ if (dir == DIR_RIGHT_TO_LEFT)
+ return 0;
+ else
+ return mWidth - getLineMax(line);
+ } else { /* align == Alignment.ALIGN_CENTER */
+ int left = getParagraphLeft(line);
+ int right = getParagraphRight(line);
+ int max = ((int) getLineMax(line)) & ~1;
+
+ return left + ((right - left) - max) / 2;
+ }
+ }
+
+ /**
+ * Get the rightmost position that should be exposed for horizontal
+ * scrolling on the specified line.
+ */
+ public float getLineRight(int line) {
+ int dir = getParagraphDirection(line);
+ Alignment align = getParagraphAlignment(line);
+
+ if (align == Alignment.ALIGN_NORMAL) {
+ if (dir == DIR_RIGHT_TO_LEFT)
+ return mWidth;
+ else
+ return getParagraphLeft(line) + getLineMax(line);
+ } else if (align == Alignment.ALIGN_OPPOSITE) {
+ if (dir == DIR_RIGHT_TO_LEFT)
+ return getLineMax(line);
+ else
+ return mWidth;
+ } else { /* align == Alignment.ALIGN_CENTER */
+ int left = getParagraphLeft(line);
+ int right = getParagraphRight(line);
+ int max = ((int) getLineMax(line)) & ~1;
+
+ return right - ((right - left) - max) / 2;
+ }
+ }
+
+ /**
+ * Gets the horizontal extent of the specified line, excluding
+ * trailing whitespace.
+ */
+ public float getLineMax(int line) {
+ return getLineMax(line, null, false);
+ }
+
+ /**
+ * Gets the horizontal extent of the specified line, including
+ * trailing whitespace.
+ */
+ public float getLineWidth(int line) {
+ return getLineMax(line, null, true);
+ }
+
+ private float getLineMax(int line, Object[] tabs, boolean full) {
+ int start = getLineStart(line);
+ int end;
+
+ if (full) {
+ end = getLineEnd(line);
+ } else {
+ end = getLineVisibleEnd(line);
+ }
+ boolean tab = getLineContainsTab(line);
+
+ if (tabs == null && tab && mText instanceof Spanned) {
+ tabs = ((Spanned) mText).getSpans(start, end, TabStopSpan.class);
+ }
+
+ return measureText(mPaint, mWorkPaint,
+ mText, start, end, null, tab, tabs);
+ }
+
+ /**
+ * Get the line number corresponding to the specified vertical position.
+ * If you ask for a position above 0, you get 0; if you ask for a position
+ * below the bottom of the text, you get the last line.
+ */
+ // FIXME: It may be faster to do a linear search for layouts without many lines.
+ public int getLineForVertical(int vertical) {
+ int high = getLineCount(), low = -1, guess;
+
+ while (high - low > 1) {
+ guess = (high + low) / 2;
+
+ if (getLineTop(guess) > vertical)
+ high = guess;
+ else
+ low = guess;
+ }
+
+ if (low < 0)
+ return 0;
+ else
+ return low;
+ }
+
+ /**
+ * Get the line number on which the specified text offset appears.
+ * If you ask for a position before 0, you get 0; if you ask for a position
+ * beyond the end of the text, you get the last line.
+ */
+ public int getLineForOffset(int offset) {
+ int high = getLineCount(), low = -1, guess;
+
+ while (high - low > 1) {
+ guess = (high + low) / 2;
+
+ if (getLineStart(guess) > offset)
+ high = guess;
+ else
+ low = guess;
+ }
+
+ if (low < 0)
+ return 0;
+ else
+ return low;
+ }
+
+ /**
+ * Get the character offset on the specfied line whose position is
+ * closest to the specified horizontal position.
+ */
+ public int getOffsetForHorizontal(int line, float horiz) {
+ int max = getLineEnd(line) - 1;
+ int min = getLineStart(line);
+ Directions dirs = getLineDirections(line);
+
+ if (line == getLineCount() - 1)
+ max++;
+
+ int best = min;
+ float bestdist = Math.abs(getPrimaryHorizontal(best) - horiz);
+
+ int here = min;
+ for (int i = 0; i < dirs.mDirections.length; i++) {
+ int there = here + dirs.mDirections[i];
+ int swap = ((i & 1) == 0) ? 1 : -1;
+
+ if (there > max)
+ there = max;
+
+ int high = there - 1 + 1, low = here + 1 - 1, guess;
+
+ while (high - low > 1) {
+ guess = (high + low) / 2;
+ int adguess = getOffsetAtStartOf(guess);
+
+ if (getPrimaryHorizontal(adguess) * swap >= horiz * swap)
+ high = guess;
+ else
+ low = guess;
+ }
+
+ if (low < here + 1)
+ low = here + 1;
+
+ if (low < there) {
+ low = getOffsetAtStartOf(low);
+
+ float dist = Math.abs(getPrimaryHorizontal(low) - horiz);
+
+ int aft = TextUtils.getOffsetAfter(mText, low);
+ if (aft < there) {
+ float other = Math.abs(getPrimaryHorizontal(aft) - horiz);
+
+ if (other < dist) {
+ dist = other;
+ low = aft;
+ }
+ }
+
+ if (dist < bestdist) {
+ bestdist = dist;
+ best = low;
+ }
+ }
+
+ float dist = Math.abs(getPrimaryHorizontal(here) - horiz);
+
+ if (dist < bestdist) {
+ bestdist = dist;
+ best = here;
+ }
+
+ here = there;
+ }
+
+ float dist = Math.abs(getPrimaryHorizontal(max) - horiz);
+
+ if (dist < bestdist) {
+ bestdist = dist;
+ best = max;
+ }
+
+ return best;
+ }
+
+ /**
+ * Return the text offset after the last character on the specified line.
+ */
+ public final int getLineEnd(int line) {
+ return getLineStart(line + 1);
+ }
+
+ /**
+ * Return the text offset after the last visible character (so whitespace
+ * is not counted) on the specified line.
+ */
+ public int getLineVisibleEnd(int line) {
+ return getLineVisibleEnd(line, getLineStart(line), getLineStart(line+1));
+ }
+
+ private int getLineVisibleEnd(int line, int start, int end) {
+ if (Config.DEBUG) {
+ Assert.assertTrue(getLineStart(line) == start && getLineStart(line+1) == end);
+ }
+
+ CharSequence text = mText;
+ char ch;
+ if (line == getLineCount() - 1) {
+ return end;
+ }
+
+ for (; end > start; end--) {
+ ch = text.charAt(end - 1);
+
+ if (ch == '\n') {
+ return end - 1;
+ }
+
+ if (ch != ' ' && ch != '\t') {
+ break;
+ }
+
+ }
+
+ return end;
+ }
+
+ /**
+ * Return the vertical position of the bottom of the specified line.
+ */
+ public final int getLineBottom(int line) {
+ return getLineTop(line + 1);
+ }
+
+ /**
+ * Return the vertical position of the baseline of the specified line.
+ */
+ public final int getLineBaseline(int line) {
+ // getLineTop(line+1) == getLineTop(line)
+ return getLineTop(line+1) - getLineDescent(line);
+ }
+
+ /**
+ * Get the ascent of the text on the specified line.
+ * The return value is negative to match the Paint.ascent() convention.
+ */
+ public final int getLineAscent(int line) {
+ // getLineTop(line+1) - getLineDescent(line) == getLineBaseLine(line)
+ return getLineTop(line) - (getLineTop(line+1) - getLineDescent(line));
+ }
+
+ /**
+ * Return the text offset that would be reached by moving left
+ * (possibly onto another line) from the specified offset.
+ */
+ public int getOffsetToLeftOf(int offset) {
+ int line = getLineForOffset(offset);
+ int start = getLineStart(line);
+ int end = getLineEnd(line);
+ Directions dirs = getLineDirections(line);
+
+ if (line != getLineCount() - 1)
+ end--;
+
+ float horiz = getPrimaryHorizontal(offset);
+
+ int best = offset;
+ float besth = Integer.MIN_VALUE;
+ int candidate;
+
+ candidate = TextUtils.getOffsetBefore(mText, offset);
+ if (candidate >= start && candidate <= end) {
+ float h = getPrimaryHorizontal(candidate);
+
+ if (h < horiz && h > besth) {
+ best = candidate;
+ besth = h;
+ }
+ }
+
+ candidate = TextUtils.getOffsetAfter(mText, offset);
+ if (candidate >= start && candidate <= end) {
+ float h = getPrimaryHorizontal(candidate);
+
+ if (h < horiz && h > besth) {
+ best = candidate;
+ besth = h;
+ }
+ }
+
+ int here = start;
+ for (int i = 0; i < dirs.mDirections.length; i++) {
+ int there = here + dirs.mDirections[i];
+ if (there > end)
+ there = end;
+
+ float h = getPrimaryHorizontal(here);
+
+ if (h < horiz && h > besth) {
+ best = here;
+ besth = h;
+ }
+
+ candidate = TextUtils.getOffsetAfter(mText, here);
+ if (candidate >= start && candidate <= end) {
+ h = getPrimaryHorizontal(candidate);
+
+ if (h < horiz && h > besth) {
+ best = candidate;
+ besth = h;
+ }
+ }
+
+ candidate = TextUtils.getOffsetBefore(mText, there);
+ if (candidate >= start && candidate <= end) {
+ h = getPrimaryHorizontal(candidate);
+
+ if (h < horiz && h > besth) {
+ best = candidate;
+ besth = h;
+ }
+ }
+
+ here = there;
+ }
+
+ float h = getPrimaryHorizontal(end);
+
+ if (h < horiz && h > besth) {
+ best = end;
+ besth = h;
+ }
+
+ if (best != offset)
+ return best;
+
+ int dir = getParagraphDirection(line);
+
+ if (dir > 0) {
+ if (line == 0)
+ return best;
+ else
+ return getOffsetForHorizontal(line - 1, 10000);
+ } else {
+ if (line == getLineCount() - 1)
+ return best;
+ else
+ return getOffsetForHorizontal(line + 1, 10000);
+ }
+ }
+
+ /**
+ * Return the text offset that would be reached by moving right
+ * (possibly onto another line) from the specified offset.
+ */
+ public int getOffsetToRightOf(int offset) {
+ int line = getLineForOffset(offset);
+ int start = getLineStart(line);
+ int end = getLineEnd(line);
+ Directions dirs = getLineDirections(line);
+
+ if (line != getLineCount() - 1)
+ end--;
+
+ float horiz = getPrimaryHorizontal(offset);
+
+ int best = offset;
+ float besth = Integer.MAX_VALUE;
+ int candidate;
+
+ candidate = TextUtils.getOffsetBefore(mText, offset);
+ if (candidate >= start && candidate <= end) {
+ float h = getPrimaryHorizontal(candidate);
+
+ if (h > horiz && h < besth) {
+ best = candidate;
+ besth = h;
+ }
+ }
+
+ candidate = TextUtils.getOffsetAfter(mText, offset);
+ if (candidate >= start && candidate <= end) {
+ float h = getPrimaryHorizontal(candidate);
+
+ if (h > horiz && h < besth) {
+ best = candidate;
+ besth = h;
+ }
+ }
+
+ int here = start;
+ for (int i = 0; i < dirs.mDirections.length; i++) {
+ int there = here + dirs.mDirections[i];
+ if (there > end)
+ there = end;
+
+ float h = getPrimaryHorizontal(here);
+
+ if (h > horiz && h < besth) {
+ best = here;
+ besth = h;
+ }
+
+ candidate = TextUtils.getOffsetAfter(mText, here);
+ if (candidate >= start && candidate <= end) {
+ h = getPrimaryHorizontal(candidate);
+
+ if (h > horiz && h < besth) {
+ best = candidate;
+ besth = h;
+ }
+ }
+
+ candidate = TextUtils.getOffsetBefore(mText, there);
+ if (candidate >= start && candidate <= end) {
+ h = getPrimaryHorizontal(candidate);
+
+ if (h > horiz && h < besth) {
+ best = candidate;
+ besth = h;
+ }
+ }
+
+ here = there;
+ }
+
+ float h = getPrimaryHorizontal(end);
+
+ if (h > horiz && h < besth) {
+ best = end;
+ besth = h;
+ }
+
+ if (best != offset)
+ return best;
+
+ int dir = getParagraphDirection(line);
+
+ if (dir > 0) {
+ if (line == getLineCount() - 1)
+ return best;
+ else
+ return getOffsetForHorizontal(line + 1, -10000);
+ } else {
+ if (line == 0)
+ return best;
+ else
+ return getOffsetForHorizontal(line - 1, -10000);
+ }
+ }
+
+ private int getOffsetAtStartOf(int offset) {
+ if (offset == 0)
+ return 0;
+
+ CharSequence text = mText;
+ char c = text.charAt(offset);
+
+ if (c >= '\uDC00' && c <= '\uDFFF') {
+ char c1 = text.charAt(offset - 1);
+
+ if (c1 >= '\uD800' && c1 <= '\uDBFF')
+ offset -= 1;
+ }
+
+ if (mSpannedText) {
+ ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset,
+ ReplacementSpan.class);
+
+ for (int i = 0; i < spans.length; i++) {
+ int start = ((Spanned) text).getSpanStart(spans[i]);
+ int end = ((Spanned) text).getSpanEnd(spans[i]);
+
+ if (start < offset && end > offset)
+ offset = start;
+ }
+ }
+
+ return offset;
+ }
+
+ /**
+ * Fills in the specified Path with a representation of a cursor
+ * at the specified offset. This will often be a vertical line
+ * but can be multiple discontinous lines in text with multiple
+ * directionalities.
+ */
+ public void getCursorPath(int point, Path dest,
+ CharSequence editingBuffer) {
+ dest.reset();
+
+ int line = getLineForOffset(point);
+ int top = getLineTop(line);
+ int bottom = getLineTop(line+1);
+
+ float h1 = getPrimaryHorizontal(point) - 0.5f;
+ float h2 = getSecondaryHorizontal(point) - 0.5f;
+
+ int caps = TextKeyListener.getMetaState(editingBuffer,
+ KeyEvent.META_SHIFT_ON);
+ int fn = TextKeyListener.getMetaState(editingBuffer,
+ KeyEvent.META_ALT_ON);
+ int dist = 0;
+
+ if (caps != 0 || fn != 0) {
+ dist = (bottom - top) >> 2;
+
+ if (fn != 0)
+ top += dist;
+ if (caps != 0)
+ bottom -= dist;
+ }
+
+ if (h1 < 0.5f)
+ h1 = 0.5f;
+ if (h2 < 0.5f)
+ h2 = 0.5f;
+
+ if (h1 == h2) {
+ dest.moveTo(h1, top);
+ dest.lineTo(h1, bottom);
+ } else {
+ dest.moveTo(h1, top);
+ dest.lineTo(h1, (top + bottom) >> 1);
+
+ dest.moveTo(h2, (top + bottom) >> 1);
+ dest.lineTo(h2, bottom);
+ }
+
+ if (caps == 2) {
+ dest.moveTo(h2, bottom);
+ dest.lineTo(h2 - dist, bottom + dist);
+ dest.lineTo(h2, bottom);
+ dest.lineTo(h2 + dist, bottom + dist);
+ } else if (caps == 1) {
+ dest.moveTo(h2, bottom);
+ dest.lineTo(h2 - dist, bottom + dist);
+
+ dest.moveTo(h2 - dist, bottom + dist - 0.5f);
+ dest.lineTo(h2 + dist, bottom + dist - 0.5f);
+
+ dest.moveTo(h2 + dist, bottom + dist);
+ dest.lineTo(h2, bottom);
+ }
+
+ if (fn == 2) {
+ dest.moveTo(h1, top);
+ dest.lineTo(h1 - dist, top - dist);
+ dest.lineTo(h1, top);
+ dest.lineTo(h1 + dist, top - dist);
+ } else if (fn == 1) {
+ dest.moveTo(h1, top);
+ dest.lineTo(h1 - dist, top - dist);
+
+ dest.moveTo(h1 - dist, top - dist + 0.5f);
+ dest.lineTo(h1 + dist, top - dist + 0.5f);
+
+ dest.moveTo(h1 + dist, top - dist);
+ dest.lineTo(h1, top);
+ }
+ }
+
+ private void addSelection(int line, int start, int end,
+ int top, int bottom, Path dest) {
+ int linestart = getLineStart(line);
+ int lineend = getLineEnd(line);
+ Directions dirs = getLineDirections(line);
+
+ if (lineend > linestart && mText.charAt(lineend - 1) == '\n')
+ lineend--;
+
+ int here = linestart;
+ for (int i = 0; i < dirs.mDirections.length; i++) {
+ int there = here + dirs.mDirections[i];
+ if (there > lineend)
+ there = lineend;
+
+ if (start <= there && end >= here) {
+ int st = Math.max(start, here);
+ int en = Math.min(end, there);
+
+ if (st != en) {
+ float h1 = getHorizontal(st, false, false, line);
+ float h2 = getHorizontal(en, true, false, line);
+
+ dest.addRect(h1, top, h2, bottom, Path.Direction.CW);
+ }
+ }
+
+ here = there;
+ }
+ }
+
+ /**
+ * Fills in the specified Path with a representation of a highlight
+ * between the specified offsets. This will often be a rectangle
+ * or a potentially discontinuous set of rectangles. If the start
+ * and end are the same, the returned path is empty.
+ */
+ public void getSelectionPath(int start, int end, Path dest) {
+ dest.reset();
+
+ if (start == end)
+ return;
+
+ if (end < start) {
+ int temp = end;
+ end = start;
+ start = temp;
+ }
+
+ int startline = getLineForOffset(start);
+ int endline = getLineForOffset(end);
+
+ int top = getLineTop(startline);
+ int bottom = getLineBottom(endline);
+
+ if (startline == endline) {
+ addSelection(startline, start, end, top, bottom, dest);
+ } else {
+ final float width = mWidth;
+
+ addSelection(startline, start, getLineEnd(startline),
+ top, getLineBottom(startline), dest);
+
+ if (getParagraphDirection(startline) == DIR_RIGHT_TO_LEFT)
+ dest.addRect(getLineLeft(startline), top,
+ 0, getLineBottom(startline), Path.Direction.CW);
+ else
+ dest.addRect(getLineRight(startline), top,
+ width, getLineBottom(startline), Path.Direction.CW);
+
+ for (int i = startline + 1; i < endline; i++) {
+ top = getLineTop(i);
+ bottom = getLineBottom(i);
+ dest.addRect(0, top, width, bottom, Path.Direction.CW);
+ }
+
+ top = getLineTop(endline);
+ bottom = getLineBottom(endline);
+
+ addSelection(endline, getLineStart(endline), end,
+ top, bottom, dest);
+
+ if (getParagraphDirection(endline) == DIR_RIGHT_TO_LEFT)
+ dest.addRect(width, top, getLineRight(endline), bottom, Path.Direction.CW);
+ else
+ dest.addRect(0, top, getLineLeft(endline), bottom, Path.Direction.CW);
+ }
+ }
+
+ /**
+ * Get the alignment of the specified paragraph, taking into account
+ * markup attached to it.
+ */
+ public final Alignment getParagraphAlignment(int line) {
+ Alignment align = mAlignment;
+
+ if (mSpannedText) {
+ Spanned sp = (Spanned) mText;
+ AlignmentSpan[] spans = sp.getSpans(getLineStart(line),
+ getLineEnd(line),
+ AlignmentSpan.class);
+
+ int spanLength = spans.length;
+ if (spanLength > 0) {
+ align = spans[spanLength-1].getAlignment();
+ }
+ }
+
+ return align;
+ }
+
+ /**
+ * Get the left edge of the specified paragraph, inset by left margins.
+ */
+ public final int getParagraphLeft(int line) {
+ int dir = getParagraphDirection(line);
+
+ int left = 0;
+
+ boolean par = false;
+ int off = getLineStart(line);
+ if (off == 0 || mText.charAt(off - 1) == '\n')
+ par = true;
+
+ if (dir == DIR_LEFT_TO_RIGHT) {
+ if (mSpannedText) {
+ Spanned sp = (Spanned) mText;
+ LeadingMarginSpan[] spans = sp.getSpans(getLineStart(line),
+ getLineEnd(line),
+ LeadingMarginSpan.class);
+
+ for (int i = 0; i < spans.length; i++) {
+ left += spans[i].getLeadingMargin(par);
+ }
+ }
+ }
+
+ return left;
+ }
+
+ /**
+ * Get the right edge of the specified paragraph, inset by right margins.
+ */
+ public final int getParagraphRight(int line) {
+ int dir = getParagraphDirection(line);
+
+ int right = mWidth;
+
+ boolean par = false;
+ int off = getLineStart(line);
+ if (off == 0 || mText.charAt(off - 1) == '\n')
+ par = true;
+
+
+ if (dir == DIR_RIGHT_TO_LEFT) {
+ if (mSpannedText) {
+ Spanned sp = (Spanned) mText;
+ LeadingMarginSpan[] spans = sp.getSpans(getLineStart(line),
+ getLineEnd(line),
+ LeadingMarginSpan.class);
+
+ for (int i = 0; i < spans.length; i++) {
+ right -= spans[i].getLeadingMargin(par);
+ }
+ }
+ }
+
+ return right;
+ }
+
+ private static void drawText(Canvas canvas,
+ CharSequence text, int start, int end,
+ int dir, Directions directions,
+ float x, int top, int y, int bottom,
+ TextPaint paint,
+ TextPaint workPaint,
+ boolean hasTabs, Object[] parspans) {
+ char[] buf;
+ if (!hasTabs) {
+ if (directions == DIRS_ALL_LEFT_TO_RIGHT) {
+ if (Config.DEBUG) {
+ Assert.assertTrue(DIR_LEFT_TO_RIGHT == dir);
+ }
+ Styled.drawText(canvas, text, start, end, dir, false, x, top, y, bottom, paint, workPaint, false);
+ return;
+ }
+ buf = null;
+ } else {
+ buf = TextUtils.obtain(end - start);
+ TextUtils.getChars(text, start, end, buf, 0);
+ }
+
+ float h = 0;
+
+ int here = 0;
+ for (int i = 0; i < directions.mDirections.length; i++) {
+ int there = here + directions.mDirections[i];
+ if (there > end - start)
+ there = end - start;
+
+ int segstart = here;
+ for (int j = hasTabs ? here : there; j <= there; j++) {
+ if (j == there || buf[j] == '\t') {
+ h += Styled.drawText(canvas, text,
+ start + segstart, start + j,
+ dir, (i & 1) != 0, x + h,
+ top, y, bottom, paint, workPaint,
+ start + j != end);
+
+ if (j != there && buf[j] == '\t')
+ h = dir * nextTab(text, start, end, h * dir, parspans);
+
+ segstart = j + 1;
+ }
+ }
+
+ here = there;
+ }
+
+ if (hasTabs)
+ TextUtils.recycle(buf);
+ }
+
+ private static float measureText(TextPaint paint,
+ TextPaint workPaint,
+ CharSequence text,
+ int start, int offset, int end,
+ int dir, Directions directions,
+ boolean trailing, boolean alt,
+ boolean hasTabs, Object[] tabs) {
+ char[] buf = null;
+
+ if (hasTabs) {
+ buf = TextUtils.obtain(end - start);
+ TextUtils.getChars(text, start, end, buf, 0);
+ }
+
+ float h = 0;
+
+ if (alt) {
+ if (dir == DIR_RIGHT_TO_LEFT)
+ trailing = !trailing;
+ }
+
+ int here = 0;
+ for (int i = 0; i < directions.mDirections.length; i++) {
+ if (alt)
+ trailing = !trailing;
+
+ int there = here + directions.mDirections[i];
+ if (there > end - start)
+ there = end - start;
+
+ int segstart = here;
+ for (int j = hasTabs ? here : there; j <= there; j++) {
+ if (j == there || buf[j] == '\t') {
+ float segw;
+
+ if (offset < start + j ||
+ (trailing && offset <= start + j)) {
+ if (dir == DIR_LEFT_TO_RIGHT && (i & 1) == 0) {
+ h += Styled.measureText(paint, workPaint, text,
+ start + segstart, offset,
+ null);
+ return h;
+ }
+
+ if (dir == DIR_RIGHT_TO_LEFT && (i & 1) != 0) {
+ h -= Styled.measureText(paint, workPaint, text,
+ start + segstart, offset,
+ null);
+ return h;
+ }
+ }
+
+ segw = Styled.measureText(paint, workPaint, text,
+ start + segstart, start + j,
+ null);
+
+ if (offset < start + j ||
+ (trailing && offset <= start + j)) {
+ if (dir == DIR_LEFT_TO_RIGHT) {
+ h += segw - Styled.measureText(paint, workPaint,
+ text,
+ start + segstart,
+ offset, null);
+ return h;
+ }
+
+ if (dir == DIR_RIGHT_TO_LEFT) {
+ h -= segw - Styled.measureText(paint, workPaint,
+ text,
+ start + segstart,
+ offset, null);
+ return h;
+ }
+ }
+
+ if (dir == DIR_RIGHT_TO_LEFT)
+ h -= segw;
+ else
+ h += segw;
+
+ if (j != there && buf[j] == '\t') {
+ if (offset == start + j)
+ return h;
+
+ h = dir * nextTab(text, start, end, h * dir, tabs);
+ }
+
+ segstart = j + 1;
+ }
+ }
+
+ here = there;
+ }
+
+ if (hasTabs)
+ TextUtils.recycle(buf);
+
+ return h;
+ }
+
+ /* package */ static float measureText(TextPaint paint,
+ TextPaint workPaint,
+ CharSequence text,
+ int start, int end,
+ Paint.FontMetricsInt fm,
+ boolean hasTabs, Object[] tabs) {
+ char[] buf = null;
+
+ if (hasTabs) {
+ buf = TextUtils.obtain(end - start);
+ TextUtils.getChars(text, start, end, buf, 0);
+ }
+
+ int len = end - start;
+
+ int here = 0;
+ float h = 0;
+ int ab = 0, be = 0;
+ int top = 0, bot = 0;
+
+ if (fm != null) {
+ fm.ascent = 0;
+ fm.descent = 0;
+ }
+
+ for (int i = hasTabs ? 0 : len; i <= len; i++) {
+ if (i == len || buf[i] == '\t') {
+ workPaint.baselineShift = 0;
+
+ h += Styled.measureText(paint, workPaint, text,
+ start + here, start + i,
+ fm);
+
+ if (fm != null) {
+ if (workPaint.baselineShift < 0) {
+ fm.ascent += workPaint.baselineShift;
+ fm.top += workPaint.baselineShift;
+ } else {
+ fm.descent += workPaint.baselineShift;
+ fm.bottom += workPaint.baselineShift;
+ }
+ }
+
+ if (i != len)
+ h = nextTab(text, start, end, h, tabs);
+
+ if (fm != null) {
+ if (fm.ascent < ab) {
+ ab = fm.ascent;
+ }
+ if (fm.descent > be) {
+ be = fm.descent;
+ }
+
+ if (fm.top < top) {
+ top = fm.top;
+ }
+ if (fm.bottom > bot) {
+ bot = fm.bottom;
+ }
+ }
+
+ here = i + 1;
+ }
+ }
+
+ if (fm != null) {
+ fm.ascent = ab;
+ fm.descent = be;
+ fm.top = top;
+ fm.bottom = bot;
+ }
+
+ if (hasTabs)
+ TextUtils.recycle(buf);
+
+ return h;
+ }
+
+ /* package */ static float nextTab(CharSequence text, int start, int end,
+ float h, Object[] tabs) {
+ float nh = Float.MAX_VALUE;
+ boolean alltabs = false;
+
+ if (text instanceof Spanned) {
+ if (tabs == null) {
+ tabs = ((Spanned) text).getSpans(start, end, TabStopSpan.class);
+ alltabs = true;
+ }
+
+ for (int i = 0; i < tabs.length; i++) {
+ if (!alltabs) {
+ if (!(tabs[i] instanceof TabStopSpan))
+ continue;
+ }
+
+ int where = ((TabStopSpan) tabs[i]).getTabStop();
+
+ if (where < nh && where > h)
+ nh = where;
+ }
+
+ if (nh != Float.MAX_VALUE)
+ return nh;
+ }
+
+ return ((int) ((h + TAB_INCREMENT) / TAB_INCREMENT)) * TAB_INCREMENT;
+ }
+
+ protected final boolean isSpanned() {
+ return mSpannedText;
+ }
+
+ private void ellipsize(int start, int end, int line,
+ char[] dest, int destoff) {
+ int ellipsisCount = getEllipsisCount(line);
+
+ if (ellipsisCount == 0) {
+ return;
+ }
+
+ int ellipsisStart = getEllipsisStart(line);
+ int linestart = getLineStart(line);
+
+ for (int i = ellipsisStart; i < ellipsisStart + ellipsisCount; i++) {
+ char c;
+
+ if (i == ellipsisStart) {
+ c = '\u2026'; // ellipsis
+ } else {
+ c = '\uFEFF'; // 0-width space
+ }
+
+ int a = i + linestart;
+
+ if (a >= start && a < end) {
+ dest[destoff + a - start] = c;
+ }
+ }
+ }
+
+ /**
+ * Stores information about bidirectional (left-to-right or right-to-left)
+ * text within the layout of a line. TODO: This work is not complete
+ * or correct and will be fleshed out in a later revision.
+ */
+ public static class Directions {
+ private short[] mDirections;
+
+ /* package */ Directions(short[] dirs) {
+ mDirections = dirs;
+ }
+ }
+
+ /**
+ * Return the offset of the first character to be ellipsized away,
+ * relative to the start of the line. (So 0 if the beginning of the
+ * line is ellipsized, not getLineStart().)
+ */
+ public abstract int getEllipsisStart(int line);
+ /**
+ * Returns the number of characters to be ellipsized away, or 0 if
+ * no ellipsis is to take place.
+ */
+ public abstract int getEllipsisCount(int line);
+
+ /* package */ static class Ellipsizer implements CharSequence, GetChars {
+ /* package */ CharSequence mText;
+ /* package */ Layout mLayout;
+ /* package */ int mWidth;
+ /* package */ TextUtils.TruncateAt mMethod;
+
+ public Ellipsizer(CharSequence s) {
+ mText = s;
+ }
+
+ public char charAt(int off) {
+ char[] buf = TextUtils.obtain(1);
+ getChars(off, off + 1, buf, 0);
+ char ret = buf[0];
+
+ TextUtils.recycle(buf);
+ return ret;
+ }
+
+ public void getChars(int start, int end, char[] dest, int destoff) {
+ int line1 = mLayout.getLineForOffset(start);
+ int line2 = mLayout.getLineForOffset(end);
+
+ TextUtils.getChars(mText, start, end, dest, destoff);
+
+ for (int i = line1; i <= line2; i++) {
+ mLayout.ellipsize(start, end, i, dest, destoff);
+ }
+ }
+
+ public int length() {
+ return mText.length();
+ }
+
+ public CharSequence subSequence(int start, int end) {
+ char[] s = new char[end - start];
+ getChars(start, end, s, 0);
+ return new String(s);
+ }
+
+ public String toString() {
+ char[] s = new char[length()];
+ getChars(0, length(), s, 0);
+ return new String(s);
+ }
+
+ }
+
+ /* package */ static class SpannedEllipsizer
+ extends Ellipsizer implements Spanned {
+ private Spanned mSpanned;
+
+ public SpannedEllipsizer(CharSequence display) {
+ super(display);
+ mSpanned = (Spanned) display;
+ }
+
+ public <T> T[] getSpans(int start, int end, Class<T> type) {
+ return mSpanned.getSpans(start, end, type);
+ }
+
+ public int getSpanStart(Object tag) {
+ return mSpanned.getSpanStart(tag);
+ }
+
+ public int getSpanEnd(Object tag) {
+ return mSpanned.getSpanEnd(tag);
+ }
+
+ public int getSpanFlags(Object tag) {
+ return mSpanned.getSpanFlags(tag);
+ }
+
+ public int nextSpanTransition(int start, int limit, Class type) {
+ return mSpanned.nextSpanTransition(start, limit, type);
+ }
+
+ public CharSequence subSequence(int start, int end) {
+ char[] s = new char[end - start];
+ getChars(start, end, s, 0);
+
+ SpannableString ss = new SpannableString(new String(s));
+ TextUtils.copySpansFrom(mSpanned, start, end, Object.class, ss, 0);
+ return ss;
+ }
+ }
+
+ private CharSequence mText;
+ private TextPaint mPaint;
+ /* package */ TextPaint mWorkPaint;
+ private int mWidth;
+ private Alignment mAlignment = Alignment.ALIGN_NORMAL;
+ private float mSpacingMult;
+ private float mSpacingAdd;
+ private static Rect sTempRect = new Rect();
+ private boolean mSpannedText;
+
+ public static final int DIR_LEFT_TO_RIGHT = 1;
+ public static final int DIR_RIGHT_TO_LEFT = -1;
+
+ public enum Alignment {
+ ALIGN_NORMAL,
+ ALIGN_OPPOSITE,
+ ALIGN_CENTER,
+ // XXX ALIGN_LEFT,
+ // XXX ALIGN_RIGHT,
+ }
+
+ private static final int TAB_INCREMENT = 20;
+
+ /* package */ static final Directions DIRS_ALL_LEFT_TO_RIGHT =
+ new Directions(new short[] { 32767 });
+ /* package */ static final Directions DIRS_ALL_RIGHT_TO_LEFT =
+ new Directions(new short[] { 0, 32767 });
+
+}
+
diff --git a/core/java/android/text/LoginFilter.java b/core/java/android/text/LoginFilter.java
new file mode 100644
index 0000000..dd2d77f
--- /dev/null
+++ b/core/java/android/text/LoginFilter.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text;
+
+/**
+ * Abstract class for filtering login-related text (user names and passwords)
+ *
+ */
+public abstract class LoginFilter implements InputFilter {
+ private boolean mAppendInvalid; // whether to append or ignore invalid characters
+ /**
+ * Base constructor for LoginFilter
+ * @param appendInvalid whether or not to append invalid characters.
+ */
+ LoginFilter(boolean appendInvalid) {
+ mAppendInvalid = appendInvalid;
+ }
+
+ /**
+ * Default constructor for LoginFilter doesn't append invalid characters.
+ */
+ LoginFilter() {
+ mAppendInvalid = false;
+ }
+
+ /**
+ * This method is called when the buffer is going to replace the
+ * range <code>dstart &hellip; dend</code> of <code>dest</code>
+ * with the new text from the range <code>start &hellip; end</code>
+ * of <code>source</code>. Returns the CharSequence that we want
+ * placed there instead, including an empty string
+ * if appropriate, or <code>null</code> to accept the original
+ * replacement. Be careful to not to reject 0-length replacements,
+ * as this is what happens when you delete text.
+ */
+ public CharSequence filter(CharSequence source, int start, int end,
+ Spanned dest, int dstart, int dend) {
+ char[] out = new char[end - start]; // reserve enough space for whole string
+ int outidx = 0;
+ boolean changed = false;
+
+ onStart();
+
+ // Scan through beginning characters in dest, calling onInvalidCharacter()
+ // for each invalid character.
+ for (int i = 0; i < dstart; i++) {
+ char c = dest.charAt(i);
+ if (!isAllowed(c)) onInvalidCharacter(c);
+ }
+
+ // Scan through changed characters rejecting disallowed chars
+ for (int i = start; i < end; i++) {
+ char c = source.charAt(i);
+ if (isAllowed(c)) {
+ // Character allowed. Add it to the sequence.
+ out[outidx++] = c;
+ } else {
+ if (mAppendInvalid) out[outidx++] = c;
+ else changed = true; // we changed the original string
+ onInvalidCharacter(c);
+ }
+ }
+
+ // Scan through remaining characters in dest, calling onInvalidCharacter()
+ // for each invalid character.
+ for (int i = dend; i < dest.length(); i++) {
+ char c = dest.charAt(i);
+ if (!isAllowed(c)) onInvalidCharacter(c);
+ }
+
+ onStop();
+
+ return changed ? new String(out, 0, outidx) : null;
+ }
+
+ /**
+ * Called when we start processing filter.
+ */
+ public void onStart() {
+
+ }
+
+ /**
+ * Called whenever we encounter an invalid character.
+ * @param c the invalid character
+ */
+ public void onInvalidCharacter(char c) {
+
+ }
+
+ /**
+ * Called when we're done processing filter
+ */
+ public void onStop() {
+
+ }
+
+ /**
+ * Returns whether or not we allow character c.
+ * Subclasses must override this method.
+ */
+ public abstract boolean isAllowed(char c);
+
+ /**
+ * This filter rejects characters in the user name that are not compatible with GMail
+ * account creation. It prevents the user from entering user names with characters other than
+ * [a-zA-Z0-9.].
+ *
+ */
+ public static class UsernameFilterGMail extends LoginFilter {
+
+ public UsernameFilterGMail() {
+ super(false);
+ }
+
+ public UsernameFilterGMail(boolean appendInvalid) {
+ super(appendInvalid);
+ }
+
+ @Override
+ public boolean isAllowed(char c) {
+ // Allow [a-zA-Z0-9@.]
+ if ('0' <= c && c <= '9')
+ return true;
+ if ('a' <= c && c <= 'z')
+ return true;
+ if ('A' <= c && c <= 'Z')
+ return true;
+ if ('.' == c)
+ return true;
+ return false;
+ }
+ }
+
+ /**
+ * This filter rejects characters in the user name that are not compatible with Google login.
+ * It is slightly less restrictive than the above filter in that it allows [a-zA-Z0-9._-].
+ *
+ */
+ public static class UsernameFilterGeneric extends LoginFilter {
+ private static final String mAllowed = "@_-."; // Additional characters
+
+ public UsernameFilterGeneric() {
+ super(false);
+ }
+
+ public UsernameFilterGeneric(boolean appendInvalid) {
+ super(appendInvalid);
+ }
+
+ @Override
+ public boolean isAllowed(char c) {
+ // Allow [a-zA-Z0-9@.]
+ if ('0' <= c && c <= '9')
+ return true;
+ if ('a' <= c && c <= 'z')
+ return true;
+ if ('A' <= c && c <= 'Z')
+ return true;
+ if (mAllowed.indexOf(c) != -1)
+ return true;
+ return false;
+ }
+ }
+
+ /**
+ * This filter is compatible with GMail passwords which restricts characters to
+ * the Latin-1 (ISO8859-1) char set.
+ *
+ */
+ public static class PasswordFilterGMail extends LoginFilter {
+
+ public PasswordFilterGMail() {
+ super(false);
+ }
+
+ public PasswordFilterGMail(boolean appendInvalid) {
+ super(appendInvalid);
+ }
+
+ // We should reject anything not in the Latin-1 (ISO8859-1) charset
+ @Override
+ public boolean isAllowed(char c) {
+ if (32 <= c && c <= 127)
+ return true; // standard charset
+ // if (128 <= c && c <= 159) return true; // nonstandard (Windows(TM)(R)) charset
+ if (160 <= c && c <= 255)
+ return true; // extended charset
+ return false;
+ }
+ }
+}
diff --git a/core/java/android/text/PackedIntVector.java b/core/java/android/text/PackedIntVector.java
new file mode 100644
index 0000000..d87f600
--- /dev/null
+++ b/core/java/android/text/PackedIntVector.java
@@ -0,0 +1,368 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text;
+
+import com.android.internal.util.ArrayUtils;
+
+
+/**
+ * PackedIntVector stores a two-dimensional array of integers,
+ * optimized for inserting and deleting rows and for
+ * offsetting the values in segments of a given column.
+ */
+class PackedIntVector {
+ private final int mColumns;
+ private int mRows;
+
+ private int mRowGapStart;
+ private int mRowGapLength;
+
+ private int[] mValues;
+ private int[] mValueGap; // starts, followed by lengths
+
+ /**
+ * Creates a new PackedIntVector with the specified width and
+ * a height of 0.
+ *
+ * @param columns the width of the PackedIntVector.
+ */
+ public PackedIntVector(int columns) {
+ mColumns = columns;
+ mRows = 0;
+
+ mRowGapStart = 0;
+ mRowGapLength = mRows;
+
+ mValues = null;
+ mValueGap = new int[2 * columns];
+ }
+
+ /**
+ * Returns the value at the specified row and column.
+ *
+ * @param row the index of the row to return.
+ * @param column the index of the column to return.
+ *
+ * @return the value stored at the specified position.
+ *
+ * @throws IndexOutOfBoundsException if the row is out of range
+ * (row &lt; 0 || row >= size()) or the column is out of range
+ * (column &lt; 0 || column >= width()).
+ */
+ public int getValue(int row, int column) {
+ final int columns = mColumns;
+
+ if (((row | column) < 0) || (row >= size()) || (column >= columns)) {
+ throw new IndexOutOfBoundsException(row + ", " + column);
+ }
+
+ if (row >= mRowGapStart) {
+ row += mRowGapLength;
+ }
+
+ int value = mValues[row * columns + column];
+
+ int[] valuegap = mValueGap;
+ if (row >= valuegap[column]) {
+ value += valuegap[column + columns];
+ }
+
+ return value;
+ }
+
+ /**
+ * Sets the value at the specified row and column.
+ *
+ * @param row the index of the row to set.
+ * @param column the index of the column to set.
+ *
+ * @throws IndexOutOfBoundsException if the row is out of range
+ * (row &lt; 0 || row >= size()) or the column is out of range
+ * (column &lt; 0 || column >= width()).
+ */
+ public void setValue(int row, int column, int value) {
+ if (((row | column) < 0) || (row >= size()) || (column >= mColumns)) {
+ throw new IndexOutOfBoundsException(row + ", " + column);
+ }
+
+ if (row >= mRowGapStart) {
+ row += mRowGapLength;
+ }
+
+ int[] valuegap = mValueGap;
+ if (row >= valuegap[column]) {
+ value -= valuegap[column + mColumns];
+ }
+
+ mValues[row * mColumns + column] = value;
+ }
+
+ /**
+ * Sets the value at the specified row and column.
+ * Private internal version: does not check args.
+ *
+ * @param row the index of the row to set.
+ * @param column the index of the column to set.
+ *
+ */
+ private void setValueInternal(int row, int column, int value) {
+ if (row >= mRowGapStart) {
+ row += mRowGapLength;
+ }
+
+ int[] valuegap = mValueGap;
+ if (row >= valuegap[column]) {
+ value -= valuegap[column + mColumns];
+ }
+
+ mValues[row * mColumns + column] = value;
+ }
+
+
+ /**
+ * Increments all values in the specified column whose row >= the
+ * specified row by the specified delta.
+ *
+ * @param startRow the row at which to begin incrementing.
+ * This may be == size(), which case there is no effect.
+ * @param column the index of the column to set.
+ *
+ * @throws IndexOutOfBoundsException if the row is out of range
+ * (startRow &lt; 0 || startRow > size()) or the column
+ * is out of range (column &lt; 0 || column >= width()).
+ */
+ public void adjustValuesBelow(int startRow, int column, int delta) {
+ if (((startRow | column) < 0) || (startRow > size()) ||
+ (column >= width())) {
+ throw new IndexOutOfBoundsException(startRow + ", " + column);
+ }
+
+ if (startRow >= mRowGapStart) {
+ startRow += mRowGapLength;
+ }
+
+ moveValueGapTo(column, startRow);
+ mValueGap[column + mColumns] += delta;
+ }
+
+ /**
+ * Inserts a new row of values at the specified row offset.
+ *
+ * @param row the row above which to insert the new row.
+ * This may be == size(), which case the new row is added
+ * at the end.
+ * @param values the new values to be added. If this is null,
+ * a row of zeroes is added.
+ *
+ * @throws IndexOutOfBoundsException if the row is out of range
+ * (row &lt; 0 || row > size()) or if the length of the
+ * values array is too small (values.length < width()).
+ */
+ public void insertAt(int row, int[] values) {
+ if ((row < 0) || (row > size())) {
+ throw new IndexOutOfBoundsException("row " + row);
+ }
+
+ if ((values != null) && (values.length < width())) {
+ throw new IndexOutOfBoundsException("value count " + values.length);
+ }
+
+ moveRowGapTo(row);
+
+ if (mRowGapLength == 0) {
+ growBuffer();
+ }
+
+ mRowGapStart++;
+ mRowGapLength--;
+
+ if (values == null) {
+ for (int i = mColumns - 1; i >= 0; i--) {
+ setValueInternal(row, i, 0);
+ }
+ } else {
+ for (int i = mColumns - 1; i >= 0; i--) {
+ setValueInternal(row, i, values[i]);
+ }
+ }
+ }
+
+ /**
+ * Deletes the specified number of rows starting with the specified
+ * row.
+ *
+ * @param row the index of the first row to be deleted.
+ * @param count the number of rows to delete.
+ *
+ * @throws IndexOutOfBoundsException if any of the rows to be deleted
+ * are out of range (row &lt; 0 || count &lt; 0 ||
+ * row + count > size()).
+ */
+ public void deleteAt(int row, int count) {
+ if (((row | count) < 0) || (row + count > size())) {
+ throw new IndexOutOfBoundsException(row + ", " + count);
+ }
+
+ moveRowGapTo(row + count);
+
+ mRowGapStart -= count;
+ mRowGapLength += count;
+
+ // TODO: Reclaim memory when the new height is much smaller
+ // than the allocated size.
+ }
+
+ /**
+ * Returns the number of rows in the PackedIntVector. This number
+ * will change as rows are inserted and deleted.
+ *
+ * @return the number of rows.
+ */
+ public int size() {
+ return mRows - mRowGapLength;
+ }
+
+ /**
+ * Returns the width of the PackedIntVector. This number is set
+ * at construction and will not change.
+ *
+ * @return the number of columns.
+ */
+ public int width() {
+ return mColumns;
+ }
+
+ /**
+ * Grows the value and gap arrays to be large enough to store at least
+ * one more than the current number of rows.
+ */
+ private final void growBuffer() {
+ final int columns = mColumns;
+ int newsize = size() + 1;
+ newsize = ArrayUtils.idealIntArraySize(newsize * columns) / columns;
+ int[] newvalues = new int[newsize * columns];
+
+ final int[] valuegap = mValueGap;
+ final int rowgapstart = mRowGapStart;
+
+ int after = mRows - (rowgapstart + mRowGapLength);
+
+ if (mValues != null) {
+ System.arraycopy(mValues, 0, newvalues, 0, columns * rowgapstart);
+ System.arraycopy(mValues, (mRows - after) * columns,
+ newvalues, (newsize - after) * columns,
+ after * columns);
+ }
+
+ for (int i = 0; i < columns; i++) {
+ if (valuegap[i] >= rowgapstart) {
+ valuegap[i] += newsize - mRows;
+
+ if (valuegap[i] < rowgapstart) {
+ valuegap[i] = rowgapstart;
+ }
+ }
+ }
+
+ mRowGapLength += newsize - mRows;
+ mRows = newsize;
+ mValues = newvalues;
+ }
+
+ /**
+ * Moves the gap in the values of the specified column to begin at
+ * the specified row.
+ */
+ private final void moveValueGapTo(int column, int where) {
+ final int[] valuegap = mValueGap;
+ final int[] values = mValues;
+ final int columns = mColumns;
+
+ if (where == valuegap[column]) {
+ return;
+ } else if (where > valuegap[column]) {
+ for (int i = valuegap[column]; i < where; i++) {
+ values[i * columns + column] += valuegap[column + columns];
+ }
+ } else /* where < valuegap[column] */ {
+ for (int i = where; i < valuegap[column]; i++) {
+ values[i * columns + column] -= valuegap[column + columns];
+ }
+ }
+
+ valuegap[column] = where;
+ }
+
+ /**
+ * Moves the gap in the row indices to begin at the specified row.
+ */
+ private final void moveRowGapTo(int where) {
+ if (where == mRowGapStart) {
+ return;
+ } else if (where > mRowGapStart) {
+ int moving = where + mRowGapLength - (mRowGapStart + mRowGapLength);
+ final int columns = mColumns;
+ final int[] valuegap = mValueGap;
+ final int[] values = mValues;
+ final int gapend = mRowGapStart + mRowGapLength;
+
+ for (int i = gapend; i < gapend + moving; i++) {
+ int destrow = i - gapend + mRowGapStart;
+
+ for (int j = 0; j < columns; j++) {
+ int val = values[i * columns+ j];
+
+ if (i >= valuegap[j]) {
+ val += valuegap[j + columns];
+ }
+
+ if (destrow >= valuegap[j]) {
+ val -= valuegap[j + columns];
+ }
+
+ values[destrow * columns + j] = val;
+ }
+ }
+ } else /* where < mRowGapStart */ {
+ int moving = mRowGapStart - where;
+ final int columns = mColumns;
+ final int[] valuegap = mValueGap;
+ final int[] values = mValues;
+ final int gapend = mRowGapStart + mRowGapLength;
+
+ for (int i = where + moving - 1; i >= where; i--) {
+ int destrow = i - where + gapend - moving;
+
+ for (int j = 0; j < columns; j++) {
+ int val = values[i * columns+ j];
+
+ if (i >= valuegap[j]) {
+ val += valuegap[j + columns];
+ }
+
+ if (destrow >= valuegap[j]) {
+ val -= valuegap[j + columns];
+ }
+
+ values[destrow * columns + j] = val;
+ }
+ }
+ }
+
+ mRowGapStart = where;
+ }
+}
diff --git a/core/java/android/text/PackedObjectVector.java b/core/java/android/text/PackedObjectVector.java
new file mode 100644
index 0000000..a29df09
--- /dev/null
+++ b/core/java/android/text/PackedObjectVector.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text;
+
+import com.android.internal.util.ArrayUtils;
+
+class PackedObjectVector<E>
+{
+ private int mColumns;
+ private int mRows;
+
+ private int mRowGapStart;
+ private int mRowGapLength;
+
+ private Object[] mValues;
+
+ public
+ PackedObjectVector(int columns)
+ {
+ mColumns = columns;
+ mRows = ArrayUtils.idealIntArraySize(0) / mColumns;
+
+ mRowGapStart = 0;
+ mRowGapLength = mRows;
+
+ mValues = new Object[mRows * mColumns];
+ }
+
+ public E
+ getValue(int row, int column)
+ {
+ if (row >= mRowGapStart)
+ row += mRowGapLength;
+
+ Object value = mValues[row * mColumns + column];
+
+ return (E) value;
+ }
+
+ public void
+ setValue(int row, int column, E value)
+ {
+ if (row >= mRowGapStart)
+ row += mRowGapLength;
+
+ mValues[row * mColumns + column] = value;
+ }
+
+ public void
+ insertAt(int row, E[] values)
+ {
+ moveRowGapTo(row);
+
+ if (mRowGapLength == 0)
+ growBuffer();
+
+ mRowGapStart++;
+ mRowGapLength--;
+
+ if (values == null)
+ for (int i = 0; i < mColumns; i++)
+ setValue(row, i, null);
+ else
+ for (int i = 0; i < mColumns; i++)
+ setValue(row, i, values[i]);
+ }
+
+ public void
+ deleteAt(int row, int count)
+ {
+ moveRowGapTo(row + count);
+
+ mRowGapStart -= count;
+ mRowGapLength += count;
+
+ if (mRowGapLength > size() * 2)
+ {
+ // dump();
+ // growBuffer();
+ }
+ }
+
+ public int
+ size()
+ {
+ return mRows - mRowGapLength;
+ }
+
+ public int
+ width()
+ {
+ return mColumns;
+ }
+
+ private void
+ growBuffer()
+ {
+ int newsize = size() + 1;
+ newsize = ArrayUtils.idealIntArraySize(newsize * mColumns) / mColumns;
+ Object[] newvalues = new Object[newsize * mColumns];
+
+ int after = mRows - (mRowGapStart + mRowGapLength);
+
+ System.arraycopy(mValues, 0, newvalues, 0, mColumns * mRowGapStart);
+ System.arraycopy(mValues, (mRows - after) * mColumns, newvalues, (newsize - after) * mColumns, after * mColumns);
+
+ mRowGapLength += newsize - mRows;
+ mRows = newsize;
+ mValues = newvalues;
+ }
+
+ private void
+ moveRowGapTo(int where)
+ {
+ if (where == mRowGapStart)
+ return;
+
+ if (where > mRowGapStart)
+ {
+ int moving = where + mRowGapLength - (mRowGapStart + mRowGapLength);
+
+ for (int i = mRowGapStart + mRowGapLength; i < mRowGapStart + mRowGapLength + moving; i++)
+ {
+ int destrow = i - (mRowGapStart + mRowGapLength) + mRowGapStart;
+
+ for (int j = 0; j < mColumns; j++)
+ {
+ Object val = mValues[i * mColumns + j];
+
+ mValues[destrow * mColumns + j] = val;
+ }
+ }
+ }
+ else /* where < mRowGapStart */
+ {
+ int moving = mRowGapStart - where;
+
+ for (int i = where + moving - 1; i >= where; i--)
+ {
+ int destrow = i - where + mRowGapStart + mRowGapLength - moving;
+
+ for (int j = 0; j < mColumns; j++)
+ {
+ Object val = mValues[i * mColumns + j];
+
+ mValues[destrow * mColumns + j] = val;
+ }
+ }
+ }
+
+ mRowGapStart = where;
+ }
+
+ public void // XXX
+ dump()
+ {
+ for (int i = 0; i < mRows; i++)
+ {
+ for (int j = 0; j < mColumns; j++)
+ {
+ Object val = mValues[i * mColumns + j];
+
+ if (i < mRowGapStart || i >= mRowGapStart + mRowGapLength)
+ System.out.print(val + " ");
+ else
+ System.out.print("(" + val + ") ");
+ }
+
+ System.out.print(" << \n");
+ }
+
+ System.out.print("-----\n\n");
+ }
+}
diff --git a/core/java/android/text/Selection.java b/core/java/android/text/Selection.java
new file mode 100644
index 0000000..0f4916a
--- /dev/null
+++ b/core/java/android/text/Selection.java
@@ -0,0 +1,426 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text;
+
+
+/**
+ * Utility class for manipulating cursors and selections in CharSequences.
+ * A cursor is a selection where the start and end are at the same offset.
+ */
+public class Selection {
+ private Selection() { /* cannot be instantiated */ }
+
+ /*
+ * Retrieving the selection
+ */
+
+ /**
+ * Return the offset of the selection anchor or cursor, or -1 if
+ * there is no selection or cursor.
+ */
+ public static final int getSelectionStart(CharSequence text) {
+ if (text instanceof Spanned)
+ return ((Spanned) text).getSpanStart(SELECTION_START);
+ else
+ return -1;
+ }
+
+ /**
+ * Return the offset of the selection edge or cursor, or -1 if
+ * there is no selection or cursor.
+ */
+ public static final int getSelectionEnd(CharSequence text) {
+ if (text instanceof Spanned)
+ return ((Spanned) text).getSpanStart(SELECTION_END);
+ else
+ return -1;
+ }
+
+ /*
+ * Setting the selection
+ */
+
+ // private static int pin(int value, int min, int max) {
+ // return value < min ? 0 : (value > max ? max : value);
+ // }
+
+ /**
+ * Set the selection anchor to <code>start</code> and the selection edge
+ * to <code>stop</code>.
+ */
+ public static void setSelection(Spannable text, int start, int stop) {
+ // int len = text.length();
+ // start = pin(start, 0, len); XXX remove unless we really need it
+ // stop = pin(stop, 0, len);
+
+ int ostart = getSelectionStart(text);
+ int oend = getSelectionEnd(text);
+
+ if (ostart != start || oend != stop) {
+ text.setSpan(SELECTION_START, start, start,
+ Spanned.SPAN_POINT_POINT);
+ text.setSpan(SELECTION_END, stop, stop,
+ Spanned.SPAN_POINT_POINT);
+ }
+ }
+
+ /**
+ * Move the cursor to offset <code>index</code>.
+ */
+ public static final void setSelection(Spannable text, int index) {
+ setSelection(text, index, index);
+ }
+
+ /**
+ * Select the entire text.
+ */
+ public static final void selectAll(Spannable text) {
+ setSelection(text, 0, text.length());
+ }
+
+ /**
+ * Move the selection edge to offset <code>index</code>.
+ */
+ public static final void extendSelection(Spannable text, int index) {
+ if (text.getSpanStart(SELECTION_END) != index)
+ text.setSpan(SELECTION_END, index, index, Spanned.SPAN_POINT_POINT);
+ }
+
+ /**
+ * Remove the selection or cursor, if any, from the text.
+ */
+ public static final void removeSelection(Spannable text) {
+ text.removeSpan(SELECTION_START);
+ text.removeSpan(SELECTION_END);
+ }
+
+ /*
+ * Moving the selection within the layout
+ */
+
+ /**
+ * Move the cursor to the buffer offset physically above the current
+ * offset, or return false if the cursor is already on the top line.
+ */
+ public static boolean moveUp(Spannable text, Layout layout) {
+ int start = getSelectionStart(text);
+ int end = getSelectionEnd(text);
+
+ if (start != end) {
+ int min = Math.min(start, end);
+ int max = Math.max(start, end);
+
+ setSelection(text, min);
+
+ if (min == 0 && max == text.length()) {
+ return false;
+ }
+
+ return true;
+ } else {
+ int line = layout.getLineForOffset(end);
+
+ if (line > 0) {
+ int move;
+
+ if (layout.getParagraphDirection(line) ==
+ layout.getParagraphDirection(line - 1)) {
+ float h = layout.getPrimaryHorizontal(end);
+ move = layout.getOffsetForHorizontal(line - 1, h);
+ } else {
+ move = layout.getLineStart(line - 1);
+ }
+
+ setSelection(text, move);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Move the cursor to the buffer offset physically below the current
+ * offset, or return false if the cursor is already on the bottom line.
+ */
+ public static boolean moveDown(Spannable text, Layout layout) {
+ int start = getSelectionStart(text);
+ int end = getSelectionEnd(text);
+
+ if (start != end) {
+ int min = Math.min(start, end);
+ int max = Math.max(start, end);
+
+ setSelection(text, max);
+
+ if (min == 0 && max == text.length()) {
+ return false;
+ }
+
+ return true;
+ } else {
+ int line = layout.getLineForOffset(end);
+
+ if (line < layout.getLineCount() - 1) {
+ int move;
+
+ if (layout.getParagraphDirection(line) ==
+ layout.getParagraphDirection(line + 1)) {
+ float h = layout.getPrimaryHorizontal(end);
+ move = layout.getOffsetForHorizontal(line + 1, h);
+ } else {
+ move = layout.getLineStart(line + 1);
+ }
+
+ setSelection(text, move);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Move the cursor to the buffer offset physically to the left of
+ * the current offset, or return false if the cursor is already
+ * at the left edge of the line and there is not another line to move it to.
+ */
+ public static boolean moveLeft(Spannable text, Layout layout) {
+ int start = getSelectionStart(text);
+ int end = getSelectionEnd(text);
+
+ if (start != end) {
+ setSelection(text, chooseHorizontal(layout, -1, start, end));
+ return true;
+ } else {
+ int to = layout.getOffsetToLeftOf(end);
+
+ if (to != end) {
+ setSelection(text, to);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Move the cursor to the buffer offset physically to the right of
+ * the current offset, or return false if the cursor is already at
+ * at the right edge of the line and there is not another line
+ * to move it to.
+ */
+ public static boolean moveRight(Spannable text, Layout layout) {
+ int start = getSelectionStart(text);
+ int end = getSelectionEnd(text);
+
+ if (start != end) {
+ setSelection(text, chooseHorizontal(layout, 1, start, end));
+ return true;
+ } else {
+ int to = layout.getOffsetToRightOf(end);
+
+ if (to != end) {
+ setSelection(text, to);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Move the selection end to the buffer offset physically above
+ * the current selection end.
+ */
+ public static boolean extendUp(Spannable text, Layout layout) {
+ int end = getSelectionEnd(text);
+ int line = layout.getLineForOffset(end);
+
+ if (line > 0) {
+ int move;
+
+ if (layout.getParagraphDirection(line) ==
+ layout.getParagraphDirection(line - 1)) {
+ float h = layout.getPrimaryHorizontal(end);
+ move = layout.getOffsetForHorizontal(line - 1, h);
+ } else {
+ move = layout.getLineStart(line - 1);
+ }
+
+ extendSelection(text, move);
+ return true;
+ } else if (end != 0) {
+ extendSelection(text, 0);
+ return true;
+ }
+
+ return true;
+ }
+
+ /**
+ * Move the selection end to the buffer offset physically below
+ * the current selection end.
+ */
+ public static boolean extendDown(Spannable text, Layout layout) {
+ int end = getSelectionEnd(text);
+ int line = layout.getLineForOffset(end);
+
+ if (line < layout.getLineCount() - 1) {
+ int move;
+
+ if (layout.getParagraphDirection(line) ==
+ layout.getParagraphDirection(line + 1)) {
+ float h = layout.getPrimaryHorizontal(end);
+ move = layout.getOffsetForHorizontal(line + 1, h);
+ } else {
+ move = layout.getLineStart(line + 1);
+ }
+
+ extendSelection(text, move);
+ return true;
+ } else if (end != text.length()) {
+ extendSelection(text, text.length());
+ return true;
+ }
+
+ return true;
+ }
+
+ /**
+ * Move the selection end to the buffer offset physically to the left of
+ * the current selection end.
+ */
+ public static boolean extendLeft(Spannable text, Layout layout) {
+ int end = getSelectionEnd(text);
+ int to = layout.getOffsetToLeftOf(end);
+
+ if (to != end) {
+ extendSelection(text, to);
+ return true;
+ }
+
+ return true;
+ }
+
+ /**
+ * Move the selection end to the buffer offset physically to the right of
+ * the current selection end.
+ */
+ public static boolean extendRight(Spannable text, Layout layout) {
+ int end = getSelectionEnd(text);
+ int to = layout.getOffsetToRightOf(end);
+
+ if (to != end) {
+ extendSelection(text, to);
+ return true;
+ }
+
+ return true;
+ }
+
+ public static boolean extendToLeftEdge(Spannable text, Layout layout) {
+ int where = findEdge(text, layout, -1);
+ extendSelection(text, where);
+ return true;
+ }
+
+ public static boolean extendToRightEdge(Spannable text, Layout layout) {
+ int where = findEdge(text, layout, 1);
+ extendSelection(text, where);
+ return true;
+ }
+
+ public static boolean moveToLeftEdge(Spannable text, Layout layout) {
+ int where = findEdge(text, layout, -1);
+ setSelection(text, where);
+ return true;
+ }
+
+ public static boolean moveToRightEdge(Spannable text, Layout layout) {
+ int where = findEdge(text, layout, 1);
+ setSelection(text, where);
+ return true;
+ }
+
+ private static int findEdge(Spannable text, Layout layout, int dir) {
+ int pt = getSelectionEnd(text);
+ int line = layout.getLineForOffset(pt);
+ int pdir = layout.getParagraphDirection(line);
+
+ if (dir * pdir < 0) {
+ return layout.getLineStart(line);
+ } else {
+ int end = layout.getLineEnd(line);
+
+ if (line == layout.getLineCount() - 1)
+ return end;
+ else
+ return end - 1;
+ }
+ }
+
+ private static int chooseHorizontal(Layout layout, int direction,
+ int off1, int off2) {
+ int line1 = layout.getLineForOffset(off1);
+ int line2 = layout.getLineForOffset(off2);
+
+ if (line1 == line2) {
+ // same line, so it goes by pure physical direction
+
+ float h1 = layout.getPrimaryHorizontal(off1);
+ float h2 = layout.getPrimaryHorizontal(off2);
+
+ if (direction < 0) {
+ // to left
+
+ if (h1 < h2)
+ return off1;
+ else
+ return off2;
+ } else {
+ // to right
+
+ if (h1 > h2)
+ return off1;
+ else
+ return off2;
+ }
+ } else {
+ // different line, so which line is "left" and which is "right"
+ // depends upon the directionality of the text
+
+ // This only checks at one end, but it's not clear what the
+ // right thing to do is if the ends don't agree. Even if it
+ // is wrong it should still not be too bad.
+ int line = layout.getLineForOffset(off1);
+ int textdir = layout.getParagraphDirection(line);
+
+ if (textdir == direction)
+ return Math.max(off1, off2);
+ else
+ return Math.min(off1, off2);
+ }
+ }
+
+ /*
+ * Public constants
+ */
+
+ public static final Object SELECTION_START = new Object();
+ public static final Object SELECTION_END = new Object();
+}
diff --git a/core/java/android/text/SpanWatcher.java b/core/java/android/text/SpanWatcher.java
new file mode 100644
index 0000000..f99882a
--- /dev/null
+++ b/core/java/android/text/SpanWatcher.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text;
+
+/**
+ * When an object of this type is attached to a Spannable, its methods
+ * will be called to notify it that other markup objects have been
+ * added, changed, or removed.
+ */
+public interface SpanWatcher {
+ /**
+ * This method is called to notify you that the specified object
+ * has been attached to the specified range of the text.
+ */
+ public void onSpanAdded(Spannable text, Object what, int start, int end);
+ /**
+ * This method is called to notify you that the specified object
+ * has been detached from the specified range of the text.
+ */
+ public void onSpanRemoved(Spannable text, Object what, int start, int end);
+ /**
+ * This method is called to notify you that the specified object
+ * has been relocated from the range <code>ostart&hellip;oend</code>
+ * to the new range <code>nstart&hellip;nend</code> of the text.
+ */
+ public void onSpanChanged(Spannable text, Object what, int ostart, int oend,
+ int nstart, int nend);
+}
diff --git a/core/java/android/text/Spannable.java b/core/java/android/text/Spannable.java
new file mode 100644
index 0000000..ae5d356
--- /dev/null
+++ b/core/java/android/text/Spannable.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text;
+
+/**
+ * This is the interface for text to which markup objects can be
+ * attached and detached. Not all Spannable classes have mutable text;
+ * see {@link Editable} for that.
+ */
+public interface Spannable
+extends Spanned
+{
+ /**
+ * Attach the specified markup object to the range <code>start&hellip;end</code>
+ * of the text, or move the object to that range if it was already
+ * attached elsewhere. See {@link Spanned} for an explanation of
+ * what the flags mean. The object can be one that has meaning only
+ * within your application, or it can be one that the text system will
+ * use to affect text display or behavior. Some noteworthy ones are
+ * the subclasses of {@link android.text.style.CharacterStyle} and
+ * {@link android.text.style.ParagraphStyle}, and
+ * {@link android.text.TextWatcher} and
+ * {@link android.text.SpanWatcher}.
+ */
+ public void setSpan(Object what, int start, int end, int flags);
+
+ /**
+ * Remove the specified object from the range of text to which it
+ * was attached, if any. It is OK to remove an object that was never
+ * attached in the first place.
+ */
+ public void removeSpan(Object what);
+
+ /**
+ * Factory used by TextView to create new Spannables. You can subclass
+ * it to provide something other than SpannableString.
+ */
+ public static class Factory {
+ private static Spannable.Factory sInstance = new Spannable.Factory();
+
+ /**
+ * Returns the standard Spannable Factory.
+ */
+ public static Spannable.Factory getInstance() {
+ return sInstance;
+ }
+
+ /**
+ * Returns a new SpannableString from the specified CharSequence.
+ * You can override this to provide a different kind of Spannable.
+ */
+ public Spannable newSpannable(CharSequence source) {
+ return new SpannableString(source);
+ }
+ }
+}
diff --git a/core/java/android/text/SpannableString.java b/core/java/android/text/SpannableString.java
new file mode 100644
index 0000000..56d0946
--- /dev/null
+++ b/core/java/android/text/SpannableString.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.text;
+
+
+/**
+ * This is the class for text whose content is immutable but to which
+ * markup objects can be attached and detached.
+ * For mutable text, see {@link SpannableStringBuilder}.
+ */
+public class SpannableString
+extends SpannableStringInternal
+implements CharSequence, GetChars, Spannable
+{
+ public SpannableString(CharSequence source) {
+ super(source, 0, source.length());
+ }
+
+ private SpannableString(CharSequence source, int start, int end) {
+ super(source, start, end);
+ }
+
+ public static SpannableString valueOf(CharSequence source) {
+ if (source instanceof SpannableString) {
+ return (SpannableString) source;
+ } else {
+ return new SpannableString(source);
+ }
+ }
+
+ public void setSpan(Object what, int start, int end, int flags) {
+ super.setSpan(what, start, end, flags);
+ }
+
+ public void removeSpan(Object what) {
+ super.removeSpan(what);
+ }
+
+ public final CharSequence subSequence(int start, int end) {
+ return new SpannableString(this, start, end);
+ }
+}
diff --git a/core/java/android/text/SpannableStringBuilder.java b/core/java/android/text/SpannableStringBuilder.java
new file mode 100644
index 0000000..223ce2f
--- /dev/null
+++ b/core/java/android/text/SpannableStringBuilder.java
@@ -0,0 +1,1136 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text;
+
+import com.android.internal.util.ArrayUtils;
+import android.graphics.Paint;
+import android.graphics.Canvas;
+
+import java.lang.reflect.Array;
+
+/**
+ * This is the class for text whose content and markup can both be changed.
+ */
+public class SpannableStringBuilder
+implements CharSequence, GetChars, Spannable, Editable, Appendable,
+ GraphicsOperations
+{
+ /**
+ * Create a new SpannableStringBuilder with empty contents
+ */
+ public SpannableStringBuilder() {
+ this("");
+ }
+
+ /**
+ * Create a new SpannableStringBuilder containing a copy of the
+ * specified text, including its spans if any.
+ */
+ public SpannableStringBuilder(CharSequence text) {
+ this(text, 0, text.length());
+ }
+
+ /**
+ * Create a new SpannableStringBuilder containing a copy of the
+ * specified slice of the specified text, including its spans if any.
+ */
+ public SpannableStringBuilder(CharSequence text, int start, int end) {
+ int srclen = end - start;
+
+ int len = ArrayUtils.idealCharArraySize(srclen + 1);
+ mText = new char[len];
+ mGapStart = srclen;
+ mGapLength = len - srclen;
+
+ TextUtils.getChars(text, start, end, mText, 0);
+
+ mSpanCount = 0;
+ int alloc = ArrayUtils.idealIntArraySize(0);
+ mSpans = new Object[alloc];
+ mSpanStarts = new int[alloc];
+ mSpanEnds = new int[alloc];
+ mSpanFlags = new int[alloc];
+
+ if (text instanceof Spanned) {
+ Spanned sp = (Spanned) text;
+ Object[] spans = sp.getSpans(start, end, Object.class);
+
+ for (int i = 0; i < spans.length; i++) {
+ int st = sp.getSpanStart(spans[i]) - start;
+ int en = sp.getSpanEnd(spans[i]) - start;
+ int fl = sp.getSpanFlags(spans[i]);
+
+ if (st < 0)
+ st = 0;
+ if (st > end - start)
+ st = end - start;
+
+ if (en < 0)
+ en = 0;
+ if (en > end - start)
+ en = end - start;
+
+ setSpan(spans[i], st, en, fl);
+ }
+ }
+ }
+
+ public static SpannableStringBuilder valueOf(CharSequence source) {
+ if (source instanceof SpannableStringBuilder) {
+ return (SpannableStringBuilder) source;
+ } else {
+ return new SpannableStringBuilder(source);
+ }
+ }
+
+ /**
+ * Return the char at the specified offset within the buffer.
+ */
+ public char charAt(int where) {
+ int len = length();
+ if (where < 0) {
+ throw new IndexOutOfBoundsException("charAt: " + where + " < 0");
+ } else if (where >= len) {
+ throw new IndexOutOfBoundsException("charAt: " + where +
+ " >= length " + len);
+ }
+
+ if (where >= mGapStart)
+ return mText[where + mGapLength];
+ else
+ return mText[where];
+ }
+
+ /**
+ * Return the number of chars in the buffer.
+ */
+ public int length() {
+ return mText.length - mGapLength;
+ }
+
+ private void resizeFor(int size) {
+ int newlen = ArrayUtils.idealCharArraySize(size + 1);
+ char[] newtext = new char[newlen];
+
+ int after = mText.length - (mGapStart + mGapLength);
+
+ System.arraycopy(mText, 0, newtext, 0, mGapStart);
+ System.arraycopy(mText, mText.length - after,
+ newtext, newlen - after, after);
+
+ for (int i = 0; i < mSpanCount; i++) {
+ if (mSpanStarts[i] > mGapStart)
+ mSpanStarts[i] += newlen - mText.length;
+ if (mSpanEnds[i] > mGapStart)
+ mSpanEnds[i] += newlen - mText.length;
+ }
+
+ int oldlen = mText.length;
+ mText = newtext;
+ mGapLength += mText.length - oldlen;
+
+ if (mGapLength < 1)
+ new Exception("mGapLength < 1").printStackTrace();
+ }
+
+ private void moveGapTo(int where) {
+ if (where == mGapStart)
+ return;
+
+ boolean atend = (where == length());
+
+ if (where < mGapStart) {
+ int overlap = mGapStart - where;
+
+ System.arraycopy(mText, where,
+ mText, mGapStart + mGapLength - overlap, overlap);
+ } else /* where > mGapStart */ {
+ int overlap = where - mGapStart;
+
+ System.arraycopy(mText, where + mGapLength - overlap,
+ mText, mGapStart, overlap);
+ }
+
+ // XXX be more clever
+ for (int i = 0; i < mSpanCount; i++) {
+ int start = mSpanStarts[i];
+ int end = mSpanEnds[i];
+
+ if (start > mGapStart)
+ start -= mGapLength;
+ if (start > where)
+ start += mGapLength;
+ else if (start == where) {
+ int flag = (mSpanFlags[i] & START_MASK) >> START_SHIFT;
+
+ if (flag == POINT || (atend && flag == PARAGRAPH))
+ start += mGapLength;
+ }
+
+ if (end > mGapStart)
+ end -= mGapLength;
+ if (end > where)
+ end += mGapLength;
+ else if (end == where) {
+ int flag = (mSpanFlags[i] & END_MASK);
+
+ if (flag == POINT || (atend && flag == PARAGRAPH))
+ end += mGapLength;
+ }
+
+ mSpanStarts[i] = start;
+ mSpanEnds[i] = end;
+ }
+
+ mGapStart = where;
+ }
+
+ // Documentation from interface
+ public SpannableStringBuilder insert(int where, CharSequence tb, int start, int end) {
+ return replace(where, where, tb, start, end);
+ }
+
+ // Documentation from interface
+ public SpannableStringBuilder insert(int where, CharSequence tb) {
+ return replace(where, where, tb, 0, tb.length());
+ }
+
+ // Documentation from interface
+ public SpannableStringBuilder delete(int start, int end) {
+ SpannableStringBuilder ret = replace(start, end, "", 0, 0);
+
+ if (mGapLength > 2 * length())
+ resizeFor(length());
+
+ return ret; // == this
+ }
+
+ // Documentation from interface
+ public void clear() {
+ replace(0, length(), "", 0, 0);
+ }
+
+ // Documentation from interface
+ public void clearSpans() {
+ for (int i = mSpanCount - 1; i >= 0; i--) {
+ Object what = mSpans[i];
+ int ostart = mSpanStarts[i];
+ int oend = mSpanEnds[i];
+
+ if (ostart > mGapStart)
+ ostart -= mGapLength;
+ if (oend > mGapStart)
+ oend -= mGapLength;
+
+ mSpanCount = i;
+ mSpans[i] = null;
+
+ sendSpanRemoved(what, ostart, oend);
+ }
+ }
+
+ // Documentation from interface
+ public SpannableStringBuilder append(CharSequence text) {
+ int length = length();
+ return replace(length, length, text, 0, text.length());
+ }
+
+ // Documentation from interface
+ public SpannableStringBuilder append(CharSequence text, int start, int end) {
+ int length = length();
+ return replace(length, length, text, start, end);
+ }
+
+ // Documentation from interface
+ public SpannableStringBuilder append(char text) {
+ return append(String.valueOf(text));
+ }
+
+ private int change(int start, int end,
+ CharSequence tb, int tbstart, int tbend) {
+ return change(true, start, end, tb, tbstart, tbend);
+ }
+
+ private int change(boolean notify, int start, int end,
+ CharSequence tb, int tbstart, int tbend) {
+ checkRange("replace", start, end);
+ int ret = tbend - tbstart;
+ TextWatcher[] recipients = null;
+
+ if (notify)
+ recipients = sendTextWillChange(start, end - start,
+ tbend - tbstart);
+
+ for (int i = mSpanCount - 1; i >= 0; i--) {
+ if ((mSpanFlags[i] & SPAN_PARAGRAPH) == SPAN_PARAGRAPH) {
+ int st = mSpanStarts[i];
+ if (st > mGapStart)
+ st -= mGapLength;
+
+ int en = mSpanEnds[i];
+ if (en > mGapStart)
+ en -= mGapLength;
+
+ int ost = st;
+ int oen = en;
+ int clen = length();
+
+ if (st > start && st <= end) {
+ for (st = end; st < clen; st++)
+ if (st > end && charAt(st - 1) == '\n')
+ break;
+ }
+
+ if (en > start && en <= end) {
+ for (en = end; en < clen; en++)
+ if (en > end && charAt(en - 1) == '\n')
+ break;
+ }
+
+ if (st != ost || en != oen)
+ setSpan(mSpans[i], st, en, mSpanFlags[i]);
+ }
+ }
+
+ moveGapTo(end);
+
+ if (tbend - tbstart >= mGapLength + (end - start))
+ resizeFor(mText.length - mGapLength +
+ tbend - tbstart - (end - start));
+
+ mGapStart += tbend - tbstart - (end - start);
+ mGapLength -= tbend - tbstart - (end - start);
+
+ if (mGapLength < 1)
+ new Exception("mGapLength < 1").printStackTrace();
+
+ TextUtils.getChars(tb, tbstart, tbend, mText, start);
+
+ if (tb instanceof Spanned) {
+ Spanned sp = (Spanned) tb;
+ Object[] spans = sp.getSpans(tbstart, tbend, Object.class);
+
+ for (int i = 0; i < spans.length; i++) {
+ int st = sp.getSpanStart(spans[i]);
+ int en = sp.getSpanEnd(spans[i]);
+
+ if (st < tbstart)
+ st = tbstart;
+ if (en > tbend)
+ en = tbend;
+
+ if (getSpanStart(spans[i]) < 0) {
+ setSpan(false, spans[i],
+ st - tbstart + start,
+ en - tbstart + start,
+ sp.getSpanFlags(spans[i]));
+ }
+ }
+ }
+
+ // no need for span fixup on pure insertion
+ if (tbend > tbstart && end - start == 0) {
+ if (notify) {
+ sendTextChange(recipients, start, end - start, tbend - tbstart);
+ sendTextHasChanged(recipients);
+ }
+
+ return ret;
+ }
+
+ boolean atend = (mGapStart + mGapLength == mText.length);
+
+ for (int i = mSpanCount - 1; i >= 0; i--) {
+ if (mSpanStarts[i] >= start &&
+ mSpanStarts[i] < mGapStart + mGapLength) {
+ int flag = (mSpanFlags[i] & START_MASK) >> START_SHIFT;
+
+ if (flag == POINT || (flag == PARAGRAPH && atend))
+ mSpanStarts[i] = mGapStart + mGapLength;
+ else
+ mSpanStarts[i] = start;
+ }
+
+ if (mSpanEnds[i] >= start &&
+ mSpanEnds[i] < mGapStart + mGapLength) {
+ int flag = (mSpanFlags[i] & END_MASK);
+
+ if (flag == POINT || (flag == PARAGRAPH && atend))
+ mSpanEnds[i] = mGapStart + mGapLength;
+ else
+ mSpanEnds[i] = start;
+ }
+
+ // remove 0-length SPAN_EXCLUSIVE_EXCLUSIVE
+ // XXX send notification on removal
+
+ if (mSpanEnds[i] < mSpanStarts[i]) {
+ System.arraycopy(mSpans, i + 1,
+ mSpans, i, mSpanCount - (i + 1));
+ System.arraycopy(mSpanStarts, i + 1,
+ mSpanStarts, i, mSpanCount - (i + 1));
+ System.arraycopy(mSpanEnds, i + 1,
+ mSpanEnds, i, mSpanCount - (i + 1));
+ System.arraycopy(mSpanFlags, i + 1,
+ mSpanFlags, i, mSpanCount - (i + 1));
+
+ mSpanCount--;
+ }
+ }
+
+ if (notify) {
+ sendTextChange(recipients, start, end - start, tbend - tbstart);
+ sendTextHasChanged(recipients);
+ }
+
+ return ret;
+ }
+
+ // Documentation from interface
+ public SpannableStringBuilder replace(int start, int end, CharSequence tb) {
+ return replace(start, end, tb, 0, tb.length());
+ }
+
+ // Documentation from interface
+ public SpannableStringBuilder replace(final int start, final int end,
+ CharSequence tb, int tbstart, int tbend) {
+ int filtercount = mFilters.length;
+ for (int i = 0; i < filtercount; i++) {
+ CharSequence repl = mFilters[i].filter(tb, tbstart, tbend,
+ this, start, end);
+
+ if (repl != null) {
+ tb = repl;
+ tbstart = 0;
+ tbend = repl.length();
+ }
+ }
+
+ if (end == start && tbstart == tbend) {
+ return this;
+ }
+
+ if (end == start || tbstart == tbend) {
+ change(start, end, tb, tbstart, tbend);
+ } else {
+ int selstart = Selection.getSelectionStart(this);
+ int selend = Selection.getSelectionEnd(this);
+
+ // XXX just make the span fixups in change() do the right thing
+ // instead of this madness!
+
+ checkRange("replace", start, end);
+ moveGapTo(end);
+ TextWatcher[] recipients;
+
+ recipients = sendTextWillChange(start, end - start,
+ tbend - tbstart);
+
+ int origlen = end - start;
+
+ if (mGapLength < 2)
+ resizeFor(length() + 1);
+
+ for (int i = mSpanCount - 1; i >= 0; i--) {
+ if (mSpanStarts[i] == mGapStart)
+ mSpanStarts[i]++;
+
+ if (mSpanEnds[i] == mGapStart)
+ mSpanEnds[i]++;
+ }
+
+ mText[mGapStart] = ' ';
+ mGapStart++;
+ mGapLength--;
+
+ if (mGapLength < 1)
+ new Exception("mGapLength < 1").printStackTrace();
+
+ int oldlen = (end + 1) - start;
+
+ int inserted = change(false, start + 1, start + 1,
+ tb, tbstart, tbend);
+ change(false, start, start + 1, "", 0, 0);
+ change(false, start + inserted, start + inserted + oldlen - 1,
+ "", 0, 0);
+
+ /*
+ * Special case to keep the cursor in the same position
+ * if it was somewhere in the middle of the replaced region.
+ * If it was at the start or the end or crossing the whole
+ * replacement, it should already be where it belongs.
+ * TODO: Is there some more general mechanism that could
+ * accomplish this?
+ */
+ if (selstart > start && selstart < end) {
+ long off = selstart - start;
+
+ off = off * inserted / (end - start);
+ selstart = (int) off + start;
+
+ setSpan(false, Selection.SELECTION_START, selstart, selstart,
+ Spanned.SPAN_POINT_POINT);
+ }
+ if (selend > start && selend < end) {
+ long off = selend - start;
+
+ off = off * inserted / (end - start);
+ selend = (int) off + start;
+
+ setSpan(false, Selection.SELECTION_END, selend, selend,
+ Spanned.SPAN_POINT_POINT);
+ }
+
+ sendTextChange(recipients, start, origlen, inserted);
+ sendTextHasChanged(recipients);
+ }
+ return this;
+ }
+
+ /**
+ * Mark the specified range of text with the specified object.
+ * The flags determine how the span will behave when text is
+ * inserted at the start or end of the span's range.
+ */
+ public void setSpan(Object what, int start, int end, int flags) {
+ setSpan(true, what, start, end, flags);
+ }
+
+ private void setSpan(boolean send,
+ Object what, int start, int end, int flags) {
+ int nstart = start;
+ int nend = end;
+
+ checkRange("setSpan", start, end);
+
+ if ((flags & START_MASK) == (PARAGRAPH << START_SHIFT)) {
+ if (start != 0 && start != length()) {
+ char c = charAt(start - 1);
+
+ if (c != '\n')
+ throw new RuntimeException(
+ "PARAGRAPH span must start at paragraph boundary");
+ }
+ }
+
+ if ((flags & END_MASK) == PARAGRAPH) {
+ if (end != 0 && end != length()) {
+ char c = charAt(end - 1);
+
+ if (c != '\n')
+ throw new RuntimeException(
+ "PARAGRAPH span must end at paragraph boundary");
+ }
+ }
+
+ if (start > mGapStart)
+ start += mGapLength;
+ else if (start == mGapStart) {
+ int flag = (flags & START_MASK) >> START_SHIFT;
+
+ if (flag == POINT || (flag == PARAGRAPH && start == length()))
+ start += mGapLength;
+ }
+
+ if (end > mGapStart)
+ end += mGapLength;
+ else if (end == mGapStart) {
+ int flag = (flags & END_MASK);
+
+ if (flag == POINT || (flag == PARAGRAPH && end == length()))
+ end += mGapLength;
+ }
+
+ int count = mSpanCount;
+ Object[] spans = mSpans;
+
+ for (int i = 0; i < count; i++) {
+ if (spans[i] == what) {
+ int ostart = mSpanStarts[i];
+ int oend = mSpanEnds[i];
+
+ if (ostart > mGapStart)
+ ostart -= mGapLength;
+ if (oend > mGapStart)
+ oend -= mGapLength;
+
+ mSpanStarts[i] = start;
+ mSpanEnds[i] = end;
+ mSpanFlags[i] = flags;
+
+ if (send)
+ sendSpanChanged(what, ostart, oend, nstart, nend);
+
+ return;
+ }
+ }
+
+ if (mSpanCount + 1 >= mSpans.length) {
+ int newsize = ArrayUtils.idealIntArraySize(mSpanCount + 1);
+ Object[] newspans = new Object[newsize];
+ int[] newspanstarts = new int[newsize];
+ int[] newspanends = new int[newsize];
+ int[] newspanflags = new int[newsize];
+
+ System.arraycopy(mSpans, 0, newspans, 0, mSpanCount);
+ System.arraycopy(mSpanStarts, 0, newspanstarts, 0, mSpanCount);
+ System.arraycopy(mSpanEnds, 0, newspanends, 0, mSpanCount);
+ System.arraycopy(mSpanFlags, 0, newspanflags, 0, mSpanCount);
+
+ mSpans = newspans;
+ mSpanStarts = newspanstarts;
+ mSpanEnds = newspanends;
+ mSpanFlags = newspanflags;
+ }
+
+ mSpans[mSpanCount] = what;
+ mSpanStarts[mSpanCount] = start;
+ mSpanEnds[mSpanCount] = end;
+ mSpanFlags[mSpanCount] = flags;
+ mSpanCount++;
+
+ if (send)
+ sendSpanAdded(what, nstart, nend);
+ }
+
+ /**
+ * Remove the specified markup object from the buffer.
+ */
+ public void removeSpan(Object what) {
+ for (int i = mSpanCount - 1; i >= 0; i--) {
+ if (mSpans[i] == what) {
+ int ostart = mSpanStarts[i];
+ int oend = mSpanEnds[i];
+
+ if (ostart > mGapStart)
+ ostart -= mGapLength;
+ if (oend > mGapStart)
+ oend -= mGapLength;
+
+ int count = mSpanCount - (i + 1);
+
+ System.arraycopy(mSpans, i + 1, mSpans, i, count);
+ System.arraycopy(mSpanStarts, i + 1, mSpanStarts, i, count);
+ System.arraycopy(mSpanEnds, i + 1, mSpanEnds, i, count);
+ System.arraycopy(mSpanFlags, i + 1, mSpanFlags, i, count);
+
+ mSpanCount--;
+ mSpans[mSpanCount] = null;
+
+ sendSpanRemoved(what, ostart, oend);
+ return;
+ }
+ }
+ }
+
+ /**
+ * Return the buffer offset of the beginning of the specified
+ * markup object, or -1 if it is not attached to this buffer.
+ */
+ public int getSpanStart(Object what) {
+ int count = mSpanCount;
+ Object[] spans = mSpans;
+
+ for (int i = count - 1; i >= 0; i--) {
+ if (spans[i] == what) {
+ int where = mSpanStarts[i];
+
+ if (where > mGapStart)
+ where -= mGapLength;
+
+ return where;
+ }
+ }
+
+ return -1;
+ }
+
+ /**
+ * Return the buffer offset of the end of the specified
+ * markup object, or -1 if it is not attached to this buffer.
+ */
+ public int getSpanEnd(Object what) {
+ int count = mSpanCount;
+ Object[] spans = mSpans;
+
+ for (int i = count - 1; i >= 0; i--) {
+ if (spans[i] == what) {
+ int where = mSpanEnds[i];
+
+ if (where > mGapStart)
+ where -= mGapLength;
+
+ return where;
+ }
+ }
+
+ return -1;
+ }
+
+ /**
+ * Return the flags of the end of the specified
+ * markup object, or 0 if it is not attached to this buffer.
+ */
+ public int getSpanFlags(Object what) {
+ int count = mSpanCount;
+ Object[] spans = mSpans;
+
+ for (int i = count - 1; i >= 0; i--) {
+ if (spans[i] == what) {
+ return mSpanFlags[i];
+ }
+ }
+
+ return 0;
+ }
+
+ /**
+ * Return an array of the spans of the specified type that overlap
+ * the specified range of the buffer. The kind may be Object.class to get
+ * a list of all the spans regardless of type.
+ */
+ public <T> T[] getSpans(int queryStart, int queryEnd, Class<T> kind) {
+ int spanCount = mSpanCount;
+ Object[] spans = mSpans;
+ int[] starts = mSpanStarts;
+ int[] ends = mSpanEnds;
+ int[] flags = mSpanFlags;
+ int gapstart = mGapStart;
+ int gaplen = mGapLength;
+
+ int count = 0;
+ Object[] ret = null;
+ Object ret1 = null;
+
+ for (int i = 0; i < spanCount; i++) {
+ int spanStart = starts[i];
+ int spanEnd = ends[i];
+
+ if (spanStart > gapstart) {
+ spanStart -= gaplen;
+ }
+ if (spanEnd > gapstart) {
+ spanEnd -= gaplen;
+ }
+
+ if (spanStart > queryEnd) {
+ continue;
+ }
+ if (spanEnd < queryStart) {
+ continue;
+ }
+
+ if (spanStart != spanEnd && queryStart != queryEnd) {
+ if (spanStart == queryEnd)
+ continue;
+ if (spanEnd == queryStart)
+ continue;
+ }
+
+ if (kind != null && !kind.isInstance(spans[i])) {
+ continue;
+ }
+
+ if (count == 0) {
+ ret1 = spans[i];
+ count++;
+ } else {
+ if (count == 1) {
+ ret = (Object[]) Array.newInstance(kind, spanCount - i + 1);
+ ret[0] = ret1;
+ }
+
+ int prio = flags[i] & SPAN_PRIORITY;
+ if (prio != 0) {
+ int j;
+
+ for (j = 0; j < count; j++) {
+ int p = getSpanFlags(ret[j]) & SPAN_PRIORITY;
+
+ if (prio > p) {
+ break;
+ }
+ }
+
+ System.arraycopy(ret, j, ret, j + 1, count - j);
+ ret[j] = spans[i];
+ count++;
+ } else {
+ ret[count++] = spans[i];
+ }
+ }
+ }
+
+ if (count == 0) {
+ return (T[]) ArrayUtils.emptyArray(kind);
+ }
+ if (count == 1) {
+ ret = (Object[]) Array.newInstance(kind, 1);
+ ret[0] = ret1;
+ return (T[]) ret;
+ }
+ if (count == ret.length) {
+ return (T[]) ret;
+ }
+
+ Object[] nret = (Object[]) Array.newInstance(kind, count);
+ System.arraycopy(ret, 0, nret, 0, count);
+ return (T[]) nret;
+ }
+
+ /**
+ * Return the next offset after <code>start</code> but less than or
+ * equal to <code>limit</code> where a span of the specified type
+ * begins or ends.
+ */
+ public int nextSpanTransition(int start, int limit, Class kind) {
+ int count = mSpanCount;
+ Object[] spans = mSpans;
+ int[] starts = mSpanStarts;
+ int[] ends = mSpanEnds;
+ int gapstart = mGapStart;
+ int gaplen = mGapLength;
+
+ if (kind == null) {
+ kind = Object.class;
+ }
+
+ for (int i = 0; i < count; i++) {
+ int st = starts[i];
+ int en = ends[i];
+
+ if (st > gapstart)
+ st -= gaplen;
+ if (en > gapstart)
+ en -= gaplen;
+
+ if (st > start && st < limit && kind.isInstance(spans[i]))
+ limit = st;
+ if (en > start && en < limit && kind.isInstance(spans[i]))
+ limit = en;
+ }
+
+ return limit;
+ }
+
+ /**
+ * Return a new CharSequence containing a copy of the specified
+ * range of this buffer, including the overlapping spans.
+ */
+ public CharSequence subSequence(int start, int end) {
+ return new SpannableStringBuilder(this, start, end);
+ }
+
+ /**
+ * Copy the specified range of chars from this buffer into the
+ * specified array, beginning at the specified offset.
+ */
+ public void getChars(int start, int end, char[] dest, int destoff) {
+ checkRange("getChars", start, end);
+
+ if (end <= mGapStart) {
+ System.arraycopy(mText, start, dest, destoff, end - start);
+ } else if (start >= mGapStart) {
+ System.arraycopy(mText, start + mGapLength,
+ dest, destoff, end - start);
+ } else {
+ System.arraycopy(mText, start, dest, destoff, mGapStart - start);
+ System.arraycopy(mText, mGapStart + mGapLength,
+ dest, destoff + (mGapStart - start),
+ end - mGapStart);
+ }
+ }
+
+ /**
+ * Return a String containing a copy of the chars in this buffer.
+ */
+ public String toString() {
+ int len = length();
+ char[] buf = new char[len];
+
+ getChars(0, len, buf, 0);
+ return new String(buf);
+ }
+
+ private TextWatcher[] sendTextWillChange(int start, int before, int after) {
+ TextWatcher[] recip = getSpans(start, start + before, TextWatcher.class);
+ int n = recip.length;
+
+ for (int i = 0; i < n; i++) {
+ recip[i].beforeTextChanged(this, start, before, after);
+ }
+
+ return recip;
+ }
+
+ private void sendTextChange(TextWatcher[] recip, int start, int before,
+ int after) {
+ int n = recip.length;
+
+ for (int i = 0; i < n; i++) {
+ recip[i].onTextChanged(this, start, before, after);
+ }
+ }
+
+ private void sendTextHasChanged(TextWatcher[] recip) {
+ int n = recip.length;
+
+ for (int i = 0; i < n; i++) {
+ recip[i].afterTextChanged(this);
+ }
+ }
+
+ private void sendSpanAdded(Object what, int start, int end) {
+ SpanWatcher[] recip = getSpans(start, end, SpanWatcher.class);
+ int n = recip.length;
+
+ for (int i = 0; i < n; i++) {
+ recip[i].onSpanAdded(this, what, start, end);
+ }
+ }
+
+ private void sendSpanRemoved(Object what, int start, int end) {
+ SpanWatcher[] recip = getSpans(start, end, SpanWatcher.class);
+ int n = recip.length;
+
+ for (int i = 0; i < n; i++) {
+ recip[i].onSpanRemoved(this, what, start, end);
+ }
+ }
+
+ private void sendSpanChanged(Object what, int s, int e, int st, int en) {
+ SpanWatcher[] recip = getSpans(Math.min(s, st), Math.max(e, en),
+ SpanWatcher.class);
+ int n = recip.length;
+
+ for (int i = 0; i < n; i++) {
+ recip[i].onSpanChanged(this, what, s, e, st, en);
+ }
+ }
+
+ private static String region(int start, int end) {
+ return "(" + start + " ... " + end + ")";
+ }
+
+ private void checkRange(final String operation, int start, int end) {
+ if (end < start) {
+ throw new IndexOutOfBoundsException(operation + " " +
+ region(start, end) +
+ " has end before start");
+ }
+
+ int len = length();
+
+ if (start > len || end > len) {
+ throw new IndexOutOfBoundsException(operation + " " +
+ region(start, end) +
+ " ends beyond length " + len);
+ }
+
+ if (start < 0 || end < 0) {
+ throw new IndexOutOfBoundsException(operation + " " +
+ region(start, end) +
+ " starts before 0");
+ }
+ }
+
+ private boolean isprint(char c) { // XXX
+ if (c >= ' ' && c <= '~')
+ return true;
+ else
+ return false;
+ }
+
+/*
+ private static final int startFlag(int flag) {
+ return (flag >> 4) & 0x0F;
+ }
+
+ private static final int endFlag(int flag) {
+ return flag & 0x0F;
+ }
+
+ public void dump() { // XXX
+ for (int i = 0; i < mGapStart; i++) {
+ System.out.print('|');
+ System.out.print(' ');
+ System.out.print(isprint(mText[i]) ? mText[i] : '.');
+ System.out.print(' ');
+ }
+
+ for (int i = mGapStart; i < mGapStart + mGapLength; i++) {
+ System.out.print('|');
+ System.out.print('(');
+ System.out.print(isprint(mText[i]) ? mText[i] : '.');
+ System.out.print(')');
+ }
+
+ for (int i = mGapStart + mGapLength; i < mText.length; i++) {
+ System.out.print('|');
+ System.out.print(' ');
+ System.out.print(isprint(mText[i]) ? mText[i] : '.');
+ System.out.print(' ');
+ }
+
+ System.out.print('\n');
+
+ for (int i = 0; i < mText.length + 1; i++) {
+ int found = 0;
+ int wfound = 0;
+
+ for (int j = 0; j < mSpanCount; j++) {
+ if (mSpanStarts[j] == i) {
+ found = 1;
+ wfound = j;
+ break;
+ }
+
+ if (mSpanEnds[j] == i) {
+ found = 2;
+ wfound = j;
+ break;
+ }
+ }
+
+ if (found == 1) {
+ if (startFlag(mSpanFlags[wfound]) == MARK)
+ System.out.print("( ");
+ if (startFlag(mSpanFlags[wfound]) == PARAGRAPH)
+ System.out.print("< ");
+ else
+ System.out.print("[ ");
+ } else if (found == 2) {
+ if (endFlag(mSpanFlags[wfound]) == POINT)
+ System.out.print(") ");
+ if (endFlag(mSpanFlags[wfound]) == PARAGRAPH)
+ System.out.print("> ");
+ else
+ System.out.print("] ");
+ } else {
+ System.out.print(" ");
+ }
+ }
+
+ System.out.print("\n");
+ }
+*/
+
+ /**
+ * Don't call this yourself -- exists for Canvas to use internally.
+ * {@hide}
+ */
+ public void drawText(Canvas c, int start, int end,
+ float x, float y, Paint p) {
+ checkRange("drawText", start, end);
+
+ if (end <= mGapStart) {
+ c.drawText(mText, start, end - start, x, y, p);
+ } else if (start >= mGapStart) {
+ c.drawText(mText, start + mGapLength, end - start, x, y, p);
+ } else {
+ char[] buf = TextUtils.obtain(end - start);
+
+ getChars(start, end, buf, 0);
+ c.drawText(buf, 0, end - start, x, y, p);
+ TextUtils.recycle(buf);
+ }
+ }
+
+ /**
+ * Don't call this yourself -- exists for Paint to use internally.
+ * {@hide}
+ */
+ public float measureText(int start, int end, Paint p) {
+ checkRange("measureText", start, end);
+
+ float ret;
+
+ if (end <= mGapStart) {
+ ret = p.measureText(mText, start, end - start);
+ } else if (start >= mGapStart) {
+ ret = p.measureText(mText, start + mGapLength, end - start);
+ } else {
+ char[] buf = TextUtils.obtain(end - start);
+
+ getChars(start, end, buf, 0);
+ ret = p.measureText(buf, 0, end - start);
+ TextUtils.recycle(buf);
+ }
+
+ return ret;
+ }
+
+ /**
+ * Don't call this yourself -- exists for Paint to use internally.
+ * {@hide}
+ */
+ public int getTextWidths(int start, int end, float[] widths, Paint p) {
+ checkRange("getTextWidths", start, end);
+
+ int ret;
+
+ if (end <= mGapStart) {
+ ret = p.getTextWidths(mText, start, end - start, widths);
+ } else if (start >= mGapStart) {
+ ret = p.getTextWidths(mText, start + mGapLength, end - start,
+ widths);
+ } else {
+ char[] buf = TextUtils.obtain(end - start);
+
+ getChars(start, end, buf, 0);
+ ret = p.getTextWidths(buf, 0, end - start, widths);
+ TextUtils.recycle(buf);
+ }
+
+ return ret;
+ }
+
+ // Documentation from interface
+ public void setFilters(InputFilter[] filters) {
+ if (filters == null) {
+ throw new IllegalArgumentException();
+ }
+
+ mFilters = filters;
+ }
+
+ // Documentation from interface
+ public InputFilter[] getFilters() {
+ return mFilters;
+ }
+
+ private static final InputFilter[] NO_FILTERS = new InputFilter[0];
+ private InputFilter[] mFilters = NO_FILTERS;
+
+ private char[] mText;
+ private int mGapStart;
+ private int mGapLength;
+
+ private Object[] mSpans;
+ private int[] mSpanStarts;
+ private int[] mSpanEnds;
+ private int[] mSpanFlags;
+ private int mSpanCount;
+
+ private static final int MARK = 1;
+ private static final int POINT = 2;
+ private static final int PARAGRAPH = 3;
+
+ private static final int START_MASK = 0xF0;
+ private static final int END_MASK = 0x0F;
+ private static final int START_SHIFT = 4;
+}
diff --git a/core/java/android/text/SpannableStringInternal.java b/core/java/android/text/SpannableStringInternal.java
new file mode 100644
index 0000000..0412285
--- /dev/null
+++ b/core/java/android/text/SpannableStringInternal.java
@@ -0,0 +1,372 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text;
+
+import com.android.internal.util.ArrayUtils;
+
+import java.lang.reflect.Array;
+
+/* package */ abstract class SpannableStringInternal
+{
+ /* package */ SpannableStringInternal(CharSequence source,
+ int start, int end) {
+ if (start == 0 && end == source.length())
+ mText = source.toString();
+ else
+ mText = source.toString().substring(start, end);
+
+ int initial = ArrayUtils.idealIntArraySize(0);
+ mSpans = new Object[initial];
+ mSpanData = new int[initial * 3];
+
+ if (source instanceof Spanned) {
+ Spanned sp = (Spanned) source;
+ Object[] spans = sp.getSpans(start, end, Object.class);
+
+ for (int i = 0; i < spans.length; i++) {
+ int st = sp.getSpanStart(spans[i]);
+ int en = sp.getSpanEnd(spans[i]);
+ int fl = sp.getSpanFlags(spans[i]);
+
+ if (st < start)
+ st = start;
+ if (en > end)
+ en = end;
+
+ setSpan(spans[i], st - start, en - start, fl);
+ }
+ }
+ }
+
+ public final int length() {
+ return mText.length();
+ }
+
+ public final char charAt(int i) {
+ return mText.charAt(i);
+ }
+
+ public final String toString() {
+ return mText;
+ }
+
+ /* subclasses must do subSequence() to preserve type */
+
+ public final void getChars(int start, int end, char[] dest, int off) {
+ mText.getChars(start, end, dest, off);
+ }
+
+ /* package */ void setSpan(Object what, int start, int end, int flags) {
+ int nstart = start;
+ int nend = end;
+
+ checkRange("setSpan", start, end);
+
+ if ((flags & Spannable.SPAN_PARAGRAPH) == Spannable.SPAN_PARAGRAPH) {
+ if (start != 0 && start != length()) {
+ char c = charAt(start - 1);
+
+ if (c != '\n')
+ throw new RuntimeException(
+ "PARAGRAPH span must start at paragraph boundary" +
+ " (" + start + " follows " + c + ")");
+ }
+
+ if (end != 0 && end != length()) {
+ char c = charAt(end - 1);
+
+ if (c != '\n')
+ throw new RuntimeException(
+ "PARAGRAPH span must end at paragraph boundary" +
+ " (" + end + " follows " + c + ")");
+ }
+ }
+
+ int count = mSpanCount;
+ Object[] spans = mSpans;
+ int[] data = mSpanData;
+
+ for (int i = 0; i < count; i++) {
+ if (spans[i] == what) {
+ int ostart = data[i * COLUMNS + START];
+ int oend = data[i * COLUMNS + END];
+
+ data[i * COLUMNS + START] = start;
+ data[i * COLUMNS + END] = end;
+ data[i * COLUMNS + FLAGS] = flags;
+
+ sendSpanChanged(what, ostart, oend, nstart, nend);
+ return;
+ }
+ }
+
+ if (mSpanCount + 1 >= mSpans.length) {
+ int newsize = ArrayUtils.idealIntArraySize(mSpanCount + 1);
+ Object[] newtags = new Object[newsize];
+ int[] newdata = new int[newsize * 3];
+
+ System.arraycopy(mSpans, 0, newtags, 0, mSpanCount);
+ System.arraycopy(mSpanData, 0, newdata, 0, mSpanCount * 3);
+
+ mSpans = newtags;
+ mSpanData = newdata;
+ }
+
+ mSpans[mSpanCount] = what;
+ mSpanData[mSpanCount * COLUMNS + START] = start;
+ mSpanData[mSpanCount * COLUMNS + END] = end;
+ mSpanData[mSpanCount * COLUMNS + FLAGS] = flags;
+ mSpanCount++;
+
+ if (this instanceof Spannable)
+ sendSpanAdded(what, nstart, nend);
+ }
+
+ /* package */ void removeSpan(Object what) {
+ int count = mSpanCount;
+ Object[] spans = mSpans;
+ int[] data = mSpanData;
+
+ for (int i = count - 1; i >= 0; i--) {
+ if (spans[i] == what) {
+ int ostart = data[i * COLUMNS + START];
+ int oend = data[i * COLUMNS + END];
+
+ int c = count - (i + 1);
+
+ System.arraycopy(spans, i + 1, spans, i, c);
+ System.arraycopy(data, (i + 1) * COLUMNS,
+ data, i * COLUMNS, c * COLUMNS);
+
+ mSpanCount--;
+
+ sendSpanRemoved(what, ostart, oend);
+ return;
+ }
+ }
+ }
+
+ public int getSpanStart(Object what) {
+ int count = mSpanCount;
+ Object[] spans = mSpans;
+ int[] data = mSpanData;
+
+ for (int i = count - 1; i >= 0; i--) {
+ if (spans[i] == what) {
+ return data[i * COLUMNS + START];
+ }
+ }
+
+ return -1;
+ }
+
+ public int getSpanEnd(Object what) {
+ int count = mSpanCount;
+ Object[] spans = mSpans;
+ int[] data = mSpanData;
+
+ for (int i = count - 1; i >= 0; i--) {
+ if (spans[i] == what) {
+ return data[i * COLUMNS + END];
+ }
+ }
+
+ return -1;
+ }
+
+ public int getSpanFlags(Object what) {
+ int count = mSpanCount;
+ Object[] spans = mSpans;
+ int[] data = mSpanData;
+
+ for (int i = count - 1; i >= 0; i--) {
+ if (spans[i] == what) {
+ return data[i * COLUMNS + FLAGS];
+ }
+ }
+
+ return 0;
+ }
+
+ public <T> T[] getSpans(int queryStart, int queryEnd, Class<T> kind) {
+ int count = 0;
+
+ int spanCount = mSpanCount;
+ Object[] spans = mSpans;
+ int[] data = mSpanData;
+ Object[] ret = null;
+ Object ret1 = null;
+
+ for (int i = 0; i < spanCount; i++) {
+ int spanStart = data[i * COLUMNS + START];
+ int spanEnd = data[i * COLUMNS + END];
+
+ if (spanStart > queryEnd) {
+ continue;
+ }
+ if (spanEnd < queryStart) {
+ continue;
+ }
+
+ if (spanStart != spanEnd && queryStart != queryEnd) {
+ if (spanStart == queryEnd) {
+ continue;
+ }
+ if (spanEnd == queryStart) {
+ continue;
+ }
+ }
+
+ if (kind != null && !kind.isInstance(spans[i])) {
+ continue;
+ }
+
+ if (count == 0) {
+ ret1 = spans[i];
+ count++;
+ } else {
+ if (count == 1) {
+ ret = (Object[]) Array.newInstance(kind, spanCount - i + 1);
+ ret[0] = ret1;
+ }
+
+ int prio = data[i * COLUMNS + FLAGS] & Spanned.SPAN_PRIORITY;
+ if (prio != 0) {
+ int j;
+
+ for (j = 0; j < count; j++) {
+ int p = getSpanFlags(ret[j]) & Spanned.SPAN_PRIORITY;
+
+ if (prio > p) {
+ break;
+ }
+ }
+
+ System.arraycopy(ret, j, ret, j + 1, count - j);
+ ret[j] = spans[i];
+ count++;
+ } else {
+ ret[count++] = spans[i];
+ }
+ }
+ }
+
+ if (count == 0) {
+ return (T[]) ArrayUtils.emptyArray(kind);
+ }
+ if (count == 1) {
+ ret = (Object[]) Array.newInstance(kind, 1);
+ ret[0] = ret1;
+ return (T[]) ret;
+ }
+ if (count == ret.length) {
+ return (T[]) ret;
+ }
+
+ Object[] nret = (Object[]) Array.newInstance(kind, count);
+ System.arraycopy(ret, 0, nret, 0, count);
+ return (T[]) nret;
+ }
+
+ public int nextSpanTransition(int start, int limit, Class kind) {
+ int count = mSpanCount;
+ Object[] spans = mSpans;
+ int[] data = mSpanData;
+
+ if (kind == null) {
+ kind = Object.class;
+ }
+
+ for (int i = 0; i < count; i++) {
+ int st = data[i * COLUMNS + START];
+ int en = data[i * COLUMNS + END];
+
+ if (st > start && st < limit && kind.isInstance(spans[i]))
+ limit = st;
+ if (en > start && en < limit && kind.isInstance(spans[i]))
+ limit = en;
+ }
+
+ return limit;
+ }
+
+ private void sendSpanAdded(Object what, int start, int end) {
+ SpanWatcher[] recip = getSpans(start, end, SpanWatcher.class);
+ int n = recip.length;
+
+ for (int i = 0; i < n; i++) {
+ recip[i].onSpanAdded((Spannable) this, what, start, end);
+ }
+ }
+
+ private void sendSpanRemoved(Object what, int start, int end) {
+ SpanWatcher[] recip = getSpans(start, end, SpanWatcher.class);
+ int n = recip.length;
+
+ for (int i = 0; i < n; i++) {
+ recip[i].onSpanRemoved((Spannable) this, what, start, end);
+ }
+ }
+
+ private void sendSpanChanged(Object what, int s, int e, int st, int en) {
+ SpanWatcher[] recip = getSpans(Math.min(s, st), Math.max(e, en),
+ SpanWatcher.class);
+ int n = recip.length;
+
+ for (int i = 0; i < n; i++) {
+ recip[i].onSpanChanged((Spannable) this, what, s, e, st, en);
+ }
+ }
+
+ private static String region(int start, int end) {
+ return "(" + start + " ... " + end + ")";
+ }
+
+ private void checkRange(final String operation, int start, int end) {
+ if (end < start) {
+ throw new IndexOutOfBoundsException(operation + " " +
+ region(start, end) +
+ " has end before start");
+ }
+
+ int len = length();
+
+ if (start > len || end > len) {
+ throw new IndexOutOfBoundsException(operation + " " +
+ region(start, end) +
+ " ends beyond length " + len);
+ }
+
+ if (start < 0 || end < 0) {
+ throw new IndexOutOfBoundsException(operation + " " +
+ region(start, end) +
+ " starts before 0");
+ }
+ }
+
+ private String mText;
+ private Object[] mSpans;
+ private int[] mSpanData;
+ private int mSpanCount;
+
+ /* package */ static final Object[] EMPTY = new Object[0];
+
+ private static final int START = 0;
+ private static final int END = 1;
+ private static final int FLAGS = 2;
+ private static final int COLUMNS = 3;
+}
diff --git a/core/java/android/text/Spanned.java b/core/java/android/text/Spanned.java
new file mode 100644
index 0000000..2b4b4d2
--- /dev/null
+++ b/core/java/android/text/Spanned.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text;
+
+/**
+ * This is the interface for text that has markup objects attached to
+ * ranges of it. Not all text classes have mutable markup or text;
+ * see {@link Spannable} for mutable markup and {@link Editable} for
+ * mutable text.
+ */
+public interface Spanned
+extends CharSequence
+{
+ /**
+ * 0-length spans with type SPAN_MARK_MARK behave like text marks:
+ * they remain at their original offset when text is inserted
+ * at that offset.
+ */
+ public static final int SPAN_MARK_MARK = 0x11;
+ /**
+ * SPAN_MARK_POINT is a synonym for {@link #SPAN_INCLUSIVE_INCLUSIVE}.
+ */
+ public static final int SPAN_MARK_POINT = 0x12;
+ /**
+ * SPAN_POINT_MARK is a synonym for {@link #SPAN_EXCLUSIVE_EXCLUSIVE}.
+ */
+ public static final int SPAN_POINT_MARK = 0x21;
+
+ /**
+ * 0-length spans with type SPAN_POINT_POINT behave like cursors:
+ * they are pushed forward by the length of the insertion when text
+ * is inserted at their offset.
+ */
+ public static final int SPAN_POINT_POINT = 0x22;
+
+ /**
+ * SPAN_PARAGRAPH behaves like SPAN_INCLUSIVE_EXCLUSIVE
+ * (SPAN_MARK_MARK), except that if either end of the span is
+ * at the end of the buffer, that end behaves like _POINT
+ * instead (so SPAN_INCLUSIVE_INCLUSIVE if it starts in the
+ * middle and ends at the end, or SPAN_EXCLUSIVE_INCLUSIVE
+ * if it both starts and ends at the end).
+ * <p>
+ * Its endpoints must be the start or end of the buffer or
+ * immediately after a \n character, and if the \n
+ * that anchors it is deleted, the endpoint is pulled to the
+ * next \n that follows in the buffer (or to the end of
+ * the buffer).
+ */
+ public static final int SPAN_PARAGRAPH = 0x33;
+
+ /**
+ * Non-0-length spans of type SPAN_INCLUSIVE_EXCLUSIVE expand
+ * to include text inserted at their starting point but not at their
+ * ending point. When 0-length, they behave like marks.
+ */
+ public static final int SPAN_INCLUSIVE_EXCLUSIVE = SPAN_MARK_MARK;
+
+ /**
+ * Spans of type SPAN_INCLUSIVE_INCLUSIVE expand
+ * to include text inserted at either their starting or ending point.
+ */
+ public static final int SPAN_INCLUSIVE_INCLUSIVE = SPAN_MARK_POINT;
+
+ /**
+ * Spans of type SPAN_EXCLUSIVE_EXCLUSIVE do not expand
+ * to include text inserted at either their starting or ending point.
+ * They can never have a length of 0 and are automatically removed
+ * from the buffer if all the text they cover is removed.
+ */
+ public static final int SPAN_EXCLUSIVE_EXCLUSIVE = SPAN_POINT_MARK;
+
+ /**
+ * Non-0-length spans of type SPAN_INCLUSIVE_EXCLUSIVE expand
+ * to include text inserted at their ending point but not at their
+ * starting point. When 0-length, they behave like points.
+ */
+ public static final int SPAN_EXCLUSIVE_INCLUSIVE = SPAN_POINT_POINT;
+
+ /**
+ * The bits numbered SPAN_USER_SHIFT and above are available
+ * for callers to use to store scalar data associated with their
+ * span object.
+ */
+ public static final int SPAN_USER_SHIFT = 24;
+ /**
+ * The bits specified by the SPAN_USER bitfield are available
+ * for callers to use to store scalar data associated with their
+ * span object.
+ */
+ public static final int SPAN_USER = 0xFFFFFFFF << SPAN_USER_SHIFT;
+
+ /**
+ * The bits numbered just above SPAN_PRIORITY_SHIFT determine the order
+ * of change notifications -- higher numbers go first. You probably
+ * don't need to set this; it is used so that when text changes, the
+ * text layout gets the chance to update itself before any other
+ * callbacks can inquire about the layout of the text.
+ */
+ public static final int SPAN_PRIORITY_SHIFT = 16;
+ /**
+ * The bits specified by the SPAN_PRIORITY bitmap determine the order
+ * of change notifications -- higher numbers go first. You probably
+ * don't need to set this; it is used so that when text changes, the
+ * text layout gets the chance to update itself before any other
+ * callbacks can inquire about the layout of the text.
+ */
+ public static final int SPAN_PRIORITY = 0xFF << SPAN_PRIORITY_SHIFT;
+
+ /**
+ * Return an array of the markup objects attached to the specified
+ * slice of this CharSequence and whose type is the specified type
+ * or a subclass of it. Specify Object.class for the type if you
+ * want all the objects regardless of type.
+ */
+ public <T> T[] getSpans(int start, int end, Class<T> type);
+
+ /**
+ * Return the beginning of the range of text to which the specified
+ * markup object is attached, or -1 if the object is not attached.
+ */
+ public int getSpanStart(Object tag);
+
+ /**
+ * Return the end of the range of text to which the specified
+ * markup object is attached, or -1 if the object is not attached.
+ */
+ public int getSpanEnd(Object tag);
+
+ /**
+ * Return the flags that were specified when {@link Spannable#setSpan} was
+ * used to attach the specified markup object, or 0 if the specified
+ * object has not been attached.
+ */
+ public int getSpanFlags(Object tag);
+
+ /**
+ * Return the first offset greater than or equal to <code>start</code>
+ * where a markup object of class <code>type</code> begins or ends,
+ * or <code>limit</code> if there are no starts or ends greater than or
+ * equal to <code>start</code> but less than <code>limit</code>. Specify
+ * <code>null</code> or Object.class for the type if you want every
+ * transition regardless of type.
+ */
+ public int nextSpanTransition(int start, int limit, Class type);
+}
diff --git a/core/java/android/text/SpannedString.java b/core/java/android/text/SpannedString.java
new file mode 100644
index 0000000..afed221
--- /dev/null
+++ b/core/java/android/text/SpannedString.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text;
+
+
+/**
+ * This is the class for text whose content and markup are immutable.
+ * For mutable markup, see {@link SpannableString}; for mutable text,
+ * see {@link SpannableStringBuilder}.
+ */
+public final class SpannedString
+extends SpannableStringInternal
+implements CharSequence, GetChars, Spanned
+{
+ public SpannedString(CharSequence source) {
+ super(source, 0, source.length());
+ }
+
+ private SpannedString(CharSequence source, int start, int end) {
+ super(source, start, end);
+ }
+
+ public CharSequence subSequence(int start, int end) {
+ return new SpannedString(this, start, end);
+ }
+
+ public static SpannedString valueOf(CharSequence source) {
+ if (source instanceof SpannedString) {
+ return (SpannedString) source;
+ } else {
+ return new SpannedString(source);
+ }
+ }
+}
diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java
new file mode 100644
index 0000000..2d18575
--- /dev/null
+++ b/core/java/android/text/StaticLayout.java
@@ -0,0 +1,1118 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text;
+
+import android.graphics.Paint;
+import com.android.internal.util.ArrayUtils;
+import android.util.Log;
+import android.text.style.LeadingMarginSpan;
+import android.text.style.LineHeightSpan;
+import android.text.style.MetricAffectingSpan;
+import android.text.style.ReplacementSpan;
+
+/**
+ * StaticLayout is a Layout for text that will not be edited after it
+ * is laid out. Use {@link DynamicLayout} for text that may change.
+ * <p>This is used by widgets to control text layout. You should not need
+ * to use this class directly unless you are implementing your own widget
+ * or custom display object, or would be tempted to call
+ * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, float, float, android.graphics.Paint)
+ * Canvas.drawText()} directly.</p>
+ */
+public class
+StaticLayout
+extends Layout
+{
+ public StaticLayout(CharSequence source, TextPaint paint,
+ int width,
+ Alignment align, float spacingmult, float spacingadd,
+ boolean includepad) {
+ this(source, 0, source.length(), paint, width, align,
+ spacingmult, spacingadd, includepad);
+ }
+
+ public StaticLayout(CharSequence source, int bufstart, int bufend,
+ TextPaint paint, int outerwidth,
+ Alignment align,
+ float spacingmult, float spacingadd,
+ boolean includepad) {
+ this(source, bufstart, bufend, paint, outerwidth, align,
+ spacingmult, spacingadd, includepad, null, 0);
+ }
+
+ public StaticLayout(CharSequence source, int bufstart, int bufend,
+ TextPaint paint, int outerwidth,
+ Alignment align,
+ float spacingmult, float spacingadd,
+ boolean includepad,
+ TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
+ super((ellipsize == null)
+ ? source
+ : (source instanceof Spanned)
+ ? new SpannedEllipsizer(source)
+ : new Ellipsizer(source),
+ paint, outerwidth, align, spacingmult, spacingadd);
+
+ /*
+ * This is annoying, but we can't refer to the layout until
+ * superclass construction is finished, and the superclass
+ * constructor wants the reference to the display text.
+ *
+ * This will break if the superclass constructor ever actually
+ * cares about the content instead of just holding the reference.
+ */
+ if (ellipsize != null) {
+ Ellipsizer e = (Ellipsizer) getText();
+
+ e.mLayout = this;
+ e.mWidth = ellipsizedWidth;
+ e.mMethod = ellipsize;
+ mEllipsizedWidth = ellipsizedWidth;
+
+ mColumns = COLUMNS_ELLIPSIZE;
+ } else {
+ mColumns = COLUMNS_NORMAL;
+ mEllipsizedWidth = outerwidth;
+ }
+
+ mLines = new int[ArrayUtils.idealIntArraySize(2 * mColumns)];
+ mLineDirections = new Directions[
+ ArrayUtils.idealIntArraySize(2 * mColumns)];
+
+ generate(source, bufstart, bufend, paint, outerwidth, align,
+ spacingmult, spacingadd, includepad, includepad,
+ ellipsize != null, ellipsizedWidth, ellipsize);
+
+ mChdirs = null;
+ mChs = null;
+ mWidths = null;
+ mFontMetricsInt = null;
+ }
+
+ /* package */ StaticLayout(boolean ellipsize) {
+ super(null, null, 0, null, 0, 0);
+
+ mColumns = COLUMNS_ELLIPSIZE;
+ mLines = new int[ArrayUtils.idealIntArraySize(2 * mColumns)];
+ mLineDirections = new Directions[
+ ArrayUtils.idealIntArraySize(2 * mColumns)];
+ }
+
+ /* package */ void generate(CharSequence source, int bufstart, int bufend,
+ TextPaint paint, int outerwidth,
+ Alignment align,
+ float spacingmult, float spacingadd,
+ boolean includepad, boolean trackpad,
+ boolean breakOnlyAtSpaces,
+ float ellipsizedWidth, TextUtils.TruncateAt where) {
+ mLineCount = 0;
+
+ int v = 0;
+ boolean needMultiply = (spacingmult != 1 || spacingadd != 0);
+
+ Paint.FontMetricsInt fm = mFontMetricsInt;
+ int[] choosehtv = null;
+
+ int end = TextUtils.indexOf(source, '\n', bufstart, bufend);
+ int bufsiz = end >= 0 ? end - bufstart : bufend - bufstart;
+ boolean first = true;
+
+ if (mChdirs == null) {
+ mChdirs = new byte[ArrayUtils.idealByteArraySize(bufsiz + 1)];
+ mChs = new char[ArrayUtils.idealCharArraySize(bufsiz + 1)];
+ mWidths = new float[ArrayUtils.idealIntArraySize((bufsiz + 1) * 2)];
+ }
+
+ byte[] chdirs = mChdirs;
+ char[] chs = mChs;
+ float[] widths = mWidths;
+
+ AlteredCharSequence alter = null;
+ Spanned spanned = null;
+
+ if (source instanceof Spanned)
+ spanned = (Spanned) source;
+
+ int DEFAULT_DIR = DIR_LEFT_TO_RIGHT; // XXX
+
+ for (int start = bufstart; start <= bufend; start = end) {
+ if (first)
+ first = false;
+ else
+ end = TextUtils.indexOf(source, '\n', start, bufend);
+
+ if (end < 0)
+ end = bufend;
+ else
+ end++;
+
+ int firstwidth = outerwidth;
+ int restwidth = outerwidth;
+
+ LineHeightSpan[] chooseht = null;
+
+ if (spanned != null) {
+ LeadingMarginSpan[] sp;
+
+ sp = spanned.getSpans(start, end, LeadingMarginSpan.class);
+ for (int i = 0; i < sp.length; i++) {
+ firstwidth -= sp[i].getLeadingMargin(true);
+ restwidth -= sp[i].getLeadingMargin(false);
+ }
+
+ chooseht = spanned.getSpans(start, end, LineHeightSpan.class);
+
+ if (chooseht.length != 0) {
+ if (choosehtv == null ||
+ choosehtv.length < chooseht.length) {
+ choosehtv = new int[ArrayUtils.idealIntArraySize(
+ chooseht.length)];
+ }
+
+ for (int i = 0; i < chooseht.length; i++) {
+ int o = spanned.getSpanStart(chooseht[i]);
+
+ if (o < start) {
+ // starts in this layout, before the
+ // current paragraph
+
+ choosehtv[i] = getLineTop(getLineForOffset(o));
+ } else {
+ // starts in this paragraph
+
+ choosehtv[i] = v;
+ }
+ }
+ }
+ }
+
+ if (end - start > chdirs.length) {
+ chdirs = new byte[ArrayUtils.idealByteArraySize(end - start)];
+ mChdirs = chdirs;
+ }
+ if (end - start > chs.length) {
+ chs = new char[ArrayUtils.idealCharArraySize(end - start)];
+ mChs = chs;
+ }
+ if ((end - start) * 2 > widths.length) {
+ widths = new float[ArrayUtils.idealIntArraySize((end - start) * 2)];
+ mWidths = widths;
+ }
+
+ TextUtils.getChars(source, start, end, chs, 0);
+ final int n = end - start;
+
+ boolean easy = true;
+ boolean altered = false;
+ int dir = DEFAULT_DIR; // XXX
+
+ for (int i = 0; i < n; i++) {
+ if (chs[i] >= FIRST_RIGHT_TO_LEFT) {
+ easy = false;
+ break;
+ }
+ }
+
+ if (!easy) {
+ AndroidCharacter.getDirectionalities(chs, chdirs, end - start);
+
+ /*
+ * Determine primary paragraph direction
+ */
+
+ for (int j = start; j < end; j++) {
+ int d = chdirs[j - start];
+
+ if (d == Character.DIRECTIONALITY_LEFT_TO_RIGHT) {
+ dir = DIR_LEFT_TO_RIGHT;
+ break;
+ }
+ if (d == Character.DIRECTIONALITY_RIGHT_TO_LEFT) {
+ dir = DIR_RIGHT_TO_LEFT;
+ break;
+ }
+ }
+
+ /*
+ * XXX Explicit overrides should go here
+ */
+
+ /*
+ * Weak type resolution
+ */
+
+ final byte SOR = dir == DIR_LEFT_TO_RIGHT ?
+ Character.DIRECTIONALITY_LEFT_TO_RIGHT :
+ Character.DIRECTIONALITY_RIGHT_TO_LEFT;
+
+ // dump(chdirs, n, "initial");
+
+ // W1 non spacing marks
+ for (int j = 0; j < n; j++) {
+ if (chdirs[j] == Character.NON_SPACING_MARK) {
+ if (j == 0)
+ chdirs[j] = SOR;
+ else
+ chdirs[j] = chdirs[j - 1];
+ }
+ }
+
+ // dump(chdirs, n, "W1");
+
+ // W2 european numbers
+ byte cur = SOR;
+ for (int j = 0; j < n; j++) {
+ byte d = chdirs[j];
+
+ if (d == Character.DIRECTIONALITY_LEFT_TO_RIGHT ||
+ d == Character.DIRECTIONALITY_RIGHT_TO_LEFT ||
+ d == Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC)
+ cur = d;
+ else if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER) {
+ if (cur ==
+ Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC)
+ chdirs[j] = Character.DIRECTIONALITY_ARABIC_NUMBER;
+ }
+ }
+
+ // dump(chdirs, n, "W2");
+
+ // W3 arabic letters
+ for (int j = 0; j < n; j++) {
+ if (chdirs[j] == Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC)
+ chdirs[j] = Character.DIRECTIONALITY_RIGHT_TO_LEFT;
+ }
+
+ // dump(chdirs, n, "W3");
+
+ // W4 single separator between numbers
+ for (int j = 1; j < n - 1; j++) {
+ byte d = chdirs[j];
+ byte prev = chdirs[j - 1];
+ byte next = chdirs[j + 1];
+
+ if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER_SEPARATOR) {
+ if (prev == Character.DIRECTIONALITY_EUROPEAN_NUMBER &&
+ next == Character.DIRECTIONALITY_EUROPEAN_NUMBER)
+ chdirs[j] = Character.DIRECTIONALITY_EUROPEAN_NUMBER;
+ } else if (d == Character.DIRECTIONALITY_COMMON_NUMBER_SEPARATOR) {
+ if (prev == Character.DIRECTIONALITY_EUROPEAN_NUMBER &&
+ next == Character.DIRECTIONALITY_EUROPEAN_NUMBER)
+ chdirs[j] = Character.DIRECTIONALITY_EUROPEAN_NUMBER;
+ if (prev == Character.DIRECTIONALITY_ARABIC_NUMBER &&
+ next == Character.DIRECTIONALITY_ARABIC_NUMBER)
+ chdirs[j] = Character.DIRECTIONALITY_ARABIC_NUMBER;
+ }
+ }
+
+ // dump(chdirs, n, "W4");
+
+ // W5 european number terminators
+ boolean adjacent = false;
+ for (int j = 0; j < n; j++) {
+ byte d = chdirs[j];
+
+ if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER)
+ adjacent = true;
+ else if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER_TERMINATOR && adjacent)
+ chdirs[j] = Character.DIRECTIONALITY_EUROPEAN_NUMBER;
+ else
+ adjacent = false;
+ }
+
+ //dump(chdirs, n, "W5");
+
+ // W5 european number terminators part 2,
+ // W6 separators and terminators
+ adjacent = false;
+ for (int j = n - 1; j >= 0; j--) {
+ byte d = chdirs[j];
+
+ if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER)
+ adjacent = true;
+ else if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER_TERMINATOR) {
+ if (adjacent)
+ chdirs[j] = Character.DIRECTIONALITY_EUROPEAN_NUMBER;
+ else
+ chdirs[j] = Character.DIRECTIONALITY_OTHER_NEUTRALS;
+ }
+ else {
+ adjacent = false;
+
+ if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER_SEPARATOR ||
+ d == Character.DIRECTIONALITY_COMMON_NUMBER_SEPARATOR ||
+ d == Character.DIRECTIONALITY_PARAGRAPH_SEPARATOR ||
+ d == Character.DIRECTIONALITY_SEGMENT_SEPARATOR)
+ chdirs[j] = Character.DIRECTIONALITY_OTHER_NEUTRALS;
+ }
+ }
+
+ // dump(chdirs, n, "W6");
+
+ // W7 strong direction of european numbers
+ cur = SOR;
+ for (int j = 0; j < n; j++) {
+ byte d = chdirs[j];
+
+ if (d == SOR ||
+ d == Character.DIRECTIONALITY_LEFT_TO_RIGHT ||
+ d == Character.DIRECTIONALITY_RIGHT_TO_LEFT)
+ cur = d;
+
+ if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER)
+ chdirs[j] = cur;
+ }
+
+ // dump(chdirs, n, "W7");
+
+ // N1, N2 neutrals
+ cur = SOR;
+ for (int j = 0; j < n; j++) {
+ byte d = chdirs[j];
+
+ if (d == Character.DIRECTIONALITY_LEFT_TO_RIGHT ||
+ d == Character.DIRECTIONALITY_RIGHT_TO_LEFT) {
+ cur = d;
+ } else if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER ||
+ d == Character.DIRECTIONALITY_ARABIC_NUMBER) {
+ cur = Character.DIRECTIONALITY_RIGHT_TO_LEFT;
+ } else {
+ byte dd = SOR;
+ int k;
+
+ for (k = j + 1; k < n; k++) {
+ dd = chdirs[k];
+
+ if (dd == Character.DIRECTIONALITY_LEFT_TO_RIGHT ||
+ dd == Character.DIRECTIONALITY_RIGHT_TO_LEFT) {
+ break;
+ }
+ if (dd == Character.DIRECTIONALITY_EUROPEAN_NUMBER ||
+ dd == Character.DIRECTIONALITY_ARABIC_NUMBER) {
+ dd = Character.DIRECTIONALITY_RIGHT_TO_LEFT;
+ break;
+ }
+ }
+
+ for (int y = j; y < k; y++) {
+ if (dd == cur)
+ chdirs[y] = cur;
+ else
+ chdirs[y] = SOR;
+ }
+
+ j = k - 1;
+ }
+ }
+
+ // dump(chdirs, n, "final");
+
+ // extra: enforce that all tabs go the primary direction
+
+ for (int j = 0; j < n; j++) {
+ if (chs[j] == '\t')
+ chdirs[j] = SOR;
+ }
+
+ // extra: enforce that object replacements go to the
+ // primary direction
+ // and that none of the underlying characters are treated
+ // as viable breakpoints
+
+ if (source instanceof Spanned) {
+ Spanned sp = (Spanned) source;
+ ReplacementSpan[] spans = sp.getSpans(start, end, ReplacementSpan.class);
+
+ for (int y = 0; y < spans.length; y++) {
+ int a = sp.getSpanStart(spans[y]);
+ int b = sp.getSpanEnd(spans[y]);
+
+ for (int x = a; x < b; x++) {
+ chdirs[x - start] = SOR;
+ chs[x - start] = '\uFFFC';
+ }
+ }
+ }
+
+ // Do mirroring for right-to-left segments
+
+ for (int i = 0; i < n; i++) {
+ if (chdirs[i] == Character.DIRECTIONALITY_RIGHT_TO_LEFT) {
+ int j;
+
+ for (j = i; j < n; j++) {
+ if (chdirs[j] !=
+ Character.DIRECTIONALITY_RIGHT_TO_LEFT)
+ break;
+ }
+
+ if (AndroidCharacter.mirror(chs, i, j - i))
+ altered = true;
+
+ i = j - 1;
+ }
+ }
+ }
+
+ CharSequence sub;
+
+ if (altered) {
+ if (alter == null)
+ alter = AlteredCharSequence.make(source, chs, start, end);
+ else
+ alter.update(chs, start, end);
+
+ sub = alter;
+ } else {
+ sub = source;
+ }
+
+ int width = firstwidth;
+
+ float w = 0;
+ int here = start;
+
+ int ok = start;
+ float okwidth = w;
+ int okascent = 0, okdescent = 0, oktop = 0, okbottom = 0;
+
+ int fit = start;
+ float fitwidth = w;
+ int fitascent = 0, fitdescent = 0, fittop = 0, fitbottom = 0;
+
+ boolean tab = false;
+
+ int next;
+ for (int i = start; i < end; i = next) {
+ if (spanned == null)
+ next = end;
+ else
+ next = spanned.nextSpanTransition(i, end,
+ MetricAffectingSpan.
+ class);
+
+ if (spanned == null) {
+ paint.getTextWidths(sub, i, next, widths);
+ System.arraycopy(widths, 0, widths,
+ end - start + (i - start), next - i);
+
+ paint.getFontMetricsInt(fm);
+ } else {
+ mWorkPaint.baselineShift = 0;
+
+ Styled.getTextWidths(paint, mWorkPaint,
+ spanned, i, next,
+ widths, fm);
+ System.arraycopy(widths, 0, widths,
+ end - start + (i - start), next - i);
+
+ if (mWorkPaint.baselineShift < 0) {
+ fm.ascent += mWorkPaint.baselineShift;
+ fm.top += mWorkPaint.baselineShift;
+ } else {
+ fm.descent += mWorkPaint.baselineShift;
+ fm.bottom += mWorkPaint.baselineShift;
+ }
+ }
+
+ int fmtop = fm.top;
+ int fmbottom = fm.bottom;
+ int fmascent = fm.ascent;
+ int fmdescent = fm.descent;
+
+ if (false) {
+ StringBuilder sb = new StringBuilder();
+ for (int j = i; j < next; j++) {
+ sb.append(widths[j - start + (end - start)]);
+ sb.append(' ');
+ }
+
+ Log.e("text", sb.toString());
+ }
+
+ for (int j = i; j < next; j++) {
+ char c = chs[j - start];
+ float before = w;
+
+ switch (c) {
+ case '\n':
+ break;
+
+ case '\t':
+ w = Layout.nextTab(sub, start, end, w, null);
+ tab = true;
+ break;
+
+ default:
+ w += widths[j - start + (end - start)];
+ }
+
+ // Log.e("text", "was " + before + " now " + w + " after " + c + " within " + width);
+
+ if (w <= width) {
+ fitwidth = w;
+ fit = j + 1;
+
+ if (fmtop < fittop)
+ fittop = fmtop;
+ if (fmascent < fitascent)
+ fitascent = fmascent;
+ if (fmdescent > fitdescent)
+ fitdescent = fmdescent;
+ if (fmbottom > fitbottom)
+ fitbottom = fmbottom;
+
+ if (c == ' ' || c == '\t') {
+ okwidth = w;
+ ok = j + 1;
+
+ if (fittop < oktop)
+ oktop = fittop;
+ if (fitascent < okascent)
+ okascent = fitascent;
+ if (fitdescent > okdescent)
+ okdescent = fitdescent;
+ if (fitbottom > okbottom)
+ okbottom = fitbottom;
+ }
+ } else if (breakOnlyAtSpaces) {
+ if (ok != here) {
+ // Log.e("text", "output ok " + here + " to " +ok);
+ v = out(source,
+ here, ok,
+ okascent, okdescent, oktop, okbottom,
+ v,
+ spacingmult, spacingadd, chooseht,
+ choosehtv, fm, tab,
+ needMultiply, start, chdirs, dir, easy,
+ ok == bufend, includepad, trackpad,
+ widths, start, end - start,
+ where, ellipsizedWidth, okwidth,
+ paint);
+
+ here = ok;
+ } else {
+ // Act like it fit even though it didn't.
+
+ fitwidth = w;
+ fit = j + 1;
+
+ if (fmtop < fittop)
+ fittop = fmtop;
+ if (fmascent < fitascent)
+ fitascent = fmascent;
+ if (fmdescent > fitdescent)
+ fitdescent = fmdescent;
+ if (fmbottom > fitbottom)
+ fitbottom = fmbottom;
+ }
+ } else {
+ if (ok != here) {
+ // Log.e("text", "output ok " + here + " to " +ok);
+ v = out(source,
+ here, ok,
+ okascent, okdescent, oktop, okbottom,
+ v,
+ spacingmult, spacingadd, chooseht,
+ choosehtv, fm, tab,
+ needMultiply, start, chdirs, dir, easy,
+ ok == bufend, includepad, trackpad,
+ widths, start, end - start,
+ where, ellipsizedWidth, okwidth,
+ paint);
+
+ here = ok;
+ } else if (fit != here) {
+ // Log.e("text", "output fit " + here + " to " +fit);
+ v = out(source,
+ here, fit,
+ fitascent, fitdescent,
+ fittop, fitbottom,
+ v,
+ spacingmult, spacingadd, chooseht,
+ choosehtv, fm, tab,
+ needMultiply, start, chdirs, dir, easy,
+ fit == bufend, includepad, trackpad,
+ widths, start, end - start,
+ where, ellipsizedWidth, fitwidth,
+ paint);
+
+ here = fit;
+ } else {
+ // Log.e("text", "output one " + here + " to " +(here + 1));
+ measureText(paint, mWorkPaint,
+ source, here, here + 1, fm, tab,
+ null);
+
+ v = out(source,
+ here, here+1,
+ fm.ascent, fm.descent,
+ fm.top, fm.bottom,
+ v,
+ spacingmult, spacingadd, chooseht,
+ choosehtv, fm, tab,
+ needMultiply, start, chdirs, dir, easy,
+ here + 1 == bufend, includepad,
+ trackpad,
+ widths, start, end - start,
+ where, ellipsizedWidth,
+ widths[here - start], paint);
+
+ here = here + 1;
+ }
+
+ if (here < i) {
+ j = next = here; // must remeasure
+ } else {
+ j = here - 1; // continue looping
+ }
+
+ ok = fit = here;
+ w = 0;
+ fitascent = fitdescent = fittop = fitbottom = 0;
+ okascent = okdescent = oktop = okbottom = 0;
+
+ width = restwidth;
+ }
+ }
+ }
+
+ if (end != here) {
+ if ((fittop | fitbottom | fitdescent | fitascent) == 0) {
+ paint.getFontMetricsInt(fm);
+
+ fittop = fm.top;
+ fitbottom = fm.bottom;
+ fitascent = fm.ascent;
+ fitdescent = fm.descent;
+ }
+
+ // Log.e("text", "output rest " + here + " to " + end);
+
+ v = out(source,
+ here, end, fitascent, fitdescent,
+ fittop, fitbottom,
+ v,
+ spacingmult, spacingadd, chooseht,
+ choosehtv, fm, tab,
+ needMultiply, start, chdirs, dir, easy,
+ end == bufend, includepad, trackpad,
+ widths, start, end - start,
+ where, ellipsizedWidth, w, paint);
+ }
+
+ start = end;
+
+ if (end == bufend)
+ break;
+ }
+
+ if (bufend == bufstart || source.charAt(bufend - 1) == '\n') {
+ // Log.e("text", "output last " + bufend);
+
+ paint.getFontMetricsInt(fm);
+
+ v = out(source,
+ bufend, bufend, fm.ascent, fm.descent,
+ fm.top, fm.bottom,
+ v,
+ spacingmult, spacingadd, null,
+ null, fm, false,
+ needMultiply, bufend, chdirs, DEFAULT_DIR, true,
+ true, includepad, trackpad,
+ widths, bufstart, 0,
+ where, ellipsizedWidth, 0, paint);
+ }
+ }
+
+/*
+ private static void dump(byte[] data, int count, String label) {
+ if (false) {
+ System.out.print(label);
+
+ for (int i = 0; i < count; i++)
+ System.out.print(" " + data[i]);
+
+ System.out.println();
+ }
+ }
+*/
+
+ private static int getFit(TextPaint paint,
+ TextPaint workPaint,
+ CharSequence text, int start, int end,
+ float wid) {
+ int high = end + 1, low = start - 1, guess;
+
+ while (high - low > 1) {
+ guess = (high + low) / 2;
+
+ if (measureText(paint, workPaint,
+ text, start, guess, null, true, null) > wid)
+ high = guess;
+ else
+ low = guess;
+ }
+
+ if (low < start)
+ return start;
+ else
+ return low;
+ }
+
+ private int out(CharSequence text, int start, int end,
+ int above, int below, int top, int bottom, int v,
+ float spacingmult, float spacingadd,
+ LineHeightSpan[] chooseht, int[] choosehtv,
+ Paint.FontMetricsInt fm, boolean tab,
+ boolean needMultiply, int pstart, byte[] chdirs,
+ int dir, boolean easy, boolean last,
+ boolean includepad, boolean trackpad,
+ float[] widths, int widstart, int widoff,
+ TextUtils.TruncateAt ellipsize, float ellipsiswidth,
+ float textwidth, TextPaint paint) {
+ int j = mLineCount;
+ int off = j * mColumns;
+ int want = off + mColumns + TOP;
+ int[] lines = mLines;
+
+ // Log.e("text", "line " + start + " to " + end + (last ? "===" : ""));
+
+ if (want >= lines.length) {
+ int nlen = ArrayUtils.idealIntArraySize(want + 1);
+ int[] grow = new int[nlen];
+ System.arraycopy(lines, 0, grow, 0, lines.length);
+ mLines = grow;
+ lines = grow;
+
+ Directions[] grow2 = new Directions[nlen];
+ System.arraycopy(mLineDirections, 0, grow2, 0,
+ mLineDirections.length);
+ mLineDirections = grow2;
+ }
+
+ if (chooseht != null) {
+ fm.ascent = above;
+ fm.descent = below;
+ fm.top = top;
+ fm.bottom = bottom;
+
+ for (int i = 0; i < chooseht.length; i++) {
+ chooseht[i].chooseHeight(text, start, end, choosehtv[i], v, fm);
+ }
+
+ above = fm.ascent;
+ below = fm.descent;
+ top = fm.top;
+ bottom = fm.bottom;
+ }
+
+ if (j == 0) {
+ if (trackpad) {
+ mTopPadding = top - above;
+ }
+
+ if (includepad) {
+ above = top;
+ }
+ }
+ if (last) {
+ if (trackpad) {
+ mBottomPadding = bottom - below;
+ }
+
+ if (includepad) {
+ below = bottom;
+ }
+ }
+
+ int extra;
+
+ if (needMultiply) {
+ extra = (int) ((below - above) * (spacingmult - 1)
+ + spacingadd + 0.5);
+ } else {
+ extra = 0;
+ }
+
+ lines[off + START] = start;
+ lines[off + TOP] = v;
+ lines[off + DESCENT] = below + extra;
+
+ v += (below - above) + extra;
+ lines[off + mColumns + START] = end;
+ lines[off + mColumns + TOP] = v;
+
+ if (tab)
+ lines[off + TAB] |= TAB_MASK;
+
+ {
+ lines[off + DIR] |= dir << DIR_SHIFT;
+
+ int cur = Character.DIRECTIONALITY_LEFT_TO_RIGHT;
+ int count = 0;
+
+ if (!easy) {
+ for (int k = start; k < end; k++) {
+ if (chdirs[k - pstart] != cur) {
+ count++;
+ cur = chdirs[k - pstart];
+ }
+ }
+ }
+
+ Directions linedirs;
+
+ if (count == 0) {
+ linedirs = DIRS_ALL_LEFT_TO_RIGHT;
+ } else {
+ short[] ld = new short[count + 1];
+
+ cur = Character.DIRECTIONALITY_LEFT_TO_RIGHT;
+ count = 0;
+ int here = start;
+
+ for (int k = start; k < end; k++) {
+ if (chdirs[k - pstart] != cur) {
+ // XXX check to make sure we don't
+ // overflow short
+ ld[count++] = (short) (k - here);
+ cur = chdirs[k - pstart];
+ here = k;
+ }
+ }
+
+ ld[count] = (short) (end - here);
+
+ if (count == 1 && ld[0] == 0) {
+ linedirs = DIRS_ALL_RIGHT_TO_LEFT;
+ } else {
+ linedirs = new Directions(ld);
+ }
+ }
+
+ mLineDirections[j] = linedirs;
+
+ if (ellipsize != null) {
+ calculateEllipsis(start, end, widths, widstart, widoff,
+ ellipsiswidth, ellipsize, j,
+ textwidth, paint);
+ }
+ }
+
+ mLineCount++;
+ return v;
+ }
+
+ private void calculateEllipsis(int linestart, int lineend,
+ float[] widths, int widstart, int widoff,
+ float avail, TextUtils.TruncateAt where,
+ int line, float textwidth, TextPaint paint) {
+ int len = lineend - linestart;
+
+ if (textwidth <= avail) {
+ // Everything fits!
+ mLines[mColumns * line + ELLIPSIS_START] = 0;
+ mLines[mColumns * line + ELLIPSIS_COUNT] = 0;
+ return;
+ }
+
+ float ellipsiswid = paint.measureText("\u2026");
+ int ellipsisStart, ellipsisCount;
+
+ if (where == TextUtils.TruncateAt.START) {
+ float sum = 0;
+ int i;
+
+ for (i = len; i >= 0; i--) {
+ float w = widths[i - 1 + linestart - widstart + widoff];
+
+ if (w + sum + ellipsiswid > avail) {
+ break;
+ }
+
+ sum += w;
+ }
+
+ ellipsisStart = 0;
+ ellipsisCount = i;
+ } else if (where == TextUtils.TruncateAt.END) {
+ float sum = 0;
+ int i;
+
+ for (i = 0; i < len; i++) {
+ float w = widths[i + linestart - widstart + widoff];
+
+ if (w + sum + ellipsiswid > avail) {
+ break;
+ }
+
+ sum += w;
+ }
+
+ ellipsisStart = i;
+ ellipsisCount = len - i;
+ } else /* where = TextUtils.TruncateAt.MIDDLE */ {
+ float lsum = 0, rsum = 0;
+ int left = 0, right = len;
+
+ float ravail = (avail - ellipsiswid) / 2;
+ for (right = len; right >= 0; right--) {
+ float w = widths[right - 1 + linestart - widstart + widoff];
+
+ if (w + rsum > ravail) {
+ break;
+ }
+
+ rsum += w;
+ }
+
+ float lavail = avail - ellipsiswid - rsum;
+ for (left = 0; left < right; left++) {
+ float w = widths[left + linestart - widstart + widoff];
+
+ if (w + lsum > lavail) {
+ break;
+ }
+
+ lsum += w;
+ }
+
+ ellipsisStart = left;
+ ellipsisCount = right - left;
+ }
+
+ mLines[mColumns * line + ELLIPSIS_START] = ellipsisStart;
+ mLines[mColumns * line + ELLIPSIS_COUNT] = ellipsisCount;
+ }
+
+ // Override the baseclass so we can directly access our members,
+ // rather than relying on member functions.
+ // The logic mirrors that of Layout.getLineForVertical
+ // FIXME: It may be faster to do a linear search for layouts without many lines.
+ public int getLineForVertical(int vertical) {
+ int high = mLineCount;
+ int low = -1;
+ int guess;
+ int[] lines = mLines;
+ while (high - low > 1) {
+ guess = (high + low) >> 1;
+ if (lines[mColumns * guess + TOP] > vertical){
+ high = guess;
+ } else {
+ low = guess;
+ }
+ }
+ if (low < 0) {
+ return 0;
+ } else {
+ return low;
+ }
+ }
+
+ public int getLineCount() {
+ return mLineCount;
+ }
+
+ public int getLineTop(int line) {
+ return mLines[mColumns * line + TOP];
+ }
+
+ public int getLineDescent(int line) {
+ return mLines[mColumns * line + DESCENT];
+ }
+
+ public int getLineStart(int line) {
+ return mLines[mColumns * line + START] & START_MASK;
+ }
+
+ public int getParagraphDirection(int line) {
+ return mLines[mColumns * line + DIR] >> DIR_SHIFT;
+ }
+
+ public boolean getLineContainsTab(int line) {
+ return (mLines[mColumns * line + TAB] & TAB_MASK) != 0;
+ }
+
+ public final Directions getLineDirections(int line) {
+ return mLineDirections[line];
+ }
+
+ public int getTopPadding() {
+ return mTopPadding;
+ }
+
+ public int getBottomPadding() {
+ return mBottomPadding;
+ }
+
+ @Override
+ public int getEllipsisCount(int line) {
+ if (mColumns < COLUMNS_ELLIPSIZE) {
+ return 0;
+ }
+
+ return mLines[mColumns * line + ELLIPSIS_COUNT];
+ }
+
+ @Override
+ public int getEllipsisStart(int line) {
+ if (mColumns < COLUMNS_ELLIPSIZE) {
+ return 0;
+ }
+
+ return mLines[mColumns * line + ELLIPSIS_START];
+ }
+
+ @Override
+ public int getEllipsizedWidth() {
+ return mEllipsizedWidth;
+ }
+
+ private int mLineCount;
+ private int mTopPadding, mBottomPadding;
+ private int mColumns;
+ private int mEllipsizedWidth;
+
+ private static final int COLUMNS_NORMAL = 3;
+ private static final int COLUMNS_ELLIPSIZE = 5;
+ private static final int START = 0;
+ private static final int DIR = START;
+ private static final int TAB = START;
+ private static final int TOP = 1;
+ private static final int DESCENT = 2;
+ private static final int ELLIPSIS_START = 3;
+ private static final int ELLIPSIS_COUNT = 4;
+
+ private int[] mLines;
+ private Directions[] mLineDirections;
+
+ private static final int START_MASK = 0x1FFFFFFF;
+ private static final int DIR_MASK = 0xC0000000;
+ private static final int DIR_SHIFT = 30;
+ private static final int TAB_MASK = 0x20000000;
+
+ private static final char FIRST_RIGHT_TO_LEFT = '\u0590';
+
+ /*
+ * These are reused across calls to generate()
+ */
+ private byte[] mChdirs;
+ private char[] mChs;
+ private float[] mWidths;
+ private Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt();
+}
diff --git a/core/java/android/text/Styled.java b/core/java/android/text/Styled.java
new file mode 100644
index 0000000..05c27ea
--- /dev/null
+++ b/core/java/android/text/Styled.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text;
+
+import android.graphics.Paint;
+import android.graphics.Canvas;
+import android.graphics.Path;
+import android.graphics.RectF;
+import android.graphics.Typeface;
+import android.graphics.MaskFilter;
+import android.graphics.Rasterizer;
+import android.graphics.LayerRasterizer;
+import android.text.style.*;
+
+/* package */ class Styled
+{
+ private static float each(Canvas canvas,
+ Spanned text, int start, int end,
+ int dir, boolean reverse,
+ float x, int top, int y, int bottom,
+ Paint.FontMetricsInt fm,
+ TextPaint realPaint,
+ TextPaint paint,
+ boolean needwid) {
+
+ boolean havewid = false;
+ float ret = 0;
+ CharacterStyle[] spans = text.getSpans(start, end, CharacterStyle.class);
+
+ ReplacementSpan replacement = null;
+
+ realPaint.bgColor = 0;
+ realPaint.baselineShift = 0;
+ paint.set(realPaint);
+
+ if (spans.length > 0) {
+ for (int i = 0; i < spans.length; i++) {
+ CharacterStyle span = spans[i];
+
+ if (span instanceof ReplacementSpan) {
+ replacement = (ReplacementSpan)span;
+ }
+ else {
+ span.updateDrawState(paint);
+ }
+ }
+ }
+
+ if (replacement == null) {
+ CharSequence tmp;
+ int tmpstart, tmpend;
+
+ if (reverse) {
+ tmp = TextUtils.getReverse(text, start, end);
+ tmpstart = 0;
+ tmpend = end - start;
+ } else {
+ tmp = text;
+ tmpstart = start;
+ tmpend = end;
+ }
+
+ if (fm != null) {
+ paint.getFontMetricsInt(fm);
+ }
+
+ if (canvas != null) {
+ if (paint.bgColor != 0) {
+ int c = paint.getColor();
+ Paint.Style s = paint.getStyle();
+ paint.setColor(paint.bgColor);
+ paint.setStyle(Paint.Style.FILL);
+
+ if (!havewid) {
+ ret = paint.measureText(tmp, tmpstart, tmpend);
+ havewid = true;
+ }
+
+ if (dir == Layout.DIR_RIGHT_TO_LEFT)
+ canvas.drawRect(x - ret, top, x, bottom, paint);
+ else
+ canvas.drawRect(x, top, x + ret, bottom, paint);
+
+ paint.setStyle(s);
+ paint.setColor(c);
+ }
+
+ if (dir == Layout.DIR_RIGHT_TO_LEFT) {
+ if (!havewid) {
+ ret = paint.measureText(tmp, tmpstart, tmpend);
+ havewid = true;
+ }
+
+ canvas.drawText(tmp, tmpstart, tmpend,
+ x - ret, y + paint.baselineShift, paint);
+ } else {
+ if (needwid) {
+ if (!havewid) {
+ ret = paint.measureText(tmp, tmpstart, tmpend);
+ havewid = true;
+ }
+ }
+
+ canvas.drawText(tmp, tmpstart, tmpend,
+ x, y + paint.baselineShift, paint);
+ }
+ } else {
+ if (needwid && !havewid) {
+ ret = paint.measureText(tmp, tmpstart, tmpend);
+ havewid = true;
+ }
+ }
+ } else {
+ ret = replacement.getSize(paint, text, start, end, fm);
+
+ if (canvas != null) {
+ if (dir == Layout.DIR_RIGHT_TO_LEFT)
+ replacement.draw(canvas, text, start, end,
+ x - ret, top, y, bottom, paint);
+ else
+ replacement.draw(canvas, text, start, end,
+ x, top, y, bottom, paint);
+ }
+ }
+
+ if (dir == Layout.DIR_RIGHT_TO_LEFT)
+ return -ret;
+ else
+ return ret;
+ }
+
+ public static int getTextWidths(TextPaint realPaint,
+ TextPaint paint,
+ Spanned text, int start, int end,
+ float[] widths, Paint.FontMetricsInt fm) {
+
+ MetricAffectingSpan[] spans = text.getSpans(start, end, MetricAffectingSpan.class);
+
+ ReplacementSpan replacement = null;
+ paint.set(realPaint);
+
+ for (int i = 0; i < spans.length; i++) {
+ MetricAffectingSpan span = spans[i];
+ if (span instanceof ReplacementSpan) {
+ replacement = (ReplacementSpan)span;
+ }
+ else {
+ span.updateMeasureState(paint);
+ }
+ }
+
+ if (replacement == null) {
+ paint.getFontMetricsInt(fm);
+ paint.getTextWidths(text, start, end, widths);
+ } else {
+ int wid = replacement.getSize(paint, text, start, end, fm);
+
+ if (end > start) {
+ widths[0] = wid;
+
+ for (int i = start + 1; i < end; i++)
+ widths[i - start] = 0;
+ }
+ }
+ return end - start;
+ }
+
+ private static float foreach(Canvas canvas,
+ CharSequence text, int start, int end,
+ int dir, boolean reverse,
+ float x, int top, int y, int bottom,
+ Paint.FontMetricsInt fm,
+ TextPaint paint,
+ TextPaint workPaint,
+ boolean needwid) {
+ if (! (text instanceof Spanned)) {
+ float ret = 0;
+
+ if (reverse) {
+ CharSequence tmp = TextUtils.getReverse(text, start, end);
+ int tmpend = end - start;
+
+ if (canvas != null || needwid)
+ ret = paint.measureText(tmp, 0, tmpend);
+
+ if (canvas != null)
+ canvas.drawText(tmp, 0, tmpend,
+ x - ret, y, paint);
+ } else {
+ if (needwid)
+ ret = paint.measureText(text, start, end);
+
+ if (canvas != null)
+ canvas.drawText(text, start, end, x, y, paint);
+ }
+
+ if (fm != null) {
+ paint.getFontMetricsInt(fm);
+ }
+
+ return ret * dir; //Layout.DIR_RIGHT_TO_LEFT == -1
+ }
+
+ float ox = x;
+ int asc = 0, desc = 0;
+ int ftop = 0, fbot = 0;
+
+ Spanned sp = (Spanned) text;
+ Class division;
+
+ if (canvas == null)
+ division = MetricAffectingSpan.class;
+ else
+ division = CharacterStyle.class;
+
+ int next;
+ for (int i = start; i < end; i = next) {
+ next = sp.nextSpanTransition(i, end, division);
+
+ x += each(canvas, sp, i, next, dir, reverse,
+ x, top, y, bottom, fm, paint, workPaint,
+ needwid || next != end);
+
+ if (fm != null) {
+ if (fm.ascent < asc)
+ asc = fm.ascent;
+ if (fm.descent > desc)
+ desc = fm.descent;
+
+ if (fm.top < ftop)
+ ftop = fm.top;
+ if (fm.bottom > fbot)
+ fbot = fm.bottom;
+ }
+ }
+
+ if (fm != null) {
+ if (start == end) {
+ paint.getFontMetricsInt(fm);
+ } else {
+ fm.ascent = asc;
+ fm.descent = desc;
+ fm.top = ftop;
+ fm.bottom = fbot;
+ }
+ }
+
+ return x - ox;
+ }
+
+ public static float drawText(Canvas canvas,
+ CharSequence text, int start, int end,
+ int dir, boolean reverse,
+ float x, int top, int y, int bottom,
+ TextPaint paint,
+ TextPaint workPaint,
+ boolean needwid) {
+ if ((dir == Layout.DIR_RIGHT_TO_LEFT && !reverse)||(reverse && dir == Layout.DIR_LEFT_TO_RIGHT)) {
+ float ch = foreach(null, text, start, end, Layout.DIR_LEFT_TO_RIGHT,
+ false, 0, 0, 0, 0, null, paint, workPaint,
+ true);
+
+ ch *= dir; // DIR_RIGHT_TO_LEFT == -1
+ foreach(canvas, text, start, end, -dir,
+ reverse, x + ch, top, y, bottom, null, paint,
+ workPaint, true);
+
+ return ch;
+ }
+
+ return foreach(canvas, text, start, end, dir, reverse,
+ x, top, y, bottom, null, paint, workPaint,
+ needwid);
+ }
+
+ public static float measureText(TextPaint paint,
+ TextPaint workPaint,
+ CharSequence text, int start, int end,
+ Paint.FontMetricsInt fm) {
+ return foreach(null, text, start, end,
+ Layout.DIR_LEFT_TO_RIGHT, false,
+ 0, 0, 0, 0, fm, paint, workPaint, true);
+ }
+}
diff --git a/core/java/android/text/TextPaint.java b/core/java/android/text/TextPaint.java
new file mode 100644
index 0000000..f13820d
--- /dev/null
+++ b/core/java/android/text/TextPaint.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text;
+
+import android.graphics.Paint;
+
+/**
+ * TextPaint is an extension of Paint that leaves room for some extra
+ * data used during text measuring and drawing.
+ */
+public class TextPaint extends Paint {
+ public int bgColor;
+ public int baselineShift;
+ public int linkColor;
+ public int[] drawableState;
+
+ public TextPaint() {
+ super();
+ }
+
+ public TextPaint(int flags) {
+ super(flags);
+ }
+
+ public TextPaint(Paint p) {
+ super(p);
+ }
+
+ /**
+ * Copy the fields from tp into this TextPaint, including the
+ * fields inherited from Paint.
+ */
+ public void set(TextPaint tp) {
+ super.set(tp);
+
+ bgColor = tp.bgColor;
+ baselineShift = tp.baselineShift;
+ linkColor = tp.linkColor;
+ drawableState = tp.drawableState;
+ }
+}
diff --git a/core/java/android/text/TextUtils.java b/core/java/android/text/TextUtils.java
new file mode 100644
index 0000000..e791aaf
--- /dev/null
+++ b/core/java/android/text/TextUtils.java
@@ -0,0 +1,1570 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text;
+
+import com.android.internal.R;
+
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.style.AbsoluteSizeSpan;
+import android.text.style.AlignmentSpan;
+import android.text.style.BackgroundColorSpan;
+import android.text.style.BulletSpan;
+import android.text.style.CharacterStyle;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.LeadingMarginSpan;
+import android.text.style.MetricAffectingSpan;
+import android.text.style.QuoteSpan;
+import android.text.style.RelativeSizeSpan;
+import android.text.style.ReplacementSpan;
+import android.text.style.ScaleXSpan;
+import android.text.style.StrikethroughSpan;
+import android.text.style.StyleSpan;
+import android.text.style.SubscriptSpan;
+import android.text.style.SuperscriptSpan;
+import android.text.style.TextAppearanceSpan;
+import android.text.style.TypefaceSpan;
+import android.text.style.URLSpan;
+import android.text.style.UnderlineSpan;
+import com.android.internal.util.ArrayUtils;
+
+import java.util.regex.Pattern;
+import java.util.Iterator;
+
+public class TextUtils
+{
+ private TextUtils() { /* cannot be instantiated */ }
+
+ private static String[] EMPTY_STRING_ARRAY = new String[]{};
+
+ public static void getChars(CharSequence s, int start, int end,
+ char[] dest, int destoff) {
+ Class c = s.getClass();
+
+ if (c == String.class)
+ ((String) s).getChars(start, end, dest, destoff);
+ else if (c == StringBuffer.class)
+ ((StringBuffer) s).getChars(start, end, dest, destoff);
+ else if (c == StringBuilder.class)
+ ((StringBuilder) s).getChars(start, end, dest, destoff);
+ else if (s instanceof GetChars)
+ ((GetChars) s).getChars(start, end, dest, destoff);
+ else {
+ for (int i = start; i < end; i++)
+ dest[destoff++] = s.charAt(i);
+ }
+ }
+
+ public static int indexOf(CharSequence s, char ch) {
+ return indexOf(s, ch, 0);
+ }
+
+ public static int indexOf(CharSequence s, char ch, int start) {
+ Class c = s.getClass();
+
+ if (c == String.class)
+ return ((String) s).indexOf(ch, start);
+
+ return indexOf(s, ch, start, s.length());
+ }
+
+ public static int indexOf(CharSequence s, char ch, int start, int end) {
+ Class c = s.getClass();
+
+ if (s instanceof GetChars || c == StringBuffer.class ||
+ c == StringBuilder.class || c == String.class) {
+ final int INDEX_INCREMENT = 500;
+ char[] temp = obtain(INDEX_INCREMENT);
+
+ while (start < end) {
+ int segend = start + INDEX_INCREMENT;
+ if (segend > end)
+ segend = end;
+
+ getChars(s, start, segend, temp, 0);
+
+ int count = segend - start;
+ for (int i = 0; i < count; i++) {
+ if (temp[i] == ch) {
+ recycle(temp);
+ return i + start;
+ }
+ }
+
+ start = segend;
+ }
+
+ recycle(temp);
+ return -1;
+ }
+
+ for (int i = start; i < end; i++)
+ if (s.charAt(i) == ch)
+ return i;
+
+ return -1;
+ }
+
+ public static int lastIndexOf(CharSequence s, char ch) {
+ return lastIndexOf(s, ch, s.length() - 1);
+ }
+
+ public static int lastIndexOf(CharSequence s, char ch, int last) {
+ Class c = s.getClass();
+
+ if (c == String.class)
+ return ((String) s).lastIndexOf(ch, last);
+
+ return lastIndexOf(s, ch, 0, last);
+ }
+
+ public static int lastIndexOf(CharSequence s, char ch,
+ int start, int last) {
+ if (last < 0)
+ return -1;
+ if (last >= s.length())
+ last = s.length() - 1;
+
+ int end = last + 1;
+
+ Class c = s.getClass();
+
+ if (s instanceof GetChars || c == StringBuffer.class ||
+ c == StringBuilder.class || c == String.class) {
+ final int INDEX_INCREMENT = 500;
+ char[] temp = obtain(INDEX_INCREMENT);
+
+ while (start < end) {
+ int segstart = end - INDEX_INCREMENT;
+ if (segstart < start)
+ segstart = start;
+
+ getChars(s, segstart, end, temp, 0);
+
+ int count = end - segstart;
+ for (int i = count - 1; i >= 0; i--) {
+ if (temp[i] == ch) {
+ recycle(temp);
+ return i + segstart;
+ }
+ }
+
+ end = segstart;
+ }
+
+ recycle(temp);
+ return -1;
+ }
+
+ for (int i = end - 1; i >= start; i--)
+ if (s.charAt(i) == ch)
+ return i;
+
+ return -1;
+ }
+
+ public static int indexOf(CharSequence s, CharSequence needle) {
+ return indexOf(s, needle, 0, s.length());
+ }
+
+ public static int indexOf(CharSequence s, CharSequence needle, int start) {
+ return indexOf(s, needle, start, s.length());
+ }
+
+ public static int indexOf(CharSequence s, CharSequence needle,
+ int start, int end) {
+ int nlen = needle.length();
+ if (nlen == 0)
+ return start;
+
+ char c = needle.charAt(0);
+
+ for (;;) {
+ start = indexOf(s, c, start);
+ if (start > end - nlen) {
+ break;
+ }
+
+ if (start < 0) {
+ return -1;
+ }
+
+ if (regionMatches(s, start, needle, 0, nlen)) {
+ return start;
+ }
+
+ start++;
+ }
+ return -1;
+ }
+
+ public static boolean regionMatches(CharSequence one, int toffset,
+ CharSequence two, int ooffset,
+ int len) {
+ char[] temp = obtain(2 * len);
+
+ getChars(one, toffset, toffset + len, temp, 0);
+ getChars(two, ooffset, ooffset + len, temp, len);
+
+ boolean match = true;
+ for (int i = 0; i < len; i++) {
+ if (temp[i] != temp[i + len]) {
+ match = false;
+ break;
+ }
+ }
+
+ recycle(temp);
+ return match;
+ }
+
+ public static String substring(CharSequence source, int start, int end) {
+ if (source instanceof String)
+ return ((String) source).substring(start, end);
+ if (source instanceof StringBuilder)
+ return ((StringBuilder) source).substring(start, end);
+ if (source instanceof StringBuffer)
+ return ((StringBuffer) source).substring(start, end);
+
+ char[] temp = obtain(end - start);
+ getChars(source, start, end, temp, 0);
+ String ret = new String(temp, 0, end - start);
+ recycle(temp);
+
+ return ret;
+ }
+
+ /**
+ * Returns a string containing the tokens joined by delimiters.
+ * @param tokens an array objects to be joined. Strings will be formed from
+ * the objects by calling object.toString().
+ */
+ public static String join(CharSequence delimiter, Object[] tokens) {
+ StringBuilder sb = new StringBuilder();
+ boolean firstTime = true;
+ for (Object token: tokens) {
+ if (firstTime) {
+ firstTime = false;
+ } else {
+ sb.append(delimiter);
+ }
+ sb.append(token);
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Returns a string containing the tokens joined by delimiters.
+ * @param tokens an array objects to be joined. Strings will be formed from
+ * the objects by calling object.toString().
+ */
+ public static String join(CharSequence delimiter, Iterable tokens) {
+ StringBuilder sb = new StringBuilder();
+ boolean firstTime = true;
+ for (Object token: tokens) {
+ if (firstTime) {
+ firstTime = false;
+ } else {
+ sb.append(delimiter);
+ }
+ sb.append(token);
+ }
+ return sb.toString();
+ }
+
+ /**
+ * String.split() returns [''] when the string to be split is empty. This returns []. This does
+ * not remove any empty strings from the result. For example split("a,", "," ) returns {"a", ""}.
+ *
+ * @param text the string to split
+ * @param expression the regular expression to match
+ * @return an array of strings. The array will be empty if text is empty
+ *
+ * @throws NullPointerException if expression or text is null
+ */
+ public static String[] split(String text, String expression) {
+ if (text.length() == 0) {
+ return EMPTY_STRING_ARRAY;
+ } else {
+ return text.split(expression, -1);
+ }
+ }
+
+ /**
+ * Splits a string on a pattern. String.split() returns [''] when the string to be
+ * split is empty. This returns []. This does not remove any empty strings from the result.
+ * @param text the string to split
+ * @param pattern the regular expression to match
+ * @return an array of strings. The array will be empty if text is empty
+ *
+ * @throws NullPointerException if expression or text is null
+ */
+ public static String[] split(String text, Pattern pattern) {
+ if (text.length() == 0) {
+ return EMPTY_STRING_ARRAY;
+ } else {
+ return pattern.split(text, -1);
+ }
+ }
+
+ /**
+ * An interface for splitting strings according to rules that are opaque to the user of this
+ * interface. This also has less overhead than split, which uses regular expressions and
+ * allocates an array to hold the results.
+ *
+ * <p>The most efficient way to use this class is:
+ *
+ * <pre>
+ * // Once
+ * TextUtils.StringSplitter splitter = new TextUtils.SimpleStringSplitter(delimiter);
+ *
+ * // Once per string to split
+ * splitter.setString(string);
+ * for (String s : splitter) {
+ * ...
+ * }
+ * </pre>
+ */
+ public interface StringSplitter extends Iterable<String> {
+ public void setString(String string);
+ }
+
+ /**
+ * A simple string splitter.
+ *
+ * <p>If the final character in the string to split is the delimiter then no empty string will
+ * be returned for the empty string after that delimeter. That is, splitting <tt>"a,b,"</tt> on
+ * comma will return <tt>"a", "b"</tt>, not <tt>"a", "b", ""</tt>.
+ */
+ public static class SimpleStringSplitter implements StringSplitter, Iterator<String> {
+ private String mString;
+ private char mDelimiter;
+ private int mPosition;
+ private int mLength;
+
+ /**
+ * Initializes the splitter. setString may be called later.
+ * @param delimiter the delimeter on which to split
+ */
+ public SimpleStringSplitter(char delimiter) {
+ mDelimiter = delimiter;
+ }
+
+ /**
+ * Sets the string to split
+ * @param string the string to split
+ */
+ public void setString(String string) {
+ mString = string;
+ mPosition = 0;
+ mLength = mString.length();
+ }
+
+ public Iterator<String> iterator() {
+ return this;
+ }
+
+ public boolean hasNext() {
+ return mPosition < mLength;
+ }
+
+ public String next() {
+ int end = mString.indexOf(mDelimiter, mPosition);
+ if (end == -1) {
+ end = mLength;
+ }
+ String nextString = mString.substring(mPosition, end);
+ mPosition = end + 1; // Skip the delimiter.
+ return nextString;
+ }
+
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ public static CharSequence stringOrSpannedString(CharSequence source) {
+ if (source == null)
+ return null;
+ if (source instanceof SpannedString)
+ return source;
+ if (source instanceof Spanned)
+ return new SpannedString(source);
+
+ return source.toString();
+ }
+
+ /**
+ * Returns true if the string is null or 0-length.
+ * @param str the string to be examined
+ * @return true if str is null or zero length
+ */
+ public static boolean isEmpty(CharSequence str) {
+ if (str == null || str.length() == 0)
+ return true;
+ else
+ return false;
+ }
+
+ /**
+ * Returns the length that the specified CharSequence would have if
+ * spaces and control characters were trimmed from the start and end,
+ * as by {@link String#trim}.
+ */
+ public static int getTrimmedLength(CharSequence s) {
+ int len = s.length();
+
+ int start = 0;
+ while (start < len && s.charAt(start) <= ' ') {
+ start++;
+ }
+
+ int end = len;
+ while (end > start && s.charAt(end - 1) <= ' ') {
+ end--;
+ }
+
+ return end - start;
+ }
+
+ /**
+ * Returns true if a and b are equal, including if they are both null.
+ *
+ * @param a first CharSequence to check
+ * @param b second CharSequence to check
+ * @return true if a and b are equal
+ */
+ public static boolean equals(CharSequence a, CharSequence b) {
+ return a == b || (a != null && a.equals(b));
+ }
+
+ // XXX currently this only reverses chars, not spans
+ public static CharSequence getReverse(CharSequence source,
+ int start, int end) {
+ return new Reverser(source, start, end);
+ }
+
+ private static class Reverser
+ implements CharSequence, GetChars
+ {
+ public Reverser(CharSequence source, int start, int end) {
+ mSource = source;
+ mStart = start;
+ mEnd = end;
+ }
+
+ public int length() {
+ return mEnd - mStart;
+ }
+
+ public CharSequence subSequence(int start, int end) {
+ char[] buf = new char[end - start];
+
+ getChars(start, end, buf, 0);
+ return new String(buf);
+ }
+
+ public String toString() {
+ return subSequence(0, length()).toString();
+ }
+
+ public char charAt(int off) {
+ return AndroidCharacter.getMirror(mSource.charAt(mEnd - 1 - off));
+ }
+
+ public void getChars(int start, int end, char[] dest, int destoff) {
+ TextUtils.getChars(mSource, start + mStart, end + mStart,
+ dest, destoff);
+ AndroidCharacter.mirror(dest, 0, end - start);
+
+ int len = end - start;
+ int n = (end - start) / 2;
+ for (int i = 0; i < n; i++) {
+ char tmp = dest[destoff + i];
+
+ dest[destoff + i] = dest[destoff + len - i - 1];
+ dest[destoff + len - i - 1] = tmp;
+ }
+ }
+
+ private CharSequence mSource;
+ private int mStart;
+ private int mEnd;
+ }
+
+ private static final int ALIGNMENT_SPAN = 1;
+ private static final int FOREGROUND_COLOR_SPAN = 2;
+ private static final int RELATIVE_SIZE_SPAN = 3;
+ private static final int SCALE_X_SPAN = 4;
+ private static final int STRIKETHROUGH_SPAN = 5;
+ private static final int UNDERLINE_SPAN = 6;
+ private static final int STYLE_SPAN = 7;
+ private static final int BULLET_SPAN = 8;
+ private static final int QUOTE_SPAN = 9;
+ private static final int LEADING_MARGIN_SPAN = 10;
+ private static final int URL_SPAN = 11;
+ private static final int BACKGROUND_COLOR_SPAN = 12;
+ private static final int TYPEFACE_SPAN = 13;
+ private static final int SUPERSCRIPT_SPAN = 14;
+ private static final int SUBSCRIPT_SPAN = 15;
+ private static final int ABSOLUTE_SIZE_SPAN = 16;
+ private static final int TEXT_APPEARANCE_SPAN = 17;
+ private static final int ANNOTATION = 18;
+
+ /**
+ * Flatten a CharSequence and whatever styles can be copied across processes
+ * into the parcel.
+ */
+ public static void writeToParcel(CharSequence cs, Parcel p,
+ int parcelableFlags) {
+ if (cs instanceof Spanned) {
+ p.writeInt(0);
+ p.writeString(cs.toString());
+
+ Spanned sp = (Spanned) cs;
+ Object[] os = sp.getSpans(0, cs.length(), Object.class);
+
+ // note to people adding to this: check more specific types
+ // before more generic types. also notice that it uses
+ // "if" instead of "else if" where there are interfaces
+ // so one object can be several.
+
+ for (int i = 0; i < os.length; i++) {
+ Object o = os[i];
+ Object prop = os[i];
+
+ if (prop instanceof CharacterStyle) {
+ prop = ((CharacterStyle) prop).getUnderlying();
+ }
+
+ if (prop instanceof AlignmentSpan) {
+ p.writeInt(ALIGNMENT_SPAN);
+ p.writeString(((AlignmentSpan) prop).getAlignment().name());
+ writeWhere(p, sp, o);
+ }
+
+ if (prop instanceof ForegroundColorSpan) {
+ p.writeInt(FOREGROUND_COLOR_SPAN);
+ p.writeInt(((ForegroundColorSpan) prop).getForegroundColor());
+ writeWhere(p, sp, o);
+ }
+
+ if (prop instanceof RelativeSizeSpan) {
+ p.writeInt(RELATIVE_SIZE_SPAN);
+ p.writeFloat(((RelativeSizeSpan) prop).getSizeChange());
+ writeWhere(p, sp, o);
+ }
+
+ if (prop instanceof ScaleXSpan) {
+ p.writeInt(SCALE_X_SPAN);
+ p.writeFloat(((ScaleXSpan) prop).getScaleX());
+ writeWhere(p, sp, o);
+ }
+
+ if (prop instanceof StrikethroughSpan) {
+ p.writeInt(STRIKETHROUGH_SPAN);
+ writeWhere(p, sp, o);
+ }
+
+ if (prop instanceof UnderlineSpan) {
+ p.writeInt(UNDERLINE_SPAN);
+ writeWhere(p, sp, o);
+ }
+
+ if (prop instanceof StyleSpan) {
+ p.writeInt(STYLE_SPAN);
+ p.writeInt(((StyleSpan) prop).getStyle());
+ writeWhere(p, sp, o);
+ }
+
+ if (prop instanceof LeadingMarginSpan) {
+ if (prop instanceof BulletSpan) {
+ p.writeInt(BULLET_SPAN);
+ writeWhere(p, sp, o);
+ } else if (prop instanceof QuoteSpan) {
+ p.writeInt(QUOTE_SPAN);
+ p.writeInt(((QuoteSpan) prop).getColor());
+ writeWhere(p, sp, o);
+ } else {
+ p.writeInt(LEADING_MARGIN_SPAN);
+ p.writeInt(((LeadingMarginSpan) prop).
+ getLeadingMargin(true));
+ p.writeInt(((LeadingMarginSpan) prop).
+ getLeadingMargin(false));
+ writeWhere(p, sp, o);
+ }
+ }
+
+ if (prop instanceof URLSpan) {
+ p.writeInt(URL_SPAN);
+ p.writeString(((URLSpan) prop).getURL());
+ writeWhere(p, sp, o);
+ }
+
+ if (prop instanceof BackgroundColorSpan) {
+ p.writeInt(BACKGROUND_COLOR_SPAN);
+ p.writeInt(((BackgroundColorSpan) prop).getBackgroundColor());
+ writeWhere(p, sp, o);
+ }
+
+ if (prop instanceof TypefaceSpan) {
+ p.writeInt(TYPEFACE_SPAN);
+ p.writeString(((TypefaceSpan) prop).getFamily());
+ writeWhere(p, sp, o);
+ }
+
+ if (prop instanceof SuperscriptSpan) {
+ p.writeInt(SUPERSCRIPT_SPAN);
+ writeWhere(p, sp, o);
+ }
+
+ if (prop instanceof SubscriptSpan) {
+ p.writeInt(SUBSCRIPT_SPAN);
+ writeWhere(p, sp, o);
+ }
+
+ if (prop instanceof AbsoluteSizeSpan) {
+ p.writeInt(ABSOLUTE_SIZE_SPAN);
+ p.writeInt(((AbsoluteSizeSpan) prop).getSize());
+ writeWhere(p, sp, o);
+ }
+
+ if (prop instanceof TextAppearanceSpan) {
+ TextAppearanceSpan tas = (TextAppearanceSpan) prop;
+ p.writeInt(TEXT_APPEARANCE_SPAN);
+
+ String tf = tas.getFamily();
+ if (tf != null) {
+ p.writeInt(1);
+ p.writeString(tf);
+ } else {
+ p.writeInt(0);
+ }
+
+ p.writeInt(tas.getTextSize());
+ p.writeInt(tas.getTextStyle());
+
+ ColorStateList csl = tas.getTextColor();
+ if (csl == null) {
+ p.writeInt(0);
+ } else {
+ p.writeInt(1);
+ csl.writeToParcel(p, parcelableFlags);
+ }
+
+ csl = tas.getLinkTextColor();
+ if (csl == null) {
+ p.writeInt(0);
+ } else {
+ p.writeInt(1);
+ csl.writeToParcel(p, parcelableFlags);
+ }
+
+ writeWhere(p, sp, o);
+ }
+
+ if (prop instanceof Annotation) {
+ p.writeInt(ANNOTATION);
+ p.writeString(((Annotation) prop).getKey());
+ p.writeString(((Annotation) prop).getValue());
+ writeWhere(p, sp, o);
+ }
+ }
+
+ p.writeInt(0);
+ } else {
+ p.writeInt(1);
+ if (cs != null) {
+ p.writeString(cs.toString());
+ } else {
+ p.writeString(null);
+ }
+ }
+ }
+
+ private static void writeWhere(Parcel p, Spanned sp, Object o) {
+ p.writeInt(sp.getSpanStart(o));
+ p.writeInt(sp.getSpanEnd(o));
+ p.writeInt(sp.getSpanFlags(o));
+ }
+
+ public static final Parcelable.Creator<CharSequence> CHAR_SEQUENCE_CREATOR
+ = new Parcelable.Creator<CharSequence>()
+ {
+ /**
+ * Read and return a new CharSequence, possibly with styles,
+ * from the parcel.
+ */
+ public CharSequence createFromParcel(Parcel p) {
+ int kind = p.readInt();
+
+ if (kind == 1)
+ return p.readString();
+
+ SpannableString sp = new SpannableString(p.readString());
+
+ while (true) {
+ kind = p.readInt();
+
+ if (kind == 0)
+ break;
+
+ switch (kind) {
+ case ALIGNMENT_SPAN:
+ readSpan(p, sp, new AlignmentSpan.Standard(
+ Layout.Alignment.valueOf(p.readString())));
+ break;
+
+ case FOREGROUND_COLOR_SPAN:
+ readSpan(p, sp, new ForegroundColorSpan(p.readInt()));
+ break;
+
+ case RELATIVE_SIZE_SPAN:
+ readSpan(p, sp, new RelativeSizeSpan(p.readFloat()));
+ break;
+
+ case SCALE_X_SPAN:
+ readSpan(p, sp, new ScaleXSpan(p.readFloat()));
+ break;
+
+ case STRIKETHROUGH_SPAN:
+ readSpan(p, sp, new StrikethroughSpan());
+ break;
+
+ case UNDERLINE_SPAN:
+ readSpan(p, sp, new UnderlineSpan());
+ break;
+
+ case STYLE_SPAN:
+ readSpan(p, sp, new StyleSpan(p.readInt()));
+ break;
+
+ case BULLET_SPAN:
+ readSpan(p, sp, new BulletSpan());
+ break;
+
+ case QUOTE_SPAN:
+ readSpan(p, sp, new QuoteSpan(p.readInt()));
+ break;
+
+ case LEADING_MARGIN_SPAN:
+ readSpan(p, sp, new LeadingMarginSpan.Standard(p.readInt(),
+ p.readInt()));
+ break;
+
+ case URL_SPAN:
+ readSpan(p, sp, new URLSpan(p.readString()));
+ break;
+
+ case BACKGROUND_COLOR_SPAN:
+ readSpan(p, sp, new BackgroundColorSpan(p.readInt()));
+ break;
+
+ case TYPEFACE_SPAN:
+ readSpan(p, sp, new TypefaceSpan(p.readString()));
+ break;
+
+ case SUPERSCRIPT_SPAN:
+ readSpan(p, sp, new SuperscriptSpan());
+ break;
+
+ case SUBSCRIPT_SPAN:
+ readSpan(p, sp, new SubscriptSpan());
+ break;
+
+ case ABSOLUTE_SIZE_SPAN:
+ readSpan(p, sp, new AbsoluteSizeSpan(p.readInt()));
+ break;
+
+ case TEXT_APPEARANCE_SPAN:
+ readSpan(p, sp, new TextAppearanceSpan(
+ p.readInt() != 0
+ ? p.readString()
+ : null,
+ p.readInt(),
+ p.readInt(),
+ p.readInt() != 0
+ ? ColorStateList.CREATOR.createFromParcel(p)
+ : null,
+ p.readInt() != 0
+ ? ColorStateList.CREATOR.createFromParcel(p)
+ : null));
+ break;
+
+ case ANNOTATION:
+ readSpan(p, sp,
+ new Annotation(p.readString(), p.readString()));
+ break;
+
+ default:
+ throw new RuntimeException("bogus span encoding " + kind);
+ }
+ }
+
+ return sp;
+ }
+
+ public CharSequence[] newArray(int size)
+ {
+ return new CharSequence[size];
+ }
+ };
+
+ /**
+ * Return a new CharSequence in which each of the source strings is
+ * replaced by the corresponding element of the destinations.
+ */
+ public static CharSequence replace(CharSequence template,
+ String[] sources,
+ CharSequence[] destinations) {
+ SpannableStringBuilder tb = new SpannableStringBuilder(template);
+
+ for (int i = 0; i < sources.length; i++) {
+ int where = indexOf(tb, sources[i]);
+
+ if (where >= 0)
+ tb.setSpan(sources[i], where, where + sources[i].length(),
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
+ for (int i = 0; i < sources.length; i++) {
+ int start = tb.getSpanStart(sources[i]);
+ int end = tb.getSpanEnd(sources[i]);
+
+ if (start >= 0) {
+ tb.replace(start, end, destinations[i]);
+ }
+ }
+
+ return tb;
+ }
+
+ /**
+ * Replace instances of "^1", "^2", etc. in the
+ * <code>template</code> CharSequence with the corresponding
+ * <code>values</code>. "^^" is used to produce a single caret in
+ * the output. Only up to 9 replacement values are supported,
+ * "^10" will be produce the first replacement value followed by a
+ * '0'.
+ *
+ * @param template the input text containing "^1"-style
+ * placeholder values. This object is not modified; a copy is
+ * returned.
+ *
+ * @param values CharSequences substituted into the template. The
+ * first is substituted for "^1", the second for "^2", and so on.
+ *
+ * @return the new CharSequence produced by doing the replacement
+ *
+ * @throws IllegalArgumentException if the template requests a
+ * value that was not provided, or if more than 9 values are
+ * provided.
+ */
+ public static CharSequence expandTemplate(CharSequence template,
+ CharSequence... values) {
+ if (values.length > 9) {
+ throw new IllegalArgumentException("max of 9 values are supported");
+ }
+
+ SpannableStringBuilder ssb = new SpannableStringBuilder(template);
+
+ try {
+ int i = 0;
+ while (i < ssb.length()) {
+ if (ssb.charAt(i) == '^') {
+ char next = ssb.charAt(i+1);
+ if (next == '^') {
+ ssb.delete(i+1, i+2);
+ ++i;
+ continue;
+ } else if (Character.isDigit(next)) {
+ int which = Character.getNumericValue(next) - 1;
+ if (which < 0) {
+ throw new IllegalArgumentException(
+ "template requests value ^" + (which+1));
+ }
+ if (which >= values.length) {
+ throw new IllegalArgumentException(
+ "template requests value ^" + (which+1) +
+ "; only " + values.length + " provided");
+ }
+ ssb.replace(i, i+2, values[which]);
+ i += values[which].length();
+ continue;
+ }
+ }
+ ++i;
+ }
+ } catch (IndexOutOfBoundsException ignore) {
+ // happens when ^ is the last character in the string.
+ }
+ return ssb;
+ }
+
+ public static int getOffsetBefore(CharSequence text, int offset) {
+ if (offset == 0)
+ return 0;
+ if (offset == 1)
+ return 0;
+
+ char c = text.charAt(offset - 1);
+
+ if (c >= '\uDC00' && c <= '\uDFFF') {
+ char c1 = text.charAt(offset - 2);
+
+ if (c1 >= '\uD800' && c1 <= '\uDBFF')
+ offset -= 2;
+ else
+ offset -= 1;
+ } else {
+ offset -= 1;
+ }
+
+ if (text instanceof Spanned) {
+ ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset,
+ ReplacementSpan.class);
+
+ for (int i = 0; i < spans.length; i++) {
+ int start = ((Spanned) text).getSpanStart(spans[i]);
+ int end = ((Spanned) text).getSpanEnd(spans[i]);
+
+ if (start < offset && end > offset)
+ offset = start;
+ }
+ }
+
+ return offset;
+ }
+
+ public static int getOffsetAfter(CharSequence text, int offset) {
+ int len = text.length();
+
+ if (offset == len)
+ return len;
+ if (offset == len - 1)
+ return len;
+
+ char c = text.charAt(offset);
+
+ if (c >= '\uD800' && c <= '\uDBFF') {
+ char c1 = text.charAt(offset + 1);
+
+ if (c1 >= '\uDC00' && c1 <= '\uDFFF')
+ offset += 2;
+ else
+ offset += 1;
+ } else {
+ offset += 1;
+ }
+
+ if (text instanceof Spanned) {
+ ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset,
+ ReplacementSpan.class);
+
+ for (int i = 0; i < spans.length; i++) {
+ int start = ((Spanned) text).getSpanStart(spans[i]);
+ int end = ((Spanned) text).getSpanEnd(spans[i]);
+
+ if (start < offset && end > offset)
+ offset = end;
+ }
+ }
+
+ return offset;
+ }
+
+ private static void readSpan(Parcel p, Spannable sp, Object o) {
+ sp.setSpan(o, p.readInt(), p.readInt(), p.readInt());
+ }
+
+ public static void copySpansFrom(Spanned source, int start, int end,
+ Class kind,
+ Spannable dest, int destoff) {
+ if (kind == null) {
+ kind = Object.class;
+ }
+
+ Object[] spans = source.getSpans(start, end, kind);
+
+ for (int i = 0; i < spans.length; i++) {
+ int st = source.getSpanStart(spans[i]);
+ int en = source.getSpanEnd(spans[i]);
+ int fl = source.getSpanFlags(spans[i]);
+
+ if (st < start)
+ st = start;
+ if (en > end)
+ en = end;
+
+ dest.setSpan(spans[i], st - start + destoff, en - start + destoff,
+ fl);
+ }
+ }
+
+ public enum TruncateAt {
+ START,
+ MIDDLE,
+ END,
+ }
+
+ public interface EllipsizeCallback {
+ /**
+ * This method is called to report that the specified region of
+ * text was ellipsized away by a call to {@link #ellipsize}.
+ */
+ public void ellipsized(int start, int end);
+ }
+
+ private static String sEllipsis = null;
+
+ /**
+ * Returns the original text if it fits in the specified width
+ * given the properties of the specified Paint,
+ * or, if it does not fit, a truncated
+ * copy with ellipsis character added at the specified edge or center.
+ */
+ public static CharSequence ellipsize(CharSequence text,
+ TextPaint p,
+ float avail, TruncateAt where) {
+ return ellipsize(text, p, avail, where, false, null);
+ }
+
+ /**
+ * Returns the original text if it fits in the specified width
+ * given the properties of the specified Paint,
+ * or, if it does not fit, a copy with ellipsis character added
+ * at the specified edge or center.
+ * If <code>preserveLength</code> is specified, the returned copy
+ * will be padded with zero-width spaces to preserve the original
+ * length and offsets instead of truncating.
+ * If <code>callback</code> is non-null, it will be called to
+ * report the start and end of the ellipsized range.
+ */
+ public static CharSequence ellipsize(CharSequence text,
+ TextPaint p,
+ float avail, TruncateAt where,
+ boolean preserveLength,
+ EllipsizeCallback callback) {
+ if (sEllipsis == null) {
+ Resources r = Resources.getSystem();
+ sEllipsis = r.getString(R.string.ellipsis);
+ }
+
+ int len = text.length();
+
+ // Use Paint.breakText() for the non-Spanned case to avoid having
+ // to allocate memory and accumulate the character widths ourselves.
+
+ if (!(text instanceof Spanned)) {
+ float wid = p.measureText(text, 0, len);
+
+ if (wid <= avail) {
+ if (callback != null) {
+ callback.ellipsized(0, 0);
+ }
+
+ return text;
+ }
+
+ float ellipsiswid = p.measureText(sEllipsis);
+
+ if (ellipsiswid > avail) {
+ if (callback != null) {
+ callback.ellipsized(0, len);
+ }
+
+ if (preserveLength) {
+ char[] buf = obtain(len);
+ for (int i = 0; i < len; i++) {
+ buf[i] = '\uFEFF';
+ }
+ String ret = new String(buf, 0, len);
+ recycle(buf);
+ return ret;
+ } else {
+ return "";
+ }
+ }
+
+ if (where == TruncateAt.START) {
+ int fit = p.breakText(text, 0, len, false,
+ avail - ellipsiswid, null);
+
+ if (callback != null) {
+ callback.ellipsized(0, len - fit);
+ }
+
+ if (preserveLength) {
+ return blank(text, 0, len - fit);
+ } else {
+ return sEllipsis + text.toString().substring(len - fit, len);
+ }
+ } else if (where == TruncateAt.END) {
+ int fit = p.breakText(text, 0, len, true,
+ avail - ellipsiswid, null);
+
+ if (callback != null) {
+ callback.ellipsized(fit, len);
+ }
+
+ if (preserveLength) {
+ return blank(text, fit, len);
+ } else {
+ return text.toString().substring(0, fit) + sEllipsis;
+ }
+ } else /* where == TruncateAt.MIDDLE */ {
+ int right = p.breakText(text, 0, len, false,
+ (avail - ellipsiswid) / 2, null);
+ float used = p.measureText(text, len - right, len);
+ int left = p.breakText(text, 0, len - right, true,
+ avail - ellipsiswid - used, null);
+
+ if (callback != null) {
+ callback.ellipsized(left, len - right);
+ }
+
+ if (preserveLength) {
+ return blank(text, left, len - right);
+ } else {
+ String s = text.toString();
+ return s.substring(0, left) + sEllipsis +
+ s.substring(len - right, len);
+ }
+ }
+ }
+
+ // But do the Spanned cases by hand, because it's such a pain
+ // to iterate the span transitions backwards and getTextWidths()
+ // will give us the information we need.
+
+ // getTextWidths() always writes into the start of the array,
+ // so measure each span into the first half and then copy the
+ // results into the second half to use later.
+
+ float[] wid = new float[len * 2];
+ TextPaint temppaint = new TextPaint();
+ Spanned sp = (Spanned) text;
+
+ int next;
+ for (int i = 0; i < len; i = next) {
+ next = sp.nextSpanTransition(i, len, MetricAffectingSpan.class);
+
+ Styled.getTextWidths(p, temppaint, sp, i, next, wid, null);
+ System.arraycopy(wid, 0, wid, len + i, next - i);
+ }
+
+ float sum = 0;
+ for (int i = 0; i < len; i++) {
+ sum += wid[len + i];
+ }
+
+ if (sum <= avail) {
+ if (callback != null) {
+ callback.ellipsized(0, 0);
+ }
+
+ return text;
+ }
+
+ float ellipsiswid = p.measureText(sEllipsis);
+
+ if (ellipsiswid > avail) {
+ if (callback != null) {
+ callback.ellipsized(0, len);
+ }
+
+ if (preserveLength) {
+ char[] buf = obtain(len);
+ for (int i = 0; i < len; i++) {
+ buf[i] = '\uFEFF';
+ }
+ SpannableString ss = new SpannableString(new String(buf, 0, len));
+ recycle(buf);
+ copySpansFrom(sp, 0, len, Object.class, ss, 0);
+ return ss;
+ } else {
+ return "";
+ }
+ }
+
+ if (where == TruncateAt.START) {
+ sum = 0;
+ int i;
+
+ for (i = len; i >= 0; i--) {
+ float w = wid[len + i - 1];
+
+ if (w + sum + ellipsiswid > avail) {
+ break;
+ }
+
+ sum += w;
+ }
+
+ if (callback != null) {
+ callback.ellipsized(0, i);
+ }
+
+ if (preserveLength) {
+ SpannableString ss = new SpannableString(blank(text, 0, i));
+ copySpansFrom(sp, 0, len, Object.class, ss, 0);
+ return ss;
+ } else {
+ SpannableStringBuilder out = new SpannableStringBuilder(sEllipsis);
+ out.insert(1, text, i, len);
+
+ return out;
+ }
+ } else if (where == TruncateAt.END) {
+ sum = 0;
+ int i;
+
+ for (i = 0; i < len; i++) {
+ float w = wid[len + i];
+
+ if (w + sum + ellipsiswid > avail) {
+ break;
+ }
+
+ sum += w;
+ }
+
+ if (callback != null) {
+ callback.ellipsized(i, len);
+ }
+
+ if (preserveLength) {
+ SpannableString ss = new SpannableString(blank(text, i, len));
+ copySpansFrom(sp, 0, len, Object.class, ss, 0);
+ return ss;
+ } else {
+ SpannableStringBuilder out = new SpannableStringBuilder(sEllipsis);
+ out.insert(0, text, 0, i);
+
+ return out;
+ }
+ } else /* where = TruncateAt.MIDDLE */ {
+ float lsum = 0, rsum = 0;
+ int left = 0, right = len;
+
+ float ravail = (avail - ellipsiswid) / 2;
+ for (right = len; right >= 0; right--) {
+ float w = wid[len + right - 1];
+
+ if (w + rsum > ravail) {
+ break;
+ }
+
+ rsum += w;
+ }
+
+ float lavail = avail - ellipsiswid - rsum;
+ for (left = 0; left < right; left++) {
+ float w = wid[len + left];
+
+ if (w + lsum > lavail) {
+ break;
+ }
+
+ lsum += w;
+ }
+
+ if (callback != null) {
+ callback.ellipsized(left, right);
+ }
+
+ if (preserveLength) {
+ SpannableString ss = new SpannableString(blank(text, left, right));
+ copySpansFrom(sp, 0, len, Object.class, ss, 0);
+ return ss;
+ } else {
+ SpannableStringBuilder out = new SpannableStringBuilder(sEllipsis);
+ out.insert(0, text, 0, left);
+ out.insert(out.length(), text, right, len);
+
+ return out;
+ }
+ }
+ }
+
+ private static String blank(CharSequence source, int start, int end) {
+ int len = source.length();
+ char[] buf = obtain(len);
+
+ if (start != 0) {
+ getChars(source, 0, start, buf, 0);
+ }
+ if (end != len) {
+ getChars(source, end, len, buf, end);
+ }
+
+ if (start != end) {
+ buf[start] = '\u2026';
+
+ for (int i = start + 1; i < end; i++) {
+ buf[i] = '\uFEFF';
+ }
+ }
+
+ String ret = new String(buf, 0, len);
+ recycle(buf);
+
+ return ret;
+ }
+
+ /**
+ * Converts a CharSequence of the comma-separated form "Andy, Bob,
+ * Charles, David" that is too wide to fit into the specified width
+ * into one like "Andy, Bob, 2 more".
+ *
+ * @param text the text to truncate
+ * @param p the Paint with which to measure the text
+ * @param avail the horizontal width available for the text
+ * @param oneMore the string for "1 more" in the current locale
+ * @param more the string for "%d more" in the current locale
+ */
+ public static CharSequence commaEllipsize(CharSequence text,
+ TextPaint p, float avail,
+ String oneMore,
+ String more) {
+ int len = text.length();
+ char[] buf = new char[len];
+ TextUtils.getChars(text, 0, len, buf, 0);
+
+ int commaCount = 0;
+ for (int i = 0; i < len; i++) {
+ if (buf[i] == ',') {
+ commaCount++;
+ }
+ }
+
+ float[] wid;
+
+ if (text instanceof Spanned) {
+ Spanned sp = (Spanned) text;
+ TextPaint temppaint = new TextPaint();
+ wid = new float[len * 2];
+
+ int next;
+ for (int i = 0; i < len; i = next) {
+ next = sp.nextSpanTransition(i, len, MetricAffectingSpan.class);
+
+ Styled.getTextWidths(p, temppaint, sp, i, next, wid, null);
+ System.arraycopy(wid, 0, wid, len + i, next - i);
+ }
+
+ System.arraycopy(wid, len, wid, 0, len);
+ } else {
+ wid = new float[len];
+ p.getTextWidths(text, 0, len, wid);
+ }
+
+ int ok = 0;
+ int okRemaining = commaCount + 1;
+ String okFormat = "";
+
+ int w = 0;
+ int count = 0;
+
+ for (int i = 0; i < len; i++) {
+ w += wid[i];
+
+ if (buf[i] == ',') {
+ count++;
+
+ int remaining = commaCount - count + 1;
+ float moreWid;
+ String format;
+
+ if (remaining == 1) {
+ format = " " + oneMore;
+ } else {
+ format = " " + String.format(more, remaining);
+ }
+
+ moreWid = p.measureText(format);
+
+ if (w + moreWid <= avail) {
+ ok = i + 1;
+ okRemaining = remaining;
+ okFormat = format;
+ }
+ }
+ }
+
+ if (w <= avail) {
+ return text;
+ } else {
+ SpannableStringBuilder out = new SpannableStringBuilder(okFormat);
+ out.insert(0, text, 0, ok);
+ return out;
+ }
+ }
+
+ /* package */ static char[] obtain(int len) {
+ char[] buf;
+
+ synchronized (sLock) {
+ buf = sTemp;
+ sTemp = null;
+ }
+
+ if (buf == null || buf.length < len)
+ buf = new char[ArrayUtils.idealCharArraySize(len)];
+
+ return buf;
+ }
+
+ /* package */ static void recycle(char[] temp) {
+ if (temp.length > 1000)
+ return;
+
+ synchronized (sLock) {
+ sTemp = temp;
+ }
+ }
+
+ /**
+ * Html-encode the string.
+ * @param s the string to be encoded
+ * @return the encoded string
+ */
+ public static String htmlEncode(String s) {
+ StringBuilder sb = new StringBuilder();
+ char c;
+ for (int i = 0; i < s.length(); i++) {
+ c = s.charAt(i);
+ switch (c) {
+ case '<':
+ sb.append("&lt;"); //$NON-NLS-1$
+ break;
+ case '>':
+ sb.append("&gt;"); //$NON-NLS-1$
+ break;
+ case '&':
+ sb.append("&amp;"); //$NON-NLS-1$
+ break;
+ case '\\':
+ sb.append("&apos;"); //$NON-NLS-1$
+ break;
+ case '"':
+ sb.append("&quot;"); //$NON-NLS-1$
+ break;
+ default:
+ sb.append(c);
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Returns a CharSequence concatenating the specified CharSequences,
+ * retaining their spans if any.
+ */
+ public static CharSequence concat(CharSequence... text) {
+ if (text.length == 0) {
+ return "";
+ }
+
+ if (text.length == 1) {
+ return text[0];
+ }
+
+ boolean spanned = false;
+ for (int i = 0; i < text.length; i++) {
+ if (text[i] instanceof Spanned) {
+ spanned = true;
+ break;
+ }
+ }
+
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < text.length; i++) {
+ sb.append(text[i]);
+ }
+
+ if (!spanned) {
+ return sb.toString();
+ }
+
+ SpannableString ss = new SpannableString(sb);
+ int off = 0;
+ for (int i = 0; i < text.length; i++) {
+ int len = text[i].length();
+
+ if (text[i] instanceof Spanned) {
+ copySpansFrom((Spanned) text[i], 0, len, Object.class, ss, off);
+ }
+
+ off += len;
+ }
+
+ return new SpannedString(ss);
+ }
+
+ /**
+ * Returns whether the given CharSequence contains any printable characters.
+ */
+ public static boolean isGraphic(CharSequence str) {
+ final int len = str.length();
+ for (int i=0; i<len; i++) {
+ int gc = Character.getType(str.charAt(i));
+ if (gc != Character.CONTROL
+ && gc != Character.FORMAT
+ && gc != Character.SURROGATE
+ && gc != Character.UNASSIGNED
+ && gc != Character.LINE_SEPARATOR
+ && gc != Character.PARAGRAPH_SEPARATOR
+ && gc != Character.SPACE_SEPARATOR) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns whether this character is a printable character.
+ */
+ public static boolean isGraphic(char c) {
+ int gc = Character.getType(c);
+ return gc != Character.CONTROL
+ && gc != Character.FORMAT
+ && gc != Character.SURROGATE
+ && gc != Character.UNASSIGNED
+ && gc != Character.LINE_SEPARATOR
+ && gc != Character.PARAGRAPH_SEPARATOR
+ && gc != Character.SPACE_SEPARATOR;
+ }
+
+ /**
+ * Returns whether the given CharSequence contains only digits.
+ */
+ public static boolean isDigitsOnly(CharSequence str) {
+ final int len = str.length();
+ for (int i = 0; i < len; i++) {
+ if (!Character.isDigit(str.charAt(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static Object sLock = new Object();
+ private static char[] sTemp = null;
+}
diff --git a/core/java/android/text/TextWatcher.java b/core/java/android/text/TextWatcher.java
new file mode 100644
index 0000000..7456b28
--- /dev/null
+++ b/core/java/android/text/TextWatcher.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text;
+
+/**
+ * When an object of a type is attached to an Editable, its methods will
+ * be called when the text is changed.
+ */
+public interface TextWatcher {
+ /**
+ * This method is called to notify you that, within <code>s</code>,
+ * the <code>count</code> characters beginning at <code>start</code>
+ * are about to be replaced by new text with length <code>after</code>.
+ * It is an error to attempt to make changes to <code>s</code> from
+ * this callback.
+ */
+ public void beforeTextChanged(CharSequence s, int start,
+ int count, int after);
+ /**
+ * This method is called to notify you that, within <code>s</code>,
+ * the <code>count</code> characters beginning at <code>start</code>
+ * have just replaced old text that had length <code>before</code>.
+ * It is an error to attempt to make changes to <code>s</code> from
+ * this callback.
+ */
+ public void onTextChanged(CharSequence s, int start, int before, int count);
+
+ /**
+ * This method is called to notify you that, somewhere within
+ * <code>s</code>, the text has been changed.
+ * It is legitimate to make further changes to <code>s</code> from
+ * this callback, but be careful not to get yourself into an infinite
+ * loop, because any changes you make will cause this method to be
+ * called again recursively.
+ * (You are not told where the change took place because other
+ * afterTextChanged() methods may already have made other changes
+ * and invalidated the offsets. But if you need to know here,
+ * you can use {@link Spannable#setSpan} in {@link #onTextChanged}
+ * to mark your place and then look up from here where the span
+ * ended up.
+ */
+ public void afterTextChanged(Editable s);
+}
diff --git a/core/java/android/text/method/ArrowKeyMovementMethod.java b/core/java/android/text/method/ArrowKeyMovementMethod.java
new file mode 100644
index 0000000..ac2e499
--- /dev/null
+++ b/core/java/android/text/method/ArrowKeyMovementMethod.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.method;
+
+import android.view.KeyEvent;
+import android.text.*;
+import android.widget.TextView;
+import android.view.View;
+import android.view.MotionEvent;
+
+// XXX this doesn't extend MetaKeyKeyListener because the signatures
+// don't match. Need to figure that out. Meanwhile the meta keys
+// won't work in fields that don't take input.
+
+public class
+ArrowKeyMovementMethod
+implements MovementMethod
+{
+ private boolean up(TextView widget, Spannable buffer) {
+ boolean cap = MetaKeyKeyListener.getMetaState(buffer,
+ KeyEvent.META_SHIFT_ON) == 1;
+ boolean alt = MetaKeyKeyListener.getMetaState(buffer,
+ KeyEvent.META_ALT_ON) == 1;
+ Layout layout = widget.getLayout();
+
+ if (cap) {
+ if (alt) {
+ Selection.extendSelection(buffer, 0);
+ return true;
+ } else {
+ return Selection.extendUp(buffer, layout);
+ }
+ } else {
+ if (alt) {
+ Selection.setSelection(buffer, 0);
+ return true;
+ } else {
+ return Selection.moveUp(buffer, layout);
+ }
+ }
+ }
+
+ private boolean down(TextView widget, Spannable buffer) {
+ boolean cap = MetaKeyKeyListener.getMetaState(buffer,
+ KeyEvent.META_SHIFT_ON) == 1;
+ boolean alt = MetaKeyKeyListener.getMetaState(buffer,
+ KeyEvent.META_ALT_ON) == 1;
+ Layout layout = widget.getLayout();
+
+ if (cap) {
+ if (alt) {
+ Selection.extendSelection(buffer, buffer.length());
+ return true;
+ } else {
+ return Selection.extendDown(buffer, layout);
+ }
+ } else {
+ if (alt) {
+ Selection.setSelection(buffer, buffer.length());
+ return true;
+ } else {
+ return Selection.moveDown(buffer, layout);
+ }
+ }
+ }
+
+ private boolean left(TextView widget, Spannable buffer) {
+ boolean cap = MetaKeyKeyListener.getMetaState(buffer,
+ KeyEvent.META_SHIFT_ON) == 1;
+ boolean alt = MetaKeyKeyListener.getMetaState(buffer,
+ KeyEvent.META_ALT_ON) == 1;
+ Layout layout = widget.getLayout();
+
+ if (cap) {
+ if (alt) {
+ return Selection.extendToLeftEdge(buffer, layout);
+ } else {
+ return Selection.extendLeft(buffer, layout);
+ }
+ } else {
+ if (alt) {
+ return Selection.moveToLeftEdge(buffer, layout);
+ } else {
+ return Selection.moveLeft(buffer, layout);
+ }
+ }
+ }
+
+ private boolean right(TextView widget, Spannable buffer) {
+ boolean cap = MetaKeyKeyListener.getMetaState(buffer,
+ KeyEvent.META_SHIFT_ON) == 1;
+ boolean alt = MetaKeyKeyListener.getMetaState(buffer,
+ KeyEvent.META_ALT_ON) == 1;
+ Layout layout = widget.getLayout();
+
+ if (cap) {
+ if (alt) {
+ return Selection.extendToRightEdge(buffer, layout);
+ } else {
+ return Selection.extendRight(buffer, layout);
+ }
+ } else {
+ if (alt) {
+ return Selection.moveToRightEdge(buffer, layout);
+ } else {
+ return Selection.moveRight(buffer, layout);
+ }
+ }
+ }
+
+ public boolean onKeyDown(TextView widget, Spannable buffer, int keyCode, KeyEvent event) {
+ boolean handled = false;
+
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_UP:
+ handled |= up(widget, buffer);
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ handled |= down(widget, buffer);
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ handled |= left(widget, buffer);
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ handled |= right(widget, buffer);
+ break;
+ }
+
+ if (handled) {
+ MetaKeyKeyListener.adjustMetaAfterKeypress(buffer);
+ MetaKeyKeyListener.resetLockedMeta(buffer);
+ }
+
+ return handled;
+ }
+
+ public boolean onKeyUp(TextView widget, Spannable buffer, int keyCode, KeyEvent event) {
+ return false;
+ }
+
+ public boolean onTrackballEvent(TextView widget, Spannable buffer,
+ MotionEvent event) {
+ boolean handled = false;
+ int x = (int) event.getX();
+ int y = (int) event.getY();
+
+ for (; y < 0; y++) {
+ handled |= up(widget, buffer);
+ }
+ for (; y > 0; y--) {
+ handled |= down(widget, buffer);
+ }
+
+ for (; x < 0; x++) {
+ handled |= left(widget, buffer);
+ }
+ for (; x > 0; x--) {
+ handled |= right(widget, buffer);
+ }
+
+ if (handled) {
+ MetaKeyKeyListener.adjustMetaAfterKeypress(buffer);
+ MetaKeyKeyListener.resetLockedMeta(buffer);
+ }
+
+ return handled;
+ }
+
+ public boolean onTouchEvent(TextView widget, Spannable buffer,
+ MotionEvent event) {
+ boolean handled = Touch.onTouchEvent(widget, buffer, event);
+
+ if (widget.isFocused()) {
+ if (event.getAction() == MotionEvent.ACTION_UP) {
+ int x = (int) event.getX();
+ int y = (int) event.getY();
+
+ x -= widget.getTotalPaddingLeft();
+ y -= widget.getTotalPaddingTop();
+
+ x += widget.getScrollX();
+ y += widget.getScrollY();
+
+ Layout layout = widget.getLayout();
+ int line = layout.getLineForVertical(y);
+ int off = layout.getOffsetForHorizontal(line, x);
+
+ boolean cap = (event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0;
+
+ if (cap) {
+ Selection.extendSelection(buffer, off);
+ } else {
+ Selection.setSelection(buffer, off);
+ }
+
+ MetaKeyKeyListener.adjustMetaAfterKeypress(buffer);
+ MetaKeyKeyListener.resetLockedMeta(buffer);
+
+ return true;
+ }
+ }
+
+ return handled;
+ }
+
+ public boolean canSelectArbitrarily() {
+ return true;
+ }
+
+ public void initialize(TextView widget, Spannable text) {
+ Selection.setSelection(text, 0);
+ }
+
+ public void onTakeFocus(TextView view, Spannable text, int dir) {
+ if ((dir & (View.FOCUS_FORWARD | View.FOCUS_DOWN)) != 0) {
+ Layout layout = view.getLayout();
+
+ if (layout == null) {
+ /*
+ * This shouldn't be null, but do something sensible if it is.
+ */
+ Selection.setSelection(text, text.length());
+ } else {
+ /*
+ * Put the cursor at the end of the first line, which is
+ * either the last offset if there is only one line, or the
+ * offset before the first character of the second line
+ * if there is more than one line.
+ */
+ if (layout.getLineCount() == 1) {
+ Selection.setSelection(text, text.length());
+ } else {
+ Selection.setSelection(text, layout.getLineStart(1) - 1);
+ }
+ }
+ } else {
+ Selection.setSelection(text, text.length());
+ }
+ }
+
+ public static MovementMethod getInstance() {
+ if (sInstance == null)
+ sInstance = new ArrowKeyMovementMethod();
+
+ return sInstance;
+ }
+
+ private static ArrowKeyMovementMethod sInstance;
+}
diff --git a/core/java/android/text/method/BaseKeyListener.java b/core/java/android/text/method/BaseKeyListener.java
new file mode 100644
index 0000000..3e92b7b
--- /dev/null
+++ b/core/java/android/text/method/BaseKeyListener.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.method;
+
+import android.view.KeyEvent;
+import android.view.View;
+import android.os.Message;
+import android.util.Log;
+import android.text.*;
+import android.widget.TextView;
+
+public abstract class BaseKeyListener
+extends MetaKeyKeyListener
+implements KeyListener {
+ /* package */ static final Object OLD_SEL_START = new Object();
+
+ /**
+ * Performs the action that happens when you press the DEL key in
+ * a TextView. If there is a selection, deletes the selection;
+ * otherwise, DEL alone deletes the character before the cursor,
+ * if any;
+ * ALT+DEL deletes everything on the line the cursor is on.
+ *
+ * @return true if anything was deleted; false otherwise.
+ */
+ public boolean backspace(View view, Editable content, int keyCode,
+ KeyEvent event) {
+ int selStart, selEnd;
+ boolean result = true;
+
+ {
+ int a = Selection.getSelectionStart(content);
+ int b = Selection.getSelectionEnd(content);
+
+ selStart = Math.min(a, b);
+ selEnd = Math.max(a, b);
+ }
+
+ if (selStart != selEnd) {
+ content.delete(selStart, selEnd);
+ } else if (altBackspace(view, content, keyCode, event)) {
+ result = true;
+ } else {
+ int to = TextUtils.getOffsetBefore(content, selEnd);
+
+ if (to != selEnd) {
+ content.delete(Math.min(to, selEnd), Math.max(to, selEnd));
+ }
+ else {
+ result = false;
+ }
+ }
+
+ if (result)
+ adjustMetaAfterKeypress(content);
+
+ return result;
+ }
+
+ private boolean altBackspace(View view, Editable content, int keyCode,
+ KeyEvent event) {
+ if (getMetaState(content, META_ALT_ON) != 1) {
+ return false;
+ }
+
+ if (!(view instanceof TextView)) {
+ return false;
+ }
+
+ Layout layout = ((TextView) view).getLayout();
+
+ if (layout == null) {
+ return false;
+ }
+
+ int l = layout.getLineForOffset(Selection.getSelectionStart(content));
+ int start = layout.getLineStart(l);
+ int end = layout.getLineEnd(l);
+
+ if (end == start) {
+ return false;
+ }
+
+ content.delete(start, end);
+ return true;
+ }
+
+ public boolean onKeyDown(View view, Editable content,
+ int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_DEL) {
+ backspace(view, content, keyCode, event);
+ return true;
+ }
+
+ return super.onKeyDown(view, content, keyCode, event);
+ }
+}
+
diff --git a/core/java/android/text/method/CharacterPickerDialog.java b/core/java/android/text/method/CharacterPickerDialog.java
new file mode 100644
index 0000000..d787132
--- /dev/null
+++ b/core/java/android/text/method/CharacterPickerDialog.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.method;
+
+import com.android.internal.R;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.os.Bundle;
+import android.text.*;
+import android.view.LayoutInflater;
+import android.view.View.OnClickListener;
+import android.view.View;
+import android.view.ViewGroup.LayoutParams;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.Button;
+import android.widget.GridView;
+import android.widget.TextView;
+
+/**
+ * Dialog for choosing accented characters related to a base character.
+ */
+public class CharacterPickerDialog extends Dialog
+ implements OnItemClickListener, OnClickListener {
+ private View mView;
+ private Editable mText;
+ private String mOptions;
+ private boolean mInsert;
+ private LayoutInflater mInflater;
+
+ /**
+ * Creates a new CharacterPickerDialog that presents the specified
+ * <code>options</code> for insertion or replacement (depending on
+ * the sense of <code>insert</code>) into <code>text</code>.
+ */
+ public CharacterPickerDialog(Context context, View view,
+ Editable text, String options,
+ boolean insert) {
+ super(context);
+
+ mView = view;
+ mText = text;
+ mOptions = options;
+ mInsert = insert;
+ mInflater = LayoutInflater.from(context);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ WindowManager.LayoutParams params = getWindow().getAttributes();
+ params.token = mView.getApplicationWindowToken();
+ params.type = params.TYPE_APPLICATION_PANEL;
+
+ setTitle(R.string.select_character);
+ setContentView(R.layout.character_picker);
+
+ GridView grid = (GridView) findViewById(R.id.characterPicker);
+ grid.setAdapter(new OptionsAdapter(getContext()));
+ grid.setOnItemClickListener(this);
+
+ findViewById(R.id.cancel).setOnClickListener(this);
+ }
+
+ /**
+ * Handles clicks on the character buttons.
+ */
+ public void onItemClick(AdapterView parent, View view, int position, long id) {
+ int selEnd = Selection.getSelectionEnd(mText);
+ String result = String.valueOf(mOptions.charAt(position));
+
+ if (mInsert || selEnd == 0) {
+ mText.insert(selEnd, result);
+ } else {
+ mText.replace(selEnd - 1, selEnd, result);
+ }
+
+ dismiss();
+ }
+
+ /**
+ * Handles clicks on the Cancel button.
+ */
+ public void onClick(View v) {
+ dismiss();
+ }
+
+ private class OptionsAdapter extends BaseAdapter {
+ private Context mContext;
+
+ public OptionsAdapter(Context context) {
+ super();
+ mContext = context;
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ Button b = (Button)
+ mInflater.inflate(R.layout.character_picker_button, null);
+ b.setText(String.valueOf(mOptions.charAt(position)));
+ return b;
+ }
+
+ public final int getCount() {
+ return mOptions.length();
+ }
+
+ public final Object getItem(int position) {
+ return String.valueOf(mOptions.charAt(position));
+ }
+
+ public final long getItemId(int position) {
+ return position;
+ }
+ }
+}
diff --git a/core/java/android/text/method/DateKeyListener.java b/core/java/android/text/method/DateKeyListener.java
new file mode 100644
index 0000000..0ca0332
--- /dev/null
+++ b/core/java/android/text/method/DateKeyListener.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.method;
+
+import android.view.KeyEvent;
+
+/**
+ * For entering dates in a text field.
+ */
+public class DateKeyListener extends NumberKeyListener
+{
+ @Override
+ protected char[] getAcceptedChars()
+ {
+ return CHARACTERS;
+ }
+
+ public static DateKeyListener getInstance() {
+ if (sInstance != null)
+ return sInstance;
+
+ sInstance = new DateKeyListener();
+ return sInstance;
+ }
+
+ /**
+ * The characters that are used.
+ *
+ * @see KeyEvent#getMatch
+ * @see #getAcceptedChars
+ */
+ public static final char[] CHARACTERS = new char[] {
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+ '/', '-', '.'
+ };
+
+ private static DateKeyListener sInstance;
+}
diff --git a/core/java/android/text/method/DateTimeKeyListener.java b/core/java/android/text/method/DateTimeKeyListener.java
new file mode 100644
index 0000000..304d326
--- /dev/null
+++ b/core/java/android/text/method/DateTimeKeyListener.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.method;
+
+import android.view.KeyEvent;
+
+/**
+ * For entering dates and times in the same text field.
+ */
+public class DateTimeKeyListener extends NumberKeyListener
+{
+ @Override
+ protected char[] getAcceptedChars()
+ {
+ return CHARACTERS;
+ }
+
+ public static DateTimeKeyListener getInstance() {
+ if (sInstance != null)
+ return sInstance;
+
+ sInstance = new DateTimeKeyListener();
+ return sInstance;
+ }
+
+ /**
+ * The characters that are used.
+ *
+ * @see KeyEvent#getMatch
+ * @see #getAcceptedChars
+ */
+ public static final char[] CHARACTERS = new char[] {
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'm',
+ 'p', ':', '/', '-', ' '
+ };
+
+ private static DateTimeKeyListener sInstance;
+}
diff --git a/core/java/android/text/method/DialerKeyListener.java b/core/java/android/text/method/DialerKeyListener.java
new file mode 100644
index 0000000..e805ad7
--- /dev/null
+++ b/core/java/android/text/method/DialerKeyListener.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.method;
+
+import android.view.KeyEvent;
+import android.view.KeyCharacterMap.KeyData;
+import android.util.SparseIntArray;
+import android.text.Spannable;
+
+/**
+ * For dialing-only text entry
+ */
+public class DialerKeyListener extends NumberKeyListener
+{
+ @Override
+ protected char[] getAcceptedChars()
+ {
+ return CHARACTERS;
+ }
+
+ public static DialerKeyListener getInstance() {
+ if (sInstance != null)
+ return sInstance;
+
+ sInstance = new DialerKeyListener();
+ return sInstance;
+ }
+
+ /**
+ * Overrides the superclass's lookup method to prefer the number field
+ * from the KeyEvent.
+ */
+ protected int lookup(KeyEvent event, Spannable content) {
+ int meta = getMetaState(content);
+ int number = event.getNumber();
+
+ /*
+ * Prefer number if no meta key is active, or if it produces something
+ * valid and the meta lookup does not.
+ */
+ if ((meta & (KeyEvent.META_ALT_ON | KeyEvent.META_SHIFT_ON)) == 0) {
+ if (number != 0) {
+ return number;
+ }
+ }
+
+ int match = super.lookup(event, content);
+
+ if (match != 0) {
+ return match;
+ } else {
+ /*
+ * If a meta key is active but the lookup with the meta key
+ * did not produce anything, try some other meta keys, because
+ * the user might have pressed SHIFT when they meant ALT,
+ * or vice versa.
+ */
+
+ if (meta != 0) {
+ KeyData kd = new KeyData();
+ char[] accepted = getAcceptedChars();
+
+ if (event.getKeyData(kd)) {
+ for (int i = 1; i < kd.meta.length; i++) {
+ if (ok(accepted, kd.meta[i])) {
+ return kd.meta[i];
+ }
+ }
+ }
+ }
+
+ /*
+ * Otherwise, use the number associated with the key, since
+ * whatever they wanted to do with the meta key does not
+ * seem to be valid here.
+ */
+
+ return number;
+ }
+ }
+
+
+ /**
+ * The characters that are used.
+ *
+ * @see KeyEvent#getMatch
+ * @see #getAcceptedChars
+ */
+ public static final char[] CHARACTERS = new char[] {
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '#', '*',
+ '+', '-', '(', ')', ',', '/', 'N', '.', ' '
+ };
+
+ private static DialerKeyListener sInstance;
+}
diff --git a/core/java/android/text/method/DigitsKeyListener.java b/core/java/android/text/method/DigitsKeyListener.java
new file mode 100644
index 0000000..99a3f1a
--- /dev/null
+++ b/core/java/android/text/method/DigitsKeyListener.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.method;
+
+import android.text.Spanned;
+import android.text.SpannableStringBuilder;
+import android.view.KeyEvent;
+
+
+/**
+ * For digits-only text entry
+ */
+public class DigitsKeyListener extends NumberKeyListener
+{
+ private char[] mAccepted;
+ private boolean mSign;
+ private boolean mDecimal;
+
+ private static final int SIGN = 1;
+ private static final int DECIMAL = 2;
+
+ @Override
+ protected char[] getAcceptedChars() {
+ return mAccepted;
+ }
+
+ /**
+ * The characters that are used.
+ *
+ * @see KeyEvent#getMatch
+ * @see #getAcceptedChars
+ */
+ private static final char[][] CHARACTERS = new char[][] {
+ new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' },
+ new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-' },
+ new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.' },
+ new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '.' },
+ };
+
+ /**
+ * Allocates a DigitsKeyListener that accepts the digits 0 through 9.
+ */
+ public DigitsKeyListener() {
+ this(false, false);
+ }
+
+ /**
+ * Allocates a DigitsKeyListener that accepts the digits 0 through 9,
+ * plus the minus sign (only at the beginning) and/or decimal point
+ * (only one per field) if specified.
+ */
+ public DigitsKeyListener(boolean sign, boolean decimal) {
+ mSign = sign;
+ mDecimal = decimal;
+
+ int kind = (sign ? SIGN : 0) | (decimal ? DECIMAL : 0);
+ mAccepted = CHARACTERS[kind];
+ }
+
+ /**
+ * Returns a DigitsKeyListener that accepts the digits 0 through 9.
+ */
+ public static DigitsKeyListener getInstance() {
+ return getInstance(false, false);
+ }
+
+ /**
+ * Returns a DigitsKeyListener that accepts the digits 0 through 9,
+ * plus the minus sign (only at the beginning) and/or decimal point
+ * (only one per field) if specified.
+ */
+ public static DigitsKeyListener getInstance(boolean sign, boolean decimal) {
+ int kind = (sign ? SIGN : 0) | (decimal ? DECIMAL : 0);
+
+ if (sInstance[kind] != null)
+ return sInstance[kind];
+
+ sInstance[kind] = new DigitsKeyListener(sign, decimal);
+ return sInstance[kind];
+ }
+
+ /**
+ * Returns a DigitsKeyListener that accepts only the characters
+ * that appear in the specified String. Note that not all characters
+ * may be available on every keyboard.
+ */
+ public static DigitsKeyListener getInstance(String accepted) {
+ // TODO: do we need a cache of these to avoid allocating?
+
+ DigitsKeyListener dim = new DigitsKeyListener();
+
+ dim.mAccepted = new char[accepted.length()];
+ accepted.getChars(0, accepted.length(), dim.mAccepted, 0);
+
+ return dim;
+ }
+
+ @Override
+ public CharSequence filter(CharSequence source, int start, int end,
+ Spanned dest, int dstart, int dend) {
+ CharSequence out = super.filter(source, start, end, dest, dstart, dend);
+
+ if (mSign == false && mDecimal == false) {
+ return out;
+ }
+
+ if (out != null) {
+ source = out;
+ start = 0;
+ end = out.length();
+ }
+
+ int sign = -1;
+ int decimal = -1;
+ int dlen = dest.length();
+
+ /*
+ * Find out if the existing text has '-' or '.' characters.
+ */
+
+ for (int i = 0; i < dstart; i++) {
+ char c = dest.charAt(i);
+
+ if (c == '-') {
+ sign = i;
+ } else if (c == '.') {
+ decimal = i;
+ }
+ }
+ for (int i = dend; i < dlen; i++) {
+ char c = dest.charAt(i);
+
+ if (c == '-') {
+ return ""; // Nothing can be inserted in front of a '-'.
+ } else if (c == '.') {
+ decimal = i;
+ }
+ }
+
+ /*
+ * If it does, we must strip them out from the source.
+ * In addition, '-' must be the very first character,
+ * and nothing can be inserted before an existing '-'.
+ * Go in reverse order so the offsets are stable.
+ */
+
+ SpannableStringBuilder stripped = null;
+
+ for (int i = end - 1; i >= start; i--) {
+ char c = source.charAt(i);
+ boolean strip = false;
+
+ if (c == '-') {
+ if (i != start || dstart != 0) {
+ strip = true;
+ } else if (sign >= 0) {
+ strip = true;
+ } else {
+ sign = i;
+ }
+ } else if (c == '.') {
+ if (decimal >= 0) {
+ strip = true;
+ } else {
+ decimal = i;
+ }
+ }
+
+ if (strip) {
+ if (end == start + 1) {
+ return ""; // Only one character, and it was stripped.
+ }
+
+ if (stripped == null) {
+ stripped = new SpannableStringBuilder(source, start, end);
+ }
+
+ stripped.delete(i - start, i + 1 - start);
+ }
+ }
+
+ if (stripped != null) {
+ return stripped;
+ } else if (out != null) {
+ return out;
+ } else {
+ return null;
+ }
+ }
+
+ private static DigitsKeyListener[] sInstance = new DigitsKeyListener[4];
+}
diff --git a/core/java/android/text/method/HideReturnsTransformationMethod.java b/core/java/android/text/method/HideReturnsTransformationMethod.java
new file mode 100644
index 0000000..ce18692
--- /dev/null
+++ b/core/java/android/text/method/HideReturnsTransformationMethod.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.method;
+
+import android.graphics.Rect;
+import android.text.GetChars;
+import android.text.Spanned;
+import android.text.SpannedString;
+import android.text.TextUtils;
+import android.view.View;
+
+/**
+ * This transformation method causes any carriage return characters (\r)
+ * to be hidden by displaying them as zero-width non-breaking space
+ * characters (\uFEFF).
+ */
+public class HideReturnsTransformationMethod
+extends ReplacementTransformationMethod {
+ private static char[] ORIGINAL = new char[] { '\r' };
+ private static char[] REPLACEMENT = new char[] { '\uFEFF' };
+
+ /**
+ * The character to be replaced is \r.
+ */
+ protected char[] getOriginal() {
+ return ORIGINAL;
+ }
+
+ /**
+ * The character that \r is replaced with is \uFEFF.
+ */
+ protected char[] getReplacement() {
+ return REPLACEMENT;
+ }
+
+ public static HideReturnsTransformationMethod getInstance() {
+ if (sInstance != null)
+ return sInstance;
+
+ sInstance = new HideReturnsTransformationMethod();
+ return sInstance;
+ }
+
+ private static HideReturnsTransformationMethod sInstance;
+}
diff --git a/core/java/android/text/method/KeyListener.java b/core/java/android/text/method/KeyListener.java
new file mode 100644
index 0000000..05ab72d
--- /dev/null
+++ b/core/java/android/text/method/KeyListener.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.method;
+
+import android.view.KeyEvent;
+import android.view.View;
+import android.os.Message;
+import android.text.*;
+import android.widget.TextView;
+
+public interface KeyListener
+{
+ /**
+ * If the key listener wants to handle this key, return true,
+ * otherwise return false and the caller (i.e. the widget host)
+ * will handle the key.
+ */
+ public boolean onKeyDown(View view, Editable text,
+ int keyCode, KeyEvent event);
+
+ /**
+ * If the key listener wants to handle this key release, return true,
+ * otherwise return false and the caller (i.e. the widget host)
+ * will handle the key.
+ */
+ public boolean onKeyUp(View view, Editable text,
+ int keyCode, KeyEvent event);
+}
diff --git a/core/java/android/text/method/LinkMovementMethod.java b/core/java/android/text/method/LinkMovementMethod.java
new file mode 100644
index 0000000..92ac531
--- /dev/null
+++ b/core/java/android/text/method/LinkMovementMethod.java
@@ -0,0 +1,256 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.method;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.text.*;
+import android.text.style.*;
+import android.view.View;
+import android.widget.TextView;
+
+public class
+LinkMovementMethod
+extends ScrollingMovementMethod
+{
+ private static final int CLICK = 1;
+ private static final int UP = 2;
+ private static final int DOWN = 3;
+
+ @Override
+ public boolean onKeyDown(TextView widget, Spannable buffer,
+ int keyCode, KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ case KeyEvent.KEYCODE_ENTER:
+ if (event.getRepeatCount() == 0) {
+ if (action(CLICK, widget, buffer)) {
+ return true;
+ }
+ }
+ }
+
+ return super.onKeyDown(widget, buffer, keyCode, event);
+ }
+
+ @Override
+ protected boolean up(TextView widget, Spannable buffer) {
+ if (action(UP, widget, buffer)) {
+ return true;
+ }
+
+ return super.up(widget, buffer);
+ }
+
+ @Override
+ protected boolean down(TextView widget, Spannable buffer) {
+ if (action(DOWN, widget, buffer)) {
+ return true;
+ }
+
+ return super.down(widget, buffer);
+ }
+
+ @Override
+ protected boolean left(TextView widget, Spannable buffer) {
+ if (action(UP, widget, buffer)) {
+ return true;
+ }
+
+ return super.left(widget, buffer);
+ }
+
+ @Override
+ protected boolean right(TextView widget, Spannable buffer) {
+ if (action(DOWN, widget, buffer)) {
+ return true;
+ }
+
+ return super.right(widget, buffer);
+ }
+
+ private boolean action(int what, TextView widget, Spannable buffer) {
+ boolean handled = false;
+
+ Layout layout = widget.getLayout();
+
+ int padding = widget.getTotalPaddingTop() +
+ widget.getTotalPaddingBottom();
+ int areatop = widget.getScrollY();
+ int areabot = areatop + widget.getHeight() - padding;
+
+ int linetop = layout.getLineForVertical(areatop);
+ int linebot = layout.getLineForVertical(areabot);
+
+ int first = layout.getLineStart(linetop);
+ int last = layout.getLineEnd(linebot);
+
+ ClickableSpan[] candidates = buffer.getSpans(first, last, ClickableSpan.class);
+
+ int a = Selection.getSelectionStart(buffer);
+ int b = Selection.getSelectionEnd(buffer);
+
+ int selStart = Math.min(a, b);
+ int selEnd = Math.max(a, b);
+
+ if (selStart < 0) {
+ if (buffer.getSpanStart(FROM_BELOW) >= 0) {
+ selStart = selEnd = buffer.length();
+ }
+ }
+
+ if (selStart > last)
+ selStart = selEnd = Integer.MAX_VALUE;
+ if (selEnd < first)
+ selStart = selEnd = -1;
+
+ switch (what) {
+ case CLICK:
+ if (selStart == selEnd) {
+ return false;
+ }
+
+ ClickableSpan[] link = buffer.getSpans(selStart, selEnd, ClickableSpan.class);
+
+ if (link.length != 1)
+ return false;
+
+ link[0].onClick(widget);
+ break;
+
+ case UP:
+ int beststart, bestend;
+
+ beststart = -1;
+ bestend = -1;
+
+ for (int i = 0; i < candidates.length; i++) {
+ int end = buffer.getSpanEnd(candidates[i]);
+
+ if (end < selEnd || selStart == selEnd) {
+ if (end > bestend) {
+ beststart = buffer.getSpanStart(candidates[i]);
+ bestend = end;
+ }
+ }
+ }
+
+ if (beststart >= 0) {
+ Selection.setSelection(buffer, bestend, beststart);
+ return true;
+ }
+
+ break;
+
+ case DOWN:
+ beststart = Integer.MAX_VALUE;
+ bestend = Integer.MAX_VALUE;
+
+ for (int i = 0; i < candidates.length; i++) {
+ int start = buffer.getSpanStart(candidates[i]);
+
+ if (start > selStart || selStart == selEnd) {
+ if (start < beststart) {
+ beststart = start;
+ bestend = buffer.getSpanEnd(candidates[i]);
+ }
+ }
+ }
+
+ if (bestend < Integer.MAX_VALUE) {
+ Selection.setSelection(buffer, beststart, bestend);
+ return true;
+ }
+
+ break;
+ }
+
+ return false;
+ }
+
+ public boolean onKeyUp(TextView widget, Spannable buffer,
+ int keyCode, KeyEvent event) {
+ return false;
+ }
+
+ @Override
+ public boolean onTouchEvent(TextView widget, Spannable buffer,
+ MotionEvent event) {
+ int action = event.getAction();
+
+ if (action == MotionEvent.ACTION_UP ||
+ action == MotionEvent.ACTION_DOWN) {
+ int x = (int) event.getX();
+ int y = (int) event.getY();
+
+ x -= widget.getTotalPaddingLeft();
+ y -= widget.getTotalPaddingTop();
+
+ x += widget.getScrollX();
+ y += widget.getScrollY();
+
+ Layout layout = widget.getLayout();
+ int line = layout.getLineForVertical(y);
+ int off = layout.getOffsetForHorizontal(line, x);
+
+ ClickableSpan[] link = buffer.getSpans(off, off, ClickableSpan.class);
+
+ if (link.length != 0) {
+ if (action == MotionEvent.ACTION_UP) {
+ link[0].onClick(widget);
+ } else if (action == MotionEvent.ACTION_DOWN) {
+ Selection.setSelection(buffer,
+ buffer.getSpanStart(link[0]),
+ buffer.getSpanEnd(link[0]));
+ }
+
+ return true;
+ } else {
+ Selection.removeSelection(buffer);
+ }
+ }
+
+ return super.onTouchEvent(widget, buffer, event);
+ }
+
+ public void initialize(TextView widget, Spannable text) {
+ Selection.removeSelection(text);
+ text.removeSpan(FROM_BELOW);
+ }
+
+ public void onTakeFocus(TextView view, Spannable text, int dir) {
+ Selection.removeSelection(text);
+
+ if ((dir & View.FOCUS_BACKWARD) != 0) {
+ text.setSpan(FROM_BELOW, 0, 0, Spannable.SPAN_POINT_POINT);
+ } else {
+ text.removeSpan(FROM_BELOW);
+ }
+ }
+
+ public static MovementMethod getInstance() {
+ if (sInstance == null)
+ sInstance = new LinkMovementMethod();
+
+ return sInstance;
+ }
+
+ private static LinkMovementMethod sInstance;
+ private static Object FROM_BELOW = new Object();
+}
diff --git a/core/java/android/text/method/MetaKeyKeyListener.java b/core/java/android/text/method/MetaKeyKeyListener.java
new file mode 100644
index 0000000..2d75b87
--- /dev/null
+++ b/core/java/android/text/method/MetaKeyKeyListener.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.method;
+
+import android.view.KeyEvent;
+import android.view.View;
+import android.text.*;
+
+/**
+ * This base class encapsulates the behavior for handling the meta keys
+ * (caps, fn, sym). Key listener that care about meta state should
+ * inherit from it; you should not instantiate this class directly in a client.
+ */
+
+public abstract class MetaKeyKeyListener {
+ public static final int META_SHIFT_ON = KeyEvent.META_SHIFT_ON;
+ public static final int META_ALT_ON = KeyEvent.META_ALT_ON;
+ public static final int META_SYM_ON = KeyEvent.META_SYM_ON;
+
+ public static final int META_CAP_LOCKED = KeyEvent.META_SHIFT_ON << 8;
+ public static final int META_ALT_LOCKED = KeyEvent.META_ALT_ON << 8;
+ public static final int META_SYM_LOCKED = KeyEvent.META_SYM_ON << 8;
+
+ private static final Object CAP = new Object();
+ private static final Object ALT = new Object();
+ private static final Object SYM = new Object();
+
+ /**
+ * Resets all meta state to inactive.
+ */
+ public static void resetMetaState(Spannable text) {
+ text.removeSpan(CAP);
+ text.removeSpan(ALT);
+ text.removeSpan(SYM);
+ }
+
+ /**
+ * Gets the state of the meta keys.
+ *
+ * @param text the buffer in which the meta key would have been pressed.
+ *
+ * @return an integer in which each bit set to one represents a pressed
+ * or locked meta key.
+ */
+ public static final int getMetaState(CharSequence text) {
+ return getActive(text, CAP, META_SHIFT_ON, META_CAP_LOCKED) |
+ getActive(text, ALT, META_ALT_ON, META_ALT_LOCKED) |
+ getActive(text, SYM, META_SYM_ON, META_SYM_LOCKED);
+ }
+
+ /**
+ * Gets the state of a particular meta key.
+ *
+ * @param meta META_SHIFT_ON, META_ALT_ON, or META_SYM_ON
+ * @param text the buffer in which the meta key would have been pressed.
+ *
+ * @return 0 if inactive, 1 if active, 2 if locked.
+ */
+ public static final int getMetaState(CharSequence text, int meta) {
+ switch (meta) {
+ case META_SHIFT_ON:
+ return getActive(text, CAP, 1, 2);
+
+ case META_ALT_ON:
+ return getActive(text, ALT, 1, 2);
+
+ case META_SYM_ON:
+ return getActive(text, SYM, 1, 2);
+
+ default:
+ return 0;
+ }
+ }
+
+ private static int getActive(CharSequence text, Object meta,
+ int on, int lock) {
+ if (!(text instanceof Spanned)) {
+ return 0;
+ }
+
+ Spanned sp = (Spanned) text;
+ int flag = sp.getSpanFlags(meta);
+
+ if (flag == LOCKED) {
+ return lock;
+ } else if (flag != 0) {
+ return on;
+ } else {
+ return 0;
+ }
+ }
+
+ /**
+ * Call this method after you handle a keypress so that the meta
+ * state will be reset to unshifted (if it is not still down)
+ * or primed to be reset to unshifted (once it is released).
+ */
+ public static void adjustMetaAfterKeypress(Spannable content) {
+ adjust(content, CAP);
+ adjust(content, ALT);
+ adjust(content, SYM);
+ }
+
+ /**
+ * Returns true if this object is one that this class would use to
+ * keep track of meta state in the specified text.
+ */
+ public static boolean isMetaTracker(CharSequence text, Object what) {
+ return what == CAP || what == ALT || what == SYM;
+ }
+
+ private static void adjust(Spannable content, Object what) {
+ int current = content.getSpanFlags(what);
+
+ if (current == PRESSED)
+ content.setSpan(what, 0, 0, USED);
+ else if (current == RELEASED)
+ content.removeSpan(what);
+ }
+
+ /**
+ * Call this if you are a method that ignores the locked meta state
+ * (arrow keys, for example) and you handle a key.
+ */
+ protected static void resetLockedMeta(Spannable content) {
+ resetLock(content, CAP);
+ resetLock(content, ALT);
+ resetLock(content, SYM);
+ }
+
+ private static void resetLock(Spannable content, Object what) {
+ int current = content.getSpanFlags(what);
+
+ if (current == LOCKED)
+ content.removeSpan(what);
+ }
+
+ /**
+ * Handles presses of the meta keys.
+ */
+ public boolean onKeyDown(View view, Editable content,
+ int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT) {
+ press(content, CAP);
+ return true;
+ }
+
+ if (keyCode == KeyEvent.KEYCODE_ALT_LEFT || keyCode == KeyEvent.KEYCODE_ALT_RIGHT
+ || keyCode == KeyEvent.KEYCODE_NUM) {
+ press(content, ALT);
+ return true;
+ }
+
+ if (keyCode == KeyEvent.KEYCODE_SYM) {
+ press(content, SYM);
+ return true;
+ }
+
+ return false; // no super to call through to
+ }
+
+ private void press(Editable content, Object what) {
+ int state = content.getSpanFlags(what);
+
+ if (state == PRESSED)
+ ; // repeat before use
+ else if (state == RELEASED)
+ content.setSpan(what, 0, 0, LOCKED);
+ else if (state == USED)
+ ; // repeat after use
+ else if (state == LOCKED)
+ content.removeSpan(what);
+ else
+ content.setSpan(what, 0, 0, PRESSED);
+ }
+
+ /**
+ * Handles release of the meta keys.
+ */
+ public boolean onKeyUp(View view, Editable content, int keyCode,
+ KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT) {
+ release(content, CAP);
+ return true;
+ }
+
+ if (keyCode == KeyEvent.KEYCODE_ALT_LEFT || keyCode == KeyEvent.KEYCODE_ALT_RIGHT
+ || keyCode == KeyEvent.KEYCODE_NUM) {
+ release(content, ALT);
+ return true;
+ }
+
+ if (keyCode == KeyEvent.KEYCODE_SYM) {
+ release(content, SYM);
+ return true;
+ }
+
+ return false; // no super to call through to
+ }
+
+ private void release(Editable content, Object what) {
+ int current = content.getSpanFlags(what);
+
+ if (current == USED)
+ content.removeSpan(what);
+ else if (current == PRESSED)
+ content.setSpan(what, 0, 0, RELEASED);
+ }
+
+ /**
+ * The meta key has been pressed but has not yet been used.
+ */
+ private static final int PRESSED =
+ Spannable.SPAN_MARK_MARK | (1 << Spannable.SPAN_USER_SHIFT);
+
+ /**
+ * The meta key has been pressed and released but has still
+ * not yet been used.
+ */
+ private static final int RELEASED =
+ Spannable.SPAN_MARK_MARK | (2 << Spannable.SPAN_USER_SHIFT);
+
+ /**
+ * The meta key has been pressed and used but has not yet been released.
+ */
+ private static final int USED =
+ Spannable.SPAN_MARK_MARK | (3 << Spannable.SPAN_USER_SHIFT);
+
+ /**
+ * The meta key has been pressed and released without use, and then
+ * pressed again; it may also have been released again.
+ */
+ private static final int LOCKED =
+ Spannable.SPAN_MARK_MARK | (4 << Spannable.SPAN_USER_SHIFT);
+}
+
diff --git a/core/java/android/text/method/MovementMethod.java b/core/java/android/text/method/MovementMethod.java
new file mode 100644
index 0000000..9e37e59
--- /dev/null
+++ b/core/java/android/text/method/MovementMethod.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.method;
+
+import android.widget.TextView;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.text.*;
+
+public interface MovementMethod
+{
+ public void initialize(TextView widget, Spannable text);
+ public boolean onKeyDown(TextView widget, Spannable text, int keyCode, KeyEvent event);
+ public boolean onKeyUp(TextView widget, Spannable text, int keyCode, KeyEvent event);
+ public void onTakeFocus(TextView widget, Spannable text, int direction);
+ public boolean onTrackballEvent(TextView widget, Spannable text,
+ MotionEvent event);
+ public boolean onTouchEvent(TextView widget, Spannable text,
+ MotionEvent event);
+
+ /**
+ * Returns true if this movement method allows arbitrary selection
+ * of any text; false if it has no selection (like a movement method
+ * that only scrolls) or a constrained selection (for example
+ * limited to links. The "Select All" menu item is disabled
+ * if arbitrary selection is not allowed.
+ */
+ public boolean canSelectArbitrarily();
+}
diff --git a/core/java/android/text/method/MultiTapKeyListener.java b/core/java/android/text/method/MultiTapKeyListener.java
new file mode 100644
index 0000000..7137d40
--- /dev/null
+++ b/core/java/android/text/method/MultiTapKeyListener.java
@@ -0,0 +1,285 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.method;
+
+import android.view.KeyEvent;
+import android.view.View;
+import android.os.Message;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.text.*;
+import android.text.method.TextKeyListener.Capitalize;
+import android.widget.TextView;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
+
+/**
+ * This is the standard key listener for alphabetic input on 12-key
+ * keyboards. You should generally not need to instantiate this yourself;
+ * TextKeyListener will do it for you.
+ */
+public class MultiTapKeyListener extends BaseKeyListener
+ implements SpanWatcher {
+ private static MultiTapKeyListener[] sInstance =
+ new MultiTapKeyListener[Capitalize.values().length * 2];
+
+ private static final SparseArray<String> sRecs = new SparseArray<String>();
+
+ private Capitalize mCapitalize;
+ private boolean mAutoText;
+
+ static {
+ sRecs.put(KeyEvent.KEYCODE_1, ".,1!@#$%^&*:/?'=()");
+ sRecs.put(KeyEvent.KEYCODE_2, "abc2ABC");
+ sRecs.put(KeyEvent.KEYCODE_3, "def3DEF");
+ sRecs.put(KeyEvent.KEYCODE_4, "ghi4GHI");
+ sRecs.put(KeyEvent.KEYCODE_5, "jkl5JKL");
+ sRecs.put(KeyEvent.KEYCODE_6, "mno6MNO");
+ sRecs.put(KeyEvent.KEYCODE_7, "pqrs7PQRS");
+ sRecs.put(KeyEvent.KEYCODE_8, "tuv8TUV");
+ sRecs.put(KeyEvent.KEYCODE_9, "wxyz9WXYZ");
+ sRecs.put(KeyEvent.KEYCODE_0, "0+");
+ sRecs.put(KeyEvent.KEYCODE_POUND, " ");
+ };
+
+ public MultiTapKeyListener(Capitalize cap,
+ boolean autotext) {
+ mCapitalize = cap;
+ mAutoText = autotext;
+ }
+
+ /**
+ * Returns a new or existing instance with the specified capitalization
+ * and correction properties.
+ */
+ public static MultiTapKeyListener getInstance(boolean autotext,
+ Capitalize cap) {
+ int off = cap.ordinal() * 2 + (autotext ? 1 : 0);
+
+ if (sInstance[off] == null) {
+ sInstance[off] = new MultiTapKeyListener(cap, autotext);
+ }
+
+ return sInstance[off];
+ }
+
+ public boolean onKeyDown(View view, Editable content,
+ int keyCode, KeyEvent event) {
+ int selStart, selEnd;
+ int pref = 0;
+
+ if (view != null) {
+ pref = TextKeyListener.getInstance().getPrefs(view.getContext());
+ }
+
+ {
+ int a = Selection.getSelectionStart(content);
+ int b = Selection.getSelectionEnd(content);
+
+ selStart = Math.min(a, b);
+ selEnd = Math.max(a, b);
+ }
+
+ int activeStart = content.getSpanStart(TextKeyListener.ACTIVE);
+ int activeEnd = content.getSpanEnd(TextKeyListener.ACTIVE);
+
+ // now for the multitap cases...
+
+ // Try to increment the character we were working on before
+ // if we have one and it's still the same key.
+
+ int rec = (content.getSpanFlags(TextKeyListener.ACTIVE)
+ & Spannable.SPAN_USER) >>> Spannable.SPAN_USER_SHIFT;
+
+ if (activeStart == selStart && activeEnd == selEnd &&
+ selEnd - selStart == 1 &&
+ rec >= 0 && rec < sRecs.size()) {
+ if (keyCode == KeyEvent.KEYCODE_STAR) {
+ char current = content.charAt(selStart);
+
+ if (Character.isLowerCase(current)) {
+ content.replace(selStart, selEnd,
+ String.valueOf(current).toUpperCase());
+ removeTimeouts(content);
+ Timeout t = new Timeout(content);
+
+ return true;
+ }
+ if (Character.isUpperCase(current)) {
+ content.replace(selStart, selEnd,
+ String.valueOf(current).toLowerCase());
+ removeTimeouts(content);
+ Timeout t = new Timeout(content);
+
+ return true;
+ }
+ }
+
+ if (sRecs.indexOfKey(keyCode) == rec) {
+ String val = sRecs.valueAt(rec);
+ char ch = content.charAt(selStart);
+ int ix = val.indexOf(ch);
+
+ if (ix >= 0) {
+ ix = (ix + 1) % (val.length());
+
+ content.replace(selStart, selEnd, val, ix, ix + 1);
+ removeTimeouts(content);
+ Timeout t = new Timeout(content);
+
+ return true;
+ }
+ }
+
+ // Is this key one we know about at all? If so, acknowledge
+ // that the selection is our fault but the key has changed
+ // or the text no longer matches, so move the selection over
+ // so that it inserts instead of replaces.
+
+ rec = sRecs.indexOfKey(keyCode);
+
+ if (rec >= 0) {
+ Selection.setSelection(content, selEnd, selEnd);
+ selStart = selEnd;
+ }
+ } else {
+ rec = sRecs.indexOfKey(keyCode);
+ }
+
+ if (rec >= 0) {
+ // We have a valid key. Replace the selection or insertion point
+ // with the first character for that key, and remember what
+ // record it came from for next time.
+
+ String val = sRecs.valueAt(rec);
+
+ int off = 0;
+ if ((pref & TextKeyListener.AUTO_CAP) != 0 &&
+ TextKeyListener.shouldCap(mCapitalize, content, selStart)) {
+ for (int i = 0; i < val.length(); i++) {
+ if (Character.isUpperCase(val.charAt(i))) {
+ off = i;
+ break;
+ }
+ }
+ }
+
+ if (selStart != selEnd) {
+ Selection.setSelection(content, selEnd);
+ }
+
+ content.setSpan(OLD_SEL_START, selStart, selStart,
+ Spannable.SPAN_MARK_MARK);
+
+ content.replace(selStart, selEnd, val, off, off + 1);
+
+ int oldStart = content.getSpanStart(OLD_SEL_START);
+ selEnd = Selection.getSelectionEnd(content);
+
+ if (selEnd != oldStart) {
+ Selection.setSelection(content, oldStart, selEnd);
+
+ content.setSpan(TextKeyListener.LAST_TYPED,
+ oldStart, selEnd,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+ content.setSpan(TextKeyListener.ACTIVE,
+ oldStart, selEnd,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE |
+ (rec << Spannable.SPAN_USER_SHIFT));
+
+ }
+
+ removeTimeouts(content);
+ Timeout t = new Timeout(content);
+
+ // Set up the callback so we can remove the timeout if the
+ // cursor moves.
+
+ if (content.getSpanStart(this) < 0) {
+ KeyListener[] methods = content.getSpans(0, content.length(),
+ KeyListener.class);
+ for (Object method : methods) {
+ content.removeSpan(method);
+ }
+ content.setSpan(this, 0, content.length(),
+ Spannable.SPAN_INCLUSIVE_INCLUSIVE);
+ }
+
+ return true;
+ }
+
+ return super.onKeyDown(view, content, keyCode, event);
+ }
+
+ public void onSpanChanged(Spannable buf,
+ Object what, int s, int e, int start, int stop) {
+ if (what == Selection.SELECTION_END) {
+ buf.removeSpan(TextKeyListener.ACTIVE);
+ removeTimeouts(buf);
+ }
+ }
+
+ private static void removeTimeouts(Spannable buf) {
+ Timeout[] timeout = buf.getSpans(0, buf.length(), Timeout.class);
+
+ for (int i = 0; i < timeout.length; i++) {
+ Timeout t = timeout[i];
+
+ t.removeCallbacks(t);
+ t.mBuffer = null;
+ buf.removeSpan(t);
+ }
+ }
+
+ private class Timeout
+ extends Handler
+ implements Runnable
+ {
+ public Timeout(Editable buffer) {
+ mBuffer = buffer;
+ mBuffer.setSpan(Timeout.this, 0, mBuffer.length(),
+ Spannable.SPAN_INCLUSIVE_INCLUSIVE);
+
+ postAtTime(this, SystemClock.uptimeMillis() + 2000);
+ }
+
+ public void run() {
+ Spannable buf = mBuffer;
+
+ if (buf != null) {
+ int st = Selection.getSelectionStart(buf);
+ int en = Selection.getSelectionEnd(buf);
+
+ int start = buf.getSpanStart(TextKeyListener.ACTIVE);
+ int end = buf.getSpanEnd(TextKeyListener.ACTIVE);
+
+ if (st == start && en == end) {
+ Selection.setSelection(buf, Selection.getSelectionEnd(buf));
+ }
+
+ buf.removeSpan(Timeout.this);
+ }
+ }
+
+ private Editable mBuffer;
+ }
+
+ public void onSpanAdded(Spannable s, Object what, int start, int end) { }
+ public void onSpanRemoved(Spannable s, Object what, int start, int end) { }
+}
+
diff --git a/core/java/android/text/method/NumberKeyListener.java b/core/java/android/text/method/NumberKeyListener.java
new file mode 100644
index 0000000..348b658
--- /dev/null
+++ b/core/java/android/text/method/NumberKeyListener.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.method;
+
+import android.view.KeyEvent;
+import android.view.View;
+import android.text.Editable;
+import android.text.InputFilter;
+import android.text.Selection;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.util.SparseIntArray;
+
+/**
+ * For numeric text entry
+ */
+public abstract class NumberKeyListener extends BaseKeyListener
+ implements InputFilter
+{
+ /**
+ * You can say which characters you can accept.
+ */
+ protected abstract char[] getAcceptedChars();
+
+ protected int lookup(KeyEvent event, Spannable content) {
+ return event.getMatch(getAcceptedChars(), getMetaState(content));
+ }
+
+ public CharSequence filter(CharSequence source, int start, int end,
+ Spanned dest, int dstart, int dend) {
+ char[] accept = getAcceptedChars();
+ boolean filter = false;
+
+ int i;
+ for (i = start; i < end; i++) {
+ if (!ok(accept, source.charAt(i))) {
+ break;
+ }
+ }
+
+ if (i == end) {
+ // It was all OK.
+ return null;
+ }
+
+ if (end - start == 1) {
+ // It was not OK, and there is only one char, so nothing remains.
+ return "";
+ }
+
+ SpannableStringBuilder filtered =
+ new SpannableStringBuilder(source, start, end);
+ i -= start;
+ end -= start;
+
+ int len = end - start;
+ // Only count down to i because the chars before that were all OK.
+ for (int j = end - 1; j >= i; j--) {
+ if (!ok(accept, source.charAt(j))) {
+ filtered.delete(j, j + 1);
+ }
+ }
+
+ return filtered;
+ }
+
+ protected static boolean ok(char[] accept, char c) {
+ for (int i = accept.length - 1; i >= 0; i--) {
+ if (accept[i] == c) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean onKeyDown(View view, Editable content,
+ int keyCode, KeyEvent event) {
+ int selStart, selEnd;
+
+ {
+ int a = Selection.getSelectionStart(content);
+ int b = Selection.getSelectionEnd(content);
+
+ selStart = Math.min(a, b);
+ selEnd = Math.max(a, b);
+ }
+
+ int i = event != null ? lookup(event, content) : 0;
+ int repeatCount = event != null ? event.getRepeatCount() : 0;
+ if (repeatCount == 0) {
+ if (i != 0) {
+ if (selStart != selEnd) {
+ Selection.setSelection(content, selEnd);
+ }
+
+ content.replace(selStart, selEnd, String.valueOf((char) i));
+
+ adjustMetaAfterKeypress(content);
+ return true;
+ }
+ } else if (i == '0' && repeatCount == 1) {
+ // Pretty hackish, it replaces the 0 with the +
+
+ if (selStart == selEnd && selEnd > 0 &&
+ content.charAt(selStart - 1) == '0') {
+ content.replace(selStart - 1, selEnd, String.valueOf('+'));
+ adjustMetaAfterKeypress(content);
+ return true;
+ }
+ }
+
+ adjustMetaAfterKeypress(content);
+ return super.onKeyDown(view, content, keyCode, event);
+ }
+}
diff --git a/core/java/android/text/method/PasswordTransformationMethod.java b/core/java/android/text/method/PasswordTransformationMethod.java
new file mode 100644
index 0000000..edaa836
--- /dev/null
+++ b/core/java/android/text/method/PasswordTransformationMethod.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.method;
+
+import android.os.Handler;
+import android.os.SystemClock;
+import android.graphics.Rect;
+import android.view.View;
+import android.text.Editable;
+import android.text.GetChars;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.text.Selection;
+import android.text.Spanned;
+import android.text.Spannable;
+import android.text.style.UpdateLayout;
+
+import java.lang.ref.WeakReference;
+
+public class PasswordTransformationMethod
+implements TransformationMethod, TextWatcher
+{
+ public CharSequence getTransformation(CharSequence source, View view) {
+ if (source instanceof Spannable) {
+ Spannable sp = (Spannable) source;
+
+ /*
+ * Remove any references to other views that may still be
+ * attached. This will happen when you flip the screen
+ * while a password field is showing; there will still
+ * be references to the old EditText in the text.
+ */
+ ViewReference[] vr = sp.getSpans(0, sp.length(),
+ ViewReference.class);
+ for (int i = 0; i < vr.length; i++) {
+ sp.removeSpan(vr[i]);
+ }
+
+ sp.setSpan(new ViewReference(view), 0, 0,
+ Spannable.SPAN_POINT_POINT);
+ }
+
+ return new PasswordCharSequence(source);
+ }
+
+ public static PasswordTransformationMethod getInstance() {
+ if (sInstance != null)
+ return sInstance;
+
+ sInstance = new PasswordTransformationMethod();
+ return sInstance;
+ }
+
+ public void beforeTextChanged(CharSequence s, int start,
+ int count, int after) {
+ // This callback isn't used.
+ }
+
+ public void onTextChanged(CharSequence s, int start,
+ int before, int count) {
+ if (s instanceof Spannable) {
+ Spannable sp = (Spannable) s;
+ ViewReference[] vr = sp.getSpans(0, s.length(),
+ ViewReference.class);
+ if (vr.length == 0) {
+ return;
+ }
+
+ /*
+ * There should generally only be one ViewReference in the text,
+ * but make sure to look through all of them if necessary in case
+ * something strange is going on. (We might still end up with
+ * multiple ViewReferences if someone moves text from one password
+ * field to another.)
+ */
+ View v = null;
+ for (int i = 0; v == null && i < vr.length; i++) {
+ v = vr[i].get();
+ }
+
+ if (v == null) {
+ return;
+ }
+
+ int pref = TextKeyListener.getInstance().getPrefs(v.getContext());
+ if ((pref & TextKeyListener.SHOW_PASSWORD) != 0) {
+ if (count > 0) {
+ Visible[] old = sp.getSpans(0, sp.length(), Visible.class);
+ for (int i = 0; i < old.length; i++) {
+ sp.removeSpan(old[i]);
+ }
+
+ sp.setSpan(new Visible(sp, this), start, start + count,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+ }
+ }
+
+ public void afterTextChanged(Editable s) {
+ // This callback isn't used.
+ }
+
+ public void onFocusChanged(View view, CharSequence sourceText,
+ boolean focused, int direction,
+ Rect previouslyFocusedRect) {
+ if (!focused) {
+ if (sourceText instanceof Spannable) {
+ Spannable sp = (Spannable) sourceText;
+
+ Visible[] old = sp.getSpans(0, sp.length(), Visible.class);
+ for (int i = 0; i < old.length; i++) {
+ sp.removeSpan(old[i]);
+ }
+ }
+ }
+ }
+
+ private static class PasswordCharSequence
+ implements CharSequence, GetChars
+ {
+ public PasswordCharSequence(CharSequence source) {
+ mSource = source;
+ }
+
+ public int length() {
+ return mSource.length();
+ }
+
+ public char charAt(int i) {
+ if (mSource instanceof Spanned) {
+ Spanned sp = (Spanned) mSource;
+
+ int st = sp.getSpanStart(TextKeyListener.ACTIVE);
+ int en = sp.getSpanEnd(TextKeyListener.ACTIVE);
+
+ if (i >= st && i < en) {
+ return mSource.charAt(i);
+ }
+
+ Visible[] visible = sp.getSpans(0, sp.length(), Visible.class);
+
+ for (int a = 0; a < visible.length; a++) {
+ if (sp.getSpanStart(visible[a].mTransformer) >= 0) {
+ st = sp.getSpanStart(visible[a]);
+ en = sp.getSpanEnd(visible[a]);
+
+ if (i >= st && i < en) {
+ return mSource.charAt(i);
+ }
+ }
+ }
+ }
+
+ return DOT;
+ }
+
+ public CharSequence subSequence(int start, int end) {
+ char[] buf = new char[end - start];
+
+ getChars(start, end, buf, 0);
+ return new String(buf);
+ }
+
+ public String toString() {
+ return subSequence(0, length()).toString();
+ }
+
+ public void getChars(int start, int end, char[] dest, int off) {
+ TextUtils.getChars(mSource, start, end, dest, off);
+
+ int st = -1, en = -1;
+ int nvisible = 0;
+ int[] starts = null, ends = null;
+
+ if (mSource instanceof Spanned) {
+ Spanned sp = (Spanned) mSource;
+
+ st = sp.getSpanStart(TextKeyListener.ACTIVE);
+ en = sp.getSpanEnd(TextKeyListener.ACTIVE);
+
+ Visible[] visible = sp.getSpans(0, sp.length(), Visible.class);
+ nvisible = visible.length;
+ starts = new int[nvisible];
+ ends = new int[nvisible];
+
+ for (int i = 0; i < nvisible; i++) {
+ if (sp.getSpanStart(visible[i].mTransformer) >= 0) {
+ starts[i] = sp.getSpanStart(visible[i]);
+ ends[i] = sp.getSpanEnd(visible[i]);
+ }
+ }
+ }
+
+ for (int i = start; i < end; i++) {
+ if (! (i >= st && i < en)) {
+ boolean visible = false;
+
+ for (int a = 0; a < nvisible; a++) {
+ if (i >= starts[a] && i < ends[a]) {
+ visible = true;
+ break;
+ }
+ }
+
+ if (!visible) {
+ dest[i - start + off] = DOT;
+ }
+ }
+ }
+ }
+
+ private CharSequence mSource;
+ }
+
+ private static class Visible
+ extends Handler
+ implements UpdateLayout, Runnable
+ {
+ public Visible(Spannable sp, PasswordTransformationMethod ptm) {
+ mText = sp;
+ mTransformer = ptm;
+ postAtTime(this, SystemClock.uptimeMillis() + 1500);
+ }
+
+ public void run() {
+ mText.removeSpan(this);
+ }
+
+ private Spannable mText;
+ private PasswordTransformationMethod mTransformer;
+ }
+
+ /**
+ * Used to stash a reference back to the View in the Editable so we
+ * can use it to check the settings.
+ */
+ private static class ViewReference extends WeakReference<View> {
+ public ViewReference(View v) {
+ super(v);
+ }
+ }
+
+ private static PasswordTransformationMethod sInstance;
+ private static char DOT = '\u2022';
+}
diff --git a/core/java/android/text/method/QwertyKeyListener.java b/core/java/android/text/method/QwertyKeyListener.java
new file mode 100644
index 0000000..ae7ba8f
--- /dev/null
+++ b/core/java/android/text/method/QwertyKeyListener.java
@@ -0,0 +1,444 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.method;
+
+import android.os.Message;
+import android.os.Handler;
+import android.text.*;
+import android.text.method.TextKeyListener.Capitalize;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.view.View;
+import android.widget.TextView;
+
+import java.util.HashMap;
+
+/**
+ * This is the standard key listener for alphabetic input on qwerty
+ * keyboards. You should generally not need to instantiate this yourself;
+ * TextKeyListener will do it for you.
+ */
+public class QwertyKeyListener extends BaseKeyListener {
+ private static QwertyKeyListener[] sInstance =
+ new QwertyKeyListener[Capitalize.values().length * 2];
+
+ public QwertyKeyListener(Capitalize cap, boolean autotext) {
+ mAutoCap = cap;
+ mAutoText = autotext;
+ }
+
+ /**
+ * Returns a new or existing instance with the specified capitalization
+ * and correction properties.
+ */
+ public static QwertyKeyListener getInstance(boolean autotext,
+ Capitalize cap) {
+ int off = cap.ordinal() * 2 + (autotext ? 1 : 0);
+
+ if (sInstance[off] == null) {
+ sInstance[off] = new QwertyKeyListener(cap, autotext);
+ }
+
+ return sInstance[off];
+ }
+
+ public boolean onKeyDown(View view, Editable content,
+ int keyCode, KeyEvent event) {
+ int selStart, selEnd;
+ int pref = 0;
+
+ if (view != null) {
+ pref = TextKeyListener.getInstance().getPrefs(view.getContext());
+ }
+
+ {
+ int a = Selection.getSelectionStart(content);
+ int b = Selection.getSelectionEnd(content);
+
+ selStart = Math.min(a, b);
+ selEnd = Math.max(a, b);
+
+ if (selStart < 0 || selEnd < 0) {
+ selStart = selEnd = 0;
+ Selection.setSelection(content, 0, 0);
+ }
+ }
+
+ int activeStart = content.getSpanStart(TextKeyListener.ACTIVE);
+ int activeEnd = content.getSpanEnd(TextKeyListener.ACTIVE);
+
+ // QWERTY keyboard normal case
+
+ int i = event.getUnicodeChar(getMetaState(content));
+
+ int count = event.getRepeatCount();
+ if (count > 0 && selStart == selEnd && selStart > 0) {
+ char c = content.charAt(selStart - 1);
+
+ if (c == i || c == Character.toUpperCase(i) && view != null) {
+ if (showCharacterPicker(view, content, c, false, count)) {
+ resetMetaState(content);
+ return true;
+ }
+ }
+ }
+
+ if (i == KeyCharacterMap.PICKER_DIALOG_INPUT) {
+ if (view != null) {
+ showCharacterPicker(view, content,
+ KeyCharacterMap.PICKER_DIALOG_INPUT, true, 1);
+ }
+ resetMetaState(content);
+ return true;
+ }
+
+ if (i == KeyCharacterMap.HEX_INPUT) {
+ int start;
+
+ if (selStart == selEnd) {
+ start = selEnd;
+
+ while (start > 0 && selEnd - start < 4 &&
+ Character.digit(content.charAt(start - 1), 16) >= 0) {
+ start--;
+ }
+ } else {
+ start = selStart;
+ }
+
+ int ch = -1;
+ try {
+ String hex = TextUtils.substring(content, start, selEnd);
+ ch = Integer.parseInt(hex, 16);
+ } catch (NumberFormatException nfe) { }
+
+ if (ch >= 0) {
+ selStart = start;
+ Selection.setSelection(content, selStart, selEnd);
+ i = ch;
+ } else {
+ i = 0;
+ }
+ }
+
+ if (i != 0) {
+ boolean dead = false;
+
+ if ((i & KeyCharacterMap.COMBINING_ACCENT) != 0) {
+ dead = true;
+ i = i & KeyCharacterMap.COMBINING_ACCENT_MASK;
+ }
+
+ if (activeStart == selStart && activeEnd == selEnd) {
+ boolean replace = false;
+
+ if (selEnd - selStart - 1 == 0) {
+ char accent = content.charAt(selStart);
+ int composed = event.getDeadChar(accent, i);
+
+ if (composed != 0) {
+ i = composed;
+ replace = true;
+ }
+ }
+
+ if (!replace) {
+ Selection.setSelection(content, selEnd);
+ content.removeSpan(TextKeyListener.ACTIVE);
+ selStart = selEnd;
+ }
+ }
+
+ if ((pref & TextKeyListener.AUTO_CAP) != 0 &&
+ Character.isLowerCase(i) &&
+ TextKeyListener.shouldCap(mAutoCap, content, selStart)) {
+ int where = content.getSpanEnd(TextKeyListener.CAPPED);
+ int flags = content.getSpanFlags(TextKeyListener.CAPPED);
+
+ if (where == selStart && (((flags >> 16) & 0xFFFF) == i)) {
+ content.removeSpan(TextKeyListener.CAPPED);
+ } else {
+ flags = i << 16;
+ i = Character.toUpperCase(i);
+
+ if (selStart == 0)
+ content.setSpan(TextKeyListener.CAPPED, 0, 0,
+ Spannable.SPAN_MARK_MARK | flags);
+ else
+ content.setSpan(TextKeyListener.CAPPED,
+ selStart - 1, selStart,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE |
+ flags);
+ }
+ }
+
+ if (selStart != selEnd) {
+ Selection.setSelection(content, selEnd);
+ }
+ content.setSpan(OLD_SEL_START, selStart, selStart,
+ Spannable.SPAN_MARK_MARK);
+
+ content.replace(selStart, selEnd, String.valueOf((char) i));
+
+ int oldStart = content.getSpanStart(OLD_SEL_START);
+ selEnd = Selection.getSelectionEnd(content);
+
+ if (oldStart < selEnd) {
+ content.setSpan(TextKeyListener.LAST_TYPED,
+ oldStart, selEnd,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+ if (dead) {
+ Selection.setSelection(content, oldStart, selEnd);
+ content.setSpan(TextKeyListener.ACTIVE, oldStart, selEnd,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+
+ adjustMetaAfterKeypress(content);
+
+ // potentially do autotext replacement if the character
+ // that was typed was an autotext terminator
+
+ if ((pref & TextKeyListener.AUTO_TEXT) != 0 && mAutoText &&
+ (i == ' ' || i == '\t' || i == '\n' ||
+ i == ',' || i == '.' || i == '!' || i == '?' ||
+ i == '"' || i == ')' || i == ']') &&
+ content.getSpanEnd(TextKeyListener.INHIBIT_REPLACEMENT)
+ != oldStart) {
+ int x;
+
+ for (x = oldStart; x > 0; x--) {
+ char c = content.charAt(x - 1);
+ if (c != '\'' && !Character.isLetter(c)) {
+ break;
+ }
+ }
+
+ String rep = getReplacement(content, x, oldStart, view);
+
+ if (rep != null) {
+ Replaced[] repl = content.getSpans(0, content.length(),
+ Replaced.class);
+ for (int a = 0; a < repl.length; a++)
+ content.removeSpan(repl[a]);
+
+ char[] orig = new char[oldStart - x];
+ TextUtils.getChars(content, x, oldStart, orig, 0);
+
+ content.setSpan(new Replaced(orig), x, oldStart,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ content.replace(x, oldStart, rep);
+ }
+ }
+
+ // Replace two spaces by a period and a space.
+
+ if ((pref & TextKeyListener.AUTO_PERIOD) != 0 && mAutoText) {
+ selEnd = Selection.getSelectionEnd(content);
+ if (selEnd - 3 >= 0) {
+ if (content.charAt(selEnd - 1) == ' ' &&
+ content.charAt(selEnd - 2) == ' ') {
+ char c = content.charAt(selEnd - 3);
+
+ if (Character.isLetter(c)) {
+ content.replace(selEnd - 2, selEnd - 1, ".");
+ }
+ }
+ }
+ }
+
+ return true;
+ } else if (keyCode == KeyEvent.KEYCODE_DEL && selStart == selEnd) {
+ // special backspace case for undoing autotext
+
+ int consider = 1;
+
+ // if backspacing over the last typed character,
+ // it undoes the autotext prior to that character
+ // (unless the character typed was newline, in which
+ // case this behavior would be confusing)
+
+ if (content.getSpanEnd(TextKeyListener.LAST_TYPED) == selStart) {
+ if (content.charAt(selStart - 1) != '\n')
+ consider = 2;
+ }
+
+ Replaced[] repl = content.getSpans(selStart - consider, selStart,
+ Replaced.class);
+
+ if (repl.length > 0) {
+ int st = content.getSpanStart(repl[0]);
+ int en = content.getSpanEnd(repl[0]);
+ String old = new String(repl[0].mText);
+
+ content.removeSpan(repl[0]);
+ content.setSpan(TextKeyListener.INHIBIT_REPLACEMENT,
+ en, en, Spannable.SPAN_POINT_POINT);
+ content.replace(st, en, old);
+
+ en = content.getSpanStart(TextKeyListener.INHIBIT_REPLACEMENT);
+ if (en - 1 >= 0) {
+ content.setSpan(TextKeyListener.INHIBIT_REPLACEMENT,
+ en - 1, en,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ } else {
+ content.removeSpan(TextKeyListener.INHIBIT_REPLACEMENT);
+ }
+
+ adjustMetaAfterKeypress(content);
+
+ return true;
+ }
+ }
+
+ return super.onKeyDown(view, content, keyCode, event);
+ }
+
+ private String getReplacement(CharSequence src, int start, int end,
+ View view) {
+ int len = end - start;
+ boolean changecase = false;
+
+ String replacement = AutoText.get(src, start, end, view);
+
+ if (replacement == null) {
+ String key = TextUtils.substring(src, start, end).toLowerCase();
+ replacement = AutoText.get(key, 0, end - start, view);
+ changecase = true;
+
+ if (replacement == null)
+ return null;
+ }
+
+ int caps = 0;
+
+ if (changecase) {
+ for (int j = start; j < end; j++) {
+ if (Character.isUpperCase(src.charAt(j)))
+ caps++;
+ }
+ }
+
+ String out;
+
+ if (caps == 0)
+ out = replacement;
+ else if (caps == 1)
+ out = toTitleCase(replacement);
+ else if (caps == len)
+ out = replacement.toUpperCase();
+ else
+ out = toTitleCase(replacement);
+
+ if (out.length() == len &&
+ TextUtils.regionMatches(src, start, out, 0, len))
+ return null;
+
+ return out;
+ }
+
+ /**
+ * Marks the specified region of <code>content</code> as having
+ * contained <code>original</code> prior to AutoText replacement.
+ * Call this method when you have done or are about to do an
+ * AutoText-style replacement on a region of text and want to let
+ * the same mechanism (the user pressing DEL immediately after the
+ * change) undo the replacement.
+ *
+ * @param content the Editable text where the replacement was made
+ * @param start the start of the replaced region
+ * @param end the end of the replaced region; the location of the cursor
+ * @param original the text to be restored if the user presses DEL
+ */
+ public static void markAsReplaced(Spannable content, int start, int end,
+ String original) {
+ Replaced[] repl = content.getSpans(0, content.length(), Replaced.class);
+ for (int a = 0; a < repl.length; a++) {
+ content.removeSpan(repl[a]);
+ }
+
+ int len = original.length();
+ char[] orig = new char[len];
+ original.getChars(0, len, orig, 0);
+
+ content.setSpan(new Replaced(orig), start, end,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
+ private static SparseArray<String> PICKER_SETS =
+ new SparseArray<String>();
+ static {
+ PICKER_SETS.put('!', "\u00A1");
+ PICKER_SETS.put('<', "\u00AB");
+ PICKER_SETS.put('>', "\u00BB");
+ PICKER_SETS.put('?', "\u00BF");
+ PICKER_SETS.put('A', "\u00C0\u00C1\u00C2\u00C4\u00C6\u00C3\u00C5");
+ PICKER_SETS.put('C', "\u00C7");
+ PICKER_SETS.put('E', "\u00C8\u00C9\u00CA\u00CB");
+ PICKER_SETS.put('I', "\u00CC\u00CD\u00CE\u00CF");
+ PICKER_SETS.put('N', "\u00D1");
+ PICKER_SETS.put('O', "\u00D8\u0152\u00D5\u00D2\u00D3\u00D4\u00D6");
+ PICKER_SETS.put('U', "\u00D9\u00DA\u00DB\u00DC");
+ PICKER_SETS.put('Y', "\u00DD\u0178");
+ PICKER_SETS.put('a', "\u00E0\u00E1\u00E2\u00E4\u00E6\u00E3\u00E5");
+ PICKER_SETS.put('c', "\u00E7");
+ PICKER_SETS.put('e', "\u00E8\u00E9\u00EA\u00EB");
+ PICKER_SETS.put('i', "\u00EC\u00ED\u00EE\u00EF");
+ PICKER_SETS.put('n', "\u00F1");
+ PICKER_SETS.put('o', "\u00F8\u0153\u00F5\u00F2\u00F3\u00F4\u00F6");
+ PICKER_SETS.put('s', "\u00A7\u00DF");
+ PICKER_SETS.put('u', "\u00F9\u00FA\u00FB\u00FC");
+ PICKER_SETS.put('y', "\u00FD\u00FF");
+ PICKER_SETS.put(KeyCharacterMap.PICKER_DIALOG_INPUT,
+ "\u2026\u00A5\u2022\u00AE\u00A9\u00B1");
+ };
+
+ private boolean showCharacterPicker(View view, Editable content, char c,
+ boolean insert, int count) {
+ String set = PICKER_SETS.get(c);
+ if (set == null) {
+ return false;
+ }
+
+ if (count == 1) {
+ new CharacterPickerDialog(view.getContext(),
+ view, content, set, insert).show();
+ }
+
+ return true;
+ }
+
+ private static String toTitleCase(String src) {
+ return Character.toUpperCase(src.charAt(0)) + src.substring(1);
+ }
+
+ /* package */ static class Replaced
+ {
+ public Replaced(char[] text) {
+ mText = text;
+ }
+
+ private char[] mText;
+ }
+
+ private Capitalize mAutoCap;
+ private boolean mAutoText;
+}
+
diff --git a/core/java/android/text/method/ReplacementTransformationMethod.java b/core/java/android/text/method/ReplacementTransformationMethod.java
new file mode 100644
index 0000000..d6f879a
--- /dev/null
+++ b/core/java/android/text/method/ReplacementTransformationMethod.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.method;
+
+import android.graphics.Rect;
+import android.text.Editable;
+import android.text.GetChars;
+import android.text.Spannable;
+import android.text.Spanned;
+import android.text.SpannedString;
+import android.text.TextUtils;
+import android.view.View;
+
+/**
+ * This transformation method causes the characters in the {@link #getOriginal}
+ * array to be replaced by the corresponding characters in the
+ * {@link #getReplacement} array.
+ */
+public abstract class ReplacementTransformationMethod
+implements TransformationMethod
+{
+ /**
+ * Returns the list of characters that are to be replaced by other
+ * characters when displayed.
+ */
+ protected abstract char[] getOriginal();
+ /**
+ * Returns a parallel array of replacement characters for the ones
+ * that are to be replaced.
+ */
+ protected abstract char[] getReplacement();
+
+ /**
+ * Returns a CharSequence that will mirror the contents of the
+ * source CharSequence but with the characters in {@link #getOriginal}
+ * replaced by ones from {@link #getReplacement}.
+ */
+ public CharSequence getTransformation(CharSequence source, View v) {
+ char[] original = getOriginal();
+ char[] replacement = getReplacement();
+
+ /*
+ * Short circuit for faster display if the text will never change.
+ */
+ if (!(source instanceof Editable)) {
+ /*
+ * Check whether the text does not contain any of the
+ * source characters so can be used unchanged.
+ */
+ boolean doNothing = true;
+ int n = original.length;
+ for (int i = 0; i < n; i++) {
+ if (TextUtils.indexOf(source, original[i]) >= 0) {
+ doNothing = false;
+ break;
+ }
+ }
+ if (doNothing) {
+ return source;
+ }
+
+ if (!(source instanceof Spannable)) {
+ /*
+ * The text contains some of the source characters,
+ * but they can be flattened out now instead of
+ * at display time.
+ */
+ if (source instanceof Spanned) {
+ return new SpannedString(new SpannedReplacementCharSequence(
+ (Spanned) source,
+ original, replacement));
+ } else {
+ return new ReplacementCharSequence(source,
+ original,
+ replacement).toString();
+ }
+ }
+ }
+
+ if (source instanceof Spanned) {
+ return new SpannedReplacementCharSequence((Spanned) source,
+ original, replacement);
+ } else {
+ return new ReplacementCharSequence(source, original, replacement);
+ }
+ }
+
+ public void onFocusChanged(View view, CharSequence sourceText,
+ boolean focused, int direction,
+ Rect previouslyFocusedRect) {
+ // This callback isn't used.
+ }
+
+ private static class ReplacementCharSequence
+ implements CharSequence, GetChars {
+ private char[] mOriginal, mReplacement;
+
+ public ReplacementCharSequence(CharSequence source, char[] original,
+ char[] replacement) {
+ mSource = source;
+ mOriginal = original;
+ mReplacement = replacement;
+ }
+
+ public int length() {
+ return mSource.length();
+ }
+
+ public char charAt(int i) {
+ char c = mSource.charAt(i);
+
+ int n = mOriginal.length;
+ for (int j = 0; j < n; j++) {
+ if (c == mOriginal[j]) {
+ c = mReplacement[j];
+ }
+ }
+
+ return c;
+ }
+
+ public CharSequence subSequence(int start, int end) {
+ char[] c = new char[end - start];
+
+ getChars(start, end, c, 0);
+ return new String(c);
+ }
+
+ public String toString() {
+ char[] c = new char[length()];
+
+ getChars(0, length(), c, 0);
+ return new String(c);
+ }
+
+ public void getChars(int start, int end, char[] dest, int off) {
+ TextUtils.getChars(mSource, start, end, dest, off);
+ int offend = end - start + off;
+ int n = mOriginal.length;
+
+ for (int i = off; i < offend; i++) {
+ char c = dest[i];
+
+ for (int j = 0; j < n; j++) {
+ if (c == mOriginal[j]) {
+ dest[i] = mReplacement[j];
+ }
+ }
+ }
+ }
+
+ private CharSequence mSource;
+ }
+
+ private static class SpannedReplacementCharSequence
+ extends ReplacementCharSequence
+ implements Spanned
+ {
+ public SpannedReplacementCharSequence(Spanned source, char[] original,
+ char[] replacement) {
+ super(source, original, replacement);
+ mSpanned = source;
+ }
+
+ public CharSequence subSequence(int start, int end) {
+ return new SpannedString(this).subSequence(start, end);
+ }
+
+ public <T> T[] getSpans(int start, int end, Class<T> type) {
+ return mSpanned.getSpans(start, end, type);
+ }
+
+ public int getSpanStart(Object tag) {
+ return mSpanned.getSpanStart(tag);
+ }
+
+ public int getSpanEnd(Object tag) {
+ return mSpanned.getSpanEnd(tag);
+ }
+
+ public int getSpanFlags(Object tag) {
+ return mSpanned.getSpanFlags(tag);
+ }
+
+ public int nextSpanTransition(int start, int end, Class type) {
+ return mSpanned.nextSpanTransition(start, end, type);
+ }
+
+ private Spanned mSpanned;
+ }
+}
diff --git a/core/java/android/text/method/ScrollingMovementMethod.java b/core/java/android/text/method/ScrollingMovementMethod.java
new file mode 100644
index 0000000..0438e1e
--- /dev/null
+++ b/core/java/android/text/method/ScrollingMovementMethod.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.method;
+
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.text.*;
+import android.widget.TextView;
+import android.view.View;
+
+public class
+ScrollingMovementMethod
+implements MovementMethod
+{
+ /**
+ * Scrolls the text to the left if possible.
+ */
+ protected boolean left(TextView widget, Spannable buffer) {
+ Layout layout = widget.getLayout();
+
+ int scrolly = widget.getScrollY();
+ int scr = widget.getScrollX();
+ int em = Math.round(layout.getPaint().getFontSpacing());
+
+ int padding = widget.getTotalPaddingTop() +
+ widget.getTotalPaddingBottom();
+ int top = layout.getLineForVertical(scrolly);
+ int bottom = layout.getLineForVertical(scrolly + widget.getHeight() -
+ padding);
+ int left = Integer.MAX_VALUE;
+
+ for (int i = top; i <= bottom; i++) {
+ left = (int) Math.min(left, layout.getLineLeft(i));
+ }
+
+ if (scr > left) {
+ int s = Math.max(scr - em, left);
+ widget.scrollTo(s, widget.getScrollY());
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Scrolls the text to the right if possible.
+ */
+ protected boolean right(TextView widget, Spannable buffer) {
+ Layout layout = widget.getLayout();
+
+ int scrolly = widget.getScrollY();
+ int scr = widget.getScrollX();
+ int em = Math.round(layout.getPaint().getFontSpacing());
+
+ int padding = widget.getTotalPaddingTop() +
+ widget.getTotalPaddingBottom();
+ int top = layout.getLineForVertical(scrolly);
+ int bottom = layout.getLineForVertical(scrolly + widget.getHeight() -
+ padding);
+ int right = 0;
+
+ for (int i = top; i <= bottom; i++) {
+ right = (int) Math.max(right, layout.getLineRight(i));
+ }
+
+ padding = widget.getTotalPaddingLeft() + widget.getTotalPaddingRight();
+ if (scr < right - (widget.getWidth() - padding)) {
+ int s = Math.min(scr + em, right - (widget.getWidth() - padding));
+ widget.scrollTo(s, widget.getScrollY());
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Scrolls the text up if possible.
+ */
+ protected boolean up(TextView widget, Spannable buffer) {
+ Layout layout = widget.getLayout();
+
+ int areatop = widget.getScrollY();
+ int line = layout.getLineForVertical(areatop);
+ int linetop = layout.getLineTop(line);
+
+ // If the top line is partially visible, bring it all the way
+ // into view; otherwise, bring the previous line into view.
+ if (areatop == linetop)
+ line--;
+
+ if (line >= 0) {
+ Touch.scrollTo(widget, layout,
+ widget.getScrollX(), layout.getLineTop(line));
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Scrolls the text down if possible.
+ */
+ protected boolean down(TextView widget, Spannable buffer) {
+ Layout layout = widget.getLayout();
+
+ int padding = widget.getTotalPaddingTop() +
+ widget.getTotalPaddingBottom();
+
+ int areabot = widget.getScrollY() + widget.getHeight() - padding;
+ int line = layout.getLineForVertical(areabot);
+
+ if (layout.getLineTop(line+1) < areabot + 1) {
+ // Less than a pixel of this line is out of view,
+ // so we must have tried to make it entirely in view
+ // and now want the next line to be in view instead.
+
+ line++;
+ }
+
+ if (line <= layout.getLineCount() - 1) {
+ widget.scrollTo(widget.getScrollX(), layout.getLineTop(line+1) -
+ (widget.getHeight() - padding));
+ Touch.scrollTo(widget, layout,
+ widget.getScrollX(), widget.getScrollY());
+ return true;
+ }
+
+ return false;
+ }
+
+ public boolean onKeyDown(TextView widget, Spannable buffer, int keyCode, KeyEvent event) {
+ boolean handled = false;
+
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ handled |= left(widget, buffer);
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ handled |= right(widget, buffer);
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_UP:
+ handled |= up(widget, buffer);
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ handled |= down(widget, buffer);
+ break;
+ }
+
+ return handled;
+ }
+
+ public boolean onKeyUp(TextView widget, Spannable buffer, int keyCode, KeyEvent event) {
+ return false;
+ }
+
+ public boolean onTouchEvent(TextView widget, Spannable buffer,
+ MotionEvent event) {
+ return Touch.onTouchEvent(widget, buffer, event);
+ }
+
+ public boolean onTrackballEvent(TextView widget, Spannable buffer,
+ MotionEvent event) {
+ boolean handled = false;
+ int x = (int) event.getX();
+ int y = (int) event.getY();
+
+ for (; y < 0; y++) {
+ handled |= up(widget, buffer);
+ }
+ for (; y > 0; y--) {
+ handled |= down(widget, buffer);
+ }
+
+ for (; x < 0; x++) {
+ handled |= left(widget, buffer);
+ }
+ for (; x > 0; x--) {
+ handled |= right(widget, buffer);
+ }
+
+ return handled;
+ }
+
+ public void initialize(TextView widget, Spannable text) { }
+
+ public boolean canSelectArbitrarily() {
+ return false;
+ }
+
+ public void onTakeFocus(TextView widget, Spannable text, int dir) {
+ Layout layout = widget.getLayout();
+
+ if (layout != null && (dir & View.FOCUS_FORWARD) != 0) {
+ widget.scrollTo(widget.getScrollX(),
+ layout.getLineTop(0));
+ }
+ if (layout != null && (dir & View.FOCUS_BACKWARD) != 0) {
+ int padding = widget.getTotalPaddingTop() +
+ widget.getTotalPaddingBottom();
+ int line = layout.getLineCount() - 1;
+
+ widget.scrollTo(widget.getScrollX(),
+ layout.getLineTop(line+1) -
+ (widget.getHeight() - padding));
+ }
+ }
+
+ public static MovementMethod getInstance() {
+ if (sInstance == null)
+ sInstance = new ScrollingMovementMethod();
+
+ return sInstance;
+ }
+
+ private static ScrollingMovementMethod sInstance;
+}
diff --git a/core/java/android/text/method/SingleLineTransformationMethod.java b/core/java/android/text/method/SingleLineTransformationMethod.java
new file mode 100644
index 0000000..a4fcf15
--- /dev/null
+++ b/core/java/android/text/method/SingleLineTransformationMethod.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.method;
+
+import android.graphics.Rect;
+import android.text.Editable;
+import android.text.GetChars;
+import android.text.Spannable;
+import android.text.Spanned;
+import android.text.SpannedString;
+import android.text.TextUtils;
+import android.view.View;
+
+/**
+ * This transformation method causes any newline characters (\n) to be
+ * displayed as spaces instead of causing line breaks.
+ */
+public class SingleLineTransformationMethod
+extends ReplacementTransformationMethod {
+ private static char[] ORIGINAL = new char[] { '\n' };
+ private static char[] REPLACEMENT = new char[] { ' ' };
+
+ /**
+ * The character to be replaced is \n.
+ */
+ protected char[] getOriginal() {
+ return ORIGINAL;
+ }
+
+ /**
+ * The character \n is replaced with is space.
+ */
+ protected char[] getReplacement() {
+ return REPLACEMENT;
+ }
+
+ public static SingleLineTransformationMethod getInstance() {
+ if (sInstance != null)
+ return sInstance;
+
+ sInstance = new SingleLineTransformationMethod();
+ return sInstance;
+ }
+
+ private static SingleLineTransformationMethod sInstance;
+}
diff --git a/core/java/android/text/method/TextKeyListener.java b/core/java/android/text/method/TextKeyListener.java
new file mode 100644
index 0000000..012e41d
--- /dev/null
+++ b/core/java/android/text/method/TextKeyListener.java
@@ -0,0 +1,339 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.method;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.os.Handler;
+import android.provider.Settings;
+import android.provider.Settings.System;
+import android.text.*;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.view.View;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * This is the key listener for typing normal text. It delegates to
+ * other key listeners appropriate to the current keyboard and language.
+ */
+public class TextKeyListener extends BaseKeyListener implements SpanWatcher {
+ private static TextKeyListener[] sInstance =
+ new TextKeyListener[Capitalize.values().length * 2];
+
+ /* package */ static final Object ACTIVE = new Object();
+ /* package */ static final Object CAPPED = new Object();
+ /* package */ static final Object INHIBIT_REPLACEMENT = new Object();
+ /* package */ static final Object LAST_TYPED = new Object();
+
+ private Capitalize mAutoCap;
+ private boolean mAutoText;
+
+ private int mPrefs;
+ private boolean mPrefsInited;
+
+ /* package */ static final int AUTO_CAP = 1;
+ /* package */ static final int AUTO_TEXT = 2;
+ /* package */ static final int AUTO_PERIOD = 4;
+ /* package */ static final int SHOW_PASSWORD = 8;
+ private WeakReference<ContentResolver> mResolver;
+ private TextKeyListener.SettingsObserver mObserver;
+
+ /**
+ * Creates a new TextKeyListener with the specified capitalization
+ * and correction properties.
+ *
+ * @param cap when, if ever, to automatically capitalize.
+ * @param autotext whether to automatically do spelling corrections.
+ */
+ public TextKeyListener(Capitalize cap, boolean autotext) {
+ mAutoCap = cap;
+ mAutoText = autotext;
+ }
+
+ /**
+ * Returns a new or existing instance with the specified capitalization
+ * and correction properties.
+ *
+ * @param cap when, if ever, to automatically capitalize.
+ * @param autotext whether to automatically do spelling corrections.
+ */
+ public static TextKeyListener getInstance(boolean autotext,
+ Capitalize cap) {
+ int off = cap.ordinal() * 2 + (autotext ? 1 : 0);
+
+ if (sInstance[off] == null) {
+ sInstance[off] = new TextKeyListener(cap, autotext);
+ }
+
+ return sInstance[off];
+ }
+
+ /**
+ * Returns a new or existing instance with no automatic capitalization
+ * or correction.
+ */
+ public static TextKeyListener getInstance() {
+ return getInstance(false, Capitalize.NONE);
+ }
+
+ /**
+ * Returns whether it makes sense to automatically capitalize at the
+ * specified position in the specified text, with the specified rules.
+ *
+ * @param cap the capitalization rules to consider.
+ * @param cs the text in which an insertion is being made.
+ * @param off the offset into that text where the insertion is being made.
+ *
+ * @return whether the character being inserted should be capitalized.
+ */
+ public static boolean shouldCap(Capitalize cap, CharSequence cs, int off) {
+ int i;
+ char c;
+
+ if (cap == Capitalize.NONE) {
+ return false;
+ }
+ if (cap == Capitalize.CHARACTERS) {
+ return true;
+ }
+
+ // Back over allowed opening punctuation.
+
+ for (i = off; i > 0; i--) {
+ c = cs.charAt(i - 1);
+
+ if (c != '"' && c != '(' && c != '[' && c != '\'') {
+ break;
+ }
+ }
+
+ // Start of paragraph, with optional whitespace.
+
+ int j = i;
+ while (j > 0 && ((c = cs.charAt(j - 1)) == ' ' || c == '\t')) {
+ j--;
+ }
+ if (j == 0 || cs.charAt(j - 1) == '\n') {
+ return true;
+ }
+
+ // Or start of word if we are that style.
+
+ if (cap == Capitalize.WORDS) {
+ return i != j;
+ }
+
+ // There must be a space if not the start of paragraph.
+
+ if (i == j) {
+ return false;
+ }
+
+ // Back over allowed closing punctuation.
+
+ for (; j > 0; j--) {
+ c = cs.charAt(j - 1);
+
+ if (c != '"' && c != ')' && c != ']' && c != '\'') {
+ break;
+ }
+ }
+
+ if (j > 0) {
+ c = cs.charAt(j - 1);
+
+ if (c == '.' || c == '?' || c == '!') {
+ // Do not capitalize if the word ends with a period but
+ // also contains a period, in which case it is an abbreviation.
+
+ if (c == '.') {
+ for (int k = j - 2; k >= 0; k--) {
+ c = cs.charAt(k);
+
+ if (c == '.') {
+ return false;
+ }
+
+ if (!Character.isLetter(c)) {
+ break;
+ }
+ }
+ }
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean onKeyDown(View view, Editable content,
+ int keyCode, KeyEvent event) {
+ KeyListener im = getKeyListener(event);
+
+ return im.onKeyDown(view, content, keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyUp(View view, Editable content,
+ int keyCode, KeyEvent event) {
+ KeyListener im = getKeyListener(event);
+
+ return im.onKeyUp(view, content, keyCode, event);
+ }
+
+ /**
+ * Clear all the input state (autotext, autocap, multitap, undo)
+ * from the specified Editable, going beyond Editable.clear(), which
+ * just clears the text but not the input state.
+ *
+ * @param e the buffer whose text and state are to be cleared.
+ */
+ public static void clear(Editable e) {
+ e.clear();
+ e.removeSpan(ACTIVE);
+ e.removeSpan(CAPPED);
+ e.removeSpan(INHIBIT_REPLACEMENT);
+ e.removeSpan(LAST_TYPED);
+
+ QwertyKeyListener.Replaced[] repl = e.getSpans(0, e.length(),
+ QwertyKeyListener.Replaced.class);
+ final int count = repl.length;
+ for (int i = 0; i < count; i++) {
+ e.removeSpan(repl[i]);
+ }
+ }
+
+ public void onSpanAdded(Spannable s, Object what, int start, int end) { }
+ public void onSpanRemoved(Spannable s, Object what, int start, int end) { }
+
+ public void onSpanChanged(Spannable s, Object what, int start, int end,
+ int st, int en) {
+ if (what == Selection.SELECTION_END) {
+ s.removeSpan(ACTIVE);
+ }
+ }
+
+ private KeyListener getKeyListener(KeyEvent event) {
+ KeyCharacterMap kmap = KeyCharacterMap.load(event.getKeyboardDevice());
+ int kind = kmap.getKeyboardType();
+
+ if (kind == KeyCharacterMap.ALPHA) {
+ return QwertyKeyListener.getInstance(mAutoText, mAutoCap);
+ } else if (kind == KeyCharacterMap.NUMERIC) {
+ return MultiTapKeyListener.getInstance(mAutoText, mAutoCap);
+ }
+
+ return NullKeyListener.getInstance();
+ }
+
+ public enum Capitalize {
+ NONE, SENTENCES, WORDS, CHARACTERS,
+ }
+
+ private static class NullKeyListener implements KeyListener
+ {
+ public boolean onKeyDown(View view, Editable content,
+ int keyCode, KeyEvent event) {
+ return false;
+ }
+
+ public boolean onKeyUp(View view, Editable content, int keyCode,
+ KeyEvent event) {
+ return false;
+ }
+
+ public static NullKeyListener getInstance() {
+ if (sInstance != null)
+ return sInstance;
+
+ sInstance = new NullKeyListener();
+ return sInstance;
+ }
+
+ private static NullKeyListener sInstance;
+ }
+
+ public void release() {
+ if (mResolver != null) {
+ final ContentResolver contentResolver = mResolver.get();
+ if (contentResolver != null) {
+ contentResolver.unregisterContentObserver(mObserver);
+ mResolver.clear();
+ }
+ mObserver = null;
+ mResolver = null;
+ mPrefsInited = false;
+ }
+ }
+
+ private void initPrefs(Context context) {
+ final ContentResolver contentResolver = context.getContentResolver();
+ mResolver = new WeakReference<ContentResolver>(contentResolver);
+ mObserver = new SettingsObserver();
+ contentResolver.registerContentObserver(Settings.System.CONTENT_URI, true, mObserver);
+
+ updatePrefs(contentResolver);
+ mPrefsInited = true;
+ }
+
+ private class SettingsObserver extends ContentObserver {
+ public SettingsObserver() {
+ super(new Handler());
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ if (mResolver != null) {
+ final ContentResolver contentResolver = mResolver.get();
+ if (contentResolver == null) {
+ mPrefsInited = false;
+ } else {
+ updatePrefs(contentResolver);
+ }
+ } else {
+ mPrefsInited = false;
+ }
+ }
+ }
+
+ private void updatePrefs(ContentResolver resolver) {
+ boolean cap = System.getInt(resolver, System.TEXT_AUTO_CAPS, 1) > 0;
+ boolean text = System.getInt(resolver, System.TEXT_AUTO_REPLACE, 1) > 0;
+ boolean period = System.getInt(resolver, System.TEXT_AUTO_PUNCTUATE, 1) > 0;
+ boolean pw = System.getInt(resolver, System.TEXT_SHOW_PASSWORD, 1) > 0;
+
+ mPrefs = (cap ? AUTO_CAP : 0) |
+ (text ? AUTO_TEXT : 0) |
+ (period ? AUTO_PERIOD : 0) |
+ (pw ? SHOW_PASSWORD : 0);
+ }
+
+ /* package */ int getPrefs(Context context) {
+ synchronized (this) {
+ if (!mPrefsInited || mResolver.get() == null) {
+ initPrefs(context);
+ }
+ }
+
+ return mPrefs;
+ }
+}
diff --git a/core/java/android/text/method/TimeKeyListener.java b/core/java/android/text/method/TimeKeyListener.java
new file mode 100644
index 0000000..9ba1fe6
--- /dev/null
+++ b/core/java/android/text/method/TimeKeyListener.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.method;
+
+import android.view.KeyEvent;
+
+/**
+ * For entering times in a text field.
+ */
+public class TimeKeyListener extends NumberKeyListener
+{
+ @Override
+ protected char[] getAcceptedChars()
+ {
+ return CHARACTERS;
+ }
+
+ public static TimeKeyListener getInstance() {
+ if (sInstance != null)
+ return sInstance;
+
+ sInstance = new TimeKeyListener();
+ return sInstance;
+ }
+
+ /**
+ * The characters that are used.
+ *
+ * @see KeyEvent#getMatch
+ * @see #getAcceptedChars
+ */
+ public static final char[] CHARACTERS = new char[] {
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'm',
+ 'p', ':'
+ };
+
+ private static TimeKeyListener sInstance;
+}
diff --git a/core/java/android/text/method/Touch.java b/core/java/android/text/method/Touch.java
new file mode 100644
index 0000000..bd01728
--- /dev/null
+++ b/core/java/android/text/method/Touch.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.method;
+
+import android.text.Layout;
+import android.text.Spannable;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.widget.TextView;
+
+public class Touch {
+ private Touch() { }
+
+ /**
+ * Scrolls the specified widget to the specified coordinates, except
+ * constrains the X scrolling position to the horizontal regions of
+ * the text that will be visible after scrolling to the specified
+ * Y position.
+ */
+ public static void scrollTo(TextView widget, Layout layout, int x, int y) {
+ int padding = widget.getTotalPaddingTop() +
+ widget.getTotalPaddingBottom();
+ int top = layout.getLineForVertical(y);
+ int bottom = layout.getLineForVertical(y + widget.getHeight() -
+ padding);
+
+ int left = Integer.MAX_VALUE;
+ int right = 0;
+
+ for (int i = top; i <= bottom; i++) {
+ left = (int) Math.min(left, layout.getLineLeft(i));
+ right = (int) Math.max(right, layout.getLineRight(i));
+ }
+
+ padding = widget.getTotalPaddingLeft() + widget.getTotalPaddingRight();
+ x = Math.min(x, right - (widget.getWidth() - padding));
+ x = Math.max(x, left);
+
+ widget.scrollTo(x, y);
+ }
+
+ /**
+ * Handles touch events for dragging. You may want to do other actions
+ * like moving the cursor on touch as well.
+ */
+ public static boolean onTouchEvent(TextView widget, Spannable buffer,
+ MotionEvent event) {
+ DragState[] ds;
+
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ buffer.setSpan(new DragState(event.getX(), event.getY()),
+ 0, 0, Spannable.SPAN_MARK_MARK);
+ return true;
+
+ case MotionEvent.ACTION_UP:
+ ds = buffer.getSpans(0, buffer.length(), DragState.class);
+
+ for (int i = 0; i < ds.length; i++) {
+ buffer.removeSpan(ds[i]);
+ }
+
+ if (ds.length > 0 && ds[0].mUsed) {
+ return true;
+ } else {
+ return false;
+ }
+
+ case MotionEvent.ACTION_MOVE:
+ ds = buffer.getSpans(0, buffer.length(), DragState.class);
+
+ if (ds.length > 0) {
+ if (ds[0].mFarEnough == false) {
+ int slop = ViewConfiguration.getTouchSlop();
+
+ if (Math.abs(event.getX() - ds[0].mX) >= slop ||
+ Math.abs(event.getY() - ds[0].mY) >= slop) {
+ ds[0].mFarEnough = true;
+ }
+ }
+
+ if (ds[0].mFarEnough) {
+ ds[0].mUsed = true;
+
+ float dx = ds[0].mX - event.getX();
+ float dy = ds[0].mY - event.getY();
+
+ ds[0].mX = event.getX();
+ ds[0].mY = event.getY();
+
+ int nx = widget.getScrollX() + (int) dx;
+ int ny = widget.getScrollY() + (int) dy;
+
+ int padding = widget.getTotalPaddingTop() +
+ widget.getTotalPaddingBottom();
+ Layout layout = widget.getLayout();
+
+ ny = Math.min(ny, layout.getHeight() - (widget.getHeight() -
+ padding));
+ ny = Math.max(ny, 0);
+
+ scrollTo(widget, layout, nx, ny);
+ widget.cancelLongPress();
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ private static class DragState {
+ public float mX;
+ public float mY;
+ public boolean mFarEnough;
+ public boolean mUsed;
+
+ public DragState(float x, float y) {
+ mX = x;
+ mY = y;
+ }
+ }
+}
diff --git a/core/java/android/text/method/TransformationMethod.java b/core/java/android/text/method/TransformationMethod.java
new file mode 100644
index 0000000..9f51c2a
--- /dev/null
+++ b/core/java/android/text/method/TransformationMethod.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.method;
+
+import android.graphics.Rect;
+import android.view.View;
+import android.widget.TextView;
+
+/**
+ * TextView uses TransformationMethods to do things like replacing the
+ * characters of passwords with dots, or keeping the newline characters
+ * from causing line breaks in single-line text fields.
+ */
+public interface TransformationMethod
+{
+ /**
+ * Returns a CharSequence that is a transformation of the source text --
+ * for example, replacing each character with a dot in a password field.
+ * Beware that the returned text must be exactly the same length as
+ * the source text, and that if the source text is Editable, the returned
+ * text must mirror it dynamically instead of doing a one-time copy.
+ */
+ public CharSequence getTransformation(CharSequence source, View view);
+
+ /**
+ * This method is called when the TextView that uses this
+ * TransformationMethod gains or loses focus.
+ */
+ public void onFocusChanged(View view, CharSequence sourceText,
+ boolean focused, int direction,
+ Rect previouslyFocusedRect);
+}
diff --git a/core/java/android/text/method/package.html b/core/java/android/text/method/package.html
new file mode 100644
index 0000000..93698b8
--- /dev/null
+++ b/core/java/android/text/method/package.html
@@ -0,0 +1,21 @@
+<html>
+<body>
+
+<p>Provides classes that monitor or modify keypad input.</p>
+<p>You can use these classes to modify the type of keypad entry
+for your application, or decipher the keypresses entered for your specific
+entry method. For example:</p>
+<pre>
+// Set the text to password display style:
+EditText txtView = (EditText)findViewById(R.id.text);
+txtView.setTransformationMethod(PasswordTransformationMethod.getInstance());
+
+//Set the input style to numbers, rather than qwerty keyboard style.
+txtView.setInputMethod(DigitsInputMethod.getInstance());
+
+// Find out whether the caps lock is on.
+// 0 is no, 1 is yes, 2 is caps lock on.
+int active = MultiTapInputMethod.getCapsActive(txtView.getText());
+</pre>
+</body>
+</html>
diff --git a/core/java/android/text/package.html b/core/java/android/text/package.html
new file mode 100644
index 0000000..162dcd8
--- /dev/null
+++ b/core/java/android/text/package.html
@@ -0,0 +1,13 @@
+<html>
+<body>
+
+<p>Provides classes used to render or track text and text spans on the screen.</p>
+<p>You can use these classes to design your own widgets that manage text,
+to handle arbitrary text spans for changes, or to handle drawing yourself
+for an existing widget.</p>
+<p>The Span&hellip; interfaces and classes are used to create or manage spans of
+text in a View item. You can use these to style the text or background, or to
+listen for changes. If creating your own widget, extend DynamicLayout, to manages
+the actual wrapping and drawing of your text.
+</body>
+</html>
diff --git a/core/java/android/text/style/AbsoluteSizeSpan.java b/core/java/android/text/style/AbsoluteSizeSpan.java
new file mode 100644
index 0000000..8f6ed5a
--- /dev/null
+++ b/core/java/android/text/style/AbsoluteSizeSpan.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.style;
+
+import android.graphics.Paint;
+import android.text.TextPaint;
+
+public class AbsoluteSizeSpan extends MetricAffectingSpan {
+
+ private int mSize;
+
+ public AbsoluteSizeSpan(int size) {
+ mSize = size;
+ }
+
+ public int getSize() {
+ return mSize;
+ }
+
+ @Override
+ public void updateDrawState(TextPaint ds) {
+ ds.setTextSize(mSize);
+ }
+
+ @Override
+ public void updateMeasureState(TextPaint ds) {
+ ds.setTextSize(mSize);
+ }
+}
diff --git a/core/java/android/text/style/AlignmentSpan.java b/core/java/android/text/style/AlignmentSpan.java
new file mode 100644
index 0000000..d51edcc
--- /dev/null
+++ b/core/java/android/text/style/AlignmentSpan.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.style;
+
+import android.text.Layout;
+
+public interface AlignmentSpan
+extends ParagraphStyle
+{
+ public Layout.Alignment getAlignment();
+
+ public static class Standard
+ implements AlignmentSpan
+ {
+ public Standard(Layout.Alignment align) {
+ mAlignment = align;
+ }
+
+ public Layout.Alignment getAlignment() {
+ return mAlignment;
+ }
+
+ private Layout.Alignment mAlignment;
+ }
+}
diff --git a/core/java/android/text/style/BackgroundColorSpan.java b/core/java/android/text/style/BackgroundColorSpan.java
new file mode 100644
index 0000000..be6ef77
--- /dev/null
+++ b/core/java/android/text/style/BackgroundColorSpan.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.style;
+
+import android.text.TextPaint;
+
+public class BackgroundColorSpan extends CharacterStyle {
+
+ private int mColor;
+
+ public BackgroundColorSpan(int color) {
+ mColor = color;
+ }
+
+ public int getBackgroundColor() {
+ return mColor;
+ }
+
+ @Override
+ public void updateDrawState(TextPaint ds) {
+ ds.bgColor = mColor;
+ }
+}
diff --git a/core/java/android/text/style/BulletSpan.java b/core/java/android/text/style/BulletSpan.java
new file mode 100644
index 0000000..70c4d33
--- /dev/null
+++ b/core/java/android/text/style/BulletSpan.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.style;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.text.Layout;
+import android.text.Spanned;
+
+public class BulletSpan implements LeadingMarginSpan {
+
+ public BulletSpan() {
+ mGapWidth = STANDARD_GAP_WIDTH;
+ }
+
+ public BulletSpan(int gapWidth) {
+ mGapWidth = gapWidth;
+ }
+
+ public BulletSpan(int gapWidth, int color) {
+ mGapWidth = gapWidth;
+ mWantColor = true;
+ mColor = color;
+ }
+
+ public int getLeadingMargin(boolean first) {
+ return 2 * BULLET_RADIUS + mGapWidth;
+ }
+
+ public void drawLeadingMargin(Canvas c, Paint p, int x, int dir,
+ int top, int baseline, int bottom,
+ CharSequence text, int start, int end,
+ boolean first, Layout l) {
+ if (((Spanned) text).getSpanStart(this) == start) {
+ Paint.Style style = p.getStyle();
+ int oldcolor = 0;
+
+ if (mWantColor) {
+ oldcolor = p.getColor();
+ p.setColor(mColor);
+ }
+
+ p.setStyle(Paint.Style.FILL);
+
+ c.drawCircle(x + dir * BULLET_RADIUS, (top + bottom) / 2.0f,
+ BULLET_RADIUS, p);
+
+ if (mWantColor) {
+ p.setColor(oldcolor);
+ }
+
+ p.setStyle(style);
+ }
+ }
+
+ private int mGapWidth;
+ private boolean mWantColor;
+ private int mColor;
+
+ private static final int BULLET_RADIUS = 3;
+ public static final int STANDARD_GAP_WIDTH = 2;
+}
diff --git a/core/java/android/text/style/CharacterStyle.java b/core/java/android/text/style/CharacterStyle.java
new file mode 100644
index 0000000..7620d29
--- /dev/null
+++ b/core/java/android/text/style/CharacterStyle.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.style;
+
+import android.graphics.Paint;
+import android.text.TextPaint;
+
+/**
+ * The classes that affect character-level text formatting extend this
+ * class. Most also extend {@link MetricAffectingSpan}.
+ */
+public abstract class CharacterStyle {
+ public abstract void updateDrawState(TextPaint tp);
+
+ /**
+ * A given CharacterStyle can only applied to a single region of a given
+ * Spanned. If you need to attach the same CharacterStyle to multiple
+ * regions, you can use this method to wrap it with a new object that
+ * will have the same effect but be a distinct object so that it can
+ * also be attached without conflict.
+ */
+ public static CharacterStyle wrap(CharacterStyle cs) {
+ if (cs instanceof MetricAffectingSpan) {
+ return new MetricAffectingSpan.Passthrough((MetricAffectingSpan) cs);
+ } else {
+ return new Passthrough(cs);
+ }
+ }
+
+ /**
+ * Returns "this" for most CharacterStyles, but for CharacterStyles
+ * that were generated by {@link #wrap}, returns the underlying
+ * CharacterStyle.
+ */
+ public CharacterStyle getUnderlying() {
+ return this;
+ }
+
+ /**
+ * A Passthrough CharacterStyle is one that
+ * passes {@link #updateDrawState} calls through to the
+ * specified CharacterStyle while still being a distinct object,
+ * and is therefore able to be attached to the same Spannable
+ * to which the specified CharacterStyle is already attached.
+ */
+ private static class Passthrough extends CharacterStyle {
+ private CharacterStyle mStyle;
+
+ /**
+ * Creates a new Passthrough of the specfied CharacterStyle.
+ */
+ public Passthrough(CharacterStyle cs) {
+ mStyle = cs;
+ }
+
+ /**
+ * Passes updateDrawState through to the underlying CharacterStyle.
+ */
+ @Override
+ public void updateDrawState(TextPaint tp) {
+ mStyle.updateDrawState(tp);
+ }
+
+ /**
+ * Returns the CharacterStyle underlying this one, or the one
+ * underlying it if it too is a Passthrough.
+ */
+ @Override
+ public CharacterStyle getUnderlying() {
+ return mStyle.getUnderlying();
+ }
+ }
+}
diff --git a/core/java/android/text/style/ClickableSpan.java b/core/java/android/text/style/ClickableSpan.java
new file mode 100644
index 0000000..a232ed7
--- /dev/null
+++ b/core/java/android/text/style/ClickableSpan.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.style;
+
+import android.text.TextPaint;
+import android.view.View;
+
+/**
+ * If an object of this type is attached to the text of a TextView
+ * with a movement method of LinkMovementMethod, the affected spans of
+ * text can be selected. If clicked, the {@link #onClick} method will
+ * be called.
+ */
+public abstract class ClickableSpan extends CharacterStyle {
+
+ /**
+ * Performs the click action associated with this span.
+ */
+ public abstract void onClick(View widget);
+
+ /**
+ * Makes the text underlined and in the link color.
+ */
+ @Override
+ public void updateDrawState(TextPaint ds) {
+ ds.setColor(ds.linkColor);
+ ds.setUnderlineText(true);
+ }
+}
diff --git a/core/java/android/text/style/DrawableMarginSpan.java b/core/java/android/text/style/DrawableMarginSpan.java
new file mode 100644
index 0000000..3c471a5
--- /dev/null
+++ b/core/java/android/text/style/DrawableMarginSpan.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.style;
+
+import android.graphics.drawable.Drawable;
+import android.graphics.Paint;
+import android.graphics.Canvas;
+import android.graphics.RectF;
+import android.text.Spanned;
+import android.text.Layout;
+
+public class DrawableMarginSpan
+implements LeadingMarginSpan, LineHeightSpan
+{
+ public DrawableMarginSpan(Drawable b) {
+ mDrawable = b;
+ }
+
+ public DrawableMarginSpan(Drawable b, int pad) {
+ mDrawable = b;
+ mPad = pad;
+ }
+
+ public int getLeadingMargin(boolean first) {
+ return mDrawable.getIntrinsicWidth() + mPad;
+ }
+
+ public void drawLeadingMargin(Canvas c, Paint p, int x, int dir,
+ int top, int baseline, int bottom,
+ CharSequence text, int start, int end,
+ boolean first, Layout layout) {
+ int st = ((Spanned) text).getSpanStart(this);
+ int ix = (int)x;
+ int itop = (int)layout.getLineTop(layout.getLineForOffset(st));
+
+ int dw = mDrawable.getIntrinsicWidth();
+ int dh = mDrawable.getIntrinsicHeight();
+
+ if (dir < 0)
+ x -= dw;
+
+ // XXX What to do about Paint?
+ mDrawable.setBounds(ix, itop, ix+dw, itop+dh);
+ mDrawable.draw(c);
+ }
+
+ public void chooseHeight(CharSequence text, int start, int end,
+ int istartv, int v,
+ Paint.FontMetricsInt fm) {
+ if (end == ((Spanned) text).getSpanEnd(this)) {
+ int ht = mDrawable.getIntrinsicHeight();
+
+ int need = ht - (v + fm.descent - fm.ascent - istartv);
+ if (need > 0)
+ fm.descent += need;
+
+ need = ht - (v + fm.bottom - fm.top - istartv);
+ if (need > 0)
+ fm.bottom += need;
+ }
+ }
+
+ private Drawable mDrawable;
+ private int mPad;
+}
diff --git a/core/java/android/text/style/DynamicDrawableSpan.java b/core/java/android/text/style/DynamicDrawableSpan.java
new file mode 100644
index 0000000..3bcc335
--- /dev/null
+++ b/core/java/android/text/style/DynamicDrawableSpan.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.style;
+
+import java.lang.ref.WeakReference;
+
+import android.graphics.drawable.Drawable;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+
+/**
+ *
+ */
+public abstract class DynamicDrawableSpan
+extends ReplacementSpan
+{
+ /**
+ * Your subclass must implement this method to provide the bitmap
+ * to be drawn. The dimensions of the bitmap must be the same
+ * from each call to the next.
+ */
+ public abstract Drawable getDrawable();
+
+ public int getSize(Paint paint, CharSequence text,
+ int start, int end,
+ Paint.FontMetricsInt fm) {
+ Drawable b = getCachedDrawable();
+
+ if (fm != null) {
+ fm.ascent = -b.getIntrinsicHeight();
+ fm.descent = 0;
+
+ fm.top = fm.ascent;
+ fm.bottom = 0;
+ }
+
+ return b.getIntrinsicWidth();
+ }
+
+ public void draw(Canvas canvas, CharSequence text,
+ int start, int end, float x,
+ int top, int y, int bottom, Paint paint) {
+ Drawable b = getCachedDrawable();
+ canvas.save();
+
+ canvas.translate(x, bottom-b.getIntrinsicHeight());;
+ b.draw(canvas);
+ canvas.restore();
+ }
+
+ private Drawable getCachedDrawable() {
+ WeakReference wr = mDrawableRef;
+ Drawable b = null;
+
+ if (wr != null)
+ b = (Drawable) wr.get();
+
+ if (b == null) {
+ b = getDrawable();
+ mDrawableRef = new WeakReference(b);
+ }
+
+ return b;
+ }
+
+ private WeakReference mDrawableRef;
+}
+
diff --git a/core/java/android/text/style/ForegroundColorSpan.java b/core/java/android/text/style/ForegroundColorSpan.java
new file mode 100644
index 0000000..5cccd9c
--- /dev/null
+++ b/core/java/android/text/style/ForegroundColorSpan.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.style;
+
+import android.graphics.Paint;
+import android.text.TextPaint;
+
+public class ForegroundColorSpan extends CharacterStyle {
+
+ private int mColor;
+
+ public ForegroundColorSpan(int color) {
+ mColor = color;
+ }
+
+ public int getForegroundColor() {
+ return mColor;
+ }
+
+ @Override
+ public void updateDrawState(TextPaint ds) {
+ ds.setColor(mColor);
+ }
+}
diff --git a/core/java/android/text/style/IconMarginSpan.java b/core/java/android/text/style/IconMarginSpan.java
new file mode 100644
index 0000000..c786a17
--- /dev/null
+++ b/core/java/android/text/style/IconMarginSpan.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.style;
+
+import android.graphics.Paint;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.RectF;
+import android.text.Spanned;
+import android.text.Layout;
+
+public class IconMarginSpan
+implements LeadingMarginSpan, LineHeightSpan
+{
+ public IconMarginSpan(Bitmap b) {
+ mBitmap = b;
+ }
+
+ public IconMarginSpan(Bitmap b, int pad) {
+ mBitmap = b;
+ mPad = pad;
+ }
+
+ public int getLeadingMargin(boolean first) {
+ return mBitmap.getWidth() + mPad;
+ }
+
+ public void drawLeadingMargin(Canvas c, Paint p, int x, int dir,
+ int top, int baseline, int bottom,
+ CharSequence text, int start, int end,
+ boolean first, Layout layout) {
+ int st = ((Spanned) text).getSpanStart(this);
+ int itop = layout.getLineTop(layout.getLineForOffset(st));
+
+ if (dir < 0)
+ x -= mBitmap.getWidth();
+
+ c.drawBitmap(mBitmap, x, itop, p);
+ }
+
+ public void chooseHeight(CharSequence text, int start, int end,
+ int istartv, int v,
+ Paint.FontMetricsInt fm) {
+ if (end == ((Spanned) text).getSpanEnd(this)) {
+ int ht = mBitmap.getHeight();
+
+ int need = ht - (v + fm.descent - fm.ascent - istartv);
+ if (need > 0)
+ fm.descent += need;
+
+ need = ht - (v + fm.bottom - fm.top - istartv);
+ if (need > 0)
+ fm.bottom += need;
+ }
+ }
+
+ private Bitmap mBitmap;
+ private int mPad;
+}
diff --git a/core/java/android/text/style/ImageSpan.java b/core/java/android/text/style/ImageSpan.java
new file mode 100644
index 0000000..de067b1
--- /dev/null
+++ b/core/java/android/text/style/ImageSpan.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.style;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.util.Log;
+
+import java.io.InputStream;
+
+public class ImageSpan extends DynamicDrawableSpan {
+ private Drawable mDrawable;
+ private Uri mContentUri;
+ private int mResourceId;
+ private Context mContext;
+ private String mSource;
+
+
+ public ImageSpan(Bitmap b) {
+ mDrawable = new BitmapDrawable(b);
+ mDrawable.setBounds(0, 0, mDrawable.getIntrinsicWidth(),
+ mDrawable.getIntrinsicHeight());
+ }
+
+ public ImageSpan(Drawable d) {
+ mDrawable = d;
+ }
+
+ public ImageSpan(Drawable d, String source) {
+ mDrawable = d;
+ mSource = source;
+ }
+
+ public ImageSpan(Context context, Uri uri) {
+ mContext = context;
+ mContentUri = uri;
+ }
+
+ public ImageSpan(Context context, int resourceId) {
+ mContext = context;
+ mResourceId = resourceId;
+ }
+
+ @Override
+ public Drawable getDrawable() {
+ Drawable drawable = null;
+
+ if (mDrawable != null) {
+ drawable = mDrawable;
+ } else if (mContentUri != null) {
+ Bitmap bitmap = null;
+ try {
+ InputStream is = mContext.getContentResolver().openInputStream(
+ mContentUri);
+ bitmap = BitmapFactory.decodeStream(is);
+ drawable = new BitmapDrawable(bitmap);
+ is.close();
+ } catch (Exception e) {
+ Log.e("sms", "Failed to loaded content " + mContentUri, e);
+ }
+ } else {
+ try {
+ drawable = mContext.getResources().getDrawable(mResourceId);
+ drawable.setBounds(0, 0, drawable.getIntrinsicWidth(),
+ drawable.getIntrinsicHeight());
+ } catch (Exception e) {
+ Log.e("sms", "Unable to find resource: " + mResourceId);
+ }
+ }
+
+ return drawable;
+ }
+
+ /**
+ * Returns the source string that was saved during construction.
+ */
+ public String getSource() {
+ return mSource;
+ }
+
+}
diff --git a/core/java/android/text/style/LeadingMarginSpan.java b/core/java/android/text/style/LeadingMarginSpan.java
new file mode 100644
index 0000000..85a27dc
--- /dev/null
+++ b/core/java/android/text/style/LeadingMarginSpan.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.style;
+
+import android.graphics.Paint;
+import android.graphics.Canvas;
+import android.text.Layout;
+
+public interface LeadingMarginSpan
+extends ParagraphStyle
+{
+ public int getLeadingMargin(boolean first);
+ public void drawLeadingMargin(Canvas c, Paint p,
+ int x, int dir,
+ int top, int baseline, int bottom,
+ CharSequence text, int start, int end,
+ boolean first, Layout layout);
+
+ public static class Standard
+ implements LeadingMarginSpan
+ {
+ public Standard(int first, int rest) {
+ mFirst = first;
+ mRest = rest;
+ }
+
+ public Standard(int every) {
+ this(every, every);
+ }
+
+ public int getLeadingMargin(boolean first) {
+ return first ? mFirst : mRest;
+ }
+
+ public void drawLeadingMargin(Canvas c, Paint p,
+ int x, int dir,
+ int top, int baseline, int bottom,
+ CharSequence text, int start, int end,
+ boolean first, Layout layout) {
+ ;
+ }
+
+ private int mFirst, mRest;
+ }
+}
diff --git a/core/java/android/text/style/LineBackgroundSpan.java b/core/java/android/text/style/LineBackgroundSpan.java
new file mode 100644
index 0000000..854aeaf
--- /dev/null
+++ b/core/java/android/text/style/LineBackgroundSpan.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.style;
+
+import android.graphics.Paint;
+import android.graphics.Canvas;
+
+public interface LineBackgroundSpan
+extends ParagraphStyle
+{
+ public void drawBackground(Canvas c, Paint p,
+ int left, int right,
+ int top, int baseline, int bottom,
+ CharSequence text, int start, int end,
+ int lnum);
+}
diff --git a/core/java/android/text/style/LineHeightSpan.java b/core/java/android/text/style/LineHeightSpan.java
new file mode 100644
index 0000000..c0ef97c
--- /dev/null
+++ b/core/java/android/text/style/LineHeightSpan.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.style;
+
+import android.graphics.Paint;
+import android.graphics.Canvas;
+import android.text.Layout;
+
+public interface LineHeightSpan
+extends ParagraphStyle, WrapTogetherSpan
+{
+ public void chooseHeight(CharSequence text, int start, int end,
+ int spanstartv, int v,
+ Paint.FontMetricsInt fm);
+}
diff --git a/core/java/android/text/style/MaskFilterSpan.java b/core/java/android/text/style/MaskFilterSpan.java
new file mode 100644
index 0000000..781bcec
--- /dev/null
+++ b/core/java/android/text/style/MaskFilterSpan.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.style;
+
+import android.graphics.Paint;
+import android.graphics.MaskFilter;
+import android.text.TextPaint;
+
+public class MaskFilterSpan extends CharacterStyle {
+
+ private MaskFilter mFilter;
+
+ public MaskFilterSpan(MaskFilter filter) {
+ mFilter = filter;
+ }
+
+ public MaskFilter getMaskFilter() {
+ return mFilter;
+ }
+
+ @Override
+ public void updateDrawState(TextPaint ds) {
+ ds.setMaskFilter(mFilter);
+ }
+}
diff --git a/core/java/android/text/style/MetricAffectingSpan.java b/core/java/android/text/style/MetricAffectingSpan.java
new file mode 100644
index 0000000..92558eb
--- /dev/null
+++ b/core/java/android/text/style/MetricAffectingSpan.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.style;
+
+import android.graphics.Paint;
+import android.text.TextPaint;
+
+/**
+ * The classes that affect character-level text formatting in a way that
+ * changes the width or height of characters extend this class.
+ */
+public abstract class MetricAffectingSpan
+extends CharacterStyle
+implements UpdateLayout {
+
+ public abstract void updateMeasureState(TextPaint p);
+
+ /**
+ * Returns "this" for most MetricAffectingSpans, but for
+ * MetricAffectingSpans that were generated by {@link #wrap},
+ * returns the underlying MetricAffectingSpan.
+ */
+ @Override
+ public MetricAffectingSpan getUnderlying() {
+ return this;
+ }
+
+ /**
+ * A Passthrough MetricAffectingSpan is one that
+ * passes {@link #updateDrawState} and {@link #updateMeasureState}
+ * calls through to the specified MetricAffectingSpan
+ * while still being a distinct object,
+ * and is therefore able to be attached to the same Spannable
+ * to which the specified MetricAffectingSpan is already attached.
+ */
+ /* package */ static class Passthrough extends MetricAffectingSpan {
+ private MetricAffectingSpan mStyle;
+
+ /**
+ * Creates a new Passthrough of the specfied MetricAffectingSpan.
+ */
+ public Passthrough(MetricAffectingSpan cs) {
+ mStyle = cs;
+ }
+
+ /**
+ * Passes updateDrawState through to the underlying MetricAffectingSpan.
+ */
+ @Override
+ public void updateDrawState(TextPaint tp) {
+ mStyle.updateDrawState(tp);
+ }
+
+ /**
+ * Passes updateMeasureState through to the underlying MetricAffectingSpan.
+ */
+ @Override
+ public void updateMeasureState(TextPaint tp) {
+ mStyle.updateMeasureState(tp);
+ }
+
+ /**
+ * Returns the MetricAffectingSpan underlying this one, or the one
+ * underlying it if it too is a Passthrough.
+ */
+ @Override
+ public MetricAffectingSpan getUnderlying() {
+ return mStyle.getUnderlying();
+ }
+ }
+}
diff --git a/core/java/android/text/style/ParagraphStyle.java b/core/java/android/text/style/ParagraphStyle.java
new file mode 100644
index 0000000..423156e
--- /dev/null
+++ b/core/java/android/text/style/ParagraphStyle.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.style;
+
+/**
+ * The classes that affect paragraph-level text formatting implement
+ * this interface.
+ */
+public interface ParagraphStyle
+{
+
+}
diff --git a/core/java/android/text/style/QuoteSpan.java b/core/java/android/text/style/QuoteSpan.java
new file mode 100644
index 0000000..3f4a32f
--- /dev/null
+++ b/core/java/android/text/style/QuoteSpan.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.style;
+
+import android.graphics.Paint;
+import android.graphics.Canvas;
+import android.graphics.RectF;
+import android.text.Layout;
+
+public class QuoteSpan
+implements LeadingMarginSpan
+{
+ private static final int STRIPE_WIDTH = 2;
+ private static final int GAP_WIDTH = 2;
+
+ private int mColor = 0xff0000ff;
+
+ public QuoteSpan() {
+ super();
+ }
+
+ public QuoteSpan(int color) {
+ this();
+ mColor = color;
+ }
+
+ public int getColor() {
+ return mColor;
+ }
+
+ public int getLeadingMargin(boolean first) {
+ return STRIPE_WIDTH + GAP_WIDTH;
+ }
+
+ public void drawLeadingMargin(Canvas c, Paint p, int x, int dir,
+ int top, int baseline, int bottom,
+ CharSequence text, int start, int end,
+ boolean first, Layout layout) {
+ Paint.Style style = p.getStyle();
+ int color = p.getColor();
+
+ p.setStyle(Paint.Style.FILL);
+ p.setColor(mColor);
+
+ c.drawRect(x, top, x + dir * STRIPE_WIDTH, bottom, p);
+
+ p.setStyle(style);
+ p.setColor(color);
+ }
+}
diff --git a/core/java/android/text/style/RasterizerSpan.java b/core/java/android/text/style/RasterizerSpan.java
new file mode 100644
index 0000000..193c700
--- /dev/null
+++ b/core/java/android/text/style/RasterizerSpan.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.style;
+
+import android.graphics.Paint;
+import android.graphics.Rasterizer;
+import android.text.TextPaint;
+
+public class RasterizerSpan extends CharacterStyle {
+
+ private Rasterizer mRasterizer;
+
+ public RasterizerSpan(Rasterizer r) {
+ mRasterizer = r;
+ }
+
+ public Rasterizer getRasterizer() {
+ return mRasterizer;
+ }
+
+ @Override
+ public void updateDrawState(TextPaint ds) {
+ ds.setRasterizer(mRasterizer);
+ }
+}
diff --git a/core/java/android/text/style/RelativeSizeSpan.java b/core/java/android/text/style/RelativeSizeSpan.java
new file mode 100644
index 0000000..a8ad076
--- /dev/null
+++ b/core/java/android/text/style/RelativeSizeSpan.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.style;
+
+import android.graphics.Paint;
+import android.text.TextPaint;
+
+public class RelativeSizeSpan extends MetricAffectingSpan {
+
+ private float mProportion;
+
+ public RelativeSizeSpan(float proportion) {
+ mProportion = proportion;
+ }
+
+ public float getSizeChange() {
+ return mProportion;
+ }
+
+ @Override
+ public void updateDrawState(TextPaint ds) {
+ ds.setTextSize(ds.getTextSize() * mProportion);
+ }
+
+ @Override
+ public void updateMeasureState(TextPaint ds) {
+ ds.setTextSize(ds.getTextSize() * mProportion);
+ }
+}
diff --git a/core/java/android/text/style/ReplacementSpan.java b/core/java/android/text/style/ReplacementSpan.java
new file mode 100644
index 0000000..26c725f
--- /dev/null
+++ b/core/java/android/text/style/ReplacementSpan.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.style;
+
+import android.graphics.Paint;
+import android.graphics.Canvas;
+import android.text.TextPaint;
+
+public abstract class ReplacementSpan extends MetricAffectingSpan {
+
+ public abstract int getSize(Paint paint, CharSequence text,
+ int start, int end,
+ Paint.FontMetricsInt fm);
+ public abstract void draw(Canvas canvas, CharSequence text,
+ int start, int end, float x,
+ int top, int y, int bottom, Paint paint);
+
+ /**
+ * This method does nothing, since ReplacementSpans are measured
+ * explicitly instead of affecting Paint properties.
+ */
+ public void updateMeasureState(TextPaint p) { }
+
+ /**
+ * This method does nothing, since ReplacementSpans are drawn
+ * explicitly instead of affecting Paint properties.
+ */
+ public void updateDrawState(TextPaint ds) { }
+}
diff --git a/core/java/android/text/style/ScaleXSpan.java b/core/java/android/text/style/ScaleXSpan.java
new file mode 100644
index 0000000..ac9e35d
--- /dev/null
+++ b/core/java/android/text/style/ScaleXSpan.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.style;
+
+import android.graphics.Paint;
+import android.text.TextPaint;
+
+public class ScaleXSpan extends MetricAffectingSpan {
+
+ private float mProportion;
+
+ public ScaleXSpan(float proportion) {
+ mProportion = proportion;
+ }
+
+ public float getScaleX() {
+ return mProportion;
+ }
+
+ @Override
+ public void updateDrawState(TextPaint ds) {
+ ds.setTextScaleX(ds.getTextScaleX() * mProportion);
+ }
+
+ @Override
+ public void updateMeasureState(TextPaint ds) {
+ ds.setTextScaleX(ds.getTextScaleX() * mProportion);
+ }
+}
diff --git a/core/java/android/text/style/StrikethroughSpan.java b/core/java/android/text/style/StrikethroughSpan.java
new file mode 100644
index 0000000..01ae38c
--- /dev/null
+++ b/core/java/android/text/style/StrikethroughSpan.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.style;
+
+import android.text.TextPaint;
+
+public class StrikethroughSpan extends CharacterStyle {
+
+ @Override
+ public void updateDrawState(TextPaint ds) {
+ ds.setStrikeThruText(true);
+ }
+}
diff --git a/core/java/android/text/style/StyleSpan.java b/core/java/android/text/style/StyleSpan.java
new file mode 100644
index 0000000..cc8b06c
--- /dev/null
+++ b/core/java/android/text/style/StyleSpan.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.style;
+
+import android.graphics.Paint;
+import android.graphics.Typeface;
+import android.text.TextPaint;
+
+/**
+ *
+ * Describes a style in a span.
+ * Note that styles are cumulative -- if both bold and italic are set in
+ * separate spans, or if the base style is bold and a span calls for italic,
+ * you get bold italic. You can't turn off a style from the base style.
+ *
+ */
+public class StyleSpan extends MetricAffectingSpan {
+
+ private int mStyle;
+
+ /**
+ *
+ * @param style An integer constant describing the style for this span. Examples
+ * include bold, italic, and normal. Values are constants defined
+ * in {@link android.graphics.Typeface}.
+ */
+ public StyleSpan(int style) {
+ mStyle = style;
+ }
+
+ /**
+ * Returns the style constant defined in {@link android.graphics.Typeface}.
+ */
+ public int getStyle() {
+ return mStyle;
+ }
+
+ @Override
+ public void updateDrawState(TextPaint ds) {
+ apply(ds, mStyle);
+ }
+
+ @Override
+ public void updateMeasureState(TextPaint paint) {
+ apply(paint, mStyle);
+ }
+
+ private static void apply(Paint paint, int style) {
+ int oldStyle;
+
+ Typeface old = paint.getTypeface();
+ if (old == null) {
+ oldStyle = 0;
+ } else {
+ oldStyle = old.getStyle();
+ }
+
+ int want = oldStyle | style;
+
+ Typeface tf;
+ if (old == null) {
+ tf = Typeface.defaultFromStyle(want);
+ } else {
+ tf = Typeface.create(old, want);
+ }
+
+ int fake = want & ~tf.getStyle();
+
+ if ((fake & Typeface.BOLD) != 0) {
+ paint.setFakeBoldText(true);
+ }
+
+ if ((fake & Typeface.ITALIC) != 0) {
+ paint.setTextSkewX(-0.25f);
+ }
+
+ paint.setTypeface(tf);
+ }
+}
diff --git a/core/java/android/text/style/SubscriptSpan.java b/core/java/android/text/style/SubscriptSpan.java
new file mode 100644
index 0000000..78d6ba9
--- /dev/null
+++ b/core/java/android/text/style/SubscriptSpan.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.style;
+
+import android.text.TextPaint;
+
+public class SubscriptSpan extends MetricAffectingSpan {
+ @Override
+ public void updateDrawState(TextPaint tp) {
+ tp.baselineShift -= (int) (tp.ascent() / 2);
+ }
+
+ @Override
+ public void updateMeasureState(TextPaint tp) {
+ tp.baselineShift -= (int) (tp.ascent() / 2);
+ }
+}
diff --git a/core/java/android/text/style/SuperscriptSpan.java b/core/java/android/text/style/SuperscriptSpan.java
new file mode 100644
index 0000000..79be4de
--- /dev/null
+++ b/core/java/android/text/style/SuperscriptSpan.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.style;
+
+import android.text.TextPaint;
+
+public class SuperscriptSpan extends MetricAffectingSpan {
+ @Override
+ public void updateDrawState(TextPaint tp) {
+ tp.baselineShift += (int) (tp.ascent() / 2);
+ }
+
+ @Override
+ public void updateMeasureState(TextPaint tp) {
+ tp.baselineShift += (int) (tp.ascent() / 2);
+ }
+}
diff --git a/core/java/android/text/style/TabStopSpan.java b/core/java/android/text/style/TabStopSpan.java
new file mode 100644
index 0000000..e5b7644
--- /dev/null
+++ b/core/java/android/text/style/TabStopSpan.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.style;
+
+public interface TabStopSpan
+extends ParagraphStyle
+{
+ public int getTabStop();
+
+ public static class Standard
+ implements TabStopSpan
+ {
+ public Standard(int where) {
+ mTab = where;
+ }
+
+ public int getTabStop() {
+ return mTab;
+ }
+
+ private int mTab;
+ }
+}
diff --git a/core/java/android/text/style/TextAppearanceSpan.java b/core/java/android/text/style/TextAppearanceSpan.java
new file mode 100644
index 0000000..c4ec976
--- /dev/null
+++ b/core/java/android/text/style/TextAppearanceSpan.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.style;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.graphics.Paint;
+import android.graphics.Typeface;
+import android.text.TextPaint;
+
+/**
+ * Sets the text color, size, style, and typeface to match a TextAppearance
+ * resource.
+ */
+public class TextAppearanceSpan extends MetricAffectingSpan {
+ private String mTypeface;
+ private int mStyle;
+ private int mTextSize;
+ private ColorStateList mTextColor;
+ private ColorStateList mTextColorLink;
+
+ /**
+ * Uses the specified TextAppearance resource to determine the
+ * text appearance. The <code>appearance</code> should be, for example,
+ * <code>android.R.style.TextAppearance_Small</code>.
+ */
+ public TextAppearanceSpan(Context context, int appearance) {
+ this(context, appearance, -1);
+ }
+
+ /**
+ * Uses the specified TextAppearance resource to determine the
+ * text appearance, and the specified text color resource
+ * to determine the color. The <code>appearance</code> should be,
+ * for example, <code>android.R.style.TextAppearance_Small</code>,
+ * and the <code>colorList</code> should be, for example,
+ * <code>android.R.styleable.Theme_textColorDim</code>.
+ */
+ public TextAppearanceSpan(Context context, int appearance,
+ int colorList) {
+ TypedArray a =
+ context.obtainStyledAttributes(appearance,
+ com.android.internal.R.styleable.TextAppearance);
+
+ mTextColor = a.getColorStateList(com.android.internal.R.styleable.
+ TextAppearance_textColor);
+ mTextColorLink = a.getColorStateList(com.android.internal.R.styleable.
+ TextAppearance_textColorLink);
+ mTextSize = a.getDimensionPixelSize(com.android.internal.R.styleable.
+ TextAppearance_textSize, -1);
+
+ mStyle = a.getInt(com.android.internal.R.styleable.TextAppearance_textStyle, 0);
+ int tf = a.getInt(com.android.internal.R.styleable.TextAppearance_typeface, 0);
+
+ switch (tf) {
+ case 1:
+ mTypeface = "sans";
+ break;
+
+ case 2:
+ mTypeface = "serif";
+ break;
+
+ case 3:
+ mTypeface = "monospace";
+ break;
+ }
+
+ a.recycle();
+
+ if (colorList >= 0) {
+ a = context.obtainStyledAttributes(com.android.internal.R.style.Theme,
+ com.android.internal.R.styleable.Theme);
+
+ mTextColor = a.getColorStateList(colorList);
+ a.recycle();
+ }
+ }
+
+ /**
+ * Makes text be drawn with the specified typeface, size, style,
+ * and colors.
+ */
+ public TextAppearanceSpan(String family, int style, int size,
+ ColorStateList color, ColorStateList linkColor) {
+ mTypeface = family;
+ mStyle = style;
+ mTextSize = size;
+ mTextColor = color;
+ mTextColorLink = linkColor;
+ }
+
+ /**
+ * Returns the typeface family specified by this span, or <code>null</code>
+ * if it does not specify one.
+ */
+ public String getFamily() {
+ return mTypeface;
+ }
+
+ /**
+ * Returns the text color specified by this span, or <code>null</code>
+ * if it does not specify one.
+ */
+ public ColorStateList getTextColor() {
+ return mTextColor;
+ }
+
+ /**
+ * Returns the link color specified by this span, or <code>null</code>
+ * if it does not specify one.
+ */
+ public ColorStateList getLinkTextColor() {
+ return mTextColorLink;
+ }
+
+ /**
+ * Returns the text size specified by this span, or <code>-1</code>
+ * if it does not specify one.
+ */
+ public int getTextSize() {
+ return mTextSize;
+ }
+
+ /**
+ * Returns the text style specified by this span, or <code>0</code>
+ * if it does not specify one.
+ */
+ public int getTextStyle() {
+ return mStyle;
+ }
+
+ @Override
+ public void updateDrawState(TextPaint ds) {
+ updateMeasureState(ds);
+
+ if (mTextColor != null) {
+ ds.setColor(mTextColor.getColorForState(ds.drawableState, 0));
+ }
+
+ if (mTextColorLink != null) {
+ ds.linkColor = mTextColor.getColorForState(ds.drawableState, 0);
+ }
+ }
+
+ @Override
+ public void updateMeasureState(TextPaint ds) {
+ if (mTypeface != null || mStyle != 0) {
+ Typeface tf = ds.getTypeface();
+ int style = 0;
+
+ if (tf != null) {
+ style = tf.getStyle();
+ }
+
+ style |= mStyle;
+
+ if (mTypeface != null) {
+ tf = Typeface.create(mTypeface, style);
+ } else if (tf == null) {
+ tf = Typeface.defaultFromStyle(style);
+ } else {
+ tf = Typeface.create(tf, style);
+ }
+
+ int fake = style & ~tf.getStyle();
+
+ if ((fake & Typeface.BOLD) != 0) {
+ ds.setFakeBoldText(true);
+ }
+
+ if ((fake & Typeface.ITALIC) != 0) {
+ ds.setTextSkewX(-0.25f);
+ }
+
+ ds.setTypeface(tf);
+ }
+
+ if (mTextSize > 0) {
+ ds.setTextSize(mTextSize);
+ }
+ }
+}
diff --git a/core/java/android/text/style/TypefaceSpan.java b/core/java/android/text/style/TypefaceSpan.java
new file mode 100644
index 0000000..7519ac2
--- /dev/null
+++ b/core/java/android/text/style/TypefaceSpan.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.style;
+
+import android.graphics.Paint;
+import android.graphics.Typeface;
+import android.text.TextPaint;
+
+/**
+ * Changes the typeface family of the text to which the span is attached.
+ */
+public class TypefaceSpan extends MetricAffectingSpan {
+ private String mFamily;
+
+ /**
+ * @param family The font family for this typeface. Examples include
+ * "monospace", "serif", and "sans-serif".
+ */
+ public TypefaceSpan(String family) {
+ mFamily = family;
+ }
+
+ /**
+ * Returns the font family name.
+ */
+ public String getFamily() {
+ return mFamily;
+ }
+
+ @Override
+ public void updateDrawState(TextPaint ds) {
+ apply(ds, mFamily);
+ }
+
+ @Override
+ public void updateMeasureState(TextPaint paint) {
+ apply(paint, mFamily);
+ }
+
+ private static void apply(Paint paint, String family) {
+ int oldStyle;
+
+ Typeface old = paint.getTypeface();
+ if (old == null) {
+ oldStyle = 0;
+ } else {
+ oldStyle = old.getStyle();
+ }
+
+ Typeface tf = Typeface.create(family, oldStyle);
+ int fake = oldStyle & ~tf.getStyle();
+
+ if ((fake & Typeface.BOLD) != 0) {
+ paint.setFakeBoldText(true);
+ }
+
+ if ((fake & Typeface.ITALIC) != 0) {
+ paint.setTextSkewX(-0.25f);
+ }
+
+ paint.setTypeface(tf);
+ }
+}
diff --git a/core/java/android/text/style/URLSpan.java b/core/java/android/text/style/URLSpan.java
new file mode 100644
index 0000000..79809b5
--- /dev/null
+++ b/core/java/android/text/style/URLSpan.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.style;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.text.TextPaint;
+import android.view.View;
+
+public class URLSpan extends ClickableSpan {
+
+ private String mURL;
+
+ public URLSpan(String url) {
+ mURL = url;
+ }
+
+ public String getURL() {
+ return mURL;
+ }
+
+ @Override
+ public void onClick(View widget) {
+ Uri uri = Uri.parse(getURL());
+ Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+ intent.addCategory(Intent.CATEGORY_BROWSABLE);
+ widget.getContext().startActivity(intent);
+ }
+}
diff --git a/core/java/android/text/style/UnderlineSpan.java b/core/java/android/text/style/UnderlineSpan.java
new file mode 100644
index 0000000..5dce0f2
--- /dev/null
+++ b/core/java/android/text/style/UnderlineSpan.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.style;
+
+import android.text.TextPaint;
+
+public class UnderlineSpan extends CharacterStyle {
+
+ @Override
+ public void updateDrawState(TextPaint ds) {
+ ds.setUnderlineText(true);
+ }
+}
diff --git a/core/java/android/text/style/UpdateLayout.java b/core/java/android/text/style/UpdateLayout.java
new file mode 100644
index 0000000..211685a
--- /dev/null
+++ b/core/java/android/text/style/UpdateLayout.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.style;
+
+/**
+ * The classes that affect character-level text formatting in a way that
+ * triggers a text layout update when one is added or remove must implement
+ * this interface.
+ */
+public interface UpdateLayout { }
diff --git a/core/java/android/text/style/WrapTogetherSpan.java b/core/java/android/text/style/WrapTogetherSpan.java
new file mode 100644
index 0000000..11721a8
--- /dev/null
+++ b/core/java/android/text/style/WrapTogetherSpan.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.style;
+
+public interface WrapTogetherSpan
+extends ParagraphStyle
+{
+
+}
diff --git a/core/java/android/text/style/package.html b/core/java/android/text/style/package.html
new file mode 100644
index 0000000..0a8520c
--- /dev/null
+++ b/core/java/android/text/style/package.html
@@ -0,0 +1,10 @@
+<html>
+<body>
+
+<p>Provides classes used to view or change the style of a span of text in a View object.
+The classes with a subclass Standard are passed in to {@link android.text.SpannableString#setSpan(java.lang.Object, int, int, int)
+SpannableString.setSpan()} or {@link android.text.SpannableStringBuilder#setSpan(java.lang.Object, int, int, int)
+SpannableStringBuilder.setSpan()} to add a new styled span to a string in a View object.
+
+</body>
+</html>
diff --git a/core/java/android/text/util/Linkify.java b/core/java/android/text/util/Linkify.java
new file mode 100644
index 0000000..79ecfbd
--- /dev/null
+++ b/core/java/android/text/util/Linkify.java
@@ -0,0 +1,533 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.util;
+
+import android.text.method.LinkMovementMethod;
+import android.text.method.MovementMethod;
+import android.text.style.URLSpan;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.webkit.WebView;
+import android.widget.TextView;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Linkify take a piece of text and a regular expression and turns all of the
+ * regex matches in the text into clickable links. This is particularly
+ * useful for matching things like email addresses, web urls, etc. and making
+ * them actionable.
+ *
+ * Alone with the pattern that is to be matched, a url scheme prefix is also
+ * required. Any pattern match that does not begin with the supplied scheme
+ * will have the scheme prepended to the matched text when the clickable url
+ * is created. For instance, if you are matching web urls you would supply
+ * the scheme <code>http://</code>. If the pattern matches example.com, which
+ * does not have a url scheme prefix, the supplied scheme will be prepended to
+ * create <code>http://example.com</code> when the clickable url link is
+ * created.
+ */
+
+public class Linkify {
+ /**
+ * Bit field indicating that web URLs should be matched in methods that
+ * take an options mask
+ */
+ public static final int WEB_URLS = 0x01;
+
+ /**
+ * Bit field indicating that email addresses should be matched in methods
+ * that take an options mask
+ */
+ public static final int EMAIL_ADDRESSES = 0x02;
+
+ /**
+ * Bit field indicating that phone numbers should be matched in methods that
+ * take an options mask
+ */
+ public static final int PHONE_NUMBERS = 0x04;
+
+ /**
+ * Bit field indicating that phone numbers should be matched in methods that
+ * take an options mask
+ */
+ public static final int MAP_ADDRESSES = 0x08;
+
+ /**
+ * Bit mask indicating that all available patterns should be matched in
+ * methods that take an options mask
+ */
+ public static final int ALL = WEB_URLS | EMAIL_ADDRESSES | PHONE_NUMBERS
+ | MAP_ADDRESSES;
+
+ /**
+ * Don't treat anything with fewer than this many digits as a
+ * phone number.
+ */
+ private static final int PHONE_NUMBER_MINIMUM_DIGITS = 5;
+
+ /**
+ * Filters out web URL matches that occur after an at-sign (@). This is
+ * to prevent turning the domain name in an email address into a web link.
+ */
+ public static final MatchFilter sUrlMatchFilter = new MatchFilter() {
+ public final boolean acceptMatch(CharSequence s, int start, int end) {
+ if (start == 0) {
+ return true;
+ }
+
+ if (s.charAt(start - 1) == '@') {
+ return false;
+ }
+
+ return true;
+ }
+ };
+
+ /**
+ * Filters out URL matches that don't have enough digits to be a
+ * phone number.
+ */
+ public static final MatchFilter sPhoneNumberMatchFilter =
+ new MatchFilter() {
+ public final boolean acceptMatch(CharSequence s, int start, int end) {
+ int digitCount = 0;
+
+ for (int i = start; i < end; i++) {
+ if (Character.isDigit(s.charAt(i))) {
+ digitCount++;
+ if (digitCount >= PHONE_NUMBER_MINIMUM_DIGITS) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+ };
+
+ /**
+ * Transforms matched phone number text into something suitable
+ * to be used in a tel: URL. It does this by removing everything
+ * but the digits and plus signs. For instance:
+ * &apos;+1 (919) 555-1212&apos;
+ * becomes &apos;+19195551212&apos;
+ */
+ public static final TransformFilter sPhoneNumberTransformFilter =
+ new TransformFilter() {
+ public final String transformUrl(final Matcher match, String url) {
+ return Regex.digitsAndPlusOnly(match);
+ }
+ };
+
+ /**
+ * MatchFilter enables client code to have more control over
+ * what is allowed to match and become a link, and what is not.
+ *
+ * For example: when matching web urls you would like things like
+ * http://www.example.com to match, as well as just example.com itelf.
+ * However, you would not want to match against the domain in
+ * support@example.com. So, when matching against a web url pattern you
+ * might also include a MatchFilter that disallows the match if it is
+ * immediately preceded by an at-sign (@).
+ */
+ public interface MatchFilter {
+ /**
+ * Examines the character span matched by the pattern and determines
+ * if the match should be turned into an actionable link.
+ *
+ * @param s The body of text against which the pattern
+ * was matched
+ * @param start The index of the first character in s that was
+ * matched by the pattern - inclusive
+ * @param end The index of the last character in s that was
+ * matched - exclusive
+ *
+ * @return Whether this match should be turned into a link
+ */
+ boolean acceptMatch(CharSequence s, int start, int end);
+ }
+
+ /**
+ * TransformFilter enables client code to have more control over
+ * how matched patterns are represented as URLs.
+ *
+ * For example: when converting a phone number such as (919) 555-1212
+ * into a tel: URL the parentheses, white space, and hyphen need to be
+ * removed to produce tel:9195551212.
+ */
+ public interface TransformFilter {
+ /**
+ * Examines the matched text and either passes it through or uses the
+ * data in the Matcher state to produce a replacement.
+ *
+ * @param match The regex matcher state that found this URL text
+ * @param url The text that was matched
+ *
+ * @return The transformed form of the URL
+ */
+ String transformUrl(final Matcher match, String url);
+ }
+
+ /**
+ * Scans the text of the provided Spannable and turns all occurrences
+ * of the link types indicated in the mask into clickable links.
+ * If the mask is nonzero, it also removes any existing URLSpans
+ * attached to the Spannable, to avoid problems if you call it
+ * repeatedly on the same text.
+ */
+ public static final boolean addLinks(Spannable text, int mask) {
+ if (mask == 0) {
+ return false;
+ }
+
+ URLSpan[] old = text.getSpans(0, text.length(), URLSpan.class);
+
+ for (int i = old.length - 1; i >= 0; i--) {
+ text.removeSpan(old[i]);
+ }
+
+ ArrayList<LinkSpec> links = new ArrayList<LinkSpec>();
+
+ if ((mask & WEB_URLS) != 0) {
+ gatherLinks(links, text, Regex.WEB_URL_PATTERN,
+ new String[] { "http://", "https://" },
+ sUrlMatchFilter, null);
+ }
+
+ if ((mask & EMAIL_ADDRESSES) != 0) {
+ gatherLinks(links, text, Regex.EMAIL_ADDRESS_PATTERN,
+ new String[] { "mailto:" },
+ null, null);
+ }
+
+ if ((mask & PHONE_NUMBERS) != 0) {
+ gatherLinks(links, text, Regex.PHONE_PATTERN,
+ new String[] { "tel:" },
+ sPhoneNumberMatchFilter, sPhoneNumberTransformFilter);
+ }
+
+ if ((mask & MAP_ADDRESSES) != 0) {
+ gatherMapLinks(links, text);
+ }
+
+ pruneOverlaps(links);
+
+ if (links.size() == 0) {
+ return false;
+ }
+
+ for (LinkSpec link: links) {
+ applyLink(link.url, link.start, link.end, text);
+ }
+
+ return true;
+ }
+
+ /**
+ * Scans the text of the provided TextView and turns all occurrences of
+ * the link types indicated in the mask into clickable links. If matches
+ * are found the movement method for the TextView is set to
+ * LinkMovementMethod.
+ */
+ public static final boolean addLinks(TextView text, int mask) {
+ if (mask == 0) {
+ return false;
+ }
+
+ CharSequence t = text.getText();
+
+ if (t instanceof Spannable) {
+ if (addLinks((Spannable) t, mask)) {
+ addLinkMovementMethod(text);
+ return true;
+ }
+
+ return false;
+ } else {
+ SpannableString s = SpannableString.valueOf(t);
+
+ if (addLinks(s, mask)) {
+ addLinkMovementMethod(text);
+ text.setText(s);
+
+ return true;
+ }
+
+ return false;
+ }
+ }
+
+ private static final void addLinkMovementMethod(TextView t) {
+ MovementMethod m = t.getMovementMethod();
+
+ if ((m == null) || !(m instanceof LinkMovementMethod)) {
+ if (t.getLinksClickable()) {
+ t.setMovementMethod(LinkMovementMethod.getInstance());
+ }
+ }
+ }
+
+ /**
+ * Applies a regex to the text of a TextView turning the matches into
+ * links. If links are found then UrlSpans are applied to the link
+ * text match areas, and the movement method for the text is changed
+ * to LinkMovementMethod.
+ *
+ * @param text TextView whose text is to be marked-up with links
+ * @param pattern Regex pattern to be used for finding links
+ * @param scheme Url scheme string (eg <code>http://</code> to be
+ * prepended to the url of links that do not have
+ * a scheme specified in the link text
+ */
+ public static final void addLinks(TextView text, Pattern pattern,
+ String scheme) {
+ addLinks(text, pattern, scheme, null, null);
+ }
+
+ /**
+ * Applies a regex to the text of a TextView turning the matches into
+ * links. If links are found then UrlSpans are applied to the link
+ * text match areas, and the movement method for the text is changed
+ * to LinkMovementMethod.
+ *
+ * @param text TextView whose text is to be marked-up with links
+ * @param p Regex pattern to be used for finding links
+ * @param scheme Url scheme string (eg <code>http://</code> to be
+ * prepended to the url of links that do not have
+ * a scheme specified in the link text
+ * @param matchFilter The filter that is used to allow the client code
+ * additional control over which pattern matches are
+ * to be converted into links.
+ */
+ public static final void addLinks(TextView text, Pattern p, String scheme,
+ MatchFilter matchFilter, TransformFilter transformFilter) {
+ SpannableString s = SpannableString.valueOf(text.getText());
+
+ if (addLinks(s, p, scheme, matchFilter, transformFilter)) {
+ text.setText(s);
+ addLinkMovementMethod(text);
+ }
+ }
+
+ /**
+ * Applies a regex to a Spannable turning the matches into
+ * links.
+ *
+ * @param text Spannable whose text is to be marked-up with
+ * links
+ * @param pattern Regex pattern to be used for finding links
+ * @param scheme Url scheme string (eg <code>http://</code> to be
+ * prepended to the url of links that do not have
+ * a scheme specified in the link text
+ */
+ public static final boolean addLinks(Spannable text, Pattern pattern,
+ String scheme) {
+ return addLinks(text, pattern, scheme, null, null);
+ }
+
+ /**
+ * Applies a regex to a Spannable turning the matches into
+ * links.
+ *
+ * @param s Spannable whose text is to be marked-up with
+ * links
+ * @param p Regex pattern to be used for finding links
+ * @param scheme Url scheme string (eg <code>http://</code> to be
+ * prepended to the url of links that do not have
+ * a scheme specified in the link text
+ * @param matchFilter The filter that is used to allow the client code
+ * additional control over which pattern matches are
+ * to be converted into links.
+ */
+ public static final boolean addLinks(Spannable s, Pattern p,
+ String scheme, MatchFilter matchFilter,
+ TransformFilter transformFilter) {
+ boolean hasMatches = false;
+ String prefix = (scheme == null) ? "" : scheme.toLowerCase();
+ Matcher m = p.matcher(s);
+
+ while (m.find()) {
+ int start = m.start();
+ int end = m.end();
+ boolean allowed = true;
+
+ if (matchFilter != null) {
+ allowed = matchFilter.acceptMatch(s, start, end);
+ }
+
+ if (allowed) {
+ String url = makeUrl(m.group(0), new String[] { prefix },
+ m, transformFilter);
+
+ applyLink(url, start, end, s);
+ hasMatches = true;
+ }
+ }
+
+ return hasMatches;
+ }
+
+ private static final void applyLink(String url, int start, int end,
+ Spannable text) {
+ URLSpan span = new URLSpan(url);
+
+ text.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
+ private static final String makeUrl(String url, String[] prefixes,
+ Matcher m, TransformFilter filter) {
+ if (filter != null) {
+ url = filter.transformUrl(m, url);
+ }
+
+ boolean hasPrefix = false;
+ for (int i = 0; i < prefixes.length; i++) {
+ if (url.regionMatches(true, 0, prefixes[i], 0,
+ prefixes[i].length())) {
+ hasPrefix = true;
+ break;
+ }
+ }
+ if (!hasPrefix) {
+ url = prefixes[0] + url;
+ }
+
+ return url;
+ }
+
+ private static final void gatherLinks(ArrayList<LinkSpec> links,
+ Spannable s, Pattern pattern, String[] schemes,
+ MatchFilter matchFilter, TransformFilter transformFilter) {
+ Matcher m = pattern.matcher(s);
+
+ while (m.find()) {
+ int start = m.start();
+ int end = m.end();
+
+ if (matchFilter == null || matchFilter.acceptMatch(s, start, end)) {
+ LinkSpec spec = new LinkSpec();
+ String url = makeUrl(m.group(0), schemes, m, transformFilter);
+
+ spec.url = url;
+ spec.start = start;
+ spec.end = end;
+
+ links.add(spec);
+ }
+ }
+ }
+
+ private static final void gatherMapLinks(ArrayList<LinkSpec> links,
+ Spannable s) {
+ String string = s.toString();
+ String address;
+ int base = 0;
+ while ((address = WebView.findAddress(string)) != null) {
+ int start = string.indexOf(address);
+ if (start < 0) {
+ break;
+ }
+ LinkSpec spec = new LinkSpec();
+ int length = address.length();
+ int end = start + length;
+ spec.start = base + start;
+ spec.end = base + end;
+ string = string.substring(end);
+ base += end;
+
+ String encodedAddress = null;
+ try {
+ encodedAddress = URLEncoder.encode(address,"UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ continue;
+ }
+ spec.url = "geo:0,0?q=" + encodedAddress;
+ links.add(spec);
+ }
+ }
+
+ private static final void pruneOverlaps(ArrayList<LinkSpec> links) {
+ Comparator<LinkSpec> c = new Comparator<LinkSpec>() {
+ public final int compare(LinkSpec a, LinkSpec b) {
+ if (a.start < b.start) {
+ return -1;
+ }
+
+ if (a.start > b.start) {
+ return 1;
+ }
+
+ if (a.end < b.end) {
+ return 1;
+ }
+
+ if (a.end > b.end) {
+ return -1;
+ }
+
+ return 0;
+ }
+
+ public final boolean equals(Object o) {
+ return false;
+ }
+ };
+
+ Collections.sort(links, c);
+
+ int len = links.size();
+ int i = 0;
+
+ while (i < len - 1) {
+ LinkSpec a = links.get(i);
+ LinkSpec b = links.get(i + 1);
+ int remove = -1;
+
+ if ((a.start <= b.start) && (a.end > b.start)) {
+ if (b.end <= a.end) {
+ remove = i + 1;
+ } else if ((a.end - a.start) > (b.end - b.start)) {
+ remove = i + 1;
+ } else if ((a.end - a.start) < (b.end - b.start)) {
+ remove = i;
+ }
+
+ if (remove != -1) {
+ links.remove(remove);
+ len--;
+ continue;
+ }
+
+ }
+
+ i++;
+ }
+ }
+}
+
+class LinkSpec {
+ String url;
+ int start;
+ int end;
+}
diff --git a/core/java/android/text/util/Regex.java b/core/java/android/text/util/Regex.java
new file mode 100644
index 0000000..55ad140
--- /dev/null
+++ b/core/java/android/text/util/Regex.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.util;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * @hide
+ */
+public class Regex {
+ /**
+ * Regular expression pattern to match all IANA top-level domains.
+ * List accurate as of 2007/06/15. List taken from:
+ * http://data.iana.org/TLD/tlds-alpha-by-domain.txt
+ * This pattern is auto-generated by //device/tools/make-iana-tld-pattern.py
+ */
+ public static final Pattern TOP_LEVEL_DOMAIN_PATTERN
+ = Pattern.compile(
+ "((aero|arpa|asia|a[cdefgilmnoqrstuwxz])"
+ + "|(biz|b[abdefghijmnorstvwyz])"
+ + "|(cat|com|coop|c[acdfghiklmnoruvxyz])"
+ + "|d[ejkmoz]"
+ + "|(edu|e[cegrstu])"
+ + "|f[ijkmor]"
+ + "|(gov|g[abdefghilmnpqrstuwy])"
+ + "|h[kmnrtu]"
+ + "|(info|int|i[delmnoqrst])"
+ + "|(jobs|j[emop])"
+ + "|k[eghimnrwyz]"
+ + "|l[abcikrstuvy]"
+ + "|(mil|mobi|museum|m[acdghklmnopqrstuvwxyz])"
+ + "|(name|net|n[acefgilopruz])"
+ + "|(org|om)"
+ + "|(pro|p[aefghklmnrstwy])"
+ + "|qa"
+ + "|r[eouw]"
+ + "|s[abcdeghijklmnortuvyz]"
+ + "|(tel|travel|t[cdfghjklmnoprtvwz])"
+ + "|u[agkmsyz]"
+ + "|v[aceginu]"
+ + "|w[fs]"
+ + "|y[etu]"
+ + "|z[amw])");
+
+ /**
+ * Regular expression pattern to match RFC 1738 URLs
+ * List accurate as of 2007/06/15. List taken from:
+ * http://data.iana.org/TLD/tlds-alpha-by-domain.txt
+ * This pattern is auto-generated by //device/tools/make-iana-tld-pattern.py
+ */
+ public static final Pattern WEB_URL_PATTERN
+ = Pattern.compile(
+ "((?:(http|https):\\/\\/(?:(?:[a-zA-Z0-9\\$\\-\\_\\.\\+\\!\\*\\'\\(\\)"
+ + "\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2}))+(?:\\:(?:[a-zA-Z0-9\\$\\-\\_"
+ + "\\.\\+\\!\\*\\'\\(\\)\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2}))+)?\\@)?)?"
+ + "((?:(?:[a-zA-Z0-9][a-zA-Z0-9\\-]*\\.)+" // named host
+ + "(?:" // plus top level domain
+ + "(?:aero|arpa|asia|a[cdefgilmnoqrstuwxz])"
+ + "|(?:biz|b[abdefghijmnorstvwyz])"
+ + "|(?:cat|com|coop|c[acdfghiklmnoruvxyz])"
+ + "|d[ejkmoz]"
+ + "|(?:edu|e[cegrstu])"
+ + "|f[ijkmor]"
+ + "|(?:gov|g[abdefghilmnpqrstuwy])"
+ + "|h[kmnrtu]"
+ + "|(?:info|int|i[delmnoqrst])"
+ + "|(?:jobs|j[emop])"
+ + "|k[eghimnrwyz]"
+ + "|l[abcikrstuvy]"
+ + "|(?:mil|mobi|museum|m[acdghklmnopqrstuvwxyz])"
+ + "|(?:name|net|n[acefgilopruz])"
+ + "|(?:org|om)"
+ + "|(?:pro|p[aefghklmnrstwy])"
+ + "|qa"
+ + "|r[eouw]"
+ + "|s[abcdeghijklmnortuvyz]"
+ + "|(?:tel|travel|t[cdfghjklmnoprtvwz])"
+ + "|u[agkmsyz]"
+ + "|v[aceginu]"
+ + "|w[fs]"
+ + "|y[etu]"
+ + "|z[amw]))"
+ + "|(?:(?:25[0-5]|2[0-4]" // or ip address
+ + "[0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9])\\.(?:25[0-5]|2[0-4][0-9]"
+ + "|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(?:25[0-5]|2[0-4][0-9]|[0-1]"
+ + "[0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(?:25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}"
+ + "|[1-9][0-9]|[0-9])))"
+ + "(?:\\:\\d{1,5})?)" // plus option port number
+ + "(\\/(?:(?:[a-zA-Z0-9\\;\\/\\?\\:\\@\\&\\=\\#\\~" // plus option query params
+ + "\\-\\.\\+\\!\\*\\'\\(\\)\\,\\_])|(?:\\%[a-fA-F0-9]{2}))*)?"
+ + "\\b"); // and finally, a word boundary this is to stop foo.sure from matching as foo.su
+
+ public static final Pattern IP_ADDRESS_PATTERN
+ = Pattern.compile(
+ "((25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9])\\.(25[0-5]|2[0-4]"
+ + "[0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1]"
+ + "[0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}"
+ + "|[1-9][0-9]|[0-9]))");
+
+ public static final Pattern DOMAIN_NAME_PATTERN
+ = Pattern.compile(
+ "(((([a-zA-Z0-9][a-zA-Z0-9\\-]*)*[a-zA-Z0-9]\\.)+"
+ + TOP_LEVEL_DOMAIN_PATTERN + ")|"
+ + IP_ADDRESS_PATTERN + ")");
+
+ public static final Pattern EMAIL_ADDRESS_PATTERN
+ = Pattern.compile(
+ "[a-zA-Z0-9\\+\\.\\_\\%\\-]+" +
+ "\\@" +
+ "[a-zA-Z0-9][a-zA-Z0-9\\-]*" +
+ "(" +
+ "\\." +
+ "[a-zA-Z0-9][a-zA-Z0-9\\-]*" +
+ ")+"
+ );
+
+ /**
+ * This pattern is intended for searching for things that look like they
+ * might be phone numbers in arbitrary text, not for validating whether
+ * something is in fact a phone number. It will miss many things that
+ * are legitimate phone numbers.
+ */
+ public static final Pattern PHONE_PATTERN
+ = Pattern.compile(
+ "(?:\\+[0-9]+)|(?:[0-9()][0-9()\\- \\.][0-9()\\- \\.]+[0-9])");
+
+ /**
+ * Convenience method to take all of the non-null matching groups in a
+ * regex Matcher and return them as a concatenated string.
+ *
+ * @param matcher The Matcher object from which grouped text will
+ * be extracted
+ *
+ * @return A String comprising all of the non-null matched
+ * groups concatenated together
+ */
+ public static final String concatGroups(Matcher matcher) {
+ StringBuilder b = new StringBuilder();
+ final int numGroups = matcher.groupCount();
+
+ for (int i = 1; i <= numGroups; i++) {
+ String s = matcher.group(i);
+
+ System.err.println("Group(" + i + ") : " + s);
+
+ if (s != null) {
+ b.append(s);
+ }
+ }
+
+ return b.toString();
+ }
+
+ /**
+ * Convenience method to return only the digits and plus signs
+ * in the matching string.
+ *
+ * @param matcher The Matcher object from which digits and plus will
+ * be extracted
+ *
+ * @return A String comprising all of the digits and plus in
+ * the match
+ */
+ public static final String digitsAndPlusOnly(Matcher matcher) {
+ StringBuilder buffer = new StringBuilder();
+ String matchingRegion = matcher.group();
+
+ for (int i = 0, size = matchingRegion.length(); i < size; i++) {
+ char character = matchingRegion.charAt(i);
+
+ if (character == '+' || Character.isDigit(character)) {
+ buffer.append(character);
+ }
+ }
+ return buffer.toString();
+ }
+}
diff --git a/core/java/android/text/util/Rfc822Token.java b/core/java/android/text/util/Rfc822Token.java
new file mode 100644
index 0000000..e6472df
--- /dev/null
+++ b/core/java/android/text/util/Rfc822Token.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.util;
+
+/**
+ * This class stores an RFC 822-like name, address, and comment,
+ * and provides methods to convert them to quoted strings.
+ */
+public class Rfc822Token {
+ private String mName, mAddress, mComment;
+
+ /**
+ * Creates a new Rfc822Token with the specified name, address,
+ * and comment.
+ */
+ public Rfc822Token(String name, String address, String comment) {
+ mName = name;
+ mAddress = address;
+ mComment = comment;
+ }
+
+ /**
+ * Returns the name part.
+ */
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Returns the address part.
+ */
+ public String getAddress() {
+ return mAddress;
+ }
+
+ /**
+ * Returns the comment part.
+ */
+ public String getComment() {
+ return mComment;
+ }
+
+ /**
+ * Changes the name to the specified name.
+ */
+ public void setName(String name) {
+ mName = name;
+ }
+
+ /**
+ * Changes the address to the specified address.
+ */
+ public void setAddress(String address) {
+ mAddress = address;
+ }
+
+ /**
+ * Changes the comment to the specified comment.
+ */
+ public void setComment(String comment) {
+ mComment = comment;
+ }
+
+ /**
+ * Returns the name (with quoting added if necessary),
+ * the comment (in parentheses), and the address (in angle brackets).
+ * This should be suitable for inclusion in an RFC 822 address list.
+ */
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+
+ if (mName != null && mName.length() != 0) {
+ sb.append(quoteNameIfNecessary(mName));
+ sb.append(' ');
+ }
+
+ if (mComment != null && mComment.length() != 0) {
+ sb.append('(');
+ sb.append(quoteComment(mComment));
+ sb.append(") ");
+ }
+
+ if (mAddress != null && mAddress.length() != 0) {
+ sb.append('<');
+ sb.append(mAddress);
+ sb.append('>');
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Returns the name, conservatively quoting it if there are any
+ * characters that are likely to cause trouble outside of a
+ * quoted string, or returning it literally if it seems safe.
+ */
+ public static String quoteNameIfNecessary(String name) {
+ int len = name.length();
+
+ for (int i = 0; i < len; i++) {
+ char c = name.charAt(i);
+
+ if (! ((c >= 'A' && i <= 'Z') ||
+ (c >= 'a' && c <= 'z') ||
+ (c == ' ') ||
+ (c >= '0' && c <= '9'))) {
+ return '"' + quoteName(name) + '"';
+ }
+ }
+
+ return name;
+ }
+
+ /**
+ * Returns the name, with internal backslashes and quotation marks
+ * preceded by backslashes. The outer quote marks themselves are not
+ * added by this method.
+ */
+ public static String quoteName(String name) {
+ StringBuilder sb = new StringBuilder();
+
+ int len = name.length();
+ for (int i = 0; i < len; i++) {
+ char c = name.charAt(i);
+
+ if (c == '\\' || c == '"') {
+ sb.append('\\');
+ }
+
+ sb.append(c);
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Returns the comment, with internal backslashes and parentheses
+ * preceded by backslashes. The outer parentheses themselves are
+ * not added by this method.
+ */
+ public static String quoteComment(String comment) {
+ int len = comment.length();
+ StringBuilder sb = new StringBuilder();
+
+ for (int i = 0; i < len; i++) {
+ char c = comment.charAt(i);
+
+ if (c == '(' || c == ')' || c == '\\') {
+ sb.append('\\');
+ }
+
+ sb.append(c);
+ }
+
+ return sb.toString();
+ }
+}
+
diff --git a/core/java/android/text/util/Rfc822Tokenizer.java b/core/java/android/text/util/Rfc822Tokenizer.java
new file mode 100644
index 0000000..d4e78b0
--- /dev/null
+++ b/core/java/android/text/util/Rfc822Tokenizer.java
@@ -0,0 +1,292 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.util;
+
+import android.widget.MultiAutoCompleteTextView;
+
+import java.util.ArrayList;
+
+/**
+ * This class works as a Tokenizer for MultiAutoCompleteTextView for
+ * address list fields, and also provides a method for converting
+ * a string of addresses (such as might be typed into such a field)
+ * into a series of Rfc822Tokens.
+ */
+public class Rfc822Tokenizer implements MultiAutoCompleteTextView.Tokenizer {
+ /**
+ * This constructor will try to take a string like
+ * "Foo Bar (something) &lt;foo\@google.com&gt;,
+ * blah\@google.com (something)"
+ * and convert it into one or more Rfc822Tokens.
+ * It does *not* decode MIME encoded-words; charset conversion
+ * must already have taken place if necessary.
+ * It will try to be tolerant of broken syntax instead of
+ * returning an error.
+ */
+ public static Rfc822Token[] tokenize(CharSequence text) {
+ ArrayList<Rfc822Token> out = new ArrayList<Rfc822Token>();
+ StringBuilder name = new StringBuilder();
+ StringBuilder address = new StringBuilder();
+ StringBuilder comment = new StringBuilder();
+
+ int i = 0;
+ int cursor = text.length();
+
+ while (i < cursor) {
+ char c = text.charAt(i);
+
+ if (c == ',' || c == ';') {
+ i++;
+
+ while (i < cursor && text.charAt(i) == ' ') {
+ i++;
+ }
+
+ crunch(name);
+
+ if (address.length() > 0) {
+ out.add(new Rfc822Token(name.toString(),
+ address.toString(),
+ comment.toString()));
+ } else if (name.length() > 0) {
+ out.add(new Rfc822Token(null,
+ name.toString(),
+ comment.toString()));
+ }
+
+ name.setLength(0);
+ address.setLength(0);
+ address.setLength(0);
+ } else if (c == '"') {
+ i++;
+
+ while (i < cursor) {
+ c = text.charAt(i);
+
+ if (c == '"') {
+ i++;
+ break;
+ } else if (c == '\\') {
+ name.append(text.charAt(i + 1));
+ i += 2;
+ } else {
+ name.append(c);
+ i++;
+ }
+ }
+ } else if (c == '(') {
+ int level = 1;
+ i++;
+
+ while (i < cursor && level > 0) {
+ c = text.charAt(i);
+
+ if (c == ')') {
+ if (level > 1) {
+ comment.append(c);
+ }
+
+ level--;
+ i++;
+ } else if (c == '(') {
+ comment.append(c);
+ level++;
+ i++;
+ } else if (c == '\\') {
+ comment.append(text.charAt(i + 1));
+ i += 2;
+ } else {
+ comment.append(c);
+ i++;
+ }
+ }
+ } else if (c == '<') {
+ i++;
+
+ while (i < cursor) {
+ c = text.charAt(i);
+
+ if (c == '>') {
+ i++;
+ break;
+ } else {
+ address.append(c);
+ i++;
+ }
+ }
+ } else if (c == ' ') {
+ name.append('\0');
+ i++;
+ } else {
+ name.append(c);
+ i++;
+ }
+ }
+
+ crunch(name);
+
+ if (address.length() > 0) {
+ out.add(new Rfc822Token(name.toString(),
+ address.toString(),
+ comment.toString()));
+ } else if (name.length() > 0) {
+ out.add(new Rfc822Token(null,
+ name.toString(),
+ comment.toString()));
+ }
+
+ return out.toArray(new Rfc822Token[out.size()]);
+ }
+
+ private static void crunch(StringBuilder sb) {
+ int i = 0;
+ int len = sb.length();
+
+ while (i < len) {
+ char c = sb.charAt(i);
+
+ if (c == '\0') {
+ if (i == 0 || i == len - 1 ||
+ sb.charAt(i - 1) == ' ' ||
+ sb.charAt(i - 1) == '\0' ||
+ sb.charAt(i + 1) == ' ' ||
+ sb.charAt(i + 1) == '\0') {
+ sb.deleteCharAt(i);
+ len--;
+ } else {
+ i++;
+ }
+ } else {
+ i++;
+ }
+ }
+
+ for (i = 0; i < len; i++) {
+ if (sb.charAt(i) == '\0') {
+ sb.setCharAt(i, ' ');
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public int findTokenStart(CharSequence text, int cursor) {
+ /*
+ * It's hard to search backward, so search forward until
+ * we reach the cursor.
+ */
+
+ int best = 0;
+ int i = 0;
+
+ while (i < cursor) {
+ i = findTokenEnd(text, i);
+
+ if (i < cursor) {
+ i++; // Skip terminating punctuation
+
+ while (i < cursor && text.charAt(i) == ' ') {
+ i++;
+ }
+
+ if (i < cursor) {
+ best = i;
+ }
+ }
+ }
+
+ return best;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public int findTokenEnd(CharSequence text, int cursor) {
+ int len = text.length();
+ int i = cursor;
+
+ while (i < len) {
+ char c = text.charAt(i);
+
+ if (c == ',' || c == ';') {
+ return i;
+ } else if (c == '"') {
+ i++;
+
+ while (i < len) {
+ c = text.charAt(i);
+
+ if (c == '"') {
+ i++;
+ break;
+ } else if (c == '\\') {
+ i += 2;
+ } else {
+ i++;
+ }
+ }
+ } else if (c == '(') {
+ int level = 1;
+ i++;
+
+ while (i < len && level > 0) {
+ c = text.charAt(i);
+
+ if (c == ')') {
+ level--;
+ i++;
+ } else if (c == '(') {
+ level++;
+ i++;
+ } else if (c == '\\') {
+ i += 2;
+ } else {
+ i++;
+ }
+ }
+ } else if (c == '<') {
+ i++;
+
+ while (i < len) {
+ c = text.charAt(i);
+
+ if (c == '>') {
+ i++;
+ break;
+ } else {
+ i++;
+ }
+ }
+ } else {
+ i++;
+ }
+ }
+
+ return i;
+ }
+
+ /**
+ * Terminates the specified address with a comma and space.
+ * This assumes that the specified text already has valid syntax.
+ * The Adapter subclass's convertToString() method must make that
+ * guarantee.
+ */
+ public CharSequence terminateToken(CharSequence text) {
+ return text + ", ";
+ }
+}
diff --git a/core/java/android/text/util/Rfc822Validator.java b/core/java/android/text/util/Rfc822Validator.java
new file mode 100644
index 0000000..9f03bb0
--- /dev/null
+++ b/core/java/android/text/util/Rfc822Validator.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.util;
+
+import android.widget.AutoCompleteTextView;
+
+import java.util.regex.Pattern;
+
+/**
+ * This class works as a Validator for AutoCompleteTextView for
+ * email addresses. If a token does not appear to be a valid address,
+ * it is trimmed of characters that cannot legitimately appear in one
+ * and has the specified domain name added. It is meant for use with
+ * {@link Rfc822Token} and {@link Rfc822Tokenizer}.
+ *
+ * @deprecated In the future make sure we don't quietly alter the user's
+ * text in ways they did not intend. Meanwhile, hide this
+ * class from the public API because it does not even have
+ * a full understanding of the syntax it claims to correct.
+ * @hide
+ */
+public class Rfc822Validator implements AutoCompleteTextView.Validator {
+ /*
+ * Regex.EMAIL_ADDRESS_PATTERN hardcodes the TLD that we accept, but we
+ * want to make sure we will keep accepting email addresses with TLD's
+ * that don't exist at the time of this writing, so this regexp relaxes
+ * that constraint by accepting any kind of top level domain, not just
+ * ".com", ".fr", etc...
+ */
+ private static final Pattern EMAIL_ADDRESS_PATTERN =
+ Pattern.compile("[^\\s@]+@[^\\s@]+\\.[a-zA-z][a-zA-Z][a-zA-Z]*");
+
+ private String mDomain;
+
+ /**
+ * Constructs a new validator that uses the specified domain name as
+ * the default when none is specified.
+ */
+ public Rfc822Validator(String domain) {
+ mDomain = domain;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean isValid(CharSequence text) {
+ Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(text);
+
+ return tokens.length == 1 &&
+ EMAIL_ADDRESS_PATTERN.
+ matcher(tokens[0].getAddress()).matches();
+ }
+
+ /**
+ * @return a string in which all the characters that are illegal for the username
+ * part of the email address have been removed.
+ */
+ private String removeIllegalCharacters(String s) {
+ StringBuilder result = new StringBuilder();
+ int length = s.length();
+ for (int i = 0; i < length; i++) {
+ char c = s.charAt(i);
+
+ /*
+ * An RFC822 atom can contain any ASCII printing character
+ * except for periods and any of the following punctuation.
+ * A local-part can contain multiple atoms, concatenated by
+ * periods, so do allow periods here.
+ */
+
+ if (c <= ' ' || c > '~') {
+ continue;
+ }
+
+ if (c == '(' || c == ')' || c == '<' || c == '>' ||
+ c == '@' || c == ',' || c == ';' || c == ':' ||
+ c == '\\' || c == '"' || c == '[' || c == ']') {
+ continue;
+ }
+
+ result.append(c);
+ }
+ return result.toString();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public CharSequence fixText(CharSequence cs) {
+ Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(cs);
+ StringBuilder sb = new StringBuilder();
+
+ for (int i = 0; i < tokens.length; i++) {
+ String text = tokens[i].getAddress();
+ int index = text.indexOf('@');
+ if (index < 0) {
+ // If there is no @, just append the domain of the account
+ tokens[i].setAddress(removeIllegalCharacters(text) + "@" + mDomain);
+ } else {
+ // Otherwise, remove everything right of the '@' and append the domain
+ // ("a@b" becomes "a@gmail.com").
+ String fix = removeIllegalCharacters(text.substring(0, index));
+ tokens[i].setAddress(fix + "@" + mDomain);
+ }
+
+ sb.append(tokens[i].toString());
+ if (i + 1 < tokens.length) {
+ sb.append(", ");
+ }
+ }
+
+ return sb;
+ }
+}
diff --git a/core/java/android/text/util/package.html b/core/java/android/text/util/package.html
new file mode 100644
index 0000000..d9312aa2
--- /dev/null
+++ b/core/java/android/text/util/package.html
@@ -0,0 +1,6 @@
+<HTML>
+<BODY>
+Utilities for converting identifiable text strings into clickable links
+and creating RFC 822-type message (SMTP) tokens.
+</BODY>
+</HTML> \ No newline at end of file
diff --git a/core/java/android/util/AndroidException.java b/core/java/android/util/AndroidException.java
new file mode 100644
index 0000000..a767ea1
--- /dev/null
+++ b/core/java/android/util/AndroidException.java
@@ -0,0 +1,34 @@
+/*
+ * 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;
+
+/**
+ * Base class for all checked exceptions thrown by the Android frameworks.
+ */
+public class AndroidException extends Exception {
+ public AndroidException() {
+ }
+
+ public AndroidException(String name) {
+ super(name);
+ }
+
+ public AndroidException(Exception cause) {
+ super(cause);
+ }
+};
+
diff --git a/core/java/android/util/AndroidRuntimeException.java b/core/java/android/util/AndroidRuntimeException.java
new file mode 100644
index 0000000..4ed17bc
--- /dev/null
+++ b/core/java/android/util/AndroidRuntimeException.java
@@ -0,0 +1,34 @@
+/*
+ * 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;
+
+/**
+ * Base class for all unchecked exceptions thrown by the Android frameworks.
+ */
+public class AndroidRuntimeException extends RuntimeException {
+ public AndroidRuntimeException() {
+ }
+
+ public AndroidRuntimeException(String name) {
+ super(name);
+ }
+
+ public AndroidRuntimeException(Exception cause) {
+ super(cause);
+ }
+};
+
diff --git a/core/java/android/util/AttributeSet.java b/core/java/android/util/AttributeSet.java
new file mode 100644
index 0000000..01a7ad4
--- /dev/null
+++ b/core/java/android/util/AttributeSet.java
@@ -0,0 +1,269 @@
+/*
+ * 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;
+
+
+/**
+ * A collection of attributes, as found associated with a tag in an XML
+ * document. Often you will not want to use this interface directly, instead
+ * passing it to {@link android.content.res.Resources.Theme#obtainStyledAttributes(AttributeSet, int[], int, int)
+ * Resources.Theme.obtainStyledAttributes()}
+ * which will take care of parsing the attributes for you. In particular,
+ * the Resources API will convert resource references (attribute values such as
+ * "@string/my_label" in the original XML) to the desired type
+ * for you; if you use AttributeSet directly then you will need to manually
+ * check for resource references
+ * (with {@link #getAttributeResourceValue(int, int)}) and do the resource
+ * lookup yourself if needed. Direct use of AttributeSet also prevents the
+ * application of themes and styles when retrieving attribute values.
+ *
+ * <p>This interface provides an efficient mechanism for retrieving
+ * data from compiled XML files, which can be retrieved for a particular
+ * XmlPullParser through {@link Xml#asAttributeSet
+ * Xml.getAttributeSet()}. Normally this will return an implementation
+ * of the interface that works on top of a generic XmlPullParser, however it
+ * is more useful in conjunction with compiled XML resources:
+ *
+ * <pre>
+ * XmlPullParser parser = resources.getXml(myResouce);
+ * AttributeSet attributes = Xml.getAttributeSet(parser);</pre>
+ *
+ * <p>The implementation returned here, unlike using
+ * the implementation on top of a generic XmlPullParser,
+ * is highly optimized by retrieving pre-computed information that was
+ * generated by aapt when compiling your resources. For example,
+ * the {@link #getAttributeFloatValue(int, float)} method returns a floating
+ * point number previous stored in the compiled resource instead of parsing
+ * at runtime the string originally in the XML file.
+ *
+ * <p>This interface also provides additional information contained in the
+ * compiled XML resource that is not available in a normal XML file, such
+ * as {@link #getAttributeNameResource(int)} which returns the resource
+ * identifier associated with a particular XML attribute name.
+ */
+public interface AttributeSet {
+ public int getAttributeCount();
+ public String getAttributeName(int index);
+ public String getAttributeValue(int index);
+ public String getAttributeValue(String namespace, String name);
+ public String getPositionDescription();
+
+ /**
+ * Return the resource ID associated with the given attribute name. This
+ * will be the identifier for an attribute resource, which can be used by
+ * styles. Returns 0 if there is no resource associated with this
+ * attribute.
+ *
+ * <p>Note that this is different than {@link #getAttributeResourceValue}
+ * in that it returns a resource identifier for the attribute name; the
+ * other method returns this attribute's value as a resource identifier.
+ *
+ * @param index Index of the desired attribute, 0...count-1.
+ *
+ * @return The resource identifier, 0 if none.
+ */
+ public int getAttributeNameResource(int index);
+
+ /**
+ * Return the index of the value of 'attribute' in the list 'options'.
+ *
+ * @param attribute Name of attribute to retrieve.
+ * @param options List of strings whose values we are checking against.
+ * @param defaultValue Value returned if attribute doesn't exist or no
+ * match is found.
+ *
+ * @return Index in to 'options' or defaultValue.
+ */
+ public int getAttributeListValue(String namespace, String attribute,
+ String[] options, int defaultValue);
+
+ /**
+ * Return the boolean value of 'attribute'.
+ *
+ * @param attribute The attribute to retrieve.
+ * @param defaultValue What to return if the attribute isn't found.
+ *
+ * @return Resulting value.
+ */
+ public boolean getAttributeBooleanValue(String namespace, String attribute,
+ boolean defaultValue);
+
+ /**
+ * Return the value of 'attribute' as a resource identifier.
+ *
+ * <p>Note that this is different than {@link #getAttributeNameResource}
+ * in that it returns a the value contained in this attribute as a
+ * resource identifier (i.e., a value originally of the form
+ * "@package:type/resource"); the other method returns a resource
+ * identifier that identifies the name of the attribute.
+ *
+ * @param attribute The attribute to retrieve.
+ * @param defaultValue What to return if the attribute isn't found.
+ *
+ * @return Resulting value.
+ */
+ public int getAttributeResourceValue(String namespace, String attribute,
+ int defaultValue);
+
+ /**
+ * Return the integer value of 'attribute'.
+ *
+ * @param attribute The attribute to retrieve.
+ * @param defaultValue What to return if the attribute isn't found.
+ *
+ * @return Resulting value.
+ */
+ public int getAttributeIntValue(String namespace, String attribute,
+ int defaultValue);
+
+ /**
+ * Return the boolean value of 'attribute' that is formatted as an
+ * unsigned value. In particular, the formats 0xn...n and #n...n are
+ * handled.
+ *
+ * @param attribute The attribute to retrieve.
+ * @param defaultValue What to return if the attribute isn't found.
+ *
+ * @return Resulting value.
+ */
+ public int getAttributeUnsignedIntValue(String namespace, String attribute,
+ int defaultValue);
+
+ /**
+ * Return the float value of 'attribute'.
+ *
+ * @param attribute The attribute to retrieve.
+ * @param defaultValue What to return if the attribute isn't found.
+ *
+ * @return Resulting value.
+ */
+ public float getAttributeFloatValue(String namespace, String attribute,
+ float defaultValue);
+
+ /**
+ * Return the index of the value of attribute at 'index' in the list
+ * 'options'.
+ *
+ * @param index Index of the desired attribute, 0...count-1.
+ * @param options List of strings whose values we are checking against.
+ * @param defaultValue Value returned if attribute doesn't exist or no
+ * match is found.
+ *
+ * @return Index in to 'options' or defaultValue.
+ */
+ public int getAttributeListValue(int index,
+ String[] options, int defaultValue);
+
+ /**
+ * Return the boolean value of attribute at 'index'.
+ *
+ * @param index Index of the desired attribute, 0...count-1.
+ * @param defaultValue What to return if the attribute isn't found.
+ *
+ * @return Resulting value.
+ */
+ public boolean getAttributeBooleanValue(int index,
+ boolean defaultValue);
+
+ /**
+ * Return the value of attribute at 'index' as a resource identifier.
+ *
+ * <p>Note that this is different than {@link #getAttributeNameResource}
+ * in that it returns a the value contained in this attribute as a
+ * resource identifier (i.e., a value originally of the form
+ * "@package:type/resource"); the other method returns a resource
+ * identifier that identifies the name of the attribute.
+ *
+ * @param index Index of the desired attribute, 0...count-1.
+ * @param defaultValue What to return if the attribute isn't found.
+ *
+ * @return Resulting value.
+ */
+ public int getAttributeResourceValue(int index,
+ int defaultValue);
+
+ /**
+ * Return the integer value of attribute at 'index'.
+ *
+ * @param index Index of the desired attribute, 0...count-1.
+ * @param defaultValue What to return if the attribute isn't found.
+ *
+ * @return Resulting value.
+ */
+ public int getAttributeIntValue(int index,
+ int defaultValue);
+
+ /**
+ * Return the integer value of attribute at 'index' that is formatted as an
+ * unsigned value. In particular, the formats 0xn...n and #n...n are
+ * handled.
+ *
+ * @param index Index of the desired attribute, 0...count-1.
+ * @param defaultValue What to return if the attribute isn't found.
+ *
+ * @return Resulting value.
+ */
+ public int getAttributeUnsignedIntValue(int index,
+ int defaultValue);
+
+ /**
+ * Return the float value of attribute at 'index'.
+ *
+ * @param index Index of the desired attribute, 0...count-1.
+ * @param defaultValue What to return if the attribute isn't found.
+ *
+ * @return Resulting value.
+ */
+ public float getAttributeFloatValue(int index,
+ float defaultValue);
+
+ /**
+ * Return the value of the "id" attribute or null if there is not one.
+ * Equivalent to getAttributeValue(null, "id").
+ *
+ * @return The id attribute's value or null.
+ */
+ public String getIdAttribute();
+
+ /**
+ * Return the value of the "class" attribute or null if there is not one.
+ * Equivalent to getAttributeValue(null, "class").
+ *
+ * @return The class attribute's value or null.
+ */
+ public String getClassAttribute();
+
+ /**
+ * Return the integer value of the "id" attribute or defaultValue if there
+ * is none.
+ * Equivalent to getAttributeResourceValue(null, "id", defaultValue);
+ *
+ * @param defaultValue What to return if the "id" attribute isn't found.
+ * @return int Resulting value.
+ */
+ public int getIdAttributeResourceValue(int defaultValue);
+
+ /**
+
+ * Return the value of the "style" attribute or 0 if there is not one.
+ * Equivalent to getAttributeResourceValue(null, "style").
+ *
+ * @return The style attribute's resource identifier or 0.
+ */
+ public int getStyleAttribute();
+}
+
diff --git a/core/java/android/util/Config.java b/core/java/android/util/Config.java
new file mode 100644
index 0000000..c0b27f8
--- /dev/null
+++ b/core/java/android/util/Config.java
@@ -0,0 +1,51 @@
+/* device/vmlibs-config/release/android/util/Config.java
+**
+** Copyright 2006, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.util;
+
+/**
+ * Build configuration. The constants in this class vary depending
+ * on release vs. debug build. This is the configuration for release builds.
+ * {@more}
+ */
+public final class Config
+{
+ /**
+ * Is this a release build?
+ */
+ public static final boolean RELEASE = true;
+
+ /**
+ * Is this a debug build?
+ */
+ public static final boolean DEBUG = false;
+
+ /**
+ * Is profiling enabled?
+ */
+ public static final boolean PROFILE = false;
+
+ /**
+ * Are VERBOSE log messages enabled?
+ */
+ public static final boolean LOGV = false;
+
+ /**
+ * Are DEBUG log messages enabled?
+ */
+ public static final boolean LOGD = true;
+}
diff --git a/core/java/android/util/DayOfMonthCursor.java b/core/java/android/util/DayOfMonthCursor.java
new file mode 100644
index 0000000..52ee00e
--- /dev/null
+++ b/core/java/android/util/DayOfMonthCursor.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+/**
+ * Helps control and display a month view of a calendar that has a current
+ * selected day.
+ * <ul>
+ * <li>Keeps track of current month, day, year</li>
+ * <li>Keeps track of current cursor position (row, column)</li>
+ * <li>Provides methods to help display the calendar</li>
+ * <li>Provides methods to move the cursor up / down / left / right.</li>
+ * </ul>
+ *
+ * This should be used by anyone who presents a month view to users and wishes
+ * to behave consistently with other widgets and apps; if we ever change our
+ * mind about when to flip the month, we can change it here only.
+ *
+ * @hide
+ */
+public class DayOfMonthCursor extends MonthDisplayHelper {
+
+ private int mRow;
+ private int mColumn;
+
+ /**
+ * @param year The initial year.
+ * @param month The initial month.
+ * @param dayOfMonth The initial dayOfMonth.
+ * @param weekStartDay What dayOfMonth of the week the week should start,
+ * in terms of {@link java.util.Calendar} constants such as
+ * {@link java.util.Calendar#SUNDAY}.
+ */
+ public DayOfMonthCursor(int year, int month, int dayOfMonth, int weekStartDay) {
+ super(year, month, weekStartDay);
+ mRow = getRowOf(dayOfMonth);
+ mColumn = getColumnOf(dayOfMonth);
+ }
+
+
+ public int getSelectedRow() {
+ return mRow;
+ }
+
+ public int getSelectedColumn() {
+ return mColumn;
+ }
+
+ public void setSelectedRowColumn(int row, int col) {
+ mRow = row;
+ mColumn = col;
+ }
+
+ public int getSelectedDayOfMonth() {
+ return getDayAt(mRow, mColumn);
+ }
+
+ public void setSelectedDayOfMonth(int dayOfMonth) {
+ mRow = getRowOf(dayOfMonth);
+ mColumn = getColumnOf(dayOfMonth);
+ }
+
+ public boolean isSelected(int row, int column) {
+ return (mRow == row) && (mColumn == column);
+ }
+
+ /**
+ * Move up one box, potentially flipping to the previous month.
+ * @return Whether the month was flipped to the previous month
+ * due to the move.
+ */
+ public boolean up() {
+ if (isWithinCurrentMonth(mRow - 1, mColumn)) {
+ // within current month, just move up
+ mRow--;
+ return false;
+ }
+ // flip back to previous month, same column, first position within month
+ previousMonth();
+ mRow = 5;
+ while(!isWithinCurrentMonth(mRow, mColumn)) {
+ mRow--;
+ }
+ return true;
+ }
+
+ /**
+ * Move down one box, potentially flipping to the next month.
+ * @return Whether the month was flipped to the next month
+ * due to the move.
+ */
+ public boolean down() {
+ if (isWithinCurrentMonth(mRow + 1, mColumn)) {
+ // within current month, just move down
+ mRow++;
+ return false;
+ }
+ // flip to next month, same column, first position within month
+ nextMonth();
+ mRow = 0;
+ while (!isWithinCurrentMonth(mRow, mColumn)) {
+ mRow++;
+ }
+ return true;
+ }
+
+ /**
+ * Move left one box, potentially flipping to the previous month.
+ * @return Whether the month was flipped to the previous month
+ * due to the move.
+ */
+ public boolean left() {
+ if (mColumn == 0) {
+ mRow--;
+ mColumn = 6;
+ } else {
+ mColumn--;
+ }
+
+ if (isWithinCurrentMonth(mRow, mColumn)) {
+ return false;
+ }
+
+ // need to flip to last day of previous month
+ previousMonth();
+ int lastDay = getNumberOfDaysInMonth();
+ mRow = getRowOf(lastDay);
+ mColumn = getColumnOf(lastDay);
+ return true;
+ }
+
+ /**
+ * Move right one box, potentially flipping to the next month.
+ * @return Whether the month was flipped to the next month
+ * due to the move.
+ */
+ public boolean right() {
+ if (mColumn == 6) {
+ mRow++;
+ mColumn = 0;
+ } else {
+ mColumn++;
+ }
+
+ if (isWithinCurrentMonth(mRow, mColumn)) {
+ return false;
+ }
+
+ // need to flip to first day of next month
+ nextMonth();
+ mRow = 0;
+ mColumn = 0;
+ while (!isWithinCurrentMonth(mRow, mColumn)) {
+ mColumn++;
+ }
+ return true;
+ }
+
+}
diff --git a/core/java/android/util/DebugUtils.java b/core/java/android/util/DebugUtils.java
new file mode 100644
index 0000000..1c5d669
--- /dev/null
+++ b/core/java/android/util/DebugUtils.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.InvocationTargetException;
+
+/**
+ * <p>Various utilities for debugging and logging.</p>
+ */
+public class DebugUtils {
+ /**
+ * <p>Filters objects against the <code>ANDROID_OBJECT_FILTER</code>
+ * environment variable. This environment variable can filter objects
+ * based on their class name and attribute values.</p>
+ *
+ * <p>Here is the syntax for <code>ANDROID_OBJECT_FILTER</code>:</p>
+ *
+ * <p><code>ClassName@attribute1=value1@attribute2=value2...</code></p>
+ *
+ * <p>Examples:</p>
+ * <ul>
+ * <li>Select TextView instances: <code>TextView</code></li>
+ * <li>Select TextView instances of text "Loading" and bottom offset of 22:
+ * <code>TextView@text=Loading.*@bottom=22</code></li>
+ * </ul>
+ *
+ * <p>The class name and the values are regular expressions.</p>
+ *
+ * <p>This class is useful for debugging and logging purpose:</p>
+ * <pre>
+ * if (Config.DEBUG) {
+ * if (DebugUtils.isObjectSelected(childView) && Config.LOGV) {
+ * Log.v(TAG, "Object " + childView + " logged!");
+ * }
+ * }
+ * </pre>
+ *
+ * <p><strong>NOTE</strong>: This method is very expensive as it relies
+ * heavily on regular expressions and reflection. Calls to this method
+ * should always be stripped out of the release binaries and avoided
+ * as much as possible in debug mode.</p>
+ *
+ * @param object any object to match against the ANDROID_OBJECT_FILTER
+ * environement variable
+ * @return true if object is selected by the ANDROID_OBJECT_FILTER
+ * environment variable, false otherwise
+ */
+ public static boolean isObjectSelected(Object object) {
+ boolean match = false;
+ String s = System.getenv("ANDROID_OBJECT_FILTER");
+ if (s != null && s.length() > 0) {
+ String[] selectors = s.split("@");
+ // first selector == class name
+ if (object.getClass().getSimpleName().matches(selectors[0])) {
+ // check potential attributes
+ for (int i = 1; i < selectors.length; i++) {
+ String[] pair = selectors[i].split("=");
+ Class<?> klass = object.getClass();
+ try {
+ Method declaredMethod = null;
+ Class<?> parent = klass;
+ do {
+ declaredMethod = parent.getDeclaredMethod("get" +
+ pair[0].substring(0, 1).toUpperCase() +
+ pair[0].substring(1),
+ (Class[]) null);
+ } while ((parent = klass.getSuperclass()) != null &&
+ declaredMethod == null);
+
+ if (declaredMethod != null) {
+ Object value = declaredMethod
+ .invoke(object, (Object[])null);
+ match |= (value != null ?
+ value.toString() : "null").matches(pair[1]);
+ }
+ } catch (NoSuchMethodException e) {
+ e.printStackTrace();
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ } catch (InvocationTargetException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+ return match;
+ }
+
+}
diff --git a/core/java/android/util/DisplayMetrics.java b/core/java/android/util/DisplayMetrics.java
new file mode 100644
index 0000000..8fc3602
--- /dev/null
+++ b/core/java/android/util/DisplayMetrics.java
@@ -0,0 +1,85 @@
+/*
+ * 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;
+
+
+/**
+ * A structure describing general information about a display, such as its
+ * size, density, and font scaling.
+ */
+public class DisplayMetrics {
+ /**
+ * The absolute width of the display in pixels.
+ */
+ public int widthPixels;
+ /**
+ * The absolute height of the display in pixels.
+ */
+ public int heightPixels;
+ /**
+ * The logical density of the display. This is a scaling factor for the
+ * Density Independent Pixel unit, where one DIP is one pixel on an
+ * approximately 160 dpi screen (for example a 240x320, 1.5"x2" screen),
+ * providing the baseline of the system's display. Thus on a 160dpi screen
+ * this density value will be 1; on a 106 dpi screen it would be .75; etc.
+ *
+ * <p>This value does not exactly follow the real screen size (as given by
+ * {@link #xdpi} and {@link #ydpi}, but rather is used to scale the size of
+ * the overall UI in steps based on gross changes in the display dpi. For
+ * example, a 240x320 screen will have a density of 1 even if its width is
+ * 1.8", 1.3", etc. However, if the screen resolution is increased to
+ * 320x480 but the screen size remained 1.5"x2" then the density would be
+ * increased (probably to 1.5).
+ */
+ public float density;
+ /**
+ * A scaling factor for fonts displayed on the display. This is the same
+ * as {@link #density}, except that it may be adjusted in smaller
+ * increments at runtime based on a user preference for the font size.
+ */
+ public float scaledDensity;
+ /**
+ * The exact physical pixels per inch of the screen in the X dimension.
+ */
+ public float xdpi;
+ /**
+ * The exact physical pixels per inch of the screen in the Y dimension.
+ */
+ public float ydpi;
+
+ public DisplayMetrics() {
+ }
+
+ public void setTo(DisplayMetrics o) {
+ widthPixels = o.widthPixels;
+ heightPixels = o.heightPixels;
+ density = o.density;
+ scaledDensity = o.scaledDensity;
+ xdpi = o.xdpi;
+ ydpi = o.ydpi;
+ }
+
+ public void setToDefaults() {
+ widthPixels = 0;
+ heightPixels = 0;
+ density = 1;
+ scaledDensity = 1;
+ xdpi = 160;
+ ydpi = 160;
+ }
+}
+
diff --git a/core/java/android/util/EventLog.java b/core/java/android/util/EventLog.java
new file mode 100644
index 0000000..24b4f73
--- /dev/null
+++ b/core/java/android/util/EventLog.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import com.google.android.collect.Lists;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * {@hide}
+ * Dynamically defined (in terms of event types), space efficient (i.e. "tight") event logging
+ * to help instrument code for large scale stability and performance monitoring.
+ *
+ * Note that this class contains all static methods. This is done for efficiency reasons.
+ *
+ * Events for the event log are self-describing binary data structures. They start with a 20 byte
+ * header (generated automatically) which contains all of the following in order:
+ *
+ * <ul>
+ * <li> Payload length: 2 bytes - length of the non-header portion </li>
+ * <li> Padding: 2 bytes - no meaning at this time </li>
+ * <li> Timestamp:
+ * <ul>
+ * <li> Seconds: 4 bytes - seconds since Epoch </li>
+ * <li> Nanoseconds: 4 bytes - plus extra nanoseconds </li>
+ * </ul></li>
+ * <li> Process ID: 4 bytes - matching {@link android.os.Process#myPid} </li>
+ * <li> Thread ID: 4 bytes - matching {@link android.os.Process#myTid} </li>
+ * </li>
+ * </ul>
+ *
+ * The above is followed by a payload, comprised of the following:
+ * <ul>
+ * <li> Tag: 4 bytes - unique integer used to identify a particular event. This number is also
+ * used as a key to map to a string that can be displayed by log reading tools.
+ * </li>
+ * <li> Type: 1 byte - can be either {@link #INT}, {@link #LONG}, {@link #STRING},
+ * or {@link #LIST}. </li>
+ * <li> Event log value: the size and format of which is one of:
+ * <ul>
+ * <li> INT: 4 bytes </li>
+ * <li> LONG: 8 bytes </li>
+ * <li> STRING:
+ * <ul>
+ * <li> Size of STRING: 4 bytes </li>
+ * <li> The string: n bytes as specified in the size fields above. </li>
+ * </ul></li>
+ * <li> {@link List LIST}:
+ * <ul>
+ * <li> Num items: 1 byte </li>
+ * <li> N value payloads, where N is the number of items specified above. </li>
+ * </ul></li>
+ * </ul>
+ * </li>
+ * <li> '\n': 1 byte - an automatically generated newline, used to help detect and recover from log
+ * corruption and enable stansard unix tools like grep, tail and wc to operate
+ * on event logs. </li>
+ * </ul>
+ *
+ * Note that all output is done in the endian-ness of the device (as determined
+ * by {@link ByteOrder#nativeOrder()}).
+ */
+
+public class EventLog {
+
+ // Value types
+ public static final byte INT = 0;
+ public static final byte LONG = 1;
+ public static final byte STRING = 2;
+ public static final byte LIST = 3;
+
+ /**
+ * An immutable tuple used to log a heterogeneous set of loggable items.
+ * The items can be Integer, Long, String, or {@link List}.
+ * The maximum number of items is 127
+ */
+ public static final class List {
+ private Object[] mItems;
+
+ /**
+ * Get a particular tuple item
+ * @param pos The position of the item in the tuple
+ */
+ public final Object getItem(int pos) {
+ return mItems[pos];
+ }
+
+ /**
+ * Get the number of items in the tuple.
+ */
+ public final byte getNumItems() {
+ return (byte) mItems.length;
+ }
+
+ /**
+ * Create a new tuple.
+ * @param items The items to create the tuple with, as varargs.
+ * @throws IllegalArgumentException if the arguments are too few (0),
+ * too many, or aren't loggable types.
+ */
+ public List(Object... items) throws IllegalArgumentException {
+ if (items.length > Byte.MAX_VALUE) {
+ throw new IllegalArgumentException(
+ "A List must have fewer than "
+ + Byte.MAX_VALUE + " items in it.");
+ }
+ if (items.length < 1) {
+ throw new IllegalArgumentException(
+ "A List must have at least one item in it.");
+ }
+ for (int i = 0; i < items.length; i++) {
+ final Object item = items[i];
+ if (item == null) {
+ // Would be nice to be able to write null strings...
+ items[i] = "";
+ } else if (!(item instanceof List ||
+ item instanceof String ||
+ item instanceof Integer ||
+ item instanceof Long)) {
+ throw new IllegalArgumentException(
+ "Attempt to create a List with illegal item type.");
+ }
+ }
+ this.mItems = items;
+ }
+ }
+
+ /**
+ * A previously logged event read from the logs.
+ */
+ public static final class Event {
+ private final ByteBuffer mBuffer;
+
+ // Layout of event log entry received from kernel.
+ private static final int LENGTH_OFFSET = 0;
+ private static final int PROCESS_OFFSET = 4;
+ private static final int THREAD_OFFSET = 8;
+ private static final int SECONDS_OFFSET = 12;
+ private static final int NANOSECONDS_OFFSET = 16;
+
+ private static final int PAYLOAD_START = 20;
+ private static final int TAG_OFFSET = 20;
+ private static final int DATA_START = 24;
+
+ /** @param data containing event, read from the system */
+ public Event(byte[] data) {
+ mBuffer = ByteBuffer.wrap(data);
+ mBuffer.order(ByteOrder.nativeOrder());
+ }
+
+ public int getProcessId() {
+ return mBuffer.getInt(PROCESS_OFFSET);
+ }
+
+ public int getThreadId() {
+ return mBuffer.getInt(THREAD_OFFSET);
+ }
+
+ public long getTimeNanos() {
+ return mBuffer.getInt(SECONDS_OFFSET) * 1000000000l
+ + mBuffer.getInt(NANOSECONDS_OFFSET);
+ }
+
+ public int getTag() {
+ return mBuffer.getInt(TAG_OFFSET);
+ }
+
+ /** @return one of Integer, Long, String, or List. */
+ public synchronized Object getData() {
+ mBuffer.limit(PAYLOAD_START + mBuffer.getShort(LENGTH_OFFSET));
+ mBuffer.position(DATA_START); // Just after the tag.
+ return decodeObject();
+ }
+
+ /** @return the loggable item at the current position in mBuffer. */
+ private Object decodeObject() {
+ if (mBuffer.remaining() < 1) return null;
+ switch (mBuffer.get()) {
+ case INT:
+ if (mBuffer.remaining() < 4) return null;
+ return mBuffer.getInt();
+
+ case LONG:
+ if (mBuffer.remaining() < 8) return null;
+ return mBuffer.getLong();
+
+ case STRING:
+ try {
+ if (mBuffer.remaining() < 4) return null;
+ int length = mBuffer.getInt();
+ if (length < 0 || mBuffer.remaining() < length) return null;
+ int start = mBuffer.position();
+ mBuffer.position(start + length);
+ return new String(mBuffer.array(), start, length, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException(e); // UTF-8 is guaranteed.
+ }
+
+ case LIST:
+ if (mBuffer.remaining() < 1) return null;
+ int length = mBuffer.get();
+ if (length <= 0) return null;
+ Object[] array = new Object[length];
+ for (int i = 0; i < length; ++i) {
+ array[i] = decodeObject();
+ if (array[i] == null) return null;
+ }
+ return new List(array);
+
+ default:
+ return null;
+ }
+ }
+ }
+
+ // We assume that the native methods deal with any concurrency issues.
+
+ /**
+ * Send an event log message.
+ * @param tag An event identifer
+ * @param value A value to log
+ * @return The number of bytes written
+ */
+ public static native int writeEvent(int tag, int value);
+
+ /**
+ * Send an event log message.
+ * @param tag An event identifer
+ * @param value A value to log
+ * @return The number of bytes written
+ */
+ public static native int writeEvent(int tag, long value);
+
+ /**
+ * Send an event log message.
+ * @param tag An event identifer
+ * @param str A value to log
+ * @return The number of bytes written
+ */
+ public static native int writeEvent(int tag, String str);
+
+ /**
+ * Send an event log message.
+ * @param tag An event identifer
+ * @param list A {@link List} to log
+ * @return The number of bytes written
+ */
+ public static native int writeEvent(int tag, List list);
+
+ /**
+ * Send an event log message.
+ * @param tag An event identifer
+ * @param list A list of values to log
+ * @return The number of bytes written
+ */
+ public static int writeEvent(int tag, Object... list) {
+ return writeEvent(tag, new List(list));
+ }
+
+ /**
+ * Read events from the log, filtered by type.
+ * @param tags to search for
+ * @param output container to add events into
+ * @throws IOException if something goes wrong reading events
+ */
+ public static native void readEvents(int[] tags, Collection<Event> output)
+ throws IOException;
+}
diff --git a/core/java/android/util/EventLogTags.java b/core/java/android/util/EventLogTags.java
new file mode 100644
index 0000000..be905e3
--- /dev/null
+++ b/core/java/android/util/EventLogTags.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.util.Log;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/** Parsed representation of /etc/event-log-tags. */
+public class EventLogTags {
+ private final static String TAG = "EventLogTags";
+
+ private final static String TAGS_FILE = "/etc/event-log-tags";
+
+ public static class Description {
+ public final int mTag;
+ public final String mName;
+ // TODO: Parse parameter descriptions when anyone has a use for them.
+
+ Description(int tag, String name) {
+ mTag = tag;
+ mName = name;
+ }
+ }
+
+ private final static Pattern COMMENT_PATTERN = Pattern.compile(
+ "^\\s*(#.*)?$");
+
+ private final static Pattern TAG_PATTERN = Pattern.compile(
+ "^\\s*(\\d+)\\s+(\\w+)\\s*(\\(.*\\))?\\s*$");
+
+ private final HashMap<String, Description> mNameMap =
+ new HashMap<String, Description>();
+
+ private final HashMap<Integer, Description> mTagMap =
+ new HashMap<Integer, Description>();
+
+ public EventLogTags() throws IOException {
+ this(new BufferedReader(new FileReader(TAGS_FILE), 256));
+ }
+
+ public EventLogTags(BufferedReader input) throws IOException {
+ String line;
+ while ((line = input.readLine()) != null) {
+ Matcher m = COMMENT_PATTERN.matcher(line);
+ if (m.matches()) continue;
+
+ m = TAG_PATTERN.matcher(line);
+ if (m.matches()) {
+ try {
+ int tag = Integer.parseInt(m.group(1));
+ Description d = new Description(tag, m.group(2));
+ mNameMap.put(d.mName, d);
+ mTagMap.put(d.mTag, d);
+ } catch (NumberFormatException e) {
+ Log.e(TAG, "Error in event log tags entry: " + line, e);
+ }
+ } else {
+ Log.e(TAG, "Can't parse event log tags entry: " + line);
+ }
+ }
+ }
+
+ public Description get(String name) {
+ return mNameMap.get(name);
+ }
+
+ public Description get(int tag) {
+ return mTagMap.get(tag);
+ }
+}
diff --git a/core/java/android/util/FloatMath.java b/core/java/android/util/FloatMath.java
new file mode 100644
index 0000000..6216638
--- /dev/null
+++ b/core/java/android/util/FloatMath.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+/**
+ * Math routines similar to those found in {@link java.lang.Math}. Performs
+ * computations on {@code float} values directly without incurring the overhead
+ * of conversions to and from {@code double}.
+ *
+ * <p>On one platform, {@code FloatMath.sqrt(100)} executes in one third of the
+ * time required by {@code java.lang.Math.sqrt(100)}.</p>
+ */
+public class FloatMath {
+
+ /** Prevents instantiation. */
+ private FloatMath() {}
+
+ /**
+ * Returns the float conversion of the most positive (i.e. closest to
+ * positive infinity) integer value which is less than the argument.
+ *
+ * @param value to be converted
+ * @return the floor of value
+ */
+ public static native float floor(float value);
+
+ /**
+ * Returns the float conversion of the most negative (i.e. closest to
+ * negative infinity) integer value which is greater than the argument.
+ *
+ * @param value to be converted
+ * @return the ceiling of value
+ */
+ public static native float ceil(float value);
+
+ /**
+ * Returns the closest float approximation of the sine of the argument.
+ *
+ * @param angle to compute the cosine of, in radians
+ * @return the sine of angle
+ */
+ public static native float sin(float angle);
+
+ /**
+ * Returns the closest float approximation of the cosine of the argument.
+ *
+ * @param angle to compute the cosine of, in radians
+ * @return the cosine of angle
+ */
+ public static native float cos(float angle);
+
+ /**
+ * Returns the closest float approximation of the square root of the
+ * argument.
+ *
+ * @param value to compute sqrt of
+ * @return the square root of value
+ */
+ public static native float sqrt(float value);
+}
diff --git a/core/java/android/util/Log.java b/core/java/android/util/Log.java
new file mode 100644
index 0000000..24f67cd
--- /dev/null
+++ b/core/java/android/util/Log.java
@@ -0,0 +1,247 @@
+/*
+ * 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 com.android.internal.os.RuntimeInit;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+/**
+ * API for sending log output.
+ *
+ * <p>Generally, use the Log.v() Log.d() Log.i() Log.w() and Log.e()
+ * methods.
+ *
+ * <p>The order in terms of verbosity, from least to most is
+ * ERROR, WARN, INFO, DEBUG, VERBOSE. Verbose should never be compiled
+ * into an application except during development. Debug logs are compiled
+ * in but stripped at runtime. Error, warning and info logs are always kept.
+ *
+ * <p><b>Tip:</b> A good convention is to declare a <code>TAG</code> constant
+ * in your class:
+ *
+ * <pre>private static final String TAG = "MyActivity";</pre>
+ *
+ * and use that in subsequent calls to the log methods.
+ * </p>
+ *
+ * <p><b>Tip:</b> Don't forget that when you make a call like
+ * <pre>Log.v(TAG, "index=" + i);</pre>
+ * that when you're building the string to pass into Log.d, the compiler uses a
+ * StringBuilder and at least three allocations occur: the StringBuilder
+ * itself, the buffer, and the String object. Realistically, there is also
+ * another buffer allocation and copy, and even more pressure on the gc.
+ * That means that if your log message is filtered out, you might be doing
+ * significant work and incurring significant overhead.
+ */
+public final class Log {
+
+ /**
+ * Priority constant for the println method; use Log.v.
+ */
+ public static final int VERBOSE = 2;
+
+ /**
+ * Priority constant for the println method; use Log.d.
+ */
+ public static final int DEBUG = 3;
+
+ /**
+ * Priority constant for the println method; use Log.i.
+ */
+ public static final int INFO = 4;
+
+ /**
+ * Priority constant for the println method; use Log.w.
+ */
+ public static final int WARN = 5;
+
+ /**
+ * Priority constant for the println method; use Log.e.
+ */
+ public static final int ERROR = 6;
+
+ /**
+ * Priority constant for the println method.
+ */
+ public static final int ASSERT = 7;
+
+ private Log() {
+ }
+
+ /**
+ * Send a {@link #VERBOSE} log message.
+ * @param tag Used to identify the source of a log message. It usually identfies
+ * the class or activity where the log call occurs.
+ * @param msg The message you would like logged.
+ */
+ public static int v(String tag, String msg) {
+ return println(VERBOSE, tag, msg);
+ }
+
+ /**
+ * Send a {@link #VERBOSE} log message and log the exception.
+ * @param tag Used to identify the source of a log message. It usually identfies
+ * the class or activity where the log call occurs.
+ * @param msg The message you would like logged.
+ * @param tr An exception to log
+ */
+ public static int v(String tag, String msg, Throwable tr) {
+ return println(VERBOSE, tag, msg + '\n' + getStackTraceString(tr));
+ }
+
+ /**
+ * Send a {@link #DEBUG} log message.
+ * @param tag Used to identify the source of a log message. It usually identfies
+ * the class or activity where the log call occurs.
+ * @param msg The message you would like logged.
+ */
+ public static int d(String tag, String msg) {
+ return println(DEBUG, tag, msg);
+ }
+
+ /**
+ * Send a {@link #DEBUG} log message and log the exception.
+ * @param tag Used to identify the source of a log message. It usually identfies
+ * the class or activity where the log call occurs.
+ * @param msg The message you would like logged.
+ * @param tr An exception to log
+ */
+ public static int d(String tag, String msg, Throwable tr) {
+ return println(DEBUG, tag, msg + '\n' + getStackTraceString(tr));
+ }
+
+ /**
+ * Send an {@link #INFO} log message.
+ * @param tag Used to identify the source of a log message. It usually identfies
+ * the class or activity where the log call occurs.
+ * @param msg The message you would like logged.
+ */
+ public static int i(String tag, String msg) {
+ return println(INFO, tag, msg);
+ }
+
+ /**
+ * Send a {@link #INFO} log message and log the exception.
+ * @param tag Used to identify the source of a log message. It usually identfies
+ * the class or activity where the log call occurs.
+ * @param msg The message you would like logged.
+ * @param tr An exception to log
+ */
+ public static int i(String tag, String msg, Throwable tr) {
+ return println(INFO, tag, msg + '\n' + getStackTraceString(tr));
+ }
+
+ /**
+ * Send a {@link #WARN} log message.
+ * @param tag Used to identify the source of a log message. It usually identfies
+ * the class or activity where the log call occurs.
+ * @param msg The message you would like logged.
+ */
+ public static int w(String tag, String msg) {
+ return println(WARN, tag, msg);
+ }
+
+ /**
+ * Send a {@link #WARN} log message and log the exception.
+ * @param tag Used to identify the source of a log message. It usually identfies
+ * the class or activity where the log call occurs.
+ * @param msg The message you would like logged.
+ * @param tr An exception to log
+ */
+ public static int w(String tag, String msg, Throwable tr) {
+ return println(WARN, tag, msg + '\n' + getStackTraceString(tr));
+ }
+
+ /**
+ * Checks to see whether or not a log for the specified tag is loggable at the specified level.
+ *
+ * The default level of any tag is set to INFO. This means that any level above and including
+ * INFO will be logged. Before you make any calls to a logging method you should check to see
+ * if your tag should be logged. You can change the default level by setting a system property:
+ * 'setprop log.tag.&lt;YOUR_LOG_TAG> &lt;LEVEL>'
+ * Where level is either VERBOSE, DEBUG, INFO, WARN, ERROR, ASSERT, or SUPPRESS. SUPRESS will
+ * turn off all logging for your tag. You can also create a local.prop file that with the
+ * following in it:
+ * 'log.tag.&lt;YOUR_LOG_TAG>=&lt;LEVEL>'
+ * and place that in /data/local.prop.
+ *
+ * @param tag The tag to check.
+ * @param level The level to check.
+ * @return Whether or not that this is allowed to be logged.
+ * @throws IllegalArgumentException is thrown if the tag.length() > 23.
+ */
+ public static native boolean isLoggable(String tag, int level);
+
+ /*
+ * Send a {@link #WARN} log message and log the exception.
+ * @param tag Used to identify the source of a log message. It usually identfies
+ * the class or activity where the log call occurs.
+ * @param tr An exception to log
+ */
+ public static int w(String tag, Throwable tr) {
+ return println(WARN, tag, getStackTraceString(tr));
+ }
+
+ /**
+ * Send an {@link #ERROR} log message.
+ * @param tag Used to identify the source of a log message. It usually identfies
+ * the class or activity where the log call occurs.
+ * @param msg The message you would like logged.
+ */
+ public static int e(String tag, String msg) {
+ return println(ERROR, tag, msg);
+ }
+
+ /**
+ * Send a {@link #ERROR} log message and log the exception.
+ * @param tag Used to identify the source of a log message. It usually identfies
+ * the class or activity where the log call occurs.
+ * @param msg The message you would like logged.
+ * @param tr An exception to log
+ */
+ public static int e(String tag, String msg, Throwable tr) {
+ int r = println(ERROR, tag, msg + '\n' + getStackTraceString(tr));
+ RuntimeInit.reportException(tag, tr, false); // asynchronous
+ return r;
+ }
+
+ /**
+ * Handy function to get a loggable stack trace from a Throwable
+ * @param tr An exception to log
+ */
+ public static String getStackTraceString(Throwable tr) {
+ if (tr == null) {
+ return "";
+ }
+ StringWriter sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw);
+ tr.printStackTrace(pw);
+ return sw.toString();
+ }
+
+ /**
+ * Low-level logging call.
+ * @param priority The priority/type of this log message
+ * @param tag Used to identify the source of a log message. It usually identfies
+ * the class or activity where the log call occurs.
+ * @param msg The message you would like logged.
+ * @return The number of bytes written.
+ */
+ public static native int println(int priority, String tag, String msg);
+}
diff --git a/core/java/android/util/LogPrinter.java b/core/java/android/util/LogPrinter.java
new file mode 100644
index 0000000..643b8d3
--- /dev/null
+++ b/core/java/android/util/LogPrinter.java
@@ -0,0 +1,47 @@
+/*
+ * 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;
+
+/**
+ * Implementation of a {@link android.util.Printer} that sends its output
+ * to the system log.
+ */
+public class LogPrinter implements Printer {
+ private final int mPriority;
+ private final String mTag;
+
+ /**
+ * Create a new Printer that sends to the log with the given priority
+ * and tag.
+ *
+ * @param priority The desired log priority:
+ * {@link android.util.Log#VERBOSE Log.VERBOSE},
+ * {@link android.util.Log#DEBUG Log.DEBUG},
+ * {@link android.util.Log#INFO Log.INFO},
+ * {@link android.util.Log#WARN Log.WARN}, or
+ * {@link android.util.Log#ERROR Log.ERROR}.
+ * @param tag A string tag to associate with each printed log statement.
+ */
+ public LogPrinter(int priority, String tag) {
+ mPriority = priority;
+ mTag = tag;
+ }
+
+ public void println(String x) {
+ Log.println(mPriority, mTag, x);
+ }
+}
diff --git a/core/java/android/util/MonthDisplayHelper.java b/core/java/android/util/MonthDisplayHelper.java
new file mode 100644
index 0000000..c3f13fc
--- /dev/null
+++ b/core/java/android/util/MonthDisplayHelper.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import java.util.Calendar;
+
+/**
+ * Helps answer common questions that come up when displaying a month in a
+ * 6 row calendar grid format.
+ *
+ * Not thread safe.
+ */
+public class MonthDisplayHelper {
+
+ // display pref
+ private final int mWeekStartDay;
+
+ // holds current month, year, helps compute display
+ private Calendar mCalendar;
+
+ // cached computed stuff that helps with display
+ private int mNumDaysInMonth;
+ private int mNumDaysInPrevMonth;
+ private int mOffset;
+
+
+ /**
+ * @param year The year.
+ * @param month The month.
+ * @param weekStartDay What day of the week the week should start.
+ */
+ public MonthDisplayHelper(int year, int month, int weekStartDay) {
+
+ if (weekStartDay < Calendar.SUNDAY || weekStartDay > Calendar.SATURDAY) {
+ throw new IllegalArgumentException();
+ }
+ mWeekStartDay = weekStartDay;
+
+ mCalendar = Calendar.getInstance();
+ mCalendar.set(Calendar.YEAR, year);
+ mCalendar.set(Calendar.MONTH, month);
+ mCalendar.set(Calendar.DAY_OF_MONTH, 1);
+ mCalendar.set(Calendar.HOUR_OF_DAY, 0);
+ mCalendar.set(Calendar.MINUTE, 0);
+ mCalendar.set(Calendar.SECOND, 0);
+ mCalendar.getTimeInMillis();
+
+ recalculate();
+ }
+
+
+ public MonthDisplayHelper(int year, int month) {
+ this(year, month, Calendar.SUNDAY);
+ }
+
+
+ public int getYear() {
+ return mCalendar.get(Calendar.YEAR);
+ }
+
+ public int getMonth() {
+ return mCalendar.get(Calendar.MONTH);
+ }
+
+
+ public int getWeekStartDay() {
+ return mWeekStartDay;
+ }
+
+ /**
+ * @return The first day of the month using a constants such as
+ * {@link java.util.Calendar#SUNDAY}.
+ */
+ public int getFirstDayOfMonth() {
+ return mCalendar.get(Calendar.DAY_OF_WEEK);
+ }
+
+ /**
+ * @return The number of days in the month.
+ */
+ public int getNumberOfDaysInMonth() {
+ return mNumDaysInMonth;
+ }
+
+
+ /**
+ * @return The offset from displaying everything starting on the very first
+ * box. For example, if the calendar is set to display the first day of
+ * the week as Sunday, and the month starts on a Wednesday, the offset is 3.
+ */
+ public int getOffset() {
+ return mOffset;
+ }
+
+
+ /**
+ * @param row Which row (0-5).
+ * @return the digits of the month to display in one
+ * of the 6 rows of a calendar month display.
+ */
+ public int[] getDigitsForRow(int row) {
+ if (row < 0 || row > 5) {
+ throw new IllegalArgumentException("row " + row
+ + " out of range (0-5)");
+ }
+
+ int [] result = new int[7];
+ for (int column = 0; column < 7; column++) {
+ result[column] = getDayAt(row, column);
+ }
+
+ return result;
+ }
+
+ /**
+ * @param row The row, 0-5, starting from the top.
+ * @param column The column, 0-6, starting from the left.
+ * @return The day at a particular row, column
+ */
+ public int getDayAt(int row, int column) {
+
+ if (row == 0 && column < mOffset) {
+ return mNumDaysInPrevMonth + column - mOffset + 1;
+ }
+
+ int day = 7 * row + column - mOffset + 1;
+
+ return (day > mNumDaysInMonth) ?
+ day - mNumDaysInMonth : day;
+ }
+
+ /**
+ * @return Which row day is in.
+ */
+ public int getRowOf(int day) {
+ return (day + mOffset - 1) / 7;
+ }
+
+ /**
+ * @return Which column day is in.
+ */
+ public int getColumnOf(int day) {
+ return (day + mOffset - 1) % 7;
+ }
+
+ /**
+ * Decrement the month.
+ */
+ public void previousMonth() {
+ mCalendar.add(Calendar.MONTH, -1);
+ recalculate();
+ }
+
+ /**
+ * Increment the month.
+ */
+ public void nextMonth() {
+ mCalendar.add(Calendar.MONTH, 1);
+ recalculate();
+ }
+
+ /**
+ * @return Whether the row and column fall within the month.
+ */
+ public boolean isWithinCurrentMonth(int row, int column) {
+
+ if (row < 0 || column < 0 || row > 5 || column > 6) {
+ return false;
+ }
+
+ if (row == 0 && column < mOffset) {
+ return false;
+ }
+
+ int day = 7 * row + column - mOffset + 1;
+ if (day > mNumDaysInMonth) {
+ return false;
+ }
+ return true;
+ }
+
+
+ // helper method that recalculates cached values based on current month / year
+ private void recalculate() {
+
+ mNumDaysInMonth = mCalendar.getActualMaximum(Calendar.DAY_OF_MONTH);
+
+ mCalendar.add(Calendar.MONTH, -1);
+ mNumDaysInPrevMonth = mCalendar.getActualMaximum(Calendar.DAY_OF_MONTH);
+ mCalendar.add(Calendar.MONTH, 1);
+
+ int firstDayOfMonth = getFirstDayOfMonth();
+ int offset = firstDayOfMonth - mWeekStartDay;
+ if (offset < 0) {
+ offset += 7;
+ }
+ mOffset = offset;
+ }
+}
diff --git a/core/java/android/util/PrintWriterPrinter.java b/core/java/android/util/PrintWriterPrinter.java
new file mode 100644
index 0000000..82c4d03
--- /dev/null
+++ b/core/java/android/util/PrintWriterPrinter.java
@@ -0,0 +1,40 @@
+/*
+ * 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 java.io.PrintWriter;
+
+/**
+ * Implementation of a {@link android.util.Printer} that sends its output
+ * to a {@link java.io.PrintWriter}.
+ */
+public class PrintWriterPrinter implements Printer {
+ private final PrintWriter mPW;
+
+ /**
+ * Create a new Printer that sends to a PrintWriter object.
+ *
+ * @param pw The PrintWriter where you would like output to go.
+ */
+ public PrintWriterPrinter(PrintWriter pw) {
+ mPW = pw;
+ }
+
+ public void println(String x) {
+ mPW.println(x);
+ }
+}
diff --git a/core/java/android/util/Printer.java b/core/java/android/util/Printer.java
new file mode 100644
index 0000000..595cf70
--- /dev/null
+++ b/core/java/android/util/Printer.java
@@ -0,0 +1,31 @@
+/*
+ * 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;
+
+/**
+ * Simple interface for printing text, allowing redirection to various
+ * targets. Standard implementations are {@link android.util.LogPrinter},
+ * {@link android.util.StringBuilderPrinter}, and
+ * {@link android.util.PrintWriterPrinter}.
+ */
+public interface Printer {
+ /**
+ * Write a line of text to the output. There is no need to terminate
+ * the given string with a newline.
+ */
+ void println(String x);
+}
diff --git a/core/java/android/util/SparseArray.java b/core/java/android/util/SparseArray.java
new file mode 100644
index 0000000..1c8b330
--- /dev/null
+++ b/core/java/android/util/SparseArray.java
@@ -0,0 +1,340 @@
+/*
+ * 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 com.android.internal.util.ArrayUtils;
+
+/**
+ * SparseArrays map integers to Objects. Unlike a normal array of Objects,
+ * there can be gaps in the indices. It is intended to be more efficient
+ * than using a HashMap to map Integers to Objects.
+ */
+public class SparseArray<E> {
+ private static final Object DELETED = new Object();
+ private boolean mGarbage = false;
+
+ /**
+ * Creates a new SparseArray containing no mappings.
+ */
+ public SparseArray() {
+ this(10);
+ }
+
+ /**
+ * Creates a new SparseArray containing no mappings that will not
+ * require any additional memory allocation to store the specified
+ * number of mappings.
+ */
+ public SparseArray(int initialCapacity) {
+ initialCapacity = ArrayUtils.idealIntArraySize(initialCapacity);
+
+ mKeys = new int[initialCapacity];
+ mValues = new Object[initialCapacity];
+ mSize = 0;
+ }
+
+ /**
+ * Gets the Object mapped from the specified key, or <code>null</code>
+ * if no such mapping has been made.
+ */
+ public E get(int key) {
+ return get(key, null);
+ }
+
+ /**
+ * Gets the Object mapped from the specified key, or the specified Object
+ * if no such mapping has been made.
+ */
+ public E get(int key, E valueIfKeyNotFound) {
+ int i = binarySearch(mKeys, 0, mSize, key);
+
+ if (i < 0 || mValues[i] == DELETED) {
+ return valueIfKeyNotFound;
+ } else {
+ return (E) mValues[i];
+ }
+ }
+
+ /**
+ * Removes the mapping from the specified key, if there was any.
+ */
+ public void delete(int key) {
+ int i = binarySearch(mKeys, 0, mSize, key);
+
+ if (i >= 0) {
+ if (mValues[i] != DELETED) {
+ mValues[i] = DELETED;
+ mGarbage = true;
+ }
+ }
+ }
+
+ /**
+ * Alias for {@link #delete(int)}.
+ */
+ public void remove(int key) {
+ delete(key);
+ }
+
+ private void gc() {
+ // Log.e("SparseArray", "gc start with " + mSize);
+
+ int n = mSize;
+ int o = 0;
+ int[] keys = mKeys;
+ Object[] values = mValues;
+
+ for (int i = 0; i < n; i++) {
+ Object val = values[i];
+
+ if (val != DELETED) {
+ if (i != o) {
+ keys[o] = keys[i];
+ values[o] = val;
+ }
+
+ o++;
+ }
+ }
+
+ mGarbage = false;
+ mSize = o;
+
+ // Log.e("SparseArray", "gc end with " + mSize);
+ }
+
+ /**
+ * Adds a mapping from the specified key to the specified value,
+ * replacing the previous mapping from the specified key if there
+ * was one.
+ */
+ public void put(int key, E value) {
+ int i = binarySearch(mKeys, 0, mSize, key);
+
+ if (i >= 0) {
+ mValues[i] = value;
+ } else {
+ i = ~i;
+
+ if (i < mSize && mValues[i] == DELETED) {
+ mKeys[i] = key;
+ mValues[i] = value;
+ return;
+ }
+
+ if (mGarbage && mSize >= mKeys.length) {
+ gc();
+
+ // Search again because indices may have changed.
+ i = ~binarySearch(mKeys, 0, mSize, key);
+ }
+
+ if (mSize >= mKeys.length) {
+ int n = ArrayUtils.idealIntArraySize(mSize + 1);
+
+ int[] nkeys = new int[n];
+ Object[] nvalues = new Object[n];
+
+ // Log.e("SparseArray", "grow " + mKeys.length + " to " + n);
+ System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length);
+ System.arraycopy(mValues, 0, nvalues, 0, mValues.length);
+
+ mKeys = nkeys;
+ mValues = nvalues;
+ }
+
+ if (mSize - i != 0) {
+ // Log.e("SparseArray", "move " + (mSize - i));
+ System.arraycopy(mKeys, i, mKeys, i + 1, mSize - i);
+ System.arraycopy(mValues, i, mValues, i + 1, mSize - i);
+ }
+
+ mKeys[i] = key;
+ mValues[i] = value;
+ mSize++;
+ }
+ }
+
+ /**
+ * Returns the number of key-value mappings that this SparseArray
+ * currently stores.
+ */
+ public int size() {
+ if (mGarbage) {
+ gc();
+ }
+
+ return mSize;
+ }
+
+ /**
+ * Given an index in the range <code>0...size()-1</code>, returns
+ * the key from the <code>index</code>th key-value mapping that this
+ * SparseArray stores.
+ */
+ public int keyAt(int index) {
+ if (mGarbage) {
+ gc();
+ }
+
+ return mKeys[index];
+ }
+
+ /**
+ * Given an index in the range <code>0...size()-1</code>, returns
+ * the value from the <code>index</code>th key-value mapping that this
+ * SparseArray stores.
+ */
+ public E valueAt(int index) {
+ if (mGarbage) {
+ gc();
+ }
+
+ return (E) mValues[index];
+ }
+
+ /**
+ * Given an index in the range <code>0...size()-1</code>, sets a new
+ * value for the <code>index</code>th key-value mapping that this
+ * SparseArray stores.
+ */
+ public void setValueAt(int index, E value) {
+ if (mGarbage) {
+ gc();
+ }
+
+ mValues[index] = value;
+ }
+
+ /**
+ * Returns the index for which {@link #keyAt} would return the
+ * specified key, or a negative number if the specified
+ * key is not mapped.
+ */
+ public int indexOfKey(int key) {
+ if (mGarbage) {
+ gc();
+ }
+
+ return binarySearch(mKeys, 0, mSize, key);
+ }
+
+ /**
+ * Returns an index for which {@link #valueAt} would return the
+ * specified key, or a negative number if no keys map to the
+ * specified value.
+ * Beware that this is a linear search, unlike lookups by key,
+ * and that multiple keys can map to the same value and this will
+ * find only one of them.
+ */
+ public int indexOfValue(E value) {
+ if (mGarbage) {
+ gc();
+ }
+
+ for (int i = 0; i < mSize; i++)
+ if (mValues[i] == value)
+ return i;
+
+ return -1;
+ }
+
+ /**
+ * Removes all key-value mappings from this SparseArray.
+ */
+ public void clear() {
+ int n = mSize;
+ Object[] values = mValues;
+
+ for (int i = 0; i < n; i++) {
+ values[i] = null;
+ }
+
+ mSize = 0;
+ mGarbage = false;
+ }
+
+ /**
+ * Puts a key/value pair into the array, optimizing for the case where
+ * the key is greater than all existing keys in the array.
+ */
+ public void append(int key, E value) {
+ if (mSize != 0 && key <= mKeys[mSize - 1]) {
+ put(key, value);
+ return;
+ }
+
+ if (mGarbage && mSize >= mKeys.length) {
+ gc();
+ }
+
+ int pos = mSize;
+ if (pos >= mKeys.length) {
+ int n = ArrayUtils.idealIntArraySize(pos + 1);
+
+ int[] nkeys = new int[n];
+ Object[] nvalues = new Object[n];
+
+ // Log.e("SparseArray", "grow " + mKeys.length + " to " + n);
+ System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length);
+ System.arraycopy(mValues, 0, nvalues, 0, mValues.length);
+
+ mKeys = nkeys;
+ mValues = nvalues;
+ }
+
+ mKeys[pos] = key;
+ mValues[pos] = value;
+ mSize = pos + 1;
+ }
+
+ private static int binarySearch(int[] a, int start, int len, int key) {
+ int high = start + len, low = start - 1, guess;
+
+ while (high - low > 1) {
+ guess = (high + low) / 2;
+
+ if (a[guess] < key)
+ low = guess;
+ else
+ high = guess;
+ }
+
+ if (high == start + len)
+ return ~(start + len);
+ else if (a[high] == key)
+ return high;
+ else
+ return ~high;
+ }
+
+ private void checkIntegrity() {
+ for (int i = 1; i < mSize; i++) {
+ if (mKeys[i] <= mKeys[i - 1]) {
+ for (int j = 0; j < mSize; j++) {
+ Log.e("FAIL", j + ": " + mKeys[j] + " -> " + mValues[j]);
+ }
+
+ throw new RuntimeException();
+ }
+ }
+ }
+
+ private int[] mKeys;
+ private Object[] mValues;
+ private int mSize;
+}
diff --git a/core/java/android/util/SparseBooleanArray.java b/core/java/android/util/SparseBooleanArray.java
new file mode 100644
index 0000000..f7799de
--- /dev/null
+++ b/core/java/android/util/SparseBooleanArray.java
@@ -0,0 +1,245 @@
+/*
+ * 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 com.android.internal.util.ArrayUtils;
+
+/**
+ * SparseBooleanArrays map integers to booleans.
+ * Unlike a normal array of booleans
+ * there can be gaps in the indices. It is intended to be more efficient
+ * than using a HashMap to map Integers to Booleans.
+ */
+public class SparseBooleanArray {
+ /**
+ * Creates a new SparseBooleanArray containing no mappings.
+ */
+ public SparseBooleanArray() {
+ this(10);
+ }
+
+ /**
+ * Creates a new SparseBooleanArray containing no mappings that will not
+ * require any additional memory allocation to store the specified
+ * number of mappings.
+ */
+ public SparseBooleanArray(int initialCapacity) {
+ initialCapacity = ArrayUtils.idealIntArraySize(initialCapacity);
+
+ mKeys = new int[initialCapacity];
+ mValues = new boolean[initialCapacity];
+ mSize = 0;
+ }
+
+ /**
+ * Gets the boolean mapped from the specified key, or <code>false</code>
+ * if no such mapping has been made.
+ */
+ public boolean get(int key) {
+ return get(key, false);
+ }
+
+ /**
+ * Gets the boolean mapped from the specified key, or the specified value
+ * if no such mapping has been made.
+ */
+ public boolean get(int key, boolean 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) {
+ System.arraycopy(mKeys, i + 1, mKeys, i, mSize - (i + 1));
+ System.arraycopy(mValues, i + 1, mValues, i, mSize - (i + 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, boolean value) {
+ int i = binarySearch(mKeys, 0, mSize, key);
+
+ if (i >= 0) {
+ mValues[i] = value;
+ } else {
+ i = ~i;
+
+ if (mSize >= mKeys.length) {
+ int n = ArrayUtils.idealIntArraySize(mSize + 1);
+
+ int[] nkeys = new int[n];
+ boolean[] nvalues = new boolean[n];
+
+ // Log.e("SparseBooleanArray", "grow " + mKeys.length + " to " + n);
+ System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length);
+ System.arraycopy(mValues, 0, nvalues, 0, mValues.length);
+
+ mKeys = nkeys;
+ mValues = nvalues;
+ }
+
+ if (mSize - i != 0) {
+ // Log.e("SparseBooleanArray", "move " + (mSize - i));
+ System.arraycopy(mKeys, i, mKeys, i + 1, mSize - i);
+ System.arraycopy(mValues, i, mValues, i + 1, mSize - i);
+ }
+
+ mKeys[i] = key;
+ mValues[i] = value;
+ mSize++;
+ }
+ }
+
+ /**
+ * Returns the number of key-value mappings that this SparseBooleanArray
+ * 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
+ * SparseBooleanArray 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
+ * SparseBooleanArray stores.
+ */
+ public boolean 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(boolean value) {
+ for (int i = 0; i < mSize; i++)
+ if (mValues[i] == value)
+ return i;
+
+ return -1;
+ }
+
+ /**
+ * Removes all key-value mappings from this SparseBooleanArray.
+ */
+ 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, boolean value) {
+ if (mSize != 0 && key <= mKeys[mSize - 1]) {
+ put(key, value);
+ return;
+ }
+
+ int pos = mSize;
+ if (pos >= mKeys.length) {
+ int n = ArrayUtils.idealIntArraySize(pos + 1);
+
+ int[] nkeys = new int[n];
+ boolean[] nvalues = new boolean[n];
+
+ // Log.e("SparseBooleanArray", "grow " + mKeys.length + " to " + n);
+ System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length);
+ System.arraycopy(mValues, 0, nvalues, 0, mValues.length);
+
+ mKeys = nkeys;
+ mValues = nvalues;
+ }
+
+ mKeys[pos] = key;
+ mValues[pos] = value;
+ mSize = pos + 1;
+ }
+
+ private static int binarySearch(int[] a, int start, int len, int key) {
+ int high = start + len, low = start - 1, guess;
+
+ while (high - low > 1) {
+ guess = (high + low) / 2;
+
+ if (a[guess] < key)
+ low = guess;
+ else
+ high = guess;
+ }
+
+ if (high == start + len)
+ return ~(start + len);
+ else if (a[high] == key)
+ return high;
+ else
+ return ~high;
+ }
+
+ private void checkIntegrity() {
+ for (int i = 1; i < mSize; i++) {
+ if (mKeys[i] <= mKeys[i - 1]) {
+ for (int j = 0; j < mSize; j++) {
+ Log.e("FAIL", j + ": " + mKeys[j] + " -> " + mValues[j]);
+ }
+
+ throw new RuntimeException();
+ }
+ }
+ }
+
+ private int[] mKeys;
+ private boolean[] mValues;
+ private int mSize;
+}
diff --git a/core/java/android/util/SparseIntArray.java b/core/java/android/util/SparseIntArray.java
new file mode 100644
index 0000000..610cfd4
--- /dev/null
+++ b/core/java/android/util/SparseIntArray.java
@@ -0,0 +1,244 @@
+/*
+ * 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 com.android.internal.util.ArrayUtils;
+
+/**
+ * SparseIntArrays map integers to integers. Unlike a normal array of integers,
+ * there can be gaps in the indices. It is intended to be more efficient
+ * than using a HashMap to map Integers to Integers.
+ */
+public class SparseIntArray {
+ /**
+ * Creates a new SparseIntArray containing no mappings.
+ */
+ public SparseIntArray() {
+ this(10);
+ }
+
+ /**
+ * Creates a new SparseIntArray containing no mappings that will not
+ * require any additional memory allocation to store the specified
+ * number of mappings.
+ */
+ public SparseIntArray(int initialCapacity) {
+ initialCapacity = ArrayUtils.idealIntArraySize(initialCapacity);
+
+ mKeys = new int[initialCapacity];
+ mValues = new int[initialCapacity];
+ mSize = 0;
+ }
+
+ /**
+ * Gets the int mapped from the specified key, or <code>0</code>
+ * if no such mapping has been made.
+ */
+ public int get(int key) {
+ return get(key, 0);
+ }
+
+ /**
+ * Gets the int mapped from the specified key, or the specified value
+ * if no such mapping has been made.
+ */
+ public int get(int key, int 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) {
+ System.arraycopy(mKeys, i + 1, mKeys, i, mSize - (i + 1));
+ System.arraycopy(mValues, i + 1, mValues, i, mSize - (i + 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, int value) {
+ int i = binarySearch(mKeys, 0, mSize, key);
+
+ if (i >= 0) {
+ mValues[i] = value;
+ } else {
+ i = ~i;
+
+ if (mSize >= mKeys.length) {
+ int n = ArrayUtils.idealIntArraySize(mSize + 1);
+
+ int[] nkeys = new int[n];
+ int[] nvalues = new int[n];
+
+ // Log.e("SparseIntArray", "grow " + mKeys.length + " to " + n);
+ System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length);
+ System.arraycopy(mValues, 0, nvalues, 0, mValues.length);
+
+ mKeys = nkeys;
+ mValues = nvalues;
+ }
+
+ if (mSize - i != 0) {
+ // Log.e("SparseIntArray", "move " + (mSize - i));
+ System.arraycopy(mKeys, i, mKeys, i + 1, mSize - i);
+ System.arraycopy(mValues, i, mValues, i + 1, mSize - i);
+ }
+
+ mKeys[i] = key;
+ mValues[i] = value;
+ mSize++;
+ }
+ }
+
+ /**
+ * Returns the number of key-value mappings that this 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
+ * SparseIntArray 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
+ * SparseIntArray stores.
+ */
+ public int 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(int 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, int value) {
+ if (mSize != 0 && key <= mKeys[mSize - 1]) {
+ put(key, value);
+ return;
+ }
+
+ int pos = mSize;
+ if (pos >= mKeys.length) {
+ int n = ArrayUtils.idealIntArraySize(pos + 1);
+
+ int[] nkeys = new int[n];
+ int[] nvalues = new int[n];
+
+ // Log.e("SparseIntArray", "grow " + mKeys.length + " to " + n);
+ System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length);
+ System.arraycopy(mValues, 0, nvalues, 0, mValues.length);
+
+ mKeys = nkeys;
+ mValues = nvalues;
+ }
+
+ mKeys[pos] = key;
+ mValues[pos] = value;
+ mSize = pos + 1;
+ }
+
+ private static int binarySearch(int[] a, int start, int len, int key) {
+ int high = start + len, low = start - 1, guess;
+
+ while (high - low > 1) {
+ guess = (high + low) / 2;
+
+ if (a[guess] < key)
+ low = guess;
+ else
+ high = guess;
+ }
+
+ if (high == start + len)
+ return ~(start + len);
+ else if (a[high] == key)
+ return high;
+ else
+ return ~high;
+ }
+
+ private void checkIntegrity() {
+ for (int i = 1; i < mSize; i++) {
+ if (mKeys[i] <= mKeys[i - 1]) {
+ for (int j = 0; j < mSize; j++) {
+ Log.e("FAIL", j + ": " + mKeys[j] + " -> " + mValues[j]);
+ }
+
+ throw new RuntimeException();
+ }
+ }
+ }
+
+ private int[] mKeys;
+ private int[] mValues;
+ private int mSize;
+}
diff --git a/core/java/android/util/StateSet.java b/core/java/android/util/StateSet.java
new file mode 100644
index 0000000..f3d8159
--- /dev/null
+++ b/core/java/android/util/StateSet.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import com.android.internal.R;
+
+/**
+ * State sets are arrays of positive ints where each element
+ * represents the state of a {@link android.view.View} (e.g. focused,
+ * selected, visible, etc.). A {@link android.view.View} may be in
+ * one or more of those states.
+ *
+ * A state spec is an array of signed ints where each element
+ * represents a required (if positive) or an undesired (if negative)
+ * {@link android.view.View} state.
+ *
+ * Utils dealing with state sets.
+ *
+ * In theory we could encapsulate the state set and state spec arrays
+ * and not have static methods here but there is some concern about
+ * performance since these methods are called during view drawing.
+ */
+
+public class StateSet {
+
+ public static final int[] WILD_CARD = new int[0];
+
+ /**
+ * Return whether the stateSetOrSpec is matched by all StateSets.
+ *
+ * @param stateSetOrSpec a state set or state spec.
+ */
+ public static boolean isWildCard(int[] stateSetOrSpec) {
+ return stateSetOrSpec.length == 0 || stateSetOrSpec[0] == 0;
+ }
+
+ /**
+ * Return whether the stateSet matches the desired stateSpec.
+ *
+ * @param stateSpec an array of required (if positive) or
+ * prohibited (if negative) {@link android.view.View} states.
+ * @param stateSet an array of {@link android.view.View} states
+ */
+ public static boolean stateSetMatches(int[] stateSpec, int[] stateSet) {
+ if (stateSet == null) {
+ return (stateSpec == null || isWildCard(stateSpec));
+ }
+ int stateSpecSize = stateSpec.length;
+ int stateSetSize = stateSet.length;
+ for (int i = 0; i < stateSpecSize; i++) {
+ int stateSpecState = stateSpec[i];
+ if (stateSpecState == 0) {
+ // We've reached the end of the cases to match against.
+ return true;
+ }
+ final boolean mustMatch;
+ if (stateSpecState > 0) {
+ mustMatch = true;
+ } else {
+ // We use negative values to indicate must-NOT-match states.
+ mustMatch = false;
+ stateSpecState = -stateSpecState;
+ }
+ boolean found = false;
+ for (int j = 0; j < stateSetSize; j++) {
+ final int state = stateSet[j];
+ if (state == 0) {
+ // We've reached the end of states to match.
+ if (mustMatch) {
+ // We didn't find this must-match state.
+ return false;
+ } else {
+ // Continue checking other must-not-match states.
+ break;
+ }
+ }
+ if (state == stateSpecState) {
+ if (mustMatch) {
+ found = true;
+ // Continue checking other other must-match states.
+ break;
+ } else {
+ // Any match of a must-not-match state returns false.
+ return false;
+ }
+ }
+ }
+ if (mustMatch && !found) {
+ // We've reached the end of states to match and we didn't
+ // find a must-match state.
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Return whether the state matches the desired stateSpec.
+ *
+ * @param stateSpec an array of required (if positive) or
+ * prohibited (if negative) {@link android.view.View} states.
+ * @param state a {@link android.view.View} state
+ */
+ public static boolean stateSetMatches(int[] stateSpec, int state) {
+ int stateSpecSize = stateSpec.length;
+ for (int i = 0; i < stateSpecSize; i++) {
+ int stateSpecState = stateSpec[i];
+ if (stateSpecState == 0) {
+ // We've reached the end of the cases to match against.
+ return true;
+ }
+ if (stateSpecState > 0) {
+ if (state != stateSpecState) {
+ return false;
+ }
+ } else {
+ // We use negative values to indicate must-NOT-match states.
+ if (state == -stateSpecState) {
+ // We matched a must-not-match case.
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ public static int[] trimStateSet(int[] states, int newSize) {
+ if (states.length == newSize) {
+ return states;
+ }
+
+ int[] trimmedStates = new int[newSize];
+ System.arraycopy(states, 0, trimmedStates, 0, newSize);
+ return trimmedStates;
+ }
+
+ public static String dump(int[] states) {
+ StringBuilder sb = new StringBuilder();
+
+ int count = states.length;
+ for (int i = 0; i < count; i++) {
+
+ switch (states[i]) {
+ case R.attr.state_window_focused:
+ sb.append("W ");
+ break;
+ case R.attr.state_pressed:
+ sb.append("P ");
+ break;
+ case R.attr.state_selected:
+ sb.append("S ");
+ break;
+ case R.attr.state_focused:
+ sb.append("F ");
+ break;
+ case R.attr.state_enabled:
+ sb.append("E ");
+ break;
+ }
+ }
+
+ return sb.toString();
+ }
+}
diff --git a/core/java/android/util/StringBuilderPrinter.java b/core/java/android/util/StringBuilderPrinter.java
new file mode 100644
index 0000000..d0fc1e7
--- /dev/null
+++ b/core/java/android/util/StringBuilderPrinter.java
@@ -0,0 +1,42 @@
+/*
+ * 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;
+
+/**
+ * Implementation of a {@link android.util.Printer} that sends its output
+ * to a {@link StringBuilder}.
+ */
+public class StringBuilderPrinter implements Printer {
+ private final StringBuilder mBuilder;
+
+ /**
+ * Create a new Printer that sends to a StringBuilder object.
+ *
+ * @param builder The StringBuilder where you would like output to go.
+ */
+ public StringBuilderPrinter(StringBuilder builder) {
+ mBuilder = builder;
+ }
+
+ public void println(String x) {
+ mBuilder.append(x);
+ int len = x.length();
+ if (len <= 0 || x.charAt(len-1) != '\n') {
+ mBuilder.append('\n');
+ }
+ }
+}
diff --git a/core/java/android/util/TimeFormatException.java b/core/java/android/util/TimeFormatException.java
new file mode 100644
index 0000000..d7a898b
--- /dev/null
+++ b/core/java/android/util/TimeFormatException.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+public class TimeFormatException extends RuntimeException
+{
+ TimeFormatException(String s)
+ {
+ super(s);
+ }
+}
+
diff --git a/core/java/android/util/TimeUtils.java b/core/java/android/util/TimeUtils.java
new file mode 100644
index 0000000..3c4e337
--- /dev/null
+++ b/core/java/android/util/TimeUtils.java
@@ -0,0 +1,109 @@
+/*
+ * 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.content.res.Resources;
+import android.content.res.XmlResourceParser;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.TimeZone;
+import java.util.Date;
+
+import com.android.internal.util.XmlUtils;
+
+public class TimeUtils {
+ /**
+ * Tries to return a time zone that would have had the specified offset
+ * and DST value at the specified moment in the specified country.
+ * Returns null if no suitable zone could be found.
+ */
+ public static TimeZone getTimeZone(int offset, boolean dst, long when,
+ String country) {
+ if (country == null) {
+ return null;
+ }
+
+ TimeZone best = null;
+
+ Resources r = Resources.getSystem();
+ XmlResourceParser parser = r.getXml(com.android.internal.R.xml.time_zones_by_country);
+ Date d = new Date(when);
+
+ TimeZone current = TimeZone.getDefault();
+ String currentName = current.getID();
+ int currentOffset = current.getOffset(when);
+ boolean currentDst = current.inDaylightTime(d);
+
+ try {
+ XmlUtils.beginDocument(parser, "timezones");
+
+ while (true) {
+ XmlUtils.nextElement(parser);
+
+ String element = parser.getName();
+ if (element == null || !(element.equals("timezone"))) {
+ break;
+ }
+
+ String code = parser.getAttributeValue(null, "code");
+
+ if (country.equals(code)) {
+ if (parser.next() == XmlPullParser.TEXT) {
+ String maybe = parser.getText();
+
+ // If the current time zone is from the right country
+ // and meets the other known properties, keep it
+ // instead of changing to another one.
+
+ if (maybe.equals(currentName)) {
+ if (currentOffset == offset && currentDst == dst) {
+ return current;
+ }
+ }
+
+ // Otherwise, take the first zone from the right
+ // country that has the correct current offset and DST.
+ // (Keep iterating instead of returning in case we
+ // haven't encountered the current time zone yet.)
+
+ if (best == null) {
+ TimeZone tz = TimeZone.getTimeZone(maybe);
+
+ if (tz.getOffset(when) == offset &&
+ tz.inDaylightTime(d) == dst) {
+ best = tz;
+ }
+ }
+ }
+ }
+ }
+ } catch (XmlPullParserException e) {
+ Log.e("TimeUtils",
+ "Got exception while getting preferred time zone.", e);
+ } catch (IOException e) {
+ Log.e("TimeUtils",
+ "Got exception while getting preferred time zone.", e);
+ } finally {
+ parser.close();
+ }
+
+ return best;
+ }
+}
diff --git a/core/java/android/util/TimingLogger.java b/core/java/android/util/TimingLogger.java
new file mode 100644
index 0000000..0f39c97
--- /dev/null
+++ b/core/java/android/util/TimingLogger.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import java.util.ArrayList;
+
+import android.os.SystemClock;
+
+/**
+ * A utility class to help log timings splits throughout a method call.
+ * Typical usage is:
+ *
+ * TimingLogger timings = new TimingLogger(TAG, "methodA");
+ * ... do some work A ...
+ * timings.addSplit("work A");
+ * ... do some work B ...
+ * timings.addSplit("work B");
+ * ... do some work C ...
+ * timings.addSplit("work C");
+ * timings.dumpToLog();
+ *
+ * The dumpToLog call would add the following to the log:
+ *
+ * D/TAG ( 3459): methodA: begin
+ * D/TAG ( 3459): methodA: 9 ms, work A
+ * D/TAG ( 3459): methodA: 1 ms, work B
+ * D/TAG ( 3459): methodA: 6 ms, work C
+ * D/TAG ( 3459): methodA: end, 16 ms
+ */
+public class TimingLogger {
+
+ /**
+ * The Log tag to use for checking Log.isLoggable and for
+ * logging the timings.
+ */
+ private String mTag;
+
+ /** A label to be included in every log. */
+ private String mLabel;
+
+ /** Used to track whether Log.isLoggable was enabled at reset time. */
+ private boolean mDisabled;
+
+ /** Stores the time of each split. */
+ ArrayList<Long> mSplits;
+
+ /** Stores the labels for each split. */
+ ArrayList<String> mSplitLabels;
+
+ /**
+ * Create and initialize a TimingLogger object that will log using
+ * the specific tag. If the Log.isLoggable is not enabled to at
+ * least the Log.VERBOSE level for that tag at creation time then
+ * the addSplit and dumpToLog call will do nothing.
+ * @param tag the log tag to use while logging the timings
+ * @param label a string to be displayed with each log
+ */
+ public TimingLogger(String tag, String label) {
+ reset(tag, label);
+ }
+
+ /**
+ * Clear and initialize a TimingLogger object that will log using
+ * the specific tag. If the Log.isLoggable is not enabled to at
+ * least the Log.VERBOSE level for that tag at creation time then
+ * the addSplit and dumpToLog call will do nothing.
+ * @param tag the log tag to use while logging the timings
+ * @param label a string to be displayed with each log
+ */
+ public void reset(String tag, String label) {
+ mTag = tag;
+ mLabel = label;
+ reset();
+ }
+
+ /**
+ * Clear and initialize a TimingLogger object that will log using
+ * the tag and label that was specified previously, either via
+ * the constructor or a call to reset(tag, label). If the
+ * Log.isLoggable is not enabled to at least the Log.VERBOSE
+ * level for that tag at creation time then the addSplit and
+ * dumpToLog call will do nothing.
+ */
+ public void reset() {
+ mDisabled = !Log.isLoggable(mTag, Log.VERBOSE);
+ if (mDisabled) return;
+ if (mSplits == null) {
+ mSplits = new ArrayList<Long>();
+ mSplitLabels = new ArrayList<String>();
+ } else {
+ mSplits.clear();
+ mSplitLabels.clear();
+ }
+ addSplit(null);
+ }
+
+ /**
+ * Add a split for the current time, labeled with splitLabel. If
+ * Log.isLoggable was not enabled to at least the Log.VERBOSE for
+ * the specified tag at construction or reset() time then this
+ * call does nothing.
+ * @param splitLabel a label to associate with this split.
+ */
+ public void addSplit(String splitLabel) {
+ if (mDisabled) return;
+ long now = SystemClock.elapsedRealtime();
+ mSplits.add(now);
+ mSplitLabels.add(splitLabel);
+ }
+
+ /**
+ * Dumps the timings to the log using Log.d(). If Log.isLoggable was
+ * not enabled to at least the Log.VERBOSE for the specified tag at
+ * construction or reset() time then this call does nothing.
+ */
+ public void dumpToLog() {
+ if (mDisabled) return;
+ Log.d(mTag, mLabel + ": begin");
+ final long first = mSplits.get(0);
+ long now = first;
+ for (int i = 1; i < mSplits.size(); i++) {
+ now = mSplits.get(i);
+ final String splitLabel = mSplitLabels.get(i);
+ final long prev = mSplits.get(i - 1);
+
+ Log.d(mTag, mLabel + ": " + (now - prev) + " ms, " + splitLabel);
+ }
+ Log.d(mTag, mLabel + ": end, " + (now - first) + " ms");
+ }
+}
diff --git a/core/java/android/util/TypedValue.java b/core/java/android/util/TypedValue.java
new file mode 100644
index 0000000..a4ee35a
--- /dev/null
+++ b/core/java/android/util/TypedValue.java
@@ -0,0 +1,477 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.util.Config;
+import android.util.Log;
+
+/**
+ * Container for a dynamically typed data value. Primarily used with
+ * {@link android.content.res.Resources} for holding resource values.
+ */
+public class TypedValue {
+ /** The value contains no data. */
+ public static final int TYPE_NULL = 0x00;
+
+ /** The <var>data</var> field holds a resource identifier. */
+ public static final int TYPE_REFERENCE = 0x01;
+ /** The <var>data</var> field holds an attribute resource
+ * identifier (referencing an attribute in the current theme
+ * style, not a resource entry). */
+ public static final int TYPE_ATTRIBUTE = 0x02;
+ /** The <var>string</var> field holds string data. In addition, if
+ * <var>data</var> is non-zero then it is the string block
+ * index of the string and <var>assetCookie</var> is the set of
+ * assets the string came from. */
+ public static final int TYPE_STRING = 0x03;
+ /** The <var>data</var> field holds an IEEE 754 floating point number. */
+ public static final int TYPE_FLOAT = 0x04;
+ /** The <var>data</var> field holds a complex number encoding a
+ * dimension value. */
+ public static final int TYPE_DIMENSION = 0x05;
+ /** The <var>data</var> field holds a complex number encoding a fraction
+ * of a container. */
+ public static final int TYPE_FRACTION = 0x06;
+
+ /** Identifies the start of plain integer values. Any type value
+ * from this to {@link #TYPE_LAST_INT} means the
+ * <var>data</var> field holds a generic integer value. */
+ public static final int TYPE_FIRST_INT = 0x10;
+
+ /** The <var>data</var> field holds a number that was
+ * originally specified in decimal. */
+ public static final int TYPE_INT_DEC = 0x10;
+ /** The <var>data</var> field holds a number that was
+ * originally specified in hexadecimal (0xn). */
+ public static final int TYPE_INT_HEX = 0x11;
+ /** The <var>data</var> field holds 0 or 1 that was originally
+ * specified as "false" or "true". */
+ public static final int TYPE_INT_BOOLEAN = 0x12;
+
+ /** Identifies the start of integer values that were specified as
+ * color constants (starting with '#'). */
+ public static final int TYPE_FIRST_COLOR_INT = 0x1c;
+
+ /** The <var>data</var> field holds a color that was originally
+ * specified as #aarrggbb. */
+ public static final int TYPE_INT_COLOR_ARGB8 = 0x1c;
+ /** The <var>data</var> field holds a color that was originally
+ * specified as #rrggbb. */
+ public static final int TYPE_INT_COLOR_RGB8 = 0x1d;
+ /** The <var>data</var> field holds a color that was originally
+ * specified as #argb. */
+ public static final int TYPE_INT_COLOR_ARGB4 = 0x1e;
+ /** The <var>data</var> field holds a color that was originally
+ * specified as #rgb. */
+ public static final int TYPE_INT_COLOR_RGB4 = 0x1f;
+
+ /** Identifies the end of integer values that were specified as color
+ * constants. */
+ public static final int TYPE_LAST_COLOR_INT = 0x1f;
+
+ /** Identifies the end of plain integer values. */
+ public static final int TYPE_LAST_INT = 0x1f;
+
+ /* ------------------------------------------------------------ */
+
+ /** Complex data: bit location of unit information. */
+ public static final int COMPLEX_UNIT_SHIFT = 0;
+ /** Complex data: mask to extract unit information (after shifting by
+ * {@link #COMPLEX_UNIT_SHIFT}). This gives us 16 possible types, as
+ * defined below. */
+ public static final int COMPLEX_UNIT_MASK = 0xf;
+
+ /** {@link #TYPE_DIMENSION} complex unit: Value is raw pixels. */
+ public static final int COMPLEX_UNIT_PX = 0;
+ /** {@link #TYPE_DIMENSION} complex unit: Value is Device Independent
+ * Pixels. */
+ public static final int COMPLEX_UNIT_DIP = 1;
+ /** {@link #TYPE_DIMENSION} complex unit: Value is a scaled pixel. */
+ public static final int COMPLEX_UNIT_SP = 2;
+ /** {@link #TYPE_DIMENSION} complex unit: Value is in points. */
+ public static final int COMPLEX_UNIT_PT = 3;
+ /** {@link #TYPE_DIMENSION} complex unit: Value is in inches. */
+ public static final int COMPLEX_UNIT_IN = 4;
+ /** {@link #TYPE_DIMENSION} complex unit: Value is in millimeters. */
+ public static final int COMPLEX_UNIT_MM = 5;
+
+ /** {@link #TYPE_FRACTION} complex unit: A basic fraction of the overall
+ * size. */
+ public static final int COMPLEX_UNIT_FRACTION = 0;
+ /** {@link #TYPE_FRACTION} complex unit: A fraction of the parent size. */
+ public static final int COMPLEX_UNIT_FRACTION_PARENT = 1;
+
+ /** Complex data: where the radix information is, telling where the decimal
+ * place appears in the mantissa. */
+ public static final int COMPLEX_RADIX_SHIFT = 4;
+ /** Complex data: mask to extract radix information (after shifting by
+ * {@link #COMPLEX_RADIX_SHIFT}). This give us 4 possible fixed point
+ * representations as defined below. */
+ public static final int COMPLEX_RADIX_MASK = 0x3;
+
+ /** Complex data: the mantissa is an integral number -- i.e., 0xnnnnnn.0 */
+ public static final int COMPLEX_RADIX_23p0 = 0;
+ /** Complex data: the mantissa magnitude is 16 bits -- i.e, 0xnnnn.nn */
+ public static final int COMPLEX_RADIX_16p7 = 1;
+ /** Complex data: the mantissa magnitude is 8 bits -- i.e, 0xnn.nnnn */
+ public static final int COMPLEX_RADIX_8p15 = 2;
+ /** Complex data: the mantissa magnitude is 0 bits -- i.e, 0x0.nnnnnn */
+ public static final int COMPLEX_RADIX_0p23 = 3;
+
+ /** Complex data: bit location of mantissa information. */
+ public static final int COMPLEX_MANTISSA_SHIFT = 8;
+ /** Complex data: mask to extract mantissa information (after shifting by
+ * {@link #COMPLEX_MANTISSA_SHIFT}). This gives us 23 bits of precision;
+ * the top bit is the sign. */
+ public static final int COMPLEX_MANTISSA_MASK = 0xffffff;
+
+ /* ------------------------------------------------------------ */
+
+ /** The type held by this value, as defined by the constants here.
+ * This tells you how to interpret the other fields in the object. */
+ public int type;
+
+ /** If the value holds a string, this is it. */
+ public CharSequence string;
+
+ /** Basic data in the value, interpreted according to {@link #type} */
+ public int data;
+
+ /** Additional information about where the value came from; only
+ * set for strings. */
+ public int assetCookie;
+
+ /** If Value came from a resource, this holds the corresponding resource id. */
+ public int resourceId;
+
+ /** If Value came from a resource, these are the configurations for which
+ * its contents can change. */
+ public int changingConfigurations = -1;
+
+ /* ------------------------------------------------------------ */
+
+ /** Return the data for this value as a float. Only use for values
+ * whose type is {@link #TYPE_FLOAT}. */
+ public final float getFloat() {
+ return Float.intBitsToFloat(data);
+ }
+
+ private static final float MANTISSA_MULT =
+ 1.0f / (1<<TypedValue.COMPLEX_MANTISSA_SHIFT);
+ private static final float[] RADIX_MULTS = new float[] {
+ 1.0f*MANTISSA_MULT, 1.0f/(1<<7)*MANTISSA_MULT,
+ 1.0f/(1<<15)*MANTISSA_MULT, 1.0f/(1<<23)*MANTISSA_MULT
+ };
+
+ /**
+ * Retrieve the base value from a complex data integer. This uses the
+ * {@link #COMPLEX_MANTISSA_MASK} and {@link #COMPLEX_RADIX_MASK} fields of
+ * the data to compute a floating point representation of the number they
+ * describe. The units are ignored.
+ *
+ * @param complex A complex data value.
+ *
+ * @return A floating point value corresponding to the complex data.
+ */
+ public static float complexToFloat(int complex)
+ {
+ return (complex&(TypedValue.COMPLEX_MANTISSA_MASK
+ <<TypedValue.COMPLEX_MANTISSA_SHIFT))
+ * RADIX_MULTS[(complex>>TypedValue.COMPLEX_RADIX_SHIFT)
+ & TypedValue.COMPLEX_RADIX_MASK];
+ }
+
+ /**
+ * Converts a complex data value holding a dimension to its final floating
+ * point value. The given <var>data</var> must be structured as a
+ * {@link #TYPE_DIMENSION}.
+ *
+ * @param data A complex data value holding a unit, magnitude, and
+ * mantissa.
+ * @param metrics Current display metrics to use in the conversion --
+ * supplies display density and scaling information.
+ *
+ * @return The complex floating point value multiplied by the appropriate
+ * metrics depending on its unit.
+ */
+ public static float complexToDimension(int data, DisplayMetrics metrics)
+ {
+ return applyDimension(
+ (data>>COMPLEX_UNIT_SHIFT)&COMPLEX_UNIT_MASK,
+ complexToFloat(data),
+ metrics);
+ }
+
+ /**
+ * Converts a complex data value holding a dimension to its final value
+ * as an integer pixel offset. This is the same as
+ * {@link #complexToDimension}, except the raw floating point value is
+ * truncated to an integer (pixel) value.
+ * The given <var>data</var> must be structured as a
+ * {@link #TYPE_DIMENSION}.
+ *
+ * @param data A complex data value holding a unit, magnitude, and
+ * mantissa.
+ * @param metrics Current display metrics to use in the conversion --
+ * supplies display density and scaling information.
+ *
+ * @return The number of pixels specified by the data and its desired
+ * multiplier and units.
+ */
+ public static int complexToDimensionPixelOffset(int data,
+ DisplayMetrics metrics)
+ {
+ return (int)applyDimension(
+ (data>>COMPLEX_UNIT_SHIFT)&COMPLEX_UNIT_MASK,
+ complexToFloat(data),
+ metrics);
+ }
+
+ /**
+ * Converts a complex data value holding a dimension to its final value
+ * as an integer pixel size. This is the same as
+ * {@link #complexToDimension}, except the raw floating point value is
+ * converted to an integer (pixel) value for use as a size. A size
+ * conversion involves rounding the base value, and ensuring that a
+ * non-zero base value is at least one pixel in size.
+ * The given <var>data</var> must be structured as a
+ * {@link #TYPE_DIMENSION}.
+ *
+ * @param data A complex data value holding a unit, magnitude, and
+ * mantissa.
+ * @param metrics Current display metrics to use in the conversion --
+ * supplies display density and scaling information.
+ *
+ * @return The number of pixels specified by the data and its desired
+ * multiplier and units.
+ */
+ public static int complexToDimensionPixelSize(int data,
+ DisplayMetrics metrics)
+ {
+ final float value = complexToFloat(data);
+ final float f = applyDimension(
+ (data>>COMPLEX_UNIT_SHIFT)&COMPLEX_UNIT_MASK,
+ value,
+ metrics);
+ final int res = (int)(f+0.5f);
+ if (res != 0) return res;
+ if (value == 0) return 0;
+ if (value > 0) return 1;
+ return -1;
+ }
+
+ public static float complexToDimensionNoisy(int data, DisplayMetrics metrics)
+ {
+ float res = complexToDimension(data, metrics);
+ System.out.println(
+ "Dimension (0x" + ((data>>TypedValue.COMPLEX_MANTISSA_SHIFT)
+ & TypedValue.COMPLEX_MANTISSA_MASK)
+ + "*" + (RADIX_MULTS[(data>>TypedValue.COMPLEX_RADIX_SHIFT)
+ & TypedValue.COMPLEX_RADIX_MASK] / MANTISSA_MULT)
+ + ")" + DIMENSION_UNIT_STRS[(data>>COMPLEX_UNIT_SHIFT)
+ & COMPLEX_UNIT_MASK]
+ + " = " + res);
+ return res;
+ }
+
+ /**
+ * Converts an unpacked complex data value holding a dimension to its final floating
+ * point value. The two parameters <var>unit</var> and <var>value</var>
+ * are as in {@link #TYPE_DIMENSION}.
+ *
+ * @param unit The unit to convert from.
+ * @param value The value to apply the unit to.
+ * @param metrics Current display metrics to use in the conversion --
+ * supplies display density and scaling information.
+ *
+ * @return The complex floating point value multiplied by the appropriate
+ * metrics depending on its unit.
+ */
+ public static float applyDimension(int unit, float value,
+ DisplayMetrics metrics)
+ {
+ switch (unit) {
+ case COMPLEX_UNIT_PX:
+ return value;
+ case COMPLEX_UNIT_DIP:
+ return value * metrics.density;
+ case COMPLEX_UNIT_SP:
+ return value * metrics.scaledDensity;
+ case COMPLEX_UNIT_PT:
+ return value * metrics.xdpi * (1.0f/72);
+ case COMPLEX_UNIT_IN:
+ return value * metrics.xdpi;
+ case COMPLEX_UNIT_MM:
+ return value * metrics.xdpi * (1.0f/25.4f);
+ }
+ return 0;
+ }
+
+ /**
+ * Return the data for this value as a dimension. Only use for values
+ * whose type is {@link #TYPE_DIMENSION}.
+ *
+ * @param metrics Current display metrics to use in the conversion --
+ * supplies display density and scaling information.
+ *
+ * @return The complex floating point value multiplied by the appropriate
+ * metrics depending on its unit.
+ */
+ public float getDimension(DisplayMetrics metrics)
+ {
+ return complexToDimension(data, metrics);
+ }
+
+ /**
+ * Converts a complex data value holding a fraction to its final floating
+ * point value. The given <var>data</var> must be structured as a
+ * {@link #TYPE_FRACTION}.
+ *
+ * @param data A complex data value holding a unit, magnitude, and
+ * mantissa.
+ * @param base The base value of this fraction. In other words, a
+ * standard fraction is multiplied by this value.
+ * @param pbase The parent base value of this fraction. In other
+ * words, a parent fraction (nn%p) is multiplied by this
+ * value.
+ *
+ * @return The complex floating point value multiplied by the appropriate
+ * base value depending on its unit.
+ */
+ public static float complexToFraction(int data, float base, float pbase)
+ {
+ switch ((data>>COMPLEX_UNIT_SHIFT)&COMPLEX_UNIT_MASK) {
+ case COMPLEX_UNIT_FRACTION:
+ return complexToFloat(data) * base;
+ case COMPLEX_UNIT_FRACTION_PARENT:
+ return complexToFloat(data) * pbase;
+ }
+ return 0;
+ }
+
+ /**
+ * Return the data for this value as a fraction. Only use for values whose
+ * type is {@link #TYPE_FRACTION}.
+ *
+ * @param base The base value of this fraction. In other words, a
+ * standard fraction is multiplied by this value.
+ * @param pbase The parent base value of this fraction. In other
+ * words, a parent fraction (nn%p) is multiplied by this
+ * value.
+ *
+ * @return The complex floating point value multiplied by the appropriate
+ * base value depending on its unit.
+ */
+ public float getFraction(float base, float pbase)
+ {
+ return complexToFraction(data, base, pbase);
+ }
+
+ /**
+ * Regardless of the actual type of the value, try to convert it to a
+ * string value. For example, a color type will be converted to a
+ * string of the form #aarrggbb.
+ *
+ * @return CharSequence The coerced string value. If the value is
+ * null or the type is not known, null is returned.
+ */
+ public final CharSequence coerceToString()
+ {
+ int t = type;
+ if (t == TYPE_STRING) {
+ return string;
+ }
+ return coerceToString(t, data);
+ }
+
+ private static final String[] DIMENSION_UNIT_STRS = new String[] {
+ "px", "dip", "sp", "pt", "in", "mm"
+ };
+ private static final String[] FRACTION_UNIT_STRS = new String[] {
+ "%", "%p"
+ };
+
+ /**
+ * Perform type conversion as per {@link #coerceToString()} on an
+ * explicitly supplied type and data.
+ *
+ * @param type The data type identifier.
+ * @param data The data value.
+ *
+ * @return String The coerced string value. If the value is
+ * null or the type is not known, null is returned.
+ */
+ public static final String coerceToString(int type, int data)
+ {
+ switch (type) {
+ case TYPE_NULL:
+ return null;
+ case TYPE_REFERENCE:
+ return "@" + data;
+ case TYPE_ATTRIBUTE:
+ return "?" + data;
+ case TYPE_FLOAT:
+ return Float.toString(Float.intBitsToFloat(data));
+ case TYPE_DIMENSION:
+ return Float.toString(complexToFloat(data)) + DIMENSION_UNIT_STRS[
+ (data>>COMPLEX_UNIT_SHIFT)&COMPLEX_UNIT_MASK];
+ case TYPE_FRACTION:
+ return Float.toString(complexToFloat(data)*100) + FRACTION_UNIT_STRS[
+ (data>>COMPLEX_UNIT_SHIFT)&COMPLEX_UNIT_MASK];
+ case TYPE_INT_HEX:
+ return "0x" + Integer.toHexString(data);
+ case TYPE_INT_BOOLEAN:
+ return data != 0 ? "true" : "false";
+ }
+
+ if (type >= TYPE_FIRST_COLOR_INT && type <= TYPE_LAST_COLOR_INT) {
+ return "#" + Integer.toHexString(data);
+ } else if (type >= TYPE_FIRST_INT && type <= TYPE_LAST_INT) {
+ return Integer.toString(data);
+ }
+
+ return null;
+ }
+
+ public void setTo(TypedValue other)
+ {
+ type = other.type;
+ string = other.string;
+ data = other.data;
+ assetCookie = other.assetCookie;
+ resourceId = other.resourceId;
+ }
+
+ public String toString()
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.append("TypedValue{t=0x").append(Integer.toHexString(type));
+ sb.append("/d=0x").append(Integer.toHexString(data));
+ if (type == TYPE_STRING) {
+ sb.append(" \"").append(string != null ? string : "<null>").append("\"");
+ }
+ if (assetCookie != 0) {
+ sb.append(" a=").append(assetCookie);
+ }
+ if (resourceId != 0) {
+ sb.append(" r=0x").append(Integer.toHexString(resourceId));
+ }
+ sb.append("}");
+ return sb.toString();
+ }
+};
+
diff --git a/core/java/android/util/Xml.java b/core/java/android/util/Xml.java
new file mode 100644
index 0000000..a2b69a5
--- /dev/null
+++ b/core/java/android/util/Xml.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import org.xml.sax.ContentHandler;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.XMLReader;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlSerializer;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.UnsupportedEncodingException;
+
+import org.apache.harmony.xml.ExpatPullParser;
+import org.apache.harmony.xml.ExpatReader;
+
+/**
+ * XML utility methods.
+ */
+public class Xml {
+
+ /**
+ * {@link org.xmlpull.v1.XmlPullParser} "relaxed" feature name.
+ *
+ * @see <a href="http://xmlpull.org/v1/doc/features.html#relaxed">
+ * specification</a>
+ */
+ public static String FEATURE_RELAXED = ExpatPullParser.FEATURE_RELAXED;
+
+ /**
+ * Parses the given xml string and fires events on the given SAX handler.
+ */
+ public static void parse(String xml, ContentHandler contentHandler)
+ throws SAXException {
+ try {
+ XMLReader reader = new ExpatReader();
+ reader.setContentHandler(contentHandler);
+ reader.parse(new InputSource(new StringReader(xml)));
+ }
+ catch (IOException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ /**
+ * Parses xml from the given reader and fires events on the given SAX
+ * handler.
+ */
+ public static void parse(Reader in, ContentHandler contentHandler)
+ throws IOException, SAXException {
+ XMLReader reader = new ExpatReader();
+ reader.setContentHandler(contentHandler);
+ reader.parse(new InputSource(in));
+ }
+
+ /**
+ * Parses xml from the given input stream and fires events on the given SAX
+ * handler.
+ */
+ public static void parse(InputStream in, Encoding encoding,
+ ContentHandler contentHandler) throws IOException, SAXException {
+ try {
+ XMLReader reader = new ExpatReader();
+ reader.setContentHandler(contentHandler);
+ InputSource source = new InputSource(in);
+ source.setEncoding(encoding.expatName);
+ reader.parse(source);
+ } catch (IOException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ /**
+ * Creates a new pull parser with namespace support.
+ *
+ * <p><b>Note:</b> This is actually slower than the SAX parser, and it's not
+ * fully implemented. If you need a fast, mostly implemented pull parser,
+ * use this. If you need a complete implementation, use KXML.
+ */
+ public static XmlPullParser newPullParser() {
+ ExpatPullParser parser = new ExpatPullParser();
+ parser.setNamespaceProcessingEnabled(true);
+ return parser;
+ }
+
+ /**
+ * Creates a new xml serializer.
+ */
+ public static XmlSerializer newSerializer() {
+ try {
+ return XmlSerializerFactory.instance.newSerializer();
+ } catch (XmlPullParserException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ /** Factory for xml serializers. Initialized on demand. */
+ static class XmlSerializerFactory {
+ static final String TYPE
+ = "org.kxml2.io.KXmlParser,org.kxml2.io.KXmlSerializer";
+ static final XmlPullParserFactory instance;
+ static {
+ try {
+ instance = XmlPullParserFactory.newInstance(TYPE, null);
+ } catch (XmlPullParserException e) {
+ throw new AssertionError(e);
+ }
+ }
+ }
+
+ /**
+ * Supported character encodings.
+ */
+ public enum Encoding {
+
+ US_ASCII("US-ASCII"),
+ UTF_8("UTF-8"),
+ UTF_16("UTF-16"),
+ ISO_8859_1("ISO-8859-1");
+
+ final String expatName;
+
+ Encoding(String expatName) {
+ this.expatName = expatName;
+ }
+ }
+
+ /**
+ * Finds an encoding by name. Returns UTF-8 if you pass {@code null}.
+ */
+ public static Encoding findEncodingByName(String encodingName)
+ throws UnsupportedEncodingException {
+ if (encodingName == null) {
+ return Encoding.UTF_8;
+ }
+
+ for (Encoding encoding : Encoding.values()) {
+ if (encoding.expatName.equalsIgnoreCase(encodingName))
+ return encoding;
+ }
+ throw new UnsupportedEncodingException(encodingName);
+ }
+
+ /**
+ * Return an AttributeSet interface for use with the given XmlPullParser.
+ * If the given parser itself implements AttributeSet, that implementation
+ * is simply returned. Otherwise a wrapper class is
+ * instantiated on top of the XmlPullParser, as a proxy for retrieving its
+ * attributes, and returned to you.
+ *
+ * @param parser The existing parser for which you would like an
+ * AttributeSet.
+ *
+ * @return An AttributeSet you can use to retrieve the
+ * attribute values at each of the tags as the parser moves
+ * through its XML document.
+ *
+ * @see AttributeSet
+ */
+ public static AttributeSet asAttributeSet(XmlPullParser parser) {
+ return (parser instanceof AttributeSet)
+ ? (AttributeSet) parser
+ : new XmlPullAttributes(parser);
+ }
+}
diff --git a/core/java/android/util/XmlPullAttributes.java b/core/java/android/util/XmlPullAttributes.java
new file mode 100644
index 0000000..12d6dd9
--- /dev/null
+++ b/core/java/android/util/XmlPullAttributes.java
@@ -0,0 +1,146 @@
+/*
+ * 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 org.xmlpull.v1.XmlPullParser;
+
+import android.util.AttributeSet;
+import com.android.internal.util.XmlUtils;
+
+/**
+ * Provides an implementation of AttributeSet on top of an XmlPullParser.
+ */
+class XmlPullAttributes implements AttributeSet {
+ public XmlPullAttributes(XmlPullParser parser) {
+ mParser = parser;
+ }
+
+ public int getAttributeCount() {
+ return mParser.getAttributeCount();
+ }
+
+ public String getAttributeName(int index) {
+ return mParser.getAttributeName(index);
+ }
+
+ public String getAttributeValue(int index) {
+ return mParser.getAttributeValue(index);
+ }
+
+ public String getAttributeValue(String namespace, String name) {
+ return mParser.getAttributeValue(namespace, name);
+ }
+
+ public String getPositionDescription() {
+ return mParser.getPositionDescription();
+ }
+
+ public int getAttributeNameResource(int index) {
+ return 0;
+ }
+
+ public int getAttributeListValue(String namespace, String attribute,
+ String[] options, int defaultValue) {
+ return XmlUtils.convertValueToList(
+ getAttributeValue(namespace, attribute), options, defaultValue);
+ }
+
+ public boolean getAttributeBooleanValue(String namespace, String attribute,
+ boolean defaultValue) {
+ return XmlUtils.convertValueToBoolean(
+ getAttributeValue(namespace, attribute), defaultValue);
+ }
+
+ public int getAttributeResourceValue(String namespace, String attribute,
+ int defaultValue) {
+ return XmlUtils.convertValueToInt(
+ getAttributeValue(namespace, attribute), defaultValue);
+ }
+
+ public int getAttributeIntValue(String namespace, String attribute,
+ int defaultValue) {
+ return XmlUtils.convertValueToInt(
+ getAttributeValue(namespace, attribute), defaultValue);
+ }
+
+ public int getAttributeUnsignedIntValue(String namespace, String attribute,
+ int defaultValue) {
+ return XmlUtils.convertValueToUnsignedInt(
+ getAttributeValue(namespace, attribute), defaultValue);
+ }
+
+ public float getAttributeFloatValue(String namespace, String attribute,
+ float defaultValue) {
+ String s = getAttributeValue(namespace, attribute);
+ if (s != null) {
+ return Float.parseFloat(s);
+ }
+ return defaultValue;
+ }
+
+ public int getAttributeListValue(int index,
+ String[] options, int defaultValue) {
+ return XmlUtils.convertValueToList(
+ getAttributeValue(index), options, defaultValue);
+ }
+
+ public boolean getAttributeBooleanValue(int index, boolean defaultValue) {
+ return XmlUtils.convertValueToBoolean(
+ getAttributeValue(index), defaultValue);
+ }
+
+ public int getAttributeResourceValue(int index, int defaultValue) {
+ return XmlUtils.convertValueToInt(
+ getAttributeValue(index), defaultValue);
+ }
+
+ public int getAttributeIntValue(int index, int defaultValue) {
+ return XmlUtils.convertValueToInt(
+ getAttributeValue(index), defaultValue);
+ }
+
+ public int getAttributeUnsignedIntValue(int index, int defaultValue) {
+ return XmlUtils.convertValueToUnsignedInt(
+ getAttributeValue(index), defaultValue);
+ }
+
+ public float getAttributeFloatValue(int index, float defaultValue) {
+ String s = getAttributeValue(index);
+ if (s != null) {
+ return Float.parseFloat(s);
+ }
+ return defaultValue;
+ }
+
+ public String getIdAttribute() {
+ return getAttributeValue(null, "id");
+ }
+
+ public String getClassAttribute() {
+ return getAttributeValue(null, "class");
+ }
+
+ public int getIdAttributeResourceValue(int defaultValue) {
+ return getAttributeResourceValue(null, "id", defaultValue);
+ }
+
+ public int getStyleAttribute() {
+ return getAttributeResourceValue(null, "style", 0);
+ }
+
+ private XmlPullParser mParser;
+}
diff --git a/core/java/android/util/package.html b/core/java/android/util/package.html
new file mode 100644
index 0000000..d918d69
--- /dev/null
+++ b/core/java/android/util/package.html
@@ -0,0 +1,6 @@
+<HTML>
+<BODY>
+Provides common utility methods such as date/time manipulation, base64 encoders
+and decoders, string and number conversion methods, and XML utilities.
+</BODY>
+</HTML> \ No newline at end of file
diff --git a/core/java/android/view/AbsSavedState.java b/core/java/android/view/AbsSavedState.java
new file mode 100644
index 0000000..840d7c1
--- /dev/null
+++ b/core/java/android/view/AbsSavedState.java
@@ -0,0 +1,89 @@
+/*
+ * 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.view;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * A {@link Parcelable} implementation that should be used by inheritance
+ * hierarchies to ensure the state of all classes along the chain is saved.
+ */
+public abstract class AbsSavedState implements Parcelable {
+ public static final AbsSavedState EMPTY_STATE = new AbsSavedState() {};
+
+ private final Parcelable mSuperState;
+
+ /**
+ * Constructor used to make the EMPTY_STATE singleton
+ */
+ private AbsSavedState() {
+ mSuperState = null;
+ }
+
+ /**
+ * Constructor called by derived classes when creating their SavedState objects
+ *
+ * @param superState The state of the superclass of this view
+ */
+ protected AbsSavedState(Parcelable superState) {
+ if (superState == null) {
+ throw new IllegalArgumentException("superState must not be null");
+ }
+ mSuperState = superState != EMPTY_STATE ? superState : null;
+ }
+
+ /**
+ * Constructor used when reading from a parcel. Reads the state of the superclass.
+ *
+ * @param source
+ */
+ protected AbsSavedState(Parcel source) {
+ // FIXME need class loader
+ Parcelable superState = (Parcelable) source.readParcelable(null);
+
+ mSuperState = superState != null ? superState : EMPTY_STATE;
+ }
+
+ final public Parcelable getSuperState() {
+ return mSuperState;
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeParcelable(mSuperState, flags);
+ }
+
+ public static final Parcelable.Creator<AbsSavedState> CREATOR
+ = new Parcelable.Creator<AbsSavedState>() {
+
+ public AbsSavedState createFromParcel(Parcel in) {
+ Parcelable superState = (Parcelable) in.readParcelable(null);
+ if (superState != null) {
+ throw new IllegalStateException("superState must be null");
+ }
+ return EMPTY_STATE;
+ }
+
+ public AbsSavedState[] newArray(int size) {
+ return new AbsSavedState[size];
+ }
+ };
+}
diff --git a/core/java/android/view/ContextMenu.java b/core/java/android/view/ContextMenu.java
new file mode 100644
index 0000000..9bfda40
--- /dev/null
+++ b/core/java/android/view/ContextMenu.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.app.Activity;
+import android.graphics.drawable.Drawable;
+import android.widget.AdapterView;
+
+/**
+ * Extension of {@link Menu} for context menus providing functionality to modify
+ * the header of the context menu.
+ * <p>
+ * Context menus do not support item shortcuts, item icons, and sub menus.
+ * <p>
+ * To show a context menu on long click, most clients will want to call
+ * {@link Activity#registerForContextMenu} and override
+ * {@link Activity#onCreateContextMenu}.
+ */
+public interface ContextMenu extends Menu {
+ /**
+ * Sets the context menu header's title to the title given in <var>titleRes</var>
+ * resource identifier.
+ *
+ * @param titleRes The string resource identifier used for the title.
+ * @return This ContextMenu so additional setters can be called.
+ */
+ public ContextMenu setHeaderTitle(int titleRes);
+
+ /**
+ * Sets the context menu header's title to the title given in <var>title</var>.
+ *
+ * @param title The character sequence used for the title.
+ * @return This ContextMenu so additional setters can be called.
+ */
+ public ContextMenu setHeaderTitle(CharSequence title);
+
+ /**
+ * Sets the context menu header's icon to the icon given in <var>iconRes</var>
+ * resource id.
+ *
+ * @param iconRes The resource identifier used for the icon.
+ * @return This ContextMenu so additional setters can be called.
+ */
+ public ContextMenu setHeaderIcon(int iconRes);
+
+ /**
+ * Sets the context menu header's icon to the icon given in <var>icon</var>
+ * {@link Drawable}.
+ *
+ * @param icon The {@link Drawable} used for the icon.
+ * @return This ContextMenu so additional setters can be called.
+ */
+ public ContextMenu setHeaderIcon(Drawable icon);
+
+ /**
+ * Sets the header of the context menu to the {@link View} given in
+ * <var>view</var>. This replaces the header title and icon (and those
+ * replace this).
+ *
+ * @param view The {@link View} used for the header.
+ * @return This ContextMenu so additional setters can be called.
+ */
+ public ContextMenu setHeaderView(View view);
+
+ /**
+ * Clears the header of the context menu.
+ */
+ public void clearHeader();
+
+ /**
+ * Additional information regarding the creation of the context menu. For example,
+ * {@link AdapterView}s use this to pass the exact item position within the adapter
+ * that initiated the context menu.
+ */
+ public interface ContextMenuInfo {
+ }
+}
diff --git a/core/java/android/view/ContextThemeWrapper.java b/core/java/android/view/ContextThemeWrapper.java
new file mode 100644
index 0000000..2045a98
--- /dev/null
+++ b/core/java/android/view/ContextThemeWrapper.java
@@ -0,0 +1,103 @@
+/*
+ * 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.view;
+
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.res.Resources;
+
+/**
+ * A ContextWrapper that allows you to modify the theme from what is in the
+ * wrapped context.
+ */
+public class ContextThemeWrapper extends ContextWrapper {
+ private Context mBase;
+ private int mThemeResource;
+ private Resources.Theme mTheme;
+ private LayoutInflater mInflater;
+
+ public ContextThemeWrapper() {
+ super(null);
+ }
+
+ public ContextThemeWrapper(Context base, int themeres) {
+ super(base);
+ mBase = base;
+ mThemeResource = themeres;
+ }
+
+ @Override protected void attachBaseContext(Context newBase) {
+ super.attachBaseContext(newBase);
+ mBase = newBase;
+ }
+
+ @Override public void setTheme(int resid) {
+ mThemeResource = resid;
+ initializeTheme();
+ }
+
+ @Override public Resources.Theme getTheme() {
+ if (mTheme != null) {
+ return mTheme;
+ }
+
+ if (mThemeResource == 0) {
+ mThemeResource = com.android.internal.R.style.Theme;
+ }
+ initializeTheme();
+
+ return mTheme;
+ }
+
+ @Override public Object getSystemService(String name) {
+ if (LAYOUT_INFLATER_SERVICE.equals(name)) {
+ if (mInflater == null) {
+ mInflater = LayoutInflater.from(mBase).cloneInContext(this);
+ }
+ return mInflater;
+ }
+ return mBase.getSystemService(name);
+ }
+
+ /**
+ * Called by {@link #setTheme} and {@link #getTheme} to apply a theme
+ * resource to the current Theme object. Can override to change the
+ * default (simple) behavior. This method will not be called in multiple
+ * threads simultaneously.
+ *
+ * @param theme The Theme object being modified.
+ * @param resid The theme style resource being applied to <var>theme</var>.
+ * @param first Set to true if this is the first time a style is being
+ * applied to <var>theme</var>.
+ */
+ protected void onApplyThemeResource(Resources.Theme theme, int resid, boolean first) {
+ theme.applyStyle(resid, true);
+ }
+
+ private void initializeTheme() {
+ final boolean first = mTheme == null;
+ if (first) {
+ mTheme = getResources().newTheme();
+ Resources.Theme theme = mBase.getTheme();
+ if (theme != null) {
+ mTheme.setTo(theme);
+ }
+ }
+ onApplyThemeResource(mTheme, mThemeResource, first);
+ }
+}
+
diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java
new file mode 100644
index 0000000..09ebeed
--- /dev/null
+++ b/core/java/android/view/Display.java
@@ -0,0 +1,121 @@
+/*
+ * 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.view;
+
+import android.util.DisplayMetrics;
+
+public class Display
+{
+ /**
+ * Specify the default Display
+ */
+ public static final int DEFAULT_DISPLAY = 0;
+
+
+ /**
+ * Use the WindowManager interface to create a Display object.
+ * Display gives you access to some information about a particular display
+ * connected to the device.
+ */
+ Display(int display) {
+ // initalize the statics when this class is first instansiated. This is
+ // done here instead of in the static block because Zygote
+ synchronized (mStaticInit) {
+ if (!mInitialized) {
+ nativeClassInit();
+ mInitialized = true;
+ }
+ }
+ mDisplay = display;
+ init(display);
+ }
+
+ /**
+ * @return index of this display.
+ */
+ public int getDisplayId() {
+ return mDisplay;
+ }
+
+ /**
+ * @return the number of displays connected to the device.
+ */
+ native static int getDisplayCount();
+
+ /**
+ * @return width of this display in pixels.
+ */
+ native public int getWidth();
+
+ /**
+ * @return height of this display in pixels.
+ */
+ native public int getHeight();
+
+ /**
+ * @return orientation of this display.
+ */
+ native public int getOrientation();
+
+ /**
+ * @return pixel format of this display.
+ */
+ public int getPixelFormat() {
+ return mPixelFormat;
+ }
+
+ /**
+ * @return refresh rate of this display in frames per second.
+ */
+ public float getRefreshRate() {
+ return mRefreshRate;
+ }
+
+ /**
+ * Initialize a DisplayMetrics object from this display's data.
+ *
+ * @param outMetrics
+ */
+ public void getMetrics(DisplayMetrics outMetrics) {
+ outMetrics.widthPixels = getWidth();
+ outMetrics.heightPixels = getHeight();
+ outMetrics.density = mDensity;
+ outMetrics.scaledDensity= outMetrics.density;
+ outMetrics.xdpi = mDpiX;
+ outMetrics.ydpi = mDpiY;
+ }
+
+ /*
+ * We use a class initializer to allow the native code to cache some
+ * field offsets.
+ */
+ native private static void nativeClassInit();
+
+ private native void init(int display);
+
+ private int mDisplay;
+ // Following fields are initialized from native code
+ private int mPixelFormat;
+ private float mRefreshRate;
+ private float mDensity;
+ private float mDpiX;
+ private float mDpiY;
+
+ private static final Object mStaticInit = new Object();
+ private static boolean mInitialized = false;
+}
+
diff --git a/core/java/android/view/FocusFinder.java b/core/java/android/view/FocusFinder.java
new file mode 100644
index 0000000..4048763
--- /dev/null
+++ b/core/java/android/view/FocusFinder.java
@@ -0,0 +1,480 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.graphics.Rect;
+
+import java.util.ArrayList;
+
+/**
+ * The algorithm used for finding the next focusable view in a given direction
+ * from a view that currently has focus.
+ */
+public class FocusFinder {
+
+ private static ThreadLocal<FocusFinder> tlFocusFinder =
+ new ThreadLocal<FocusFinder>() {
+
+ protected FocusFinder initialValue() {
+ return new FocusFinder();
+ }
+ };
+
+ /**
+ * Get the focus finder for this thread.
+ */
+ public static FocusFinder getInstance() {
+ return tlFocusFinder.get();
+ }
+
+ Rect mFocusedRect = new Rect();
+ Rect mOtherRect = new Rect();
+ Rect mBestCandidateRect = new Rect();
+
+ // enforce thread local access
+ private FocusFinder() {}
+
+ /**
+ * Find the next view to take focus in root's descendants, starting from the view
+ * that currently is focused.
+ * @param root Contains focused
+ * @param focused Has focus now.
+ * @param direction Direction to look.
+ * @return The next focusable view, or null if none exists.
+ */
+ public final View findNextFocus(ViewGroup root, View focused, int direction) {
+
+ if (focused != null) {
+ // check for user specified next focus
+ View userSetNextFocus = focused.findUserSetNextFocus(root, direction);
+ if (userSetNextFocus != null &&
+ userSetNextFocus.isFocusable() &&
+ (!userSetNextFocus.isInTouchMode() ||
+ userSetNextFocus.isFocusableInTouchMode())) {
+ return userSetNextFocus;
+ }
+
+ // fill in interesting rect from focused
+ focused.getFocusedRect(mFocusedRect);
+ root.offsetDescendantRectToMyCoords(focused, mFocusedRect);
+ } else {
+ // make up a rect at top left or bottom right of root
+ switch (direction) {
+ case View.FOCUS_RIGHT:
+ case View.FOCUS_DOWN:
+ final int rootTop = root.getScrollY();
+ final int rootLeft = root.getScrollX();
+ mFocusedRect.set(rootLeft, rootTop, rootLeft, rootTop);
+ break;
+
+ case View.FOCUS_LEFT:
+ case View.FOCUS_UP:
+ final int rootBottom = root.getScrollY() + root.getHeight();
+ final int rootRight = root.getScrollX() + root.getWidth();
+ mFocusedRect.set(rootRight, rootBottom,
+ rootRight, rootBottom);
+ break;
+ }
+ }
+ return findNextFocus(root, focused, mFocusedRect, direction);
+ }
+
+ /**
+ * Find the next view to take focus in root's descendants, searching from
+ * a particular rectangle in root's coordinates.
+ * @param root Contains focusedRect.
+ * @param focusedRect The starting point of the search.
+ * @param direction Direction to look.
+ * @return The next focusable view, or null if none exists.
+ */
+ public View findNextFocusFromRect(ViewGroup root, Rect focusedRect, int direction) {
+ return findNextFocus(root, null, focusedRect, direction);
+ }
+
+ private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {
+ ArrayList<View> focusables = root.getFocusables(direction);
+
+ // initialize the best candidate to something impossible
+ // (so the first plausible view will become the best choice)
+ mBestCandidateRect.set(focusedRect);
+ switch(direction) {
+ case View.FOCUS_LEFT:
+ mBestCandidateRect.offset(focusedRect.width() + 1, 0);
+ break;
+ case View.FOCUS_RIGHT:
+ mBestCandidateRect.offset(-(focusedRect.width() + 1), 0);
+ break;
+ case View.FOCUS_UP:
+ mBestCandidateRect.offset(0, focusedRect.height() + 1);
+ break;
+ case View.FOCUS_DOWN:
+ mBestCandidateRect.offset(0, -(focusedRect.height() + 1));
+ }
+
+ View closest = null;
+
+ int numFocusables = focusables.size();
+ for (int i = 0; i < numFocusables; i++) {
+ View focusable = focusables.get(i);
+
+ // only interested in other non-root views
+ if (focusable == focused || focusable == root) continue;
+
+ // get visible bounds of other view in same coordinate system
+ focusable.getDrawingRect(mOtherRect);
+ root.offsetDescendantRectToMyCoords(focusable, mOtherRect);
+
+ if (isBetterCandidate(direction, focusedRect, mOtherRect, mBestCandidateRect)) {
+ mBestCandidateRect.set(mOtherRect);
+ closest = focusable;
+ }
+ }
+ return closest;
+ }
+
+ /**
+ * Is rect1 a better candidate than rect2 for a focus search in a particular
+ * direction from a source rect? This is the core routine that determines
+ * the order of focus searching.
+ * @param direction the direction (up, down, left, right)
+ * @param source The source we are searching from
+ * @param rect1 The candidate rectangle
+ * @param rect2 The current best candidate.
+ * @return Whether the candidate is the new best.
+ */
+ boolean isBetterCandidate(int direction, Rect source, Rect rect1, Rect rect2) {
+
+ // to be a better candidate, need to at least be a candidate in the first
+ // place :)
+ if (!isCandidate(source, rect1, direction)) {
+ return false;
+ }
+
+ // we know that rect1 is a candidate.. if rect2 is not a candidate,
+ // rect1 is better
+ if (!isCandidate(source, rect2, direction)) {
+ return true;
+ }
+
+ // if rect1 is better by beam, it wins
+ if (beamBeats(direction, source, rect1, rect2)) {
+ return true;
+ }
+
+ // if rect2 is better, then rect1 cant' be :)
+ if (beamBeats(direction, source, rect2, rect1)) {
+ return false;
+ }
+
+ // otherwise, do fudge-tastic comparison of the major and minor axis
+ return (getWeightedDistanceFor(
+ majorAxisDistance(direction, source, rect1),
+ minorAxisDistance(direction, source, rect1))
+ < getWeightedDistanceFor(
+ majorAxisDistance(direction, source, rect2),
+ minorAxisDistance(direction, source, rect2)));
+ }
+
+ /**
+ * One rectangle may be another candidate than another by virtue of being
+ * exclusively in the beam of the source rect.
+ * @return Whether rect1 is a better candidate than rect2 by virtue of it being in src's
+ * beam
+ */
+ boolean beamBeats(int direction, Rect source, Rect rect1, Rect rect2) {
+ final boolean rect1InSrcBeam = beamsOverlap(direction, source, rect1);
+ final boolean rect2InSrcBeam = beamsOverlap(direction, source, rect2);
+
+ // if rect1 isn't exclusively in the src beam, it doesn't win
+ if (rect2InSrcBeam || !rect1InSrcBeam) {
+ return false;
+ }
+
+ // we know rect1 is in the beam, and rect2 is not
+
+ // if rect1 is to the direction of, and rect2 is not, rect1 wins.
+ // for example, for direction left, if rect1 is to the left of the source
+ // and rect2 is below, then we always prefer the in beam rect1, since rect2
+ // could be reached by going down.
+ if (!isToDirectionOf(direction, source, rect2)) {
+ return true;
+ }
+
+ // for horizontal directions, being exclusively in beam always wins
+ if ((direction == View.FOCUS_LEFT || direction == View.FOCUS_RIGHT)) {
+ return true;
+ }
+
+ // for vertical directions, beams only beat up to a point:
+ // now, as long as rect2 isn't completely closer, rect1 wins
+ // e.g for direction down, completely closer means for rect2's top
+ // edge to be closer to the source's top edge than rect1's bottom edge.
+ return (majorAxisDistance(direction, source, rect1)
+ < majorAxisDistanceToFarEdge(direction, source, rect2));
+ }
+
+ /**
+ * Fudge-factor opportunity: how to calculate distance given major and minor
+ * axis distances. Warning: this fudge factor is finely tuned, be sure to
+ * run all focus tests if you dare tweak it.
+ */
+ int getWeightedDistanceFor(int majorAxisDistance, int minorAxisDistance) {
+ return 13 * majorAxisDistance * majorAxisDistance
+ + minorAxisDistance * minorAxisDistance;
+ }
+
+ /**
+ * Is destRect a candidate for the next focus given the direction? This
+ * checks whether the dest is at least partially to the direction of (e.g left of)
+ * from source.
+ *
+ * Includes an edge case for an empty rect (which is used in some cases when
+ * searching from a point on the screen).
+ */
+ boolean isCandidate(Rect srcRect, Rect destRect, int direction) {
+ switch (direction) {
+ case View.FOCUS_LEFT:
+ return (srcRect.right > destRect.right || srcRect.left >= destRect.right)
+ && srcRect.left > destRect.left;
+ case View.FOCUS_RIGHT:
+ return (srcRect.left < destRect.left || srcRect.right <= destRect.left)
+ && srcRect.right < destRect.right;
+ case View.FOCUS_UP:
+ return (srcRect.bottom > destRect.bottom || srcRect.top >= destRect.bottom)
+ && srcRect.top > destRect.top;
+ case View.FOCUS_DOWN:
+ return (srcRect.top < destRect.top || srcRect.bottom <= destRect.top)
+ && srcRect.bottom < destRect.bottom;
+ }
+ throw new IllegalArgumentException("direction must be one of "
+ + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
+ }
+
+
+ /**
+ * Do the "beams" w.r.t the given direcition's axos of rect1 and rect2 overlap?
+ * @param direction the direction (up, down, left, right)
+ * @param rect1 The first rectangle
+ * @param rect2 The second rectangle
+ * @return whether the beams overlap
+ */
+ boolean beamsOverlap(int direction, Rect rect1, Rect rect2) {
+ switch (direction) {
+ case View.FOCUS_LEFT:
+ case View.FOCUS_RIGHT:
+ return (rect2.bottom >= rect1.top) && (rect2.top <= rect1.bottom);
+ case View.FOCUS_UP:
+ case View.FOCUS_DOWN:
+ return (rect2.right >= rect1.left) && (rect2.left <= rect1.right);
+ }
+ throw new IllegalArgumentException("direction must be one of "
+ + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
+ }
+
+ /**
+ * e.g for left, is 'to left of'
+ */
+ boolean isToDirectionOf(int direction, Rect src, Rect dest) {
+ switch (direction) {
+ case View.FOCUS_LEFT:
+ return src.left >= dest.right;
+ case View.FOCUS_RIGHT:
+ return src.right <= dest.left;
+ case View.FOCUS_UP:
+ return src.top >= dest.bottom;
+ case View.FOCUS_DOWN:
+ return src.bottom <= dest.top;
+ }
+ throw new IllegalArgumentException("direction must be one of "
+ + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
+ }
+
+ /**
+ * @return The distance from the edge furthest in the given direction
+ * of source to the edge nearest in the given direction of dest. If the
+ * dest is not in the direction from source, return 0.
+ */
+ static int majorAxisDistance(int direction, Rect source, Rect dest) {
+ return Math.max(0, majorAxisDistanceRaw(direction, source, dest));
+ }
+
+ static int majorAxisDistanceRaw(int direction, Rect source, Rect dest) {
+ switch (direction) {
+ case View.FOCUS_LEFT:
+ return source.left - dest.right;
+ case View.FOCUS_RIGHT:
+ return dest.left - source.right;
+ case View.FOCUS_UP:
+ return source.top - dest.bottom;
+ case View.FOCUS_DOWN:
+ return dest.top - source.bottom;
+ }
+ throw new IllegalArgumentException("direction must be one of "
+ + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
+ }
+
+ /**
+ * @return The distance along the major axis w.r.t the direction from the
+ * edge of source to the far edge of dest. If the
+ * dest is not in the direction from source, return 1 (to break ties with
+ * {@link #majorAxisDistance}).
+ */
+ static int majorAxisDistanceToFarEdge(int direction, Rect source, Rect dest) {
+ return Math.max(1, majorAxisDistanceToFarEdgeRaw(direction, source, dest));
+ }
+
+ static int majorAxisDistanceToFarEdgeRaw(int direction, Rect source, Rect dest) {
+ switch (direction) {
+ case View.FOCUS_LEFT:
+ return source.left - dest.left;
+ case View.FOCUS_RIGHT:
+ return dest.right - source.right;
+ case View.FOCUS_UP:
+ return source.top - dest.top;
+ case View.FOCUS_DOWN:
+ return dest.bottom - source.bottom;
+ }
+ throw new IllegalArgumentException("direction must be one of "
+ + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
+ }
+
+ /**
+ * Find the distance on the minor axis w.r.t the direction to the nearest
+ * edge of the destination rectange.
+ * @param direction the direction (up, down, left, right)
+ * @param source The source rect.
+ * @param dest The destination rect.
+ * @return The distance.
+ */
+ static int minorAxisDistance(int direction, Rect source, Rect dest) {
+ switch (direction) {
+ case View.FOCUS_LEFT:
+ case View.FOCUS_RIGHT:
+ // the distance between the center verticals
+ return Math.abs(
+ ((source.top + source.height() / 2) -
+ ((dest.top + dest.height() / 2))));
+ case View.FOCUS_UP:
+ case View.FOCUS_DOWN:
+ // the distance between the center horizontals
+ return Math.abs(
+ ((source.left + source.width() / 2) -
+ ((dest.left + dest.width() / 2))));
+ }
+ throw new IllegalArgumentException("direction must be one of "
+ + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
+ }
+
+ /**
+ * Find the nearest touchable view to the specified view.
+ *
+ * @param root The root of the tree in which to search
+ * @param x X coordinate from which to start the search
+ * @param y Y coordinate from which to start the search
+ * @param direction Direction to look
+ * @param deltas Offset from the <x, y> to the edge of the nearest view. Note that this array
+ * may already be populated with values.
+ * @return The nearest touchable view, or null if none exists.
+ */
+ public View findNearestTouchable(ViewGroup root, int x, int y, int direction, int[] deltas) {
+ ArrayList<View> touchables = root.getTouchables();
+ int minDistance = Integer.MAX_VALUE;
+ View closest = null;
+
+ int numTouchables = touchables.size();
+
+ int edgeSlop = ViewConfiguration.getEdgeSlop();
+
+ Rect closestBounds = new Rect();
+ Rect touchableBounds = mOtherRect;
+
+ for (int i = 0; i < numTouchables; i++) {
+ View touchable = touchables.get(i);
+
+ // get visible bounds of other view in same coordinate system
+ touchable.getDrawingRect(touchableBounds);
+
+ root.offsetRectBetweenParentAndChild(touchable, touchableBounds, true, true);
+
+ if (!isTouchCandidate(x, y, touchableBounds, direction)) {
+ continue;
+ }
+
+ int distance = Integer.MAX_VALUE;
+
+ switch (direction) {
+ case View.FOCUS_LEFT:
+ distance = x - touchableBounds.right + 1;
+ break;
+ case View.FOCUS_RIGHT:
+ distance = touchableBounds.left;
+ break;
+ case View.FOCUS_UP:
+ distance = y - touchableBounds.bottom + 1;
+ break;
+ case View.FOCUS_DOWN:
+ distance = touchableBounds.top;
+ break;
+ }
+
+ if (distance < edgeSlop) {
+ // Give preference to innermost views
+ if (closest == null ||
+ closestBounds.contains(touchableBounds) ||
+ (!touchableBounds.contains(closestBounds) && distance < minDistance)) {
+ minDistance = distance;
+ closest = touchable;
+ closestBounds.set(touchableBounds);
+ switch (direction) {
+ case View.FOCUS_LEFT:
+ deltas[0] = -distance;
+ break;
+ case View.FOCUS_RIGHT:
+ deltas[0] = distance;
+ break;
+ case View.FOCUS_UP:
+ deltas[1] = -distance;
+ break;
+ case View.FOCUS_DOWN:
+ deltas[1] = distance;
+ break;
+ }
+ }
+ }
+ }
+ return closest;
+ }
+
+
+ /**
+ * Is destRect a candidate for the next touch given the direction?
+ */
+ private boolean isTouchCandidate(int x, int y, Rect destRect, int direction) {
+ switch (direction) {
+ case View.FOCUS_LEFT:
+ return destRect.left <= x && destRect.top <= y && y <= destRect.bottom;
+ case View.FOCUS_RIGHT:
+ return destRect.left >= x && destRect.top <= y && y <= destRect.bottom;
+ case View.FOCUS_UP:
+ return destRect.top <= y && destRect.left <= x && x <= destRect.right;
+ case View.FOCUS_DOWN:
+ return destRect.top >= y && destRect.left <= x && x <= destRect.right;
+ }
+ throw new IllegalArgumentException("direction must be one of "
+ + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
+ }
+}
diff --git a/core/java/android/view/FocusFinderHelper.java b/core/java/android/view/FocusFinderHelper.java
new file mode 100644
index 0000000..69dc056
--- /dev/null
+++ b/core/java/android/view/FocusFinderHelper.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.graphics.Rect;
+
+/**
+ * A helper class that allows unit tests to access FocusFinder methods.
+ * @hide
+ */
+public class FocusFinderHelper {
+
+ private FocusFinder mFocusFinder;
+
+ /**
+ * Wrap the FocusFinder object
+ */
+ public FocusFinderHelper(FocusFinder focusFinder) {
+ mFocusFinder = focusFinder;
+ }
+
+ public boolean isBetterCandidate(int direction, Rect source, Rect rect1, Rect rect2) {
+ return mFocusFinder.isBetterCandidate(direction, source, rect1, rect2);
+ }
+
+ public boolean beamBeats(int direction, Rect source, Rect rect1, Rect rect2) {
+ return mFocusFinder.beamBeats(direction, source, rect1, rect2);
+ }
+
+ public boolean isCandidate(Rect srcRect, Rect destRect, int direction) {
+ return mFocusFinder.isCandidate(srcRect, destRect, direction);
+ }
+
+ public boolean beamsOverlap(int direction, Rect rect1, Rect rect2) {
+ return mFocusFinder.beamsOverlap(direction, rect1, rect2);
+ }
+
+ public static int majorAxisDistance(int direction, Rect source, Rect dest) {
+ return FocusFinder.majorAxisDistance(direction, source, dest);
+ }
+
+ public static int majorAxisDistanceToFarEdge(int direction, Rect source, Rect dest) {
+ return FocusFinder.majorAxisDistanceToFarEdge(direction, source, dest);
+ }
+}
diff --git a/core/java/android/view/GestureDetector.java b/core/java/android/view/GestureDetector.java
new file mode 100644
index 0000000..fc9af05
--- /dev/null
+++ b/core/java/android/view/GestureDetector.java
@@ -0,0 +1,368 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.os.Handler;
+import android.os.Message;
+
+/**
+ * Detects various gestures and events using the supplied {@link MotionEvent}s.
+ * The {@link OnGestureListener} callback will notify users when a particular
+ * motion event has occurred. This class should only be used with {@link MotionEvent}s
+ * reported via touch (don't use for trackball events).
+ *
+ * To use this class:
+ * <ul>
+ * <li>Create an instance of the {@code GestureDetector} for your {@link View}
+ * <li>In the {@link View#onTouchEvent(MotionEvent)} method ensure you call
+ * {@link #onTouchEvent(MotionEvent)}. The methods defined in your callback
+ * will be executed when the events occur.
+ * </ul>
+ */
+public class GestureDetector {
+
+ /**
+ * The listener that is used to notify when gestures occur.
+ * If you want to listen for all the different gestures then implement
+ * this interface. If you only want to listen for a subset it might
+ * be easier to extend {@link SimpleOnGestureListener}.
+ */
+ public interface OnGestureListener {
+
+ /**
+ * Notified when a tap occurs with the down {@link MotionEvent}
+ * that triggered it. This will be triggered immediately for
+ * every down event. All other events should be preceded by this.
+ *
+ * @param e The down motion event.
+ */
+ boolean onDown(MotionEvent e);
+
+ /**
+ * The user has performed a down {@link MotionEvent} and not performed
+ * a move or up yet. This event is commonly used to provide visual
+ * feedback to the user to let them know that their action has been
+ * recognized i.e. highlight an element.
+ *
+ * @param e The down motion event
+ */
+ void onShowPress(MotionEvent e);
+
+ /**
+ * Notified when a tap occurs with the up {@link MotionEvent}
+ * that triggered it.
+ *
+ * @param e The up motion event that completed the first tap
+ * @return true if the event is consumed, else false
+ */
+ boolean onSingleTapUp(MotionEvent e);
+
+ /**
+ * Notified when a scroll occurs with the initial on down {@link MotionEvent} and the
+ * current move {@link MotionEvent}. The distance in x and y is also supplied for
+ * convenience.
+ *
+ * @param e1 The first down motion event that started the scrolling.
+ * @param e2 The move motion event that triggered the current onScroll.
+ * @param distanceX The distance along the X axis that has been scrolled since the last
+ * call to onScroll. This is NOT the distance between {@code e1}
+ * and {@code e2}.
+ * @param distanceY The distance along the Y axis that has been scrolled since the last
+ * call to onScroll. This is NOT the distance between {@code e1}
+ * and {@code e2}.
+ * @return true if the event is consumed, else false
+ */
+ boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);
+
+ /**
+ * Notified when a long press occurs with the initial on down {@link MotionEvent}
+ * that trigged it.
+ *
+ * @param e The initial on down motion event that started the longpress.
+ */
+ void onLongPress(MotionEvent e);
+
+ /**
+ * Notified of a fling event when it occurs with the initial on down {@link MotionEvent}
+ * and the matching up {@link MotionEvent}. The calculated velocity is supplied along
+ * the x and y axis in pixels per second.
+ *
+ * @param e1 The first down motion event that started the fling.
+ * @param e2 The move motion event that triggered the current onFling.
+ * @param velocityX The velocity of this fling measured in pixels per second
+ * along the x axis.
+ * @param velocityY The velocity of this fling measured in pixels per second
+ * along the y axis.
+ * @return true if the event is consumed, else false
+ */
+ boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);
+ }
+
+ /**
+ * A convenience class to extend when you only want to listen for a
+ * subset of all the gestures. This implements all methods in the
+ * {@link OnGestureListener} but does nothing and return {@code false}
+ * for all applicable methods.
+ */
+ public static class SimpleOnGestureListener implements OnGestureListener {
+ public boolean onSingleTapUp(MotionEvent e) {
+ return false;
+ }
+
+ public void onLongPress(MotionEvent e) {
+ }
+
+ public boolean onScroll(MotionEvent e1, MotionEvent e2,
+ float distanceX, float distanceY) {
+ return false;
+ }
+
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
+ float velocityY) {
+ return false;
+ }
+
+ public void onShowPress(MotionEvent e) {
+ }
+
+ public boolean onDown(MotionEvent e) {
+ return false;
+ }
+ }
+
+ private static final int TOUCH_SLOP_SQUARE = ViewConfiguration.getTouchSlop()
+ * ViewConfiguration.getTouchSlop();
+
+ // constants for Message.what used by GestureHandler below
+ private static final int SHOW_PRESS = 1;
+ private static final int LONG_PRESS = 2;
+
+ private final Handler mHandler;
+ private final OnGestureListener mListener;
+
+ private boolean mInLongPress;
+ private boolean mAlwaysInTapRegion;
+
+ private MotionEvent mCurrentDownEvent;
+ private MotionEvent mCurrentUpEvent;
+
+ private float mLastMotionY;
+ private float mLastMotionX;
+
+ private boolean mIsLongpressEnabled;
+
+ /**
+ * Determines speed during touch scrolling
+ */
+ private VelocityTracker mVelocityTracker;
+
+ private class GestureHandler extends Handler {
+ GestureHandler() {
+ super();
+ }
+
+ GestureHandler(Handler handler) {
+ super(handler.getLooper());
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case SHOW_PRESS:
+ mListener.onShowPress(mCurrentDownEvent);
+ break;
+
+ case LONG_PRESS:
+ dispatchLongPress();
+ break;
+
+ default:
+ throw new RuntimeException("Unknown message " + msg); //never
+ }
+ }
+ }
+
+ /**
+ * Creates a GestureDetector with the supplied listener.
+ * This variant of the constructor should be used from a non-UI thread
+ * (as it allows specifying the Handler).
+ *
+ * @param listener the listener invoked for all the callbacks, this must
+ * not be null.
+ * @param handler the handler to use, this must
+ * not be null.
+ *
+ * @throws NullPointerException if either {@code listener} or
+ * {@code handler} is null.
+ */
+ public GestureDetector(OnGestureListener listener, Handler handler) {
+ mHandler = new GestureHandler(handler);
+ mListener = listener;
+ init();
+ }
+
+ /**
+ * Creates a GestureDetector with the supplied listener.
+ * You may only use this constructor from a UI thread (this is the usual situation).
+ * @see android.os.Handler#Handler()
+ *
+ * @param listener the listener invoked for all the callbacks, this must
+ * not be null.
+ * @throws NullPointerException if {@code listener} is null.
+ */
+ public GestureDetector(OnGestureListener listener) {
+ mHandler = new GestureHandler();
+ mListener = listener;
+ init();
+ }
+
+ private void init() {
+ if (mListener == null) {
+ throw new NullPointerException("OnGestureListener must not be null");
+ }
+ mIsLongpressEnabled = true;
+ }
+
+ /**
+ * Set whether longpress is enabled, if this is enabled when a user
+ * presses and holds down you get a longpress event and nothing further.
+ * If it's disabled the user can press and hold down and then later
+ * moved their finger and you will get scroll events. By default
+ * longpress is enabled.
+ *
+ * @param isLongpressEnabled whether longpress should be enabled.
+ */
+ public void setIsLongpressEnabled(boolean isLongpressEnabled) {
+ mIsLongpressEnabled = isLongpressEnabled;
+ }
+
+ /**
+ * @return true if longpress is enabled, else false.
+ */
+ public boolean isLongpressEnabled() {
+ return mIsLongpressEnabled;
+ }
+
+ /**
+ * Analyzes the given motion event and if applicable triggers the
+ * appropriate callbacks on the {@link OnGestureListener} supplied.
+ *
+ * @param ev The current motion event.
+ * @return true if the {@link OnGestureListener} consumed the event,
+ * else false.
+ */
+ public boolean onTouchEvent(MotionEvent ev) {
+ final long tapTime = ViewConfiguration.getTapTimeout();
+ final long longpressTime = ViewConfiguration.getLongPressTimeout();
+ final int touchSlop = ViewConfiguration.getTouchSlop();
+ final int action = ev.getAction();
+ final float y = ev.getY();
+ final float x = ev.getX();
+
+ if (mVelocityTracker == null) {
+ mVelocityTracker = VelocityTracker.obtain();
+ }
+ mVelocityTracker.addMovement(ev);
+
+ boolean handled = false;
+
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ mLastMotionX = x;
+ mLastMotionY = y;
+ mCurrentDownEvent = MotionEvent.obtain(ev);
+ mAlwaysInTapRegion = true;
+ mInLongPress = false;
+
+ if (mIsLongpressEnabled) {
+ mHandler.removeMessages(LONG_PRESS);
+ mHandler.sendEmptyMessageAtTime(LONG_PRESS, mCurrentDownEvent.getDownTime()
+ + tapTime + longpressTime);
+ }
+ mHandler.sendEmptyMessageAtTime(SHOW_PRESS, mCurrentDownEvent.getDownTime() + tapTime);
+ handled = mListener.onDown(ev);
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ if (mInLongPress) {
+ break;
+ }
+ final float scrollX = mLastMotionX - x;
+ final float scrollY = mLastMotionY - y;
+ if (mAlwaysInTapRegion) {
+ final int deltaX = (int) (x - mCurrentDownEvent.getX());
+ final int deltaY = (int) (y - mCurrentDownEvent.getY());
+ int distance = (deltaX * deltaX) + (deltaY * deltaY);
+ if (distance > TOUCH_SLOP_SQUARE) {
+ handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
+ mLastMotionX = x;
+ mLastMotionY = y;
+ mAlwaysInTapRegion = false;
+ mHandler.removeMessages(SHOW_PRESS);
+ mHandler.removeMessages(LONG_PRESS);
+ }
+ } else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) {
+ handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
+ mLastMotionX = x;
+ mLastMotionY = y;
+ }
+ break;
+
+ case MotionEvent.ACTION_UP:
+ mCurrentUpEvent = MotionEvent.obtain(ev);
+ if (mInLongPress) {
+ mInLongPress = false;
+ break;
+ }
+ if (mAlwaysInTapRegion) {
+ handled = mListener.onSingleTapUp(ev);
+ } else {
+
+ // A fling must travel the minimum tap distance
+ final VelocityTracker velocityTracker = mVelocityTracker;
+ velocityTracker.computeCurrentVelocity(1000);
+ final float velocityY = velocityTracker.getYVelocity();
+ final float velocityX = velocityTracker.getXVelocity();
+
+ if ((Math.abs(velocityY) > ViewConfiguration.getMinimumFlingVelocity())
+ || (Math.abs(velocityX) > ViewConfiguration.getMinimumFlingVelocity())){
+ handled = mListener.onFling(mCurrentDownEvent, mCurrentUpEvent, velocityX, velocityY);
+ }
+ }
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ mHandler.removeMessages(SHOW_PRESS);
+ mHandler.removeMessages(LONG_PRESS);
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ mHandler.removeMessages(SHOW_PRESS);
+ mHandler.removeMessages(LONG_PRESS);
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ if (mInLongPress) {
+ mInLongPress = false;
+ break;
+ }
+ }
+ return handled;
+ }
+
+ private void dispatchLongPress() {
+ mInLongPress = true;
+ mListener.onLongPress(mCurrentDownEvent);
+ }
+}
diff --git a/core/java/android/view/Gravity.java b/core/java/android/view/Gravity.java
new file mode 100644
index 0000000..ff9ab18
--- /dev/null
+++ b/core/java/android/view/Gravity.java
@@ -0,0 +1,179 @@
+/*
+ * 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.view;
+import android.graphics.Rect;
+
+/**
+ * Standard constants and tools for placing an object within a potentially
+ * larger container.
+ */
+public class Gravity
+{
+ /** Contstant indicating that no gravity has been set **/
+ public static final int NO_GRAVITY = 0x0000;
+
+ /** Raw bit indicating the gravity for an axis has been specified. */
+ public static final int AXIS_SPECIFIED = 0x0001;
+
+ /** Raw bit controlling how the left/top edge is placed. */
+ public static final int AXIS_PULL_BEFORE = 0x0002;
+ /** Raw bit controlling how the right/bottom edge is placed. */
+ public static final int AXIS_PULL_AFTER = 0x0004;
+
+ /** Bits defining the horizontal axis. */
+ public static final int AXIS_X_SHIFT = 0;
+ /** Bits defining the vertical axis. */
+ public static final int AXIS_Y_SHIFT = 4;
+
+ /** Push object to the top of its container, not changing its size. */
+ public static final int TOP = (AXIS_PULL_BEFORE|AXIS_SPECIFIED)<<AXIS_Y_SHIFT;
+ /** Push object to the bottom of its container, not changing its size. */
+ public static final int BOTTOM = (AXIS_PULL_AFTER|AXIS_SPECIFIED)<<AXIS_Y_SHIFT;
+ /** Push object to the left of its container, not changing its size. */
+ public static final int LEFT = (AXIS_PULL_BEFORE|AXIS_SPECIFIED)<<AXIS_X_SHIFT;
+ /** Push object to the right of its container, not changing its size. */
+ public static final int RIGHT = (AXIS_PULL_AFTER|AXIS_SPECIFIED)<<AXIS_X_SHIFT;
+
+ /** Place object in the vertical center of its container, not changing its
+ * size. */
+ public static final int CENTER_VERTICAL = AXIS_SPECIFIED<<AXIS_Y_SHIFT;
+ /** Grow the vertical size of the object if needed so it completely fills
+ * its container. */
+ public static final int FILL_VERTICAL = TOP|BOTTOM;
+
+ /** Place object in the horizontal center of its container, not changing its
+ * size. */
+ public static final int CENTER_HORIZONTAL = AXIS_SPECIFIED<<AXIS_X_SHIFT;
+ /** Grow the horizontal size of the object if needed so it completely fills
+ * its container. */
+ public static final int FILL_HORIZONTAL = LEFT|RIGHT;
+
+ /** Place the object in the center of its container in both the vertical
+ * and horizontal axis, not changing its size. */
+ public static final int CENTER = CENTER_VERTICAL|CENTER_HORIZONTAL;
+
+ /** Grow the horizontal and vertical size of the obejct if needed so it
+ * completely fills its container. */
+ public static final int FILL = FILL_VERTICAL|FILL_HORIZONTAL;
+
+ /**
+ * Binary mask to get the horizontal gravity of a gravity.
+ */
+ public static final int HORIZONTAL_GRAVITY_MASK = (AXIS_SPECIFIED |
+ AXIS_PULL_BEFORE | AXIS_PULL_AFTER) << AXIS_X_SHIFT;
+ /**
+ * Binary mask to get the vertical gravity of a gravity.
+ */
+ public static final int VERTICAL_GRAVITY_MASK = (AXIS_SPECIFIED |
+ AXIS_PULL_BEFORE | AXIS_PULL_AFTER) << AXIS_Y_SHIFT;
+
+ /**
+ * Apply a gravity constant to an object.
+ *
+ * @param gravity The desired placement of the object, as defined by the
+ * constants in this class.
+ * @param w The horizontal size of the object.
+ * @param h The vertical size of the object.
+ * @param container The frame of the containing space, in which the object
+ * will be placed. Should be large enough to contain the
+ * width and height of the object.
+ * @param outRect Receives the computed frame of the object in its
+ * container.
+ */
+ public static void apply(int gravity, int w, int h, Rect container,
+ Rect outRect) {
+ apply(gravity, w, h, container, 0, 0, outRect);
+ }
+
+ /**
+ * Apply a gravity constant to an object.
+ *
+ * @param gravity The desired placement of the object, as defined by the
+ * constants in this class.
+ * @param w The horizontal size of the object.
+ * @param h The vertical size of the object.
+ * @param container The frame of the containing space, in which the object
+ * will be placed. Should be large enough to contain the
+ * width and height of the object.
+ * @param xAdj Offset to apply to the X axis. If gravity is LEFT this
+ * pushes it to the right; if gravity is RIGHT it pushes it to
+ * the left; if gravity is CENTER_HORIZONTAL it pushes it to the
+ * right or left; otherwise it is ignored.
+ * @param yAdj Offset to apply to the Y axis. If gravity is TOP this pushes
+ * it down; if gravity is BOTTOM it pushes it up; if gravity is
+ * CENTER_VERTICAL it pushes it down or up; otherwise it is
+ * ignored.
+ * @param outRect Receives the computed frame of the object in its
+ * container.
+ */
+ public static void apply(int gravity, int w, int h, Rect container,
+ int xAdj, int yAdj, Rect outRect) {
+ if ((gravity&((AXIS_PULL_BEFORE|AXIS_PULL_AFTER)<<AXIS_X_SHIFT))
+ == ((AXIS_PULL_BEFORE|AXIS_PULL_AFTER)<<AXIS_X_SHIFT)) {
+ outRect.left = container.left;
+ outRect.right = container.right;
+ } else {
+ outRect.left = applyMovement(
+ gravity>>AXIS_X_SHIFT, w, container.left, container.right, xAdj);
+ outRect.right = outRect.left + w;
+ }
+
+ if ((gravity&((AXIS_PULL_BEFORE|AXIS_PULL_AFTER)<<AXIS_Y_SHIFT))
+ == ((AXIS_PULL_BEFORE|AXIS_PULL_AFTER)<<AXIS_Y_SHIFT)) {
+ outRect.top = container.top;
+ outRect.bottom = container.bottom;
+ } else {
+ outRect.top = applyMovement(
+ gravity>>AXIS_Y_SHIFT, h, container.top, container.bottom, yAdj);
+ outRect.bottom = outRect.top + h;
+ }
+ }
+
+ /**
+ * <p>Indicate whether the supplied gravity has a vertical pull.</p>
+ *
+ * @param gravity the gravity to check for vertical pull
+ * @return true if the supplied gravity has a vertical pull
+ */
+ public static boolean isVertical(int gravity) {
+ return gravity > 0 && (gravity & VERTICAL_GRAVITY_MASK) != 0;
+ }
+
+ /**
+ * <p>Indicate whether the supplied gravity has an horizontal pull.</p>
+ *
+ * @param gravity the gravity to check for horizontal pull
+ * @return true if the supplied gravity has an horizontal pull
+ */
+ public static boolean isHorizontal(int gravity) {
+ return gravity > 0 && (gravity & HORIZONTAL_GRAVITY_MASK) != 0;
+ }
+
+ private static int applyMovement(int mode, int size,
+ int start, int end, int adj) {
+ if ((mode & AXIS_PULL_BEFORE) != 0) {
+ return start + adj;
+ }
+
+ if ((mode & AXIS_PULL_AFTER) != 0) {
+ return end - size - adj;
+ }
+
+ return start + ((end - start - size)/2) + adj;
+ }
+}
+
diff --git a/core/java/android/view/IApplicationToken.aidl b/core/java/android/view/IApplicationToken.aidl
new file mode 100644
index 0000000..6bff5b3
--- /dev/null
+++ b/core/java/android/view/IApplicationToken.aidl
@@ -0,0 +1,28 @@
+/* //device/java/android/android/view/IApplicationToken.aidl
+**
+** Copyright 2007, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.view;
+
+/** {@hide} */
+interface IApplicationToken
+{
+ void windowsVisible();
+ void windowsGone();
+ boolean keyDispatchingTimedOut();
+ long getKeyDispatchingTimeout();
+}
+
diff --git a/core/java/android/view/IOnKeyguardExitResult.aidl b/core/java/android/view/IOnKeyguardExitResult.aidl
new file mode 100644
index 0000000..47d5220
--- /dev/null
+++ b/core/java/android/view/IOnKeyguardExitResult.aidl
@@ -0,0 +1,25 @@
+/* //device/java/android/android/hardware/ISensorListener.aidl
+**
+** Copyright 2008, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.view;
+
+/** @hide */
+oneway interface IOnKeyguardExitResult {
+
+ void onKeyguardExitResult(boolean success);
+
+}
diff --git a/core/java/android/view/IRotationWatcher.aidl b/core/java/android/view/IRotationWatcher.aidl
new file mode 100644
index 0000000..2c83642
--- /dev/null
+++ b/core/java/android/view/IRotationWatcher.aidl
@@ -0,0 +1,25 @@
+/* //device/java/android/android/hardware/ISensorListener.aidl
+**
+** Copyright 2008, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.view;
+
+/**
+ * {@hide}
+ */
+oneway interface IRotationWatcher {
+ void onRotationChanged(int rotation);
+}
diff --git a/core/java/android/view/IWindow.aidl b/core/java/android/view/IWindow.aidl
new file mode 100644
index 0000000..b4a3067
--- /dev/null
+++ b/core/java/android/view/IWindow.aidl
@@ -0,0 +1,57 @@
+/* //device/java/android/android/view/IWindow.aidl
+**
+** Copyright 2007, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.view;
+
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+import android.os.ParcelFileDescriptor;
+
+/**
+ * API back to a client window that the Window Manager uses to inform it of
+ * interesting things happening.
+ *
+ * {@hide}
+ */
+oneway interface IWindow {
+ /**
+ * ===== NOTICE =====
+ * The first method must remain the first method. Scripts
+ * and tools rely on their transaction number to work properly.
+ */
+
+ /**
+ * Invoked by the view server to tell a window to execute the specified
+ * command. Any response from the receiver must be sent through the
+ * specified file descriptor.
+ */
+ void executeCommand(String command, String parameters, in ParcelFileDescriptor descriptor);
+
+ void resized(int w, int h, boolean reportDraw);
+ void dispatchKey(in KeyEvent event);
+ void dispatchPointer(in MotionEvent event, long eventTime);
+ void dispatchTrackball(in MotionEvent event, long eventTime);
+ void dispatchAppVisibility(boolean visible);
+ void dispatchGetNewSurface();
+
+ /**
+ * Tell the window that it is either gaining or losing focus. Keep it up
+ * to date on the current state showing navigational focus (touch mode) too.
+ */
+ void windowFocusChanged(boolean hasFocus, boolean inTouchMode);
+}
diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl
new file mode 100644
index 0000000..e6d52e2
--- /dev/null
+++ b/core/java/android/view/IWindowManager.aidl
@@ -0,0 +1,125 @@
+/* //device/java/android/android/view/IWindowManager.aidl
+**
+** Copyright 2006, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.view;
+
+import android.content.res.Configuration;
+import android.view.IApplicationToken;
+import android.view.IOnKeyguardExitResult;
+import android.view.IRotationWatcher;
+import android.view.IWindowSession;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+/**
+ * System private interface to the window manager.
+ *
+ * {@hide}
+ */
+interface IWindowManager
+{
+ /**
+ * ===== NOTICE =====
+ * The first three methods must remain the first three methods. Scripts
+ * and tools rely on their transaction number to work properly.
+ */
+ // This is used for debugging
+ boolean startViewServer(int port); // Transaction #1
+ boolean stopViewServer(); // Transaction #2
+ boolean isViewServerRunning(); // Transaction #3
+
+ IWindowSession openSession(IBinder token);
+
+ // These can only be called when injecting events to your own window,
+ // or by holding the INJECT_EVENTS permission.
+ boolean injectKeyEvent(in KeyEvent ev, boolean sync);
+ boolean injectPointerEvent(in MotionEvent ev, boolean sync);
+ boolean injectTrackballEvent(in MotionEvent ev, boolean sync);
+
+ // These can only be called when holding the MANAGE_APP_TOKENS permission.
+ void pauseKeyDispatching(IBinder token);
+ void resumeKeyDispatching(IBinder token);
+ void setEventDispatching(boolean enabled);
+ void addAppToken(int addPos, IApplicationToken token,
+ int groupId, int requestedOrientation, boolean fullscreen);
+ void setAppGroupId(IBinder token, int groupId);
+ Configuration updateOrientationFromAppTokens(IBinder freezeThisOneIfNeeded);
+ void setAppOrientation(IApplicationToken token, int requestedOrientation);
+ int getAppOrientation(IApplicationToken token);
+ void setFocusedApp(IBinder token, boolean moveFocusNow);
+ void prepareAppTransition(int transit);
+ void executeAppTransition();
+ void setAppStartingWindow(IBinder token, String pkg, int theme,
+ CharSequence nonLocalizedLabel, int labelRes,
+ int icon, IBinder transferFrom, boolean createIfNeeded);
+ void setAppWillBeHidden(IBinder token);
+ void setAppVisibility(IBinder token, boolean visible);
+ void startAppFreezingScreen(IBinder token, int configChanges);
+ void stopAppFreezingScreen(IBinder token, boolean force);
+ void removeAppToken(IBinder token);
+ void moveAppToken(int index, IBinder token);
+ void moveAppTokensToTop(in List<IBinder> tokens);
+ void moveAppTokensToBottom(in List<IBinder> tokens);
+
+ // these require DISABLE_KEYGUARD permission
+ void disableKeyguard(IBinder token, String tag);
+ void reenableKeyguard(IBinder token);
+ void exitKeyguardSecurely(IOnKeyguardExitResult callback);
+ boolean inKeyguardRestrictedInputMode();
+
+
+ // These can only be called with the SET_ANIMATON_SCALE permission.
+ float getAnimationScale(int which);
+ float[] getAnimationScales();
+ void setAnimationScale(int which, float scale);
+ void setAnimationScales(in float[] scales);
+
+ // These require the READ_INPUT_STATE permission.
+ int getSwitchState(int sw);
+ int getSwitchStateForDevice(int devid, int sw);
+ int getScancodeState(int sw);
+ int getScancodeStateForDevice(int devid, int sw);
+ int getKeycodeState(int sw);
+ int getKeycodeStateForDevice(int devid, int sw);
+
+ // For testing
+ void setInTouchMode(boolean showFocus);
+
+ // These can only be called with the SET_ORIENTATION permission.
+ /**
+ * Change the current screen rotation, constants as per
+ * {@link android.view.Surface}.
+ * @param rotation the intended rotation.
+ * @param alwaysSendConfiguration Flag to force a new configuration to
+ * be evaluated. This can be used when there are other parameters in
+ * configuration that are changing.
+ * {@link android.view.Surface}.
+ */
+ void setRotation(int rotation, boolean alwaysSendConfiguration);
+
+ /**
+ * Retrieve the current screen orientation, constants as per
+ * {@link android.view.Surface}.
+ */
+ int getRotation();
+
+ /**
+ * Watch the rotation of the screen. Returns the current rotation,
+ * calls back when it changes.
+ */
+ int watchRotation(IRotationWatcher watcher);
+}
diff --git a/core/java/android/view/IWindowSession.aidl b/core/java/android/view/IWindowSession.aidl
new file mode 100644
index 0000000..c2c0b97
--- /dev/null
+++ b/core/java/android/view/IWindowSession.aidl
@@ -0,0 +1,73 @@
+/* //device/java/android/android/view/IWindowSession.aidl
+**
+** Copyright 2006, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.view;
+
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.view.IWindow;
+import android.view.MotionEvent;
+import android.view.WindowManager;
+import android.view.Surface;
+
+/**
+ * System private per-application interface to the window manager.
+ *
+ * {@hide}
+ */
+interface IWindowSession {
+ int add(IWindow window, in WindowManager.LayoutParams attrs,
+ in int viewVisibility, out Rect outCoveredInsets);
+ void remove(IWindow window);
+
+ /**
+ * Change the parameters of a window. You supply the
+ * new parameters, it returns the new frame of the window on screen (the
+ * position should be ignored) and surface of the window. The surface
+ * will be invalid if the window is currently hidden, else you can use it
+ * to draw the window's contents.
+ *
+ * @param window The window being modified.
+ * @param attrs If non-null, new attributes to apply to the window.
+ * @param requestedWidth The width the window wants to be.
+ * @param requestedHeight The height the window wants to be.
+ * @param viewVisibility Window root view's visibility.
+ * @param outFrame Object in which is placed the new position/size on
+ * screen.
+ * @param outCoveredInsets Object in which is placed the insets for the areas covered by
+ * system windows (e.g. status bar)
+ * @param outSurface Object in which is placed the new display surface.
+ *
+ * @return int Result flags: {@link WindowManagerImpl#RELAYOUT_SHOW_FOCUS},
+ * {@link WindowManagerImpl#RELAYOUT_FIRST_TIME}.
+ */
+ int relayout(IWindow window, in WindowManager.LayoutParams attrs,
+ int requestedWidth, int requestedHeight, int viewVisibility,
+ out Rect outFrame, out Rect outCoveredInsets, out Surface outSurface);
+
+ void finishDrawing(IWindow window);
+
+ void finishKey(IWindow window);
+ MotionEvent getPendingPointerMove(IWindow window);
+ MotionEvent getPendingTrackballMove(IWindow window);
+
+ void setTransparentRegion(IWindow window, in Region region);
+
+ void setInTouchMode(boolean showFocus);
+ boolean getInTouchMode();
+}
+
diff --git a/core/java/android/view/InflateException.java b/core/java/android/view/InflateException.java
new file mode 100644
index 0000000..7b39d33
--- /dev/null
+++ b/core/java/android/view/InflateException.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+/**
+ * This exception is thrown by an inflater on error conditions.
+ */
+public class InflateException extends RuntimeException {
+
+ public InflateException() {
+ super();
+ }
+
+ public InflateException(String detailMessage, Throwable throwable) {
+ super(detailMessage, throwable);
+ }
+
+ public InflateException(String detailMessage) {
+ super(detailMessage);
+ }
+
+ public InflateException(Throwable throwable) {
+ super(throwable);
+ }
+
+}
diff --git a/core/java/android/view/KeyCharacterMap.java b/core/java/android/view/KeyCharacterMap.java
new file mode 100644
index 0000000..0347d50
--- /dev/null
+++ b/core/java/android/view/KeyCharacterMap.java
@@ -0,0 +1,521 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.text.method.MetaKeyKeyListener;
+import android.util.SparseIntArray;
+import android.os.SystemClock;
+import android.util.SparseArray;
+
+import java.lang.Character;
+import java.lang.ref.WeakReference;
+
+public class KeyCharacterMap
+{
+ /**
+ * The id of the device's primary built in keyboard is always 0.
+ */
+ public static final int BUILT_IN_KEYBOARD = 0;
+
+ /** A numeric (12-key) keyboard. */
+ public static final int NUMERIC = 1;
+
+ /** A keyboard with all the letters, but with more than one letter
+ * per key. */
+ public static final int PREDICTIVE = 2;
+
+ /** A keyboard with all the letters, and maybe some numbers. */
+ public static final int ALPHA = 3;
+
+ /**
+ * This private-use character is used to trigger Unicode character
+ * input by hex digits.
+ */
+ public static final char HEX_INPUT = '\uEF00';
+
+ /**
+ * This private-use character is used to bring up a character picker for
+ * miscellaneous symbols.
+ */
+ public static final char PICKER_DIALOG_INPUT = '\uEF01';
+
+ private static Object sLock = new Object();
+ private static SparseArray<WeakReference<KeyCharacterMap>> sInstances
+ = new SparseArray<WeakReference<KeyCharacterMap>>();
+
+ public static KeyCharacterMap load(int keyboard)
+ {
+ synchronized (sLock) {
+ KeyCharacterMap result;
+ WeakReference<KeyCharacterMap> ref = sInstances.get(keyboard);
+ if (ref != null) {
+ result = ref.get();
+ if (result != null) {
+ return result;
+ }
+ }
+ result = new KeyCharacterMap(keyboard);
+ sInstances.put(keyboard, new WeakReference<KeyCharacterMap>(result));
+ return result;
+ }
+ }
+
+ private KeyCharacterMap(int keyboardDevice)
+ {
+ mKeyboardDevice = keyboardDevice;
+ mPointer = ctor_native(keyboardDevice);
+ }
+
+ /**
+ * <p>
+ * Returns the Unicode character that the specified key would produce
+ * when the specified meta bits (see {@link MetaKeyKeyListener})
+ * were active.
+ * </p><p>
+ * Returns 0 if the key is not one that is used to type Unicode
+ * characters.
+ * </p><p>
+ * If the return value has bit {@link #COMBINING_ACCENT} set, the
+ * key is a "dead key" that should be combined with another to
+ * actually produce a character -- see {@link #getDeadChar} --
+ * after masking with {@link #COMBINING_ACCENT_MASK}.
+ * </p>
+ */
+ public int get(int keyCode, int meta)
+ {
+ if ((meta & MetaKeyKeyListener.META_CAP_LOCKED) != 0) {
+ meta |= KeyEvent.META_SHIFT_ON;
+ }
+ if ((meta & MetaKeyKeyListener.META_ALT_LOCKED) != 0) {
+ meta |= KeyEvent.META_ALT_ON;
+ }
+
+ // Ignore caps lock on keys where alt and shift have the same effect.
+ if ((meta & MetaKeyKeyListener.META_CAP_LOCKED) != 0) {
+ if (get_native(mPointer, keyCode, KeyEvent.META_SHIFT_ON) ==
+ get_native(mPointer, keyCode, KeyEvent.META_ALT_ON)) {
+ meta &= ~KeyEvent.META_SHIFT_ON;
+ }
+ }
+
+ int ret = get_native(mPointer, keyCode, meta);
+ int map = COMBINING.get(ret);
+
+ if (map != 0) {
+ return map;
+ } else {
+ return ret;
+ }
+ }
+
+ /**
+ * Gets the number or symbol associated with the key. The character value
+ * is returned, not the numeric value. If the key is not a number, but is
+ * a symbol, the symbol is retuned.
+ */
+ public char getNumber(int keyCode)
+ {
+ return getNumber_native(mPointer, keyCode);
+ }
+
+ /**
+ * The same as {@link #getMatch(int,char[],int) getMatch(keyCode, chars, 0)}.
+ */
+ public char getMatch(int keyCode, char[] chars)
+ {
+ return getMatch(keyCode, chars, 0);
+ }
+
+ /**
+ * If one of the chars in the array can be generated by keyCode,
+ * return the char; otherwise return '\0'.
+ * @param keyCode the key code to look at
+ * @param chars the characters to try to find
+ * @param modifiers the modifier bits to prefer. If any of these bits
+ * are set, if there are multiple choices, that could
+ * work, the one for this modifier will be set.
+ */
+ public char getMatch(int keyCode, char[] chars, int modifiers)
+ {
+ if (chars == null) {
+ // catch it here instead of in native
+ throw new NullPointerException();
+ }
+ return getMatch_native(mPointer, keyCode, chars, modifiers);
+ }
+
+ /**
+ * Get the primary character for this key. In other words, the label
+ * that is physically printed on it.
+ */
+ public char getDisplayLabel(int keyCode)
+ {
+ return getDisplayLabel_native(mPointer, keyCode);
+ }
+
+ /**
+ * Get the character that is produced by putting accent on the character
+ * c.
+ * For example, getDeadChar('`', 'e') returns &egrave;.
+ */
+ public static int getDeadChar(int accent, int c)
+ {
+ return DEAD.get((accent << 16) | c);
+ }
+
+ public static class KeyData {
+ public static final int META_LENGTH = 4;
+
+ /**
+ * The display label (see {@link #getDisplayLabel}).
+ */
+ public char displayLabel;
+ /**
+ * The "number" value (see {@link #getNumber}).
+ */
+ public char number;
+ /**
+ * The character that will be generated in various meta states
+ * (the same ones used for {@link #get} and defined as
+ * {@link KeyEvent#META_SHIFT_ON} and {@link KeyEvent#META_ALT_ON}).
+ * <table>
+ * <tr><th>Index</th><th align="left">Value</th></tr>
+ * <tr><td>0</td><td>no modifiers</td></tr>
+ * <tr><td>1</td><td>caps</td></tr>
+ * <tr><td>2</td><td>alt</td></tr>
+ * <tr><td>3</td><td>caps + alt</td></tr>
+ * </table>
+ */
+ public char[] meta = new char[META_LENGTH];
+ }
+
+ /**
+ * Get the characters conversion data for a given keyCode.
+ *
+ * @param keyCode the keyCode to look for
+ * @param results a {@link KeyData} that will be filled with the results.
+ *
+ * @return whether the key was mapped or not. If the key was not mapped,
+ * results is not modified.
+ */
+ public boolean getKeyData(int keyCode, KeyData results)
+ {
+ if (results.meta.length >= KeyData.META_LENGTH) {
+ return getKeyData_native(mPointer, keyCode, results);
+ } else {
+ throw new IndexOutOfBoundsException("results.meta.length must be >= " +
+ KeyData.META_LENGTH);
+ }
+ }
+
+ /**
+ * Get an array of KeyEvent objects that if put into the input stream
+ * could plausibly generate the provided sequence of characters. It is
+ * not guaranteed that the sequence is the only way to generate these
+ * events or that it is optimal.
+ *
+ * @return an array of KeyEvent objects, or null if the given char array
+ * can not be generated using the current key character map.
+ */
+ public KeyEvent[] getEvents(char[] chars)
+ {
+ if (chars == null) {
+ throw new NullPointerException();
+ }
+
+ long[] keys = getEvents_native(mPointer, chars);
+ if (keys == null) {
+ return null;
+ }
+
+ // how big should the array be
+ int len = keys.length*2;
+ int N = keys.length;
+ for (int i=0; i<N; i++) {
+ int mods = (int)(keys[i] >> 32);
+ if ((mods & KeyEvent.META_ALT_ON) != 0) {
+ len += 2;
+ }
+ if ((mods & KeyEvent.META_SHIFT_ON) != 0) {
+ len += 2;
+ }
+ if ((mods & KeyEvent.META_SYM_ON) != 0) {
+ len += 2;
+ }
+ }
+
+ // create the events
+ KeyEvent[] rv = new KeyEvent[len];
+ int index = 0;
+ long now = SystemClock.uptimeMillis();
+ int device = mKeyboardDevice;
+ for (int i=0; i<N; i++) {
+ int mods = (int)(keys[i] >> 32);
+ int meta = 0;
+
+ if ((mods & KeyEvent.META_ALT_ON) != 0) {
+ meta |= KeyEvent.META_ALT_ON;
+ rv[index] = new KeyEvent(now, now, KeyEvent.ACTION_DOWN,
+ KeyEvent.KEYCODE_ALT_LEFT, 0, meta, device, 0);
+ index++;
+ }
+ if ((mods & KeyEvent.META_SHIFT_ON) != 0) {
+ meta |= KeyEvent.META_SHIFT_ON;
+ rv[index] = new KeyEvent(now, now, KeyEvent.ACTION_DOWN,
+ KeyEvent.KEYCODE_SHIFT_LEFT, 0, meta, device, 0);
+ index++;
+ }
+ if ((mods & KeyEvent.META_SYM_ON) != 0) {
+ meta |= KeyEvent.META_SYM_ON;
+ rv[index] = new KeyEvent(now, now, KeyEvent.ACTION_DOWN,
+ KeyEvent.KEYCODE_SYM, 0, meta, device, 0);
+ index++;
+ }
+
+ int key = (int)(keys[i]);
+ rv[index] = new KeyEvent(now, now, KeyEvent.ACTION_DOWN,
+ key, 0, meta, device, 0);
+ index++;
+ rv[index] = new KeyEvent(now, now, KeyEvent.ACTION_UP,
+ key, 0, meta, device, 0);
+ index++;
+
+ if ((mods & KeyEvent.META_ALT_ON) != 0) {
+ meta &= ~KeyEvent.META_ALT_ON;
+ rv[index] = new KeyEvent(now, now, KeyEvent.ACTION_UP,
+ KeyEvent.KEYCODE_ALT_LEFT, 0, meta, device, 0);
+ index++;
+ }
+ if ((mods & KeyEvent.META_SHIFT_ON) != 0) {
+ meta &= ~KeyEvent.META_SHIFT_ON;
+ rv[index] = new KeyEvent(now, now, KeyEvent.ACTION_UP,
+ KeyEvent.KEYCODE_SHIFT_LEFT, 0, meta, device, 0);
+ index++;
+ }
+ if ((mods & KeyEvent.META_SYM_ON) != 0) {
+ meta &= ~KeyEvent.META_SYM_ON;
+ rv[index] = new KeyEvent(now, now, KeyEvent.ACTION_UP,
+ KeyEvent.KEYCODE_SYM, 0, meta, device, 0);
+ index++;
+ }
+ }
+
+ return rv;
+ }
+
+ /**
+ * Does this character key produce a glyph?
+ */
+ public boolean isPrintingKey(int keyCode)
+ {
+ int type = Character.getType(get(keyCode, 0));
+
+ switch (type)
+ {
+ case Character.SPACE_SEPARATOR:
+ case Character.LINE_SEPARATOR:
+ case Character.PARAGRAPH_SEPARATOR:
+ case Character.CONTROL:
+ case Character.FORMAT:
+ return false;
+ default:
+ return true;
+ }
+ }
+
+ protected void finalize() throws Throwable
+ {
+ dtor_native(mPointer);
+ }
+
+ /**
+ * Returns {@link #NUMERIC}, {@link #PREDICTIVE} or {@link #ALPHA}.
+ */
+ public int getKeyboardType()
+ {
+ return getKeyboardType_native(mPointer);
+ }
+
+ private int mPointer;
+ private int mKeyboardDevice;
+
+ private static native int ctor_native(int id);
+ private static native void dtor_native(int ptr);
+ private static native char get_native(int ptr, int keycode,
+ int meta);
+ private static native char getNumber_native(int ptr, int keycode);
+ private static native char getMatch_native(int ptr, int keycode,
+ char[] chars, int modifiers);
+ private static native char getDisplayLabel_native(int ptr, int keycode);
+ private static native boolean getKeyData_native(int ptr, int keycode,
+ KeyData results);
+ private static native int getKeyboardType_native(int ptr);
+ private static native long[] getEvents_native(int ptr, char[] str);
+
+ /**
+ * Maps Unicode combining diacritical to display-form dead key
+ * (display character shifted left 16 bits).
+ */
+ private static SparseIntArray COMBINING = new SparseIntArray();
+
+ /**
+ * Maps combinations of (display-form) dead key and second character
+ * to combined output character.
+ */
+ private static SparseIntArray DEAD = new SparseIntArray();
+
+ /*
+ * TODO: Change the table format to support full 21-bit-wide
+ * accent characters and combined characters if ever necessary.
+ */
+ private static final int ACUTE = '\u00B4' << 16;
+ private static final int GRAVE = '`' << 16;
+ private static final int CIRCUMFLEX = '^' << 16;
+ private static final int TILDE = '~' << 16;
+ private static final int UMLAUT = '\u00A8' << 16;
+
+ /*
+ * This bit will be set in the return value of {@link #get(int, int)} if the
+ * key is a "dead key."
+ */
+ public static final int COMBINING_ACCENT = 0x80000000;
+ /**
+ * Mask the return value from {@link #get(int, int)} with this value to get
+ * a printable representation of the accent character of a "dead key."
+ */
+ public static final int COMBINING_ACCENT_MASK = 0x7FFFFFFF;
+
+ static {
+ COMBINING.put('\u0300', (GRAVE >> 16) | COMBINING_ACCENT);
+ COMBINING.put('\u0301', (ACUTE >> 16) | COMBINING_ACCENT);
+ COMBINING.put('\u0302', (CIRCUMFLEX >> 16) | COMBINING_ACCENT);
+ COMBINING.put('\u0303', (TILDE >> 16) | COMBINING_ACCENT);
+ COMBINING.put('\u0308', (UMLAUT >> 16) | COMBINING_ACCENT);
+
+ DEAD.put(ACUTE | 'A', '\u00C1');
+ DEAD.put(ACUTE | 'C', '\u0106');
+ DEAD.put(ACUTE | 'E', '\u00C9');
+ DEAD.put(ACUTE | 'G', '\u01F4');
+ DEAD.put(ACUTE | 'I', '\u00CD');
+ DEAD.put(ACUTE | 'K', '\u1E30');
+ DEAD.put(ACUTE | 'L', '\u0139');
+ DEAD.put(ACUTE | 'M', '\u1E3E');
+ DEAD.put(ACUTE | 'N', '\u0143');
+ DEAD.put(ACUTE | 'O', '\u00D3');
+ DEAD.put(ACUTE | 'P', '\u1E54');
+ DEAD.put(ACUTE | 'R', '\u0154');
+ DEAD.put(ACUTE | 'S', '\u015A');
+ DEAD.put(ACUTE | 'U', '\u00DA');
+ DEAD.put(ACUTE | 'W', '\u1E82');
+ DEAD.put(ACUTE | 'Y', '\u00DD');
+ DEAD.put(ACUTE | 'Z', '\u0179');
+ DEAD.put(ACUTE | 'a', '\u00E1');
+ DEAD.put(ACUTE | 'c', '\u0107');
+ DEAD.put(ACUTE | 'e', '\u00E9');
+ DEAD.put(ACUTE | 'g', '\u01F5');
+ DEAD.put(ACUTE | 'i', '\u00ED');
+ DEAD.put(ACUTE | 'k', '\u1E31');
+ DEAD.put(ACUTE | 'l', '\u013A');
+ DEAD.put(ACUTE | 'm', '\u1E3F');
+ DEAD.put(ACUTE | 'n', '\u0144');
+ DEAD.put(ACUTE | 'o', '\u00F3');
+ DEAD.put(ACUTE | 'p', '\u1E55');
+ DEAD.put(ACUTE | 'r', '\u0155');
+ DEAD.put(ACUTE | 's', '\u015B');
+ DEAD.put(ACUTE | 'u', '\u00FA');
+ DEAD.put(ACUTE | 'w', '\u1E83');
+ DEAD.put(ACUTE | 'y', '\u00FD');
+ DEAD.put(ACUTE | 'z', '\u017A');
+ DEAD.put(CIRCUMFLEX | 'A', '\u00C2');
+ DEAD.put(CIRCUMFLEX | 'C', '\u0108');
+ DEAD.put(CIRCUMFLEX | 'E', '\u00CA');
+ DEAD.put(CIRCUMFLEX | 'G', '\u011C');
+ DEAD.put(CIRCUMFLEX | 'H', '\u0124');
+ DEAD.put(CIRCUMFLEX | 'I', '\u00CE');
+ DEAD.put(CIRCUMFLEX | 'J', '\u0134');
+ DEAD.put(CIRCUMFLEX | 'O', '\u00D4');
+ DEAD.put(CIRCUMFLEX | 'S', '\u015C');
+ DEAD.put(CIRCUMFLEX | 'U', '\u00DB');
+ DEAD.put(CIRCUMFLEX | 'W', '\u0174');
+ DEAD.put(CIRCUMFLEX | 'Y', '\u0176');
+ DEAD.put(CIRCUMFLEX | 'Z', '\u1E90');
+ DEAD.put(CIRCUMFLEX | 'a', '\u00E2');
+ DEAD.put(CIRCUMFLEX | 'c', '\u0109');
+ DEAD.put(CIRCUMFLEX | 'e', '\u00EA');
+ DEAD.put(CIRCUMFLEX | 'g', '\u011D');
+ DEAD.put(CIRCUMFLEX | 'h', '\u0125');
+ DEAD.put(CIRCUMFLEX | 'i', '\u00EE');
+ DEAD.put(CIRCUMFLEX | 'j', '\u0135');
+ DEAD.put(CIRCUMFLEX | 'o', '\u00F4');
+ DEAD.put(CIRCUMFLEX | 's', '\u015D');
+ DEAD.put(CIRCUMFLEX | 'u', '\u00FB');
+ DEAD.put(CIRCUMFLEX | 'w', '\u0175');
+ DEAD.put(CIRCUMFLEX | 'y', '\u0177');
+ DEAD.put(CIRCUMFLEX | 'z', '\u1E91');
+ DEAD.put(GRAVE | 'A', '\u00C0');
+ DEAD.put(GRAVE | 'E', '\u00C8');
+ DEAD.put(GRAVE | 'I', '\u00CC');
+ DEAD.put(GRAVE | 'N', '\u01F8');
+ DEAD.put(GRAVE | 'O', '\u00D2');
+ DEAD.put(GRAVE | 'U', '\u00D9');
+ DEAD.put(GRAVE | 'W', '\u1E80');
+ DEAD.put(GRAVE | 'Y', '\u1EF2');
+ DEAD.put(GRAVE | 'a', '\u00E0');
+ DEAD.put(GRAVE | 'e', '\u00E8');
+ DEAD.put(GRAVE | 'i', '\u00EC');
+ DEAD.put(GRAVE | 'n', '\u01F9');
+ DEAD.put(GRAVE | 'o', '\u00F2');
+ DEAD.put(GRAVE | 'u', '\u00F9');
+ DEAD.put(GRAVE | 'w', '\u1E81');
+ DEAD.put(GRAVE | 'y', '\u1EF3');
+ DEAD.put(TILDE | 'A', '\u00C3');
+ DEAD.put(TILDE | 'E', '\u1EBC');
+ DEAD.put(TILDE | 'I', '\u0128');
+ DEAD.put(TILDE | 'N', '\u00D1');
+ DEAD.put(TILDE | 'O', '\u00D5');
+ DEAD.put(TILDE | 'U', '\u0168');
+ DEAD.put(TILDE | 'V', '\u1E7C');
+ DEAD.put(TILDE | 'Y', '\u1EF8');
+ DEAD.put(TILDE | 'a', '\u00E3');
+ DEAD.put(TILDE | 'e', '\u1EBD');
+ DEAD.put(TILDE | 'i', '\u0129');
+ DEAD.put(TILDE | 'n', '\u00F1');
+ DEAD.put(TILDE | 'o', '\u00F5');
+ DEAD.put(TILDE | 'u', '\u0169');
+ DEAD.put(TILDE | 'v', '\u1E7D');
+ DEAD.put(TILDE | 'y', '\u1EF9');
+ DEAD.put(UMLAUT | 'A', '\u00C4');
+ DEAD.put(UMLAUT | 'E', '\u00CB');
+ DEAD.put(UMLAUT | 'H', '\u1E26');
+ DEAD.put(UMLAUT | 'I', '\u00CF');
+ DEAD.put(UMLAUT | 'O', '\u00D6');
+ DEAD.put(UMLAUT | 'U', '\u00DC');
+ DEAD.put(UMLAUT | 'W', '\u1E84');
+ DEAD.put(UMLAUT | 'X', '\u1E8C');
+ DEAD.put(UMLAUT | 'Y', '\u0178');
+ DEAD.put(UMLAUT | 'a', '\u00E4');
+ DEAD.put(UMLAUT | 'e', '\u00EB');
+ DEAD.put(UMLAUT | 'h', '\u1E27');
+ DEAD.put(UMLAUT | 'i', '\u00EF');
+ DEAD.put(UMLAUT | 'o', '\u00F6');
+ DEAD.put(UMLAUT | 't', '\u1E97');
+ DEAD.put(UMLAUT | 'u', '\u00FC');
+ DEAD.put(UMLAUT | 'w', '\u1E85');
+ DEAD.put(UMLAUT | 'x', '\u1E8D');
+ DEAD.put(UMLAUT | 'y', '\u00FF');
+ }
+}
diff --git a/core/java/android/view/KeyEvent.aidl b/core/java/android/view/KeyEvent.aidl
new file mode 100644
index 0000000..dc15ecf
--- /dev/null
+++ b/core/java/android/view/KeyEvent.aidl
@@ -0,0 +1,20 @@
+/* //device/java/android/android.view.KeyEvent.aidl
+**
+** Copyright 2007, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.view;
+
+parcelable KeyEvent;
diff --git a/core/java/android/view/KeyEvent.java b/core/java/android/view/KeyEvent.java
new file mode 100644
index 0000000..24b0316
--- /dev/null
+++ b/core/java/android/view/KeyEvent.java
@@ -0,0 +1,786 @@
+/*
+ * 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.view;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.view.KeyCharacterMap;
+import android.view.KeyCharacterMap.KeyData;
+
+/**
+ * Contains constants for key events.
+ */
+public class KeyEvent implements Parcelable {
+ // key codes
+ public static final int KEYCODE_UNKNOWN = 0;
+ public static final int KEYCODE_SOFT_LEFT = 1;
+ public static final int KEYCODE_SOFT_RIGHT = 2;
+ public static final int KEYCODE_HOME = 3;
+ public static final int KEYCODE_BACK = 4;
+ public static final int KEYCODE_CALL = 5;
+ public static final int KEYCODE_ENDCALL = 6;
+ public static final int KEYCODE_0 = 7;
+ public static final int KEYCODE_1 = 8;
+ public static final int KEYCODE_2 = 9;
+ public static final int KEYCODE_3 = 10;
+ public static final int KEYCODE_4 = 11;
+ public static final int KEYCODE_5 = 12;
+ public static final int KEYCODE_6 = 13;
+ public static final int KEYCODE_7 = 14;
+ public static final int KEYCODE_8 = 15;
+ public static final int KEYCODE_9 = 16;
+ public static final int KEYCODE_STAR = 17;
+ public static final int KEYCODE_POUND = 18;
+ public static final int KEYCODE_DPAD_UP = 19;
+ public static final int KEYCODE_DPAD_DOWN = 20;
+ public static final int KEYCODE_DPAD_LEFT = 21;
+ public static final int KEYCODE_DPAD_RIGHT = 22;
+ public static final int KEYCODE_DPAD_CENTER = 23;
+ public static final int KEYCODE_VOLUME_UP = 24;
+ public static final int KEYCODE_VOLUME_DOWN = 25;
+ public static final int KEYCODE_POWER = 26;
+ public static final int KEYCODE_CAMERA = 27;
+ public static final int KEYCODE_CLEAR = 28;
+ public static final int KEYCODE_A = 29;
+ public static final int KEYCODE_B = 30;
+ public static final int KEYCODE_C = 31;
+ public static final int KEYCODE_D = 32;
+ public static final int KEYCODE_E = 33;
+ public static final int KEYCODE_F = 34;
+ public static final int KEYCODE_G = 35;
+ public static final int KEYCODE_H = 36;
+ public static final int KEYCODE_I = 37;
+ public static final int KEYCODE_J = 38;
+ public static final int KEYCODE_K = 39;
+ public static final int KEYCODE_L = 40;
+ public static final int KEYCODE_M = 41;
+ public static final int KEYCODE_N = 42;
+ public static final int KEYCODE_O = 43;
+ public static final int KEYCODE_P = 44;
+ public static final int KEYCODE_Q = 45;
+ public static final int KEYCODE_R = 46;
+ public static final int KEYCODE_S = 47;
+ public static final int KEYCODE_T = 48;
+ public static final int KEYCODE_U = 49;
+ public static final int KEYCODE_V = 50;
+ public static final int KEYCODE_W = 51;
+ public static final int KEYCODE_X = 52;
+ public static final int KEYCODE_Y = 53;
+ public static final int KEYCODE_Z = 54;
+ public static final int KEYCODE_COMMA = 55;
+ public static final int KEYCODE_PERIOD = 56;
+ public static final int KEYCODE_ALT_LEFT = 57;
+ public static final int KEYCODE_ALT_RIGHT = 58;
+ public static final int KEYCODE_SHIFT_LEFT = 59;
+ public static final int KEYCODE_SHIFT_RIGHT = 60;
+ public static final int KEYCODE_TAB = 61;
+ public static final int KEYCODE_SPACE = 62;
+ public static final int KEYCODE_SYM = 63;
+ public static final int KEYCODE_EXPLORER = 64;
+ public static final int KEYCODE_ENVELOPE = 65;
+ public static final int KEYCODE_ENTER = 66;
+ public static final int KEYCODE_DEL = 67;
+ public static final int KEYCODE_GRAVE = 68;
+ public static final int KEYCODE_MINUS = 69;
+ public static final int KEYCODE_EQUALS = 70;
+ public static final int KEYCODE_LEFT_BRACKET = 71;
+ public static final int KEYCODE_RIGHT_BRACKET = 72;
+ public static final int KEYCODE_BACKSLASH = 73;
+ public static final int KEYCODE_SEMICOLON = 74;
+ public static final int KEYCODE_APOSTROPHE = 75;
+ public static final int KEYCODE_SLASH = 76;
+ public static final int KEYCODE_AT = 77;
+ public static final int KEYCODE_NUM = 78;
+ public static final int KEYCODE_HEADSETHOOK = 79;
+ public static final int KEYCODE_FOCUS = 80; // *Camera* focus
+ public static final int KEYCODE_PLUS = 81;
+ public static final int KEYCODE_MENU = 82;
+ public static final int KEYCODE_NOTIFICATION = 83;
+ public static final int KEYCODE_SEARCH = 84;
+
+ // NOTE: If you add a new keycode here you must also add it to:
+ // isSystem()
+ // include/ui/KeycodeLabels.h
+ // tools/puppet_master/PuppetMaster/nav_keys.py
+ // apps/common/res/values/attrs.xml
+ // commands/monkey/Monkey.java
+ // emulator?
+
+ public static final int MAX_KEYCODE = 84;
+
+ /**
+ * {@link #getAction} value: the key has been pressed down.
+ */
+ public static final int ACTION_DOWN = 0;
+ /**
+ * {@link #getAction} value: the key has been released.
+ */
+ public static final int ACTION_UP = 1;
+ /**
+ * {@link #getAction} value: multiple duplicate key events have
+ * occurred in a row. The {#link {@link #getRepeatCount()} method returns
+ * the number of duplicates.
+ */
+ public static final int ACTION_MULTIPLE = 2;
+
+ /**
+ * <p>This mask is used to check whether one of the ALT meta keys is pressed.</p>
+ *
+ * @see #isAltPressed()
+ * @see #getMetaState()
+ * @see #KEYCODE_ALT_LEFT
+ * @see #KEYCODE_ALT_RIGHT
+ */
+ public static final int META_ALT_ON = 0x02;
+
+ /**
+ * <p>This mask is used to check whether the left ALT meta key is pressed.</p>
+ *
+ * @see #isAltPressed()
+ * @see #getMetaState()
+ * @see #KEYCODE_ALT_LEFT
+ */
+ public static final int META_ALT_LEFT_ON = 0x10;
+
+ /**
+ * <p>This mask is used to check whether the right the ALT meta key is pressed.</p>
+ *
+ * @see #isAltPressed()
+ * @see #getMetaState()
+ * @see #KEYCODE_ALT_RIGHT
+ */
+ public static final int META_ALT_RIGHT_ON = 0x20;
+
+ /**
+ * <p>This mask is used to check whether one of the SHIFT meta keys is pressed.</p>
+ *
+ * @see #isShiftPressed()
+ * @see #getMetaState()
+ * @see #KEYCODE_SHIFT_LEFT
+ * @see #KEYCODE_SHIFT_RIGHT
+ */
+ public static final int META_SHIFT_ON = 0x1;
+
+ /**
+ * <p>This mask is used to check whether the left SHIFT meta key is pressed.</p>
+ *
+ * @see #isShiftPressed()
+ * @see #getMetaState()
+ * @see #KEYCODE_SHIFT_LEFT
+ */
+ public static final int META_SHIFT_LEFT_ON = 0x40;
+
+ /**
+ * <p>This mask is used to check whether the right SHIFT meta key is pressed.</p>
+ *
+ * @see #isShiftPressed()
+ * @see #getMetaState()
+ * @see #KEYCODE_SHIFT_RIGHT
+ */
+ public static final int META_SHIFT_RIGHT_ON = 0x80;
+
+ /**
+ * <p>This mask is used to check whether the SYM meta key is pressed.</p>
+ *
+ * @see #isSymPressed()
+ * @see #getMetaState()
+ */
+ public static final int META_SYM_ON = 0x4;
+
+ /**
+ * This mask is set if the device woke because of this key event.
+ */
+ public static final int FLAG_WOKE_HERE = 0x1;
+
+ /**
+ * Get the character that is produced by putting accent on the character
+ * c.
+ * For example, getDeadChar('`', 'e') returns &egrave;.
+ */
+ public static int getDeadChar(int accent, int c) {
+ return KeyCharacterMap.getDeadChar(accent, c);
+ }
+
+ private int mMetaState;
+ private int mAction;
+ private int mKeyCode;
+ private int mScancode;
+ private int mRepeatCount;
+ private int mDeviceId;
+ private int mFlags;
+ private long mDownTime;
+ private long mEventTime;
+
+ public interface Callback {
+ /**
+ * Called when a key down event has occurred.
+ *
+ * @param keyCode The value in event.getKeyCode().
+ * @param event Description of the key event.
+ *
+ * @return If you handled the event, return true. If you want to allow
+ * the event to be handled by the next receiver, return false.
+ */
+ boolean onKeyDown(int keyCode, KeyEvent event);
+
+ /**
+ * Called when a key up event has occurred.
+ *
+ * @param keyCode The value in event.getKeyCode().
+ * @param event Description of the key event.
+ *
+ * @return If you handled the event, return true. If you want to allow
+ * the event to be handled by the next receiver, return false.
+ */
+ boolean onKeyUp(int keyCode, KeyEvent event);
+
+ /**
+ * Called when multiple down/up pairs of the same key have occurred
+ * in a row.
+ *
+ * @param keyCode The value in event.getKeyCode().
+ * @param count Number of pairs as returned by event.getRepeatCount().
+ * @param event Description of the key event.
+ *
+ * @return If you handled the event, return true. If you want to allow
+ * the event to be handled by the next receiver, return false.
+ */
+ boolean onKeyMultiple(int keyCode, int count, KeyEvent event);
+ }
+
+ /**
+ * Create a new key event.
+ *
+ * @param action Action code: either {@link #ACTION_DOWN},
+ * {@link #ACTION_UP}, or {@link #ACTION_MULTIPLE}.
+ * @param code The key code.
+ */
+ public KeyEvent(int action, int code) {
+ mAction = action;
+ mKeyCode = code;
+ mRepeatCount = 0;
+ }
+
+ /**
+ * Create a new key event.
+ *
+ * @param downTime The time (in {@link android.os.SystemClock#uptimeMillis})
+ * at which this key code originally went down.
+ * @param eventTime The time (in {@link android.os.SystemClock#uptimeMillis})
+ * at which this event happened.
+ * @param action Action code: either {@link #ACTION_DOWN},
+ * {@link #ACTION_UP}, or {@link #ACTION_MULTIPLE}.
+ * @param code The key code.
+ * @param repeat A repeat count for down events (> 0 if this is after the
+ * initial down) or event count for multiple events.
+ */
+ public KeyEvent(long downTime, long eventTime, int action,
+ int code, int repeat) {
+ mDownTime = downTime;
+ mEventTime = eventTime;
+ mAction = action;
+ mKeyCode = code;
+ mRepeatCount = repeat;
+ }
+
+ /**
+ * Create a new key event.
+ *
+ * @param downTime The time (in {@link android.os.SystemClock#uptimeMillis})
+ * at which this key code originally went down.
+ * @param eventTime The time (in {@link android.os.SystemClock#uptimeMillis})
+ * at which this event happened.
+ * @param action Action code: either {@link #ACTION_DOWN},
+ * {@link #ACTION_UP}, or {@link #ACTION_MULTIPLE}.
+ * @param code The key code.
+ * @param repeat A repeat count for down events (> 0 if this is after the
+ * initial down) or event count for multiple events.
+ * @param metaState Flags indicating which meta keys are currently pressed.
+ */
+ public KeyEvent(long downTime, long eventTime, int action,
+ int code, int repeat, int metaState) {
+ mDownTime = downTime;
+ mEventTime = eventTime;
+ mAction = action;
+ mKeyCode = code;
+ mRepeatCount = repeat;
+ mMetaState = metaState;
+ }
+
+ /**
+ * Create a new key event.
+ *
+ * @param downTime The time (in {@link android.os.SystemClock#uptimeMillis})
+ * at which this key code originally went down.
+ * @param eventTime The time (in {@link android.os.SystemClock#uptimeMillis})
+ * at which this event happened.
+ * @param action Action code: either {@link #ACTION_DOWN},
+ * {@link #ACTION_UP}, or {@link #ACTION_MULTIPLE}.
+ * @param code The key code.
+ * @param repeat A repeat count for down events (> 0 if this is after the
+ * initial down) or event count for multiple events.
+ * @param metaState Flags indicating which meta keys are currently pressed.
+ * @param device The device ID that generated the key event.
+ * @param scancode Raw device scan code of the event.
+ */
+ public KeyEvent(long downTime, long eventTime, int action,
+ int code, int repeat, int metaState,
+ int device, int scancode) {
+ mDownTime = downTime;
+ mEventTime = eventTime;
+ mAction = action;
+ mKeyCode = code;
+ mRepeatCount = repeat;
+ mMetaState = metaState;
+ mDeviceId = device;
+ mScancode = scancode;
+ }
+
+ /**
+ * Create a new key event.
+ *
+ * @param downTime The time (in {@link android.os.SystemClock#uptimeMillis})
+ * at which this key code originally went down.
+ * @param eventTime The time (in {@link android.os.SystemClock#uptimeMillis})
+ * at which this event happened.
+ * @param action Action code: either {@link #ACTION_DOWN},
+ * {@link #ACTION_UP}, or {@link #ACTION_MULTIPLE}.
+ * @param code The key code.
+ * @param repeat A repeat count for down events (> 0 if this is after the
+ * initial down) or event count for multiple events.
+ * @param metaState Flags indicating which meta keys are currently pressed.
+ * @param device The device ID that generated the key event.
+ * @param scancode Raw device scan code of the event.
+ * @param flags The flags for this key event
+ */
+ public KeyEvent(long downTime, long eventTime, int action,
+ int code, int repeat, int metaState,
+ int device, int scancode, int flags) {
+ mDownTime = downTime;
+ mEventTime = eventTime;
+ mAction = action;
+ mKeyCode = code;
+ mRepeatCount = repeat;
+ mMetaState = metaState;
+ mDeviceId = device;
+ mScancode = scancode;
+ mFlags = flags;
+ }
+
+ /**
+ * Copy an existing key event, modifying its time and repeat count.
+ *
+ * @param origEvent The existing event to be copied.
+ * @param eventTime The new event time
+ * (in {@link android.os.SystemClock#uptimeMillis}) of the event.
+ * @param newRepeat The new repeat count of the event.
+ */
+ public KeyEvent(KeyEvent origEvent, long eventTime, int newRepeat) {
+ mDownTime = origEvent.mDownTime;
+ mEventTime = eventTime;
+ mAction = origEvent.mAction;
+ mKeyCode = origEvent.mKeyCode;
+ mRepeatCount = newRepeat;
+ mMetaState = origEvent.mMetaState;
+ mDeviceId = origEvent.mDeviceId;
+ mScancode = origEvent.mScancode;
+ mFlags = origEvent.mFlags;
+ }
+
+ /**
+ * Don't use in new code, instead explicitly check
+ * {@link #getAction()}.
+ *
+ * @return If the action is ACTION_DOWN, returns true; else false.
+ *
+ * @deprecated
+ * @hide
+ */
+ @Deprecated public final boolean isDown() {
+ return mAction == ACTION_DOWN;
+ }
+
+ /**
+ * Is this a system key? System keys can not be used for menu shortcuts.
+ *
+ * TODO: this information should come from a table somewhere.
+ * TODO: should the dpad keys be here? arguably, because they also shouldn't be menu shortcuts
+ */
+ public final boolean isSystem() {
+ switch (mKeyCode) {
+ case KEYCODE_MENU:
+ case KEYCODE_SOFT_RIGHT:
+ case KEYCODE_HOME:
+ case KEYCODE_BACK:
+ case KEYCODE_CALL:
+ case KEYCODE_ENDCALL:
+ case KEYCODE_VOLUME_UP:
+ case KEYCODE_VOLUME_DOWN:
+ case KEYCODE_POWER:
+ case KEYCODE_HEADSETHOOK:
+ case KEYCODE_CAMERA:
+ case KEYCODE_FOCUS:
+ case KEYCODE_SEARCH:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+
+ /**
+ * <p>Returns the state of the meta keys.</p>
+ *
+ * @return an integer in which each bit set to 1 represents a pressed
+ * meta key
+ *
+ * @see #isAltPressed()
+ * @see #isShiftPressed()
+ * @see #isSymPressed()
+ * @see #META_ALT_ON
+ * @see #META_SHIFT_ON
+ * @see #META_SYM_ON
+ */
+ public final int getMetaState() {
+ return mMetaState;
+ }
+
+ /**
+ * Returns the flags for this key event.
+ *
+ * @see #FLAG_WOKE_HERE
+ */
+ public final int getFlags() {
+ return mFlags;
+ }
+
+ /**
+ * Returns true if this key code is a modifier key.
+ *
+ * @return whether the provided keyCode is one of
+ * {@link #KEYCODE_SHIFT_LEFT} {@link #KEYCODE_SHIFT_RIGHT},
+ * {@link #KEYCODE_ALT_LEFT}, {@link #KEYCODE_ALT_RIGHT}
+ * or {@link #KEYCODE_SYM}.
+ */
+ public static boolean isModifierKey(int keyCode) {
+ return keyCode == KEYCODE_SHIFT_LEFT || keyCode == KEYCODE_SHIFT_RIGHT
+ || keyCode == KEYCODE_ALT_LEFT || keyCode == KEYCODE_ALT_RIGHT
+ || keyCode == KEYCODE_SYM;
+ }
+
+ /**
+ * <p>Returns the pressed state of the ALT meta key.</p>
+ *
+ * @return true if the ALT key is pressed, false otherwise
+ *
+ * @see #KEYCODE_ALT_LEFT
+ * @see #KEYCODE_ALT_RIGHT
+ * @see #META_ALT_ON
+ */
+ public final boolean isAltPressed() {
+ return (mMetaState & META_ALT_ON) != 0;
+ }
+
+ /**
+ * <p>Returns the pressed state of the SHIFT meta key.</p>
+ *
+ * @return true if the SHIFT key is pressed, false otherwise
+ *
+ * @see #KEYCODE_SHIFT_LEFT
+ * @see #KEYCODE_SHIFT_RIGHT
+ * @see #META_SHIFT_ON
+ */
+ public final boolean isShiftPressed() {
+ return (mMetaState & META_SHIFT_ON) != 0;
+ }
+
+ /**
+ * <p>Returns the pressed state of the SYM meta key.</p>
+ *
+ * @return true if the SYM key is pressed, false otherwise
+ *
+ * @see #KEYCODE_SYM
+ * @see #META_SYM_ON
+ */
+ public final boolean isSymPressed() {
+ return (mMetaState & META_SYM_ON) != 0;
+ }
+
+ /**
+ * Retrieve the action of this key event. May be either
+ * {@link #ACTION_DOWN}, {@link #ACTION_UP}, or {@link #ACTION_MULTIPLE}.
+ *
+ * @return The event action: ACTION_DOWN, ACTION_UP, or ACTION_MULTIPLE.
+ */
+ public final int getAction() {
+ return mAction;
+ }
+
+ /**
+ * Retrieve the key code of the key event. This is the physical key that
+ * was pressed -- not the Unicode character.
+ *
+ * @return The key code of the event.
+ */
+ public final int getKeyCode() {
+ return mKeyCode;
+ }
+
+ /**
+ * Retrieve the hardware key id of this key event. These values are not
+ * reliable and vary from device to device.
+ *
+ * {@more}
+ * Mostly this is here for debugging purposes.
+ */
+ public final int getScanCode() {
+ return mScancode;
+ }
+
+ /**
+ * Retrieve the repeat count of the event. For both key up and key down
+ * events, this is the number of times the key has repeated with the first
+ * down starting at 0 and counting up from there. For multiple key
+ * events, this is the number of down/up pairs that have occurred.
+ *
+ * @return The number of times the key has repeated.
+ */
+ public final int getRepeatCount() {
+ return mRepeatCount;
+ }
+
+ /**
+ * Retrieve the time of the most recent key down event,
+ * in the {@link android.os.SystemClock#uptimeMillis} time base. If this
+ * is a down event, this will be the same as {@link #getEventTime()}.
+ * Note that when chording keys, this value is the down time of the
+ * most recently pressed key, which may <em>not</em> be the same physical
+ * key of this event.
+ *
+ * @return Returns the most recent key down time, in the
+ * {@link android.os.SystemClock#uptimeMillis} time base
+ */
+ public final long getDownTime() {
+ return mDownTime;
+ }
+
+ /**
+ * Retrieve the time this event occurred,
+ * in the {@link android.os.SystemClock#uptimeMillis} time base.
+ *
+ * @return Returns the time this event occurred,
+ * in the {@link android.os.SystemClock#uptimeMillis} time base.
+ */
+ public final long getEventTime() {
+ return mEventTime;
+ }
+
+ /**
+ * Return the id for the keyboard that this event came from. A device
+ * id of 0 indicates the event didn't come from a physical device and
+ * maps to the default keymap. The other numbers are arbitrary and
+ * you shouldn't depend on the values.
+ *
+ * @see KeyCharacterMap#load
+ */
+ public final int getDeviceId() {
+ return mDeviceId;
+ }
+
+ /**
+ * Renamed to {@link #getDeviceId}.
+ *
+ * @hide
+ * @deprecated
+ */
+ public final int getKeyboardDevice() {
+ return mDeviceId;
+ }
+
+ /**
+ * Get the primary character for this key. In other words, the label
+ * that is physically printed on it.
+ */
+ public char getDisplayLabel() {
+ return KeyCharacterMap.load(mDeviceId).getDisplayLabel(mKeyCode);
+ }
+
+ /**
+ * <p>
+ * Returns the Unicode character that the key would produce.
+ * </p><p>
+ * Returns 0 if the key is not one that is used to type Unicode
+ * characters.
+ * </p><p>
+ * If the return value has bit
+ * {@link KeyCharacterMap#COMBINING_ACCENT}
+ * set, the key is a "dead key" that should be combined with another to
+ * actually produce a character -- see {@link #getDeadChar} --
+ * after masking with
+ * {@link KeyCharacterMap#COMBINING_ACCENT_MASK}.
+ * </p>
+ */
+ public int getUnicodeChar() {
+ return getUnicodeChar(mMetaState);
+ }
+
+ /**
+ * <p>
+ * Returns the Unicode character that the key would produce.
+ * </p><p>
+ * Returns 0 if the key is not one that is used to type Unicode
+ * characters.
+ * </p><p>
+ * If the return value has bit
+ * {@link KeyCharacterMap#COMBINING_ACCENT}
+ * set, the key is a "dead key" that should be combined with another to
+ * actually produce a character -- see {@link #getDeadChar} -- after masking
+ * with {@link KeyCharacterMap#COMBINING_ACCENT_MASK}.
+ * </p>
+ */
+ public int getUnicodeChar(int meta) {
+ return KeyCharacterMap.load(mDeviceId).get(mKeyCode, meta);
+ }
+
+ /**
+ * Get the characters conversion data for the key event..
+ *
+ * @param results a {@link KeyData} that will be filled with the results.
+ *
+ * @return whether the key was mapped or not. If the key was not mapped,
+ * results is not modified.
+ */
+ public boolean getKeyData(KeyData results) {
+ return KeyCharacterMap.load(mDeviceId).getKeyData(mKeyCode, results);
+ }
+
+ /**
+ * The same as {@link #getMatch(char[],int) getMatch(chars, 0)}.
+ */
+ public char getMatch(char[] chars) {
+ return getMatch(chars, 0);
+ }
+
+ /**
+ * If one of the chars in the array can be generated by the keyCode of this
+ * key event, return the char; otherwise return '\0'.
+ * @param chars the characters to try to find
+ * @param modifiers the modifier bits to prefer. If any of these bits
+ * are set, if there are multiple choices, that could
+ * work, the one for this modifier will be set.
+ */
+ public char getMatch(char[] chars, int modifiers) {
+ return KeyCharacterMap.load(mDeviceId).getMatch(mKeyCode, chars, modifiers);
+ }
+
+ /**
+ * Gets the number or symbol associated with the key. The character value
+ * is returned, not the numeric value. If the key is not a number, but is
+ * a symbol, the symbol is retuned.
+ */
+ public char getNumber() {
+ return KeyCharacterMap.load(mDeviceId).getNumber(mKeyCode);
+ }
+
+ /**
+ * Does the key code of this key produce a glyph?
+ */
+ public boolean isPrintingKey() {
+ return KeyCharacterMap.load(mDeviceId).isPrintingKey(mKeyCode);
+ }
+
+ /**
+ * Deliver this key event to a {@link Callback} interface. If this is
+ * an ACTION_MULTIPLE event and it is not handled, then an attempt will
+ * be made to deliver a single normal event.
+ *
+ * @param receiver The Callback that will be given the event.
+ *
+ * @return The return value from the Callback method that was called.
+ */
+ public final boolean dispatch(Callback receiver) {
+ switch (mAction) {
+ case ACTION_DOWN:
+ return receiver.onKeyDown(mKeyCode, this);
+ case ACTION_UP:
+ return receiver.onKeyUp(mKeyCode, this);
+ case ACTION_MULTIPLE:
+ final int count = mRepeatCount;
+ final int code = mKeyCode;
+ if (receiver.onKeyMultiple(code, count, this)) {
+ return true;
+ }
+ mAction = ACTION_DOWN;
+ mRepeatCount = 0;
+ boolean handled = receiver.onKeyDown(code, this);
+ if (handled) {
+ mAction = ACTION_UP;
+ receiver.onKeyUp(code, this);
+ }
+ mAction = ACTION_MULTIPLE;
+ mRepeatCount = count;
+ return handled;
+ }
+ return false;
+ }
+
+ public String toString() {
+ return "KeyEvent{action=" + mAction + " code=" + mKeyCode
+ + " repeat=" + mRepeatCount
+ + " meta=" + mMetaState + " scancode=" + mScancode
+ + " mFlags=" + mFlags + "}";
+ }
+
+ public static final Parcelable.Creator<KeyEvent> CREATOR
+ = new Parcelable.Creator<KeyEvent>() {
+ public KeyEvent createFromParcel(Parcel in) {
+ return new KeyEvent(in);
+ }
+
+ public KeyEvent[] newArray(int size) {
+ return new KeyEvent[size];
+ }
+ };
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(mAction);
+ out.writeInt(mKeyCode);
+ out.writeInt(mRepeatCount);
+ out.writeInt(mMetaState);
+ out.writeInt(mDeviceId);
+ out.writeInt(mScancode);
+ out.writeInt(mFlags);
+ out.writeLong(mDownTime);
+ out.writeLong(mEventTime);
+ }
+
+ private KeyEvent(Parcel in) {
+ mAction = in.readInt();
+ mKeyCode = in.readInt();
+ mRepeatCount = in.readInt();
+ mMetaState = in.readInt();
+ mDeviceId = in.readInt();
+ mScancode = in.readInt();
+ mFlags = in.readInt();
+ mDownTime = in.readLong();
+ mEventTime = in.readLong();
+ }
+}
diff --git a/core/java/android/view/LayoutInflater.java b/core/java/android/view/LayoutInflater.java
new file mode 100644
index 0000000..94acd3f
--- /dev/null
+++ b/core/java/android/view/LayoutInflater.java
@@ -0,0 +1,744 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.util.AttributeSet;
+import android.util.Xml;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.lang.reflect.Constructor;
+import java.util.HashMap;
+
+/**
+ * This class is used to instantiate layout XML file into its corresponding View
+ * objects. It is never be used directly -- use
+ * {@link android.app.Activity#getLayoutInflater()} or
+ * {@link Context#getSystemService} to retrieve a standard LayoutInflater instance
+ * that is already hooked up to the current context and correctly configured
+ * for the device you are running on. For example:
+ *
+ * <pre>LayoutInflater inflater = (LayoutInflater)context.getSystemService
+ * Context.LAYOUT_INFLATER_SERVICE);</pre>
+ *
+ * <p>
+ * To create a new LayoutInflater with an additional {@link Factory} for your
+ * own views, you can use {@link #cloneInContext} to clone an existing
+ * ViewFactory, and then call {@link #setFactory} on it to include your
+ * Factory.
+ *
+ * <p>
+ * For performance reasons, view inflation relies heavily on pre-processing of
+ * XML files that is done at build time. Therefore, it is not currently possible
+ * to use LayoutInflater with an XmlPullParser over a plain XML file at runtime;
+ * it only works with an XmlPullParser returned from a compiled resource
+ * (R.<em>something</em> file.)
+ *
+ * @see Context#getSystemService
+ */
+public abstract class LayoutInflater {
+ private final boolean DEBUG = false;
+
+ /**
+ * This field should be made private, so it is hidden from the SDK.
+ * {@hide}
+ */
+ protected final Context mContext;
+
+ // these are optional, set by the caller
+ private boolean mFactorySet;
+ private Factory mFactory;
+ private Filter mFilter;
+
+ private final Object[] mConstructorArgs = new Object[2];
+
+ private static final Class[] mConstructorSignature = new Class[] {
+ Context.class, AttributeSet.class};
+
+ private static final HashMap<String, Constructor> sConstructorMap =
+ new HashMap<String, Constructor>();
+
+ private HashMap<String, Boolean> mFilterMap;
+
+ private static final String TAG_MERGE = "merge";
+ private static final String TAG_INCLUDE = "include";
+ private static final String TAG_REQUEST_FOCUS = "requestFocus";
+
+ /**
+ * Hook to allow clients of the LayoutInflater to restrict the set of Views that are allowed
+ * to be inflated.
+ *
+ */
+ public interface Filter {
+ /**
+ * Hook to allow clients of the LayoutInflater to restrict the set of Views
+ * that are allowed to be inflated.
+ *
+ * @param clazz The class object for the View that is about to be inflated
+ *
+ * @return True if this class is allowed to be inflated, or false otherwise
+ */
+ boolean onLoadClass(Class clazz);
+ }
+
+ public interface Factory {
+ /**
+ * Hook you can supply that is called when inflating from a LayoutInflater.
+ * You can use this to customize the tag names available in your XML
+ * layout files.
+ *
+ * <p>
+ * Note that it is good practice to prefix these custom names with your
+ * package (i.e., com.coolcompany.apps) to avoid conflicts with system
+ * names.
+ *
+ * @param name Tag name to be inflated.
+ * @param context The context the view is being created in.
+ * @param attrs Inflation attributes as specified in XML file.
+ *
+ * @return View Newly created view. Return null for the default
+ * behavior.
+ */
+ public View onCreateView(String name, Context context, AttributeSet attrs);
+ }
+
+ private static class FactoryMerger implements Factory {
+ private final Factory mF1, mF2;
+
+ FactoryMerger(Factory f1, Factory f2) {
+ mF1 = f1;
+ mF2 = f2;
+ }
+
+ public View onCreateView(String name, Context context, AttributeSet attrs) {
+ View v = mF1.onCreateView(name, context, attrs);
+ if (v != null) return v;
+ return mF2.onCreateView(name, context, attrs);
+ }
+ }
+
+ /**
+ * Create a new LayoutInflater instance associated with a particular Context.
+ * Applications will almost always want to use
+ * {@link Context#getSystemService Context.getSystemService()} to retrieve
+ * the standard {@link Context#LAYOUT_INFLATER_SERVICE Context.INFLATER_SERVICE}.
+ *
+ * @param context The Context in which this LayoutInflater will create its
+ * Views; most importantly, this supplies the theme from which the default
+ * values for their attributes are retrieved.
+ */
+ protected LayoutInflater(Context context) {
+ mContext = context;
+ }
+
+ /**
+ * Create a new LayoutInflater instance that is a copy of an existing
+ * LayoutInflater, optionally with its Context changed. For use in
+ * implementing {@link #cloneInContext}.
+ *
+ * @param original The original LayoutInflater to copy.
+ * @param newContext The new Context to use.
+ */
+ protected LayoutInflater(LayoutInflater original, Context newContext) {
+ mContext = newContext;
+ mFactory = original.mFactory;
+ mFilter = original.mFilter;
+ }
+
+ /**
+ * Obtains the LayoutInflater from the given context.
+ */
+ public static LayoutInflater from(Context context) {
+ LayoutInflater LayoutInflater =
+ (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ if (LayoutInflater == null) {
+ throw new AssertionError("LayoutInflater not found.");
+ }
+ return LayoutInflater;
+ }
+
+ /**
+ * Create a copy of the existing LayoutInflater object, with the copy
+ * pointing to a different Context than the original. This is used by
+ * {@link ContextThemeWrapper} to create a new LayoutInflater to go along
+ * with the new Context theme.
+ *
+ * @param newContext The new Context to associate with the new LayoutInflater.
+ * May be the same as the original Context if desired.
+ *
+ * @return Returns a brand spanking new LayoutInflater object associated with
+ * the given Context.
+ */
+ public abstract LayoutInflater cloneInContext(Context newContext);
+
+ /**
+ * Return the context we are running in, for access to resources, class
+ * loader, etc.
+ */
+ public Context getContext() {
+ return mContext;
+ }
+
+ /**
+ * Return the current factory (or null). This is called on each element
+ * name. If the factory returns a View, add that to the hierarchy. If it
+ * returns null, proceed to call onCreateView(name).
+ */
+ public final Factory getFactory() {
+ return mFactory;
+ }
+
+ /**
+ * Attach a custom Factory interface for creating views while using
+ * this LayoutInflater. This must not be null, and can only be set once;
+ * after setting, you can not change the factory. This is
+ * called on each element name as the xml is parsed. If the factory returns
+ * a View, that is added to the hierarchy. If it returns null, the next
+ * factory default {@link #onCreateView} method is called.
+ *
+ * <p>If you have an existing
+ * LayoutInflater and want to add your own factory to it, use
+ * {@link #cloneInContext} to clone the existing instance and then you
+ * can use this function (once) on the returned new instance. This will
+ * merge your own factory with whatever factory the original instance is
+ * using.
+ */
+ public void setFactory(Factory factory) {
+ if (mFactorySet) {
+ throw new IllegalStateException("A factory has already been set on this LayoutInflater");
+ }
+ if (factory == null) {
+ throw new NullPointerException("Given factory can not be null");
+ }
+ mFactorySet = true;
+ if (mFactory == null) {
+ mFactory = factory;
+ } else {
+ mFactory = new FactoryMerger(factory, mFactory);
+ }
+ }
+
+ /**
+ * @return The {@link Filter} currently used by this LayoutInflater to restrict the set of Views
+ * that are allowed to be inflated.
+ */
+ public Filter getFilter() {
+ return mFilter;
+ }
+
+ /**
+ * Sets the {@link Filter} to by this LayoutInflater. If a view is attempted to be inflated
+ * which is not allowed by the {@link Filter}, the {@link #inflate(int, ViewGroup)} call will
+ * throw an {@link InflateException}. This filter will replace any previous filter set on this
+ * LayoutInflater.
+ *
+ * @param filter The Filter which restricts the set of Views that are allowed to be inflated.
+ * This filter will replace any previous filter set on this LayoutInflater.
+ */
+ public void setFilter(Filter filter) {
+ mFilter = filter;
+ if (filter != null) {
+ mFilterMap = new HashMap<String, Boolean>();
+ }
+ }
+
+ /**
+ * Inflate a new view hierarchy from the specified xml resource. Throws
+ * {@link InflateException} if there is an error.
+ *
+ * @param resource ID for an XML layout resource to load (e.g.,
+ * <code>R.layout.main_page</code>)
+ * @param root Optional view to be the parent of the generated hierarchy.
+ * @return The root View of the inflated hierarchy. If root was supplied,
+ * this is the root View; otherwise it is the root of the inflated
+ * XML file.
+ */
+ public View inflate(int resource, ViewGroup root) {
+ return inflate(resource, root, root != null);
+ }
+
+ /**
+ * Inflate a new view hierarchy from the specified xml node. Throws
+ * {@link InflateException} if there is an error. *
+ * <p>
+ * <em><strong>Important</strong></em>&nbsp;&nbsp;&nbsp;For performance
+ * reasons, view inflation relies heavily on pre-processing of XML files
+ * that is done at build time. Therefore, it is not currently possible to
+ * use LayoutInflater with an XmlPullParser over a plain XML file at runtime.
+ *
+ * @param parser XML dom node containing the description of the view
+ * hierarchy.
+ * @param root Optional view to be the parent of the generated hierarchy.
+ * @return The root View of the inflated hierarchy. If root was supplied,
+ * this is the root View; otherwise it is the root of the inflated
+ * XML file.
+ */
+ public View inflate(XmlPullParser parser, ViewGroup root) {
+ return inflate(parser, root, root != null);
+ }
+
+ /**
+ * Inflate a new view hierarchy from the specified xml resource. Throws
+ * {@link InflateException} if there is an error.
+ *
+ * @param resource ID for an XML layout resource to load (e.g.,
+ * <code>R.layout.main_page</code>)
+ * @param root Optional view to be the parent of the generated hierarchy (if
+ * <em>attachToRoot</em> is true), or else simply an object that
+ * provides a set of LayoutParams values for root of the returned
+ * hierarchy (if <em>attachToRoot</em> is false.)
+ * @param attachToRoot Whether the inflated hierarchy should be attached to
+ * the root parameter? If false, root is only used to create the
+ * correct subclass of LayoutParams for the root view in the XML.
+ * @return The root View of the inflated hierarchy. If root was supplied and
+ * attachToRoot is true, this is root; otherwise it is the root of
+ * the inflated XML file.
+ */
+ public View inflate(int resource, ViewGroup root, boolean attachToRoot) {
+ if (DEBUG) System.out.println("INFLATING from resource: " + resource);
+ XmlResourceParser parser = getContext().getResources().getLayout(resource);
+ try {
+ return inflate(parser, root, attachToRoot);
+ } finally {
+ parser.close();
+ }
+ }
+
+ /**
+ * Inflate a new view hierarchy from the specified XML node. Throws
+ * {@link InflateException} if there is an error.
+ * <p>
+ * <em><strong>Important</strong></em>&nbsp;&nbsp;&nbsp;For performance
+ * reasons, view inflation relies heavily on pre-processing of XML files
+ * that is done at build time. Therefore, it is not currently possible to
+ * use LayoutInflater with an XmlPullParser over a plain XML file at runtime.
+ *
+ * @param parser XML dom node containing the description of the view
+ * hierarchy.
+ * @param root Optional view to be the parent of the generated hierarchy (if
+ * <em>attachToRoot</em> is true), or else simply an object that
+ * provides a set of LayoutParams values for root of the returned
+ * hierarchy (if <em>attachToRoot</em> is false.)
+ * @param attachToRoot Whether the inflated hierarchy should be attached to
+ * the root parameter? If false, root is only used to create the
+ * correct subclass of LayoutParams for the root view in the XML.
+ * @return The root View of the inflated hierarchy. If root was supplied and
+ * attachToRoot is true, this is root; otherwise it is the root of
+ * the inflated XML file.
+ */
+ public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
+ synchronized (mConstructorArgs) {
+ final AttributeSet attrs = Xml.asAttributeSet(parser);
+ mConstructorArgs[0] = mContext;
+ View result = root;
+
+ try {
+ // Look for the root node.
+ int type;
+ while ((type = parser.next()) != XmlPullParser.START_TAG &&
+ type != XmlPullParser.END_DOCUMENT) {
+ // Empty
+ }
+
+ if (type != XmlPullParser.START_TAG) {
+ throw new InflateException(parser.getPositionDescription()
+ + ": No start tag found!");
+ }
+
+ final String name = parser.getName();
+
+ if (DEBUG) {
+ System.out.println("**************************");
+ System.out.println("Creating root view: "
+ + name);
+ System.out.println("**************************");
+ }
+
+ if (TAG_MERGE.equals(name)) {
+ if (root == null || !attachToRoot) {
+ throw new InflateException("<merge /> can be used only with a valid "
+ + "ViewGroup root and attachToRoot=true");
+ }
+
+ rInflate(parser, root, attrs);
+ } else {
+ // Temp is the root view that was found in the xml
+ View temp = createViewFromTag(name, attrs);
+
+ ViewGroup.LayoutParams params = null;
+
+ if (root != null) {
+ if (DEBUG) {
+ System.out.println("Creating params from root: " +
+ root);
+ }
+ // Create layout params that match root, if supplied
+ params = root.generateLayoutParams(attrs);
+ if (!attachToRoot) {
+ // Set the layout params for temp if we are not
+ // attaching. (If we are, we use addView, below)
+ temp.setLayoutParams(params);
+ }
+ }
+
+ if (DEBUG) {
+ System.out.println("-----> start inflating children");
+ }
+ // Inflate all children under temp
+ rInflate(parser, temp, attrs);
+ if (DEBUG) {
+ System.out.println("-----> done inflating children");
+ }
+
+ // We are supposed to attach all the views we found (int temp)
+ // to root. Do that now.
+ if (root != null && attachToRoot) {
+ root.addView(temp, params);
+ }
+
+ // Decide whether to return the root that was passed in or the
+ // top view found in xml.
+ if (root == null || !attachToRoot) {
+ result = temp;
+ }
+ }
+
+ } catch (XmlPullParserException e) {
+ InflateException ex = new InflateException(e.getMessage());
+ ex.initCause(e);
+ throw ex;
+ } catch (IOException e) {
+ InflateException ex = new InflateException(
+ parser.getPositionDescription()
+ + ": " + e.getMessage());
+ ex.initCause(e);
+ throw ex;
+ }
+
+ return result;
+ }
+ }
+
+ /**
+ * Low-level function for instantiating a view by name. This attempts to
+ * instantiate a view class of the given <var>name</var> found in this
+ * LayoutInflater's ClassLoader.
+ *
+ * <p>
+ * There are two things that can happen in an error case: either the
+ * exception describing the error will be thrown, or a null will be
+ * returned. You must deal with both possibilities -- the former will happen
+ * the first time createView() is called for a class of a particular name,
+ * the latter every time there-after for that class name.
+ *
+ * @param name The full name of the class to be instantiated.
+ * @param attrs The XML attributes supplied for this instance.
+ *
+ * @return View The newly instantied view, or null.
+ */
+ public final View createView(String name, String prefix, AttributeSet attrs)
+ throws ClassNotFoundException, InflateException {
+ Constructor constructor = sConstructorMap.get(name);
+
+ try {
+ if (constructor == null) {
+ // Class not found in the cache, see if it's real, and try to add it
+ Class clazz = mContext.getClassLoader().loadClass(
+ prefix != null ? (prefix + name) : name);
+
+ if (mFilter != null && clazz != null) {
+ boolean allowed = mFilter.onLoadClass(clazz);
+ if (!allowed) {
+ failNotAllowed(name, prefix, attrs);
+ }
+ }
+ constructor = clazz.getConstructor(mConstructorSignature);
+ sConstructorMap.put(name, constructor);
+ } else {
+ // If we have a filter, apply it to cached constructor
+ if (mFilter != null) {
+ // Have we seen this name before?
+ Boolean allowedState = mFilterMap.get(name);
+ if (allowedState == null) {
+ // New class -- remember whether it is allowed
+ Class clazz = mContext.getClassLoader().loadClass(
+ prefix != null ? (prefix + name) : name);
+
+ boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
+ mFilterMap.put(name, allowed);
+ if (!allowed) {
+ failNotAllowed(name, prefix, attrs);
+ }
+ } else if (allowedState.equals(Boolean.FALSE)) {
+ failNotAllowed(name, prefix, attrs);
+ }
+ }
+ }
+
+ Object[] args = mConstructorArgs;
+ args[1] = attrs;
+ return (View) constructor.newInstance(args);
+
+ } catch (NoSuchMethodException e) {
+ InflateException ie = new InflateException(attrs.getPositionDescription()
+ + ": Error inflating class "
+ + (prefix != null ? (prefix + name) : name));
+ ie.initCause(e);
+ throw ie;
+
+ } catch (ClassNotFoundException e) {
+ // If loadClass fails, we should propagate the exception.
+ throw e;
+ } catch (Exception e) {
+ InflateException ie = new InflateException(attrs.getPositionDescription()
+ + ": Error inflating class "
+ + (constructor == null ? "<unknown>" : constructor.getClass().getName()));
+ ie.initCause(e);
+ throw ie;
+ }
+ }
+
+ /**
+ * Throw an excpetion because the specified class is not allowed to be inflated.
+ */
+ private void failNotAllowed(String name, String prefix, AttributeSet attrs) {
+ InflateException ie = new InflateException(attrs.getPositionDescription()
+ + ": Class not allowed to be inflated "
+ + (prefix != null ? (prefix + name) : name));
+ throw ie;
+ }
+
+ /**
+ * This routine is responsible for creating the correct subclass of View
+ * given the xml element name. Override it to handle custom view objects. If
+ * you override this in your subclass be sure to call through to
+ * super.onCreateView(name) for names you do not recognize.
+ *
+ * @param name The fully qualified class name of the View to be create.
+ * @param attrs An AttributeSet of attributes to apply to the View.
+ *
+ * @return View The View created.
+ */
+ protected View onCreateView(String name, AttributeSet attrs)
+ throws ClassNotFoundException {
+ return createView(name, "android.view.", attrs);
+ }
+
+ /*
+ * default visibility so the BridgeInflater can override it.
+ */
+ View createViewFromTag(String name, AttributeSet attrs) {
+ if (name.equals("view")) {
+ name = attrs.getAttributeValue(null, "class");
+ }
+
+ if (DEBUG) System.out.println("******** Creating view: " + name);
+
+ try {
+ View view = (mFactory == null) ? null : mFactory.onCreateView(name,
+ mContext, attrs);
+
+ if (view == null) {
+ if (-1 == name.indexOf('.')) {
+ view = onCreateView(name, attrs);
+ } else {
+ view = createView(name, null, attrs);
+ }
+ }
+
+ if (DEBUG) System.out.println("Created view is: " + view);
+ return view;
+
+ } catch (InflateException e) {
+ throw e;
+
+ } catch (ClassNotFoundException e) {
+ InflateException ie = new InflateException(attrs.getPositionDescription()
+ + ": Error inflating class " + name);
+ ie.initCause(e);
+ throw ie;
+
+ } catch (Exception e) {
+ InflateException ie = new InflateException(attrs.getPositionDescription()
+ + ": Error inflating class " + name);
+ ie.initCause(e);
+ throw ie;
+ }
+ }
+
+ /**
+ * Recursive method used to descend down the xml hierarchy and instantiate
+ * views, instantiate their children, and then call onFinishInflate().
+ */
+ private void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs)
+ throws XmlPullParserException, IOException {
+
+ final int depth = parser.getDepth();
+ int type;
+
+ while (((type = parser.next()) != XmlPullParser.END_TAG ||
+ parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
+
+ if (type != XmlPullParser.START_TAG) {
+ continue;
+ }
+
+ final String name = parser.getName();
+
+ if (TAG_REQUEST_FOCUS.equals(name)) {
+ parseRequestFocus(parser, parent);
+ } else if (TAG_INCLUDE.equals(name)) {
+ if (parser.getDepth() == 0) {
+ throw new InflateException("<include /> cannot be the root element");
+ }
+ parseInclude(parser, parent, attrs);
+ } else if (TAG_MERGE.equals(name)) {
+ throw new InflateException("<merge /> must be the root element");
+ } else {
+ final View view = createViewFromTag(name, attrs);
+ final ViewGroup viewGroup = (ViewGroup) parent;
+ final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
+ rInflate(parser, view, attrs);
+ viewGroup.addView(view, params);
+ }
+ }
+
+ parent.onFinishInflate();
+ }
+
+ private void parseRequestFocus(XmlPullParser parser, View parent)
+ throws XmlPullParserException, IOException {
+ int type;
+ parent.requestFocus();
+ final int currentDepth = parser.getDepth();
+ while (((type = parser.next()) != XmlPullParser.END_TAG ||
+ parser.getDepth() > currentDepth) && type != XmlPullParser.END_DOCUMENT) {
+ // Empty
+ }
+ }
+
+ private void parseInclude(XmlPullParser parser, View parent, AttributeSet attrs)
+ throws XmlPullParserException, IOException {
+
+ int type;
+
+ if (parent instanceof ViewGroup) {
+ final int layout = attrs.getAttributeResourceValue(null, "layout", 0);
+ if (layout == 0) {
+ final String value = attrs.getAttributeValue(null, "layout");
+ if (value == null) {
+ throw new InflateException("You must specifiy a layout in the"
+ + " include tag: <include layout=\"@layout/layoutID\" />");
+ } else {
+ throw new InflateException("You must specifiy a valid layout "
+ + "reference. The layout ID " + value + " is not valid.");
+ }
+ } else {
+ final XmlResourceParser childParser =
+ getContext().getResources().getLayout(layout);
+
+ try {
+ final AttributeSet childAttrs = Xml.asAttributeSet(childParser);
+
+ while ((type = childParser.next()) != XmlPullParser.START_TAG &&
+ type != XmlPullParser.END_DOCUMENT) {
+ // Empty.
+ }
+
+ if (type != XmlPullParser.START_TAG) {
+ throw new InflateException(childParser.getPositionDescription() +
+ ": No start tag found!");
+ }
+
+ final String childName = childParser.getName();
+
+ if (TAG_MERGE.equals(childName)) {
+ // Inflate all children.
+ rInflate(childParser, parent, childAttrs);
+ } else {
+ final View view = createViewFromTag(childName, childAttrs);
+ final ViewGroup group = (ViewGroup) parent;
+
+ // We try to load the layout params set in the <include /> tag. If
+ // they don't exist, we will rely on the layout params set in the
+ // included XML file.
+ // During a layoutparams generation, a runtime exception is thrown
+ // if either layout_width or layout_height is missing. We catch
+ // this exception and set localParams accordingly: true means we
+ // successfully loaded layout params from the <include /> tag,
+ // false means we need to rely on the included layout params.
+ ViewGroup.LayoutParams params = null;
+ try {
+ params = group.generateLayoutParams(attrs);
+ } catch (RuntimeException e) {
+ params = group.generateLayoutParams(childAttrs);
+ } finally {
+ if (params != null) {
+ view.setLayoutParams(params);
+ }
+ }
+
+ // Inflate all children.
+ rInflate(childParser, view, childAttrs);
+
+ // Attempt to override the included layout's android:id with the
+ // one set on the <include /> tag itself.
+ TypedArray a = mContext.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.View, 0, 0);
+ int id = a.getResourceId(com.android.internal.R.styleable.View_id, View.NO_ID);
+ // While we're at it, let's try to override android:visibility.
+ int visibility = a.getInt(com.android.internal.R.styleable.View_visibility, -1);
+ a.recycle();
+
+ if (id != View.NO_ID) {
+ view.setId(id);
+ }
+
+ switch (visibility) {
+ case 0:
+ view.setVisibility(View.VISIBLE);
+ break;
+ case 1:
+ view.setVisibility(View.INVISIBLE);
+ break;
+ case 2:
+ view.setVisibility(View.GONE);
+ break;
+ }
+
+ group.addView(view);
+ }
+ } finally {
+ childParser.close();
+ }
+ }
+ } else {
+ throw new InflateException("<include /> can only be used inside of a ViewGroup");
+ }
+
+ final int currentDepth = parser.getDepth();
+ while (((type = parser.next()) != XmlPullParser.END_TAG ||
+ parser.getDepth() > currentDepth) && type != XmlPullParser.END_DOCUMENT) {
+ // Empty
+ }
+ }
+}
diff --git a/core/java/android/view/Menu.java b/core/java/android/view/Menu.java
new file mode 100644
index 0000000..f2ec076
--- /dev/null
+++ b/core/java/android/view/Menu.java
@@ -0,0 +1,427 @@
+/*
+ * 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.view;
+
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Intent;
+
+/**
+ * Interface for managing the items in a menu.
+ * <p>
+ * By default, every Activity supports an options menu of actions or options.
+ * You can add items to this menu and handle clicks on your additions. The
+ * easiest way of adding menu items is inflating an XML file into the
+ * {@link Menu} via {@link MenuInflater}. The easiest way of attaching code to
+ * clicks is via {@link Activity#onOptionsItemSelected(MenuItem)} and
+ * {@link Activity#onContextItemSelected(MenuItem)}.
+ * <p>
+ * Different menu types support different features:
+ * <ol>
+ * <li><b>Context menus</b>: Do not support item shortcuts, item icons, and sub
+ * menus.
+ * <li><b>Options menus</b>: The <b>icon menus</b> do not support item check
+ * marks and only show the item's
+ * {@link MenuItem#setTitleCondensed(CharSequence) condensed title}. The
+ * <b>expanded menus</b> (only available if six or more menu items are visible,
+ * reached via the 'More' item in the icon menu) do not show item icons, and
+ * item check marks are discouraged.
+ * <li><b>Sub menus</b>: Do not support item icons, or nested sub menus.
+ * </ol>
+ */
+public interface Menu {
+
+ /**
+ * This is the part of an order integer that the user can provide.
+ * @hide
+ */
+ static final int USER_MASK = 0x0000ffff;
+ /**
+ * Bit shift of the user portion of the order integer.
+ * @hide
+ */
+ static final int USER_SHIFT = 0;
+
+ /**
+ * This is the part of an order integer that supplies the category of the
+ * item.
+ * @hide
+ */
+ static final int CATEGORY_MASK = 0xffff0000;
+ /**
+ * Bit shift of the category portion of the order integer.
+ * @hide
+ */
+ static final int CATEGORY_SHIFT = 16;
+
+ /**
+ * Value to use for group and item identifier integers when you don't care
+ * about them.
+ */
+ static final int NONE = 0;
+
+ /**
+ * First value for group and item identifier integers.
+ */
+ static final int FIRST = 1;
+
+ // Implementation note: Keep these CATEGORY_* in sync with the category enum
+ // in attrs.xml
+
+ /**
+ * Category code for the order integer for items/groups that are part of a
+ * container -- or/add this with your base value.
+ */
+ static final int CATEGORY_CONTAINER = 0x00010000;
+
+ /**
+ * Category code for the order integer for items/groups that are provided by
+ * the system -- or/add this with your base value.
+ */
+ static final int CATEGORY_SYSTEM = 0x00020000;
+
+ /**
+ * Category code for the order integer for items/groups that are
+ * user-supplied secondary (infrequently used) options -- or/add this with
+ * your base value.
+ */
+ static final int CATEGORY_SECONDARY = 0x00030000;
+
+ /**
+ * Category code for the order integer for items/groups that are
+ * alternative actions on the data that is currently displayed -- or/add
+ * this with your base value.
+ */
+ static final int CATEGORY_ALTERNATIVE = 0x00040000;
+
+ /**
+ * Flag for {@link #addIntentOptions}: if set, do not automatically remove
+ * any existing menu items in the same group.
+ */
+ static final int FLAG_APPEND_TO_GROUP = 0x0001;
+
+ /**
+ * Flag for {@link #performShortcut}: if set, do not close the menu after
+ * executing the shortcut.
+ */
+ static final int FLAG_PERFORM_NO_CLOSE = 0x0001;
+
+ /**
+ * Flag for {@link #performShortcut(int, KeyEvent, int)}: if set, always
+ * close the menu after executing the shortcut. Closing the menu also resets
+ * the prepared state.
+ */
+ static final int FLAG_ALWAYS_PERFORM_CLOSE = 0x0002;
+
+ /**
+ * Add a new item to the menu. This item displays the given title for its
+ * label.
+ *
+ * @param title The text to display for the item.
+ * @return The newly added menu item.
+ */
+ public MenuItem add(CharSequence title);
+
+ /**
+ * Add a new item to the menu. This item displays the given title for its
+ * label.
+ *
+ * @param titleRes Resource identifier of title string.
+ * @return The newly added menu item.
+ */
+ public MenuItem add(int titleRes);
+
+ /**
+ * Add a new item to the menu. This item displays the given title for its
+ * label.
+ *
+ * @param groupId The group identifier that this item should be part of.
+ * This can be used to define groups of items for batch state
+ * changes. Normally use {@link #NONE} if an item should not be in a
+ * group.
+ * @param itemId Unique item ID. Use {@link #NONE} if you do not need a
+ * unique ID.
+ * @param order The order for the item. Use {@link #NONE} if you do not care
+ * about the order. See {@link MenuItem#getOrder()}.
+ * @param title The text to display for the item.
+ * @return The newly added menu item.
+ */
+ public MenuItem add(int groupId, int itemId, int order, CharSequence title);
+
+ /**
+ * Variation on {@link #add(int, int, int, CharSequence)} that takes a
+ * string resource identifier instead of the string itself.
+ *
+ * @param groupId The group identifier that this item should be part of.
+ * This can also be used to define groups of items for batch state
+ * changes. Normally use {@link #NONE} if an item should not be in a
+ * group.
+ * @param itemId Unique item ID. Use {@link #NONE} if you do not need a
+ * unique ID.
+ * @param order The order for the item. Use {@link #NONE} if you do not care
+ * about the order. See {@link MenuItem#getOrder()}.
+ * @param titleRes Resource identifier of title string.
+ * @return The newly added menu item.
+ */
+ public MenuItem add(int groupId, int itemId, int order, int titleRes);
+
+ /**
+ * Add a new sub-menu to the menu. This item displays the given title for
+ * its label. To modify other attributes on the submenu's menu item, use
+ * {@link SubMenu#getItem()}.
+ *
+ * @param title The text to display for the item.
+ * @return The newly added sub-menu
+ */
+ SubMenu addSubMenu(final CharSequence title);
+
+ /**
+ * Add a new sub-menu to the menu. This item displays the given title for
+ * its label. To modify other attributes on the submenu's menu item, use
+ * {@link SubMenu#getItem()}.
+ *
+ * @param titleRes Resource identifier of title string.
+ * @return The newly added sub-menu
+ */
+ SubMenu addSubMenu(final int titleRes);
+
+ /**
+ * Add a new sub-menu to the menu. This item displays the given
+ * <var>title</var> for its label. To modify other attributes on the
+ * submenu's menu item, use {@link SubMenu#getItem()}.
+ *<p>
+ * Note that you can only have one level of sub-menus, i.e. you cannnot add
+ * a subMenu to a subMenu: An {@link UnsupportedOperationException} will be
+ * thrown if you try.
+ *
+ * @param groupId The group identifier that this item should be part of.
+ * This can also be used to define groups of items for batch state
+ * changes. Normally use {@link #NONE} if an item should not be in a
+ * group.
+ * @param itemId Unique item ID. Use {@link #NONE} if you do not need a
+ * unique ID.
+ * @param order The order for the item. Use {@link #NONE} if you do not care
+ * about the order. See {@link MenuItem#getOrder()}.
+ * @param title The text to display for the item.
+ * @return The newly added sub-menu
+ */
+ SubMenu addSubMenu(final int groupId, final int itemId, int order, final CharSequence title);
+
+ /**
+ * Variation on {@link #addSubMenu(int, int, int, CharSequence)} that takes
+ * a string resource identifier for the title instead of the string itself.
+ *
+ * @param groupId The group identifier that this item should be part of.
+ * This can also be used to define groups of items for batch state
+ * changes. Normally use {@link #NONE} if an item should not be in a group.
+ * @param itemId Unique item ID. Use {@link #NONE} if you do not need a unique ID.
+ * @param order The order for the item. Use {@link #NONE} if you do not care about the
+ * order. See {@link MenuItem#getOrder()}.
+ * @param titleRes Resource identifier of title string.
+ * @return The newly added sub-menu
+ */
+ SubMenu addSubMenu(int groupId, int itemId, int order, int titleRes);
+
+ /**
+ * Add a group of menu items corresponding to actions that can be performed
+ * for a particular Intent. The Intent is most often configured with a null
+ * action, the data that the current activity is working with, and includes
+ * either the {@link Intent#CATEGORY_ALTERNATIVE} or
+ * {@link Intent#CATEGORY_SELECTED_ALTERNATIVE} to find activities that have
+ * said they would like to be included as optional action. You can, however,
+ * use any Intent you want.
+ *
+ * <p>
+ * See {@link android.content.pm.PackageManager#queryIntentActivityOptions}
+ * for more * details on the <var>caller</var>, <var>specifics</var>, and
+ * <var>intent</var> arguments. The list returned by that function is used
+ * to populate the resulting menu items.
+ *
+ * <p>
+ * All of the menu items of possible options for the intent will be added
+ * with the given group and id. You can use the group to control ordering of
+ * the items in relation to other items in the menu. Normally this function
+ * will automatically remove any existing items in the menu in the same
+ * group and place a divider above and below the added items; this behavior
+ * can be modified with the <var>flags</var> parameter. For each of the
+ * generated items {@link MenuItem#setIntent} is called to associate the
+ * appropriate Intent with the item; this means the activity will
+ * automatically be started for you without having to do anything else.
+ *
+ * @param groupId The group identifier that the items should be part of.
+ * This can also be used to define groups of items for batch state
+ * changes. Normally use {@link #NONE} if the items should not be in
+ * a group.
+ * @param itemId Unique item ID. Use {@link #NONE} if you do not need a
+ * unique ID.
+ * @param order The order for the items. Use {@link #NONE} if you do not
+ * care about the order. See {@link MenuItem#getOrder()}.
+ * @param caller The current activity component name as defined by
+ * queryIntentActivityOptions().
+ * @param specifics Specific items to place first as defined by
+ * queryIntentActivityOptions().
+ * @param intent Intent describing the kinds of items to populate in the
+ * list as defined by queryIntentActivityOptions().
+ * @param flags Additional options controlling how the items are added.
+ * @param outSpecificItems Optional array in which to place the menu items
+ * that were generated for each of the <var>specifics</var> that were
+ * requested. Entries may be null if no activity was found for that
+ * specific action.
+ * @return The number of menu items that were added.
+ *
+ * @see #FLAG_APPEND_TO_GROUP
+ * @see MenuItem#setIntent
+ * @see android.content.pm.PackageManager#queryIntentActivityOptions
+ */
+ public int addIntentOptions(int groupId, int itemId, int order,
+ ComponentName caller, Intent[] specifics,
+ Intent intent, int flags, MenuItem[] outSpecificItems);
+
+ /**
+ * Remove the item with the given identifier.
+ *
+ * @param id The item to be removed. If there is no item with this
+ * identifier, nothing happens.
+ */
+ public void removeItem(int id);
+
+ /**
+ * Remove all items in the given group.
+ *
+ * @param groupId The group to be removed. If there are no items in this
+ * group, nothing happens.
+ */
+ public void removeGroup(int groupId);
+
+ /**
+ * Remove all existing items from the menu, leaving it empty as if it had
+ * just been created.
+ */
+ public void clear();
+
+ /**
+ * Control whether a particular group of items can show a check mark. This
+ * is similar to calling {@link MenuItem#setCheckable} on all of the menu items
+ * with the given group identifier, but in addition you can control whether
+ * this group contains a mutually-exclusive set items. This should be called
+ * after the items of the group have been added to the menu.
+ *
+ * @param group The group of items to operate on.
+ * @param checkable Set to true to allow a check mark, false to
+ * disallow. The default is false.
+ * @param exclusive If set to true, only one item in this group can be
+ * checked at a time; checking an item will automatically
+ * uncheck all others in the group. If set to false, each
+ * item can be checked independently of the others.
+ *
+ * @see MenuItem#setCheckable
+ * @see MenuItem#setChecked
+ */
+ public void setGroupCheckable(int group, boolean checkable, boolean exclusive);
+
+ /**
+ * Show or hide all menu items that are in the given group.
+ *
+ * @param group The group of items to operate on.
+ * @param visible If true the items are visible, else they are hidden.
+ *
+ * @see MenuItem#setVisible
+ */
+ public void setGroupVisible(int group, boolean visible);
+
+ /**
+ * Enable or disable all menu items that are in the given group.
+ *
+ * @param group The group of items to operate on.
+ * @param enabled If true the items will be enabled, else they will be disabled.
+ *
+ * @see MenuItem#setEnabled
+ */
+ public void setGroupEnabled(int group, boolean enabled);
+
+ /**
+ * Return whether the menu currently has item items that are visible.
+ *
+ * @return True if there is one or more item visible,
+ * else false.
+ */
+ public boolean hasVisibleItems();
+
+ /**
+ * Return the menu item with a particular identifier.
+ *
+ * @param id The identifier to find.
+ *
+ * @return The menu item object, or null if there is no item with
+ * this identifier.
+ */
+ public MenuItem findItem(int id);
+
+ /**
+ * Get the number of items in the menu. Note that this will change any
+ * times items are added or removed from the menu.
+ *
+ * @return The item count.
+ */
+ public int size();
+
+ /**
+ * Execute the menu item action associated with the given shortcut
+ * character.
+ *
+ * @param keyCode The keycode of the shortcut key.
+ * @param event Key event message.
+ * @param flags Additional option flags or 0.
+ *
+ * @return If the given shortcut exists and is shown, returns
+ * true; else returns false.
+ *
+ * @see #FLAG_PERFORM_NO_CLOSE
+ */
+ public boolean performShortcut(int keyCode, KeyEvent event, int flags);
+
+ /**
+ * Is a keypress one of the defined shortcut keys for this window.
+ * @param keyCode the key code from {@link KeyEvent} to check.
+ * @param event the {@link KeyEvent} to use to help check.
+ */
+ boolean isShortcutKey(int keyCode, KeyEvent event);
+
+ /**
+ * Execute the menu item action associated with the given menu identifier.
+ *
+ * @param id Identifier associated with the menu item.
+ * @param flags Additional option flags or 0.
+ *
+ * @return If the given identifier exists and is shown, returns
+ * true; else returns false.
+ *
+ * @see #FLAG_PERFORM_NO_CLOSE
+ */
+ public boolean performIdentifierAction(int id, int flags);
+
+
+ /**
+ * Control whether the menu should be running in qwerty mode (alphabetic
+ * shortcuts) or 12-key mode (numeric shortcuts).
+ *
+ * @param isQwerty If true the menu will use alphabetic shortcuts; else it
+ * will use numeric shortcuts.
+ */
+ public void setQwertyMode(boolean isQwerty);
+}
+
diff --git a/core/java/android/view/MenuInflater.java b/core/java/android/view/MenuInflater.java
new file mode 100644
index 0000000..46c805c
--- /dev/null
+++ b/core/java/android/view/MenuInflater.java
@@ -0,0 +1,325 @@
+/*
+ * 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.view;
+
+import com.android.internal.view.menu.MenuItemImpl;
+
+import java.io.IOException;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.util.AttributeSet;
+import android.util.Xml;
+
+/**
+ * This class is used to instantiate menu XML files into Menu objects.
+ * <p>
+ * For performance reasons, menu inflation relies heavily on pre-processing of
+ * XML files that is done at build time. Therefore, it is not currently possible
+ * to use MenuInflater with an XmlPullParser over a plain XML file at runtime;
+ * it only works with an XmlPullParser returned from a compiled resource (R.
+ * <em>something</em> file.)
+ */
+public class MenuInflater {
+ /** Menu tag name in XML. */
+ private static final String XML_MENU = "menu";
+
+ /** Group tag name in XML. */
+ private static final String XML_GROUP = "group";
+
+ /** Item tag name in XML. */
+ private static final String XML_ITEM = "item";
+
+ private static final int NO_ID = 0;
+
+ private Context mContext;
+
+ /**
+ * Constructs a menu inflater.
+ *
+ * @see Activity#getMenuInflater()
+ */
+ public MenuInflater(Context context) {
+ mContext = context;
+ }
+
+ /**
+ * Inflate a menu hierarchy from the specified XML resource. Throws
+ * {@link InflateException} if there is an error.
+ *
+ * @param menuRes Resource ID for an XML layout resource to load (e.g.,
+ * <code>R.menu.main_activity</code>)
+ * @param menu The Menu to inflate into. The items and submenus will be
+ * added to this Menu.
+ */
+ public void inflate(int menuRes, Menu menu) {
+ XmlResourceParser parser = null;
+ try {
+ parser = mContext.getResources().getLayout(menuRes);
+ AttributeSet attrs = Xml.asAttributeSet(parser);
+
+ parseMenu(parser, attrs, menu);
+ } catch (XmlPullParserException e) {
+ throw new InflateException("Error inflating menu XML", e);
+ } catch (IOException e) {
+ throw new InflateException("Error inflating menu XML", e);
+ } finally {
+ if (parser != null) parser.close();
+ }
+ }
+
+ /**
+ * Called internally to fill the given menu. If a sub menu is seen, it will
+ * call this recursively.
+ */
+ private void parseMenu(XmlPullParser parser, AttributeSet attrs, Menu menu)
+ throws XmlPullParserException, IOException {
+ MenuState menuState = new MenuState(menu);
+
+ int eventType = parser.getEventType();
+ String tagName;
+ boolean lookingForEndOfUnknownTag = false;
+ String unknownTagName = null;
+
+ // This loop will skip to the menu start tag
+ do {
+ if (eventType == XmlPullParser.START_TAG) {
+ tagName = parser.getName();
+ if (tagName.equals(XML_MENU)) {
+ // Go to next tag
+ eventType = parser.next();
+ break;
+ }
+
+ throw new RuntimeException("Expecting menu, got " + tagName);
+ }
+ eventType = parser.next();
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ boolean reachedEndOfMenu = false;
+ while (!reachedEndOfMenu) {
+ switch (eventType) {
+ case XmlPullParser.START_TAG:
+ if (lookingForEndOfUnknownTag) {
+ break;
+ }
+
+ tagName = parser.getName();
+ if (tagName.equals(XML_GROUP)) {
+ menuState.readGroup(attrs);
+ } else if (tagName.equals(XML_ITEM)) {
+ menuState.readItem(attrs);
+ } else if (tagName.equals(XML_MENU)) {
+ // A menu start tag denotes a submenu for an item
+ SubMenu subMenu = menuState.addSubMenuItem();
+
+ // Parse the submenu into returned SubMenu
+ parseMenu(parser, attrs, subMenu);
+ } else {
+ lookingForEndOfUnknownTag = true;
+ unknownTagName = tagName;
+ }
+ break;
+
+ case XmlPullParser.END_TAG:
+ tagName = parser.getName();
+ if (lookingForEndOfUnknownTag && tagName.equals(unknownTagName)) {
+ lookingForEndOfUnknownTag = false;
+ unknownTagName = null;
+ } else if (tagName.equals(XML_GROUP)) {
+ menuState.resetGroup();
+ } else if (tagName.equals(XML_ITEM)) {
+ // Add the item if it hasn't been added (if the item was
+ // a submenu, it would have been added already)
+ if (!menuState.hasAddedItem()) {
+ menuState.addItem();
+ }
+ } else if (tagName.equals(XML_MENU)) {
+ reachedEndOfMenu = true;
+ }
+ break;
+
+ case XmlPullParser.END_DOCUMENT:
+ throw new RuntimeException("Unexpected end of document");
+ }
+
+ eventType = parser.next();
+ }
+ }
+
+ /**
+ * State for the current menu.
+ * <p>
+ * Groups can not be nested unless there is another menu (which will have
+ * its state class).
+ */
+ private class MenuState {
+ private Menu menu;
+
+ /*
+ * Group state is set on items as they are added, allowing an item to
+ * override its group state. (As opposed to set on items at the group end tag.)
+ */
+ private int groupId;
+ private int groupCategory;
+ private int groupOrder;
+ private int groupCheckable;
+ private boolean groupVisible;
+ private boolean groupEnabled;
+
+ private boolean itemAdded;
+ private int itemId;
+ private int itemCategoryOrder;
+ private String itemTitle;
+ private String itemTitleCondensed;
+ private int itemIconResId;
+ private char itemAlphabeticShortcut;
+ private char itemNumericShortcut;
+ /**
+ * Sync to attrs.xml enum:
+ * - 0: none
+ * - 1: all
+ * - 2: exclusive
+ */
+ private int itemCheckable;
+ private boolean itemChecked;
+ private boolean itemVisible;
+ private boolean itemEnabled;
+
+ private static final int defaultGroupId = NO_ID;
+ private static final int defaultItemId = NO_ID;
+ private static final int defaultItemCategory = 0;
+ private static final int defaultItemOrder = 0;
+ private static final int defaultItemCheckable = 0;
+ private static final boolean defaultItemChecked = false;
+ private static final boolean defaultItemVisible = true;
+ private static final boolean defaultItemEnabled = true;
+
+ public MenuState(final Menu menu) {
+ this.menu = menu;
+
+ resetGroup();
+ }
+
+ public void resetGroup() {
+ groupId = defaultGroupId;
+ groupCategory = defaultItemCategory;
+ groupOrder = defaultItemOrder;
+ groupCheckable = defaultItemCheckable;
+ groupVisible = defaultItemVisible;
+ groupEnabled = defaultItemEnabled;
+ }
+
+ /**
+ * Called when the parser is pointing to a group tag.
+ */
+ public void readGroup(AttributeSet attrs) {
+ TypedArray a = mContext.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.MenuGroup);
+
+ groupId = a.getResourceId(com.android.internal.R.styleable.MenuGroup_id, defaultGroupId);
+ groupCategory = a.getInt(com.android.internal.R.styleable.MenuGroup_menuCategory, defaultItemCategory);
+ groupOrder = a.getInt(com.android.internal.R.styleable.MenuGroup_orderInCategory, defaultItemOrder);
+ groupCheckable = a.getInt(com.android.internal.R.styleable.MenuGroup_checkableBehavior, defaultItemCheckable);
+ groupVisible = a.getBoolean(com.android.internal.R.styleable.MenuGroup_visible, defaultItemVisible);
+ groupEnabled = a.getBoolean(com.android.internal.R.styleable.MenuGroup_enabled, defaultItemEnabled);
+
+ a.recycle();
+ }
+
+ /**
+ * Called when the parser is pointing to an item tag.
+ */
+ public void readItem(AttributeSet attrs) {
+ TypedArray a = mContext.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.MenuItem);
+
+ // Inherit attributes from the group as default value
+ itemId = a.getResourceId(com.android.internal.R.styleable.MenuItem_id, defaultItemId);
+ final int category = a.getInt(com.android.internal.R.styleable.MenuItem_menuCategory, groupCategory);
+ final int order = a.getInt(com.android.internal.R.styleable.MenuItem_orderInCategory, groupOrder);
+ itemCategoryOrder = (category & Menu.CATEGORY_MASK) | (order & Menu.USER_MASK);
+ itemTitle = a.getString(com.android.internal.R.styleable.MenuItem_title);
+ itemTitleCondensed = a.getString(com.android.internal.R.styleable.MenuItem_titleCondensed);
+ itemIconResId = a.getResourceId(com.android.internal.R.styleable.MenuItem_icon, 0);
+ itemAlphabeticShortcut =
+ getShortcut(a.getString(com.android.internal.R.styleable.MenuItem_alphabeticShortcut));
+ itemNumericShortcut =
+ getShortcut(a.getString(com.android.internal.R.styleable.MenuItem_numericShortcut));
+ if (a.hasValue(com.android.internal.R.styleable.MenuItem_checkable)) {
+ // Item has attribute checkable, use it
+ itemCheckable = a.getBoolean(com.android.internal.R.styleable.MenuItem_checkable, false) ? 1 : 0;
+ } else {
+ // Item does not have attribute, use the group's (group can have one more state
+ // for checkable that represents the exclusive checkable)
+ itemCheckable = groupCheckable;
+ }
+ itemChecked = a.getBoolean(com.android.internal.R.styleable.MenuItem_checked, defaultItemChecked);
+ itemVisible = a.getBoolean(com.android.internal.R.styleable.MenuItem_visible, groupVisible);
+ itemEnabled = a.getBoolean(com.android.internal.R.styleable.MenuItem_enabled, groupEnabled);
+
+ a.recycle();
+
+ itemAdded = false;
+ }
+
+ private char getShortcut(String shortcutString) {
+ if (shortcutString == null) {
+ return 0;
+ } else {
+ return shortcutString.charAt(0);
+ }
+ }
+
+ private void setItem(MenuItem item) {
+ item.setChecked(itemChecked)
+ .setVisible(itemVisible)
+ .setEnabled(itemEnabled)
+ .setCheckable(itemCheckable >= 1)
+ .setTitleCondensed(itemTitleCondensed)
+ .setIcon(itemIconResId)
+ .setAlphabeticShortcut(itemAlphabeticShortcut)
+ .setNumericShortcut(itemNumericShortcut);
+
+ if (itemCheckable >= 2) {
+ ((MenuItemImpl) item).setExclusiveCheckable(true);
+ }
+ }
+
+ public void addItem() {
+ itemAdded = true;
+ setItem(menu.add(groupId, itemId, itemCategoryOrder, itemTitle));
+ }
+
+ public SubMenu addSubMenuItem() {
+ itemAdded = true;
+ SubMenu subMenu = menu.addSubMenu(groupId, itemId, itemCategoryOrder, itemTitle);
+ setItem(subMenu.getItem());
+ return subMenu;
+ }
+
+ public boolean hasAddedItem() {
+ return itemAdded;
+ }
+ }
+
+}
diff --git a/core/java/android/view/MenuItem.java b/core/java/android/view/MenuItem.java
new file mode 100644
index 0000000..92cf4af
--- /dev/null
+++ b/core/java/android/view/MenuItem.java
@@ -0,0 +1,368 @@
+package android.view;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.graphics.drawable.Drawable;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.View.OnCreateContextMenuListener;
+
+/**
+ * Interface for direct access to a previously created menu item.
+ * <p>
+ * An Item is returned by calling one of the {@link android.view.Menu#add}
+ * methods.
+ * <p>
+ * For a feature set of specific menu types, see {@link Menu}.
+ */
+public interface MenuItem {
+ /**
+ * Interface definition for a callback to be invoked when a menu item is
+ * clicked.
+ *
+ * @see Activity#onContextItemSelected(MenuItem)
+ * @see Activity#onOptionsItemSelected(MenuItem)
+ */
+ public interface OnMenuItemClickListener {
+ /**
+ * Called when a menu item has been invoked. This is the first code
+ * that is executed; if it returns true, no other callbacks will be
+ * executed.
+ *
+ * @param item The menu item that was invoked.
+ *
+ * @return Return true to consume this click and prevent others from
+ * executing.
+ */
+ public boolean onMenuItemClick(MenuItem item);
+ }
+
+ /**
+ * Return the identifier for this menu item. The identifier can not
+ * be changed after the menu is created.
+ *
+ * @return The menu item's identifier.
+ */
+ public int getItemId();
+
+ /**
+ * Return the group identifier that this menu item is part of. The group
+ * identifier can not be changed after the menu is created.
+ *
+ * @return The menu item's group identifier.
+ */
+ public int getGroupId();
+
+ /**
+ * Return the category and order within the category of this item. This
+ * item will be shown before all items (within its category) that have
+ * order greater than this value.
+ * <p>
+ * An order integer contains the item's category (the upper bits of the
+ * integer; set by or/add the category with the order within the
+ * category) and the ordering of the item within that category (the
+ * lower bits). Example categories are {@link Menu#CATEGORY_SYSTEM},
+ * {@link Menu#CATEGORY_SECONDARY}, {@link Menu#CATEGORY_ALTERNATIVE},
+ * {@link Menu#CATEGORY_CONTAINER}. See {@link Menu} for a full list.
+ *
+ * @return The order of this item.
+ */
+ public int getOrder();
+
+ /**
+ * Change the title associated with this item.
+ *
+ * @param title The new text to be displayed.
+ * @return This Item so additional setters can be called.
+ */
+ public MenuItem setTitle(CharSequence title);
+
+ /**
+ * Change the title associated with this item.
+ * <p>
+ * Some menu types do not sufficient space to show the full title, and
+ * instead a condensed title is preferred. See {@link Menu} for more
+ * information.
+ *
+ * @param title The resource id of the new text to be displayed.
+ * @return This Item so additional setters can be called.
+ * @see #setTitleCondensed(CharSequence)
+ */
+
+ public MenuItem setTitle(int title);
+
+ /**
+ * Retrieve the current title of the item.
+ *
+ * @return The title.
+ */
+ public CharSequence getTitle();
+
+ /**
+ * Change the condensed title associated with this item. The condensed
+ * title is used in situations where the normal title may be too long to
+ * be displayed.
+ *
+ * @param title The new text to be displayed as the condensed title.
+ * @return This Item so additional setters can be called.
+ */
+ public MenuItem setTitleCondensed(CharSequence title);
+
+ /**
+ * Retrieve the current condensed title of the item. If a condensed
+ * title was never set, it will return the normal title.
+ *
+ * @return The condensed title, if it exists.
+ * Otherwise the normal title.
+ */
+ public CharSequence getTitleCondensed();
+
+ /**
+ * Change the icon associated with this item. This icon will not always be
+ * shown, so the title should be sufficient in describing this item. See
+ * {@link Menu} for the menu types that support icons.
+ *
+ * @param icon The new icon (as a Drawable) to be displayed.
+ * @return This Item so additional setters can be called.
+ */
+ public MenuItem setIcon(Drawable icon);
+
+ /**
+ * Change the icon associated with this item. This icon will not always be
+ * shown, so the title should be sufficient in describing this item. See
+ * {@link Menu} for the menu types that support icons.
+ * <p>
+ * This method will set the resource ID of the icon which will be used to
+ * lazily get the Drawable when this item is being shown.
+ *
+ * @param iconRes The new icon (as a resource ID) to be displayed.
+ * @return This Item so additional setters can be called.
+ */
+ public MenuItem setIcon(int iconRes);
+
+ /**
+ * Returns the icon for this item as a Drawable (getting it from resources if it hasn't been
+ * loaded before).
+ *
+ * @return The icon as a Drawable.
+ */
+ public Drawable getIcon();
+
+ /**
+ * Change the Intent associated with this item. By default there is no
+ * Intent associated with a menu item. If you set one, and nothing
+ * else handles the item, then the default behavior will be to call
+ * {@link android.content.Context#startActivity} with the given Intent.
+ *
+ * <p>Note that setIntent() can not be used with the versions of
+ * {@link Menu#add} that take a Runnable, because {@link Runnable#run}
+ * does not return a value so there is no way to tell if it handled the
+ * item. In this case it is assumed that the Runnable always handles
+ * the item, and the intent will never be started.
+ *
+ * @see #getIntent
+ * @param intent The Intent to associated with the item. This Intent
+ * object is <em>not</em> copied, so be careful not to
+ * modify it later.
+ * @return This Item so additional setters can be called.
+ */
+ public MenuItem setIntent(Intent intent);
+
+ /**
+ * Return the Intent associated with this item. This returns a
+ * reference to the Intent which you can change as desired to modify
+ * what the Item is holding.
+ *
+ * @see #setIntent
+ * @return Returns the last value supplied to {@link #setIntent}, or
+ * null.
+ */
+ public Intent getIntent();
+
+ /**
+ * Change both the numeric and alphabetic shortcut associated with this
+ * item. Note that the shortcut will be triggered when the key that
+ * generates the given character is pressed alone or along with with the alt
+ * key. Also note that case is not significant and that alphabetic shortcut
+ * characters will be displayed in lower case.
+ * <p>
+ * See {@link Menu} for the menu types that support shortcuts.
+ *
+ * @param numericChar The numeric shortcut key. This is the shortcut when
+ * using a numeric (e.g., 12-key) keyboard.
+ * @param alphaChar The alphabetic shortcut key. This is the shortcut when
+ * using a keyboard with alphabetic keys.
+ * @return This Item so additional setters can be called.
+ */
+ public MenuItem setShortcut(char numericChar, char alphaChar);
+
+ /**
+ * Change the numeric shortcut associated with this item.
+ * <p>
+ * See {@link Menu} for the menu types that support shortcuts.
+ *
+ * @param numericChar The numeric shortcut key. This is the shortcut when
+ * using a 12-key (numeric) keyboard.
+ * @return This Item so additional setters can be called.
+ */
+ public MenuItem setNumericShortcut(char numericChar);
+
+ /**
+ * Return the char for this menu item's numeric (12-key) shortcut.
+ *
+ * @return Numeric character to use as a shortcut.
+ */
+ public char getNumericShortcut();
+
+ /**
+ * Change the alphabetic shortcut associated with this item. The shortcut
+ * will be triggered when the key that generates the given character is
+ * pressed alone or along with with the alt key. Case is not significant and
+ * shortcut characters will be displayed in lower case. Note that menu items
+ * with the characters '\b' or '\n' as shortcuts will get triggered by the
+ * Delete key or Carriage Return key, respectively.
+ * <p>
+ * See {@link Menu} for the menu types that support shortcuts.
+ *
+ * @param alphaChar The alphabetic shortcut key. This is the shortcut when
+ * using a keyboard with alphabetic keys.
+ * @return This Item so additional setters can be called.
+ */
+ public MenuItem setAlphabeticShortcut(char alphaChar);
+
+ /**
+ * Return the char for this menu item's alphabetic shortcut.
+ *
+ * @return Alphabetic character to use as a shortcut.
+ */
+ public char getAlphabeticShortcut();
+
+ /**
+ * Control whether this item can display a check mark. Setting this does
+ * not actually display a check mark (see {@link #setChecked} for that);
+ * rather, it ensures there is room in the item in which to display a
+ * check mark.
+ * <p>
+ * See {@link Menu} for the menu types that support check marks.
+ *
+ * @param checkable Set to true to allow a check mark, false to
+ * disallow. The default is false.
+ * @see #setChecked
+ * @see #isCheckable
+ * @see Menu#setGroupCheckable
+ * @return This Item so additional setters can be called.
+ */
+ public MenuItem setCheckable(boolean checkable);
+
+ /**
+ * Return whether the item can currently display a check mark.
+ *
+ * @return If a check mark can be displayed, returns true.
+ *
+ * @see #setCheckable
+ */
+ public boolean isCheckable();
+
+ /**
+ * Control whether this item is shown with a check mark. Note that you
+ * must first have enabled checking with {@link #setCheckable} or else
+ * the check mark will not appear. If this item is a member of a group that contains
+ * mutually-exclusive items (set via {@link Menu#setGroupCheckable(int, boolean, boolean)},
+ * the other items in the group will be unchecked.
+ * <p>
+ * See {@link Menu} for the menu types that support check marks.
+ *
+ * @see #setCheckable
+ * @see #isChecked
+ * @see Menu#setGroupCheckable
+ * @param checked Set to true to display a check mark, false to hide
+ * it. The default value is false.
+ * @return This Item so additional setters can be called.
+ */
+ public MenuItem setChecked(boolean checked);
+
+ /**
+ * Return whether the item is currently displaying a check mark.
+ *
+ * @return If a check mark is displayed, returns true.
+ *
+ * @see #setChecked
+ */
+ public boolean isChecked();
+
+ /**
+ * Sets the visibility of the menu item. Even if a menu item is not visible,
+ * it may still be invoked via its shortcut (to completely disable an item,
+ * set it to invisible and {@link #setEnabled(boolean) disabled}).
+ *
+ * @param visible If true then the item will be visible; if false it is
+ * hidden.
+ * @return This Item so additional setters can be called.
+ */
+ public MenuItem setVisible(boolean visible);
+
+ /**
+ * Return the visibility of the menu item.
+ *
+ * @return If true the item is visible; else it is hidden.
+ */
+ public boolean isVisible();
+
+ /**
+ * Sets whether the menu item is enabled. Disabling a menu item will not
+ * allow it to be invoked via its shortcut. The menu item will still be
+ * visible.
+ *
+ * @param enabled If true then the item will be invokable; if false it is
+ * won't be invokable.
+ * @return This Item so additional setters can be called.
+ */
+ public MenuItem setEnabled(boolean enabled);
+
+ /**
+ * Return the enabled state of the menu item.
+ *
+ * @return If true the item is enabled and hence invokable; else it is not.
+ */
+ public boolean isEnabled();
+
+ /**
+ * Check whether this item has an associated sub-menu. I.e. it is a
+ * sub-menu of another menu.
+ *
+ * @return If true this item has a menu; else it is a
+ * normal item.
+ */
+ public boolean hasSubMenu();
+
+ /**
+ * Get the sub-menu to be invoked when this item is selected, if it has
+ * one. See {@link #hasSubMenu()}.
+ *
+ * @return The associated menu if there is one, else null
+ */
+ public SubMenu getSubMenu();
+
+ /**
+ * Set a custom listener for invocation of this menu item. In most
+ * situations, it is more efficient and easier to use
+ * {@link Activity#onOptionsItemSelected(MenuItem)} or
+ * {@link Activity#onContextItemSelected(MenuItem)}.
+ *
+ * @param menuItemClickListener The object to receive invokations.
+ * @return This Item so additional setters can be called.
+ * @see Activity#onOptionsItemSelected(MenuItem)
+ * @see Activity#onContextItemSelected(MenuItem)
+ */
+ public MenuItem setOnMenuItemClickListener(MenuItem.OnMenuItemClickListener menuItemClickListener);
+
+ /**
+ * Gets the extra information linked to this menu item. This extra
+ * information is set by the View that added this menu item to the
+ * menu.
+ *
+ * @see OnCreateContextMenuListener
+ * @return The extra information linked to the View that added this
+ * menu item to the menu. This can be null.
+ */
+ public ContextMenuInfo getMenuInfo();
+} \ No newline at end of file
diff --git a/core/java/android/view/MotionEvent.aidl b/core/java/android/view/MotionEvent.aidl
new file mode 100644
index 0000000..3c89988
--- /dev/null
+++ b/core/java/android/view/MotionEvent.aidl
@@ -0,0 +1,20 @@
+/* //device/java/android/android.view.KeyEvent.aidl
+**
+** Copyright 2007, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.view;
+
+parcelable MotionEvent;
diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java
new file mode 100644
index 0000000..ab05e16
--- /dev/null
+++ b/core/java/android/view/MotionEvent.java
@@ -0,0 +1,659 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.util.Config;
+
+/**
+ * Object used to report movement (mouse, pen, finger, trackball) events. This
+ * class may hold either absolute or relative movements, depending on what
+ * it is being used for.
+ */
+public final class MotionEvent implements Parcelable {
+ public static final int ACTION_DOWN = 0;
+ public static final int ACTION_UP = 1;
+ public static final int ACTION_MOVE = 2;
+ public static final int ACTION_CANCEL = 3;
+
+ private static final boolean TRACK_RECYCLED_LOCATION = false;
+
+ /**
+ * Flag indicating the motion event intersected the top edge of the screen.
+ */
+ public static final int EDGE_TOP = 0x00000001;
+
+ /**
+ * Flag indicating the motion event intersected the bottom edge of the screen.
+ */
+ public static final int EDGE_BOTTOM = 0x00000002;
+
+ /**
+ * Flag indicating the motion event intersected the left edge of the screen.
+ */
+ public static final int EDGE_LEFT = 0x00000004;
+
+ /**
+ * Flag indicating the motion event intersected the right edge of the screen.
+ */
+ public static final int EDGE_RIGHT = 0x00000008;
+
+ static private final int MAX_RECYCLED = 10;
+ static private Object gRecyclerLock = new Object();
+ static private int gRecyclerUsed = 0;
+ static private MotionEvent gRecyclerTop = null;
+
+ private long mDownTime;
+ private long mEventTime;
+ private int mAction;
+ private float mX;
+ private float mY;
+ private float mRawX;
+ private float mRawY;
+ private float mPressure;
+ private float mSize;
+ private int mMetaState;
+ private int mNumHistory;
+ private float[] mHistory;
+ private long[] mHistoryTimes;
+ private float mXPrecision;
+ private float mYPrecision;
+ private int mDeviceId;
+ private int mEdgeFlags;
+
+ private MotionEvent mNext;
+ private RuntimeException mRecycledLocation;
+ private boolean mRecycled;
+
+ private MotionEvent() {
+ }
+
+ static private MotionEvent obtain() {
+ synchronized (gRecyclerLock) {
+ if (gRecyclerTop == null) {
+ return new MotionEvent();
+ }
+ MotionEvent ev = gRecyclerTop;
+ gRecyclerTop = ev.mNext;
+ gRecyclerUsed--;
+ ev.mRecycledLocation = null;
+ ev.mRecycled = false;
+ return ev;
+ }
+ }
+
+ /**
+ * Create a new MotionEvent, filling in all of the basic values that
+ * define the motion.
+ *
+ * @param downTime The time (in ms) when the user originally pressed down to start
+ * a stream of position events. This must be obtained from {@link SystemClock#uptimeMillis()}.
+ * @param eventTime The the time (in ms) when this specific event was generated. This
+ * must be obtained from {@link SystemClock#uptimeMillis()}.
+ * @param action The kind of action being performed -- one of either
+ * {@link #ACTION_DOWN}, {@link #ACTION_MOVE}, {@link #ACTION_UP}, or
+ * {@link #ACTION_CANCEL}.
+ * @param x The X coordinate of this event.
+ * @param y The Y coordinate of this event.
+ * @param pressure The current pressure of this event. The pressure generally
+ * ranges from 0 (no pressure at all) to 1 (normal pressure), however
+ * values higher than 1 may be generated depending on the calibration of
+ * the input device.
+ * @param size A scaled value of the approximate size of the area being pressed when
+ * touched with the finger. The actual value in pixels corresponding to the finger
+ * touch is normalized with a device specific range of values
+ * and scaled to a value between 0 and 1.
+ * @param metaState The state of any meta / modifier keys that were in effect when
+ * the event was generated.
+ * @param xPrecision The precision of the X coordinate being reported.
+ * @param yPrecision The precision of the Y coordinate being reported.
+ * @param deviceId The id for the device that this event came from. An id of
+ * zero indicates that the event didn't come from a physical device; other
+ * numbers are arbitrary and you shouldn't depend on the values.
+ * @param edgeFlags A bitfield indicating which edges, if any, where touched by this
+ * MotionEvent.
+ */
+ static public MotionEvent obtain(long downTime, long eventTime, int action,
+ float x, float y, float pressure, float size, int metaState,
+ float xPrecision, float yPrecision, int deviceId, int edgeFlags) {
+ MotionEvent ev = obtain();
+ ev.mDeviceId = deviceId;
+ ev.mEdgeFlags = edgeFlags;
+ ev.mDownTime = downTime;
+ ev.mEventTime = eventTime;
+ ev.mAction = action;
+ ev.mX = ev.mRawX = x;
+ ev.mY = ev.mRawY = y;
+ ev.mPressure = pressure;
+ ev.mSize = size;
+ ev.mMetaState = metaState;
+ ev.mXPrecision = xPrecision;
+ ev.mYPrecision = yPrecision;
+
+ return ev;
+ }
+
+ /**
+ * Create a new MotionEvent, filling in a subset of the basic motion
+ * values. Those not specified here are: device id (always 0), pressure
+ * and size (always 1), x and y precision (always 1), and edgeFlags (always 0).
+ *
+ * @param downTime The time (in ms) when the user originally pressed down to start
+ * a stream of position events. This must be obtained from {@link SystemClock#uptimeMillis()}.
+ * @param eventTime The the time (in ms) when this specific event was generated. This
+ * must be obtained from {@link SystemClock#uptimeMillis()}.
+ * @param action The kind of action being performed -- one of either
+ * {@link #ACTION_DOWN}, {@link #ACTION_MOVE}, {@link #ACTION_UP}, or
+ * {@link #ACTION_CANCEL}.
+ * @param x The X coordinate of this event.
+ * @param y The Y coordinate of this event.
+ * @param metaState The state of any meta / modifier keys that were in effect when
+ * the event was generated.
+ */
+ static public MotionEvent obtain(long downTime, long eventTime, int action,
+ float x, float y, int metaState) {
+ MotionEvent ev = obtain();
+ ev.mDeviceId = 0;
+ ev.mEdgeFlags = 0;
+ ev.mDownTime = downTime;
+ ev.mEventTime = eventTime;
+ ev.mAction = action;
+ ev.mX = ev.mRawX = x;
+ ev.mY = ev.mRawY = y;
+ ev.mPressure = 1.0f;
+ ev.mSize = 1.0f;
+ ev.mMetaState = metaState;
+ ev.mXPrecision = 1.0f;
+ ev.mYPrecision = 1.0f;
+
+ return ev;
+ }
+
+ /**
+ * Create a new MotionEvent, copying from an existing one.
+ */
+ static public MotionEvent obtain(MotionEvent o) {
+ MotionEvent ev = obtain();
+ ev.mDeviceId = o.mDeviceId;
+ ev.mEdgeFlags = o.mEdgeFlags;
+ ev.mDownTime = o.mDownTime;
+ ev.mEventTime = o.mEventTime;
+ ev.mAction = o.mAction;
+ ev.mX = o.mX;
+ ev.mRawX = o.mRawX;
+ ev.mY = o.mY;
+ ev.mRawY = o.mRawY;
+ ev.mPressure = o.mPressure;
+ ev.mSize = o.mSize;
+ ev.mMetaState = o.mMetaState;
+ ev.mXPrecision = o.mXPrecision;
+ ev.mYPrecision = o.mYPrecision;
+ final int N = o.mNumHistory;
+ ev.mNumHistory = N;
+ if (N > 0) {
+ // could be more efficient about this...
+ ev.mHistory = (float[])o.mHistory.clone();
+ ev.mHistoryTimes = (long[])o.mHistoryTimes.clone();
+ }
+ return ev;
+ }
+
+ /**
+ * Recycle the MotionEvent, to be re-used by a later caller. After calling
+ * this function you must not ever touch the event again.
+ */
+ public 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");
+ } else if (mRecycled) {
+ throw new RuntimeException(toString() + " recycled twice!");
+ }
+
+ //Log.w("MotionEvent", "Recycling event " + this, mRecycledLocation);
+ synchronized (gRecyclerLock) {
+ if (gRecyclerUsed < MAX_RECYCLED) {
+ gRecyclerUsed++;
+ mNumHistory = 0;
+ mNext = gRecyclerTop;
+ gRecyclerTop = this;
+ }
+ }
+ }
+
+ /**
+ * Return the kind of action being performed -- one of either
+ * {@link #ACTION_DOWN}, {@link #ACTION_MOVE}, {@link #ACTION_UP}, or
+ * {@link #ACTION_CANCEL}.
+ */
+ public final int getAction() {
+ return mAction;
+ }
+
+ /**
+ * Returns the time (in ms) when the user originally pressed down to start
+ * a stream of position events.
+ */
+ public final long getDownTime() {
+ return mDownTime;
+ }
+
+ /**
+ * Returns the time (in ms) when this specific event was generated.
+ */
+ public final long getEventTime() {
+ return mEventTime;
+ }
+
+ /**
+ * Returns the X coordinate of this event. Whole numbers are pixels; the
+ * value may have a fraction for input devices that are sub-pixel precise.
+ */
+ public final float getX() {
+ return mX;
+ }
+
+ /**
+ * Returns the Y coordinate of this event. Whole numbers are pixels; the
+ * value may have a fraction for input devices that are sub-pixel precise.
+ */
+ public final float getY() {
+ return mY;
+ }
+
+ /**
+ * Returns the current pressure of this event. The pressure generally
+ * ranges from 0 (no pressure at all) to 1 (normal pressure), however
+ * values higher than 1 may be generated depending on the calibration of
+ * the input device.
+ */
+ public final float getPressure() {
+ return mPressure;
+ }
+
+ /**
+ * Returns a scaled value of the approximate size, of the area being pressed when
+ * touched with the finger. The actual value in pixels corresponding to the finger
+ * touch is normalized with the device specific range of values
+ * and scaled to a value between 0 and 1. The value of size can be used to
+ * determine fat touch events.
+ */
+ public final float getSize() {
+ return mSize;
+ }
+
+ /**
+ * Returns the state of any meta / modifier keys that were in effect when
+ * the event was generated. This is the same values as those
+ * returned by {@link KeyEvent#getMetaState() KeyEvent.getMetaState}.
+ *
+ * @return an integer in which each bit set to 1 represents a pressed
+ * meta key
+ *
+ * @see KeyEvent#getMetaState()
+ */
+ public final int getMetaState() {
+ return mMetaState;
+ }
+
+ /**
+ * Returns the original raw X coordinate of this event. For touch
+ * events on the screen, this is the original location of the event
+ * on the screen, before it had been adjusted for the containing window
+ * and views.
+ */
+ public final float getRawX() {
+ return mRawX;
+ }
+
+ /**
+ * Returns the original raw Y coordinate of this event. For touch
+ * events on the screen, this is the original location of the event
+ * on the screen, before it had been adjusted for the containing window
+ * and views.
+ */
+ public final float getRawY() {
+ return mRawY;
+ }
+
+ /**
+ * Return the precision of the X coordinates being reported. You can
+ * multiple this number with {@link #getX} to find the actual hardware
+ * value of the X coordinate.
+ * @return Returns the precision of X coordinates being reported.
+ */
+ public final float getXPrecision() {
+ return mXPrecision;
+ }
+
+ /**
+ * Return the precision of the Y coordinates being reported. You can
+ * multiple this number with {@link #getY} to find the actual hardware
+ * value of the Y coordinate.
+ * @return Returns the precision of Y coordinates being reported.
+ */
+ public final float getYPrecision() {
+ return mYPrecision;
+ }
+
+ /**
+ * Returns the number of historical points in this event. These are
+ * movements that have occurred between this event and the previous event.
+ * This only applies to ACTION_MOVE events -- all other actions will have
+ * a size of 0.
+ *
+ * @return Returns the number of historical points in the event.
+ */
+ public final int getHistorySize() {
+ return mNumHistory;
+ }
+
+ /**
+ * Returns the time that a historical movement occurred between this event
+ * and the previous event. Only applies to ACTION_MOVE events.
+ *
+ * @param pos Which historical value to return; must be less than
+ * {@link #getHistorySize}
+ *
+ * @see #getHistorySize
+ * @see #getEventTime
+ */
+ public final long getHistoricalEventTime(int pos) {
+ return mHistoryTimes[pos];
+ }
+
+ /**
+ * Returns a historical X coordinate that occurred between this event
+ * and the previous event. Only applies to ACTION_MOVE events.
+ *
+ * @param pos Which historical value to return; must be less than
+ * {@link #getHistorySize}
+ *
+ * @see #getHistorySize
+ * @see #getX
+ */
+ public final float getHistoricalX(int pos) {
+ return mHistory[pos*4];
+ }
+
+ /**
+ * Returns a historical Y coordinate that occurred between this event
+ * and the previous event. Only applies to ACTION_MOVE events.
+ *
+ * @param pos Which historical value to return; must be less than
+ * {@link #getHistorySize}
+ *
+ * @see #getHistorySize
+ * @see #getY
+ */
+ public final float getHistoricalY(int pos) {
+ return mHistory[pos*4 + 1];
+ }
+
+ /**
+ * Returns a historical pressure coordinate that occurred between this event
+ * and the previous event. Only applies to ACTION_MOVE events.
+ *
+ * @param pos Which historical value to return; must be less than
+ * {@link #getHistorySize}
+ *
+ * @see #getHistorySize
+ * @see #getPressure
+ */
+ public final float getHistoricalPressure(int pos) {
+ return mHistory[pos*4 + 2];
+ }
+
+ /**
+ * Returns a historical size coordinate that occurred between this event
+ * and the previous event. Only applies to ACTION_MOVE events.
+ *
+ * @param pos Which historical value to return; must be less than
+ * {@link #getHistorySize}
+ *
+ * @see #getHistorySize
+ * @see #getSize
+ */
+ public final float getHistoricalSize(int pos) {
+ return mHistory[pos*4 + 3];
+ }
+
+ /**
+ * Return the id for the device that this event came from. An id of
+ * zero indicates that the event didn't come from a physical device; other
+ * numbers are arbitrary and you shouldn't depend on the values.
+ */
+ public final int getDeviceId() {
+ return mDeviceId;
+ }
+
+ /**
+ * Returns a bitfield indicating which edges, if any, where touched by this
+ * MotionEvent. For touch events, clients can use this to determine if the
+ * user's finger was touching the edge of the display.
+ *
+ * @see #EDGE_LEFT
+ * @see #EDGE_TOP
+ * @see #EDGE_RIGHT
+ * @see #EDGE_BOTTOM
+ */
+ public final int getEdgeFlags() {
+ return mEdgeFlags;
+ }
+
+
+ /**
+ * Sets the bitfield indicating which edges, if any, where touched by this
+ * MotionEvent.
+ *
+ * @see #getEdgeFlags()
+ */
+ public final void setEdgeFlags(int flags) {
+ mEdgeFlags = flags;
+ }
+
+ /**
+ * Sets this event's action.
+ */
+ public final void setAction(int action) {
+ mAction = action;
+ }
+
+ /**
+ * Adjust this event's location.
+ * @param deltaX Amount to add to the current X coordinate of the event.
+ * @param deltaY Amount to add to the current Y coordinate of the event.
+ */
+ public final void offsetLocation(float deltaX, float deltaY) {
+ mX += deltaX;
+ mY += deltaY;
+ final int N = mNumHistory*4;
+ if (N <= 0) {
+ return;
+ }
+ final float[] pos = mHistory;
+ for (int i=0; i<N; i+=4) {
+ pos[i] += deltaX;
+ pos[i+1] += deltaY;
+ }
+ }
+
+ /**
+ * Set this event's location. Applies {@link #offsetLocation} with a
+ * delta from the current location to the given new location.
+ *
+ * @param x New absolute X location.
+ * @param y New absolute Y location.
+ */
+ public final void setLocation(float x, float y) {
+ float deltaX = x-mX;
+ float deltaY = y-mY;
+ if (deltaX != 0 || deltaY != 0) {
+ offsetLocation(deltaX, deltaY);
+ }
+ }
+
+ /**
+ * Add a new movement to the batch of movements in this event. The event's
+ * current location, position and size is updated to the new values. In
+ * the future, the current values in the event will be added to a list of
+ * historic values.
+ *
+ * @param x The new X position.
+ * @param y The new Y position.
+ * @param pressure The new pressure.
+ * @param size The new size.
+ */
+ public final void addBatch(long eventTime, float x, float y,
+ float pressure, float size, int metaState) {
+ float[] history = mHistory;
+ long[] historyTimes = mHistoryTimes;
+ int N;
+ int avail;
+ if (history == null) {
+ mHistory = history = new float[8*4];
+ mHistoryTimes = historyTimes = new long[8];
+ mNumHistory = N = 0;
+ avail = 8;
+ } else {
+ N = mNumHistory;
+ avail = history.length/4;
+ if (N == avail) {
+ avail += 8;
+ float[] newHistory = new float[avail*4];
+ System.arraycopy(history, 0, newHistory, 0, N*4);
+ mHistory = history = newHistory;
+ long[] newHistoryTimes = new long[avail];
+ System.arraycopy(historyTimes, 0, newHistoryTimes, 0, N);
+ mHistoryTimes = historyTimes = newHistoryTimes;
+ }
+ }
+
+ historyTimes[N] = mEventTime;
+
+ final int pos = N*4;
+ history[pos] = mX;
+ history[pos+1] = mY;
+ history[pos+2] = mPressure;
+ history[pos+3] = mSize;
+ mNumHistory = N+1;
+
+ mEventTime = eventTime;
+ mX = mRawX = x;
+ mY = mRawY = y;
+ mPressure = pressure;
+ mSize = size;
+ mMetaState |= metaState;
+ }
+
+ @Override
+ public String toString() {
+ return "MotionEvent{" + Integer.toHexString(System.identityHashCode(this))
+ + " action=" + mAction + " x=" + mX
+ + " y=" + mY + " pressure=" + mPressure + " size=" + mSize + "}";
+ }
+
+ public static final Parcelable.Creator<MotionEvent> CREATOR
+ = new Parcelable.Creator<MotionEvent>() {
+ public MotionEvent createFromParcel(Parcel in) {
+ MotionEvent ev = obtain();
+ ev.readFromParcel(in);
+ return ev;
+ }
+
+ public MotionEvent[] newArray(int size) {
+ return new MotionEvent[size];
+ }
+ };
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeLong(mDownTime);
+ out.writeLong(mEventTime);
+ out.writeInt(mAction);
+ out.writeFloat(mX);
+ out.writeFloat(mY);
+ out.writeFloat(mPressure);
+ out.writeFloat(mSize);
+ out.writeInt(mMetaState);
+ out.writeFloat(mRawX);
+ out.writeFloat(mRawY);
+ final int N = mNumHistory;
+ out.writeInt(N);
+ if (N > 0) {
+ final int N4 = N*4;
+ int i;
+ float[] history = mHistory;
+ for (i=0; i<N4; i++) {
+ out.writeFloat(history[i]);
+ }
+ long[] times = mHistoryTimes;
+ for (i=0; i<N; i++) {
+ out.writeLong(times[i]);
+ }
+ }
+ out.writeFloat(mXPrecision);
+ out.writeFloat(mYPrecision);
+ out.writeInt(mDeviceId);
+ out.writeInt(mEdgeFlags);
+ }
+
+ private void readFromParcel(Parcel in) {
+ mDownTime = in.readLong();
+ mEventTime = in.readLong();
+ mAction = in.readInt();
+ mX = in.readFloat();
+ mY = in.readFloat();
+ mPressure = in.readFloat();
+ mSize = in.readFloat();
+ mMetaState = in.readInt();
+ mRawX = in.readFloat();
+ mRawY = in.readFloat();
+ final int N = in.readInt();
+ if ((mNumHistory=N) > 0) {
+ final int N4 = N*4;
+ float[] history = mHistory;
+ if (history == null || history.length < N4) {
+ mHistory = history = new float[N4 + (4*4)];
+ }
+ for (int i=0; i<N4; i++) {
+ history[i] = in.readFloat();
+ }
+ long[] times = mHistoryTimes;
+ if (times == null || times.length < N) {
+ mHistoryTimes = times = new long[N + 4];
+ }
+ for (int i=0; i<N; i++) {
+ times[i] = in.readLong();
+ }
+ }
+ mXPrecision = in.readFloat();
+ mYPrecision = in.readFloat();
+ mDeviceId = in.readInt();
+ mEdgeFlags = in.readInt();
+ }
+
+}
+
diff --git a/core/java/android/view/OrientationListener.java b/core/java/android/view/OrientationListener.java
new file mode 100644
index 0000000..0add025
--- /dev/null
+++ b/core/java/android/view/OrientationListener.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.content.Context;
+import android.hardware.SensorListener;
+import android.hardware.SensorManager;
+import android.util.Config;
+import android.util.Log;
+
+/**
+ * Helper class for receiving notifications from the SensorManager when
+ * the orientation of the device has changed.
+ */
+public abstract class OrientationListener implements SensorListener {
+
+ private static final String TAG = "OrientationListener";
+ private static final boolean DEBUG = false;
+ private static final boolean localLOGV = DEBUG ? Config.LOGD : Config.LOGV;
+ private SensorManager mSensorManager;
+ private int mOrientation = ORIENTATION_UNKNOWN;
+ private boolean mEnabled = false;
+
+ /**
+ * Returned from onOrientationChanged when the device orientation cannot be determined
+ * (typically when the device is in a close to flat position).
+ *
+ * @see #onOrientationChanged
+ */
+ public static final int ORIENTATION_UNKNOWN = -1;
+
+ /**
+ * Creates a new OrientationListener.
+ *
+ * @param context for the OrientationListener.
+ */
+ public OrientationListener(Context context) {
+ mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
+ }
+
+ /**
+ * Enables the OrientationListener so it will monitor the sensor and call
+ * {@link #onOrientationChanged} when the device orientation changes.
+ */
+ public void enable() {
+ if (mEnabled == false) {
+ if (localLOGV) Log.d(TAG, "OrientationListener enabled");
+ mSensorManager.registerListener(this, SensorManager.SENSOR_ACCELEROMETER);
+ mEnabled = true;
+ }
+ }
+
+ /**
+ * Disables the OrientationListener.
+ */
+ public void disable() {
+ if (mEnabled == true) {
+ if (localLOGV) Log.d(TAG, "OrientationListener disabled");
+ mSensorManager.unregisterListener(this);
+ mEnabled = false;
+ }
+ }
+
+ /**
+ *
+ */
+ public void onSensorChanged(int sensor, float[] values) {
+ int orientation = ORIENTATION_UNKNOWN;
+ float X = values[SensorManager.RAW_DATA_X];
+ float Y = values[SensorManager.RAW_DATA_Y];
+ float Z = values[SensorManager.RAW_DATA_Z];
+ float magnitude = X*X + Y*Y;
+ // Don't trust the angle if the magnitude is small compared to the y value
+ if (magnitude * 4 >= Z*Z) {
+ float OneEightyOverPi = 57.29577957855f;
+ float angle = (float)Math.atan2(-Y, X) * OneEightyOverPi;
+ orientation = 90 - (int)Math.round(angle);
+ // normalize to 0 - 359 range
+ while (orientation >= 360) {
+ orientation -= 360;
+ }
+ while (orientation < 0) {
+ orientation += 360;
+ }
+ }
+
+ if (orientation != mOrientation) {
+ mOrientation = orientation;
+ onOrientationChanged(orientation);
+ }
+ }
+
+ public void onAccuracyChanged(int sensor, int accuracy) {
+ // TODO Auto-generated method stub
+
+ }
+
+ /**
+ * Called when the orientation of the device has changed.
+ * orientation parameter is in degrees, ranging from 0 to 359.
+ * orientation is 0 degrees when the device is oriented in its natural position,
+ * 90 degrees when its left side is at the top, 180 degrees when it is upside down,
+ * and 270 degrees when its right side is to the top.
+ * {@link #ORIENTATION_UNKNOWN} is returned when the device is close to flat
+ * and the orientation cannot be determined.
+ *
+ * @param orientation The new orientation of the device.
+ *
+ * @see #ORIENTATION_UNKNOWN
+ */
+ abstract public void onOrientationChanged(int orientation);
+}
diff --git a/core/java/android/view/RawInputEvent.java b/core/java/android/view/RawInputEvent.java
new file mode 100644
index 0000000..580a80d
--- /dev/null
+++ b/core/java/android/view/RawInputEvent.java
@@ -0,0 +1,169 @@
+/**
+ *
+ */
+package android.view;
+
+/**
+ * @hide
+ * This really belongs in services.jar; WindowManagerPolicy should go there too.
+ */
+public class RawInputEvent {
+ // Event class as defined by EventHub.
+ public static final int CLASS_KEYBOARD = 0x00000001;
+ public static final int CLASS_TOUCHSCREEN = 0x00000002;
+ public static final int CLASS_TRACKBALL = 0x00000004;
+
+ // More special classes for QueuedEvent below.
+ public static final int CLASS_CONFIGURATION_CHANGED = 0x10000000;
+
+ // Event types.
+
+ public static final int EV_SYN = 0x00;
+ public static final int EV_KEY = 0x01;
+ public static final int EV_REL = 0x02;
+ public static final int EV_ABS = 0x03;
+ public static final int EV_MSC = 0x04;
+ public static final int EV_SW = 0x05;
+ public static final int EV_LED = 0x11;
+ public static final int EV_SND = 0x12;
+ public static final int EV_REP = 0x14;
+ public static final int EV_FF = 0x15;
+ public static final int EV_PWR = 0x16;
+ public static final int EV_FF_STATUS = 0x17;
+
+ // Platform-specific event types.
+
+ public static final int EV_DEVICE_ADDED = 0x10000000;
+ public static final int EV_DEVICE_REMOVED = 0x20000000;
+
+ // Special key (EV_KEY) scan codes for pointer buttons.
+
+ public static final int BTN_FIRST = 0x100;
+
+ public static final int BTN_MISC = 0x100;
+ public static final int BTN_0 = 0x100;
+ public static final int BTN_1 = 0x101;
+ public static final int BTN_2 = 0x102;
+ public static final int BTN_3 = 0x103;
+ public static final int BTN_4 = 0x104;
+ public static final int BTN_5 = 0x105;
+ public static final int BTN_6 = 0x106;
+ public static final int BTN_7 = 0x107;
+ public static final int BTN_8 = 0x108;
+ public static final int BTN_9 = 0x109;
+
+ public static final int BTN_MOUSE = 0x110;
+ public static final int BTN_LEFT = 0x110;
+ public static final int BTN_RIGHT = 0x111;
+ public static final int BTN_MIDDLE = 0x112;
+ public static final int BTN_SIDE = 0x113;
+ public static final int BTN_EXTRA = 0x114;
+ public static final int BTN_FORWARD = 0x115;
+ public static final int BTN_BACK = 0x116;
+ public static final int BTN_TASK = 0x117;
+
+ public static final int BTN_JOYSTICK = 0x120;
+ public static final int BTN_TRIGGER = 0x120;
+ public static final int BTN_THUMB = 0x121;
+ public static final int BTN_THUMB2 = 0x122;
+ public static final int BTN_TOP = 0x123;
+ public static final int BTN_TOP2 = 0x124;
+ public static final int BTN_PINKIE = 0x125;
+ public static final int BTN_BASE = 0x126;
+ public static final int BTN_BASE2 = 0x127;
+ public static final int BTN_BASE3 = 0x128;
+ public static final int BTN_BASE4 = 0x129;
+ public static final int BTN_BASE5 = 0x12a;
+ public static final int BTN_BASE6 = 0x12b;
+ public static final int BTN_DEAD = 0x12f;
+
+ public static final int BTN_GAMEPAD = 0x130;
+ public static final int BTN_A = 0x130;
+ public static final int BTN_B = 0x131;
+ public static final int BTN_C = 0x132;
+ public static final int BTN_X = 0x133;
+ public static final int BTN_Y = 0x134;
+ public static final int BTN_Z = 0x135;
+ public static final int BTN_TL = 0x136;
+ public static final int BTN_TR = 0x137;
+ public static final int BTN_TL2 = 0x138;
+ public static final int BTN_TR2 = 0x139;
+ public static final int BTN_SELECT = 0x13a;
+ public static final int BTN_START = 0x13b;
+ public static final int BTN_MODE = 0x13c;
+ public static final int BTN_THUMBL = 0x13d;
+ public static final int BTN_THUMBR = 0x13e;
+
+ public static final int BTN_DIGI = 0x140;
+ public static final int BTN_TOOL_PEN = 0x140;
+ public static final int BTN_TOOL_RUBBER = 0x141;
+ public static final int BTN_TOOL_BRUSH = 0x142;
+ public static final int BTN_TOOL_PENCIL = 0x143;
+ public static final int BTN_TOOL_AIRBRUSH = 0x144;
+ public static final int BTN_TOOL_FINGER = 0x145;
+ public static final int BTN_TOOL_MOUSE = 0x146;
+ public static final int BTN_TOOL_LENS = 0x147;
+ public static final int BTN_TOUCH = 0x14a;
+ public static final int BTN_STYLUS = 0x14b;
+ public static final int BTN_STYLUS2 = 0x14c;
+ public static final int BTN_TOOL_DOUBLETAP = 0x14d;
+ public static final int BTN_TOOL_TRIPLETAP = 0x14e;
+
+ public static final int BTN_WHEEL = 0x150;
+ public static final int BTN_GEAR_DOWN = 0x150;
+ public static final int BTN_GEAR_UP = 0x151;
+
+ public static final int BTN_LAST = 0x15f;
+
+ // Relative axes (EV_REL) scan codes.
+
+ public static final int REL_X = 0x00;
+ public static final int REL_Y = 0x01;
+ public static final int REL_Z = 0x02;
+ public static final int REL_RX = 0x03;
+ public static final int REL_RY = 0x04;
+ public static final int REL_RZ = 0x05;
+ public static final int REL_HWHEEL = 0x06;
+ public static final int REL_DIAL = 0x07;
+ public static final int REL_WHEEL = 0x08;
+ public static final int REL_MISC = 0x09;
+ public static final int REL_MAX = 0x0f;
+
+ // Absolute axes (EV_ABS) scan codes.
+
+ public static final int ABS_X = 0x00;
+ public static final int ABS_Y = 0x01;
+ public static final int ABS_Z = 0x02;
+ public static final int ABS_RX = 0x03;
+ public static final int ABS_RY = 0x04;
+ public static final int ABS_RZ = 0x05;
+ public static final int ABS_THROTTLE = 0x06;
+ public static final int ABS_RUDDER = 0x07;
+ public static final int ABS_WHEEL = 0x08;
+ public static final int ABS_GAS = 0x09;
+ public static final int ABS_BRAKE = 0x0a;
+ public static final int ABS_HAT0X = 0x10;
+ public static final int ABS_HAT0Y = 0x11;
+ public static final int ABS_HAT1X = 0x12;
+ public static final int ABS_HAT1Y = 0x13;
+ public static final int ABS_HAT2X = 0x14;
+ public static final int ABS_HAT2Y = 0x15;
+ public static final int ABS_HAT3X = 0x16;
+ public static final int ABS_HAT3Y = 0x17;
+ public static final int ABS_PRESSURE = 0x18;
+ public static final int ABS_DISTANCE = 0x19;
+ public static final int ABS_TILT_X = 0x1a;
+ public static final int ABS_TILT_Y = 0x1b;
+ public static final int ABS_TOOL_WIDTH = 0x1c;
+ public static final int ABS_VOLUME = 0x20;
+ public static final int ABS_MISC = 0x28;
+ public static final int ABS_MAX = 0x3f;
+
+ public int deviceId;
+ public int type;
+ public int scancode;
+ public int keycode;
+ public int flags;
+ public int value;
+ public long when;
+}
diff --git a/core/java/android/view/SoundEffectConstants.java b/core/java/android/view/SoundEffectConstants.java
new file mode 100644
index 0000000..4a77af4
--- /dev/null
+++ b/core/java/android/view/SoundEffectConstants.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+/**
+ * Constants to be used to play sound effects via {@link View#playSoundEffect(int)}
+ */
+public class SoundEffectConstants {
+
+ private SoundEffectConstants() {}
+
+ public static final int CLICK = 0;
+
+ public static final int NAVIGATION_LEFT = 1;
+ public static final int NAVIGATION_UP = 2;
+ public static final int NAVIGATION_RIGHT = 3;
+ public static final int NAVIGATION_DOWN = 4;
+
+ /**
+ * Get the sonification constant for the focus directions.
+ * @param direction One of {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN},
+ * {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT}, {@link View#FOCUS_FORWARD}
+ * or {@link View#FOCUS_BACKWARD}
+
+ * @return The appropriate sonification constant.
+ */
+ public static int getContantForFocusDirection(int direction) {
+ switch (direction) {
+ case View.FOCUS_RIGHT:
+ return SoundEffectConstants.NAVIGATION_RIGHT;
+ case View.FOCUS_FORWARD:
+ case View.FOCUS_DOWN:
+ return SoundEffectConstants.NAVIGATION_DOWN;
+ case View.FOCUS_LEFT:
+ return SoundEffectConstants.NAVIGATION_LEFT;
+ case View.FOCUS_BACKWARD:
+ case View.FOCUS_UP:
+ return SoundEffectConstants.NAVIGATION_UP;
+ }
+ throw new IllegalArgumentException("direction must be one of "
+ + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, FOCUS_FORWARD, FOCUS_BACKWARD}.");
+ }
+}
diff --git a/core/java/android/view/SubMenu.java b/core/java/android/view/SubMenu.java
new file mode 100644
index 0000000..e981486
--- /dev/null
+++ b/core/java/android/view/SubMenu.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.graphics.drawable.Drawable;
+
+/**
+ * Subclass of {@link Menu} for sub menus.
+ * <p>
+ * Sub menus do not support item icons, or nested sub menus.
+ */
+
+public interface SubMenu extends Menu {
+ /**
+ * Sets the submenu header's title to the title given in <var>titleRes</var>
+ * resource identifier.
+ *
+ * @param titleRes The string resource identifier used for the title.
+ * @return This SubMenu so additional setters can be called.
+ */
+ public SubMenu setHeaderTitle(int titleRes);
+
+ /**
+ * Sets the submenu header's title to the title given in <var>title</var>.
+ *
+ * @param title The character sequence used for the title.
+ * @return This SubMenu so additional setters can be called.
+ */
+ public SubMenu setHeaderTitle(CharSequence title);
+
+ /**
+ * Sets the submenu header's icon to the icon given in <var>iconRes</var>
+ * resource id.
+ *
+ * @param iconRes The resource identifier used for the icon.
+ * @return This SubMenu so additional setters can be called.
+ */
+ public SubMenu setHeaderIcon(int iconRes);
+
+ /**
+ * Sets the submenu header's icon to the icon given in <var>icon</var>
+ * {@link Drawable}.
+ *
+ * @param icon The {@link Drawable} used for the icon.
+ * @return This SubMenu so additional setters can be called.
+ */
+ public SubMenu setHeaderIcon(Drawable icon);
+
+ /**
+ * Sets the header of the submenu to the {@link View} given in
+ * <var>view</var>. This replaces the header title and icon (and those
+ * replace this).
+ *
+ * @param view The {@link View} used for the header.
+ * @return This SubMenu so additional setters can be called.
+ */
+ public SubMenu setHeaderView(View view);
+
+ /**
+ * Clears the header of the submenu.
+ */
+ public void clearHeader();
+
+ /**
+ * Change the icon associated with this submenu's item in its parent menu.
+ *
+ * @see MenuItem#setIcon(int)
+ * @param iconRes The new icon (as a resource ID) to be displayed.
+ * @return This SubMenu so additional setters can be called.
+ */
+ public SubMenu setIcon(int iconRes);
+
+ /**
+ * Change the icon associated with this submenu's item in its parent menu.
+ *
+ * @see MenuItem#setIcon(Drawable)
+ * @param icon The new icon (as a Drawable) to be displayed.
+ * @return This SubMenu so additional setters can be called.
+ */
+ public SubMenu setIcon(Drawable icon);
+
+ /**
+ * Gets the {@link MenuItem} that represents this submenu in the parent
+ * menu. Use this for setting additional item attributes.
+ *
+ * @return The {@link MenuItem} that launches the submenu when invoked.
+ */
+ public MenuItem getItem();
+}
diff --git a/core/java/android/view/Surface.aidl b/core/java/android/view/Surface.aidl
new file mode 100644
index 0000000..90bf37a
--- /dev/null
+++ b/core/java/android/view/Surface.aidl
@@ -0,0 +1,20 @@
+/* //device/java/android/android/view/Surface.aidl
+**
+** Copyright 2007, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.view;
+
+parcelable Surface;
diff --git a/core/java/android/view/Surface.java b/core/java/android/view/Surface.java
new file mode 100644
index 0000000..54ccf33
--- /dev/null
+++ b/core/java/android/view/Surface.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.graphics.*;
+import android.os.Parcelable;
+import android.os.Parcel;
+import android.util.Log;
+
+/**
+ * Handle on to a raw buffer that is being managed by the screen compositor.
+ */
+public class Surface implements Parcelable {
+ private static final String LOG_TAG = "Surface";
+
+ /* flags used in constructor (keep in sync with ISurfaceComposer.h) */
+
+ /** Surface is created hidden */
+ public static final int HIDDEN = 0x00000004;
+
+ /** The surface is to be used by hardware accelerators or DMA engines */
+ public static final int HARDWARE = 0x00000010;
+
+ /** Implies "HARDWARE", the surface is to be used by the GPU
+ * additionally the backbuffer is never preserved for these
+ * surfaces. */
+ public static final int GPU = 0x00000028;
+
+ /** The surface contains secure content, special measures will
+ * be taken to disallow the surface's content to be copied from
+ * another process. In particular, screenshots and VNC servers will
+ * be disabled, but other measures can take place, for instance the
+ * surface might not be hardware accelerated. */
+ public static final int SECURE = 0x00000080;
+
+ /** Creates a surface where color components are interpreted as
+ * "non pre-multiplied" by their alpha channel. Of course this flag is
+ * meaningless for surfaces without an alpha channel. By default
+ * surfaces are pre-multiplied, which means that each color component is
+ * already multiplied by its alpha value. In this case the blending
+ * equation used is:
+ *
+ * DEST = SRC + DEST * (1-SRC_ALPHA)
+ *
+ * By contrast, non pre-multiplied surfaces use the following equation:
+ *
+ * DEST = SRC * SRC_ALPHA * DEST * (1-SRC_ALPHA)
+ *
+ * pre-multiplied surfaces must always be used if transparent pixels are
+ * composited on top of each-other into the surface. A pre-multiplied
+ * surface can never lower the value of the alpha component of a given
+ * pixel.
+ *
+ * In some rare situations, a non pre-multiplied surface is preferable.
+ *
+ */
+ public static final int NON_PREMULTIPLIED = 0x00000100;
+
+ /**
+ * Creates a surface without a rendering buffer. Instead, the content
+ * of the surface must be pushed by an external entity. This is type
+ * of surface can be used for efficient camera preview or movie
+ * play back.
+ */
+ public static final int PUSH_BUFFERS = 0x00000200;
+
+ /** Creates a normal surface. This is the default */
+ public static final int FX_SURFACE_NORMAL = 0x00000000;
+
+ /** Creates a Blur surface. Everything behind this surface is blurred
+ * by some amount. The quality and refresh speed of the blur effect
+ * is not settable or guaranteed.
+ * It is an error to lock a Blur surface, since it doesn't have
+ * a backing store.
+ */
+ public static final int FX_SURFACE_BLUR = 0x00010000;
+
+ /** Creates a Dim surface. Everything behind this surface is dimmed
+ * by the amount specified in setAlpha().
+ * It is an error to lock a Dim surface, since it doesn't have
+ * a backing store.
+ */
+ public static final int FX_SURFACE_DIM = 0x00020000;
+
+ /** Mask used for FX values above */
+ public static final int FX_SURFACE_MASK = 0x000F0000;
+
+ /* flags used with setFlags() (keep in sync with ISurfaceComposer.h) */
+
+ /** Hide the surface. Equivalent to calling hide() */
+ public static final int SURFACE_HIDDEN = 0x01;
+
+ /** Freeze the surface. Equivalent to calling freeze() */
+ public static final int SURACE_FROZEN = 0x02;
+
+ /** Enable dithering when compositing this surface */
+ public static final int SURFACE_DITHER = 0x04;
+
+ public static final int SURFACE_BLUR_FREEZE= 0x10;
+
+ /* orientations for setOrientation() */
+ public static final int ROTATION_0 = 0;
+ public static final int ROTATION_90 = 1;
+ public static final int ROTATION_180 = 2;
+ public static final int ROTATION_270 = 3;
+
+ @SuppressWarnings("unused")
+ private int mSurface;
+ @SuppressWarnings("unused")
+ private int mSaveCount;
+ @SuppressWarnings("unused")
+ private Canvas mCanvas;
+
+ /**
+ * Exception thrown when a surface couldn't be created or resized
+ */
+ public static class OutOfResourcesException extends Exception {
+ public OutOfResourcesException() {
+ }
+ public OutOfResourcesException(String name) {
+ super(name);
+ }
+ }
+
+ /*
+ * We use a class initializer to allow the native code to cache some
+ * field offsets.
+ */
+ native private static void nativeClassInit();
+ static { nativeClassInit(); }
+
+
+ /**
+ * create a surface
+ * {@hide}
+ */
+ public Surface(SurfaceSession s,
+ int pid, int display, int w, int h, int format, int flags)
+ throws OutOfResourcesException {
+ mCanvas = new Canvas();
+ init(s,pid,display,w,h,format,flags);
+ }
+
+ /**
+ * Create an empty surface, which will later be filled in by
+ * readFromParcel().
+ * {@hide}
+ */
+ public Surface() {
+ mCanvas = new Canvas();
+ }
+
+ /**
+ * Copy another surface to this one. This surface now holds a reference
+ * to the same data as the original surface, and is -not- the owner.
+ * {@hide}
+ */
+ public native void copyFrom(Surface o);
+
+ /**
+ * Does this object hold a valid surface? Returns true if it holds
+ * a physical surface, so lockCanvas() will succeed. Otherwise
+ * returns false.
+ */
+ public native boolean isValid();
+
+ /** Call this free the surface up. {@hide} */
+ public native void clear();
+
+ /** draw into a surface */
+ public Canvas lockCanvas(Rect dirty) throws OutOfResourcesException {
+ /* the dirty rectangle may be expanded to the surface's size, if
+ * for instance it has been resized or if the bits were lost, since
+ * the last call.
+ */
+ return lockCanvasNative(dirty);
+ }
+
+ private native Canvas lockCanvasNative(Rect dirty);
+
+ /** unlock the surface and asks a page flip */
+ public native void unlockCanvasAndPost(Canvas canvas);
+
+ /**
+ * unlock the surface. the screen won't be updated until
+ * post() or postAll() is called
+ */
+ public native void unlockCanvas(Canvas canvas);
+
+ /** start/end a transaction {@hide} */
+ public static native void openTransaction();
+ /** {@hide} */
+ public static native void closeTransaction();
+
+ /**
+ * Freezes the specified display, No updating of the screen will occur
+ * until unfreezeDisplay() is called. Everything else works as usual though,
+ * in particular transactions.
+ * @param display
+ * {@hide}
+ */
+ public static native void freezeDisplay(int display);
+
+ /**
+ * resume updating the specified display.
+ * @param display
+ * {@hide}
+ */
+ public static native void unfreezeDisplay(int display);
+
+ /**
+ * set the orientation of the given display.
+ * @param display
+ * @param orientation
+ */
+ public static native void setOrientation(int display, int orientation);
+
+ /**
+ * set surface parameters.
+ * needs to be inside open/closeTransaction block
+ */
+ public native void setLayer(int zorder);
+ public native void setPosition(int x, int y);
+ public native void setSize(int w, int h);
+
+ public native void hide();
+ public native void show();
+ public native void setTransparentRegionHint(Region region);
+ public native void setAlpha(float alpha);
+ public native void setMatrix(float dsdx, float dtdx,
+ float dsdy, float dtdy);
+
+ public native void freeze();
+ public native void unfreeze();
+
+ public native void setFreezeTint(int tint);
+
+ public native void setFlags(int flags, int mask);
+
+ @Override
+ public String toString() {
+ return "Surface(native-token=" + mSurface + ")";
+ }
+
+ private Surface(Parcel source) throws OutOfResourcesException {
+ init(source);
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public native void readFromParcel(Parcel source);
+ public native void writeToParcel(Parcel dest, int flags);
+
+ public static final Parcelable.Creator<Surface> CREATOR
+ = new Parcelable.Creator<Surface>()
+ {
+ public Surface createFromParcel(Parcel source) {
+ try {
+ return new Surface(source);
+ } catch (Exception e) {
+ Log.e(LOG_TAG, "Exception creating surface from parcel", e);
+ }
+ return null;
+ }
+
+ public Surface[] newArray(int size) {
+ return new Surface[size];
+ }
+ };
+
+ /* no user serviceable parts here ... */
+ @Override
+ protected void finalize() throws Throwable {
+ clear();
+ }
+
+ private native void init(SurfaceSession s,
+ int pid, int display, int w, int h, int format, int flags)
+ throws OutOfResourcesException;
+
+ private native void init(Parcel source);
+}
diff --git a/core/java/android/view/SurfaceHolder.java b/core/java/android/view/SurfaceHolder.java
new file mode 100644
index 0000000..21a72e7
--- /dev/null
+++ b/core/java/android/view/SurfaceHolder.java
@@ -0,0 +1,284 @@
+/*
+ * 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.view;
+
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import static android.view.WindowManager.LayoutParams.MEMORY_TYPE_NORMAL;
+import static android.view.WindowManager.LayoutParams.MEMORY_TYPE_HARDWARE;
+import static android.view.WindowManager.LayoutParams.MEMORY_TYPE_GPU;
+import static android.view.WindowManager.LayoutParams.MEMORY_TYPE_PUSH_BUFFERS;
+
+/**
+ * Abstract interface to someone holding a display surface. Allows you to
+ * control the surface size and format, edit the pixels in the surface, and
+ * monitor changes to the surface. This interface is typically available
+ * through the {@link SurfaceView} class.
+ *
+ * <p>When using this interface from a thread different than the one running
+ * its {@link SurfaceView}, you will want to carefully read the
+ * {@link #lockCanvas} and {@link Callback#surfaceCreated Callback.surfaceCreated}.
+ */
+public interface SurfaceHolder {
+ /**
+ * Surface type.
+ *
+ * @see #SURFACE_TYPE_NORMAL
+ * @see #SURFACE_TYPE_HARDWARE
+ * @see #SURFACE_TYPE_GPU
+ * @see #SURFACE_TYPE_PUSH_BUFFERS
+ */
+
+ /** Surface type: creates a regular surface, usually in main, non
+ * contiguous, cached/buffered RAM. */
+ public static final int SURFACE_TYPE_NORMAL = MEMORY_TYPE_NORMAL;
+ /** Surface type: creates a suited to be used with DMA engines and
+ * hardware accelerators. */
+ public static final int SURFACE_TYPE_HARDWARE = MEMORY_TYPE_HARDWARE;
+ /** Surface type: creates a surface suited to be used with the GPU */
+ public static final int SURFACE_TYPE_GPU = MEMORY_TYPE_GPU;
+ /** Surface type: creates a "push" surface, that is a surface that
+ * doesn't owns its buffers. With such a surface lockCanvas will fail. */
+ public static final int SURFACE_TYPE_PUSH_BUFFERS = MEMORY_TYPE_PUSH_BUFFERS;
+
+ /**
+ * Exception that is thrown from {@link #lockCanvas} when called on a Surface
+ * whose is SURFACE_TYPE_PUSH_BUFFERS.
+ */
+ public static class BadSurfaceTypeException extends RuntimeException {
+ public BadSurfaceTypeException() {
+ }
+
+ public BadSurfaceTypeException(String name) {
+ super(name);
+ }
+ }
+
+ /**
+ * A client may implement this interface to receive information about
+ * changes to the surface. When used with a {@link SurfaceView}, the
+ * Surface being held is only available between calls to
+ * {@link #surfaceCreated(SurfaceHolder)} and
+ * {@link #surfaceDestroyed(SurfaceHolder). The Callback is set with
+ * {@link SurfaceHolder#addCallback SurfaceHolder.addCallback} method.
+ */
+ public interface Callback {
+ /**
+ * This is called immediately after the surface is first created.
+ * Implementations of this should start up whatever rendering code
+ * they desire. Note that only one thread can ever draw into
+ * a {@link Surface}, so you should not draw into the Surface here
+ * if your normal rendering will be in another thread.
+ *
+ * @param holder The SurfaceHolder whose surface is being created.
+ */
+ public void surfaceCreated(SurfaceHolder holder);
+
+ /**
+ * This is called immediately after any structural changes (format or
+ * size) have been made to the surface. You should at this point update
+ * the imagery in the surface. This method is always called at least
+ * once, after {@link #surfaceCreated}.
+ *
+ * @param holder The SurfaceHolder whose surface has changed.
+ * @param format The new PixelFormat of the surface.
+ * @param width The new width of the surface.
+ * @param height The new height of the surface.
+ */
+ public void surfaceChanged(SurfaceHolder holder, int format, int width,
+ int height);
+
+ /**
+ * This is called immediately before a surface is being destroyed. After
+ * returning from this call, you should no longer try to access this
+ * surface. If you have a rendering thread that directly accesses
+ * the surface, you must ensure that thread is no longer touching the
+ * Surface before returning from this function.
+ *
+ * @param holder The SurfaceHolder whose surface is being destroyed.
+ */
+ public void surfaceDestroyed(SurfaceHolder holder);
+ }
+
+ /**
+ * Add a Callback interface for this holder. There can several Callback
+ * interfaces associated to a holder.
+ *
+ * @param callback The new Callback interface.
+ */
+ public void addCallback(Callback callback);
+
+ /**
+ * Removes a previously added Callback interface from this holder.
+ *
+ * @param callback The Callback interface to remove.
+ */
+ public void removeCallback(Callback callback);
+
+ /**
+ * Use this method to find out if the surface is in the process of being
+ * created from Callback methods. This is intended to be used with
+ * {@link Callback#surfaceChanged}.
+ *
+ * @return true if the surface is in the process of being created.
+ */
+ public boolean isCreating();
+
+ /**
+ * Sets the surface's type. Surfaces intended to be used with OpenGL ES
+ * should be of SURFACE_TYPE_GPU, surfaces accessed by DMA engines and
+ * hardware accelerators should be of type SURFACE_TYPE_HARDWARE.
+ * Failing to set the surface's type appropriately could result in
+ * degraded performance or failure.
+ *
+ * @param type The surface's memory type.
+ */
+ public void setType(int type);
+
+ /**
+ * Make the surface a fixed size. It will never change from this size.
+ * When working with a {link SurfaceView}, this must be called from the
+ * same thread running the SurfaceView's window.
+ *
+ * @param width The surface's width.
+ * @param height The surface's height.
+ */
+ public void setFixedSize(int width, int height);
+
+ /**
+ * Allow the surface to resized based on layout of its container (this is
+ * the default). When this is enabled, you should monitor
+ * {@link Callback#surfaceChanged} for changes to the size of the surface.
+ * When working with a {link SurfaceView}, this must be called from the
+ * same thread running the SurfaceView's window.
+ */
+ public void setSizeFromLayout();
+
+ /**
+ * Set the desired PixelFormat of the surface. The default is OPAQUE.
+ * When working with a {link SurfaceView}, this must be called from the
+ * same thread running the SurfaceView's window.
+ *
+ * @param format A constant from PixelFormat.
+ *
+ * @see android.graphics.PixelFormat
+ */
+ public void setFormat(int format);
+
+ /**
+ * Enable or disable option to keep the screen turned on while this
+ * surface is displayed. The default is false, allowing it to turn off.
+ * Enabling the option effectivelty.
+ * This is safe to call from any thread.
+ *
+ * @param screenOn Supply to true to force the screen to stay on, false
+ * to allow it to turn off.
+ */
+ public void setKeepScreenOn(boolean screenOn);
+
+ /**
+ * Start editing the pixels in the surface. The returned Canvas can be used
+ * to draw into the surface's bitmap. A null is returned if the surface has
+ * not been created or otherwise can not be edited. You will usually need
+ * to implement {@link Callback#surfaceCreated Callback.surfaceCreated}
+ * to find out when the Surface is available for use.
+ *
+ * <p>The content of the Surface is never preserved between unlockCanvas() and
+ * lockCanvas(), for this reason, every pixel within the Surface area
+ * must be written. The only exception to this rule is when a dirty
+ * rectangle is specified, in which case, non dirty pixels will be
+ * preserved.
+ *
+ * <p>If you call this repeatedly when the Surface is not ready (before
+ * {@link Callback#surfaceCreated Callback.surfaceCreated} or after
+ * {@link Callback#surfaceDestroyed Callback.surfaceDestroyed}), your calls
+ * will be throttled to a slow rate in order to avoid consuming CPU.
+ *
+ * <p>If null is not returned, this function internally holds a lock until
+ * the corresponding {@link #unlockCanvasAndPost} call, preventing
+ * {@link SurfaceView} from creating, destroying, or modifying the surface
+ * while it is being drawn. This can be more convenience than accessing
+ * the Surface directly, as you do not need to do special synchronization
+ * with a drawing thread in {@link Callback#surfaceDestroyed
+ * Callback.surfaceDestroyed}.
+ *
+ * @return Canvas Use to draw into the surface.
+ */
+ public Canvas lockCanvas();
+
+
+ /**
+ * Just like {@link #lockCanvas()} but allows to specify a dirty rectangle.
+ * Every
+ * pixel within that rectangle must be written; however pixels outside
+ * the dirty rectangle will be preserved by the next call to lockCanvas().
+ *
+ * @see android.view.SurfaceHolder#lockCanvas
+ *
+ * @param dirty Area of the Surface that will be modified.
+ * @return Canvas Use to draw into the surface.
+ */
+ public Canvas lockCanvas(Rect dirty);
+
+ /**
+ * Finish editing pixels in the surface. After this call, the surface's
+ * current pixels will be shown on the screen, but its content is lost,
+ * in particular there is no guarantee that the content of the Surface
+ * will remain unchanged when lockCanvas() is called again.
+ *
+ * @see android.view.SurfaceHolder.lockCanvas
+ *
+ * @param canvas The Canvas previously returned by lockCanvas().
+ */
+ public void unlockCanvasAndPost(Canvas canvas);
+
+ /**
+ * Retrieve the current size of the surface. Note: do not modify the
+ * returned Rect. This is only safe to call from the thread of
+ * {@link SurfaceView}'s window, or while inside of
+ * {@link #lockCanvas()}.
+ *
+ * @return Rect The surface's dimensions. The left and top are always 0.
+ */
+ public Rect getSurfaceFrame();
+
+ /**
+ * Direct access to the surface object. The Surface may not always be
+ * available -- for example when using a {@link SurfaceView} the holder's
+ * Surface is not created until the view has been attached to the window
+ * manager and performed a layout in order to determine the dimensions
+ * and screen position of the Surface. You will thus usually need
+ * to implement {@link Callback#surfaceCreated Callback.surfaceCreated}
+ * to find out when the Surface is available for use.
+ *
+ * <p>Note that if you directly access the Surface from another thread,
+ * it is critical that you correctly implement
+ * {@link Callback#surfaceCreated Callback.surfaceCreated} and
+ * {@link Callback#surfaceDestroyed Callback.surfaceDestroyed} to ensure
+ * that thread only accesses the Surface while it is valid, and that the
+ * Surface does not get destroyed while the thread is using it.
+ *
+ * <p>This method is intended to be used by frameworks which often need
+ * direct access to the Surface object (usually to pass it to native code).
+ * When designing APIs always use SurfaceHolder to pass surfaces around
+ * as opposed to the Surface object itself. A rule of thumb is that
+ * application code should never have to call this method.
+ *
+ * @return Surface The surface.
+ */
+ public Surface getSurface();
+}
diff --git a/core/java/android/view/SurfaceSession.java b/core/java/android/view/SurfaceSession.java
new file mode 100644
index 0000000..2a04675
--- /dev/null
+++ b/core/java/android/view/SurfaceSession.java
@@ -0,0 +1,49 @@
+/*
+ * 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.view;
+
+
+/**
+ * An instance of this class represents a connection to the surface
+ * flinger, in which you can create one or more Surface instances that will
+ * be composited to the screen.
+ * {@hide}
+ */
+public class SurfaceSession {
+ /** Create a new connection with the surface flinger. */
+ public SurfaceSession() {
+ init();
+ }
+
+ /** Forcibly detach native resources associated with this object.
+ * Unlike destroy(), after this call any surfaces that were created
+ * from the session will no longer work. The session itself is destroyed.
+ */
+ public native void kill();
+
+ /* no user serviceable parts here ... */
+ @Override
+ protected void finalize() throws Throwable {
+ destroy();
+ }
+
+ private native void init();
+ private native void destroy();
+
+ private int mClient;
+}
+
diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java
new file mode 100644
index 0000000..57689f2
--- /dev/null
+++ b/core/java/android/view/SurfaceView.java
@@ -0,0 +1,593 @@
+/*
+ * 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.view;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.PixelFormat;
+import android.graphics.PorterDuff;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.os.Handler;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.ParcelFileDescriptor;
+import android.util.AttributeSet;
+import android.util.Config;
+import android.util.Log;
+import java.util.ArrayList;
+
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * Provides a dedicated drawing surface embedded inside of a view hierarchy.
+ * You can control the format of this surface and, if you like, its size; the
+ * SurfaceView takes care of placing the surface at the correct location on the
+ * screen
+ *
+ * <p>The surface is Z ordered so that it is behind the window holding its
+ * SurfaceView; the SurfaceView punches a hole in its window to allow its
+ * surface to be displayed. The view hierarchy will take care of correctly
+ * compositing with the Surface any siblings of the SurfaceView that would
+ * normally appear on top of it. This can be used to place overlays such as
+ * buttons on top of the Surface, though note however that it can have an
+ * impact on performance since a full alpha-blended composite will be performed
+ * each time the Surface changes.
+ *
+ * <p>Access to the underlying surface is provided via the SurfaceHolder interface,
+ * which can be retrieved by calling {@link #getHolder}.
+ *
+ * <p>The Surface will be created for you while the SurfaceView's window is
+ * visible; you should implement {@link SurfaceHolder.Callback#surfaceCreated}
+ * and {@link SurfaceHolder.Callback#surfaceDestroyed} to discover when the
+ * Surface is created and destroyed as the window is shown and hidden.
+ *
+ * <p>One of the purposes of this class is to provide a surface in which a
+ * secondary thread can render in to the screen. If you are going to use it
+ * this way, you need to be aware of some threading semantics:
+ *
+ * <ul>
+ * <li> All SurfaceView and
+ * {@link SurfaceHolder.Callback SurfaceHolder.Callback} methods will be called
+ * from the thread running the SurfaceView's window (typically the main thread
+ * of the application). They thus need to correctly synchronize with any
+ * state that is also touched by the drawing thread.
+ * <li> You must ensure that the drawing thread only touches the underlying
+ * Surface while it is valid -- between
+ * {@link SurfaceHolder.Callback#surfaceCreated SurfaceHolder.Callback.surfaceCreated()}
+ * and
+ * {@link SurfaceHolder.Callback#surfaceDestroyed SurfaceHolder.Callback.surfaceDestroyed()}.
+ * </ul>
+ */
+public class SurfaceView extends View {
+ static private final String TAG = "SurfaceView";
+ static private final boolean DEBUG = false;
+ static private final boolean localLOGV = DEBUG ? true : Config.LOGV;
+
+ final ArrayList<SurfaceHolder.Callback> mCallbacks
+ = new ArrayList<SurfaceHolder.Callback>();
+
+ final ReentrantLock mSurfaceLock = new ReentrantLock();
+ final Surface mSurface = new Surface();
+ boolean mDrawingStopped = true;
+
+ final WindowManager.LayoutParams mLayout
+ = new WindowManager.LayoutParams();
+ IWindowSession mSession;
+ MyWindow mWindow;
+ final Rect mWinFrame = new Rect();
+ final Rect mCoveredInsets = new Rect();
+
+ static final int KEEP_SCREEN_ON_MSG = 1;
+ static final int GET_NEW_SURFACE_MSG = 2;
+
+ boolean mIsCreating = false;
+
+ final Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case KEEP_SCREEN_ON_MSG: {
+ setKeepScreenOn(msg.arg1 != 0);
+ } break;
+ case GET_NEW_SURFACE_MSG: {
+ handleGetNewSurface();
+ } break;
+ }
+ }
+ };
+
+ boolean mRequestedVisible = false;
+ int mRequestedWidth = -1;
+ int mRequestedHeight = -1;
+ int mRequestedFormat = PixelFormat.OPAQUE;
+ int mRequestedType = -1;
+
+ boolean mHaveFrame = false;
+ boolean mDestroyReportNeeded = false;
+ boolean mNewSurfaceNeeded = false;
+ long mLastLockTime = 0;
+
+ boolean mVisible = false;
+ int mLeft = -1;
+ int mTop = -1;
+ int mWidth = -1;
+ int mHeight = -1;
+ int mFormat = -1;
+ int mType = -1;
+ final Rect mSurfaceFrame = new Rect();
+
+ public SurfaceView(Context context) {
+ super(context);
+ setWillNotDraw(true);
+ }
+
+ public SurfaceView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ setWillNotDraw(true);
+ }
+
+ public SurfaceView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ setWillNotDraw(true);
+ }
+
+ /**
+ * Return the SurfaceHolder providing access and control over this
+ * SurfaceView's underlying surface.
+ *
+ * @return SurfaceHolder The holder of the surface.
+ */
+ public SurfaceHolder getHolder() {
+ return mSurfaceHolder;
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ mParent.requestTransparentRegion(this);
+ mSession = getWindowSession();
+ mLayout.token = getWindowToken();
+ mLayout.setTitle("SurfaceView");
+ }
+
+ @Override
+ protected void onWindowVisibilityChanged(int visibility) {
+ super.onWindowVisibilityChanged(visibility);
+ mRequestedVisible = visibility == VISIBLE;
+ updateWindow(false);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ mRequestedVisible = false;
+ updateWindow(false);
+ mHaveFrame = false;
+ if (mWindow != null) {
+ try {
+ mSession.remove(mWindow);
+ } catch (RemoteException ex) {
+ }
+ mWindow = null;
+ }
+ mSession = null;
+ mLayout.token = null;
+
+ super.onDetachedFromWindow();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int width = getDefaultSize(mRequestedWidth, widthMeasureSpec);
+ int height = getDefaultSize(mRequestedHeight, heightMeasureSpec);
+ setMeasuredDimension(width, height);
+ }
+
+ @Override
+ protected void onScrollChanged(int l, int t, int oldl, int oldt) {
+ super.onScrollChanged(l, t, oldl, oldt);
+ updateWindow(false);
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ updateWindow(false);
+ }
+
+ @Override
+ public boolean gatherTransparentRegion(Region region) {
+ boolean opaque = true;
+ if ((mPrivateFlags & SKIP_DRAW) == 0) {
+ // this view draws, remove it from the transparent region
+ opaque = super.gatherTransparentRegion(region);
+ } else if (region != null) {
+ int w = getWidth();
+ int h = getHeight();
+ if (w>0 && h>0) {
+ getLocationInWindow(mLocation);
+ // otherwise, punch a hole in the whole hierarchy
+ int l = mLocation[0];
+ int t = mLocation[1];
+ region.op(l, t, l+w, t+h, Region.Op.UNION);
+ }
+ }
+ if (PixelFormat.formatHasAlpha(mRequestedFormat)) {
+ opaque = false;
+ }
+ return opaque;
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ // draw() is not called when SKIP_DRAW is set
+ if ((mPrivateFlags & SKIP_DRAW) == 0) {
+ // punch a whole in the view-hierarchy below us
+ canvas.drawColor(0, PorterDuff.Mode.CLEAR);
+ }
+ super.draw(canvas);
+ }
+
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ // if SKIP_DRAW is cleared, draw() has already punched a hole
+ if ((mPrivateFlags & SKIP_DRAW) == SKIP_DRAW) {
+ // punch a whole in the view-hierarchy below us
+ canvas.drawColor(0, PorterDuff.Mode.CLEAR);
+ }
+ // reposition ourselves where the surface is
+ mHaveFrame = true;
+ updateWindow(false);
+ super.dispatchDraw(canvas);
+ }
+
+ private void updateWindow(boolean force) {
+ if (!mHaveFrame) {
+ return;
+ }
+
+ int myWidth = mRequestedWidth;
+ if (myWidth <= 0) myWidth = getWidth();
+ int myHeight = mRequestedHeight;
+ if (myHeight <= 0) myHeight = getHeight();
+
+ getLocationInWindow(mLocation);
+ final boolean creating = mWindow == null;
+ final boolean formatChanged = mFormat != mRequestedFormat;
+ final boolean sizeChanged = mWidth != myWidth || mHeight != myHeight;
+ final boolean visibleChanged = mVisible != mRequestedVisible
+ || mNewSurfaceNeeded;
+ final boolean typeChanged = mType != mRequestedType;
+ if (force || creating || formatChanged || sizeChanged || visibleChanged
+ || typeChanged || mLeft != mLocation[0] || mTop != mLocation[1]) {
+
+ if (localLOGV) Log.i(TAG, "Changes: creating=" + creating
+ + " format=" + formatChanged + " size=" + sizeChanged
+ + " visible=" + visibleChanged
+ + " left=" + (mLeft != mLocation[0])
+ + " top=" + (mTop != mLocation[1]));
+
+ try {
+ final boolean visible = mVisible = mRequestedVisible;
+ mLeft = mLocation[0];
+ mTop = mLocation[1];
+ mWidth = myWidth;
+ mHeight = myHeight;
+ mFormat = mRequestedFormat;
+ mType = mRequestedType;
+
+ mLayout.x = mLeft;
+ mLayout.y = mTop;
+ mLayout.width = getWidth();
+ mLayout.height = getHeight();
+ mLayout.format = mRequestedFormat;
+ mLayout.flags |=WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
+ | WindowManager.LayoutParams.FLAG_SCALED
+ | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+ | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
+ ;
+
+ mLayout.memoryType = mRequestedType;
+
+ if (mWindow == null) {
+ mWindow = new MyWindow();
+ mLayout.type = WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA;
+ mLayout.gravity = Gravity.LEFT|Gravity.TOP;
+ mSession.add(mWindow, mLayout,
+ mVisible ? VISIBLE : GONE, mCoveredInsets);
+ }
+
+ if (visibleChanged && (!visible || mNewSurfaceNeeded)) {
+ reportSurfaceDestroyed();
+ }
+
+ mNewSurfaceNeeded = false;
+
+ mSurfaceLock.lock();
+ mDrawingStopped = !visible;
+ final int relayoutResult = mSession.relayout(
+ mWindow, mLayout, mWidth, mHeight,
+ visible ? VISIBLE : GONE, mWinFrame, mCoveredInsets, mSurface);
+ if (localLOGV) Log.i(TAG, "New surface: " + mSurface
+ + ", vis=" + visible + ", frame=" + mWinFrame);
+ mSurfaceFrame.left = 0;
+ mSurfaceFrame.top = 0;
+ mSurfaceFrame.right = mWinFrame.width();
+ mSurfaceFrame.bottom = mWinFrame.height();
+ mSurfaceLock.unlock();
+
+ try {
+ if (visible) {
+ mDestroyReportNeeded = true;
+
+ SurfaceHolder.Callback callbacks[];
+ synchronized (mCallbacks) {
+ callbacks = new SurfaceHolder.Callback[mCallbacks.size()];
+ mCallbacks.toArray(callbacks);
+ }
+
+ if (visibleChanged) {
+ mIsCreating = true;
+ for (SurfaceHolder.Callback c : callbacks) {
+ c.surfaceCreated(mSurfaceHolder);
+ }
+ }
+ if (creating || formatChanged || sizeChanged
+ || visibleChanged) {
+ for (SurfaceHolder.Callback c : callbacks) {
+ c.surfaceChanged(mSurfaceHolder, mFormat, mWidth, mHeight);
+ }
+ }
+ }
+ } finally {
+ mIsCreating = false;
+ if (creating || (relayoutResult&WindowManagerImpl.RELAYOUT_FIRST_TIME) != 0) {
+ mSession.finishDrawing(mWindow);
+ }
+ }
+ } catch (RemoteException ex) {
+ }
+ if (localLOGV) Log.v(
+ TAG, "Layout: x=" + mLayout.x + " y=" + mLayout.y +
+ " w=" + mLayout.width + " h=" + mLayout.height +
+ ", frame=" + mSurfaceFrame);
+ }
+ }
+
+ private void reportSurfaceDestroyed() {
+ if (mDestroyReportNeeded) {
+ mDestroyReportNeeded = false;
+ SurfaceHolder.Callback callbacks[];
+ synchronized (mCallbacks) {
+ callbacks = new SurfaceHolder.Callback[mCallbacks.size()];
+ mCallbacks.toArray(callbacks);
+ }
+ for (SurfaceHolder.Callback c : callbacks) {
+ c.surfaceDestroyed(mSurfaceHolder);
+ }
+ }
+ super.onDetachedFromWindow();
+ }
+
+ void handleGetNewSurface() {
+ mNewSurfaceNeeded = true;
+ updateWindow(false);
+ }
+
+ private class MyWindow extends IWindow.Stub {
+ public void resized(int w, int h, boolean reportDraw) {
+ if (localLOGV) Log.v(
+ "SurfaceView", SurfaceView.this + " got resized: w=" +
+ w + " h=" + h + ", cur w=" + mCurWidth + " h=" + mCurHeight);
+ synchronized (this) {
+ if (mCurWidth != w || mCurHeight != h) {
+ mCurWidth = w;
+ mCurHeight = h;
+ }
+ if (reportDraw) {
+ try {
+ mSession.finishDrawing(mWindow);
+ } catch (RemoteException e) {
+ }
+ }
+ }
+ }
+
+ public void dispatchKey(KeyEvent event) {
+ //Log.w("SurfaceView", "Unexpected key event in surface: " + event);
+ if (mSession != null && mSurface != null) {
+ try {
+ mSession.finishKey(mWindow);
+ } catch (RemoteException ex) {
+ }
+ }
+ }
+
+ public void dispatchPointer(MotionEvent event, long eventTime) {
+ Log.w("SurfaceView", "Unexpected pointer event in surface: " + event);
+ //if (mSession != null && mSurface != null) {
+ // try {
+ // //mSession.finishKey(mWindow);
+ // } catch (RemoteException ex) {
+ // }
+ //}
+ }
+
+ public void dispatchTrackball(MotionEvent event, long eventTime) {
+ Log.w("SurfaceView", "Unexpected trackball event in surface: " + event);
+ //if (mSession != null && mSurface != null) {
+ // try {
+ // //mSession.finishKey(mWindow);
+ // } catch (RemoteException ex) {
+ // }
+ //}
+ }
+
+ public void dispatchAppVisibility(boolean visible) {
+ // The point of SurfaceView is to let the app control the surface.
+ }
+
+ public void dispatchGetNewSurface() {
+ Message msg = mHandler.obtainMessage(GET_NEW_SURFACE_MSG);
+ mHandler.sendMessage(msg);
+ }
+
+ public void windowFocusChanged(boolean hasFocus, boolean touchEnabled) {
+ Log.w("SurfaceView", "Unexpected focus in surface: focus=" + hasFocus + ", touchEnabled=" + touchEnabled);
+ }
+
+ public void executeCommand(String command, String parameters, ParcelFileDescriptor out) {
+ }
+
+ int mCurWidth = -1;
+ int mCurHeight = -1;
+ }
+
+ private SurfaceHolder mSurfaceHolder = new SurfaceHolder() {
+
+ private static final String LOG_TAG = "SurfaceHolder";
+
+ public boolean isCreating() {
+ return mIsCreating;
+ }
+
+ public void addCallback(Callback callback) {
+ synchronized (mCallbacks) {
+ // This is a linear search, but in practice we'll
+ // have only a couple callbacks, so it doesn't matter.
+ if (mCallbacks.contains(callback) == false) {
+ mCallbacks.add(callback);
+ }
+ }
+ }
+
+ public void removeCallback(Callback callback) {
+ synchronized (mCallbacks) {
+ mCallbacks.remove(callback);
+ }
+ }
+
+ public void setFixedSize(int width, int height) {
+ if (mRequestedWidth != width || mRequestedHeight != height) {
+ mRequestedWidth = width;
+ mRequestedHeight = height;
+ requestLayout();
+ }
+ }
+
+ public void setSizeFromLayout() {
+ if (mRequestedWidth != -1 || mRequestedHeight != -1) {
+ mRequestedWidth = mRequestedHeight = -1;
+ requestLayout();
+ }
+ }
+
+ public void setFormat(int format) {
+ mRequestedFormat = format;
+ if (mWindow != null) {
+ updateWindow(false);
+ }
+ }
+
+ public void setType(int type) {
+ switch (type) {
+ case SURFACE_TYPE_NORMAL:
+ case SURFACE_TYPE_HARDWARE:
+ case SURFACE_TYPE_GPU:
+ case SURFACE_TYPE_PUSH_BUFFERS:
+ mRequestedType = type;
+ if (mWindow != null) {
+ updateWindow(false);
+ }
+ break;
+ }
+ }
+
+ public void setKeepScreenOn(boolean screenOn) {
+ Message msg = mHandler.obtainMessage(KEEP_SCREEN_ON_MSG);
+ msg.arg1 = screenOn ? 1 : 0;
+ mHandler.sendMessage(msg);
+ }
+
+ public Canvas lockCanvas() {
+ return internalLockCanvas(null);
+ }
+
+ public Canvas lockCanvas(Rect dirty) {
+ return internalLockCanvas(dirty);
+ }
+
+ private final Canvas internalLockCanvas(Rect dirty) {
+ if (mType == SURFACE_TYPE_PUSH_BUFFERS) {
+ throw new BadSurfaceTypeException(
+ "Surface type is SURFACE_TYPE_PUSH_BUFFERS");
+ }
+ mSurfaceLock.lock();
+
+ if (localLOGV) Log.i(TAG, "Locking canvas... stopped="
+ + mDrawingStopped + ", win=" + mWindow);
+
+ Canvas c = null;
+ if (!mDrawingStopped && mWindow != null) {
+ Rect frame = dirty != null ? dirty : mSurfaceFrame;
+ try {
+ c = mSurface.lockCanvas(frame);
+ } catch (Exception e) {
+ Log.e(LOG_TAG, "Exception locking surface", e);
+ }
+ }
+
+ if (localLOGV) Log.i(TAG, "Returned canvas: " + c);
+ if (c != null) {
+ mLastLockTime = SystemClock.uptimeMillis();
+ return c;
+ }
+
+ // If the Surface is not ready to be drawn, then return null,
+ // but throttle calls to this function so it isn't called more
+ // than every 100ms.
+ long now = SystemClock.uptimeMillis();
+ long nextTime = mLastLockTime + 100;
+ if (nextTime > now) {
+ try {
+ Thread.sleep(nextTime-now);
+ } catch (InterruptedException e) {
+ }
+ now = SystemClock.uptimeMillis();
+ }
+ mLastLockTime = now;
+ mSurfaceLock.unlock();
+
+ return null;
+ }
+
+ public void unlockCanvasAndPost(Canvas canvas) {
+ mSurface.unlockCanvasAndPost(canvas);
+ mSurfaceLock.unlock();
+ }
+
+ public Surface getSurface() {
+ return mSurface;
+ }
+
+ public Rect getSurfaceFrame() {
+ return mSurfaceFrame;
+ }
+ };
+}
+
diff --git a/core/java/android/view/TouchDelegate.java b/core/java/android/view/TouchDelegate.java
new file mode 100644
index 0000000..057df92
--- /dev/null
+++ b/core/java/android/view/TouchDelegate.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.graphics.Rect;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+
+/**
+ * Helper class to handle situations where you want a view to have a larger touch area than its
+ * actual view bounds. The view whose touch area is changed is called the delegate view. This
+ * class should be used by an ancestor of the delegate. To use a TouchDelegate, first create an
+ * instance that specifies the bounds that should be mapped to the delegate and the delegate
+ * view itself.
+ * <p>
+ * The ancestor should then forward all of its touch events received in its
+ * {@link android.view.View#onTouchEvent(MotionEvent)} to {@link #onTouchEvent(MotionEvent)}.
+ * </p>
+ */
+public class TouchDelegate {
+
+ /**
+ * View that should receive forwarded touch events
+ */
+ private View mDelegateView;
+
+ /**
+ * Bounds in local coordinates of the containing view that should be mapped to the delegate
+ * view. This rect is used for initial hit testing.
+ */
+ private Rect mBounds;
+
+ /**
+ * mBounds inflated to include some slop. This rect is to track whether the motion events
+ * should be considered to be be within the delegate view.
+ */
+ private Rect mSlopBounds;
+
+ /**
+ * True if the delegate had been targeted on a down event (intersected mBounds).
+ */
+ private boolean mDelegateTargeted;
+
+ /**
+ * The touchable region of the View extends above its actual extent.
+ */
+ public static final int ABOVE = 1;
+
+ /**
+ * The touchable region of the View extends below its actual extent.
+ */
+ public static final int BELOW = 2;
+
+ /**
+ * The touchable region of the View extends to the left of its
+ * actual extent.
+ */
+ public static final int TO_LEFT = 4;
+
+ /**
+ * The touchable region of the View extends to the right of its
+ * actual extent.
+ */
+ public static final int TO_RIGHT = 8;
+
+ /**
+ * Constructor
+ *
+ * @param bounds Bounds in local coordinates of the containing view that should be mapped to
+ * the delegate view
+ * @param delegateView The view that should receive motion events
+ */
+ public TouchDelegate(Rect bounds, View delegateView) {
+ mBounds = bounds;
+
+ int slop = ViewConfiguration.getTouchSlop();
+ mSlopBounds = new Rect(bounds);
+ mSlopBounds.inset(-slop, -slop);
+ mDelegateView = delegateView;
+ }
+
+ /**
+ * Will forward touch events to the delegate view if the event is within the bounds
+ * specified in the constructor.
+ *
+ * @param event The touch event to forward
+ * @return True if the event was forwarded to the delegate, false otherwise.
+ */
+ public boolean onTouchEvent(MotionEvent event) {
+ int x = (int)event.getX();
+ int y = (int)event.getY();
+ boolean sendToDelegate = false;
+ boolean hit = true;
+ boolean handled = false;
+
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ Rect bounds = mBounds;
+
+ if (bounds.contains(x, y)) {
+ mDelegateTargeted = true;
+ sendToDelegate = true;
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_MOVE:
+ sendToDelegate = mDelegateTargeted;
+ if (sendToDelegate) {
+ Rect slopBounds = mSlopBounds;
+ if (!slopBounds.contains(x, y)) {
+ hit = false;
+ }
+ }
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ sendToDelegate = mDelegateTargeted;
+ mDelegateTargeted = false;
+ break;
+ }
+ if (sendToDelegate) {
+ final View delegateView = mDelegateView;
+
+ if (hit) {
+ // Offset event coordinates to be inside the target view
+ event.setLocation(delegateView.getWidth() / 2, delegateView.getHeight() / 2);
+ } else {
+ // Offset event coordinates to be outside the target view (in case it does
+ // something like tracking pressed state)
+ int slop = ViewConfiguration.getTouchSlop();
+ event.setLocation(-(slop * 2), -(slop * 2));
+ }
+ handled = delegateView.dispatchTouchEvent(event);
+ }
+ return handled;
+ }
+}
diff --git a/core/java/android/view/VelocityTracker.java b/core/java/android/view/VelocityTracker.java
new file mode 100644
index 0000000..c80167e
--- /dev/null
+++ b/core/java/android/view/VelocityTracker.java
@@ -0,0 +1,215 @@
+/*
+ * 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.view;
+
+import android.util.Config;
+import android.util.Log;
+
+/**
+ * Helper for tracking the velocity of touch events, for implementing
+ * flinging and other such gestures. Use {@link #obtain} to retrieve a
+ * new instance of the class when you are going to begin tracking, put
+ * the motion events you receive into it with {@link #addMovement(MotionEvent)},
+ * and when you want to determine the velocity call
+ * {@link #computeCurrentVelocity(int)} and then {@link #getXVelocity()}
+ * and {@link #getXVelocity()}.
+ */
+public final class VelocityTracker {
+ static final String TAG = "VelocityTracker";
+ static final boolean DEBUG = false;
+ static final boolean localLOGV = DEBUG || Config.LOGV;
+
+ static final int NUM_PAST = 10;
+ static final int LONGEST_PAST_TIME = 200;
+
+ static final VelocityTracker[] mPool = new VelocityTracker[1];
+
+ final float mPastX[] = new float[NUM_PAST];
+ final float mPastY[] = new float[NUM_PAST];
+ final long mPastTime[] = new long[NUM_PAST];
+
+ float mYVelocity;
+ float mXVelocity;
+
+ /**
+ * Retrieve a new VelocityTracker object to watch the velocity of a
+ * motion. Be sure to call {@link #recycle} when done. You should
+ * generally only maintain an active object while tracking a movement,
+ * so that the VelocityTracker can be re-used elsewhere.
+ *
+ * @return Returns a new VelocityTracker.
+ */
+ static public VelocityTracker obtain() {
+ synchronized (mPool) {
+ VelocityTracker vt = mPool[0];
+ if (vt != null) {
+ vt.clear();
+ return vt;
+ }
+ return new VelocityTracker();
+ }
+ }
+
+ /**
+ * Return a VelocityTracker object back to be re-used by others. You must
+ * not touch the object after calling this function.
+ */
+ public void recycle() {
+ synchronized (mPool) {
+ mPool[0] = this;
+ }
+ }
+
+ private VelocityTracker() {
+ }
+
+ /**
+ * Reset the velocity tracker back to its initial state.
+ */
+ public void clear() {
+ mPastTime[0] = 0;
+ }
+
+ /**
+ * Add a user's movement to the tracker. You should call this for the
+ * initial {@link MotionEvent#ACTION_DOWN}, the following
+ * {@link MotionEvent#ACTION_MOVE} events that you receive, and the
+ * final {@link MotionEvent#ACTION_UP}. You can, however, call this
+ * for whichever events you desire.
+ *
+ * @param ev The MotionEvent you received and would like to track.
+ */
+ public void addMovement(MotionEvent ev) {
+ long time = ev.getEventTime();
+ final int N = ev.getHistorySize();
+ for (int i=0; i<N; i++) {
+ addPoint(ev.getHistoricalX(i), ev.getHistoricalY(i),
+ ev.getHistoricalEventTime(i));
+ }
+ addPoint(ev.getX(), ev.getY(), time);
+ }
+
+ private void addPoint(float x, float y, long time) {
+ int drop = -1;
+ int i;
+ if (localLOGV) Log.v(TAG, "Adding past y=" + y + " time=" + time);
+ final long[] pastTime = mPastTime;
+ for (i=0; i<NUM_PAST; i++) {
+ if (pastTime[i] == 0) {
+ break;
+ } else if (pastTime[i] < time-LONGEST_PAST_TIME) {
+ if (localLOGV) Log.v(TAG, "Dropping past too old at "
+ + i + " time=" + pastTime[i]);
+ drop = i;
+ }
+ }
+ if (localLOGV) Log.v(TAG, "Add index: " + i);
+ if (i == NUM_PAST && drop < 0) {
+ drop = 0;
+ }
+ if (drop == i) drop--;
+ final float[] pastX = mPastX;
+ final float[] pastY = mPastY;
+ if (drop >= 0) {
+ if (localLOGV) Log.v(TAG, "Dropping up to #" + drop);
+ final int start = drop+1;
+ final int count = NUM_PAST-drop-1;
+ System.arraycopy(pastX, start, pastX, 0, count);
+ System.arraycopy(pastY, start, pastY, 0, count);
+ System.arraycopy(pastTime, start, pastTime, 0, count);
+ i -= (drop+1);
+ }
+ pastX[i] = x;
+ pastY[i] = y;
+ pastTime[i] = time;
+ i++;
+ if (i < NUM_PAST) {
+ pastTime[i] = 0;
+ }
+ }
+
+ /**
+ * Compute the current velocity based on the points that have been
+ * collected. Only call this when you actually want to retrieve velocity
+ * information, as it is relatively expensive. You can then retrieve
+ * the velocity with {@link #getXVelocity()} and
+ * {@link #getYVelocity()}.
+ *
+ * @param units The units you would like the velocity in. A value of 1
+ * provides pixels per millisecond, 1000 provides pixels per second, etc.
+ */
+ public void computeCurrentVelocity(int units) {
+ final float[] pastX = mPastX;
+ final float[] pastY = mPastY;
+ final long[] pastTime = mPastTime;
+
+ // Kind-of stupid.
+ final float oldestX = pastX[0];
+ final float oldestY = pastY[0];
+ final long oldestTime = pastTime[0];
+ float accumX = 0;
+ float accumY = 0;
+ int N=0;
+ while (N < NUM_PAST) {
+ if (pastTime[N] == 0) {
+ break;
+ }
+ N++;
+ }
+ // Skip the last received event, since it is probably pretty noisy.
+ if (N > 3) N--;
+
+ for (int i=1; i < N; i++) {
+ final int dur = (int)(pastTime[i] - oldestTime);
+ if (dur == 0) continue;
+ float dist = pastX[i] - oldestX;
+ float vel = (dist/dur) * units; // pixels/frame.
+ if (accumX == 0) accumX = vel;
+ else accumX = (accumX + vel) * .5f;
+
+ dist = pastY[i] - oldestY;
+ vel = (dist/dur) * units; // pixels/frame.
+ if (accumY == 0) accumY = vel;
+ else accumY = (accumY + vel) * .5f;
+ }
+ mXVelocity = accumX;
+ mYVelocity = accumY;
+
+ if (localLOGV) Log.v(TAG, "Y velocity=" + mYVelocity +" X velocity="
+ + mXVelocity + " N=" + N);
+ }
+
+ /**
+ * Retrieve the last computed X velocity. You must first call
+ * {@link #computeCurrentVelocity(int)} before calling this function.
+ *
+ * @return The previously computed X velocity.
+ */
+ public float getXVelocity() {
+ return mXVelocity;
+ }
+
+ /**
+ * Retrieve the last computed Y velocity. You must first call
+ * {@link #computeCurrentVelocity(int)} before calling this function.
+ *
+ * @return The previously computed Y velocity.
+ */
+ public float getYVelocity() {
+ return mYVelocity;
+ }
+}
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
new file mode 100644
index 0000000..30402f8
--- /dev/null
+++ b/core/java/android/view/View.java
@@ -0,0 +1,7481 @@
+/*
+ * 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.view;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.LinearGradient;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.graphics.Shader;
+import android.graphics.Point;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.util.AttributeSet;
+import android.util.EventLog;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.animation.Animation;
+import android.widget.ScrollBarDrawable;
+
+import com.android.internal.R;
+import com.android.internal.view.menu.MenuBuilder;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * <p>
+ * The <code>View</code> class represents the basic UI building block. A view
+ * occupies a rectangular area on the screen and is responsible for drawing and
+ * event handling. <code>View</code> is the base class for <em>widgets</em>,
+ * used to create interactive graphical user interfaces.
+ * </p>
+ *
+ * <a name="Using"></a>
+ * <h3>Using Views</h3>
+ * <p>
+ * All of the views in a window are arranged in a single tree. You can add views
+ * either from code or by specifying a tree of views in one or more XML layout
+ * files. There are many specialized subclasses of views that act as controls or
+ * are capable of displaying text, images, or other content.
+ * </p>
+ * <p>
+ * Once you have created a tree of views, there are typically a few types of
+ * common operations you may wish to perform:
+ * <ul>
+ * <li><strong>Set properties:</strong> for example setting the text of a
+ * {@link android.widget.TextView}. The available properties and the methods
+ * that set them will vary among the different subclasses of views. Note that
+ * properties that are known at build time can be set in the XML layout
+ * files.</li>
+ * <li><strong>Set focus:</strong> The framework will handled moving focus in
+ * response to user input. To force focus to a specific view, call
+ * {@link #requestFocus}.</li>
+ * <li><strong>Set up listeners:</strong> Views allow clients to set listeners
+ * that will be notified when something interesting happens to the view. For
+ * example, all views will let you set a listener to be notified when the view
+ * gains or loses focus. You can register such a listener using
+ * {@link #setOnFocusChangeListener}. Other view subclasses offer more
+ * specialized listeners. For example, a Button exposes a listener to notify
+ * clients when the button is clicked.</li>
+ * <li><strong>Set visibility:</strong> You can hide or show views using
+ * {@link #setVisibility}.</li>
+ * </ul>
+ * </p>
+ * <p><em>
+ * Note: The Android framework is responsible for measuring, laying out and
+ * drawing views. You should not call methods that perform these actions on
+ * views yourself unless you are actually implementing a
+ * {@link android.view.ViewGroup}.
+ * </em></p>
+ *
+ * <a name="Lifecycle"></a>
+ * <h3>Implementing a Custom View</h3>
+ *
+ * <p>
+ * To implement a custom view, you will usually begin by providing overrides for
+ * some of the standard methods that the framework calls on all views. You do
+ * not need to override all of these methods. In fact, you can start by just
+ * overriding {@link #onDraw(android.graphics.Canvas)}.
+ * <table border="2" width="85%" align="center" cellpadding="5">
+ * <thead>
+ * <tr><th>Category</th> <th>Methods</th> <th>Description</th></tr>
+ * </thead>
+ *
+ * <tbody>
+ * <tr>
+ * <td rowspan="2">Creation</td>
+ * <td>Constructors</td>
+ * <td>There is a form of the constructor that are called when the view
+ * is created from code and a form that is called when the view is
+ * inflated from a layout file. The second form should parse and apply
+ * any attributes defined in the layout file.
+ * </td>
+ * </tr>
+ * <tr>
+ * <td><code>{@link #onFinishInflate()}</code></td>
+ * <td>Called after a view and all of its children has been inflated
+ * from XML.</td>
+ * </tr>
+ *
+ * <tr>
+ * <td rowspan="3">Layout</td>
+ * <td><code>{@link #onMeasure}</code></td>
+ * <td>Called to determine the size requirements for this view and all
+ * of its children.
+ * </td>
+ * </tr>
+ * <tr>
+ * <td><code>{@link #onLayout}</code></td>
+ * <td>Called when this view should assign a size and position to all
+ * of its children.
+ * </td>
+ * </tr>
+ * <tr>
+ * <td><code>{@link #onSizeChanged}</code></td>
+ * <td>Called when the size of this view has changed.
+ * </td>
+ * </tr>
+ *
+ * <tr>
+ * <td>Drawing</td>
+ * <td><code>{@link #onDraw}</code></td>
+ * <td>Called when the view should render its content.
+ * </td>
+ * </tr>
+ *
+ * <tr>
+ * <td rowspan="4">Event processing</td>
+ * <td><code>{@link #onKeyDown}</code></td>
+ * <td>Called when a new key event occurs.
+ * </td>
+ * </tr>
+ * <tr>
+ * <td><code>{@link #onKeyUp}</code></td>
+ * <td>Called when a key up event occurs.
+ * </td>
+ * </tr>
+ * <tr>
+ * <td><code>{@link #onTrackballEvent}</code></td>
+ * <td>Called when a trackball motion event occurs.
+ * </td>
+ * </tr>
+ * <tr>
+ * <td><code>{@link #onTouchEvent}</code></td>
+ * <td>Called when a touch screen motion event occurs.
+ * </td>
+ * </tr>
+ *
+ * <tr>
+ * <td rowspan="2">Focus</td>
+ * <td><code>{@link #onFocusChanged}</code></td>
+ * <td>Called when the view gains or loses focus.
+ * </td>
+ * </tr>
+ *
+ * <tr>
+ * <td><code>{@link #onWindowFocusChanged}</code></td>
+ * <td>Called when the window containing the view gains or loses focus.
+ * </td>
+ * </tr>
+ *
+ * <tr>
+ * <td rowspan="3">Attaching</td>
+ * <td><code>{@link #onAttachedToWindow()}</code></td>
+ * <td>Called when the view is attached to a window.
+ * </td>
+ * </tr>
+ *
+ * <tr>
+ * <td><code>{@link #onDetachedFromWindow}</code></td>
+ * <td>Called when the view is detached from its window.
+ * </td>
+ * </tr>
+ *
+ * <tr>
+ * <td><code>{@link #onWindowVisibilityChanged}</code></td>
+ * <td>Called when the visibility of the window containing the view
+ * has changed.
+ * </td>
+ * </tr>
+ * </tbody>
+ *
+ * </table>
+ * </p>
+ *
+ * <a name="IDs"></a>
+ * <h3>IDs</h3>
+ * Views may have an integer id associated with them. These ids are typically
+ * assigned in the layout XML files, and are used to find specific views within
+ * the view tree. A common pattern is to:
+ * <ul>
+ * <li>Define a Button in the layout file and assign it a unique ID.
+ * <pre>
+ * &lt;Button id="@+id/my_button"
+ * android:layout_width="wrap_content"
+ * android:layout_height="wrap_content"
+ * android:text="@string/my_button_text"/&gt;
+ * </pre></li>
+ * <li>From the onCreate method of an Activity, find the Button
+ * <pre class="prettyprint">
+ * Button myButton = (Button) findViewById(R.id.my_button);
+ * </pre></li>
+ * </ul>
+ * <p>
+ * View IDs need not be unique throughout the tree, but it is good practice to
+ * ensure that they are at least unique within the part of the tree you are
+ * searching.
+ * </p>
+ *
+ * <a name="Position"></a>
+ * <h3>Position</h3>
+ * <p>
+ * The geometry of a view is that of a rectangle. A view has a location,
+ * expressed as a pair of <em>left</em> and <em>top</em> coordinates, and
+ * two dimensions, expressed as a width and a height. The unit for location
+ * and dimensions is the pixel.
+ * </p>
+ *
+ * <p>
+ * It is possible to retrieve the location of a view by invoking the methods
+ * {@link #getLeft()} and {@link #getTop()}. The former returns the left, or X,
+ * coordinate of the rectangle representing the view. The latter returns the
+ * top, or Y, coordinate of the rectangle representing the view. These methods
+ * both return the location of the view relative to its parent. For instance,
+ * when getLeft() returns 20, that means the view is located 20 pixels to the
+ * right of the left edge of its direct parent.
+ * </p>
+ *
+ * <p>
+ * In addition, several convenience methods are offered to avoid unnecessary
+ * computations, namely {@link #getRight()} and {@link #getBottom()}.
+ * These methods return the coordinates of the right and bottom edges of the
+ * rectangle representing the view. For instance, calling {@link #getRight()}
+ * is similar to the following computation: <code>getLeft() + getWidth()</code>
+ * (see <a href="#SizePaddingMargins">Size</a> for more information about the width.)
+ * </p>
+ *
+ * <a name="SizePaddingMargins"></a>
+ * <h3>Size, padding and margins</h3>
+ * <p>
+ * The size of a view is expressed with a width and a height. A view actually
+ * possess two pairs of width and height values.
+ * </p>
+ *
+ * <p>
+ * The first pair is known as <em>measured width</em> and
+ * <em>measured height</em>. These dimensions define how big a view wants to be
+ * within its parent (see <a href="#Layout">Layout</a> for more details.) The
+ * measured dimensions can be obtained by calling {@link #getMeasuredWidth()}
+ * and {@link #getMeasuredHeight()}.
+ * </p>
+ *
+ * <p>
+ * The second pair is simply known as <em>width</em> and <em>height</em>, or
+ * sometimes <em>drawing width</em> and <em>drawing height</em>. These
+ * dimensions define the actual size of the view on screen, at drawing time and
+ * after layout. These values may, but do not have to, be different from the
+ * measured width and height. The width and height can be obtained by calling
+ * {@link #getWidth()} and {@link #getHeight()}.
+ * </p>
+ *
+ * <p>
+ * To measure its dimensions, a view takes into account its padding. The padding
+ * is expressed in pixels for the left, top, right and bottom parts of the view.
+ * Padding can be used to offset the content of the view by a specific amount of
+ * pixels. For instance, a left padding of 2 will push the view's content by
+ * 2 pixels to the right of the left edge. Padding can be set using the
+ * {@link #setPadding(int, int, int, int)} method and queried by calling
+ * {@link #getPaddingLeft()}, {@link #getPaddingTop()},
+ * {@link #getPaddingRight()} and {@link #getPaddingBottom()}.
+ * </p>
+ *
+ * <p>
+ * Even though a view can define a padding, it does not provide any support for
+ * margins. However, view groups provide such a support. Refer to
+ * {@link android.view.ViewGroup} and
+ * {@link android.view.ViewGroup.MarginLayoutParams} for further information.
+ * </p>
+ *
+ * <a name="Layout"></a>
+ * <h3>Layout</h3>
+ * <p>
+ * Layout is a two pass process: a measure pass and a layout pass. The measuring
+ * pass is implemented in {@link #measure(int, int)} and is a top-down traversal
+ * of the view tree. Each view pushes dimension specifications down the tree
+ * during the recursion. At the end of the measure pass, every view has stored
+ * its measurements. The second pass happens in
+ * {@link #layout(int,int,int,int)} and is also top-down. During
+ * this pass each parent is responsible for positioning all of its children
+ * using the sizes computed in the measure pass.
+ * </p>
+ *
+ * <p>
+ * When a view's measure() method returns, its {@link #getMeasuredWidth()} and
+ * {@link #getMeasuredHeight()} values must be set, along with those for all of
+ * that view's descendants. A view's measured width and measured height values
+ * must respect the constraints imposed by the view's parents. This guarantees
+ * that at the end of the measure pass, all parents accept all of their
+ * children's measurements. A parent view may call measure() more than once on
+ * its children. For example, the parent may measure each child once with
+ * unspecified dimensions to find out how big they want to be, then call
+ * measure() on them again with actual numbers if the sum of all the children's
+ * unconstrained sizes is too big or too small.
+ * </p>
+ *
+ * <p>
+ * The measure pass uses two classes to communicate dimensions. The
+ * {@link MeasureSpec} class is used by views to tell their parents how they
+ * want to be measured and positioned. The base LayoutParams class just
+ * describes how big the view wants to be for both width and height. For each
+ * dimension, it can specify one of:
+ * <ul>
+ * <li> an exact number
+ * <li>FILL_PARENT, which means the view wants to be as big as its parent
+ * (minus padding)
+ * <li> WRAP_CONTENT, which means that the view wants to be just big enough to
+ * enclose its content (plus padding).
+ * </ul>
+ * There are subclasses of LayoutParams for different subclasses of ViewGroup.
+ * For example, AbsoluteLayout has its own subclass of LayoutParams which adds
+ * an X and Y value.
+ * </p>
+ *
+ * <p>
+ * MeasureSpecs are used to push requirements down the tree from parent to
+ * child. A MeasureSpec can be in one of three modes:
+ * <ul>
+ * <li>UNSPECIFIED: This is used by a parent to determine the desired dimension
+ * of a child view. For example, a LinearLayout may call measure() on its child
+ * with the height set to UNSPECIFIED and a width of EXACTLY 240 to find out how
+ * tall the child view wants to be given a width of 240 pixels.
+ * <li>EXACTLY: This is used by the parent to impose an exact size on the
+ * child. The child must use this size, and guarantee that all of its
+ * descendants will fit within this size.
+ * <li>AT_MOST: This is used by the parent to impose a maximum size on the
+ * child. The child must gurantee that it and all of its descendants will fit
+ * within this size.
+ * </ul>
+ * </p>
+ *
+ * <p>
+ * To intiate a layout, call {@link #requestLayout}. This method is typically
+ * called by a view on itself when it believes that is can no longer fit within
+ * its current bounds.
+ * </p>
+ *
+ * <a name="Drawing"></a>
+ * <h3>Drawing</h3>
+ * <p>
+ * Drawing is handled by walking the tree and rendering each view that
+ * intersects the the invalid region. Because the tree is traversed in-order,
+ * this means that parents will draw before (i.e., behind) their children, with
+ * siblings drawn in the order they appear in the tree.
+ * </p>
+ *
+ * <p>
+ * The framework will not draw views that are not in the invalid region, and also
+ * will take care of drawing the views background for you.
+ * </p>
+ *
+ * <p>
+ * To force a view to draw, call {@link #invalidate()}.
+ * </p>
+ *
+ * <a name="EventHandlingThreading"></a>
+ * <h3>Event Handling and Threading</h3>
+ * <p>
+ * The basic cycle of a view is as follows:
+ * <ol>
+ * <li>An event comes in and is dispatched to the appropriate view. The view
+ * handles the event and notifies any listeners.</li>
+ * <li>If in the course of processing the event, the view's bounds may need
+ * to be changed, the view will call {@link #requestLayout()}.</li>
+ * <li>Similarly, if in the course of processing the event the view's appearance
+ * may need to be changed, the view will call {@link #invalidate()}.</li>
+ * <li>If either {@link #requestLayout()} or {@link #invalidate()} were called,
+ * the framework will take care of measuring, laying out, and drawing the tree
+ * as appropriate.</li>
+ * </ol>
+ * </p>
+ *
+ * <p><em>Note: The entire view tree is single threaded. You must always be on
+ * the UI thread when calling any method on any view.</em>
+ * If you are doing work on other threads and want to update the state of a view
+ * from that thread, you should use a {@link Handler}.
+ * </p>
+ *
+ * <a name="FocusHandling"></a>
+ * <h3>Focus Handling</h3>
+ * <p>
+ * The framework will handle routine focus movement in response to user input.
+ * This includes changing the focus as views are removed or hidden, or as new
+ * views become available. Views indicate their willingness to take focus
+ * through the {@link #isFocusable} method. To change whether a view can take
+ * focus, call {@link #setFocusable(boolean)}. When in touch mode (see notes below)
+ * views indicate whether they still would like focus via {@link #isFocusableInTouchMode}
+ * and can change this via {@link #setFocusableInTouchMode(boolean)}.
+ * </p>
+ * <p>
+ * Focus movement is based on an algorithm which finds the nearest neighbor in a
+ * given direction. In rare cases, the default algorithm may not match the
+ * intended behavior of the developer. In these situations, you can provide
+ * explicit overrides by using these XML attributes in the layout file:
+ * <pre>
+ * nextFocusDown
+ * nextFocusLeft
+ * nextFocusRight
+ * nextFocusUp
+ * </pre>
+ * </p>
+ *
+ *
+ * <p>
+ * To get a particular view to take focus, call {@link #requestFocus()}.
+ * </p>
+ *
+ * <a name="TouchMode"></a>
+ * <h3>Touch Mode</h3>
+ * <p>
+ * When a user is navigating a user interface via directional keys such as a D-pad, it is
+ * necessary to give focus to actionable items such as buttons so the user can see
+ * what will take input. If the device has touch capabilities, however, and the user
+ * begins interacting with the interface by touching it, it is no longer necessary to
+ * always highlight, or give focus to, a particular view. This motivates a mode
+ * for interaction named 'touch mode'.
+ * </p>
+ * <p>
+ * For a touch capable device, once the user touches the screen, the device
+ * will enter touch mode. From this point onward, only views for which
+ * {@link #isFocusableInTouchMode} is true will be focusable, such as text editing widgets.
+ * Other views that are touchable, like buttons, will not take focus when touched; they will
+ * only fire the on click listeners.
+ * </p>
+ * <p>
+ * Any time a user hits a directional key, such as a D-pad direction, the view device will
+ * exit touch mode, and find a view to take focus, so that the user may resume interacting
+ * with the user interface without touching the screen again.
+ * </p>
+ * <p>
+ * The touch mode state is maintained across {@link android.app.Activity}s. Call
+ * {@link #isInTouchMode} to see whether the device is currently in touch mode.
+ * </p>
+ *
+ * <a name="Scrolling"></a>
+ * <h3>Scrolling</h3>
+ * <p>
+ * The framework provides basic support for views that wish to internally
+ * scroll their content. This includes keeping track of the X and Y scroll
+ * offset as well as mechanisms for drawing scrollbars. See
+ * {@link #scrollBy(int, int)}, {@link #scrollTo(int, int)} for more details.
+ * </p>
+ *
+ * <a name="Tags"></a>
+ * <h3>Tags</h3>
+ * <p>
+ * Unlike IDs, tags are not used to identify views. Tags are essentially an
+ * extra piece of information that can be associated with a view. They are most
+ * often used as a convenience to store data related to views in the views
+ * themselves rather than by putting them in a separate structure.
+ * </p>
+ *
+ * <a name="Animation"></a>
+ * <h3>Animation</h3>
+ * <p>
+ * You can attach an {@link Animation} object to a view using
+ * {@link #setAnimation(Animation)} or
+ * {@link #startAnimation(Animation)}. The animation can alter the scale,
+ * rotation, translation and alpha of a view over time. If the animation is
+ * attached to a view that has children, the animation will affect the entire
+ * subtree rooted by that node. When an animation is started, the framework will
+ * take care of redrawing the appropriate views until the animation completes.
+ * </p>
+ *
+ * @attr ref android.R.styleable#View_fitsSystemWindows
+ * @attr ref android.R.styleable#View_nextFocusDown
+ * @attr ref android.R.styleable#View_nextFocusLeft
+ * @attr ref android.R.styleable#View_nextFocusRight
+ * @attr ref android.R.styleable#View_nextFocusUp
+ * @attr ref android.R.styleable#View_scrollX
+ * @attr ref android.R.styleable#View_scrollY
+ * @attr ref android.R.styleable#View_scrollbarTrackHorizontal
+ * @attr ref android.R.styleable#View_scrollbarThumbHorizontal
+ * @attr ref android.R.styleable#View_scrollbarSize
+ * @attr ref android.R.styleable#View_scrollbars
+ * @attr ref android.R.styleable#View_scrollbarThumbVertical
+ * @attr ref android.R.styleable#View_scrollbarTrackVertical
+ * @attr ref android.R.styleable#View_scrollbarAlwaysDrawHorizontalTrack
+ * @attr ref android.R.styleable#View_scrollbarAlwaysDrawVerticalTrack
+ *
+ * @see android.view.ViewGroup
+ */
+public class View implements Drawable.Callback, KeyEvent.Callback {
+ private static final boolean DBG = false;
+
+ /**
+ * The logging tag used by this class with android.util.Log.
+ */
+ protected static final String VIEW_LOG_TAG = "View";
+
+ /**
+ * Used to mark a View that has no ID.
+ */
+ public static final int NO_ID = -1;
+
+ /**
+ * This view does not want keystrokes. Use with TAKES_FOCUS_MASK when
+ * calling setFlags.
+ */
+ private static final int NOT_FOCUSABLE = 0x00000000;
+
+ /**
+ * This view wants keystrokes. Use with TAKES_FOCUS_MASK when calling
+ * setFlags.
+ */
+ private static final int FOCUSABLE = 0x00000001;
+
+ /**
+ * Mask for use with setFlags indicating bits used for focus.
+ */
+ private static final int FOCUSABLE_MASK = 0x00000001;
+
+ /**
+ * This view will adjust its padding to fit sytem windows (e.g. status bar)
+ */
+ private static final int FITS_SYSTEM_WINDOWS = 0x00000002;
+
+ /**
+ * This view is visible. Use with {@link #setVisibility}.
+ */
+ public static final int VISIBLE = 0x00000000;
+
+ /**
+ * This view is invisible, but it still takes up space for layout purposes.
+ * Use with {@link #setVisibility}.
+ */
+ public static final int INVISIBLE = 0x00000004;
+
+ /**
+ * This view is invisible, and it doesn't take any space for layout
+ * purposes. Use with {@link #setVisibility}.
+ */
+ public static final int GONE = 0x00000008;
+
+ /**
+ * Mask for use with setFlags indicating bits used for visibility.
+ * {@hide}
+ */
+ static final int VISIBILITY_MASK = 0x0000000C;
+
+ private static final int[] VISIBILITY_FLAGS = {VISIBLE, INVISIBLE, GONE};
+
+ /**
+ * This view is enabled. Intrepretation varies by subclass.
+ * Use with ENABLED_MASK when calling setFlags.
+ * {@hide}
+ */
+ static final int ENABLED = 0x00000000;
+
+ /**
+ * This view is disabled. Intrepretation varies by subclass.
+ * Use with ENABLED_MASK when calling setFlags.
+ * {@hide}
+ */
+ static final int DISABLED = 0x00000020;
+
+ /**
+ * Mask for use with setFlags indicating bits used for indicating whether
+ * this view is enabled
+ * {@hide}
+ */
+ static final int ENABLED_MASK = 0x00000020;
+
+ /**
+ * This view won't draw. {@link #onDraw} won't be called and further
+ * optimizations
+ * will be performed. It is okay to have this flag set and a background.
+ * Use with DRAW_MASK when calling setFlags.
+ * {@hide}
+ */
+ static final int WILL_NOT_DRAW = 0x00000080;
+
+ /**
+ * Mask for use with setFlags indicating bits used for indicating whether
+ * this view is will draw
+ * {@hide}
+ */
+ static final int DRAW_MASK = 0x00000080;
+
+ /**
+ * <p>This view doesn't show scrollbars.</p>
+ * {@hide}
+ */
+ static final int SCROLLBARS_NONE = 0x00000000;
+
+ /**
+ * <p>This view shows horizontal scrollbars.</p>
+ * {@hide}
+ */
+ static final int SCROLLBARS_HORIZONTAL = 0x00000100;
+
+ /**
+ * <p>This view shows vertical scrollbars.</p>
+ * {@hide}
+ */
+ static final int SCROLLBARS_VERTICAL = 0x00000200;
+
+ /**
+ * <p>Mask for use with setFlags indicating bits used for indicating which
+ * scrollbars are enabled.</p>
+ * {@hide}
+ */
+ static final int SCROLLBARS_MASK = 0x00000300;
+
+ // note 0x00000400 and 0x00000800 are now available for next flags...
+
+ /**
+ * <p>This view doesn't show fading edges.</p>
+ * {@hide}
+ */
+ static final int FADING_EDGE_NONE = 0x00000000;
+
+ /**
+ * <p>This view shows horizontal fading edges.</p>
+ * {@hide}
+ */
+ static final int FADING_EDGE_HORIZONTAL = 0x00001000;
+
+ /**
+ * <p>This view shows vertical fading edges.</p>
+ * {@hide}
+ */
+ static final int FADING_EDGE_VERTICAL = 0x00002000;
+
+ /**
+ * <p>Mask for use with setFlags indicating bits used for indicating which
+ * fading edges are enabled.</p>
+ * {@hide}
+ */
+ static final int FADING_EDGE_MASK = 0x00003000;
+
+ /**
+ * <p>Indicates this view can be clicked. When clickable, a View reacts
+ * to clicks by notifying the OnClickListener.<p>
+ * {@hide}
+ */
+ static final int CLICKABLE = 0x00004000;
+
+ /**
+ * <p>Indicates this view is caching its drawing into a bitmap.</p>
+ * {@hide}
+ */
+ static final int DRAWING_CACHE_ENABLED = 0x00008000;
+
+ /**
+ * <p>Indicates that no icicle should be saved for this view.<p>
+ * {@hide}
+ */
+ static final int SAVE_DISABLED = 0x000010000;
+
+ /**
+ * <p>Mask for use with setFlags indicating bits used for the saveEnabled
+ * property.</p>
+ * {@hide}
+ */
+ static final int SAVE_DISABLED_MASK = 0x000010000;
+
+ /**
+ * <p>Indicates that no drawing cache should ever be created for this view.<p>
+ * {@hide}
+ */
+ static final int WILL_NOT_CACHE_DRAWING = 0x000020000;
+
+ /**
+ * <p>Indicates this view can take / keep focus when int touch mode.</p>
+ * {@hide}
+ */
+ static final int FOCUSABLE_IN_TOUCH_MODE = 0x00040000;
+
+ /**
+ * <p>Enables low quality mode for the drawing cache.</p>
+ */
+ public static final int DRAWING_CACHE_QUALITY_LOW = 0x00080000;
+
+ /**
+ * <p>Enables high quality mode for the drawing cache.</p>
+ */
+ public static final int DRAWING_CACHE_QUALITY_HIGH = 0x00100000;
+
+ /**
+ * <p>Enables automatic quality mode for the drawing cache.</p>
+ */
+ public static final int DRAWING_CACHE_QUALITY_AUTO = 0x00000000;
+
+ private static final int[] DRAWING_CACHE_QUALITY_FLAGS = {
+ DRAWING_CACHE_QUALITY_AUTO, DRAWING_CACHE_QUALITY_LOW, DRAWING_CACHE_QUALITY_HIGH
+ };
+
+ /**
+ * <p>Mask for use with setFlags indicating bits used for the cache
+ * quality property.</p>
+ * {@hide}
+ */
+ static final int DRAWING_CACHE_QUALITY_MASK = 0x00180000;
+
+ /**
+ * <p>
+ * Indicates this view can be long clicked. When long clickable, a View
+ * reacts to long clicks by notifying the OnLongClickListener or showing a
+ * context menu.
+ * </p>
+ * {@hide}
+ */
+ static final int LONG_CLICKABLE = 0x00200000;
+
+ /**
+ * <p>Indicates that this view gets its drawable states from its direct parent
+ * and ignores its original internal states.</p>
+ *
+ * @hide
+ */
+ static final int DUPLICATE_PARENT_STATE = 0x00400000;
+
+ /**
+ * The scrollbar style to display the scrollbars inside the content area,
+ * without increasing the padding. The scrollbars will be overlaid with
+ * translucency on the view's content.
+ */
+ public static final int SCROLLBARS_INSIDE_OVERLAY = 0;
+
+ /**
+ * The scrollbar style to display the scrollbars inside the padded area,
+ * increasing the padding of the view. The scrollbars will not overlap the
+ * content area of the view.
+ */
+ public static final int SCROLLBARS_INSIDE_INSET = 0x01000000;
+
+ /**
+ * The scrollbar style to display the scrollbars at the edge of the view,
+ * without increasing the padding. The scrollbars will be overlaid with
+ * translucency.
+ */
+ public static final int SCROLLBARS_OUTSIDE_OVERLAY = 0x02000000;
+
+ /**
+ * The scrollbar style to display the scrollbars at the edge of the view,
+ * increasing the padding of the view. The scrollbars will only overlap the
+ * background, if any.
+ */
+ public static final int SCROLLBARS_OUTSIDE_INSET = 0x03000000;
+
+ /**
+ * Mask to check if the scrollbar style is overlay or inset.
+ * {@hide}
+ */
+ static final int SCROLLBARS_INSET_MASK = 0x01000000;
+
+ /**
+ * Mask to check if the scrollbar style is inside or outside.
+ * {@hide}
+ */
+ static final int SCROLLBARS_OUTSIDE_MASK = 0x02000000;
+
+ /**
+ * Mask for scrollbar style.
+ * {@hide}
+ */
+ static final int SCROLLBARS_STYLE_MASK = 0x03000000;
+
+ /**
+ * View flag indicating that the screen should remain on while the
+ * window containing this view is visible to the user. This effectively
+ * takes care of automatically setting the WindowManager's
+ * {@link WindowManager.LayoutParams#FLAG_KEEP_SCREEN_ON}.
+ */
+ public static final int KEEP_SCREEN_ON = 0x04000000;
+
+ /**
+ * View flag indicating whether this view should have sound effects enabled
+ * for events such as clicking and touching.
+ */
+ public static final int SOUND_EFFECTS_ENABLED = 0x08000000;
+
+ /**
+ * Use with {@link #focusSearch}. Move focus to the previous selectable
+ * item.
+ */
+ public static final int FOCUS_BACKWARD = 0x00000001;
+
+ /**
+ * Use with {@link #focusSearch}. Move focus to the next selectable
+ * item.
+ */
+ public static final int FOCUS_FORWARD = 0x00000002;
+
+ /**
+ * Use with {@link #focusSearch}. Move focus to the left.
+ */
+ public static final int FOCUS_LEFT = 0x00000011;
+
+ /**
+ * Use with {@link #focusSearch}. Move focus up.
+ */
+ public static final int FOCUS_UP = 0x00000021;
+
+ /**
+ * Use with {@link #focusSearch}. Move focus to the right.
+ */
+ public static final int FOCUS_RIGHT = 0x00000042;
+
+ /**
+ * Use with {@link #focusSearch}. Move focus down.
+ */
+ public static final int FOCUS_DOWN = 0x00000082;
+
+ /**
+ * Base View state sets
+ */
+ // Singles
+ /**
+ * Indicates the view has no states set. States are used with
+ * {@link android.graphics.drawable.Drawable} to change the drawing of the
+ * view depending on its state.
+ *
+ * @see android.graphics.drawable.Drawable
+ * @see #getDrawableState()
+ */
+ protected static final int[] EMPTY_STATE_SET = {};
+ /**
+ * Indicates the view is enabled. States are used with
+ * {@link android.graphics.drawable.Drawable} to change the drawing of the
+ * view depending on its state.
+ *
+ * @see android.graphics.drawable.Drawable
+ * @see #getDrawableState()
+ */
+ protected static final int[] ENABLED_STATE_SET = {R.attr.state_enabled};
+ /**
+ * Indicates the view is focused. States are used with
+ * {@link android.graphics.drawable.Drawable} to change the drawing of the
+ * view depending on its state.
+ *
+ * @see android.graphics.drawable.Drawable
+ * @see #getDrawableState()
+ */
+ protected static final int[] FOCUSED_STATE_SET = {R.attr.state_focused};
+ /**
+ * Indicates the view is selected. States are used with
+ * {@link android.graphics.drawable.Drawable} to change the drawing of the
+ * view depending on its state.
+ *
+ * @see android.graphics.drawable.Drawable
+ * @see #getDrawableState()
+ */
+ protected static final int[] SELECTED_STATE_SET = {R.attr.state_selected};
+ /**
+ * Indicates the view is pressed. States are used with
+ * {@link android.graphics.drawable.Drawable} to change the drawing of the
+ * view depending on its state.
+ *
+ * @see android.graphics.drawable.Drawable
+ * @see #getDrawableState()
+ * @hide
+ */
+ protected static final int[] PRESSED_STATE_SET = {R.attr.state_pressed};
+ /**
+ * Indicates the view's window has focus. States are used with
+ * {@link android.graphics.drawable.Drawable} to change the drawing of the
+ * view depending on its state.
+ *
+ * @see android.graphics.drawable.Drawable
+ * @see #getDrawableState()
+ */
+ protected static final int[] WINDOW_FOCUSED_STATE_SET =
+ {R.attr.state_window_focused};
+ // Doubles
+ /**
+ * Indicates the view is enabled and has the focus.
+ *
+ * @see #ENABLED_STATE_SET
+ * @see #FOCUSED_STATE_SET
+ */
+ protected static final int[] ENABLED_FOCUSED_STATE_SET =
+ stateSetUnion(ENABLED_STATE_SET, FOCUSED_STATE_SET);
+ /**
+ * Indicates the view is enabled and selected.
+ *
+ * @see #ENABLED_STATE_SET
+ * @see #SELECTED_STATE_SET
+ */
+ protected static final int[] ENABLED_SELECTED_STATE_SET =
+ stateSetUnion(ENABLED_STATE_SET, SELECTED_STATE_SET);
+ /**
+ * Indicates the view is enabled and that its window has focus.
+ *
+ * @see #ENABLED_STATE_SET
+ * @see #WINDOW_FOCUSED_STATE_SET
+ */
+ protected static final int[] ENABLED_WINDOW_FOCUSED_STATE_SET =
+ stateSetUnion(ENABLED_STATE_SET, WINDOW_FOCUSED_STATE_SET);
+ /**
+ * Indicates the view is focused and selected.
+ *
+ * @see #FOCUSED_STATE_SET
+ * @see #SELECTED_STATE_SET
+ */
+ protected static final int[] FOCUSED_SELECTED_STATE_SET =
+ stateSetUnion(FOCUSED_STATE_SET, SELECTED_STATE_SET);
+ /**
+ * Indicates the view has the focus and that its window has the focus.
+ *
+ * @see #FOCUSED_STATE_SET
+ * @see #WINDOW_FOCUSED_STATE_SET
+ */
+ protected static final int[] FOCUSED_WINDOW_FOCUSED_STATE_SET =
+ stateSetUnion(FOCUSED_STATE_SET, WINDOW_FOCUSED_STATE_SET);
+ /**
+ * Indicates the view is selected and that its window has the focus.
+ *
+ * @see #SELECTED_STATE_SET
+ * @see #WINDOW_FOCUSED_STATE_SET
+ */
+ protected static final int[] SELECTED_WINDOW_FOCUSED_STATE_SET =
+ stateSetUnion(SELECTED_STATE_SET, WINDOW_FOCUSED_STATE_SET);
+ // Triples
+ /**
+ * Indicates the view is enabled, focused and selected.
+ *
+ * @see #ENABLED_STATE_SET
+ * @see #FOCUSED_STATE_SET
+ * @see #SELECTED_STATE_SET
+ */
+ protected static final int[] ENABLED_FOCUSED_SELECTED_STATE_SET =
+ stateSetUnion(ENABLED_FOCUSED_STATE_SET, SELECTED_STATE_SET);
+ /**
+ * Indicates the view is enabled, focused and its window has the focus.
+ *
+ * @see #ENABLED_STATE_SET
+ * @see #FOCUSED_STATE_SET
+ * @see #WINDOW_FOCUSED_STATE_SET
+ */
+ protected static final int[] ENABLED_FOCUSED_WINDOW_FOCUSED_STATE_SET =
+ stateSetUnion(ENABLED_FOCUSED_STATE_SET, WINDOW_FOCUSED_STATE_SET);
+ /**
+ * Indicates the view is enabled, selected and its window has the focus.
+ *
+ * @see #ENABLED_STATE_SET
+ * @see #SELECTED_STATE_SET
+ * @see #WINDOW_FOCUSED_STATE_SET
+ */
+ protected static final int[] ENABLED_SELECTED_WINDOW_FOCUSED_STATE_SET =
+ stateSetUnion(ENABLED_SELECTED_STATE_SET, WINDOW_FOCUSED_STATE_SET);
+ /**
+ * Indicates the view is focused, selected and its window has the focus.
+ *
+ * @see #FOCUSED_STATE_SET
+ * @see #SELECTED_STATE_SET
+ * @see #WINDOW_FOCUSED_STATE_SET
+ */
+ protected static final int[] FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET =
+ stateSetUnion(FOCUSED_SELECTED_STATE_SET, WINDOW_FOCUSED_STATE_SET);
+ /**
+ * Indicates the view is enabled, focused, selected and its window
+ * has the focus.
+ *
+ * @see #ENABLED_STATE_SET
+ * @see #FOCUSED_STATE_SET
+ * @see #SELECTED_STATE_SET
+ * @see #WINDOW_FOCUSED_STATE_SET
+ */
+ protected static final int[] ENABLED_FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET =
+ stateSetUnion(ENABLED_FOCUSED_SELECTED_STATE_SET,
+ WINDOW_FOCUSED_STATE_SET);
+
+ /**
+ * Indicates the view is pressed and its window has the focus.
+ *
+ * @see #PRESSED_STATE_SET
+ * @see #WINDOW_FOCUSED_STATE_SET
+ */
+ protected static final int[] PRESSED_WINDOW_FOCUSED_STATE_SET =
+ stateSetUnion(PRESSED_STATE_SET, WINDOW_FOCUSED_STATE_SET);
+
+ /**
+ * Indicates the view is pressed and selected.
+ *
+ * @see #PRESSED_STATE_SET
+ * @see #SELECTED_STATE_SET
+ */
+ protected static final int[] PRESSED_SELECTED_STATE_SET =
+ stateSetUnion(PRESSED_STATE_SET, SELECTED_STATE_SET);
+
+ /**
+ * Indicates the view is pressed, selected and its window has the focus.
+ *
+ * @see #PRESSED_STATE_SET
+ * @see #SELECTED_STATE_SET
+ * @see #WINDOW_FOCUSED_STATE_SET
+ */
+ protected static final int[] PRESSED_SELECTED_WINDOW_FOCUSED_STATE_SET =
+ stateSetUnion(PRESSED_SELECTED_STATE_SET, WINDOW_FOCUSED_STATE_SET);
+
+ /**
+ * Indicates the view is pressed and focused.
+ *
+ * @see #PRESSED_STATE_SET
+ * @see #FOCUSED_STATE_SET
+ */
+ protected static final int[] PRESSED_FOCUSED_STATE_SET =
+ stateSetUnion(PRESSED_STATE_SET, FOCUSED_STATE_SET);
+
+ /**
+ * Indicates the view is pressed, focused and its window has the focus.
+ *
+ * @see #PRESSED_STATE_SET
+ * @see #FOCUSED_STATE_SET
+ * @see #WINDOW_FOCUSED_STATE_SET
+ */
+ protected static final int[] PRESSED_FOCUSED_WINDOW_FOCUSED_STATE_SET =
+ stateSetUnion(PRESSED_FOCUSED_STATE_SET, WINDOW_FOCUSED_STATE_SET);
+
+ /**
+ * Indicates the view is pressed, focused and selected.
+ *
+ * @see #PRESSED_STATE_SET
+ * @see #SELECTED_STATE_SET
+ * @see #FOCUSED_STATE_SET
+ */
+ protected static final int[] PRESSED_FOCUSED_SELECTED_STATE_SET =
+ stateSetUnion(PRESSED_FOCUSED_STATE_SET, SELECTED_STATE_SET);
+
+ /**
+ * Indicates the view is pressed, focused, selected and its window has the focus.
+ *
+ * @see #PRESSED_STATE_SET
+ * @see #FOCUSED_STATE_SET
+ * @see #SELECTED_STATE_SET
+ * @see #WINDOW_FOCUSED_STATE_SET
+ */
+ protected static final int[] PRESSED_FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET =
+ stateSetUnion(PRESSED_FOCUSED_SELECTED_STATE_SET, WINDOW_FOCUSED_STATE_SET);
+
+ /**
+ * Indicates the view is pressed and enabled.
+ *
+ * @see #PRESSED_STATE_SET
+ * @see #ENABLED_STATE_SET
+ */
+ protected static final int[] PRESSED_ENABLED_STATE_SET =
+ stateSetUnion(PRESSED_STATE_SET, ENABLED_STATE_SET);
+
+ /**
+ * Indicates the view is pressed, enabled and its window has the focus.
+ *
+ * @see #PRESSED_STATE_SET
+ * @see #ENABLED_STATE_SET
+ * @see #WINDOW_FOCUSED_STATE_SET
+ */
+ protected static final int[] PRESSED_ENABLED_WINDOW_FOCUSED_STATE_SET =
+ stateSetUnion(PRESSED_ENABLED_STATE_SET, WINDOW_FOCUSED_STATE_SET);
+
+ /**
+ * Indicates the view is pressed, enabled and selected.
+ *
+ * @see #PRESSED_STATE_SET
+ * @see #ENABLED_STATE_SET
+ * @see #SELECTED_STATE_SET
+ */
+ protected static final int[] PRESSED_ENABLED_SELECTED_STATE_SET =
+ stateSetUnion(PRESSED_ENABLED_STATE_SET, SELECTED_STATE_SET);
+
+ /**
+ * Indicates the view is pressed, enabled, selected and its window has the
+ * focus.
+ *
+ * @see #PRESSED_STATE_SET
+ * @see #ENABLED_STATE_SET
+ * @see #SELECTED_STATE_SET
+ * @see #WINDOW_FOCUSED_STATE_SET
+ */
+ protected static final int[] PRESSED_ENABLED_SELECTED_WINDOW_FOCUSED_STATE_SET =
+ stateSetUnion(PRESSED_ENABLED_SELECTED_STATE_SET, WINDOW_FOCUSED_STATE_SET);
+
+ /**
+ * Indicates the view is pressed, enabled and focused.
+ *
+ * @see #PRESSED_STATE_SET
+ * @see #ENABLED_STATE_SET
+ * @see #FOCUSED_STATE_SET
+ */
+ protected static final int[] PRESSED_ENABLED_FOCUSED_STATE_SET =
+ stateSetUnion(PRESSED_ENABLED_STATE_SET, FOCUSED_STATE_SET);
+
+ /**
+ * Indicates the view is pressed, enabled, focused and its window has the
+ * focus.
+ *
+ * @see #PRESSED_STATE_SET
+ * @see #ENABLED_STATE_SET
+ * @see #FOCUSED_STATE_SET
+ * @see #WINDOW_FOCUSED_STATE_SET
+ */
+ protected static final int[] PRESSED_ENABLED_FOCUSED_WINDOW_FOCUSED_STATE_SET =
+ stateSetUnion(PRESSED_ENABLED_FOCUSED_STATE_SET, WINDOW_FOCUSED_STATE_SET);
+
+ /**
+ * Indicates the view is pressed, enabled, focused and selected.
+ *
+ * @see #PRESSED_STATE_SET
+ * @see #ENABLED_STATE_SET
+ * @see #SELECTED_STATE_SET
+ * @see #FOCUSED_STATE_SET
+ */
+ protected static final int[] PRESSED_ENABLED_FOCUSED_SELECTED_STATE_SET =
+ stateSetUnion(PRESSED_ENABLED_FOCUSED_STATE_SET, SELECTED_STATE_SET);
+
+ /**
+ * Indicates the view is pressed, enabled, focused, selected and its window
+ * has the focus.
+ *
+ * @see #PRESSED_STATE_SET
+ * @see #ENABLED_STATE_SET
+ * @see #SELECTED_STATE_SET
+ * @see #FOCUSED_STATE_SET
+ * @see #WINDOW_FOCUSED_STATE_SET
+ */
+ protected static final int[] PRESSED_ENABLED_FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET =
+ stateSetUnion(PRESSED_ENABLED_FOCUSED_SELECTED_STATE_SET, WINDOW_FOCUSED_STATE_SET);
+
+ /**
+ * The order here is very important to {@link #getDrawableState()}
+ */
+ private static final int[][] VIEW_STATE_SETS = {
+ EMPTY_STATE_SET, // 0 0 0 0 0
+ WINDOW_FOCUSED_STATE_SET, // 0 0 0 0 1
+ SELECTED_STATE_SET, // 0 0 0 1 0
+ SELECTED_WINDOW_FOCUSED_STATE_SET, // 0 0 0 1 1
+ FOCUSED_STATE_SET, // 0 0 1 0 0
+ FOCUSED_WINDOW_FOCUSED_STATE_SET, // 0 0 1 0 1
+ FOCUSED_SELECTED_STATE_SET, // 0 0 1 1 0
+ FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET, // 0 0 1 1 1
+ ENABLED_STATE_SET, // 0 1 0 0 0
+ ENABLED_WINDOW_FOCUSED_STATE_SET, // 0 1 0 0 1
+ ENABLED_SELECTED_STATE_SET, // 0 1 0 1 0
+ ENABLED_SELECTED_WINDOW_FOCUSED_STATE_SET, // 0 1 0 1 1
+ ENABLED_FOCUSED_STATE_SET, // 0 1 1 0 0
+ ENABLED_FOCUSED_WINDOW_FOCUSED_STATE_SET, // 0 1 1 0 1
+ ENABLED_FOCUSED_SELECTED_STATE_SET, // 0 1 1 1 0
+ ENABLED_FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET, // 0 1 1 1 1
+ PRESSED_STATE_SET, // 1 0 0 0 0
+ PRESSED_WINDOW_FOCUSED_STATE_SET, // 1 0 0 0 1
+ PRESSED_SELECTED_STATE_SET, // 1 0 0 1 0
+ PRESSED_SELECTED_WINDOW_FOCUSED_STATE_SET, // 1 0 0 1 1
+ PRESSED_FOCUSED_STATE_SET, // 1 0 1 0 0
+ PRESSED_FOCUSED_WINDOW_FOCUSED_STATE_SET, // 1 0 1 0 1
+ PRESSED_FOCUSED_SELECTED_STATE_SET, // 1 0 1 1 0
+ PRESSED_FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET, // 1 0 1 1 1
+ PRESSED_ENABLED_STATE_SET, // 1 1 0 0 0
+ PRESSED_ENABLED_WINDOW_FOCUSED_STATE_SET, // 1 1 0 0 1
+ PRESSED_ENABLED_SELECTED_STATE_SET, // 1 1 0 1 0
+ PRESSED_ENABLED_SELECTED_WINDOW_FOCUSED_STATE_SET, // 1 1 0 1 1
+ PRESSED_ENABLED_FOCUSED_STATE_SET, // 1 1 1 0 0
+ PRESSED_ENABLED_FOCUSED_WINDOW_FOCUSED_STATE_SET, // 1 1 1 0 1
+ PRESSED_ENABLED_FOCUSED_SELECTED_STATE_SET, // 1 1 1 1 0
+ PRESSED_ENABLED_FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET, // 1 1 1 1 1
+ };
+
+ /**
+ * Used by views that contain lists of items. This state indicates that
+ * the view is showing the last item.
+ * @hide
+ */
+ protected static final int[] LAST_STATE_SET = {R.attr.state_last};
+ /**
+ * Used by views that contain lists of items. This state indicates that
+ * the view is showing the first item.
+ * @hide
+ */
+ protected static final int[] FIRST_STATE_SET = {R.attr.state_first};
+ /**
+ * Used by views that contain lists of items. This state indicates that
+ * the view is showing the middle item.
+ * @hide
+ */
+ protected static final int[] MIDDLE_STATE_SET = {R.attr.state_middle};
+ /**
+ * Used by views that contain lists of items. This state indicates that
+ * the view is showing only one item.
+ * @hide
+ */
+ protected static final int[] SINGLE_STATE_SET = {R.attr.state_single};
+ /**
+ * Used by views that contain lists of items. This state indicates that
+ * the view is pressed and showing the last item.
+ * @hide
+ */
+ protected static final int[] PRESSED_LAST_STATE_SET = {R.attr.state_last, R.attr.state_pressed};
+ /**
+ * Used by views that contain lists of items. This state indicates that
+ * the view is pressed and showing the first item.
+ * @hide
+ */
+ protected static final int[] PRESSED_FIRST_STATE_SET = {R.attr.state_first, R.attr.state_pressed};
+ /**
+ * Used by views that contain lists of items. This state indicates that
+ * the view is pressed and showing the middle item.
+ * @hide
+ */
+ protected static final int[] PRESSED_MIDDLE_STATE_SET = {R.attr.state_middle, R.attr.state_pressed};
+ /**
+ * Used by views that contain lists of items. This state indicates that
+ * the view is pressed and showing only one item.
+ * @hide
+ */
+ protected static final int[] PRESSED_SINGLE_STATE_SET = {R.attr.state_single, R.attr.state_pressed};
+
+ /**
+ * The animation currently associated with this view.
+ * @hide
+ */
+ protected Animation mCurrentAnimation = null;
+
+ /**
+ * Width as measured during measure pass.
+ * {@hide}
+ */
+ @ViewDebug.ExportedProperty
+ protected int mMeasuredWidth;
+
+ /**
+ * Height as measured during measure pass.
+ * {@hide}
+ */
+ @ViewDebug.ExportedProperty
+ protected int mMeasuredHeight;
+
+ /**
+ * Used to store a pair of coordinates, for instance returned values
+ * returned by {@link #getLocationInWindow(int[])}.
+ *
+ * This field should be made private, so it is hidden from the SDK.
+ * {@hide}
+ */
+ protected final int[] mLocation = new int[2];
+
+ /**
+ * The view's identifier.
+ * {@hide}
+ *
+ * @see #setId(int)
+ * @see #getId()
+ */
+ @ViewDebug.ExportedProperty(resolveId = true)
+ int mID = NO_ID;
+
+ /**
+ * The view's tag.
+ * {@hide}
+ *
+ * @see #setTag(Object)
+ * @see #getTag()
+ */
+ protected Object mTag;
+
+ // for mPrivateFlags:
+ /** {@hide} */
+ static final int WANTS_FOCUS = 0x00000001;
+ /** {@hide} */
+ static final int FOCUSED = 0x00000002;
+ /** {@hide} */
+ static final int SELECTED = 0x00000004;
+ /** {@hide} */
+ static final int IS_ROOT_NAMESPACE = 0x00000008;
+ /** {@hide} */
+ static final int HAS_BOUNDS = 0x00000010;
+ /** {@hide} */
+ static final int DRAWN = 0x00000020;
+ /** {@hide} */
+ static final int SKIP_DRAW = 0x00000080;
+ /** {@hide} */
+ static final int ONLY_DRAWS_BACKGROUND = 0x00000100;
+ /** {@hide} */
+ static final int REQUEST_TRANSPARENT_REGIONS = 0x00000200;
+ /** {@hide} */
+ static final int DRAWABLE_STATE_DIRTY = 0x00000400;
+ /** {@hide} */
+ static final int MEASURED_DIMENSION_SET = 0x00000800;
+ /** {@hide} */
+ static final int FORCE_LAYOUT = 0x00001000;
+
+ private static final int LAYOUT_REQUIRED = 0x00002000;
+
+ private static final int PRESSED = 0x00004000;
+
+ /** {@hide} */
+ static final int DRAWING_CACHE_VALID = 0x00008000;
+ /**
+ * Flag used to indicate that this view should be drawn once more (and only once
+ * more) after its animation has completed.
+ * {@hide}
+ */
+ static final int ANIMATION_STARTED = 0x00010000;
+
+ private static final int SAVE_STATE_CALLED = 0x00020000;
+
+ /**
+ * Indicates that the View returned true when onSetAlpha() was called and that
+ * the alpha must be restored.
+ * {@hide}
+ */
+ static final int ALPHA_SET = 0x00040000;
+
+ // Note: flag 0x00000040 is available
+
+ /**
+ * The parent this view is attached to.
+ * {@hide}
+ *
+ * @see #getParent()
+ */
+ protected ViewParent mParent;
+
+ /**
+ * {@hide}
+ */
+ AttachInfo mAttachInfo;
+
+ /**
+ * {@hide}
+ */
+ int mPrivateFlags;
+
+ /**
+ * Count of how many windows this view has been attached to.
+ */
+ int mWindowAttachCount;
+
+ /**
+ * The layout parameters associated with this view and used by the parent
+ * {@link android.view.ViewGroup} to determine how this view should be
+ * laid out.
+ * {@hide}
+ */
+ protected ViewGroup.LayoutParams mLayoutParams;
+
+ /**
+ * The view flags hold various views states.
+ * {@hide}
+ */
+ int mViewFlags;
+
+ /**
+ * The distance in pixels from the left edge of this view's parent
+ * to the left edge of this view.
+ * {@hide}
+ */
+ @ViewDebug.ExportedProperty
+ protected int mLeft;
+ /**
+ * The distance in pixels from the left edge of this view's parent
+ * to the right edge of this view.
+ * {@hide}
+ */
+ @ViewDebug.ExportedProperty
+ protected int mRight;
+ /**
+ * The distance in pixels from the top edge of this view's parent
+ * to the top edge of this view.
+ * {@hide}
+ */
+ @ViewDebug.ExportedProperty
+ protected int mTop;
+ /**
+ * The distance in pixels from the top edge of this view's parent
+ * to the bottom edge of this view.
+ * {@hide}
+ */
+ @ViewDebug.ExportedProperty
+ protected int mBottom;
+
+ /**
+ * The offset, in pixels, by which the content of this view is scrolled
+ * horizontally.
+ * {@hide}
+ */
+ @ViewDebug.ExportedProperty
+ protected int mScrollX;
+ /**
+ * The offset, in pixels, by which the content of this view is scrolled
+ * vertically.
+ * {@hide}
+ */
+ @ViewDebug.ExportedProperty
+ protected int mScrollY;
+
+ /**
+ * The left padding in pixels, that is the distance in pixels between the
+ * left edge of this view and the left edge of its content.
+ * {@hide}
+ */
+ @ViewDebug.ExportedProperty
+ protected int mPaddingLeft;
+ /**
+ * The right padding in pixels, that is the distance in pixels between the
+ * right edge of this view and the right edge of its content.
+ * {@hide}
+ */
+ @ViewDebug.ExportedProperty
+ protected int mPaddingRight;
+ /**
+ * The top padding in pixels, that is the distance in pixels between the
+ * top edge of this view and the top edge of its content.
+ * {@hide}
+ */
+ @ViewDebug.ExportedProperty
+ protected int mPaddingTop;
+ /**
+ * The bottom padding in pixels, that is the distance in pixels between the
+ * bottom edge of this view and the bottom edge of its content.
+ * {@hide}
+ */
+ @ViewDebug.ExportedProperty
+ protected int mPaddingBottom;
+
+ /**
+ * Cache the paddingRight set by the user to append to the scrollbar's size.
+ */
+ @ViewDebug.ExportedProperty
+ int mUserPaddingRight;
+
+ /**
+ * Cache the paddingBottom set by the user to append to the scrollbar's size.
+ */
+ @ViewDebug.ExportedProperty
+ int mUserPaddingBottom;
+
+ private int mOldWidthMeasureSpec = Integer.MIN_VALUE;
+ private int mOldHeightMeasureSpec = Integer.MIN_VALUE;
+
+ private Resources mResources = null;
+
+ private Drawable mBGDrawable;
+
+ private int mBackgroundResource;
+ private boolean mBackgroundSizeChanged;
+
+ /**
+ * Listener used to dispatch focus change events.
+ * This field should be made private, so it is hidden from the SDK.
+ * {@hide}
+ */
+ protected OnFocusChangeListener mOnFocusChangeListener;
+
+ /**
+ * Listener used to dispatch click events.
+ * This field should be made private, so it is hidden from the SDK.
+ * {@hide}
+ */
+ protected OnClickListener mOnClickListener;
+
+ /**
+ * Listener used to dispatch long click events.
+ * This field should be made private, so it is hidden from the SDK.
+ * {@hide}
+ */
+ protected OnLongClickListener mOnLongClickListener;
+
+ /**
+ * Listener used to build the context menu.
+ * This field should be made private, so it is hidden from the SDK.
+ * {@hide}
+ */
+ protected OnCreateContextMenuListener mOnCreateContextMenuListener;
+
+ private OnKeyListener mOnKeyListener;
+
+ private OnTouchListener mOnTouchListener;
+
+ /**
+ * The application environment this view lives in.
+ * This field should be made private, so it is hidden from the SDK.
+ * {@hide}
+ */
+ protected Context mContext;
+
+ private ScrollabilityCache mScrollCache;
+
+ private int[] mDrawableState = null;
+
+ private Bitmap mDrawingCache;
+
+ /**
+ * Used for local (within a stackframe) calls that need a rect temporarily
+ */
+ private final Rect mTempRect = new Rect();
+
+ /**
+ * When this view has focus and the next focus is {@link #FOCUS_LEFT},
+ * the user may specify which view to go to next.
+ */
+ private int mNextFocusLeftId = View.NO_ID;
+
+ /**
+ * When this view has focus and the next focus is {@link #FOCUS_RIGHT},
+ * the user may specify which view to go to next.
+ */
+ private int mNextFocusRightId = View.NO_ID;
+
+ /**
+ * When this view has focus and the next focus is {@link #FOCUS_UP},
+ * the user may specify which view to go to next.
+ */
+ private int mNextFocusUpId = View.NO_ID;
+
+ /**
+ * When this view has focus and the next focus is {@link #FOCUS_DOWN},
+ * the user may specify which view to go to next.
+ */
+ private int mNextFocusDownId = View.NO_ID;
+
+ private CheckForLongPress mPendingCheckForLongPress;
+
+ /**
+ * Whether the long press's action has been invoked. The tap's action is invoked on the
+ * up event while a long press is invoked as soon as the long press duration is reached, so
+ * a long press could be performed before the tap is checked, in which case the tap's action
+ * should not be invoked.
+ */
+ private boolean mHasPerformedLongPress;
+
+ /**
+ * The minimum height of the view. We'll try our best to have the height
+ * of this view to at least this amount.
+ */
+ private int mMinHeight;
+
+ /**
+ * The minimum width of the view. We'll try our best to have the width
+ * of this view to at least this amount.
+ */
+ private int mMinWidth;
+
+ /**
+ * The delegate to handle touch events that are physically in this view
+ * but should be handled by another view.
+ */
+ private TouchDelegate mTouchDelegate = null;
+
+ /**
+ * Solid color to use as a background when creating the drawing cache. Enables
+ * the cache to use 16 bit bitmaps instead of 32 bit.
+ */
+ private int mDrawingCacheBackgroundColor = 0;
+
+ /**
+ * Special tree observer used when mAttachInfo is null.
+ */
+ private ViewTreeObserver mFloatingTreeObserver;
+
+ // Used for debug only
+ static long sInstanceCount = 0;
+
+ /**
+ * Simple constructor to use when creating a view from code.
+ *
+ * @param context The Context the view is running in, through which it can
+ * access the current theme, resources, etc.
+ */
+ public View(Context context) {
+ mContext = context;
+ mResources = context != null ? context.getResources() : null;
+ ++sInstanceCount;
+ }
+
+ /**
+ * Constructor that is called when inflating a view from XML. This is called
+ * when a view is being constructed from an XML file, supplying attributes
+ * that were specified in the XML file. This version uses a default style of
+ * 0, so the only attribute values applied are those in the Context's Theme
+ * and the given AttributeSet.
+ *
+ * <p>
+ * The method onFinishInflate() will be called after all children have been
+ * added.
+ *
+ * @param context The Context the view is running in, through which it can
+ * access the current theme, resources, etc.
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ * @see #View(Context, AttributeSet, int)
+ */
+ public View(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ /**
+ * Perform inflation from XML and apply a class-specific base style. This
+ * constructor of View allows subclasses to use their own base style when
+ * they are inflating. For example, a Button class's constructor would call
+ * this version of the super class constructor and supply
+ * <code>R.attr.buttonStyle</code> for <var>defStyle</var>; this allows
+ * the theme's button style to modify all of the base view attributes (in
+ * particular its background) as well as the Button class's attributes.
+ *
+ * @param context The Context the view is running in, through which it can
+ * access the current theme, resources, etc.
+ * @param attrs The attributes of the XML tag that is inflating the view.
+ * @param defStyle The default style to apply to this view. If 0, no style
+ * will be applied (beyond what is included in the theme). This may
+ * either be an attribute resource, whose value will be retrieved
+ * from the current theme, or an explicit style resource.
+ * @see #View(Context, AttributeSet)
+ */
+ public View(Context context, AttributeSet attrs, int defStyle) {
+ this(context);
+
+ TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.View,
+ defStyle, 0);
+
+ Drawable background = null;
+
+ int leftPadding = -1;
+ int topPadding = -1;
+ int rightPadding = -1;
+ int bottomPadding = -1;
+
+ int padding = -1;
+
+ int viewFlagValues = 0;
+ int viewFlagMasks = 0;
+
+ int x = 0;
+ int y = 0;
+
+ int scrollbarStyle = SCROLLBARS_INSIDE_OVERLAY;
+
+ viewFlagValues |= SOUND_EFFECTS_ENABLED;
+ viewFlagMasks |= SOUND_EFFECTS_ENABLED;
+
+ final int N = a.getIndexCount();
+ for (int i = 0; i < N; i++) {
+ int attr = a.getIndex(i);
+ switch (attr) {
+ case com.android.internal.R.styleable.View_background:
+ background = a.getDrawable(attr);
+ break;
+ case com.android.internal.R.styleable.View_padding:
+ padding = a.getDimensionPixelSize(attr, -1);
+ break;
+ case com.android.internal.R.styleable.View_paddingLeft:
+ leftPadding = a.getDimensionPixelSize(attr, -1);
+ break;
+ case com.android.internal.R.styleable.View_paddingTop:
+ topPadding = a.getDimensionPixelSize(attr, -1);
+ break;
+ case com.android.internal.R.styleable.View_paddingRight:
+ rightPadding = a.getDimensionPixelSize(attr, -1);
+ break;
+ case com.android.internal.R.styleable.View_paddingBottom:
+ bottomPadding = a.getDimensionPixelSize(attr, -1);
+ break;
+ case com.android.internal.R.styleable.View_scrollX:
+ x = a.getDimensionPixelOffset(attr, 0);
+ break;
+ case com.android.internal.R.styleable.View_scrollY:
+ y = a.getDimensionPixelOffset(attr, 0);
+ break;
+ case com.android.internal.R.styleable.View_id:
+ mID = a.getResourceId(attr, NO_ID);
+ break;
+ case com.android.internal.R.styleable.View_tag:
+ mTag = a.getText(attr);
+ break;
+ case com.android.internal.R.styleable.View_fitsSystemWindows:
+ if (a.getBoolean(attr, false)) {
+ viewFlagValues |= FITS_SYSTEM_WINDOWS;
+ viewFlagMasks |= FITS_SYSTEM_WINDOWS;
+ }
+ break;
+ case com.android.internal.R.styleable.View_focusable:
+ if (a.getBoolean(attr, false)) {
+ viewFlagValues |= FOCUSABLE;
+ viewFlagMasks |= FOCUSABLE_MASK;
+ }
+ break;
+ case com.android.internal.R.styleable.View_focusableInTouchMode:
+ if (a.getBoolean(attr, false)) {
+ viewFlagValues |= FOCUSABLE_IN_TOUCH_MODE | FOCUSABLE;
+ viewFlagMasks |= FOCUSABLE_IN_TOUCH_MODE | FOCUSABLE_MASK;
+ }
+ break;
+ case com.android.internal.R.styleable.View_clickable:
+ if (a.getBoolean(attr, false)) {
+ viewFlagValues |= CLICKABLE;
+ viewFlagMasks |= CLICKABLE;
+ }
+ break;
+ case com.android.internal.R.styleable.View_longClickable:
+ if (a.getBoolean(attr, false)) {
+ viewFlagValues |= LONG_CLICKABLE;
+ viewFlagMasks |= LONG_CLICKABLE;
+ }
+ break;
+ case com.android.internal.R.styleable.View_saveEnabled:
+ if (!a.getBoolean(attr, true)) {
+ viewFlagValues |= SAVE_DISABLED;
+ viewFlagMasks |= SAVE_DISABLED_MASK;
+ }
+ break;
+ case com.android.internal.R.styleable.View_duplicateParentState:
+ if (a.getBoolean(attr, false)) {
+ viewFlagValues |= DUPLICATE_PARENT_STATE;
+ viewFlagMasks |= DUPLICATE_PARENT_STATE;
+ }
+ break;
+ case com.android.internal.R.styleable.View_visibility:
+ final int visibility = a.getInt(attr, 0);
+ if (visibility != 0) {
+ viewFlagValues |= VISIBILITY_FLAGS[visibility];
+ viewFlagMasks |= VISIBILITY_MASK;
+ }
+ break;
+ case com.android.internal.R.styleable.View_drawingCacheQuality:
+ final int cacheQuality = a.getInt(attr, 0);
+ if (cacheQuality != 0) {
+ viewFlagValues |= DRAWING_CACHE_QUALITY_FLAGS[cacheQuality];
+ viewFlagMasks |= DRAWING_CACHE_QUALITY_MASK;
+ }
+ break;
+ case com.android.internal.R.styleable.View_soundEffectsEnabled:
+ if (!a.getBoolean(attr, true)) {
+ viewFlagValues &= ~SOUND_EFFECTS_ENABLED;
+ viewFlagMasks |= SOUND_EFFECTS_ENABLED;
+ }
+ case R.styleable.View_scrollbars:
+ final int scrollbars = a.getInt(attr, SCROLLBARS_NONE);
+ if (scrollbars != SCROLLBARS_NONE) {
+ viewFlagValues |= scrollbars;
+ viewFlagMasks |= SCROLLBARS_MASK;
+ initializeScrollbars(a);
+ }
+ break;
+ case R.styleable.View_fadingEdge:
+ final int fadingEdge = a.getInt(attr, FADING_EDGE_NONE);
+ if (fadingEdge != FADING_EDGE_NONE) {
+ viewFlagValues |= fadingEdge;
+ viewFlagMasks |= FADING_EDGE_MASK;
+ initializeFadingEdge(a);
+ }
+ break;
+ case R.styleable.View_scrollbarStyle:
+ scrollbarStyle = a.getInt(attr, SCROLLBARS_INSIDE_OVERLAY);
+ if (scrollbarStyle != SCROLLBARS_INSIDE_OVERLAY) {
+ viewFlagValues |= scrollbarStyle & SCROLLBARS_STYLE_MASK;
+ viewFlagMasks |= SCROLLBARS_STYLE_MASK;
+ }
+ break;
+ case com.android.internal.R.styleable.View_keepScreenOn:
+ if (a.getBoolean(attr, false)) {
+ viewFlagValues |= KEEP_SCREEN_ON;
+ viewFlagMasks |= KEEP_SCREEN_ON;
+ }
+ break;
+ case R.styleable.View_nextFocusLeft:
+ mNextFocusLeftId = a.getResourceId(attr, View.NO_ID);
+ break;
+ case R.styleable.View_nextFocusRight:
+ mNextFocusRightId = a.getResourceId(attr, View.NO_ID);
+ break;
+ case R.styleable.View_nextFocusUp:
+ mNextFocusUpId = a.getResourceId(attr, View.NO_ID);
+ break;
+ case R.styleable.View_nextFocusDown:
+ mNextFocusDownId = a.getResourceId(attr, View.NO_ID);
+ break;
+ case R.styleable.View_minWidth:
+ mMinWidth = a.getDimensionPixelSize(attr, 0);
+ break;
+ case R.styleable.View_minHeight:
+ mMinHeight = a.getDimensionPixelSize(attr, 0);
+ break;
+ }
+ }
+
+ if (background != null) {
+ setBackgroundDrawable(background);
+ }
+
+ if (padding >= 0) {
+ leftPadding = padding;
+ topPadding = padding;
+ rightPadding = padding;
+ bottomPadding = padding;
+ }
+
+ // If the user specified the padding (either with android:padding or
+ // android:paddingLeft/Top/Right/Bottom), use this padding, otherwise
+ // use the default padding or the padding from the background drawable
+ // (stored at this point in mPadding*)
+ setPadding(leftPadding >= 0 ? leftPadding : mPaddingLeft,
+ topPadding >= 0 ? topPadding : mPaddingTop,
+ rightPadding >= 0 ? rightPadding : mPaddingRight,
+ bottomPadding >= 0 ? bottomPadding : mPaddingBottom);
+
+ if (viewFlagMasks != 0) {
+ setFlags(viewFlagValues, viewFlagMasks);
+ }
+
+ // Needs to be called after mViewFlags is set
+ if (scrollbarStyle != SCROLLBARS_INSIDE_OVERLAY) {
+ recomputePadding();
+ }
+
+ if (x != 0 || y != 0) {
+ scrollTo(x, y);
+ }
+
+ a.recycle();
+ }
+
+ /**
+ * Non-public constructor for use in testing
+ */
+ View() {
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ super.finalize();
+ --sInstanceCount;
+ }
+
+ /**
+ * <p>
+ * Initializes the fading edges from a given set of styled attributes. This
+ * method should be called by subclasses that need fading edges and when an
+ * instance of these subclasses is created programmatically rather than
+ * being inflated from XML. This method is automatically called when the XML
+ * is inflated.
+ * </p>
+ *
+ * @param a the styled attributes set to initialize the fading edges from
+ */
+ protected void initializeFadingEdge(TypedArray a) {
+ initScrollCache();
+
+ mScrollCache.fadingEdgeLength = a.getDimensionPixelSize(
+ R.styleable.View_fadingEdgeLength, ViewConfiguration.getFadingEdgeLength());
+ }
+
+ /**
+ * Returns the size of the vertical faded edges used to indicate that more
+ * content in this view is visible.
+ *
+ * @return The size in pixels of the vertical faded edge or 0 if vertical
+ * faded edges are not enabled for this view.
+ * @attr ref android.R.styleable#View_fadingEdgeLength
+ */
+ public int getVerticalFadingEdgeLength() {
+ if (isVerticalFadingEdgeEnabled()) {
+ ScrollabilityCache cache = mScrollCache;
+ if (cache != null) {
+ return cache.fadingEdgeLength;
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Set the size of the faded edge used to indicate that more content in this
+ * view is available. Will not change whether the fading edge is enabled; use
+ * {@link #setVerticalFadingEdgeEnabled} or {@link #setHorizontalFadingEdgeEnabled}
+ * to enable the fading edge for the vertical or horizontal fading edges.
+ *
+ * @param length The size in pixels of the faded edge used to indicate that more
+ * content in this view is visible.
+ */
+ public void setFadingEdgeLength(int length) {
+ initScrollCache();
+ mScrollCache.fadingEdgeLength = length;
+ }
+
+ /**
+ * Returns the size of the horizontal faded edges used to indicate that more
+ * content in this view is visible.
+ *
+ * @return The size in pixels of the horizontal faded edge or 0 if horizontal
+ * faded edges are not enabled for this view.
+ * @attr ref android.R.styleable#View_fadingEdgeLength
+ */
+ public int getHorizontalFadingEdgeLength() {
+ if (isHorizontalFadingEdgeEnabled()) {
+ ScrollabilityCache cache = mScrollCache;
+ if (cache != null) {
+ return cache.fadingEdgeLength;
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Returns the width of the vertical scrollbar.
+ *
+ * @return The width in pixels of the vertical scrollbar or 0 if there
+ * is no vertical scrollbar.
+ */
+ public int getVerticalScrollbarWidth() {
+ ScrollabilityCache cache = mScrollCache;
+ if (cache != null) {
+ ScrollBarDrawable scrollBar = cache.scrollBar;
+ if (scrollBar != null) {
+ int size = scrollBar.getSize(true);
+ if (size <= 0) {
+ size = cache.scrollBarSize;
+ }
+ return size;
+ }
+ return 0;
+ }
+ return 0;
+ }
+
+ /**
+ * Returns the height of the horizontal scrollbar.
+ *
+ * @return The height in pixels of the horizontal scrollbar or 0 if
+ * there is no horizontal scrollbar.
+ */
+ protected int getHorizontalScrollbarHeight() {
+ ScrollabilityCache cache = mScrollCache;
+ if (cache != null) {
+ ScrollBarDrawable scrollBar = cache.scrollBar;
+ if (scrollBar != null) {
+ int size = scrollBar.getSize(false);
+ if (size <= 0) {
+ size = cache.scrollBarSize;
+ }
+ return size;
+ }
+ return 0;
+ }
+ return 0;
+ }
+
+ /**
+ * <p>
+ * Initializes the scrollbars from a given set of styled attributes. This
+ * method should be called by subclasses that need scrollbars and when an
+ * instance of these subclasses is created programmatically rather than
+ * being inflated from XML. This method is automatically called when the XML
+ * is inflated.
+ * </p>
+ *
+ * @param a the styled attributes set to initialize the scrollbars from
+ */
+ protected void initializeScrollbars(TypedArray a) {
+ initScrollCache();
+
+ if (mScrollCache.scrollBar == null) {
+ mScrollCache.scrollBar = new ScrollBarDrawable();
+ }
+
+ mScrollCache.scrollBarSize = a.getDimensionPixelSize(
+ com.android.internal.R.styleable.View_scrollbarSize,
+ ViewConfiguration.getScrollBarSize());
+
+ Drawable track = a.getDrawable(R.styleable.View_scrollbarTrackHorizontal);
+ mScrollCache.scrollBar.setHorizontalTrackDrawable(track);
+
+ Drawable thumb = a.getDrawable(R.styleable.View_scrollbarThumbHorizontal);
+ if (thumb != null) {
+ mScrollCache.scrollBar.setHorizontalThumbDrawable(thumb);
+ }
+
+ boolean alwaysDraw = a.getBoolean(R.styleable.View_scrollbarAlwaysDrawHorizontalTrack,
+ false);
+ if (alwaysDraw) {
+ mScrollCache.scrollBar.setAlwaysDrawHorizontalTrack(true);
+ }
+
+ track = a.getDrawable(R.styleable.View_scrollbarTrackVertical);
+ mScrollCache.scrollBar.setVerticalTrackDrawable(track);
+
+ thumb = a.getDrawable(R.styleable.View_scrollbarThumbVertical);
+ if (thumb != null) {
+ mScrollCache.scrollBar.setVerticalThumbDrawable(thumb);
+ }
+
+ alwaysDraw = a.getBoolean(R.styleable.View_scrollbarAlwaysDrawVerticalTrack,
+ false);
+ if (alwaysDraw) {
+ mScrollCache.scrollBar.setAlwaysDrawVerticalTrack(true);
+ }
+
+ // Re-apply user/background padding so that scrollbar(s) get added
+ recomputePadding();
+ }
+
+ /**
+ * <p>
+ * Initalizes the scrollability cache if necessary.
+ * </p>
+ */
+ private void initScrollCache() {
+ if (mScrollCache == null) {
+ mScrollCache = new ScrollabilityCache();
+ }
+ }
+
+ /**
+ * Register a callback to be invoked when focus of this view changed.
+ *
+ * @param l The callback that will run.
+ */
+ public void setOnFocusChangeListener(OnFocusChangeListener l) {
+ mOnFocusChangeListener = l;
+ }
+
+ /**
+ * Returns the focus-change callback registered for this view.
+ *
+ * @return The callback, or null if one is not registered.
+ */
+ public OnFocusChangeListener getOnFocusChangeListener() {
+ return mOnFocusChangeListener;
+ }
+
+ /**
+ * Register a callback to be invoked when this view is clicked. If this view is not
+ * clickable, it becomes clickable.
+ *
+ * @param l The callback that will run
+ *
+ * @see #setClickable(boolean)
+ */
+ public void setOnClickListener(OnClickListener l) {
+ if (!isClickable()) {
+ setClickable(true);
+ }
+ mOnClickListener = l;
+ }
+
+ /**
+ * Register a callback to be invoked when this view is clicked and held. If this view is not
+ * long clickable, it becomes long clickable.
+ *
+ * @param l The callback that will run
+ *
+ * @see #setLongClickable(boolean)
+ */
+ public void setOnLongClickListener(OnLongClickListener l) {
+ if (!isLongClickable()) {
+ setLongClickable(true);
+ }
+ mOnLongClickListener = l;
+ }
+
+ /**
+ * Register a callback to be invoked when the context menu for this view is
+ * being built. If this view is not long clickable, it becomes long clickable.
+ *
+ * @param l The callback that will run
+ *
+ */
+ public void setOnCreateContextMenuListener(OnCreateContextMenuListener l) {
+ if (!isLongClickable()) {
+ setLongClickable(true);
+ }
+ mOnCreateContextMenuListener = l;
+ }
+
+ /**
+ * Call this view's OnClickListener, if it is defined.
+ *
+ * @return True there was an assigned OnClickListener that was called, false
+ * otherwise is returned.
+ */
+ public boolean performClick() {
+ if (mOnClickListener != null) {
+ playSoundEffect(SoundEffectConstants.CLICK);
+ mOnClickListener.onClick(this);
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Call this view's OnLongClickListener, if it is defined. Invokes the context menu
+ * if the OnLongClickListener did not consume the event.
+ *
+ * @return True there was an assigned OnLongClickListener that was called, false
+ * otherwise is returned.
+ */
+ public boolean performLongClick() {
+ boolean handled = false;
+ if (mOnLongClickListener != null) {
+ handled = mOnLongClickListener.onLongClick(View.this);
+ }
+ if (!handled) {
+ handled = showContextMenu();
+ }
+ return handled;
+ }
+
+ /**
+ * Bring up the context menu for this view.
+ *
+ * @return Whether a context menu was displayed.
+ */
+ public boolean showContextMenu() {
+ return getParent().showContextMenuForChild(this);
+ }
+
+ /**
+ * Register a callback to be invoked when a key is pressed in this view.
+ * @param l the key listener to attach to this view
+ */
+ public void setOnKeyListener(OnKeyListener l) {
+ mOnKeyListener = l;
+ }
+
+ /**
+ * Register a callback to be invoked when a touch event is sent to this view.
+ * @param l the touch listener to attach to this view
+ */
+ public void setOnTouchListener(OnTouchListener l) {
+ mOnTouchListener = l;
+ }
+
+ /**
+ * Give this view focus. This will cause {@link #onFocusChanged} to be called.
+ *
+ * Note: this does not check whether this {@link View} should get focus, it just
+ * gives it focus no matter what. It should only be called internally by framework
+ * code that knows what it is doing, namely {@link #requestFocus(int, Rect)}.
+ *
+ * @param direction values are View.FOCUS_UP, View.FOCUS_DOWN,
+ * View.FOCUS_LEFT or View.FOCUS_RIGHT. This is the direction which
+ * focus moved when requestFocus() is called. It may not always
+ * apply, in which case use the default View.FOCUS_DOWN.
+ * @param previouslyFocusedRect The rectangle of the view that had focus
+ * prior in this View's coordinate system.
+ */
+ void handleFocusGainInternal(int direction, Rect previouslyFocusedRect) {
+ if (DBG) {
+ System.out.println(this + " requestFocus()");
+ }
+
+ if ((mPrivateFlags & FOCUSED) == 0) {
+ mPrivateFlags |= FOCUSED;
+
+ if (mParent != null) {
+ mParent.requestChildFocus(this, this);
+ }
+
+ onFocusChanged(true, direction, previouslyFocusedRect);
+ refreshDrawableState();
+ }
+ }
+
+ /**
+ * Request that a rectangle of this view be visible on the screen,
+ * scrolling if necessary just enough.
+ *
+ * A View should call this if it maintains some notion of which part
+ * of its content is interesting. For example, a text editing view
+ * should call this when its cursor moves.
+ *
+ * @param rectangle The rectangle.
+ * @return Whether any parent scrolled.
+ */
+ public boolean requestRectangleOnScreen(Rect rectangle) {
+ return requestRectangleOnScreen(rectangle, false);
+ }
+
+ /**
+ * Request that a rectangle of this view be visible on the screen,
+ * scrolling if necessary just enough.
+ *
+ * A View should call this if it maintains some notion of which part
+ * of its content is interesting. For example, a text editing view
+ * should call this when its cursor moves.
+ *
+ * When <code>immediate</code> is set to true, scrolling will not be
+ * animated.
+ *
+ * @param rectangle The rectangle.
+ * @param immediate True to forbid animated scrolling, false otherwise
+ * @return Whether any parent scrolled.
+ */
+ public boolean requestRectangleOnScreen(Rect rectangle, boolean immediate) {
+ View child = this;
+ ViewParent parent = mParent;
+ boolean scrolled = false;
+ while (parent instanceof ViewGroup) {
+ ViewGroup vgParent = (ViewGroup) parent;
+ scrolled |= vgParent.requestChildRectangleOnScreen(child,
+ rectangle, immediate);
+
+ // offset rect so next call has the rectangle in the
+ // coordinate system of its direct child.
+ rectangle.offset(child.getLeft(), child.getTop());
+ rectangle.offset(-child.getScrollX(), -child.getScrollY());
+
+ child = (View) parent;
+ parent = child.getParent();
+ }
+ return scrolled;
+ }
+
+ /**
+ * Called when this view wants to give up focus. This will cause
+ * {@link #onFocusChanged} to be called.
+ */
+ public void clearFocus() {
+ if (DBG) {
+ System.out.println(this + " clearFocus()");
+ }
+
+ if ((mPrivateFlags & FOCUSED) != 0) {
+ mPrivateFlags &= ~FOCUSED;
+
+ if (mParent != null) {
+ mParent.clearChildFocus(this);
+ }
+
+ onFocusChanged(false, 0, null);
+ refreshDrawableState();
+ }
+ }
+
+ /**
+ * Called to clear the focus of a view that is about to be removed.
+ * Doesn't call clearChildFocus, which prevents this view from taking
+ * focus again before it has been removed from the parent
+ */
+ void clearFocusForRemoval() {
+ if ((mPrivateFlags & FOCUSED) != 0) {
+ mPrivateFlags &= ~FOCUSED;
+
+ onFocusChanged(false, 0, null);
+ refreshDrawableState();
+ }
+ }
+
+ /**
+ * Called internally by the view system when a new view is getting focus.
+ * This is what clears the old focus.
+ */
+ void unFocus() {
+ if (DBG) {
+ System.out.println(this + " unFocus()");
+ }
+
+ if ((mPrivateFlags & FOCUSED) != 0) {
+ mPrivateFlags &= ~FOCUSED;
+
+ onFocusChanged(false, 0, null);
+ refreshDrawableState();
+ }
+ }
+
+ /**
+ * Returns true if this view has focus iteself, or is the ancestor of the
+ * view that has focus.
+ *
+ * @return True if this view has or contains focus, false otherwise.
+ */
+ @ViewDebug.ExportedProperty
+ public boolean hasFocus() {
+ return (mPrivateFlags & FOCUSED) != 0;
+ }
+
+ /**
+ * Returns true if this view is focusable or if it contains a reachable View
+ * for which {@link #hasFocusable()} returns true. A "reachable hasFocusable()"
+ * is a View whose parents do not block descendants focus.
+ *
+ * Only {@link #VISIBLE} views are considered focusable.
+ *
+ * @return True if the view is focusable or if the view contains a focusable
+ * View, false otherwise.
+ *
+ * @see ViewGroup#FOCUS_BLOCK_DESCENDANTS
+ */
+ public boolean hasFocusable() {
+ return (mViewFlags & VISIBILITY_MASK) == VISIBLE && isFocusable();
+ }
+
+ /**
+ * Called by the view system when the focus state of this view changes.
+ * When the focus change event is caused by directional navigation, direction
+ * and previouslyFocusedRect provide insight into where the focus is coming from.
+ *
+ * @param gainFocus True if the View has focus; false otherwise.
+ * @param direction The direction focus has moved when requestFocus()
+ * is called to give this view focus. Values are
+ * View.FOCUS_UP, View.FOCUS_DOWN, View.FOCUS_LEFT or
+ * View.FOCUS_RIGHT. It may not always apply, in which
+ * case use the default.
+ * @param previouslyFocusedRect The rectangle, in this view's coordinate
+ * system, of the previously focused view. If applicable, this will be
+ * passed in as finer grained information about where the focus is coming
+ * from (in addition to direction). Will be <code>null</code> otherwise.
+ */
+ protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
+ if (!gainFocus) {
+ if (isPressed()) {
+ setPressed(false);
+ }
+ }
+ invalidate();
+ if (mOnFocusChangeListener != null) {
+ mOnFocusChangeListener.onFocusChange(this, gainFocus);
+ }
+ }
+
+ /**
+ * Returns true if this view has focus
+ *
+ * @return True if this view has focus, false otherwise.
+ */
+ @ViewDebug.ExportedProperty
+ public boolean isFocused() {
+ return (mPrivateFlags & FOCUSED) != 0;
+ }
+
+ /**
+ * Find the view in the hierarchy rooted at this view that currently has
+ * focus.
+ *
+ * @return The view that currently has focus, or null if no focused view can
+ * be found.
+ */
+ public View findFocus() {
+ return (mPrivateFlags & FOCUSED) != 0 ? this : null;
+ }
+
+ /**
+ * Returns the quality of the drawing cache.
+ *
+ * @return One of {@link #DRAWING_CACHE_QUALITY_AUTO},
+ * {@link #DRAWING_CACHE_QUALITY_LOW}, or {@link #DRAWING_CACHE_QUALITY_HIGH}
+ *
+ * @see #setDrawingCacheQuality(int)
+ * @see #setDrawingCacheEnabled(boolean)
+ * @see #isDrawingCacheEnabled()
+ *
+ * @attr ref android.R.styleable#View_drawingCacheQuality
+ */
+ public int getDrawingCacheQuality() {
+ return mViewFlags & DRAWING_CACHE_QUALITY_MASK;
+ }
+
+ /**
+ * Set the drawing cache quality of this view. This value is used only when the
+ * drawing cache is enabled
+ *
+ * @param quality One of {@link #DRAWING_CACHE_QUALITY_AUTO},
+ * {@link #DRAWING_CACHE_QUALITY_LOW}, or {@link #DRAWING_CACHE_QUALITY_HIGH}
+ *
+ * @see #getDrawingCacheQuality()
+ * @see #setDrawingCacheEnabled(boolean)
+ * @see #isDrawingCacheEnabled()
+ *
+ * @attr ref android.R.styleable#View_drawingCacheQuality
+ */
+ public void setDrawingCacheQuality(int quality) {
+ setFlags(quality, DRAWING_CACHE_QUALITY_MASK);
+ }
+
+ /**
+ * Returns whether the screen should remain on, corresponding to the current
+ * value of {@link #KEEP_SCREEN_ON}.
+ *
+ * @return Returns true if {@link #KEEP_SCREEN_ON} is set.
+ *
+ * @see #setKeepScreenOn(boolean)
+ *
+ * @attr ref android.R.styleable#View_keepScreenOn
+ */
+ public boolean getKeepScreenOn() {
+ return (mViewFlags & KEEP_SCREEN_ON) != 0;
+ }
+
+ /**
+ * Controls whether the screen should remain on, modifying the
+ * value of {@link #KEEP_SCREEN_ON}.
+ *
+ * @param keepScreenOn Supply true to set {@link #KEEP_SCREEN_ON}.
+ *
+ * @see #getKeepScreenOn()
+ *
+ * @attr ref android.R.styleable#View_keepScreenOn
+ */
+ public void setKeepScreenOn(boolean keepScreenOn) {
+ setFlags(keepScreenOn ? KEEP_SCREEN_ON : 0, KEEP_SCREEN_ON);
+ }
+
+ /**
+ * @return The user specified next focus ID.
+ *
+ * @attr ref android.R.styleable#View_nextFocusLeft
+ */
+ public int getNextFocusLeftId() {
+ return mNextFocusLeftId;
+ }
+
+ /**
+ * Set the id of the view to use for the next focus
+ *
+ * @param nextFocusLeftId
+ *
+ * @attr ref android.R.styleable#View_nextFocusLeft
+ */
+ public void setNextFocusLeftId(int nextFocusLeftId) {
+ mNextFocusLeftId = nextFocusLeftId;
+ }
+
+ /**
+ * @return The user specified next focus ID.
+ *
+ * @attr ref android.R.styleable#View_nextFocusRight
+ */
+ public int getNextFocusRightId() {
+ return mNextFocusRightId;
+ }
+
+ /**
+ * Set the id of the view to use for the next focus
+ *
+ * @param nextFocusRightId
+ *
+ * @attr ref android.R.styleable#View_nextFocusRight
+ */
+ public void setNextFocusRightId(int nextFocusRightId) {
+ mNextFocusRightId = nextFocusRightId;
+ }
+
+ /**
+ * @return The user specified next focus ID.
+ *
+ * @attr ref android.R.styleable#View_nextFocusUp
+ */
+ public int getNextFocusUpId() {
+ return mNextFocusUpId;
+ }
+
+ /**
+ * Set the id of the view to use for the next focus
+ *
+ * @param nextFocusUpId
+ *
+ * @attr ref android.R.styleable#View_nextFocusUp
+ */
+ public void setNextFocusUpId(int nextFocusUpId) {
+ mNextFocusUpId = nextFocusUpId;
+ }
+
+ /**
+ * @return The user specified next focus ID.
+ *
+ * @attr ref android.R.styleable#View_nextFocusDown
+ */
+ public int getNextFocusDownId() {
+ return mNextFocusDownId;
+ }
+
+ /**
+ * Set the id of the view to use for the next focus
+ *
+ * @param nextFocusDownId
+ *
+ * @attr ref android.R.styleable#View_nextFocusDown
+ */
+ public void setNextFocusDownId(int nextFocusDownId) {
+ mNextFocusDownId = nextFocusDownId;
+ }
+
+ /**
+ * Returns the visibility of this view and all of its ancestors
+ *
+ * @return True if this view and all of its ancestors are {@link #VISIBLE}
+ */
+ public boolean isShown() {
+ View current = this;
+ //noinspection ConstantConditions
+ do {
+ if ((current.mViewFlags & VISIBILITY_MASK) != VISIBLE) {
+ return false;
+ }
+ ViewParent parent = current.mParent;
+ if (parent == null) {
+ return false; // We are not attached to the view root
+ }
+ if (parent instanceof ViewRoot) {
+ return true;
+ }
+ current = (View) parent;
+ } while (current != null);
+
+ return false;
+ }
+
+ /**
+ * Apply the insets for system windows to this view, if the FITS_SYSTEM_WINDOWS flag
+ * is set
+ *
+ * @param insets Insets for system windows
+ *
+ * @return True if this view applied the insets, false otherwise
+ */
+ protected boolean fitSystemWindows(Rect insets) {
+ if ((mViewFlags & FITS_SYSTEM_WINDOWS) == FITS_SYSTEM_WINDOWS) {
+ mPaddingLeft = insets.left;
+ mPaddingTop = insets.top;
+ mPaddingRight = insets.right;
+ mPaddingBottom = insets.bottom;
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns the visibility status for this view.
+ *
+ * @return One of {@link #VISIBLE}, {@link #INVISIBLE}, or {@link #GONE}.
+ * @attr ref android.R.styleable#View_visibility
+ */
+ @ViewDebug.ExportedProperty(mapping = {
+ @ViewDebug.IntToString(from = 0, to = "VISIBLE"),
+ @ViewDebug.IntToString(from = 4, to = "INVISIBLE"),
+ @ViewDebug.IntToString(from = 8, to = "GONE")
+ })
+ public int getVisibility() {
+ return mViewFlags & VISIBILITY_MASK;
+ }
+
+ /**
+ * Set the enabled state of this view.
+ *
+ * @param visibility One of {@link #VISIBLE}, {@link #INVISIBLE}, or {@link #GONE}.
+ * @attr ref android.R.styleable#View_visibility
+ */
+ public void setVisibility(int visibility) {
+ setFlags(visibility, VISIBILITY_MASK);
+ }
+
+ /**
+ * Returns the enabled status for this view. The interpretation of the
+ * enabled state varies by subclass.
+ *
+ * @return True if this view is enabled, false otherwise.
+ */
+ @ViewDebug.ExportedProperty
+ public boolean isEnabled() {
+ return (mViewFlags & ENABLED_MASK) == ENABLED;
+ }
+
+ /**
+ * Set the enabled state of this view. The interpretation of the enabled
+ * state varies by subclass.
+ *
+ * @param enabled True if this view is enabled, false otherwise.
+ */
+ public void setEnabled(boolean enabled) {
+ setFlags(enabled ? ENABLED : DISABLED, ENABLED_MASK);
+
+ /*
+ * The View most likely has to change its appearance, so refresh
+ * the drawable state.
+ */
+ refreshDrawableState();
+
+ // Invalidate too, since the default behavior for views is to be
+ // be drawn at 50% alpha rather than to change the drawable.
+ invalidate();
+ }
+
+ /**
+ * Set whether this view can receive the focus.
+ *
+ * Setting this to false will also ensure that this view is not focusable
+ * in touch mode.
+ *
+ * @param focusable If true, this view can receive the focus.
+ *
+ * @see #setFocusableInTouchMode(boolean)
+ * @attr ref android.R.styleable#View_focusable
+ */
+ public void setFocusable(boolean focusable) {
+ if (!focusable) {
+ setFlags(0, FOCUSABLE_IN_TOUCH_MODE);
+ }
+ setFlags(focusable ? FOCUSABLE : NOT_FOCUSABLE, FOCUSABLE_MASK);
+ }
+
+ /**
+ * Set whether this view can receive focus while in touch mode.
+ *
+ * Setting this to true will also ensure that this view is focusable.
+ *
+ * @param focusableInTouchMode If true, this view can receive the focus while
+ * in touch mode.
+ *
+ * @see #setFocusable(boolean)
+ * @attr ref android.R.styleable#View_focusableInTouchMode
+ */
+ public void setFocusableInTouchMode(boolean focusableInTouchMode) {
+ // Focusable in touch mode should always be set before the focusable flag
+ // otherwise, setting the focusable flag will trigger a focusableViewAvailable()
+ // which, in touch mode, will not successfully request focus on this view
+ // because the focusable in touch mode flag is not set
+ setFlags(focusableInTouchMode ? FOCUSABLE_IN_TOUCH_MODE : 0, FOCUSABLE_IN_TOUCH_MODE);
+ if (focusableInTouchMode) {
+ setFlags(FOCUSABLE, FOCUSABLE_MASK);
+ }
+ }
+
+ /**
+ * Set whether this view should have sound effects enabled for events such as
+ * clicking and touching.
+ *
+ * You may wish to disable sound effects for a view if you already play sounds,
+ * for instance, a dial key that plays dtmf tones.
+ *
+ * @param soundEffectsEnabled whether sound effects are enabled for this view.
+ * @see #isSoundEffectsEnabled()
+ * @see #playSoundEffect(int)
+ * @attr ref android.R.styleable#View_soundEffectsEnabled
+ */
+ public void setSoundEffectsEnabled(boolean soundEffectsEnabled) {
+ setFlags(soundEffectsEnabled ? SOUND_EFFECTS_ENABLED: 0, SOUND_EFFECTS_ENABLED);
+ }
+
+ /**
+ * @return whether this view should have sound effects enabled for events such as
+ * clicking and touching.
+ *
+ * @see #setSoundEffectsEnabled(boolean)
+ * @see #playSoundEffect(int)
+ * @attr ref android.R.styleable#View_soundEffectsEnabled
+ */
+ @ViewDebug.ExportedProperty
+ public boolean isSoundEffectsEnabled() {
+ return SOUND_EFFECTS_ENABLED == (mViewFlags & SOUND_EFFECTS_ENABLED);
+ }
+
+ /**
+ * If this view doesn't do any drawing on its own, set this flag to
+ * allow further optimizations. By default, this flag is not set on
+ * View, but could be set on some View subclasses such as ViewGroup.
+ *
+ * Typically, if you override {@link #onDraw} you should clear this flag.
+ *
+ * @param willNotDraw whether or not this View draw on its own
+ */
+ public void setWillNotDraw(boolean willNotDraw) {
+ setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
+ }
+
+ /**
+ * Returns whether or not this View draws on its own.
+ *
+ * @return true if this view has nothing to draw, false otherwise
+ */
+ @ViewDebug.ExportedProperty
+ public boolean willNotDraw() {
+ return (mViewFlags & DRAW_MASK) == WILL_NOT_DRAW;
+ }
+
+ /**
+ * When a View's drawing cache is enabled, drawing is redirected to an
+ * offscreen bitmap. Some views, like an ImageView, must be able to
+ * bypass this mechanism if they already draw a single bitmap, to avoid
+ * unnecessary usage of the memory.
+ *
+ * @param willNotCacheDrawing true if this view does not cache its
+ * drawing, false otherwise
+ */
+ public void setWillNotCacheDrawing(boolean willNotCacheDrawing) {
+ setFlags(willNotCacheDrawing ? WILL_NOT_CACHE_DRAWING : 0, WILL_NOT_CACHE_DRAWING);
+ }
+
+ /**
+ * Returns whether or not this View can cache its drawing or not.
+ *
+ * @return true if this view does not cache its drawing, false otherwise
+ */
+ @ViewDebug.ExportedProperty
+ public boolean willNotCacheDrawing() {
+ return (mViewFlags & WILL_NOT_CACHE_DRAWING) == WILL_NOT_CACHE_DRAWING;
+ }
+
+ /**
+ * Indicates whether this view reacts to click events or not.
+ *
+ * @return true if the view is clickable, false otherwise
+ *
+ * @see #setClickable(boolean)
+ * @attr ref android.R.styleable#View_clickable
+ */
+ @ViewDebug.ExportedProperty
+ public boolean isClickable() {
+ return (mViewFlags & CLICKABLE) == CLICKABLE;
+ }
+
+ /**
+ * Enables or disables click events for this view. When a view
+ * is clickable it will change its state to "pressed" on every click.
+ * Subclasses should set the view clickable to visually react to
+ * user's clicks.
+ *
+ * @param clickable true to make the view clickable, false otherwise
+ *
+ * @see #isClickable()
+ * @attr ref android.R.styleable#View_clickable
+ */
+ public void setClickable(boolean clickable) {
+ setFlags(clickable ? CLICKABLE : 0, CLICKABLE);
+ }
+
+ /**
+ * Indicates whether this view reacts to long click events or not.
+ *
+ * @return true if the view is long clickable, false otherwise
+ *
+ * @see #setLongClickable(boolean)
+ * @attr ref android.R.styleable#View_longClickable
+ */
+ public boolean isLongClickable() {
+ return (mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE;
+ }
+
+ /**
+ * Enables or disables long click events for this view. When a view is long
+ * clickable it reacts to the user holding down the button for a longer
+ * duration than a tap. This event can either launch the listener or a
+ * context menu.
+ *
+ * @param longClickable true to make the view long clickable, false otherwise
+ * @see #isLongClickable()
+ * @attr ref android.R.styleable#View_longClickable
+ */
+ public void setLongClickable(boolean longClickable) {
+ setFlags(longClickable ? LONG_CLICKABLE : 0, LONG_CLICKABLE);
+ }
+
+ /**
+ * Sets the pressed that for this view.
+ *
+ * @see #isClickable()
+ * @see #setClickable(boolean)
+ *
+ * @param pressed Pass true to set the View's internal state to "pressed", or false to reverts
+ * the View's internal state from a previously set "pressed" state.
+ */
+ public void setPressed(boolean pressed) {
+ if (pressed) {
+ mPrivateFlags |= PRESSED;
+ } else {
+ mPrivateFlags &= ~PRESSED;
+ }
+ refreshDrawableState();
+ dispatchSetPressed(pressed);
+ }
+
+ /**
+ * Dispatch setPressed to all of this View's children.
+ *
+ * @see #setPressed(boolean)
+ *
+ * @param pressed The new pressed state
+ */
+ protected void dispatchSetPressed(boolean pressed) {
+ }
+
+ /**
+ * Indicates whether the view is currently in pressed state. Unless
+ * {@link #setPressed(boolean)} is explicitly called, only clickable views can enter
+ * the pressed state.
+ *
+ * @see #setPressed
+ * @see #isClickable()
+ * @see #setClickable(boolean)
+ *
+ * @return true if the view is currently pressed, false otherwise
+ */
+ public boolean isPressed() {
+ return (mPrivateFlags & PRESSED) == PRESSED;
+ }
+
+ /**
+ * Indicates whether this view will save its state (that is,
+ * whether its {@link #onSaveInstanceState} method will be called).
+ *
+ * @return Returns true if the view state saving is enabled, else false.
+ *
+ * @see #setSaveEnabled(boolean)
+ * @attr ref android.R.styleable#View_saveEnabled
+ */
+ public boolean isSaveEnabled() {
+ return (mViewFlags & SAVE_DISABLED_MASK) != SAVE_DISABLED;
+ }
+
+ /**
+ * Controls whether the saving of this view's state is
+ * enabled (that is, whether its {@link #onSaveInstanceState} method
+ * will be called). Note that even if freezing is enabled, the
+ * view still must have an id assigned to it (via {@link #setId setId()})
+ * for its state to be saved. This flag can only disable the
+ * saving of this view; any child views may still have their state saved.
+ *
+ * @param enabled Set to false to <em>disable</em> state saving, or true
+ * (the default) to allow it.
+ *
+ * @see #isSaveEnabled()
+ * @see #setId(int)
+ * @see #onSaveInstanceState()
+ * @attr ref android.R.styleable#View_saveEnabled
+ */
+ public void setSaveEnabled(boolean enabled) {
+ setFlags(enabled ? 0 : SAVE_DISABLED, SAVE_DISABLED_MASK);
+ }
+
+
+ /**
+ * Returns whether this View is able to take focus.
+ *
+ * @return True if this view can take focus, or false otherwise.
+ * @attr ref android.R.styleable#View_focusable
+ */
+ @ViewDebug.ExportedProperty
+ public final boolean isFocusable() {
+ return FOCUSABLE == (mViewFlags & FOCUSABLE_MASK);
+ }
+
+ /**
+ * When a view is focusable, it may not want to take focus when in touch mode.
+ * For example, a button would like focus when the user is navigating via a D-pad
+ * so that the user can click on it, but once the user starts touching the screen,
+ * the button shouldn't take focus
+ * @return Whether the view is focusable in touch mode.
+ * @attr ref android.R.styleable#View_focusableInTouchMode
+ */
+ @ViewDebug.ExportedProperty
+ public final boolean isFocusableInTouchMode() {
+ return FOCUSABLE_IN_TOUCH_MODE == (mViewFlags & FOCUSABLE_IN_TOUCH_MODE);
+ }
+
+ /**
+ * Find the nearest view in the specified direction that can take focus.
+ * This does not actually give focus to that view.
+ *
+ * @param direction One of FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, and FOCUS_RIGHT
+ *
+ * @return The nearest focusable in the specified direction, or null if none
+ * can be found.
+ */
+ public View focusSearch(int direction) {
+ if (mParent != null) {
+ return mParent.focusSearch(this, direction);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * This method is the last chance for the focused view and its ancestors to
+ * respond to an arrow key. This is called when the focused view did not
+ * consume the key internally, nor could the view system find a new view in
+ * the requested direction to give focus to.
+ *
+ * @param focused The currently focused view.
+ * @param direction The direction focus wants to move. One of FOCUS_UP,
+ * FOCUS_DOWN, FOCUS_LEFT, and FOCUS_RIGHT.
+ * @return True if the this view consumed this unhandled move.
+ */
+ public boolean dispatchUnhandledMove(View focused, int direction) {
+ return false;
+ }
+
+ /**
+ * If a user manually specified the next view id for a particular direction,
+ * use the root to look up the view. Once a view is found, it is cached
+ * for future lookups.
+ * @param root The root view of the hierarchy containing this view.
+ * @param direction One of FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, and FOCUS_RIGHT
+ * @return The user specified next view, or null if there is none.
+ */
+ View findUserSetNextFocus(View root, int direction) {
+ switch (direction) {
+ case FOCUS_LEFT:
+ if (mNextFocusLeftId == View.NO_ID) return null;
+ return findViewShouldExist(root, mNextFocusLeftId);
+ case FOCUS_RIGHT:
+ if (mNextFocusRightId == View.NO_ID) return null;
+ return findViewShouldExist(root, mNextFocusRightId);
+ case FOCUS_UP:
+ if (mNextFocusUpId == View.NO_ID) return null;
+ return findViewShouldExist(root, mNextFocusUpId);
+ case FOCUS_DOWN:
+ if (mNextFocusDownId == View.NO_ID) return null;
+ return findViewShouldExist(root, mNextFocusDownId);
+ }
+ return null;
+ }
+
+ private static View findViewShouldExist(View root, int childViewId) {
+ View result = root.findViewById(childViewId);
+ if (result == null) {
+ Log.w(VIEW_LOG_TAG, "couldn't find next focus view specified "
+ + "by user for id " + childViewId);
+ }
+ return result;
+ }
+
+ /**
+ * Find and return all focusable views that are descendants of this view,
+ * possibly including this view if it is focusable itself.
+ *
+ * @param direction The direction of the focus
+ * @return A list of focusable views
+ */
+ public ArrayList<View> getFocusables(int direction) {
+ ArrayList<View> result = new ArrayList<View>(24);
+ addFocusables(result, direction);
+ return result;
+ }
+
+ /**
+ * Add any focusable views that are descendants of this view (possibly
+ * including this view if it is focusable itself) to views. If we are in touch mode,
+ * only add views that are also focusable in touch mode.
+ *
+ * @param views Focusable views found so far
+ * @param direction The direction of the focus
+ */
+ public void addFocusables(ArrayList<View> views, int direction) {
+ if (!isFocusable()) return;
+
+ if (isInTouchMode() && !isFocusableInTouchMode()) return;
+
+ views.add(this);
+ }
+
+ /**
+ * Find and return all touchable views that are descendants of this view,
+ * possibly including this view if it is touchable itself.
+ *
+ * @return A list of touchable views
+ */
+ public ArrayList<View> getTouchables() {
+ ArrayList<View> result = new ArrayList<View>();
+ addTouchables(result);
+ return result;
+ }
+
+ /**
+ * Add any touchable views that are descendants of this view (possibly
+ * including this view if it is touchable itself) to views.
+ *
+ * @param views Touchable views found so far
+ */
+ public void addTouchables(ArrayList<View> views) {
+ final int viewFlags = mViewFlags;
+
+ if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
+ && (viewFlags & ENABLED_MASK) == ENABLED) {
+ views.add(this);
+ }
+ }
+
+ /**
+ * Call this to try to give focus to a specific view or to one of its
+ * descendants.
+ *
+ * A view will not actually take focus if it is not focusable ({@link #isFocusable} returns false),
+ * or if it is focusable and it is not focusable in touch mode ({@link #isFocusableInTouchMode})
+ * while the device is in touch mode.
+ *
+ * See also {@link #focusSearch}, which is what you call to say that you
+ * have focus, and you want your parent to look for the next one.
+ *
+ * This is equivalent to calling {@link #requestFocus(int, Rect)} with arguments
+ * {@link #FOCUS_DOWN} and <code>null</code>.
+ *
+ * @return Whether this view or one of its descendants actually took focus.
+ */
+ public final boolean requestFocus() {
+ return requestFocus(View.FOCUS_DOWN);
+ }
+
+
+ /**
+ * Call this to try to give focus to a specific view or to one of its
+ * descendants and give it a hint about what direction focus is heading.
+ *
+ * A view will not actually take focus if it is not focusable ({@link #isFocusable} returns false),
+ * or if it is focusable and it is not focusable in touch mode ({@link #isFocusableInTouchMode})
+ * while the device is in touch mode.
+ *
+ * See also {@link #focusSearch}, which is what you call to say that you
+ * have focus, and you want your parent to look for the next one.
+ *
+ * This is equivalent to calling {@link #requestFocus(int, Rect)} with
+ * <code>null</code> set for the previously focused rectangle.
+ *
+ * @param direction One of FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, and FOCUS_RIGHT
+ * @return Whether this view or one of its descendants actually took focus.
+ */
+ public final boolean requestFocus(int direction) {
+ return requestFocus(direction, null);
+ }
+
+ /**
+ * Call this to try to give focus to a specific view or to one of its descendants
+ * and give it hints about the direction and a specific rectangle that the focus
+ * is coming from. The rectangle can help give larger views a finer grained hint
+ * about where focus is coming from, and therefore, where to show selection, or
+ * forward focus change internally.
+ *
+ * A view will not actually take focus if it is not focusable ({@link #isFocusable} returns false),
+ * or if it is focusable and it is not focusable in touch mode ({@link #isFocusableInTouchMode})
+ * while the device is in touch mode.
+ *
+ * A View will not take focus if it is not visible.
+ *
+ * A View will not take focus if one of its parents has {@link android.view.ViewGroup#getDescendantFocusability()}
+ * equal to {@link ViewGroup#FOCUS_BLOCK_DESCENDANTS}.
+ *
+ * See also {@link #focusSearch}, which is what you call to say that you
+ * have focus, and you want your parent to look for the next one.
+ *
+ * You may wish to override this method if your custom {@link View} has an internal
+ * {@link View} that it wishes to forward the request to.
+ *
+ * @param direction One of FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, and FOCUS_RIGHT
+ * @param previouslyFocusedRect The rectangle (in this View's coordinate system)
+ * to give a finer grained hint about where focus is coming from. May be null
+ * if there is no hint.
+ * @return Whether this view or one of its descendants actually took focus.
+ */
+ public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
+ // need to be focusable
+ if ((mViewFlags & FOCUSABLE_MASK) != FOCUSABLE ||
+ (mViewFlags & VISIBILITY_MASK) != VISIBLE) {
+ return false;
+ }
+
+ // need to be focusable in touch mode if in touch mode
+ if (isInTouchMode() &&
+ (FOCUSABLE_IN_TOUCH_MODE != (mViewFlags & FOCUSABLE_IN_TOUCH_MODE))) {
+ return false;
+ }
+
+ // need to not have any parents blocking us
+ if (hasAncestorThatBlocksDescendantFocus()) {
+ return false;
+ }
+
+ handleFocusGainInternal(direction, previouslyFocusedRect);
+ return true;
+ }
+
+ /**
+ * Call this to try to give focus to a specific view or to one of its descendants. This is a
+ * special variant of {@link #requestFocus() } that will allow views that are not focuable in
+ * touch mode to request focus when they are touched.
+ *
+ * @return Whether this view or one of its descendants actually took focus.
+ *
+ * @see #isInTouchMode()
+ *
+ */
+ public final boolean requestFocusFromTouch() {
+ // Leave touch mode if we need to
+ if (isInTouchMode()) {
+ View root = getRootView();
+ if (root != null) {
+ ViewRoot viewRoot = (ViewRoot)root.getParent();
+ if (viewRoot != null) {
+ viewRoot.ensureTouchMode(false);
+ }
+ }
+ }
+ return requestFocus(View.FOCUS_DOWN);
+ }
+
+ /**
+ * @return Whether any ancestor of this view blocks descendant focus.
+ */
+ private boolean hasAncestorThatBlocksDescendantFocus() {
+ ViewParent ancestor = mParent;
+ while (ancestor instanceof ViewGroup) {
+ final ViewGroup vgAncestor = (ViewGroup) ancestor;
+ if (vgAncestor.getDescendantFocusability() == ViewGroup.FOCUS_BLOCK_DESCENDANTS) {
+ return true;
+ } else {
+ ancestor = vgAncestor.getParent();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Dispatch a key event to the next view on the focus path. This path runs
+ * from the top of the view tree down to the currently focused view. If this
+ * view has focus, it will dispatch to itself. Otherwise it will dispatch
+ * the next node down the focus path. This method also fires any key
+ * listeners.
+ *
+ * @param event The key event to be dispatched.
+ * @return True if the event was handled, false otherwise.
+ */
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ // If any attached key listener a first crack at the event.
+ //noinspection SimplifiableIfStatement
+ if (mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
+ && mOnKeyListener.onKey(this, event.getKeyCode(), event)) {
+ return true;
+ }
+
+ return event.dispatch(this);
+ }
+
+ /**
+ * Dispatches a key shortcut event.
+ *
+ * @param event The key event to be dispatched.
+ * @return True if the event was handled by the view, false otherwise.
+ */
+ public boolean dispatchKeyShortcutEvent(KeyEvent event) {
+ return onKeyShortcut(event.getKeyCode(), event);
+ }
+
+ /**
+ * Pass the touch screen motion event down to the target view, or this
+ * view if it is the target.
+ *
+ * @param event The motion event to be dispatched.
+ * @return True if the event was handled by the view, false otherwise.
+ */
+ public boolean dispatchTouchEvent(MotionEvent event) {
+ if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
+ mOnTouchListener.onTouch(this, event)) {
+ return true;
+ }
+ return onTouchEvent(event);
+ }
+
+ /**
+ * Pass a trackball motion event down to the focused view.
+ *
+ * @param event The motion event to be dispatched.
+ * @return True if the event was handled by the view, false otherwise.
+ */
+ public boolean dispatchTrackballEvent(MotionEvent event) {
+ //Log.i("view", "view=" + this + ", " + event.toString());
+ return onTrackballEvent(event);
+ }
+
+ /**
+ * Called when the window containing this view gains or loses window focus.
+ * ViewGroups should override to route to their children.
+ *
+ * @param hasFocus True if the window containing this view now has focus,
+ * false otherwise.
+ */
+ public void dispatchWindowFocusChanged(boolean hasFocus) {
+ onWindowFocusChanged(hasFocus);
+ }
+
+ /**
+ * Called when the window containing this view gains or loses focus. Note
+ * that this is separate from view focus: to receive key events, both
+ * your view and its window must have focus. If a window is displayed
+ * on top of yours that takes input focus, then your own window will lose
+ * focus but the view focus will remain unchanged.
+ *
+ * @param hasWindowFocus True if the window containing this view now has
+ * focus, false otherwise.
+ */
+ public void onWindowFocusChanged(boolean hasWindowFocus) {
+ if (!hasWindowFocus) {
+ if (isPressed()) {
+ setPressed(false);
+ }
+ }
+ refreshDrawableState();
+ }
+
+ /**
+ * Returns true if this view is in a window that currently has window focus.
+ * Note that this is not the same as the view itself having focus.
+ *
+ * @return True if this view is in a window that currently has window focus.
+ */
+ public boolean hasWindowFocus() {
+ return mAttachInfo != null && mAttachInfo.mHasWindowFocus;
+ }
+
+ /**
+ * Dispatch a window visibility change down the view hierarchy.
+ * ViewGroups should override to route to their children.
+ *
+ * @param visibility The new visibility of the window.
+ *
+ * @see #onWindowVisibilityChanged
+ */
+ public void dispatchWindowVisibilityChanged(int visibility) {
+ onWindowVisibilityChanged(visibility);
+ }
+
+ /**
+ * Called when the window containing has change its visibility
+ * (between {@link #GONE}, {@link #INVISIBLE}, and {@link #VISIBLE}). Note
+ * that this tells you whether or not your window is being made visible
+ * to the window manager; this does <em>not</em> tell you whether or not
+ * your window is obscured by other windows on the screen, even if it
+ * is itself visible.
+ *
+ * @param visibility The new visibility of the window.
+ */
+ protected void onWindowVisibilityChanged(int visibility) {
+ }
+
+ /**
+ * Returns the current visibility of the window this view is attached to
+ * (either {@link #GONE}, {@link #INVISIBLE}, or {@link #VISIBLE}).
+ *
+ * @return Returns the current visibility of the view's window.
+ */
+ public int getWindowVisibility() {
+ return mAttachInfo != null ? mAttachInfo.mWindowVisibility : GONE;
+ }
+
+ /**
+ * Private function to aggregate all per-view attributes in to the view
+ * root.
+ */
+ void dispatchCollectViewAttributes(int visibility) {
+ performCollectViewAttributes(visibility);
+ }
+
+ void performCollectViewAttributes(int visibility) {
+ if (((visibility | mViewFlags) & (VISIBILITY_MASK | KEEP_SCREEN_ON))
+ == (VISIBLE | KEEP_SCREEN_ON)) {
+ mAttachInfo.mKeepScreenOn = true;
+ }
+ }
+
+ void needGlobalAttributesUpdate(boolean force) {
+ AttachInfo ai = mAttachInfo;
+ if (ai != null) {
+ if (ai.mKeepScreenOn || force) {
+ ai.mRecomputeGlobalAttributes = true;
+ }
+ }
+ }
+
+ /**
+ * Returns whether the device is currently in touch mode. Touch mode is entered
+ * once the user begins interacting with the device by touch, and affects various
+ * things like whether focus is always visible to the user.
+ *
+ * @return Whether the device is in touch mode.
+ */
+ @ViewDebug.ExportedProperty
+ public boolean isInTouchMode() {
+ if (mAttachInfo != null) {
+ return mAttachInfo.mInTouchMode;
+ } else {
+ return ViewRoot.isInTouchMode();
+ }
+ }
+
+ /**
+ * Returns the context the view is running in, through which it can
+ * access the current theme, resources, etc.
+ *
+ * @return The view's Context.
+ */
+ public final Context getContext() {
+ return mContext;
+ }
+
+ /**
+ * Default implementation of {@link KeyEvent.Callback#onKeyMultiple(int, int, KeyEvent)
+ * KeyEvent.Callback.onKeyMultiple()}: perform press of the view
+ * when {@link KeyEvent#KEYCODE_DPAD_CENTER} or {@link KeyEvent#KEYCODE_ENTER}
+ * is released, if the view is enabled and clickable.
+ *
+ * @param keyCode A key code that represents the button pressed, from
+ * {@link android.view.KeyEvent}.
+ * @param event The KeyEvent object that defines the button action.
+ */
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ boolean result = false;
+
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ case KeyEvent.KEYCODE_ENTER: {
+ if ((mViewFlags & ENABLED_MASK) == DISABLED) {
+ return true;
+ }
+ // Long clickable items don't necessarily have to be clickable
+ if (((mViewFlags & CLICKABLE) == CLICKABLE ||
+ (mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) &&
+ (event.getRepeatCount() == 0)) {
+ setPressed(true);
+ if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
+ postCheckForLongClick();
+ }
+ return true;
+ }
+ break;
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Default implementation of {@link KeyEvent.Callback#onKeyMultiple(int, int, KeyEvent)
+ * KeyEvent.Callback.onKeyMultiple()}: perform clicking of the view
+ * when {@link KeyEvent#KEYCODE_DPAD_CENTER} or
+ * {@link KeyEvent#KEYCODE_ENTER} is released.
+ *
+ * @param keyCode A key code that represents the button pressed, from
+ * {@link android.view.KeyEvent}.
+ * @param event The KeyEvent object that defines the button action.
+ */
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ boolean result = false;
+
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ case KeyEvent.KEYCODE_ENTER: {
+ if ((mViewFlags & ENABLED_MASK) == DISABLED) {
+ return true;
+ }
+ if ((mViewFlags & CLICKABLE) == CLICKABLE && isPressed()) {
+ setPressed(false);
+
+ if (!mHasPerformedLongPress) {
+ // This is a tap, so remove the longpress check
+ if (mPendingCheckForLongPress != null) {
+ removeCallbacks(mPendingCheckForLongPress);
+ }
+
+ result = performClick();
+ }
+ }
+ break;
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Default implementation of {@link KeyEvent.Callback#onKeyMultiple(int, int, KeyEvent)
+ * KeyEvent.Callback.onKeyMultiple()}: always returns false (doesn't handle
+ * the event).
+ *
+ * @param keyCode A key code that represents the button pressed, from
+ * {@link android.view.KeyEvent}.
+ * @param repeatCount The number of times the action was made.
+ * @param event The KeyEvent object that defines the button action.
+ */
+ public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
+ return false;
+ }
+
+ /**
+ * Called when an unhandled key shortcut event occurs.
+ *
+ * @param keyCode The value in event.getKeyCode().
+ * @param event Description of the key event.
+ * @return If you handled the event, return true. If you want to allow the
+ * event to be handled by the next receiver, return false.
+ */
+ public boolean onKeyShortcut(int keyCode, KeyEvent event) {
+ return false;
+ }
+
+ /**
+ * Show the context menu for this view. It is not safe to hold on to the
+ * menu after returning from this method.
+ *
+ * @param menu The context menu to populate
+ */
+ public void createContextMenu(ContextMenu menu) {
+ ContextMenuInfo menuInfo = getContextMenuInfo();
+
+ // Sets the current menu info so all items added to menu will have
+ // my extra info set.
+ ((MenuBuilder)menu).setCurrentMenuInfo(menuInfo);
+
+ onCreateContextMenu(menu);
+ if (mOnCreateContextMenuListener != null) {
+ mOnCreateContextMenuListener.onCreateContextMenu(menu, this, menuInfo);
+ }
+
+ // Clear the extra information so subsequent items that aren't mine don't
+ // have my extra info.
+ ((MenuBuilder)menu).setCurrentMenuInfo(null);
+
+ if (mParent != null) {
+ mParent.createContextMenu(menu);
+ }
+ }
+
+ /**
+ * Views should implement this if they have extra information to associate
+ * with the context menu. The return result is supplied as a parameter to
+ * the {@link OnCreateContextMenuListener#onCreateContextMenu(ContextMenu, View, ContextMenuInfo)}
+ * callback.
+ *
+ * @return Extra information about the item for which the context menu
+ * should be shown. This information will vary across different
+ * subclasses of View.
+ */
+ protected ContextMenuInfo getContextMenuInfo() {
+ return null;
+ }
+
+ /**
+ * Views should implement this if the view itself is going to add items to
+ * the context menu.
+ *
+ * @param menu the context menu to populate
+ */
+ protected void onCreateContextMenu(ContextMenu menu) {
+ }
+
+ /**
+ * Implement this method to handle trackball motion events. The
+ * <em>relative</em> movement of the trackball since the last event
+ * can be retrieve with {@link MotionEvent#getX MotionEvent.getX()} and
+ * {@link MotionEvent#getY MotionEvent.getY()}. These are normalized so
+ * that a movement of 1 corresponds to the user pressing one DPAD key (so
+ * they will often be fractional values, representing the more fine-grained
+ * movement information available from a trackball).
+ *
+ * @param event The motion event.
+ * @return True if the event was handled, false otherwise.
+ */
+ public boolean onTrackballEvent(MotionEvent event) {
+ return false;
+ }
+
+ /**
+ * Implement this method to handle touch screen motion events.
+ *
+ * @param event The motion event.
+ * @return True if the event was handled, false otherwise.
+ */
+ public boolean onTouchEvent(MotionEvent event) {
+ final int viewFlags = mViewFlags;
+
+ if ((viewFlags & ENABLED_MASK) == DISABLED) {
+ // A disabled view that is clickable still consumes the touch
+ // events, it just doesn't respond to them.
+ return (((viewFlags & CLICKABLE) == CLICKABLE ||
+ (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
+ }
+
+ if (mTouchDelegate != null) {
+ if (mTouchDelegate.onTouchEvent(event)) {
+ return true;
+ }
+ }
+
+ if (((viewFlags & CLICKABLE) == CLICKABLE ||
+ (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_UP:
+ if ((mPrivateFlags & PRESSED) != 0) {
+ // take focus if we don't have it already and we should in
+ // touch mode.
+ boolean focusTaken = false;
+ if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
+ focusTaken = requestFocus();
+ }
+
+ if (!mHasPerformedLongPress) {
+ // This is a tap, so remove the longpress check
+ if (mPendingCheckForLongPress != null) {
+ removeCallbacks(mPendingCheckForLongPress);
+ }
+
+ // Only perform take click actions if we were in the pressed state
+ if (!focusTaken) {
+ performClick();
+ }
+ }
+
+ final UnsetPressedState unsetPressedState = new UnsetPressedState();
+ if (!post(unsetPressedState)) {
+ // If the post failed, unpress right now
+ unsetPressedState.run();
+ }
+ }
+ break;
+
+ case MotionEvent.ACTION_DOWN:
+ mPrivateFlags |= PRESSED;
+ refreshDrawableState();
+ if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
+ postCheckForLongClick();
+ }
+ break;
+
+ case MotionEvent.ACTION_CANCEL:
+ mPrivateFlags &= ~PRESSED;
+ refreshDrawableState();
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ final int x = (int) event.getX();
+ final int y = (int) event.getY();
+
+ // Be lenient about moving outside of buttons
+ int slop = ViewConfiguration.getTouchSlop();
+ if ((x < 0 - slop) || (x >= getWidth() + slop) ||
+ (y < 0 - slop) || (y >= getHeight() + slop)) {
+ // Outside button
+ if ((mPrivateFlags & PRESSED) != 0) {
+ // Remove any future long press checks
+ if (mPendingCheckForLongPress != null) {
+ removeCallbacks(mPendingCheckForLongPress);
+ }
+
+ // Need to switch from pressed to not pressed
+ mPrivateFlags &= ~PRESSED;
+ refreshDrawableState();
+ }
+ } else {
+ // Inside button
+ if ((mPrivateFlags & PRESSED) == 0) {
+ // Need to switch from not pressed to pressed
+ mPrivateFlags |= PRESSED;
+ refreshDrawableState();
+ }
+ }
+ break;
+ }
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Cancels a pending long press. Your subclass can use this if you
+ * want the context menu to come up if the user presses and holds
+ * at the same place, but you don't want it to come up if they press
+ * and then move around enough to cause scrolling.
+ */
+ public void cancelLongPress() {
+ if (mPendingCheckForLongPress != null) {
+ removeCallbacks(mPendingCheckForLongPress);
+ }
+ }
+
+ /**
+ * Sets the TouchDelegate for this View.
+ */
+ public void setTouchDelegate(TouchDelegate delegate) {
+ mTouchDelegate = delegate;
+ }
+
+ /**
+ * Gets the TouchDelegate for this View.
+ */
+ public TouchDelegate getTouchDelegate() {
+ return mTouchDelegate;
+ }
+
+ /**
+ * Set flags controlling behavior of this view.
+ *
+ * @param flags Constant indicating the value which should be set
+ * @param mask Constant indicating the bit range that should be changed
+ */
+ void setFlags(int flags, int mask) {
+ int old = mViewFlags;
+ mViewFlags = (mViewFlags & ~mask) | (flags & mask);
+
+ int changed = mViewFlags ^ old;
+ if (changed == 0) {
+ return;
+ }
+ int privateFlags = mPrivateFlags;
+
+ /* Check if the FOCUSABLE bit has changed */
+ if (((changed & FOCUSABLE_MASK) != 0) &&
+ ((privateFlags & HAS_BOUNDS) !=0)) {
+ if (((old & FOCUSABLE_MASK) == FOCUSABLE)
+ && ((privateFlags & FOCUSED) != 0)) {
+ /* Give up focus if we are no longer focusable */
+ clearFocus();
+ } else if (((old & FOCUSABLE_MASK) == NOT_FOCUSABLE)
+ && ((privateFlags & FOCUSED) == 0)) {
+ /*
+ * Tell the view system that we are now available to take focus
+ * if no one else already has it.
+ */
+ if (mParent != null) mParent.focusableViewAvailable(this);
+ }
+ }
+
+ if ((flags & VISIBILITY_MASK) == VISIBLE) {
+ if ((changed & VISIBILITY_MASK) != 0) {
+ /*
+ * If this view is becoming visible, set the DRAWN flag so that
+ * the next invalidate() will not be skipped.
+ */
+ mPrivateFlags |= DRAWN;
+
+ needGlobalAttributesUpdate(true);
+
+ // a view becoming visible is worth notifying the parent
+ // about in case nothing has focus. even if this specific view
+ // isn't focusable, it may contain something that is, so let
+ // the root view try to give this focus if nothing else does.
+ if ((mParent != null) && (mBottom > mTop) && (mRight > mLeft)) {
+ mParent.focusableViewAvailable(this);
+ }
+ }
+ }
+
+ /* Check if the GONE bit has changed */
+ if ((changed & GONE) != 0) {
+ needGlobalAttributesUpdate(false);
+ requestLayout();
+ invalidate();
+
+ if (((mViewFlags & VISIBILITY_MASK) == GONE) && hasFocus()) {
+ clearFocus();
+ }
+ }
+
+ /* Check if the VISIBLE bit has changed */
+ if ((changed & INVISIBLE) != 0) {
+ needGlobalAttributesUpdate(false);
+ invalidate();
+
+ if (((mViewFlags & VISIBILITY_MASK) == INVISIBLE) && hasFocus()) {
+ // root view becoming invisible shouldn't clear focus
+ if (getRootView() != this) {
+ clearFocus();
+ }
+ }
+ }
+
+ if ((changed & WILL_NOT_CACHE_DRAWING) != 0) {
+ if (mDrawingCache != null) {
+ mDrawingCache.recycle();
+ }
+ mDrawingCache = null;
+ }
+
+ if ((changed & DRAWING_CACHE_ENABLED) != 0) {
+ if (mDrawingCache != null) {
+ mDrawingCache.recycle();
+ }
+ mDrawingCache = null;
+ mPrivateFlags &= ~DRAWING_CACHE_VALID;
+ }
+
+ if ((changed & DRAWING_CACHE_QUALITY_MASK) != 0) {
+ if (mDrawingCache != null) {
+ mDrawingCache.recycle();
+ }
+ mDrawingCache = null;
+ mPrivateFlags &= ~DRAWING_CACHE_VALID;
+ }
+
+ if ((changed & DRAW_MASK) != 0) {
+ if ((mViewFlags & WILL_NOT_DRAW) != 0) {
+ if (mBGDrawable != null) {
+ mPrivateFlags &= ~SKIP_DRAW;
+ mPrivateFlags |= ONLY_DRAWS_BACKGROUND;
+ } else {
+ mPrivateFlags |= SKIP_DRAW;
+ }
+ } else {
+ mPrivateFlags &= ~SKIP_DRAW;
+ }
+ requestLayout();
+ invalidate();
+ }
+
+ if ((changed & KEEP_SCREEN_ON) != 0) {
+ if (mParent != null) {
+ mParent.recomputeViewAttributes(this);
+ }
+ }
+ }
+
+ /**
+ * Change the view's z order in the tree, so it's on top of other sibling
+ * views
+ */
+ public void bringToFront() {
+ if (mParent != null) {
+ mParent.bringChildToFront(this);
+ }
+ }
+
+ /**
+ * This is called in response to an internal scroll in this view (i.e., the
+ * view scrolled its own contents). This is typically as a result of
+ * {@link #scrollBy(int, int)} or {@link #scrollTo(int, int)} having been
+ * called.
+ *
+ * @param l Current horizontal scroll origin.
+ * @param t Current vertical scroll origin.
+ * @param oldl Previous horizontal scroll origin.
+ * @param oldt Previous vertical scroll origin.
+ */
+ protected void onScrollChanged(int l, int t, int oldl, int oldt) {
+ mBackgroundSizeChanged = true;
+ }
+
+ /**
+ * This is called during layout when the size of this view has changed. If
+ * you were just added to the view hierarchy, you're called with the old
+ * values of 0.
+ *
+ * @param w Current width of this view.
+ * @param h Current height of this view.
+ * @param oldw Old width of this view.
+ * @param oldh Old height of this view.
+ */
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ }
+
+ /**
+ * Called by draw to draw the child views. This may be overridden
+ * by derived classes to gain control just before its children are drawn
+ * (but after its own view has been drawn).
+ * @param canvas the canvas on which to draw the view
+ */
+ protected void dispatchDraw(Canvas canvas) {
+ }
+
+ /**
+ * Gets the parent of this view. Note that the parent is a
+ * ViewParent and not necessarily a View.
+ *
+ * @return Parent of this view.
+ */
+ public final ViewParent getParent() {
+ return mParent;
+ }
+
+ /**
+ * Return the scrolled left position of this view. This is the left edge of
+ * the displayed part of your view. You do not need to draw any pixels
+ * farther left, since those are outside of the frame of your view on
+ * screen.
+ *
+ * @return The left edge of the displayed part of your view, in pixels.
+ */
+ public final int getScrollX() {
+ return mScrollX;
+ }
+
+ /**
+ * Return the scrolled top position of this view. This is the top edge of
+ * the displayed part of your view. You do not need to draw any pixels above
+ * it, since those are outside of the frame of your view on screen.
+ *
+ * @return The top edge of the displayed part of your view, in pixels.
+ */
+ public final int getScrollY() {
+ return mScrollY;
+ }
+
+ /**
+ * Return the width of the your view.
+ *
+ * @return The width of your view, in pixels.
+ */
+ @ViewDebug.ExportedProperty
+ public final int getWidth() {
+ return mRight - mLeft;
+ }
+
+ /**
+ * Return the height of your view.
+ *
+ * @return The height of your view, in pixels.
+ */
+ @ViewDebug.ExportedProperty
+ public final int getHeight() {
+ return mBottom - mTop;
+ }
+
+ /**
+ * Return the visible drawing bounds of your view. Fills in the output
+ * rectangle with the values from getScrollX(), getScrollY(),
+ * getWidth(), and getHeight().
+ *
+ * @param outRect The (scrolled) drawing bounds of the view.
+ */
+ public void getDrawingRect(Rect outRect) {
+ outRect.left = mScrollX;
+ outRect.top = mScrollY;
+ outRect.right = mScrollX + (mRight - mLeft);
+ outRect.bottom = mScrollY + (mBottom - mTop);
+ }
+
+ /**
+ * The width of this view as measured in the most recent call to measure().
+ * This should be used during measurement and layout calculations only. Use
+ * {@link #getWidth()} to see how wide a view is after layout.
+ *
+ * @return The measured width of this view.
+ */
+ public final int getMeasuredWidth() {
+ return mMeasuredWidth;
+ }
+
+ /**
+ * The height of this view as measured in the most recent call to measure().
+ * This should be used during measurement and layout calculations only. Use
+ * {@link #getHeight()} to see how tall a view is after layout.
+ *
+ * @return The measured height of this view.
+ */
+ public final int getMeasuredHeight() {
+ return mMeasuredHeight;
+ }
+
+ /**
+ * Top position of this view relative to its parent.
+ *
+ * @return The top of this view, in pixels.
+ */
+ public final int getTop() {
+ return mTop;
+ }
+
+ /**
+ * Bottom position of this view relative to its parent.
+ *
+ * @return The bottom of this view, in pixels.
+ */
+ public final int getBottom() {
+ return mBottom;
+ }
+
+ /**
+ * Left position of this view relative to its parent.
+ *
+ * @return The left edge of this view, in pixels.
+ */
+ public final int getLeft() {
+ return mLeft;
+ }
+
+ /**
+ * Right position of this view relative to its parent.
+ *
+ * @return The right edge of this view, in pixels.
+ */
+ public final int getRight() {
+ return mRight;
+ }
+
+ /**
+ * Hit rectangle in parent's coordinates
+ *
+ * @param outRect The hit rectangle of the view.
+ */
+ public void getHitRect(Rect outRect) {
+ outRect.set(mLeft, mTop, mRight, mBottom);
+ }
+
+ /**
+ * When a view has focus and the user navigates away from it, the next view is searched for
+ * starting from the rectangle filled in by this method.
+ *
+ * By default, the rectange is the {@link #getDrawingRect})of the view. However, if your
+ * view maintains some idea of internal selection, such as a cursor, or a selected row
+ * or column, you should override this method and fill in a more specific rectangle.
+ *
+ * @param r The rectangle to fill in, in this view's coordinates.
+ */
+ public void getFocusedRect(Rect r) {
+ getDrawingRect(r);
+ }
+
+ /**
+ * If some part of this view is not clipped by any of its parents, then
+ * return that area in r in global (root) coordinates. To convert r to local
+ * coordinates, offset it by -globalOffset (e.g. r.offset(-globalOffset.x,
+ * -globalOffset.y)) If the view is completely clipped or translated out,
+ * return false.
+ *
+ * @param r If true is returned, r holds the global coordinates of the
+ * visible portion of this view.
+ * @param globalOffset If true is returned, globalOffset holds the dx,dy
+ * between this view and its root. globalOffet may be null.
+ * @return true if r is non-empty (i.e. part of the view is visible at the
+ * root level.
+ */
+ public boolean getGlobalVisibleRect(Rect r, Point globalOffset) {
+ int width = mRight - mLeft;
+ int height = mBottom - mTop;
+ if (width > 0 && height > 0) {
+ r.set(0, 0, width, height);
+ if (globalOffset != null) {
+ globalOffset.set(-mScrollX, -mScrollY);
+ }
+ return mParent == null || mParent.getChildVisibleRect(this, r, globalOffset);
+ }
+ return false;
+ }
+
+ public final boolean getGlobalVisibleRect(Rect r) {
+ return getGlobalVisibleRect(r, null);
+ }
+
+ public final boolean getLocalVisibleRect(Rect r) {
+ Point offset = new Point();
+ if (getGlobalVisibleRect(r, offset)) {
+ r.offset(-offset.x, -offset.y); // make r local
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Offset this view's vertical location by the specified number of pixels.
+ *
+ * @param offset the number of pixels to offset the view by
+ */
+ public void offsetTopAndBottom(int offset) {
+ mTop += offset;
+ mBottom += offset;
+ }
+
+ /**
+ * Offset this view's horizontal location by the specified amount of pixels.
+ *
+ * @param offset the numer of pixels to offset the view by
+ */
+ public void offsetLeftAndRight(int offset) {
+ mLeft += offset;
+ mRight += offset;
+ }
+
+ /**
+ * Get the LayoutParams associated with this view. All views should have
+ * layout parameters. These supply parameters to the <i>parent</i> of this
+ * view specifying how it should be arranged. There are many subclasses of
+ * ViewGroup.LayoutParams, and these correspond to the different subclasses
+ * of ViewGroup that are responsible for arranging their children.
+ * @return The LayoutParams associated with this view
+ */
+ @ViewDebug.ExportedProperty(deepExport = true, prefix = "layout_")
+ public ViewGroup.LayoutParams getLayoutParams() {
+ return mLayoutParams;
+ }
+
+ /**
+ * Set the layout parameters associated with this view. These supply
+ * parameters to the <i>parent</i> of this view specifying how it should be
+ * arranged. There are many subclasses of ViewGroup.LayoutParams, and these
+ * correspond to the different subclasses of ViewGroup that are responsible
+ * for arranging their children.
+ *
+ * @param params the layout parameters for this view
+ */
+ public void setLayoutParams(ViewGroup.LayoutParams params) {
+ if (params == null) {
+ throw new NullPointerException("params == null");
+ }
+ mLayoutParams = params;
+ requestLayout();
+ }
+
+ /**
+ * Set the scrolled position of your view. This will cause a call to
+ * {@link #onScrollChanged(int, int, int, int)} and the view will be
+ * invalidated.
+ * @param x the x position to scroll to
+ * @param y the y position to scroll to
+ */
+ public void scrollTo(int x, int y) {
+ if (mScrollX != x || mScrollY != y) {
+ int oldX = mScrollX;
+ int oldY = mScrollY;
+ mScrollX = x;
+ mScrollY = y;
+ onScrollChanged(mScrollX, mScrollY, oldX, oldY);
+ invalidate();
+ }
+ }
+
+ /**
+ * Move the scrolled position of your view. This will cause a call to
+ * {@link #onScrollChanged(int, int, int, int)} and the view will be
+ * invalidated.
+ * @param x the amount of pixels to scroll by horizontally
+ * @param y the amount of pixels to scroll by vertically
+ */
+ public void scrollBy(int x, int y) {
+ scrollTo(mScrollX + x, mScrollY + y);
+ }
+
+ /**
+ * Mark the the area defined by dirty as needing to be drawn. If the view is
+ * visible, {@link #onDraw} will be called at some point in the future.
+ * This must be called from a UI thread. To call from a non-UI thread, call
+ * {@link #postInvalidate()}.
+ *
+ * WARNING: This method is destructive to dirty.
+ * @param dirty the rectangle representing the bounds of the dirty region
+ */
+ public void invalidate(Rect dirty) {
+ if (ViewDebug.TRACE_HIERARCHY) {
+ ViewDebug.trace(this, ViewDebug.HierarchyTraceType.INVALIDATE);
+ }
+
+ if ((mPrivateFlags & (DRAWN | HAS_BOUNDS)) == (DRAWN | HAS_BOUNDS)) {
+ mPrivateFlags &= ~DRAWING_CACHE_VALID;
+ ViewParent p = mParent;
+ if (p != null) {
+ final int scrollX = mScrollX;
+ final int scrollY = mScrollY;
+ mTempRect.set(dirty.left - scrollX, dirty.top - scrollY,
+ dirty.right - scrollX, dirty.bottom - scrollY);
+ p.invalidateChild(this, mTempRect);
+ }
+ }
+ }
+
+ /**
+ * Mark the the area defined by the rect (l,t,r,b) as needing to be drawn.
+ * The coordinates of the dirty rect are relative to the view.
+ * If the view is visible, {@link #onDraw} will be called at some point
+ * in the future. This must be called from a UI thread. To call
+ * from a non-UI thread, call {@link #postInvalidate()}.
+ * @param l the left position of the dirty region
+ * @param t the top position of the dirty region
+ * @param r the right position of the dirty region
+ * @param b the bottom position of the dirty region
+ */
+ public void invalidate(int l, int t, int r, int b) {
+ if (ViewDebug.TRACE_HIERARCHY) {
+ ViewDebug.trace(this, ViewDebug.HierarchyTraceType.INVALIDATE);
+ }
+
+ if ((mPrivateFlags & (DRAWN | HAS_BOUNDS)) == (DRAWN | HAS_BOUNDS)) {
+ mPrivateFlags &= ~DRAWING_CACHE_VALID;
+ ViewParent p = mParent;
+ if (p != null && l < r && t < b) {
+ final int scrollX = mScrollX;
+ final int scrollY = mScrollY;
+ mTempRect.set(l - scrollX, t - scrollY, r - scrollX, b - scrollY);
+ p.invalidateChild(this, mTempRect);
+ }
+ }
+ }
+
+ /**
+ * Invalidate the whole view. If the view is visible, {@link #onDraw} will
+ * be called at some point in the future. This must be called from a
+ * UI thread. To call from a non-UI thread, call {@link #postInvalidate()}.
+ */
+ public void invalidate() {
+ if (ViewDebug.TRACE_HIERARCHY) {
+ ViewDebug.trace(this, ViewDebug.HierarchyTraceType.INVALIDATE);
+ }
+
+ if ((mPrivateFlags & (DRAWN | HAS_BOUNDS)) == (DRAWN | HAS_BOUNDS)) {
+ mPrivateFlags &= ~DRAWN & ~DRAWING_CACHE_VALID;
+ ViewParent p = mParent;
+ if (p != null) {
+ mTempRect.set(0, 0, mRight - mLeft, mBottom - mTop);
+ // Don't call invalidate -- we don't want to internally scroll
+ // our own bounds
+ p.invalidateChild(this, mTempRect);
+ }
+ }
+ }
+
+ /**
+ * @return A handler associated with the thread running the View. This
+ * handler can be used to pump events in the UI events queue.
+ */
+ protected Handler getHandler() {
+ if (mAttachInfo != null) {
+ return mAttachInfo.mHandler;
+ }
+ return null;
+ }
+
+ /**
+ * Causes the Runnable to be added to the message queue.
+ * The runnable will be run on the user interface thread.
+ *
+ * @param action The Runnable that will be executed.
+ *
+ * @return Returns true if the Runnable was successfully placed in to the
+ * message queue. Returns false on failure, usually because the
+ * looper processing the message queue is exiting.
+ */
+ public boolean post(Runnable action) {
+ Handler handler;
+ if (mAttachInfo != null) {
+ handler = mAttachInfo.mHandler;
+ } else {
+ handler = ViewRoot.sUiThreads.get();
+ if (handler == null) {
+ // Assume that post will succeed later
+ ViewRoot.sRunQueue.post(action);
+ return true;
+ }
+ }
+
+ return handler.post(action);
+ }
+
+ /**
+ * Causes the Runnable to be added to the message queue, to be run
+ * after the specified amount of time elapses.
+ * The runnable will be run on the user interface thread.
+ *
+ * @param action The Runnable that will be executed.
+ * @param delayMillis The delay (in milliseconds) until the Runnable
+ * will be executed.
+ *
+ * @return true if the Runnable was successfully placed in to the
+ * message queue. Returns false on failure, usually because the
+ * looper processing the message queue is exiting. Note that a
+ * result of true does not mean the Runnable will be processed --
+ * if the looper is quit before the delivery time of the message
+ * occurs then the message will be dropped.
+ */
+ public boolean postDelayed(Runnable action, long delayMillis) {
+ Handler handler;
+ if (mAttachInfo != null) {
+ handler = mAttachInfo.mHandler;
+ } else {
+ handler = ViewRoot.sUiThreads.get();
+ if (handler == null) {
+ // Assume that post will succeed later
+ ViewRoot.sRunQueue.postDelayed(action, delayMillis);
+ return true;
+ }
+ }
+
+ return handler.postDelayed(action, delayMillis);
+ }
+
+ /**
+ * Removes the specified Runnable from the message queue.
+ *
+ * @param action The Runnable to remove from the message handling queue
+ *
+ * @return true if this view could ask the Handler to remove the Runnable,
+ * false otherwise. When the returned value is true, the Runnable
+ * may or may not have been actually removed from the message queue
+ * (for instance, if the Runnable was not in the queue already.)
+ */
+ public boolean removeCallbacks(Runnable action) {
+ Handler handler;
+ if (mAttachInfo != null) {
+ handler = mAttachInfo.mHandler;
+ } else {
+ handler = ViewRoot.sUiThreads.get();
+ if (handler == null) {
+ // Assume that post will succeed later
+ ViewRoot.sRunQueue.removeCallbacks(action);
+ return true;
+ }
+ }
+
+ handler.removeCallbacks(action);
+ return true;
+ }
+
+ /**
+ * Cause an invalidate to happen on a subsequent cycle through the event loop.
+ * Use this to invalidate the View from a non-UI thread.
+ *
+ * @see #invalidate()
+ */
+ public void postInvalidate() {
+ // We try only with the AttachInfo because there's no point in invalidating
+ // if we are not attached to our window
+ if (mAttachInfo != null) {
+ Message msg = Message.obtain();
+ msg.what = AttachInfo.INVALIDATE_MSG;
+ msg.obj = this;
+ mAttachInfo.mHandler.sendMessage(msg);
+ }
+ }
+
+ /**
+ * Cause an invalidate of the specified area to happen on a subsequent cycle
+ * through the event loop. Use this to invalidate the View from a non-UI thread.
+ *
+ * @param left The left coordinate of the rectangle to invalidate.
+ * @param top The top coordinate of the rectangle to invalidate.
+ * @param right The right coordinate of the rectangle to invalidate.
+ * @param bottom The bottom coordinate of the rectangle to invalidate.
+ *
+ * @see #invalidate(int, int, int, int)
+ * @see #invalidate(Rect)
+ */
+ public void postInvalidate(int left, int top, int right, int bottom) {
+ // We try only with the AttachInfo because there's no point in invalidating
+ // if we are not attached to our window
+ if (mAttachInfo != null) {
+ Message msg = Message.obtain();
+ msg.what = AttachInfo.INVALIDATE_RECT_MSG;
+ msg.obj = this;
+ msg.arg1 = (left << 16) | (top & 0xFFFF);
+ msg.arg2 = (right << 16) | (bottom & 0xFFFF);
+ mAttachInfo.mHandler.sendMessage(msg);
+ }
+ }
+
+ /**
+ * Cause an invalidate to happen on a subsequent cycle through the event
+ * loop. Waits for the specified amount of time.
+ *
+ * @param delayMilliseconds the duration in milliseconds to delay the
+ * invalidation by
+ */
+ public void postInvalidateDelayed(long delayMilliseconds) {
+ // We try only with the AttachInfo because there's no point in invalidating
+ // if we are not attached to our window
+ if (mAttachInfo != null) {
+ Message msg = Message.obtain();
+ msg.what = AttachInfo.INVALIDATE_MSG;
+ msg.obj = this;
+ mAttachInfo.mHandler.sendMessageDelayed(msg, delayMilliseconds);
+ }
+ }
+
+ /**
+ * Cause an invalidate of the specified area to happen on a subsequent cycle
+ * through the event loop. Waits for the specified amount of time.
+ *
+ * @param delayMilliseconds the duration in milliseconds to delay the
+ * invalidation by
+ * @param left The left coordinate of the rectangle to invalidate.
+ * @param top The top coordinate of the rectangle to invalidate.
+ * @param right The right coordinate of the rectangle to invalidate.
+ * @param bottom The bottom coordinate of the rectangle to invalidate.
+ */
+ public void postInvalidateDelayed(long delayMilliseconds, int left, int top
+ , int right, int bottom) {
+ // We try only with the AttachInfo because there's no point in invalidating
+ // if we are not attached to our window
+ if (mAttachInfo != null) {
+ Message msg = Message.obtain();
+ msg.what = AttachInfo.INVALIDATE_RECT_MSG;
+ msg.obj = this;
+ msg.arg1 = (left << 16) | (top & 0xFFFF);
+ msg.arg2 = (right << 16) | (bottom & 0xFFFF);
+ mAttachInfo.mHandler.sendMessageDelayed(msg, delayMilliseconds);
+ }
+ }
+
+ /**
+ * Called by a parent to request that a child update its values for mScrollX
+ * and mScrollY if necessary. This will typically be done if the child is
+ * animating a scroll using a {@link android.widget.Scroller Scroller}
+ * object.
+ */
+ public void computeScroll() {
+ }
+
+ /**
+ * <p>Indicate whether the horizontal edges are faded when the view is
+ * scrolled horizontally.</p>
+ *
+ * @return true if the horizontal edges should are faded on scroll, false
+ * otherwise
+ *
+ * @see #setHorizontalFadingEdgeEnabled(boolean)
+ * @attr ref android.R.styleable#View_fadingEdge
+ */
+ public boolean isHorizontalFadingEdgeEnabled() {
+ return (mViewFlags & FADING_EDGE_HORIZONTAL) == FADING_EDGE_HORIZONTAL;
+ }
+
+ /**
+ * <p>Define whether the horizontal edges should be faded when this view
+ * is scrolled horizontally.</p>
+ *
+ * @param horizontalFadingEdgeEnabled true if the horizontal edges should
+ * be faded when the view is scrolled
+ * horizontally
+ *
+ * @see #isHorizontalFadingEdgeEnabled()
+ * @attr ref android.R.styleable#View_fadingEdge
+ */
+ public void setHorizontalFadingEdgeEnabled(boolean horizontalFadingEdgeEnabled) {
+ if (isHorizontalFadingEdgeEnabled() != horizontalFadingEdgeEnabled) {
+ if (horizontalFadingEdgeEnabled) {
+ initScrollCache();
+ }
+
+ mViewFlags ^= FADING_EDGE_HORIZONTAL;
+ }
+ }
+
+ /**
+ * <p>Indicate whether the vertical edges are faded when the view is
+ * scrolled horizontally.</p>
+ *
+ * @return true if the vertical edges should are faded on scroll, false
+ * otherwise
+ *
+ * @see #setVerticalFadingEdgeEnabled(boolean)
+ * @attr ref android.R.styleable#View_fadingEdge
+ */
+ public boolean isVerticalFadingEdgeEnabled() {
+ return (mViewFlags & FADING_EDGE_VERTICAL) == FADING_EDGE_VERTICAL;
+ }
+
+ /**
+ * <p>Define whether the vertical edges should be faded when this view
+ * is scrolled vertically.</p>
+ *
+ * @param verticalFadingEdgeEnabled true if the vertical edges should
+ * be faded when the view is scrolled
+ * vertically
+ *
+ * @see #isVerticalFadingEdgeEnabled()
+ * @attr ref android.R.styleable#View_fadingEdge
+ */
+ public void setVerticalFadingEdgeEnabled(boolean verticalFadingEdgeEnabled) {
+ if (isVerticalFadingEdgeEnabled() != verticalFadingEdgeEnabled) {
+ if (verticalFadingEdgeEnabled) {
+ initScrollCache();
+ }
+
+ mViewFlags ^= FADING_EDGE_VERTICAL;
+ }
+ }
+
+ /**
+ * Returns the strength, or intensity, of the top faded edge. The strength is
+ * a value between 0.0 (no fade) and 1.0 (full fade). The default implementation
+ * returns 0.0 or 1.0 but no value in between.
+ *
+ * Subclasses should override this method to provide a smoother fade transition
+ * when scrolling occurs.
+ *
+ * @return the intensity of the top fade as a float between 0.0f and 1.0f
+ */
+ protected float getTopFadingEdgeStrength() {
+ return computeVerticalScrollOffset() > 0 ? 1.0f : 0.0f;
+ }
+
+ /**
+ * Returns the strength, or intensity, of the bottom faded edge. The strength is
+ * a value between 0.0 (no fade) and 1.0 (full fade). The default implementation
+ * returns 0.0 or 1.0 but no value in between.
+ *
+ * Subclasses should override this method to provide a smoother fade transition
+ * when scrolling occurs.
+ *
+ * @return the intensity of the bottom fade as a float between 0.0f and 1.0f
+ */
+ protected float getBottomFadingEdgeStrength() {
+ return computeVerticalScrollOffset() + computeVerticalScrollExtent() <
+ computeVerticalScrollRange() ? 1.0f : 0.0f;
+ }
+
+ /**
+ * Returns the strength, or intensity, of the left faded edge. The strength is
+ * a value between 0.0 (no fade) and 1.0 (full fade). The default implementation
+ * returns 0.0 or 1.0 but no value in between.
+ *
+ * Subclasses should override this method to provide a smoother fade transition
+ * when scrolling occurs.
+ *
+ * @return the intensity of the left fade as a float between 0.0f and 1.0f
+ */
+ protected float getLeftFadingEdgeStrength() {
+ return computeHorizontalScrollOffset() > 0 ? 1.0f : 0.0f;
+ }
+
+ /**
+ * Returns the strength, or intensity, of the right faded edge. The strength is
+ * a value between 0.0 (no fade) and 1.0 (full fade). The default implementation
+ * returns 0.0 or 1.0 but no value in between.
+ *
+ * Subclasses should override this method to provide a smoother fade transition
+ * when scrolling occurs.
+ *
+ * @return the intensity of the right fade as a float between 0.0f and 1.0f
+ */
+ protected float getRightFadingEdgeStrength() {
+ return computeHorizontalScrollOffset() + computeHorizontalScrollExtent() <
+ computeHorizontalScrollRange() ? 1.0f : 0.0f;
+ }
+
+ /**
+ * <p>Indicate whether the horizontal scrollbar should be drawn or not. The
+ * scrollbar is not drawn by default.</p>
+ *
+ * @return true if the horizontal scrollbar should be painted, false
+ * otherwise
+ *
+ * @see #setHorizontalScrollBarEnabled(boolean)
+ */
+ public boolean isHorizontalScrollBarEnabled() {
+ return (mViewFlags & SCROLLBARS_HORIZONTAL) == SCROLLBARS_HORIZONTAL;
+ }
+
+ /**
+ * <p>Define whether the horizontal scrollbar should be drawn or not. The
+ * scrollbar is not drawn by default.</p>
+ *
+ * @param horizontalScrollBarEnabled true if the horizontal scrollbar should
+ * be painted
+ *
+ * @see #isHorizontalScrollBarEnabled()
+ */
+ public void setHorizontalScrollBarEnabled(boolean horizontalScrollBarEnabled) {
+ if (isHorizontalScrollBarEnabled() != horizontalScrollBarEnabled) {
+ mViewFlags ^= SCROLLBARS_HORIZONTAL;
+ recomputePadding();
+ }
+ }
+
+ /**
+ * <p>Indicate whether the vertical scrollbar should be drawn or not. The
+ * scrollbar is not drawn by default.</p>
+ *
+ * @return true if the vertical scrollbar should be painted, false
+ * otherwise
+ *
+ * @see #setVerticalScrollBarEnabled(boolean)
+ */
+ public boolean isVerticalScrollBarEnabled() {
+ return (mViewFlags & SCROLLBARS_VERTICAL) == SCROLLBARS_VERTICAL;
+ }
+
+ /**
+ * <p>Define whether the vertical scrollbar should be drawn or not. The
+ * scrollbar is not drawn by default.</p>
+ *
+ * @param verticalScrollBarEnabled true if the vertical scrollbar should
+ * be painted
+ *
+ * @see #isVerticalScrollBarEnabled()
+ */
+ public void setVerticalScrollBarEnabled(boolean verticalScrollBarEnabled) {
+ if (isVerticalScrollBarEnabled() != verticalScrollBarEnabled) {
+ mViewFlags ^= SCROLLBARS_VERTICAL;
+ recomputePadding();
+ }
+ }
+
+ private void recomputePadding() {
+ setPadding(mPaddingLeft, mPaddingTop, mUserPaddingRight, mUserPaddingBottom);
+ }
+
+ /**
+ * <p>Specify the style of the scrollbars. The scrollbars can be overlaid or
+ * inset. When inset, they add to the padding of the view. And the scrollbars
+ * can be drawn inside the padding area or on the edge of the view. For example,
+ * if a view has a background drawable and you want to draw the scrollbars
+ * inside the padding specified by the drawable, you can use
+ * SCROLLBARS_INSIDE_OVERLAY or SCROLLBARS_INSIDE_INSET. If you want them to
+ * appear at the edge of the view, ignoring the padding, then you can use
+ * SCROLLBARS_OUTSIDE_OVERLAY or SCROLLBARS_OUTSIDE_INSET.</p>
+ * @param style the style of the scrollbars. Should be one of
+ * SCROLLBARS_INSIDE_OVERLAY, SCROLLBARS_INSIDE_INSET,
+ * SCROLLBARS_OUTSIDE_OVERLAY or SCROLLBARS_OUTSIDE_INSET.
+ * @see #SCROLLBARS_INSIDE_OVERLAY
+ * @see #SCROLLBARS_INSIDE_INSET
+ * @see #SCROLLBARS_OUTSIDE_OVERLAY
+ * @see #SCROLLBARS_OUTSIDE_INSET
+ */
+ public void setScrollBarStyle(int style) {
+ if (style != (mViewFlags & SCROLLBARS_STYLE_MASK)) {
+ mViewFlags = (mViewFlags & ~SCROLLBARS_STYLE_MASK) | (style & SCROLLBARS_STYLE_MASK);
+ recomputePadding();
+ }
+ }
+
+ /**
+ * <p>Returns the current scrollbar style.</p>
+ * @return the current scrollbar style
+ * @see #SCROLLBARS_INSIDE_OVERLAY
+ * @see #SCROLLBARS_INSIDE_INSET
+ * @see #SCROLLBARS_OUTSIDE_OVERLAY
+ * @see #SCROLLBARS_OUTSIDE_INSET
+ */
+ public int getScrollBarStyle() {
+ return mViewFlags & SCROLLBARS_STYLE_MASK;
+ }
+
+ /**
+ * <p>Compute the horizontal range that the horizontal scrollbar
+ * represents.</p>
+ *
+ * <p>The range is expressed in arbitrary units that must be the same as the
+ * units used by {@link #computeHorizontalScrollExtent()} and
+ * {@link #computeHorizontalScrollOffset()}.</p>
+ *
+ * <p>The default range is the drawing width of this view.</p>
+ *
+ * @return the total horizontal range represented by the horizontal
+ * scrollbar
+ *
+ * @see #computeHorizontalScrollExtent()
+ * @see #computeHorizontalScrollOffset()
+ * @see android.widget.ScrollBarDrawable
+ */
+ protected int computeHorizontalScrollRange() {
+ return getWidth();
+ }
+
+ /**
+ * <p>Compute the horizontal offset of the horizontal scrollbar's thumb
+ * within the horizontal range. This value is used to compute the position
+ * of the thumb within the scrollbar's track.</p>
+ *
+ * <p>The range is expressed in arbitrary units that must be the same as the
+ * units used by {@link #computeHorizontalScrollRange()} and
+ * {@link #computeHorizontalScrollExtent()}.</p>
+ *
+ * <p>The default offset is the scroll offset of this view.</p>
+ *
+ * @return the horizontal offset of the scrollbar's thumb
+ *
+ * @see #computeHorizontalScrollRange()
+ * @see #computeHorizontalScrollExtent()
+ * @see android.widget.ScrollBarDrawable
+ */
+ protected int computeHorizontalScrollOffset() {
+ return mScrollX;
+ }
+
+ /**
+ * <p>Compute the horizontal extent of the horizontal scrollbar's thumb
+ * within the horizontal range. This value is used to compute the length
+ * of the thumb within the scrollbar's track.</p>
+ *
+ * <p>The range is expressed in arbitrary units that must be the same as the
+ * units used by {@link #computeHorizontalScrollRange()} and
+ * {@link #computeHorizontalScrollOffset()}.</p>
+ *
+ * <p>The default extent is the drawing width of this view.</p>
+ *
+ * @return the horizontal extent of the scrollbar's thumb
+ *
+ * @see #computeHorizontalScrollRange()
+ * @see #computeHorizontalScrollOffset()
+ * @see android.widget.ScrollBarDrawable
+ */
+ protected int computeHorizontalScrollExtent() {
+ return getWidth();
+ }
+
+ /**
+ * <p>Compute the vertical range that the vertical scrollbar represents.</p>
+ *
+ * <p>The range is expressed in arbitrary units that must be the same as the
+ * units used by {@link #computeVerticalScrollExtent()} and
+ * {@link #computeVerticalScrollOffset()}.</p>
+ *
+ * @return the total vertical range represented by the vertical scrollbar
+ *
+ * <p>The default range is the drawing height of this view.</p>
+ *
+ * @see #computeVerticalScrollExtent()
+ * @see #computeVerticalScrollOffset()
+ * @see android.widget.ScrollBarDrawable
+ */
+ protected int computeVerticalScrollRange() {
+ return getHeight();
+ }
+
+ /**
+ * <p>Compute the vertical offset of the vertical scrollbar's thumb
+ * within the horizontal range. This value is used to compute the position
+ * of the thumb within the scrollbar's track.</p>
+ *
+ * <p>The range is expressed in arbitrary units that must be the same as the
+ * units used by {@link #computeVerticalScrollRange()} and
+ * {@link #computeVerticalScrollExtent()}.</p>
+ *
+ * <p>The default offset is the scroll offset of this view.</p>
+ *
+ * @return the vertical offset of the scrollbar's thumb
+ *
+ * @see #computeVerticalScrollRange()
+ * @see #computeVerticalScrollExtent()
+ * @see android.widget.ScrollBarDrawable
+ */
+ protected int computeVerticalScrollOffset() {
+ return mScrollY;
+ }
+
+ /**
+ * <p>Compute the vertical extent of the horizontal scrollbar's thumb
+ * within the vertical range. This value is used to compute the length
+ * of the thumb within the scrollbar's track.</p>
+ *
+ * <p>The range is expressed in arbitrary units that must be the same as the
+ * units used by {@link #computeHorizontalScrollRange()} and
+ * {@link #computeVerticalScrollOffset()}.</p>
+ *
+ * <p>The default extent is the drawing height of this view.</p>
+ *
+ * @return the vertical extent of the scrollbar's thumb
+ *
+ * @see #computeVerticalScrollRange()
+ * @see #computeVerticalScrollOffset()
+ * @see android.widget.ScrollBarDrawable
+ */
+ protected int computeVerticalScrollExtent() {
+ return getHeight();
+ }
+
+ /**
+ * <p>Request the drawing of the horizontal and the vertical scrollbar. The
+ * scrollbars are painted only if they have been awakened first.</p>
+ *
+ * @param canvas the canvas on which to draw the scrollbars
+ */
+ private void onDrawScrollBars(Canvas canvas) {
+ // scrollbars are drawn only when the animation is running
+ final ScrollabilityCache cache = mScrollCache;
+ if (cache != null) {
+ final int viewFlags = mViewFlags;
+
+ final boolean drawHorizontalScrollBar =
+ (viewFlags & SCROLLBARS_HORIZONTAL) == SCROLLBARS_HORIZONTAL;
+ final boolean drawVerticalScrollBar =
+ (viewFlags & SCROLLBARS_VERTICAL) == SCROLLBARS_VERTICAL;
+
+ if (drawVerticalScrollBar || drawHorizontalScrollBar) {
+ final int width = mRight - mLeft;
+ final int height = mBottom - mTop;
+
+ final ScrollBarDrawable scrollBar = cache.scrollBar;
+ int size = scrollBar.getSize(false);
+ if (size <= 0) {
+ size = cache.scrollBarSize;
+ }
+
+ if (drawHorizontalScrollBar) {
+ onDrawHorizontalScrollBar(canvas, scrollBar, width, height, size);
+ }
+
+ if (drawVerticalScrollBar) {
+ onDrawVerticalScrollBar(canvas, scrollBar, width, height, size);
+ }
+ }
+ }
+ }
+
+ /**
+ * <p>Draw the horizontal scrollbar if
+ * {@link #isHorizontalScrollBarEnabled()} returns true.</p>
+ *
+ * <p>The length of the scrollbar and its thumb is computed according to the
+ * values returned by {@link #computeHorizontalScrollRange()},
+ * {@link #computeHorizontalScrollExtent()} and
+ * {@link #computeHorizontalScrollOffset()}. Refer to
+ * {@link android.widget.ScrollBarDrawable} for more information about how
+ * these values relate to each other.</p>
+ *
+ * @param canvas the canvas on which to draw the scrollbar
+ * @param scrollBar the scrollbar's drawable
+ * @param width the width of the drawing surface
+ * @param height the height of the drawing surface
+ * @param size the size of the scrollbar
+ *
+ * @see #isHorizontalScrollBarEnabled()
+ * @see #computeHorizontalScrollRange()
+ * @see #computeHorizontalScrollExtent()
+ * @see #computeHorizontalScrollOffset()
+ * @see android.widget.ScrollBarDrawable
+ */
+ private void onDrawHorizontalScrollBar(Canvas canvas, ScrollBarDrawable scrollBar, int width,
+ int height, int size) {
+
+ final int viewFlags = mViewFlags;
+ final int scrollX = mScrollX;
+ final int scrollY = mScrollY;
+ final int inside = (viewFlags & SCROLLBARS_OUTSIDE_MASK) == 0 ? ~0 : 0;
+ final int top = scrollY + height - size - (mUserPaddingBottom & inside);
+
+ final int verticalScrollBarGap =
+ (viewFlags & SCROLLBARS_VERTICAL) == SCROLLBARS_VERTICAL ?
+ getVerticalScrollbarWidth() : 0;
+
+ scrollBar.setBounds(scrollX + (mPaddingLeft & inside) + getScrollBarPaddingLeft(), top,
+ scrollX + width - (mUserPaddingRight & inside) - verticalScrollBarGap, top + size);
+ scrollBar.setParameters(
+ computeHorizontalScrollRange(),
+ computeHorizontalScrollOffset(),
+ computeHorizontalScrollExtent(), false);
+ scrollBar.draw(canvas);
+ }
+
+ /**
+ * <p>Draw the vertical scrollbar if {@link #isVerticalScrollBarEnabled()}
+ * returns true.</p>
+ *
+ * <p>The length of the scrollbar and its thumb is computed according to the
+ * values returned by {@link #computeVerticalScrollRange()},
+ * {@link #computeVerticalScrollExtent()} and
+ * {@link #computeVerticalScrollOffset()}. Refer to
+ * {@link android.widget.ScrollBarDrawable} for more information about how
+ * these values relate to each other.</p>
+ *
+ * @param canvas the canvas on which to draw the scrollbar
+ * @param scrollBar the scrollbar's drawable
+ * @param width the width of the drawing surface
+ * @param height the height of the drawing surface
+ * @param size the size of the scrollbar
+ *
+ * @see #isVerticalScrollBarEnabled()
+ * @see #computeVerticalScrollRange()
+ * @see #computeVerticalScrollExtent()
+ * @see #computeVerticalScrollOffset()
+ * @see android.widget.ScrollBarDrawable
+ */
+ private void onDrawVerticalScrollBar(Canvas canvas, ScrollBarDrawable scrollBar, int width,
+ int height, int size) {
+
+ final int scrollX = mScrollX;
+ final int scrollY = mScrollY;
+ final int inside = (mViewFlags & SCROLLBARS_OUTSIDE_MASK) == 0 ? ~0 : 0;
+ // TODO: Deal with RTL languages to position scrollbar on left
+ final int left = scrollX + width - size - (mUserPaddingRight & inside);
+
+ scrollBar.setBounds(left, scrollY + (mPaddingTop & inside),
+ left + size, scrollY + height - (mUserPaddingBottom & inside));
+ scrollBar.setParameters(
+ computeVerticalScrollRange(),
+ computeVerticalScrollOffset(),
+ computeVerticalScrollExtent(), true);
+ scrollBar.draw(canvas);
+ }
+
+ /**
+ * Implement this to do your drawing.
+ *
+ * @param canvas the canvas on which the background will be drawn
+ */
+ protected void onDraw(Canvas canvas) {
+ }
+
+ /*
+ * Caller is responsible for calling requestLayout if necessary.
+ * (This allows addViewInLayout to not request a new layout.)
+ */
+ void assignParent(ViewParent parent) {
+ if (mParent == null) {
+ mParent = parent;
+ } else if (parent == null) {
+ mParent = null;
+ } else {
+ throw new RuntimeException("view " + this + " being added, but"
+ + " it already has a parent");
+ }
+ }
+
+ /**
+ * This is called when the view is attached to a window. At this point it
+ * has a Surface and will start drawing. Note that this function is
+ * guaranteed to be called before {@link #onDraw}, however it may be called
+ * any time before the first onDraw -- including before or after
+ * {@link #onMeasure}.
+ *
+ * @see #onDetachedFromWindow()
+ */
+ protected void onAttachedToWindow() {
+ if ((mPrivateFlags & REQUEST_TRANSPARENT_REGIONS) != 0) {
+ mParent.requestTransparentRegion(this);
+ }
+ }
+
+ /**
+ * This is called when the view is detached from a window. At this point it
+ * no longer has a surface for drawing.
+ *
+ * @see #onAttachedToWindow()
+ */
+ protected void onDetachedFromWindow() {
+ if (mPendingCheckForLongPress != null) {
+ removeCallbacks(mPendingCheckForLongPress);
+ }
+ }
+
+ /**
+ * @return The number of times this view has been attached to a window
+ */
+ protected int getWindowAttachCount() {
+ return mWindowAttachCount;
+ }
+
+ /**
+ * Retrieve a unique token identifying the window this view is attached to.
+ * @return Return the window's token for use in
+ * {@link WindowManager.LayoutParams#token WindowManager.LayoutParams.token}.
+ */
+ public IBinder getWindowToken() {
+ return mAttachInfo != null ? mAttachInfo.mWindowToken : null;
+ }
+
+ /**
+ * Retrieve a unique token identifying the top-level "real" window of
+ * the window that this view is attached to. That is, this is like
+ * {@link #getWindowToken}, except if the window this view in is a panel
+ * window (attached to another containing window), then the token of
+ * the containing window is returned instead.
+ *
+ * @return Returns the associated window token, either
+ * {@link #getWindowToken()} or the containing window's token.
+ */
+ public IBinder getApplicationWindowToken() {
+ AttachInfo ai = mAttachInfo;
+ if (ai != null) {
+ IBinder appWindowToken = ai.mPanelParentWindowToken;
+ if (appWindowToken == null) {
+ appWindowToken = ai.mWindowToken;
+ }
+ return appWindowToken;
+ }
+ return null;
+ }
+
+ /**
+ * Retrieve private session object this view hierarchy is using to
+ * communicate with the window manager.
+ * @return the session object to communicate with the window manager
+ */
+ /*package*/ IWindowSession getWindowSession() {
+ return mAttachInfo != null ? mAttachInfo.mSession : null;
+ }
+
+ /**
+ * @param info the {@link android.view.View.AttachInfo} to associated with
+ * this view
+ */
+ void dispatchAttachedToWindow(AttachInfo info, int visibility) {
+ //System.out.println("Attached! " + this);
+ mAttachInfo = info;
+ mWindowAttachCount++;
+ if (mFloatingTreeObserver != null) {
+ info.mTreeObserver.merge(mFloatingTreeObserver);
+ mFloatingTreeObserver = null;
+ }
+ performCollectViewAttributes(visibility);
+ onAttachedToWindow();
+ int vis = info.mWindowVisibility;
+ if (vis != GONE) {
+ onWindowVisibilityChanged(vis);
+ }
+ }
+
+ void dispatchDetachedFromWindow() {
+ //System.out.println("Detached! " + this);
+ AttachInfo info = mAttachInfo;
+ if (info != null) {
+ int vis = info.mWindowVisibility;
+ if (vis != GONE) {
+ onWindowVisibilityChanged(GONE);
+ }
+ }
+
+ onDetachedFromWindow();
+ mAttachInfo = null;
+ }
+
+ /**
+ * Store this view hierarchy's frozen state into the given container.
+ *
+ * @param container The SparseArray in which to save the view's state.
+ *
+ * @see #restoreHierarchyState
+ * @see #dispatchSaveInstanceState
+ * @see #onSaveInstanceState
+ */
+ public void saveHierarchyState(SparseArray<Parcelable> container) {
+ dispatchSaveInstanceState(container);
+ }
+
+ /**
+ * Called by {@link #saveHierarchyState} to store the state for this view and its children.
+ * May be overridden to modify how freezing happens to a view's children; for example, some
+ * views may want to not store state for their children.
+ *
+ * @param container The SparseArray in which to save the view's state.
+ *
+ * @see #dispatchRestoreInstanceState
+ * @see #saveHierarchyState
+ * @see #onSaveInstanceState
+ */
+ protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
+ if (mID != NO_ID && (mViewFlags & SAVE_DISABLED_MASK) == 0) {
+ mPrivateFlags &= ~SAVE_STATE_CALLED;
+ Parcelable state = onSaveInstanceState();
+ if ((mPrivateFlags & SAVE_STATE_CALLED) == 0) {
+ throw new IllegalStateException(
+ "Derived class did not call super.onSaveInstanceState()");
+ }
+ if (state != null) {
+ // Log.i("View", "Freezing #" + Integer.toHexString(mID)
+ // + ": " + state);
+ container.put(mID, state);
+ }
+ }
+ }
+
+ /**
+ * Hook allowing a view to generate a representation of its internal state
+ * that can later be used to create a new instance with that same state.
+ * This state should only contain information that is not persistent or can
+ * not be reconstructed later. For example, you will never store your
+ * current position on screen because that will be computed again when a
+ * new instance of the view is placed in its view hierarchy.
+ * <p>
+ * Some examples of things you may store here: the current cursor position
+ * in a text view (but usually not the text itself since that is stored in a
+ * content provider or other persistent storage), the currently selected
+ * item in a list view.
+ *
+ * @return Returns a Parcelable object containing the view's current dynamic
+ * state, or null if there is nothing interesting to save. The
+ * default implementation returns null.
+ * @see #onRestoreInstanceState
+ * @see #saveHierarchyState
+ * @see #dispatchSaveInstanceState
+ * @see #setSaveEnabled(boolean)
+ */
+ protected Parcelable onSaveInstanceState() {
+ mPrivateFlags |= SAVE_STATE_CALLED;
+ return BaseSavedState.EMPTY_STATE;
+ }
+
+ /**
+ * Restore this view hierarchy's frozen state from the given container.
+ *
+ * @param container The SparseArray which holds previously frozen states.
+ *
+ * @see #saveHierarchyState
+ * @see #dispatchRestoreInstanceState
+ * @see #onRestoreInstanceState
+ */
+ public void restoreHierarchyState(SparseArray<Parcelable> container) {
+ dispatchRestoreInstanceState(container);
+ }
+
+ /**
+ * Called by {@link #restoreHierarchyState} to retrieve the state for this view and its
+ * children. May be overridden to modify how restoreing happens to a view's children; for
+ * example, some views may want to not store state for their children.
+ *
+ * @param container The SparseArray which holds previously saved state.
+ *
+ * @see #dispatchSaveInstanceState
+ * @see #restoreHierarchyState
+ * @see #onRestoreInstanceState
+ */
+ protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
+ if (mID != NO_ID) {
+ Parcelable state = container.get(mID);
+ if (state != null) {
+ // Log.i("View", "Restoreing #" + Integer.toHexString(mID)
+ // + ": " + state);
+ mPrivateFlags &= ~SAVE_STATE_CALLED;
+ onRestoreInstanceState(state);
+ if ((mPrivateFlags & SAVE_STATE_CALLED) == 0) {
+ throw new IllegalStateException(
+ "Derived class did not call super.onRestoreInstanceState()");
+ }
+ }
+ }
+ }
+
+ /**
+ * Hook allowing a view to re-apply a representation of its internal state that had previously
+ * been generated by {@link #onSaveInstanceState}. This function will never be called with a
+ * null state.
+ *
+ * @param state The frozen state that had previously been returned by
+ * {@link #onSaveInstanceState}.
+ *
+ * @see #onSaveInstanceState
+ * @see #restoreHierarchyState
+ * @see #dispatchRestoreInstanceState
+ */
+ protected void onRestoreInstanceState(Parcelable state) {
+ mPrivateFlags |= SAVE_STATE_CALLED;
+ if (state != BaseSavedState.EMPTY_STATE && state != null) {
+ throw new IllegalArgumentException("Wrong state class -- expecting View State");
+ }
+ }
+
+ /**
+ * <p>Return the time at which the drawing of the view hierarchy started.</p>
+ *
+ * @return the drawing start time in milliseconds
+ */
+ public long getDrawingTime() {
+ return mAttachInfo != null ? mAttachInfo.mDrawingTime : 0;
+ }
+
+ /**
+ * <p>Enables or disables the duplication of the parent's state into this view. When
+ * duplication is enabled, this view gets its drawable state from its parent rather
+ * than from its own internal properties.</p>
+ *
+ * <p>Note: in the current implementation, setting this property to true after the
+ * view was added to a ViewGroup might have no effect at all. This property should
+ * always be used from XML or set to true before adding this view to a ViewGroup.</p>
+ *
+ * <p>Note: if this view's parent addStateFromChildren property is enabled and this
+ * property is enabled, an exception will be thrown.</p>
+ *
+ * @param enabled True to enable duplication of the parent's drawable state, false
+ * to disable it.
+ *
+ * @see #getDrawableState()
+ * @see #isDuplicateParentStateEnabled()
+ */
+ public void setDuplicateParentStateEnabled(boolean enabled) {
+ setFlags(enabled ? DUPLICATE_PARENT_STATE : 0, DUPLICATE_PARENT_STATE);
+ }
+
+ /**
+ * <p>Indicates whether this duplicates its drawable state from its parent.</p>
+ *
+ * @return True if this view's drawable state is duplicated from the parent,
+ * false otherwise
+ *
+ * @see #getDrawableState()
+ * @see #setDuplicateParentStateEnabled(boolean)
+ */
+ public boolean isDuplicateParentStateEnabled() {
+ return (mViewFlags & DUPLICATE_PARENT_STATE) == DUPLICATE_PARENT_STATE;
+ }
+
+ /**
+ * <p>Enables or disables the drawing cache. When the drawing cache is enabled, the next call
+ * to {@link #getDrawingCache()} or {@link #buildDrawingCache()} will draw the view in a
+ * bitmap. Calling {@link #draw(android.graphics.Canvas)} will not draw from the cache when
+ * the cache is enabled. To benefit from the cache, you must request the drawing cache by
+ * calling {@link #getDrawingCache()} and draw it on screen if the returned bitmap is not
+ * null.</p>
+ *
+ * @param enabled true to enable the drawing cache, false otherwise
+ *
+ * @see #isDrawingCacheEnabled()
+ * @see #getDrawingCache()
+ * @see #buildDrawingCache()
+ */
+ public void setDrawingCacheEnabled(boolean enabled) {
+ setFlags(enabled ? DRAWING_CACHE_ENABLED : 0, DRAWING_CACHE_ENABLED);
+ }
+
+ /**
+ * <p>Indicates whether the drawing cache is enabled for this view.</p>
+ *
+ * @return true if the drawing cache is enabled
+ *
+ * @see #setDrawingCacheEnabled(boolean)
+ * @see #getDrawingCache()
+ */
+ @ViewDebug.ExportedProperty
+ public boolean isDrawingCacheEnabled() {
+ return (mViewFlags & DRAWING_CACHE_ENABLED) == DRAWING_CACHE_ENABLED;
+ }
+
+ /**
+ * <p>Returns the bitmap in which this view drawing is cached. The returned bitmap
+ * is null when caching is disabled. If caching is enabled and the cache is not ready,
+ * this method will create it. Calling {@link #draw(android.graphics.Canvas)} will not
+ * draw from the cache when the cache is enabled. To benefit from the cache, you must
+ * request the drawing cache by calling this method and draw it on screen if the
+ * returned bitmap is not null.</p>
+ *
+ * @return a bitmap representing this view or null if cache is disabled
+ *
+ * @see #setDrawingCacheEnabled(boolean)
+ * @see #isDrawingCacheEnabled()
+ * @see #buildDrawingCache()
+ * @see #destroyDrawingCache()
+ */
+ public Bitmap getDrawingCache() {
+ if ((mViewFlags & WILL_NOT_CACHE_DRAWING) == WILL_NOT_CACHE_DRAWING) {
+ return null;
+ }
+ if ((mViewFlags & DRAWING_CACHE_ENABLED) == DRAWING_CACHE_ENABLED &&
+ ((mPrivateFlags & DRAWING_CACHE_VALID) == 0 || mDrawingCache == null)) {
+ buildDrawingCache();
+ }
+ return mDrawingCache;
+ }
+
+ /**
+ * <p>Frees the resources used by the drawing cache. If you call
+ * {@link #buildDrawingCache()} manually without calling
+ * {@link #setDrawingCacheEnabled(boolean) setDrawingCacheEnabled(true)}, you
+ * should cleanup the cache with this method afterwards.</p>
+ *
+ * @see #setDrawingCacheEnabled(boolean)
+ * @see #buildDrawingCache()
+ * @see #getDrawingCache()
+ */
+ public void destroyDrawingCache() {
+ if (mDrawingCache != null) {
+ mDrawingCache.recycle();
+ mDrawingCache = null;
+ }
+ }
+
+ /**
+ * Setting a solid background color for the drawing cache's bitmaps will improve
+ * perfromance and memory usage. Note, though that this should only be used if this
+ * view will always be drawn on top of a solid color.
+ *
+ * @param color The background color to use for the drawing cache's bitmap
+ *
+ * @see #setDrawingCacheEnabled(boolean)
+ * @see #buildDrawingCache()
+ * @see #getDrawingCache()
+ */
+ public void setDrawingCacheBackgroundColor(int color) {
+ mDrawingCacheBackgroundColor = color;
+ }
+
+ /**
+ * @see #setDrawingCacheBackgroundColor(int)
+ *
+ * @return The background color to used for the drawing cache's bitmap
+ */
+ public int getDrawingCacheBackgroundColor() {
+ return mDrawingCacheBackgroundColor;
+ }
+
+ /**
+ * <p>Forces the drawing cache to be built if the drawing cache is invalid.</p>
+ *
+ * <p>If you call {@link #buildDrawingCache()} manually without calling
+ * {@link #setDrawingCacheEnabled(boolean) setDrawingCacheEnabled(true)}, you
+ * should cleanup the cache by calling {@link #destroyDrawingCache()} afterwards.</p>
+ *
+ * @see #getDrawingCache()
+ * @see #destroyDrawingCache()
+ */
+ public void buildDrawingCache() {
+ if ((mPrivateFlags & DRAWING_CACHE_VALID) == 0 || mDrawingCache == null) {
+ if (ViewDebug.TRACE_HIERARCHY) {
+ ViewDebug.trace(this, ViewDebug.HierarchyTraceType.BUILD_CACHE);
+ }
+ if (ViewRoot.PROFILE_DRAWING) {
+ EventLog.writeEvent(60002, hashCode());
+ }
+
+ final int width = mRight - mLeft;
+ final int height = mBottom - mTop;
+
+ final int drawingCacheBackgroundColor = mDrawingCacheBackgroundColor;
+ final boolean opaque = drawingCacheBackgroundColor != 0 ||
+ (mBGDrawable != null && mBGDrawable.getOpacity() == PixelFormat.OPAQUE);
+
+ if (width <= 0 || height <= 0 ||
+ (width * height * (opaque ? 2 : 4) >= // Projected bitmap size in bytes
+ ViewConfiguration.getMaximumDrawingCacheSize())) {
+ if (mDrawingCache != null) {
+ mDrawingCache.recycle();
+ }
+ mDrawingCache = null;
+ return;
+ }
+
+ boolean clear = true;
+ Bitmap bitmap = mDrawingCache;
+
+ if (bitmap == null || bitmap.getWidth() != width || bitmap.getHeight() != height) {
+
+ Bitmap.Config quality;
+ if (!opaque) {
+ switch (mViewFlags & DRAWING_CACHE_QUALITY_MASK) {
+ case DRAWING_CACHE_QUALITY_AUTO:
+ quality = Bitmap.Config.ARGB_8888;
+ break;
+ case DRAWING_CACHE_QUALITY_LOW:
+ quality = Bitmap.Config.ARGB_4444;
+ break;
+ case DRAWING_CACHE_QUALITY_HIGH:
+ quality = Bitmap.Config.ARGB_8888;
+ break;
+ default:
+ quality = Bitmap.Config.ARGB_8888;
+ break;
+ }
+ } else {
+ quality = Bitmap.Config.RGB_565;
+ }
+
+ mDrawingCache = bitmap = Bitmap.createBitmap(width, height, quality);
+
+ clear = drawingCacheBackgroundColor != 0;
+ }
+
+ Canvas canvas;
+ final AttachInfo attachInfo = mAttachInfo;
+ if (attachInfo != null) {
+ canvas = attachInfo.mCanvas;
+ if (canvas == null) {
+ canvas = new Canvas();
+ }
+ canvas.setBitmap(bitmap);
+ // Temporarily clobber the cached Canvas in case one of our children
+ // is also using a drawing cache. Without this, the children would
+ // steal the canvas by attaching their own bitmap to it and bad, bad
+ // thing would happen (invisible views, corrupted drawings, etc.)
+ attachInfo.mCanvas = null;
+ } else {
+ // This case should hopefully never or seldom happen
+ canvas = new Canvas(bitmap);
+ }
+
+ if (clear) {
+ bitmap.eraseColor(drawingCacheBackgroundColor);
+ }
+
+ computeScroll();
+ final int restoreCount = canvas.save();
+ canvas.translate(-mScrollX, -mScrollY);
+
+ // Fast path for layouts with no backgrounds
+ if ((mPrivateFlags & SKIP_DRAW) == SKIP_DRAW) {
+ mPrivateFlags |= DRAWN;
+ if (ViewDebug.TRACE_HIERARCHY) {
+ ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW);
+ }
+ dispatchDraw(canvas);
+ } else {
+ draw(canvas);
+ }
+
+ canvas.restoreToCount(restoreCount);
+
+ if (attachInfo != null) {
+ // Restore the cached Canvas for our siblings
+ attachInfo.mCanvas = canvas;
+ }
+ mPrivateFlags |= DRAWING_CACHE_VALID;
+ }
+ }
+
+
+ /**
+ * Manually render this view (and all of its children) to the given Canvas.
+ * The view must have already done a full layout before this function is
+ * called. When implementing a view, do not override this method; instead,
+ * you should implement {@link #onDraw}.
+ *
+ * @param canvas The Canvas to which the View is rendered.
+ */
+ public void draw(Canvas canvas) {
+ if (ViewDebug.TRACE_HIERARCHY) {
+ ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW);
+ }
+
+ /*
+ * Draw traversal performs several drawing steps which must be executed
+ * in the appropriate order:
+ *
+ * 1. Draw the background
+ * 2. If necessary, save the canvas' layers to prepare for fading
+ * 3. Draw view's content
+ * 4. Draw children
+ * 5. If necessary, draw the fading edges and restore layers
+ * 6. Draw decorations (scrollbars for instance)
+ */
+
+ // Step 1, draw the background, if needed
+ int saveCount;
+
+ final Drawable background = mBGDrawable;
+ if (background != null) {
+ final int scrollX = mScrollX;
+ final int scrollY = mScrollY;
+
+ if (mBackgroundSizeChanged) {
+ background.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
+ mBackgroundSizeChanged = false;
+ }
+
+ if ((scrollX | scrollY) == 0) {
+ background.draw(canvas);
+ } else {
+ canvas.translate(scrollX, scrollY);
+ background.draw(canvas);
+ canvas.translate(-scrollX, -scrollY);
+ }
+ }
+
+ // skip step 2 & 5 if possible (common case)
+ final int viewFlags = mViewFlags;
+ boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
+ boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
+ if (!verticalEdges && !horizontalEdges) {
+ // Step 3, draw the content
+ mPrivateFlags |= DRAWN;
+ onDraw(canvas);
+
+ // Step 4, draw the children
+ dispatchDraw(canvas);
+
+ // Step 6, draw decorations (scrollbars)
+ onDrawScrollBars(canvas);
+
+ // we're done...
+ return;
+ }
+
+ /*
+ * Here we do the full fledged routine...
+ * (this is an uncommon case where speed matters less,
+ * this is why we repeat some of the tests that have been
+ * done above)
+ */
+
+ boolean drawTop = false;
+ boolean drawBottom = false;
+ boolean drawLeft = false;
+ boolean drawRight = false;
+
+ float topFadeStrength = 0.0f;
+ float bottomFadeStrength = 0.0f;
+ float leftFadeStrength = 0.0f;
+ float rightFadeStrength = 0.0f;
+
+ // Step 2, save the canvas' layers
+ final int paddingLeft = mPaddingLeft;
+ final int paddingTop = mPaddingTop;
+
+ final int left = mScrollX + paddingLeft;
+ final int right = left + mRight - mLeft - mPaddingRight - paddingLeft;
+ final int top = mScrollY + paddingTop;
+ final int bottom = top + mBottom - mTop - mPaddingBottom - paddingTop;
+
+ final ScrollabilityCache scrollabilityCache = mScrollCache;
+ int length = scrollabilityCache.fadingEdgeLength;
+
+ // clip the fade length if top and bottom fades overlap
+ // overlapping fades produce odd-looking artifacts
+ if (verticalEdges && (top + length > bottom - length)) {
+ length = (bottom - top) / 2;
+ }
+
+ // also clip horizontal fades if necessary
+ if (horizontalEdges && (left + length > right - length)) {
+ length = (right - left) / 2;
+ }
+
+ if (verticalEdges) {
+ topFadeStrength = Math.max(0.0f, Math.min(1.0f, getTopFadingEdgeStrength()));
+ drawTop = topFadeStrength >= 0.0f;
+ bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, getBottomFadingEdgeStrength()));
+ drawBottom = bottomFadeStrength >= 0.0f;
+ }
+
+ if (horizontalEdges) {
+ leftFadeStrength = Math.max(0.0f, Math.min(1.0f, getLeftFadingEdgeStrength()));
+ drawLeft = leftFadeStrength >= 0.0f;
+ rightFadeStrength = Math.max(0.0f, Math.min(1.0f, getRightFadingEdgeStrength()));
+ drawRight = rightFadeStrength >= 0.0f;
+ }
+
+ saveCount = canvas.getSaveCount();
+
+ int solidColor = getSolidColor();
+ if (solidColor == 0) {
+ final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;
+
+ if (drawTop) {
+ canvas.saveLayer(left, top, right, top + length, null, flags);
+ }
+
+ if (drawBottom) {
+ canvas.saveLayer(left, bottom - length, right, bottom, null, flags);
+ }
+
+ if (drawLeft) {
+ canvas.saveLayer(left, top, left + length, bottom, null, flags);
+ }
+
+ if (drawRight) {
+ canvas.saveLayer(right - length, top, right, bottom, null, flags);
+ }
+ } else {
+ scrollabilityCache.setFadeColor(solidColor);
+ }
+
+ // Step 3, draw the content
+ mPrivateFlags |= DRAWN;
+ onDraw(canvas);
+
+ // Step 4, draw the children
+ dispatchDraw(canvas);
+
+ // Step 5, draw the fade effect and restore layers
+ final Paint p = scrollabilityCache.paint;
+ final Matrix matrix = scrollabilityCache.matrix;
+ final Shader fade = scrollabilityCache.shader;
+ final float fadeHeight = scrollabilityCache.fadingEdgeLength;
+
+ if (drawTop) {
+ matrix.setScale(1, fadeHeight * topFadeStrength);
+ matrix.postTranslate(left, top);
+ fade.setLocalMatrix(matrix);
+ canvas.drawRect(left, top, right, top + length, p);
+ }
+
+ if (drawBottom) {
+ matrix.setScale(1, fadeHeight * bottomFadeStrength);
+ matrix.postRotate(180);
+ matrix.postTranslate(left, bottom);
+ fade.setLocalMatrix(matrix);
+ canvas.drawRect(left, bottom - length, right, bottom, p);
+ }
+
+ if (drawLeft) {
+ matrix.setScale(1, fadeHeight * leftFadeStrength);
+ matrix.postRotate(-90);
+ matrix.postTranslate(left, top);
+ fade.setLocalMatrix(matrix);
+ canvas.drawRect(left, top, left + length, bottom, p);
+ }
+
+ if (drawRight) {
+ matrix.setScale(1, fadeHeight * rightFadeStrength);
+ matrix.postRotate(90);
+ matrix.postTranslate(right, top);
+ fade.setLocalMatrix(matrix);
+ canvas.drawRect(right - length, top, right, bottom, p);
+ }
+
+ canvas.restoreToCount(saveCount);
+
+ // Step 6, draw decorations (scrollbars)
+ onDrawScrollBars(canvas);
+ }
+
+ /**
+ * Override this if your view is known to always be drawn on top of a solid color background,
+ * and needs to draw fading edges. Returning a non-zero color enables the view system to
+ * optimize the drawing of the fading edges. If you do return a non-zero color, the alpha
+ * should be set to 0xFF.
+ *
+ * @see #setVerticalFadingEdgeEnabled
+ * @see #setHorizontalFadingEdgeEnabled
+ *
+ * @return The known solid color background for this view, or 0 if the color may vary
+ */
+ public int getSolidColor() {
+ return 0;
+ }
+
+ /**
+ * Build a human readable string representation of the specified view flags.
+ *
+ * @param flags the view flags to convert to a string
+ * @return a String representing the supplied flags
+ */
+ private static String printFlags(int flags) {
+ String output = "";
+ int numFlags = 0;
+ if ((flags & FOCUSABLE_MASK) == FOCUSABLE) {
+ output += "TAKES_FOCUS";
+ numFlags++;
+ }
+
+ switch (flags & VISIBILITY_MASK) {
+ case INVISIBLE:
+ if (numFlags > 0) {
+ output += " ";
+ }
+ output += "INVISIBLE";
+ // USELESS HERE numFlags++;
+ break;
+ case GONE:
+ if (numFlags > 0) {
+ output += " ";
+ }
+ output += "GONE";
+ // USELESS HERE numFlags++;
+ break;
+ default:
+ break;
+ }
+ return output;
+ }
+
+ /**
+ * Build a human readable string representation of the specified private
+ * view flags.
+ *
+ * @param privateFlags the private view flags to convert to a string
+ * @return a String representing the supplied flags
+ */
+ private static String printPrivateFlags(int privateFlags) {
+ String output = "";
+ int numFlags = 0;
+
+ if ((privateFlags & WANTS_FOCUS) == WANTS_FOCUS) {
+ output += "WANTS_FOCUS";
+ numFlags++;
+ }
+
+ if ((privateFlags & FOCUSED) == FOCUSED) {
+ if (numFlags > 0) {
+ output += " ";
+ }
+ output += "FOCUSED";
+ numFlags++;
+ }
+
+ if ((privateFlags & SELECTED) == SELECTED) {
+ if (numFlags > 0) {
+ output += " ";
+ }
+ output += "SELECTED";
+ numFlags++;
+ }
+
+ if ((privateFlags & IS_ROOT_NAMESPACE) == IS_ROOT_NAMESPACE) {
+ if (numFlags > 0) {
+ output += " ";
+ }
+ output += "IS_ROOT_NAMESPACE";
+ numFlags++;
+ }
+
+ if ((privateFlags & HAS_BOUNDS) == HAS_BOUNDS) {
+ if (numFlags > 0) {
+ output += " ";
+ }
+ output += "HAS_BOUNDS";
+ numFlags++;
+ }
+
+ if ((privateFlags & DRAWN) == DRAWN) {
+ if (numFlags > 0) {
+ output += " ";
+ }
+ output += "DRAWN";
+ // USELESS HERE numFlags++;
+ }
+ return output;
+ }
+
+ /**
+ * <p>Indicates whether or not this view's layout will be requested during
+ * the next hierarchy layout pass.</p>
+ *
+ * @return true if the layout will be forced during next layout pass
+ */
+ public boolean isLayoutRequested() {
+ return (mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT;
+ }
+
+ /**
+ * Assign a size and position to a view and all of its
+ * descendants
+ *
+ * <p>This is the second phase of the layout mechanism.
+ * (The first is measuring). In this phase, each parent calls
+ * layout on all of its children to position them.
+ * This is typically done using the child measurements
+ * that were stored in the measure pass().
+ *
+ * Derived classes with children should override
+ * onLayout. In that method, they should
+ * call layout on each of their their children.
+ *
+ * @param l Left position, relative to parent
+ * @param t Top position, relative to parent
+ * @param r Right position, relative to parent
+ * @param b Bottom position, relative to parent
+ */
+ public final void layout(int l, int t, int r, int b) {
+ boolean changed = setFrame(l, t, r, b);
+ if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {
+ if (ViewDebug.TRACE_HIERARCHY) {
+ ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);
+ }
+
+ onLayout(changed, l, t, r, b);
+ mPrivateFlags &= ~LAYOUT_REQUIRED;
+ }
+ mPrivateFlags &= ~FORCE_LAYOUT;
+ }
+
+ /**
+ * Called from layout when this view should
+ * assign a size and position to each of its children.
+ *
+ * Derived classes with children should override
+ * this method and call layout on each of
+ * their their children.
+ * @param changed This is a new size or position for this view
+ * @param left Left position, relative to parent
+ * @param top Top position, relative to parent
+ * @param right Right position, relative to parent
+ * @param bottom Bottom position, relative to parent
+ */
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ }
+
+ /**
+ * Assign a size and position to this view.
+ *
+ * This is called from layout.
+ *
+ * @param left Left position, relative to parent
+ * @param top Top position, relative to parent
+ * @param right Right position, relative to parent
+ * @param bottom Bottom position, relative to parent
+ * @return true if the new size and position are different than the
+ * previous ones
+ * {@hide}
+ */
+ protected boolean setFrame(int left, int top, int right, int bottom) {
+ boolean changed = false;
+
+ if (DBG) {
+ System.out.println(this + " View.setFrame(" + left + "," + top + ","
+ + right + "," + bottom + ")");
+ }
+
+ if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
+ changed = true;
+
+ // Remember our drawn bit
+ int drawn = mPrivateFlags & DRAWN;
+
+ // Invalidate our old position
+ invalidate();
+
+
+ int oldWidth = mRight - mLeft;
+ int oldHeight = mBottom - mTop;
+
+ mLeft = left;
+ mTop = top;
+ mRight = right;
+ mBottom = bottom;
+
+ mPrivateFlags |= HAS_BOUNDS;
+
+ int newWidth = right - left;
+ int newHeight = bottom - top;
+
+ if (newWidth != oldWidth || newHeight != oldHeight) {
+ onSizeChanged(newWidth, newHeight, oldWidth, oldHeight);
+ }
+
+ if ((mViewFlags & VISIBILITY_MASK) == VISIBLE) {
+ // If we are visible, force the DRAWN bit to on so that
+ // this invalidate will go through (at least to our parent).
+ // This is because someone may have invalidated this view
+ // before this call to setFrame came in, therby clearing
+ // the DRAWN bit.
+ mPrivateFlags |= DRAWN;
+ invalidate();
+ }
+
+ // Reset drawn bit to original value (invalidate turns it off)
+ mPrivateFlags |= drawn;
+
+ mBackgroundSizeChanged = true;
+ }
+ return changed;
+ }
+
+ /**
+ * Finalize inflating a view from XML. This is called as the last phase
+ * of inflation, after all child views have been added.
+ *
+ * <p>Even if the subclass overrides onFinishInflate, they should always be
+ * sure to call the super method, so that we get called.
+ */
+ protected void onFinishInflate() {
+ }
+
+ /**
+ * Returns the resources associated with this view.
+ *
+ * @return Resources object.
+ */
+ public Resources getResources() {
+ return mResources;
+ }
+
+ /**
+ * Invalidates the specified Drawable.
+ *
+ * @param drawable the drawable to invalidate
+ */
+ public void invalidateDrawable(Drawable drawable) {
+ if (verifyDrawable(drawable)) {
+ final Rect dirty = drawable.getBounds();
+ final int scrollX = mScrollX;
+ final int scrollY = mScrollY;
+
+ invalidate(dirty.left + scrollX, dirty.top + scrollY,
+ dirty.right + scrollX, dirty.bottom + scrollY);
+ }
+ }
+
+ /**
+ * Schedules an action on a drawable to occur at a specified time.
+ *
+ * @param who the recipient of the action
+ * @param what the action to run on the drawable
+ * @param when the time at which the action must occur. Uses the
+ * {@link SystemClock#uptimeMillis} timebase.
+ */
+ public void scheduleDrawable(Drawable who, Runnable what, long when) {
+ if (verifyDrawable(who) && what != null && mAttachInfo != null) {
+ mAttachInfo.mHandler.postAtTime(what, who, when);
+ }
+ }
+
+ /**
+ * Cancels a scheduled action on a drawable.
+ *
+ * @param who the recipient of the action
+ * @param what the action to cancel
+ */
+ public void unscheduleDrawable(Drawable who, Runnable what) {
+ if (verifyDrawable(who) && what != null && mAttachInfo != null) {
+ mAttachInfo.mHandler.removeCallbacks(what, who);
+ }
+ }
+
+ /**
+ * Unschedule any events associated with the given Drawable. This can be
+ * used when selecting a new Drawable into a view, so that the previous
+ * one is completely unscheduled.
+ *
+ * @param who The Drawable to unschedule.
+ *
+ * @see #drawableStateChanged
+ */
+ public void unscheduleDrawable(Drawable who) {
+ if (mAttachInfo != null) {
+ mAttachInfo.mHandler.removeCallbacksAndMessages(who);
+ }
+ }
+
+ /**
+ * If your view subclass is displaying its own Drawable objects, it should
+ * override this function and return true for any Drawable it is
+ * displaying. This allows animations for those drawables to be
+ * scheduled.
+ *
+ * <p>Be sure to call through to the super class when overriding this
+ * function.
+ *
+ * @param who The Drawable to verify. Return true if it is one you are
+ * displaying, else return the result of calling through to the
+ * super class.
+ *
+ * @return boolean If true than the Drawable is being displayed in the
+ * view; else false and it is not allowed to animate.
+ *
+ * @see #unscheduleDrawable
+ * @see #drawableStateChanged
+ */
+ protected boolean verifyDrawable(Drawable who) {
+ return who == mBGDrawable;
+ }
+
+ /**
+ * This function is called whenever the state of the view changes in such
+ * a way that it impacts the state of drawables being shown.
+ *
+ * <p>Be sure to call through to the superclass when overriding this
+ * function.
+ *
+ * @see Drawable#setState
+ */
+ protected void drawableStateChanged() {
+ Drawable d = mBGDrawable;
+ if (d != null && d.isStateful()) {
+ d.setState(getDrawableState());
+ }
+ }
+
+ /**
+ * Call this to force a view to update its drawable state. This will cause
+ * drawableStateChanged to be called on this view. Views that are interested
+ * in the new state should call getDrawableState.
+ *
+ * @see #drawableStateChanged
+ * @see #getDrawableState
+ */
+ public void refreshDrawableState() {
+ mPrivateFlags |= DRAWABLE_STATE_DIRTY;
+ drawableStateChanged();
+
+ ViewParent parent = mParent;
+ if (parent != null) {
+ parent.childDrawableStateChanged(this);
+ }
+ }
+
+ /**
+ * Return an array of resource IDs of the drawable states representing the
+ * current state of the view.
+ *
+ * @return The current drawable state
+ *
+ * @see Drawable#setState
+ * @see #drawableStateChanged
+ * @see #onCreateDrawableState
+ */
+ public final int[] getDrawableState() {
+ if ((mDrawableState != null) && ((mPrivateFlags & DRAWABLE_STATE_DIRTY) == 0)) {
+ return mDrawableState;
+ } else {
+ mDrawableState = onCreateDrawableState(0);
+ mPrivateFlags &= ~DRAWABLE_STATE_DIRTY;
+ return mDrawableState;
+ }
+ }
+
+ /**
+ * Generate the new {@link android.graphics.drawable.Drawable} state for
+ * this view. This is called by the view
+ * system when the cached Drawable state is determined to be invalid. To
+ * retrieve the current state, you should use {@link #getDrawableState}.
+ *
+ * @param extraSpace if non-zero, this is the number of extra entries you
+ * would like in the returned array in which you can place your own
+ * states.
+ *
+ * @return Returns an array holding the current {@link Drawable} state of
+ * the view.
+ *
+ * @see #mergeDrawableStates
+ */
+ protected int[] onCreateDrawableState(int extraSpace) {
+ if ((mViewFlags & DUPLICATE_PARENT_STATE) == DUPLICATE_PARENT_STATE &&
+ mParent instanceof View) {
+ return ((View) mParent).onCreateDrawableState(extraSpace);
+ }
+
+ int[] drawableState;
+
+ int privateFlags = mPrivateFlags;
+
+ boolean isPressed = (privateFlags & PRESSED) != 0;
+ int viewStateIndex = (isPressed ? 1 : 0);
+
+ boolean isEnabled = (mViewFlags & ENABLED_MASK) == ENABLED;
+ viewStateIndex = (viewStateIndex << 1) + (isEnabled ? 1 : 0);
+
+ boolean isFocused = isFocused();
+ viewStateIndex = (viewStateIndex << 1) + (isFocused ? 1 : 0);
+
+ boolean isSelected = (privateFlags & SELECTED) != 0;
+ viewStateIndex = (viewStateIndex << 1) + (isSelected ? 1 : 0);
+
+ boolean hasWindowFocus = hasWindowFocus();
+ viewStateIndex = (viewStateIndex << 1) + (hasWindowFocus ? 1 : 0);
+
+ drawableState = VIEW_STATE_SETS[viewStateIndex];
+
+ //noinspection ConstantIfStatement
+ if (false) {
+ Log.i("View", "drawableStateIndex=" + viewStateIndex);
+ Log.i("View", toString() + " pressed=" + isPressed
+ + " en=" + isEnabled + " fo=" + isFocused
+ + " sl=" + isSelected + " wf=" + hasWindowFocus
+ + ": " + Arrays.toString(drawableState));
+ }
+
+ if (extraSpace == 0) {
+ return drawableState;
+ }
+
+ final int[] fullState;
+ if (drawableState != null) {
+ fullState = new int[drawableState.length + extraSpace];
+ System.arraycopy(drawableState, 0, fullState, 0, drawableState.length);
+ } else {
+ fullState = new int[extraSpace];
+ }
+
+ return fullState;
+ }
+
+ /**
+ * Merge your own state values in <var>additionalState</var> into the base
+ * state values <var>baseState</var> that were returned by
+ * {@link #onCreateDrawableState}.
+ *
+ * @param baseState The base state values returned by
+ * {@link #onCreateDrawableState}, which will be modified to also hold your
+ * own additional state values.
+ *
+ * @param additionalState The additional state values you would like
+ * added to <var>baseState</var>; this array is not modified.
+ *
+ * @return As a convenience, the <var>baseState</var> array you originally
+ * passed into the function is returned.
+ *
+ * @see #onCreateDrawableState
+ */
+ protected static int[] mergeDrawableStates(int[] baseState, int[] additionalState) {
+ final int N = baseState.length;
+ int i = N - 1;
+ while (i >= 0 && baseState[i] == 0) {
+ i--;
+ }
+ System.arraycopy(additionalState, 0, baseState, i + 1, additionalState.length);
+ return baseState;
+ }
+
+ /**
+ * Sets the background color for this view.
+ * @param color the color of the background
+ */
+ public void setBackgroundColor(int color) {
+ setBackgroundDrawable(new ColorDrawable(color));
+ }
+
+ /**
+ * Set the background to a given resource. The resource should refer to
+ * a Drawable object.
+ * @param resid The identifier of the resource.
+ * @attr ref android.R.styleable#View_background
+ */
+ public void setBackgroundResource(int resid) {
+ if (resid != 0 && resid == mBackgroundResource) {
+ return;
+ }
+
+ Drawable d= null;
+ if (resid != 0) {
+ d = mResources.getDrawable(resid);
+ }
+ setBackgroundDrawable(d);
+
+ mBackgroundResource = resid;
+ }
+
+ /**
+ * Set the background to a given Drawable, or remove the background. If the
+ * background has padding, this View's padding is set to the background's
+ * padding. However, when a background is removed, this View's padding isn't
+ * touched. If setting the padding is desired, please use
+ * {@link #setPadding(int, int, int, int)}.
+ *
+ * @param d The Drawable to use as the background, or null to remove the
+ * background
+ */
+ public void setBackgroundDrawable(Drawable d) {
+ boolean requestLayout = false;
+
+ mBackgroundResource = 0;
+
+ /*
+ * Regardless of whether we're setting a new background or not, we want
+ * to clear the previous drawable.
+ */
+ if (mBGDrawable != null) {
+ mBGDrawable.setCallback(null);
+ unscheduleDrawable(mBGDrawable);
+ }
+
+ if (d != null) {
+ final Rect padding = mTempRect;
+ if (d.getPadding(padding)) {
+ setPadding(padding.left, padding.top, padding.right, padding.bottom);
+ }
+
+ // Compare the minimum sizes of the old Drawable and the new. If there isn't an old or
+ // if it has a different minimum size, we should layout again
+ if (mBGDrawable == null || mBGDrawable.getMinimumHeight() != d.getMinimumHeight() ||
+ mBGDrawable.getMinimumWidth() != d.getMinimumWidth()) {
+ requestLayout = true;
+ }
+
+ d.setCallback(this);
+ if (d.isStateful()) {
+ d.setState(getDrawableState());
+ }
+ d.setVisible(getVisibility() == VISIBLE, false);
+ mBGDrawable = d;
+
+ if ((mPrivateFlags & SKIP_DRAW) != 0) {
+ mPrivateFlags &= ~SKIP_DRAW;
+ mPrivateFlags |= ONLY_DRAWS_BACKGROUND;
+ requestLayout = true;
+ }
+ } else {
+ /* Remove the background */
+ mBGDrawable = null;
+
+ if ((mPrivateFlags & ONLY_DRAWS_BACKGROUND) != 0) {
+ /*
+ * This view ONLY drew the background before and we're removing
+ * the background, so now it won't draw anything
+ * (hence we SKIP_DRAW)
+ */
+ mPrivateFlags &= ~ONLY_DRAWS_BACKGROUND;
+ mPrivateFlags |= SKIP_DRAW;
+ }
+
+ /*
+ * When the background is set, we try to apply its padding to this
+ * View. When the background is removed, we don't touch this View's
+ * padding. This is noted in the Javadocs. Hence, we don't need to
+ * requestLayout(), the invalidate() below is sufficient.
+ */
+
+ // The old background's minimum size could have affected this
+ // View's layout, so let's requestLayout
+ requestLayout = true;
+ }
+
+ if (requestLayout) {
+ requestLayout();
+ }
+
+ mBackgroundSizeChanged = true;
+ invalidate();
+ }
+
+ /**
+ * Gets the background drawable
+ * @return The drawable used as the background for this view, if any.
+ */
+ public Drawable getBackground() {
+ return mBGDrawable;
+ }
+
+ private int getScrollBarPaddingLeft() {
+ // TODO: Deal with RTL languages
+ return 0;
+ }
+
+ /*
+ * Returns the pixels occupied by the vertical scrollbar, if not overlaid
+ */
+ private int getScrollBarPaddingRight() {
+ // TODO: Deal with RTL languages
+ if ((mViewFlags & SCROLLBARS_VERTICAL) == 0) {
+ return 0;
+ }
+ return (mViewFlags & SCROLLBARS_INSET_MASK) == 0 ? 0 : getVerticalScrollbarWidth();
+ }
+
+ /*
+ * Returns the pixels occupied by the horizontal scrollbar, if not overlaid
+ */
+ private int getScrollBarPaddingBottom() {
+ if ((mViewFlags & SCROLLBARS_HORIZONTAL) == 0) {
+ return 0;
+ }
+ return (mViewFlags & SCROLLBARS_INSET_MASK) == 0 ? 0 : getHorizontalScrollbarHeight();
+ }
+
+ /**
+ * Sets the padding. The view may add on the space required to display
+ * the scrollbars, depending on the style and visibility of the scrollbars.
+ * So the values returned from {@link #getPaddingLeft}, {@link #getPaddingTop},
+ * {@link #getPaddingRight} and {@link #getPaddingBottom} may be different
+ * from the values set in this call.
+ *
+ * @attr ref android.R.styleable#View_padding
+ * @attr ref android.R.styleable#View_paddingBottom
+ * @attr ref android.R.styleable#View_paddingLeft
+ * @attr ref android.R.styleable#View_paddingRight
+ * @attr ref android.R.styleable#View_paddingTop
+ * @param left the left padding in pixels
+ * @param top the top padding in pixels
+ * @param right the right padding in pixels
+ * @param bottom the bottom padding in pixels
+ */
+ public void setPadding(int left, int top, int right, int bottom) {
+ boolean changed = false;
+
+ mUserPaddingRight = right;
+ mUserPaddingBottom = bottom;
+
+ if (mPaddingLeft != left + getScrollBarPaddingLeft()) {
+ changed = true;
+ mPaddingLeft = left;
+ }
+ if (mPaddingTop != top) {
+ changed = true;
+ mPaddingTop = top;
+ }
+ if (mPaddingRight != right + getScrollBarPaddingRight()) {
+ changed = true;
+ mPaddingRight = right + getScrollBarPaddingRight();
+ }
+ if (mPaddingBottom != bottom + getScrollBarPaddingBottom()) {
+ changed = true;
+ mPaddingBottom = bottom + getScrollBarPaddingBottom();
+ }
+
+ if (changed) {
+ requestLayout();
+ }
+ }
+
+ /**
+ * Returns the top padding of this view.
+ *
+ * @return the top padding in pixels
+ */
+ public int getPaddingTop() {
+ return mPaddingTop;
+ }
+
+ /**
+ * Returns the bottom padding of this view. If there are inset and enabled
+ * scrollbars, this value may include the space required to display the
+ * scrollbars as well.
+ *
+ * @return the bottom padding in pixels
+ */
+ public int getPaddingBottom() {
+ return mPaddingBottom;
+ }
+
+ /**
+ * Returns the left padding of this view. If there are inset and enabled
+ * scrollbars, this value may include the space required to display the
+ * scrollbars as well.
+ *
+ * @return the left padding in pixels
+ */
+ public int getPaddingLeft() {
+ return mPaddingLeft;
+ }
+
+ /**
+ * Returns the right padding of this view. If there are inset and enabled
+ * scrollbars, this value may include the space required to display the
+ * scrollbars as well.
+ *
+ * @return the right padding in pixels
+ */
+ public int getPaddingRight() {
+ return mPaddingRight;
+ }
+
+ /**
+ * Changes the selection state of this view. A view can be selected or not.
+ * Note that selection is not the same as focus. Views are typically
+ * selected in the context of an AdapterView like ListView or GridView;
+ * the selected view is the view that is highlighted.
+ *
+ * @param selected true if the view must be selected, false otherwise
+ */
+ public void setSelected(boolean selected) {
+ if (((mPrivateFlags & SELECTED) != 0) != selected) {
+ mPrivateFlags = (mPrivateFlags & ~SELECTED) | (selected ? SELECTED : 0);
+ invalidate();
+ refreshDrawableState();
+ dispatchSetSelected(selected);
+ }
+ }
+
+ /**
+ * Dispatch setSelected to all of this View's children.
+ *
+ * @see #setSelected(boolean)
+ *
+ * @param selected The new selected state
+ */
+ protected void dispatchSetSelected(boolean selected) {
+ }
+
+ /**
+ * Indicates the selection state of this view.
+ *
+ * @return true if the view is selected, false otherwise
+ */
+ @ViewDebug.ExportedProperty
+ public boolean isSelected() {
+ return (mPrivateFlags & SELECTED) != 0;
+ }
+
+ /**
+ * Returns the ViewTreeObserver for this view's hierarchy. The view tree
+ * observer can be used to get notifications when global events, like
+ * layout, happen.
+ *
+ * The returned ViewTreeObserver observer is not guaranteed to remain
+ * valid for the lifetime of this View. If the caller of this method keeps
+ * a long-lived reference to ViewTreeObserver, it should always check for
+ * the return value of {@link ViewTreeObserver#isAlive()}.
+ *
+ * @return The ViewTreeObserver for this view's hierarchy.
+ */
+ public ViewTreeObserver getViewTreeObserver() {
+ if (mAttachInfo != null) {
+ return mAttachInfo.mTreeObserver;
+ }
+ if (mFloatingTreeObserver == null) {
+ mFloatingTreeObserver = new ViewTreeObserver();
+ }
+ return mFloatingTreeObserver;
+ }
+
+ /**
+ * <p>Finds the topmost view in the current view hierarchy.</p>
+ *
+ * @return the topmost view containing this view
+ */
+ public View getRootView() {
+ View parent = this;
+
+ while (parent.mParent != null && parent.mParent instanceof View) {
+ parent = (View) parent.mParent;
+ }
+
+ return parent;
+ }
+
+ /**
+ * <p>Computes the coordinates of this view on the screen. The argument
+ * must be an array of two integers. After the method returns, the array
+ * contains the x and y location in that order.</p>
+ *
+ * @param location an array of two integers in which to hold the coordinates
+ */
+ public void getLocationOnScreen(int[] location) {
+ getLocationInWindow(location);
+
+ final AttachInfo info = mAttachInfo;
+ location[0] += info.mWindowLeft;
+ location[1] += info.mWindowTop;
+ }
+
+ /**
+ * <p>Computes the coordinates of this view in its window. The argument
+ * must be an array of two integers. After the method returns, the array
+ * contains the x and y location in that order.</p>
+ *
+ * @param location an array of two integers in which to hold the coordinates
+ */
+ public void getLocationInWindow(int[] location) {
+ if (location == null || location.length < 2) {
+ throw new IllegalArgumentException("location must be an array of "
+ + "two integers");
+ }
+
+ location[0] = mLeft;
+ location[1] = mTop;
+
+ if (!(mParent instanceof View)) {
+ return;
+ }
+
+ View parent = (View) mParent;
+
+ while (parent != null) {
+ location[0] += parent.mLeft - parent.mScrollX;
+ location[1] += parent.mTop - parent.mScrollY;
+
+ final ViewParent viewParent = parent.mParent;
+ if (viewParent != null && viewParent instanceof View) {
+ parent = (View) viewParent;
+ } else {
+ parent = null;
+ }
+ }
+ }
+
+ /**
+ * {@hide}
+ * @param id the id of the view to be found
+ * @return the view of the specified id, null if cannot be found
+ */
+ protected View findViewTraversal(int id) {
+ if (id == mID) {
+ return this;
+ }
+ return null;
+ }
+
+ /**
+ * {@hide}
+ * @param tag the tag of the view to be found
+ * @return the view of specified tag, null if cannot be found
+ */
+ protected View findViewWithTagTraversal(Object tag) {
+ if (tag != null && tag.equals(mTag)) {
+ return this;
+ }
+ return null;
+ }
+
+ /**
+ * Look for a child view with the given id. If this view has the given
+ * id, return this view.
+ *
+ * @param id The id to search for.
+ * @return The view that has the given id in the hierarchy or null
+ */
+ public final View findViewById(int id) {
+ if (id < 0) {
+ return null;
+ }
+ return findViewTraversal(id);
+ }
+
+ /**
+ * Look for a child view with the given tag. If this view has the given
+ * tag, return this view.
+ *
+ * @param tag The tag to search for, using "tag.equals(getTag())".
+ * @return The View that has the given tag in the hierarchy or null
+ */
+ public final View findViewWithTag(Object tag) {
+ if (tag == null) {
+ return null;
+ }
+ return findViewWithTagTraversal(tag);
+ }
+
+ /**
+ * Sets the identifier for this view. The identifier does not have to be
+ * unique in this view's hierarchy. The identifier should be a positive
+ * number.
+ *
+ * @see #NO_ID
+ * @see #getId
+ * @see #findViewById
+ *
+ * @param id a number used to identify the view
+ *
+ * @attr ref android.R.styleable#View_id
+ */
+ public void setId(int id) {
+ mID = id;
+ }
+
+ /**
+ * {@hide}
+ *
+ * @param isRoot true if the view belongs to the root namespace, false
+ * otherwise
+ */
+ public void setIsRootNamespace(boolean isRoot) {
+ if (isRoot) {
+ mPrivateFlags |= IS_ROOT_NAMESPACE;
+ } else {
+ mPrivateFlags &= ~IS_ROOT_NAMESPACE;
+ }
+ }
+
+ /**
+ * {@hide}
+ *
+ * @return true if the view belongs to the root namespace, false otherwise
+ */
+ public boolean isRootNamespace() {
+ return (mPrivateFlags&IS_ROOT_NAMESPACE) != 0;
+ }
+
+ /**
+ * Returns this view's identifier.
+ *
+ * @return a positive integer used to identify the view or {@link #NO_ID}
+ * if the view has no ID
+ *
+ * @see #setId
+ * @see #findViewById
+ * @attr ref android.R.styleable#View_id
+ */
+ public int getId() {
+ return mID;
+ }
+
+ /**
+ * Returns this view's tag.
+ *
+ * @return the Object stored in this view as a tag
+ */
+ @ViewDebug.ExportedProperty
+ public Object getTag() {
+ return mTag;
+ }
+
+ /**
+ * Sets the tag associated with this view. A tag can be used to mark
+ * a view in its hierarchy and does not have to be unique within the
+ * hierarchy. Tags can also be used to store data within a view without
+ * resorting to another data structure.
+ *
+ * @param tag an Object to tag the view with
+ */
+ public void setTag(final Object tag) {
+ mTag = tag;
+ }
+
+ /**
+ * Prints information about this view in the log output, with the tag
+ * {@link #VIEW_LOG_TAG}.
+ *
+ * @hide
+ */
+ public void debug() {
+ debug(0);
+ }
+
+ /**
+ * Prints information about this view in the log output, with the tag
+ * {@link #VIEW_LOG_TAG}. Each line in the output is preceded with an
+ * indentation defined by the <code>depth</code>.
+ *
+ * @param depth the indentation level
+ *
+ * @hide
+ */
+ protected void debug(int depth) {
+ String output = debugIndent(depth - 1);
+
+ output += "+ " + this;
+ int id = getId();
+ if (id != -1) {
+ output += " (id=" + id + ")";
+ }
+ Object tag = getTag();
+ if (tag != null) {
+ output += " (tag=" + tag + ")";
+ }
+ Log.d(VIEW_LOG_TAG, output);
+
+ if ((mPrivateFlags & FOCUSED) != 0) {
+ output = debugIndent(depth) + " FOCUSED";
+ Log.d(VIEW_LOG_TAG, output);
+ }
+
+ output = debugIndent(depth);
+ output += "frame={" + mLeft + ", " + mTop + ", " + mRight
+ + ", " + mBottom + "} scroll={" + mScrollX + ", " + mScrollY
+ + "} ";
+ Log.d(VIEW_LOG_TAG, output);
+
+ if (mPaddingLeft != 0 || mPaddingTop != 0 || mPaddingRight != 0
+ || mPaddingBottom != 0) {
+ output = debugIndent(depth);
+ output += "padding={" + mPaddingLeft + ", " + mPaddingTop
+ + ", " + mPaddingRight + ", " + mPaddingBottom + "}";
+ Log.d(VIEW_LOG_TAG, output);
+ }
+
+ output = debugIndent(depth);
+ output += "mMeasureWidth=" + mMeasuredWidth +
+ " mMeasureHeight=" + mMeasuredHeight;
+ Log.d(VIEW_LOG_TAG, output);
+
+ output = debugIndent(depth);
+ if (mLayoutParams == null) {
+ output += "BAD! no layout params";
+ } else {
+ output = mLayoutParams.debug(output);
+ }
+ Log.d(VIEW_LOG_TAG, output);
+
+ output = debugIndent(depth);
+ output += "flags={";
+ output += View.printFlags(mViewFlags);
+ output += "}";
+ Log.d(VIEW_LOG_TAG, output);
+
+ output = debugIndent(depth);
+ output += "privateFlags={";
+ output += View.printPrivateFlags(mPrivateFlags);
+ output += "}";
+ Log.d(VIEW_LOG_TAG, output);
+ }
+
+ /**
+ * Creates an string of whitespaces used for indentation.
+ *
+ * @param depth the indentation level
+ * @return a String containing (depth * 2 + 3) * 2 white spaces
+ *
+ * @hide
+ */
+ protected static String debugIndent(int depth) {
+ StringBuilder spaces = new StringBuilder((depth * 2 + 3) * 2);
+ for (int i = 0; i < (depth * 2) + 3; i++) {
+ spaces.append(' ').append(' ');
+ }
+ return spaces.toString();
+ }
+
+ /**
+ * <p>Return the offset of the widget's text baseline from the widget's top
+ * boundary. If this widget does not support baseline alignment, this
+ * method returns -1. </p>
+ *
+ * @return the offset of the baseline within the widget's bounds or -1
+ * if baseline alignment is not supported
+ */
+ @ViewDebug.ExportedProperty
+ public int getBaseline() {
+ return -1;
+ }
+
+ /**
+ * Call this when something has changed which has invalidated the
+ * layout of this view. This will schedule a layout pass of the view
+ * tree.
+ */
+ public void requestLayout() {
+ if (ViewDebug.TRACE_HIERARCHY) {
+ ViewDebug.trace(this, ViewDebug.HierarchyTraceType.REQUEST_LAYOUT);
+ }
+
+ mPrivateFlags |= FORCE_LAYOUT;
+
+ if (mParent != null && !mParent.isLayoutRequested()) {
+ mParent.requestLayout();
+ }
+ }
+
+ /**
+ * Forces this view to be laid out during the next layout pass.
+ * This method does not call requestLayout() or forceLayout()
+ * on the parent.
+ */
+ public void forceLayout() {
+ mPrivateFlags |= FORCE_LAYOUT;
+ }
+
+ /**
+ * <p>
+ * This is called to find out how big a view should be. The parent
+ * supplies constraint information in the width and height parameters.
+ * </p>
+ *
+ * <p>
+ * The actual mesurement work of a view is performed in
+ * {@link #onMeasure(int, int)}, called by this method. Therefore, only
+ * {@link #onMeasure(int, int)} can and must be overriden by subclasses.
+ * </p>
+ *
+ *
+ * @param widthMeasureSpec Horizontal space requirements as imposed by the
+ * parent
+ * @param heightMeasureSpec Vertical space requirements as imposed by the
+ * parent
+ *
+ * @see #onMeasure(int, int)
+ */
+ public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
+ if ((mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT ||
+ widthMeasureSpec != mOldWidthMeasureSpec ||
+ heightMeasureSpec != mOldHeightMeasureSpec) {
+
+ // first clears the measured dimension flag
+ mPrivateFlags &= ~MEASURED_DIMENSION_SET;
+
+ if (ViewDebug.TRACE_HIERARCHY) {
+ ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_MEASURE);
+ }
+
+ // measure ourselves, this should set the measured dimension flag back
+ onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ // flag not set, setMeasuredDimension() was not invoked, we raise
+ // an exception to warn the developer
+ if ((mPrivateFlags & MEASURED_DIMENSION_SET) != MEASURED_DIMENSION_SET) {
+ throw new IllegalStateException("onMeasure() did not set the"
+ + " measured dimension by calling"
+ + " setMeasuredDimension()");
+ }
+
+ mPrivateFlags |= LAYOUT_REQUIRED;
+ }
+
+ mOldWidthMeasureSpec = widthMeasureSpec;
+ mOldHeightMeasureSpec = heightMeasureSpec;
+ }
+
+ /**
+ * <p>
+ * Measure the view and its content to determine the measured width and the
+ * measured height. This method is invoked by {@link #measure(int, int)} and
+ * should be overriden by subclasses to provide accurate and efficient
+ * measurement of their contents.
+ * </p>
+ *
+ * <p>
+ * <strong>CONTRACT:</strong> When overriding this method, you
+ * <em>must</em> call {@link #setMeasuredDimension(int, int)} to store the
+ * measured width and height of this view. Failure to do so will trigger an
+ * <code>IllegalStateException</code>, thrown by
+ * {@link #measure(int, int)}. Calling the superclass'
+ * {@link #onMeasure(int, int)} is a valid use.
+ * </p>
+ *
+ * <p>
+ * The base class implementation of measure defaults to the background size,
+ * unless a larger size is allowed by the MeasureSpec. Subclasses should
+ * override {@link #onMeasure(int, int)} to provide better measurements of
+ * their content.
+ * </p>
+ *
+ * <p>
+ * If this method is overridden, it is the subclass's responsibility to make
+ * sure the measured height and width are at least the view's minimum height
+ * and width ({@link #getSuggestedMinimumHeight()} and
+ * {@link #getSuggestedMinimumWidth()}).
+ * </p>
+ *
+ * @param widthMeasureSpec horizontal space requirements as imposed by the parent.
+ * The requirements are encoded with
+ * {@link android.view.View.MeasureSpec}.
+ * @param heightMeasureSpec vertical space requirements as imposed by the parent.
+ * The requirements are encoded with
+ * {@link android.view.View.MeasureSpec}.
+ *
+ * @see #getMeasuredWidth()
+ * @see #getMeasuredHeight()
+ * @see #setMeasuredDimension(int, int)
+ * @see #getSuggestedMinimumHeight()
+ * @see #getSuggestedMinimumWidth()
+ * @see android.view.View.MeasureSpec#getMode(int)
+ * @see android.view.View.MeasureSpec#getSize(int)
+ */
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
+ getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
+ }
+
+ /**
+ * <p>This mehod must be called by {@link #onMeasure(int, int)} to store the
+ * measured width and measured height. Failing to do so will trigger an
+ * exception at measurement time.</p>
+ *
+ * @param measuredWidth the measured width of this view
+ * @param measuredHeight the measured height of this view
+ */
+ protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
+ mMeasuredWidth = measuredWidth;
+ mMeasuredHeight = measuredHeight;
+
+ mPrivateFlags |= MEASURED_DIMENSION_SET;
+ }
+
+ /**
+ * Utility to reconcile a desired size with constraints imposed by a MeasureSpec.
+ * Will take the desired size, unless a different size is imposed by the constraints.
+ *
+ * @param size How big the view wants to be
+ * @param measureSpec Constraints imposed by the parent
+ * @return The size this view should be.
+ */
+ public static int resolveSize(int size, int measureSpec) {
+ int result = size;
+ int specMode = MeasureSpec.getMode(measureSpec);
+ int specSize = MeasureSpec.getSize(measureSpec);
+ switch (specMode) {
+ case MeasureSpec.UNSPECIFIED:
+ result = size;
+ break;
+ case MeasureSpec.AT_MOST:
+ result = Math.min(size, specSize);
+ break;
+ case MeasureSpec.EXACTLY:
+ result = specSize;
+ break;
+ }
+ return result;
+ }
+
+ /**
+ * Utility to return a default size. Uses the supplied size if the
+ * MeasureSpec imposed no contraints. Will get larger if allowed
+ * by the MeasureSpec.
+ *
+ * @param size Default size for this view
+ * @param measureSpec Constraints imposed by the parent
+ * @return The size this view should be.
+ */
+ public static int getDefaultSize(int size, int measureSpec) {
+ int result = size;
+ int specMode = MeasureSpec.getMode(measureSpec);
+ int specSize = MeasureSpec.getSize(measureSpec);
+
+ switch (specMode) {
+ case MeasureSpec.UNSPECIFIED:
+ result = size;
+ break;
+ case MeasureSpec.AT_MOST:
+ case MeasureSpec.EXACTLY:
+ result = specSize;
+ break;
+ }
+ return result;
+ }
+
+ /**
+ * Returns the suggested minimum height that the view should use. This
+ * returns the maximum of the view's minimum height
+ * and the background's minimum height
+ * ({@link android.graphics.drawable.Drawable#getMinimumHeight()}).
+ * <p>
+ * When being used in {@link #onMeasure(int, int)}, the caller should still
+ * ensure the returned height is within the requirements of the parent.
+ *
+ * @return The suggested minimum height of the view.
+ */
+ protected int getSuggestedMinimumHeight() {
+ int suggestedMinHeight = mMinHeight;
+
+ if (mBGDrawable != null) {
+ final int bgMinHeight = mBGDrawable.getMinimumHeight();
+ if (suggestedMinHeight < bgMinHeight) {
+ suggestedMinHeight = bgMinHeight;
+ }
+ }
+
+ return suggestedMinHeight;
+ }
+
+ /**
+ * Returns the suggested minimum width that the view should use. This
+ * returns the maximum of the view's minimum width)
+ * and the background's minimum width
+ * ({@link android.graphics.drawable.Drawable#getMinimumWidth()}).
+ * <p>
+ * When being used in {@link #onMeasure(int, int)}, the caller should still
+ * ensure the returned width is within the requirements of the parent.
+ *
+ * @return The suggested minimum width of the view.
+ */
+ protected int getSuggestedMinimumWidth() {
+ int suggestedMinWidth = mMinWidth;
+
+ if (mBGDrawable != null) {
+ final int bgMinWidth = mBGDrawable.getMinimumWidth();
+ if (suggestedMinWidth < bgMinWidth) {
+ suggestedMinWidth = bgMinWidth;
+ }
+ }
+
+ return suggestedMinWidth;
+ }
+
+ /**
+ * Sets the minimum height of the view. It is not guaranteed the view will
+ * be able to achieve this minimum height (for example, if its parent layout
+ * constrains it with less available height).
+ *
+ * @param minHeight The minimum height the view will try to be.
+ */
+ public void setMinimumHeight(int minHeight) {
+ mMinHeight = minHeight;
+ }
+
+ /**
+ * Sets the minimum width of the view. It is not guaranteed the view will
+ * be able to achieve this minimum width (for example, if its parent layout
+ * constrains it with less available width).
+ *
+ * @param minWidth The minimum width the view will try to be.
+ */
+ public void setMinimumWidth(int minWidth) {
+ mMinWidth = minWidth;
+ }
+
+ /**
+ * Get the animation currently associated with this view.
+ *
+ * @return The animation that is currently playing or
+ * scheduled to play for this view.
+ */
+ public Animation getAnimation() {
+ return mCurrentAnimation;
+ }
+
+ /**
+ * Start the specified animation now.
+ *
+ * @param animation the animation to start now
+ */
+ public void startAnimation(Animation animation) {
+ animation.setStartTime(Animation.START_ON_FIRST_FRAME);
+ setAnimation(animation);
+ invalidate();
+ }
+
+ /**
+ * Cancels any animations for this view.
+ */
+ public void clearAnimation() {
+ mCurrentAnimation = null;
+ }
+
+ /**
+ * Sets the next animation to play for this view.
+ * If you want the animation to play immediately, use
+ * startAnimation. This method provides allows fine-grained
+ * control over the start time and invalidation, but you
+ * must make sure that 1) the animation has a start time set, and
+ * 2) the view will be invalidated when the animation is supposed to
+ * start.
+ *
+ * @param animation The next animation, or null.
+ */
+ public void setAnimation(Animation animation) {
+ mCurrentAnimation = animation;
+ if (animation != null) {
+ animation.reset();
+ }
+ }
+
+ /**
+ * Invoked by a parent ViewGroup to notify the start of the animation
+ * currently associated with this view. If you override this method,
+ * always call super.onAnimationStart();
+ *
+ * @see #setAnimation(android.view.animation.Animation)
+ * @see #getAnimation()
+ */
+ protected void onAnimationStart() {
+ mPrivateFlags |= ANIMATION_STARTED;
+ }
+
+ /**
+ * Invoked by a parent ViewGroup to notify the end of the animation
+ * currently associated with this view. If you override this method,
+ * always call super.onAnimationEnd();
+ *
+ * @see #setAnimation(android.view.animation.Animation)
+ * @see #getAnimation()
+ */
+ protected void onAnimationEnd() {
+ mPrivateFlags &= ~ANIMATION_STARTED;
+ }
+
+ /**
+ * Invoked if there is a Transform that involves alpha. Subclass that can
+ * draw themselves with the specified alpha should return true, and then
+ * respect that alpha when their onDraw() is called. If this returns false
+ * then the view may be redirected to draw into an offscreen buffer to
+ * fulfill the request, which will look fine, but may be slower than if the
+ * subclass handles it internally. The default implementation returns false.
+ *
+ * @param alpha The alpha (0..255) to apply to the view's drawing
+ * @return true if the view can draw with the specified alpha.
+ */
+ protected boolean onSetAlpha(int alpha) {
+ return false;
+ }
+
+ /**
+ * This is used by the RootView to perform an optimization when
+ * the view hierarchy contains one or several SurfaceView.
+ * SurfaceView is always considered transparent, but its children are not,
+ * therefore all View objects remove themselves from the global transparent
+ * region (passed as a parameter to this function).
+ *
+ * @param region The transparent region for this ViewRoot (window).
+ *
+ * @return Returns true if the effective visibility of the view at this
+ * point is opaque, regardless of the transparent region; returns false
+ * if it is possible for underlying windows to be seen behind the view.
+ *
+ * {@hide}
+ */
+ public boolean gatherTransparentRegion(Region region) {
+ if (region != null) {
+ final int pflags = mPrivateFlags;
+ if ((pflags & SKIP_DRAW) == 0) {
+ // The SKIP_DRAW flag IS NOT set, so this view draws. We need to
+ // remove it from the transparent region.
+ getLocationInWindow(mLocation);
+ region.op(mLocation[0], mLocation[1],
+ mLocation[0] + mRight - mLeft, mLocation[1] + mBottom - mTop,
+ Region.Op.DIFFERENCE);
+ } else if ((pflags & ONLY_DRAWS_BACKGROUND) != 0 && mBGDrawable != null) {
+ // The ONLY_DRAWS_BACKGROUND flag IS set and the background drawable
+ // exists, so we remove the background drawable's non-transparent
+ // parts from this transparent region.
+ applyDrawableToTransparentRegion(mBGDrawable, region);
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Play a sound effect for this view.
+ *
+ * The framework will play sound effects for some built in actions, such as
+ * clicking, but you may wish to play these effects in your widget,
+ * for instance, for internal navigation.
+ *
+ * The sound effect will only be played if sound effects are enabled by the user, and
+ * {@link #isSoundEffectsEnabled()} is true.
+ *
+ * @param soundConstant One of the constants defined in {@link SoundEffectConstants}
+ */
+ protected void playSoundEffect(int soundConstant) {
+ if (mAttachInfo == null || mAttachInfo.mSoundEffectPlayer == null || !isSoundEffectsEnabled()) {
+ return;
+ }
+ mAttachInfo.mSoundEffectPlayer.playSoundEffect(soundConstant);
+ }
+
+ /**
+ * Given a Drawable whose bounds have been set to draw into this view,
+ * update a Region being computed for {@link #gatherTransparentRegion} so
+ * that any non-transparent parts of the Drawable are removed from the
+ * given transparent region.
+ *
+ * @param dr The Drawable whose transparency is to be applied to the region.
+ * @param region A Region holding the current transparency information,
+ * where any parts of the region that are set are considered to be
+ * transparent. On return, this region will be modified to have the
+ * transparency information reduced by the corresponding parts of the
+ * Drawable that are not transparent.
+ * {@hide}
+ */
+ public void applyDrawableToTransparentRegion(Drawable dr, Region region) {
+ if (DBG) {
+ Log.i("View", "Getting transparent region for: " + this);
+ }
+ final Region r = dr.getTransparentRegion();
+ final Rect db = dr.getBounds();
+ if (r != null) {
+ final int w = getRight()-getLeft();
+ final int h = getBottom()-getTop();
+ if (db.left > 0) {
+ //Log.i("VIEW", "Drawable left " + db.left + " > view 0");
+ r.op(0, 0, db.left, h, Region.Op.UNION);
+ }
+ if (db.right < w) {
+ //Log.i("VIEW", "Drawable right " + db.right + " < view " + w);
+ r.op(db.right, 0, w, h, Region.Op.UNION);
+ }
+ if (db.top > 0) {
+ //Log.i("VIEW", "Drawable top " + db.top + " > view 0");
+ r.op(0, 0, w, db.top, Region.Op.UNION);
+ }
+ if (db.bottom < h) {
+ //Log.i("VIEW", "Drawable bottom " + db.bottom + " < view " + h);
+ r.op(0, db.bottom, w, h, Region.Op.UNION);
+ }
+ getLocationInWindow(mLocation);
+ r.translate(mLocation[0], mLocation[1]);
+ region.op(r, Region.Op.INTERSECT);
+ } else {
+ region.op(db, Region.Op.DIFFERENCE);
+ }
+ }
+
+ private void postCheckForLongClick() {
+ mHasPerformedLongPress = false;
+
+ if (mPendingCheckForLongPress == null) {
+ mPendingCheckForLongPress = new CheckForLongPress();
+ }
+ mPendingCheckForLongPress.rememberWindowAttachCount();
+ postDelayed(mPendingCheckForLongPress, ViewConfiguration.getLongPressTimeout());
+ }
+
+ private static int[] stateSetUnion(final int[] stateSet1,
+ final int[] stateSet2) {
+ final int stateSet1Length = stateSet1.length;
+ final int stateSet2Length = stateSet2.length;
+ final int[] newSet = new int[stateSet1Length + stateSet2Length];
+ int k = 0;
+ int i = 0;
+ int j = 0;
+ // This is a merge of the two input state sets and assumes that the
+ // input sets are sorted by the order imposed by ViewDrawableStates.
+ for (int viewState : R.styleable.ViewDrawableStates) {
+ if (i < stateSet1Length && stateSet1[i] == viewState) {
+ newSet[k++] = viewState;
+ i++;
+ } else if (j < stateSet2Length && stateSet2[j] == viewState) {
+ newSet[k++] = viewState;
+ j++;
+ }
+ if (k > 1) {
+ assert(newSet[k - 1] > newSet[k - 2]);
+ }
+ }
+ return newSet;
+ }
+
+ /**
+ * Inflate a view from an XML resource. This convenience method wraps the {@link
+ * LayoutInflater} class, which provides a full range of options for view inflation.
+ *
+ * @param context The Context object for your activity or application.
+ * @param resource The resource ID to inflate
+ * @param root A view group that will be the parent. Used to properly inflate the
+ * layout_* parameters.
+ * @see LayoutInflater
+ */
+ public static View inflate(Context context, int resource, ViewGroup root) {
+ LayoutInflater factory = LayoutInflater.from(context);
+ return factory.inflate(resource, root);
+ }
+
+ /**
+ * A MeasureSpec encapsulates the layout requirements passed from parent to child.
+ * Each MeasureSpec represents a requirement for either the width or the height.
+ * A MeasureSpec is comprised of a size and a mode. There are three possible
+ * modes:
+ * <dl>
+ * <dt>UNSPECIFIED</dt>
+ * <dd>
+ * The parent has not imposed any constraint on the child. It can be whatever size
+ * it wants.
+ * </dd>
+ *
+ * <dt>EXACTLY</dt>
+ * <dd>
+ * The parent has determined an exact size for the child. The child is going to be
+ * given those bounds regardless of how big it wants to be.
+ * </dd>
+ *
+ * <dt>AT_MOST</dt>
+ * <dd>
+ * The child can be as large as it wants up to the specified size.
+ * </dd>
+ * </dl>
+ *
+ * MeasureSpecs are implemented as ints to reduce object allocation. This class
+ * is provided to pack and unpack the &lt;size, mode&gt; tuple into the int.
+ */
+ public static class MeasureSpec {
+ private static final int MODE_SHIFT = 30;
+ private static final int MODE_MASK = 0x3 << MODE_SHIFT;
+
+ /**
+ * Measure specification mode: The parent has not imposed any constraint
+ * on the child. It can be whatever size it wants.
+ */
+ public static final int UNSPECIFIED = 0 << MODE_SHIFT;
+
+ /**
+ * Measure specification mode: The parent has determined an exact size
+ * for the child. The child is going to be given those bounds regardless
+ * of how big it wants to be.
+ */
+ public static final int EXACTLY = 1 << MODE_SHIFT;
+
+ /**
+ * Measure specification mode: The child can be as large as it wants up
+ * to the specified size.
+ */
+ public static final int AT_MOST = 2 << MODE_SHIFT;
+
+ /**
+ * Creates a measure specification based on the supplied size and mode.
+ *
+ * The mode must always be one of the following:
+ * <ul>
+ * <li>{@link android.view.View.MeasureSpec#UNSPECIFIED}</li>
+ * <li>{@link android.view.View.MeasureSpec#EXACTLY}</li>
+ * <li>{@link android.view.View.MeasureSpec#AT_MOST}</li>
+ * </ul>
+ *
+ * @param size the size of the measure specification
+ * @param mode the mode of the measure specification
+ * @return the measure specification based on size and mode
+ */
+ public static int makeMeasureSpec(int size, int mode) {
+ return size + mode;
+ }
+
+ /**
+ * Extracts the mode from the supplied measure specification.
+ *
+ * @param measureSpec the measure specification to extract the mode from
+ * @return {@link android.view.View.MeasureSpec#UNSPECIFIED},
+ * {@link android.view.View.MeasureSpec#AT_MOST} or
+ * {@link android.view.View.MeasureSpec#EXACTLY}
+ */
+ public static int getMode(int measureSpec) {
+ return (measureSpec & MODE_MASK);
+ }
+
+ /**
+ * Extracts the size from the supplied measure specification.
+ *
+ * @param measureSpec the measure specification to extract the size from
+ * @return the size in pixels defined in the supplied measure specification
+ */
+ public static int getSize(int measureSpec) {
+ return (measureSpec & ~MODE_MASK);
+ }
+
+ /**
+ * Returns a String representation of the specified measure
+ * specification.
+ *
+ * @param measureSpec the measure specification to convert to a String
+ * @return a String with the following format: "MeasureSpec: MODE SIZE"
+ */
+ public static String toString(int measureSpec) {
+ int mode = getMode(measureSpec);
+ int size = getSize(measureSpec);
+
+ StringBuilder sb = new StringBuilder("MeasureSpec: ");
+
+ if (mode == UNSPECIFIED)
+ sb.append("UNSPECIFIED ");
+ else if (mode == EXACTLY)
+ sb.append("EXACTLY ");
+ else if (mode == AT_MOST)
+ sb.append("AT_MOST ");
+ else
+ sb.append(mode).append(" ");
+
+ sb.append(size);
+ return sb.toString();
+ }
+ }
+
+ class CheckForLongPress implements Runnable {
+
+ private int mOriginalWindowAttachCount;
+
+ public void run() {
+ if (isPressed() && (mParent != null) && hasWindowFocus()
+ && mOriginalWindowAttachCount == mWindowAttachCount) {
+ if (performLongClick()) {
+ mHasPerformedLongPress = true;
+ }
+ }
+ }
+
+ public void rememberWindowAttachCount() {
+ mOriginalWindowAttachCount = mWindowAttachCount;
+ }
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when a key event is
+ * dispatched to this view. The callback will be invoked before the key
+ * event is given to the view.
+ */
+ public interface OnKeyListener {
+ /**
+ * Called when a key is dispatched to a view. This allows listeners to
+ * get a chance to respond before the target view.
+ *
+ * @param v The view the key has been dispatched to.
+ * @param keyCode The code for the physical key that was pressed
+ * @param event The KeyEvent object containing full information about
+ * the event.
+ * @return True if the listener has consumed the event, false otherwise.
+ */
+ boolean onKey(View v, int keyCode, KeyEvent event);
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when a touch event is
+ * dispatched to this view. The callback will be invoked before the touch
+ * event is given to the view.
+ */
+ public interface OnTouchListener {
+ /**
+ * Called when a touch event is dispatched to a view. This allows listeners to
+ * get a chance to respond before the target view.
+ *
+ * @param v The view the touch event has been dispatched to.
+ * @param event The MotionEvent object containing full information about
+ * the event.
+ * @return True if the listener has consumed the event, false otherwise.
+ */
+ boolean onTouch(View v, MotionEvent event);
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when a view has been clicked and held.
+ */
+ public interface OnLongClickListener {
+ /**
+ * Called when a view has been clicked and held.
+ *
+ * @param v The view that was clicked and held.
+ *
+ * return True if the callback consumed the long click, false otherwise
+ */
+ boolean onLongClick(View v);
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when the focus state of
+ * a view changed.
+ */
+ public interface OnFocusChangeListener {
+ /**
+ * Called when the focus state of a view has changed.
+ *
+ * @param v The view whose state has changed.
+ * @param hasFocus The new focus state of v.
+ */
+ void onFocusChange(View v, boolean hasFocus);
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when a view is clicked.
+ */
+ public interface OnClickListener {
+ /**
+ * Called when a view has been clicked.
+ *
+ * @param v The view that was clicked.
+ */
+ void onClick(View v);
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when the context menu
+ * for this view is being built.
+ */
+ public interface OnCreateContextMenuListener {
+ /**
+ * Called when the context menu for this view is being built. It is not
+ * safe to hold onto the menu after this method returns.
+ *
+ * @param menu The context menu that is being built
+ * @param v The view for which the context menu is being built
+ * @param menuInfo Extra information about the item for which the
+ * context menu should be shown. This information will vary
+ * depending on the class of v.
+ */
+ void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo);
+ }
+
+ private final class UnsetPressedState implements Runnable {
+ public void run() {
+ setPressed(false);
+ }
+ }
+
+ /**
+ * Base class for derived classes that want to save and restore their own
+ * state in {@link #onSaveInstanceState}.
+ */
+ public static class BaseSavedState extends AbsSavedState {
+ /**
+ * Constructor used when reading from a parcel. Reads the state of the superclass.
+ *
+ * @param source
+ */
+ public BaseSavedState(Parcel source) {
+ super(source);
+ }
+
+ /**
+ * Constructor called by derived classes when creating their SavedState objects
+ *
+ * @param superState The state of the superclass of this view
+ */
+ public BaseSavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ public static final Parcelable.Creator<BaseSavedState> CREATOR =
+ new Parcelable.Creator<BaseSavedState>() {
+ public BaseSavedState createFromParcel(Parcel in) {
+ return new BaseSavedState(in);
+ }
+
+ public BaseSavedState[] newArray(int size) {
+ return new BaseSavedState[size];
+ }
+ };
+ }
+
+ /**
+ * A set of information given to a view when it is attached to its parent
+ * window.
+ */
+ static class AttachInfo {
+
+ interface SoundEffectPlayer {
+ void playSoundEffect(int effectId);
+ }
+
+ IBinder mWindowToken;
+ IBinder mPanelParentWindowToken;
+ Surface mSurface;
+ IWindowSession mSession;
+ SoundEffectPlayer mSoundEffectPlayer;
+
+ /**
+ * Left position of this view's window
+ */
+ int mWindowLeft;
+
+ /**
+ * Top position of this view's window
+ */
+ int mWindowTop;
+
+ /**
+ * Indicates whether the view's window currently has the focus.
+ */
+ boolean mHasWindowFocus;
+
+ /**
+ * The current visibility of the window.
+ */
+ int mWindowVisibility;
+
+ /**
+ * Indicates the time at which drawing started to occur.
+ */
+ long mDrawingTime;
+
+ /**
+ * Indicates whether the view's window is currently in touch mode.
+ */
+ boolean mInTouchMode;
+
+ /**
+ * Indicates that ViewRoot should trigger a global layout change
+ * the next time it performs a traversal
+ */
+ boolean mRecomputeGlobalAttributes;
+
+ /**
+ * Set to true when attributes (like mKeepScreenOn) need to be
+ * recomputed.
+ */
+ boolean mAttributesChanged;
+
+ /**
+ * Set during a traveral if any views want to keep the screen on.
+ */
+ boolean mKeepScreenOn;
+
+ /**
+ * The view tree observer used to dispatch global events like
+ * layout, pre-draw, touch mode change, etc.
+ */
+ final ViewTreeObserver mTreeObserver = new ViewTreeObserver();
+
+ /**
+ * A Canvas used by the view hierarchy to perform bitmap caching.
+ */
+ Canvas mCanvas;
+
+ /**
+ * A Handler supplied by a view's {@link android.view.ViewRoot}. This
+ * handler can be used to pump events in the UI events queue.
+ */
+ final Handler mHandler;
+
+ /**
+ * Identifier for messages requesting the view to be invalidated.
+ * Such messages should be sent to {@link #mHandler}.
+ */
+ static final int INVALIDATE_MSG = 0x1;
+
+ /**
+ * Identifier for messages requesting the view to invalidate a region.
+ * Such messages should be sent to {@link #mHandler}.
+ */
+ static final int INVALIDATE_RECT_MSG = 0x2;
+
+ AttachInfo(Handler handler) {
+ this(handler, null);
+ }
+
+ /**
+ * Creates a new set of attachment information with the specified
+ * events handler and thread.
+ *
+ * @param handler the events handler the view must use
+ */
+ AttachInfo(Handler handler, SoundEffectPlayer effectPlayer) {
+ mHandler = handler;
+ mSoundEffectPlayer = effectPlayer;
+ }
+ }
+
+ /**
+ * <p>ScrollabilityCache holds various fields used by a View when scrolling
+ * is supported. This avoids keeping too many unused fields in most
+ * instances of View.</p>
+ */
+ private static class ScrollabilityCache {
+ public int fadingEdgeLength = ViewConfiguration.getFadingEdgeLength();
+
+ public int scrollBarSize = ViewConfiguration.getScrollBarSize();
+ public ScrollBarDrawable scrollBar;
+
+ public final Paint paint;
+ public final Matrix matrix;
+ public Shader shader;
+
+ private int mLastColor = 0;
+
+ public ScrollabilityCache() {
+ paint = new Paint();
+ matrix = new Matrix();
+ // use use a height of 1, and then wack the matrix each time we
+ // actually use it.
+ shader = new LinearGradient(0, 0, 0, 1, 0xFF000000, 0, Shader.TileMode.CLAMP);
+
+ paint.setShader(shader);
+ paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
+ }
+
+ public void setFadeColor(int color) {
+ if (color != 0 && color != mLastColor) {
+ mLastColor = color;
+ color |= 0xFF000000;
+
+ shader = new LinearGradient(0, 0, 0, 1, color, 0, Shader.TileMode.CLAMP);
+
+ paint.setShader(shader);
+ paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER));
+ }
+ }
+ }
+}
diff --git a/core/java/android/view/ViewConfiguration.java b/core/java/android/view/ViewConfiguration.java
new file mode 100644
index 0000000..38be806
--- /dev/null
+++ b/core/java/android/view/ViewConfiguration.java
@@ -0,0 +1,228 @@
+/*
+ * 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.view;
+
+/**
+ * Contains methods to standard constants used in the UI for timeouts, sizes, and distances.
+ *
+ */
+public class ViewConfiguration {
+
+ /**
+ * Defines the width of the horizontal scrollbar and the height of the vertical scrollbar in
+ * pixels
+ */
+ private static final int SCROLL_BAR_SIZE = 6;
+
+ /**
+ * Defines the length of the fading edges in pixels
+ */
+ private static final int FADING_EDGE_LENGTH = 12;
+
+ /**
+ * Defines the duration in milliseconds of the pressed state in child
+ * components.
+ */
+ private static final int PRESSED_STATE_DURATION = 85;
+
+ /**
+ * Defines the duration in milliseconds before a press turns into
+ * a long press
+ */
+ private static final int LONG_PRESS_TIMEOUT = 500;
+
+ /**
+ * Defines the duration in milliseconds we will wait to see if a touch event
+ * is a top or a scroll. If the user does not move within this interval, it is
+ * considered to be a tap.
+ */
+ private static final int TAP_TIMEOUT = 100;
+
+ /**
+ * Defines the duration in milliseconds we will wait to see if a touch event
+ * is a jump tap. If the user does not complete the jump tap within this interval, it is
+ * considered to be a tap.
+ */
+ private static final int JUMP_TAP_TIMEOUT = 500;
+
+ /**
+ * Defines the duration in milliseconds we want to display zoom controls in response
+ * to a user panning within an application.
+ */
+ private static final int ZOOM_CONTROLS_TIMEOUT = 3000;
+
+ /**
+ * Defines the duration in milliseconds a user needs to hold down the
+ * appropriate button to bring up the global actions dialog (power off,
+ * lock screen, etc).
+ */
+ private static final int GLOBAL_ACTIONS_KEY_TIMEOUT = 1000;
+
+
+ /**
+ * Inset in pixels to look for touchable content when the user touches the edge of the screen
+ */
+ private static final int EDGE_SLOP = 12;
+
+ /**
+ * Distance a touch can wander before we think the user is scrolling in pixels
+ */
+ private static final int TOUCH_SLOP = 12;
+
+ /**
+ * Distance 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
+ */
+ private static final int MINIMUM_FLING_VELOCITY = 50;
+
+ /**
+ * The maximum size of View's drawing cache, expressed in bytes. This size
+ * should be at least equal to the size of the screen in ARGB888 format.
+ */
+ private static final int MAXIMUM_DRAWING_CACHE_SIZE = 320 * 480 * 4; // One HVGA screen, ARGB8888
+
+ /**
+ * The coefficient of friction applied to flings/scrolls.
+ */
+ private static float SCROLL_FRICTION = 0.015f;
+
+ /**
+ * @return The width of the horizontal scrollbar and the height of the vertical
+ * scrollbar in pixels
+ */
+ public static int getScrollBarSize() {
+ return SCROLL_BAR_SIZE;
+ }
+
+ /**
+ * @return Defines the length of the fading edges in pixels
+ */
+ public static int getFadingEdgeLength() {
+ return FADING_EDGE_LENGTH;
+ }
+
+ /**
+ * @return Defines the duration in milliseconds of the pressed state in child
+ * components.
+ */
+ public static int getPressedStateDuration() {
+ return PRESSED_STATE_DURATION;
+ }
+
+ /**
+ * @return Defines the duration in milliseconds before a press turns into
+ * a long press
+ */
+ public static int getLongPressTimeout() {
+ return LONG_PRESS_TIMEOUT;
+ }
+
+ /**
+ * @return Defines the duration in milliseconds we will wait to see if a touch event
+ * is a top or a scroll. If the user does not move within this interval, it is
+ * considered to be a tap.
+ */
+ public static int getTapTimeout() {
+ return TAP_TIMEOUT;
+ }
+
+ /**
+ * @return Defines the duration in milliseconds we will wait to see if a touch event
+ * is a jump tap. If the user does not move within this interval, it is
+ * considered to be a tap.
+ */
+ public static int getJumpTapTimeout() {
+ return JUMP_TAP_TIMEOUT;
+ }
+
+ /**
+ * @return Inset in pixels to look for touchable content when the user touches the edge of the
+ * screen
+ */
+ public static int getEdgeSlop() {
+ return EDGE_SLOP;
+ }
+
+ /**
+ * @return Distance a touch can wander before we think the user is scrolling in pixels
+ */
+ public static int getTouchSlop() {
+ return TOUCH_SLOP;
+ }
+
+ /**
+ * @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.
+ */
+ public static int getWindowTouchSlop() {
+ return WINDOW_TOUCH_SLOP;
+ }
+
+ /**
+ * Minimum velocity to initiate a fling, as measured in pixels per second
+ */
+ public static int getMinimumFlingVelocity() {
+ return MINIMUM_FLING_VELOCITY;
+ }
+
+ /**
+ * The maximum drawing cache size expressed in bytes.
+ *
+ * @return the maximum size of View's drawing cache expressed in bytes
+ */
+ public static int getMaximumDrawingCacheSize() {
+ return MAXIMUM_DRAWING_CACHE_SIZE;
+ }
+
+ /**
+ * The amount of time that the zoom controls should be
+ * displayed on the screen expressed in milliseconds.
+ *
+ * @return the time the zoom controls should be visible expressed
+ * in milliseconds.
+ */
+ public static long getZoomControlsTimeout() {
+ return ZOOM_CONTROLS_TIMEOUT;
+ }
+
+ /**
+ * The amount of time a user needs to press the relevant key to bring up
+ * the global actions dialog.
+ *
+ * @return how long a user needs to press the relevant key to bring up
+ * the global actions dialog.
+ */
+ public static long getGlobalActionKeyTimeout() {
+ return GLOBAL_ACTIONS_KEY_TIMEOUT;
+ }
+
+ /**
+ * The amount of friction applied to scrolls and flings.
+ *
+ * @return A scalar dimensionless value representing the coefficient of
+ * friction.
+ */
+ public static float getScrollFriction() {
+ return SCROLL_FRICTION;
+ }
+}
diff --git a/core/java/android/view/ViewDebug.java b/core/java/android/view/ViewDebug.java
new file mode 100644
index 0000000..1bf46b4
--- /dev/null
+++ b/core/java/android/view/ViewDebug.java
@@ -0,0 +1,927 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.util.Log;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+
+import java.io.File;
+import java.io.BufferedWriter;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.FileOutputStream;
+import java.io.DataOutputStream;
+import java.io.OutputStreamWriter;
+import java.io.BufferedOutputStream;
+import java.io.OutputStream;
+import java.util.List;
+import java.util.LinkedList;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.lang.annotation.Target;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.InvocationTargetException;
+
+/**
+ * Various debugging/tracing tools related to {@link View} and the view hierarchy.
+ */
+public class ViewDebug {
+ /**
+ * Enables or disables view hierarchy tracing. Any invoker of
+ * {@link #trace(View, android.view.ViewDebug.HierarchyTraceType)} should first
+ * check that this value is set to true as not to affect performance.
+ */
+ public static final boolean TRACE_HIERARCHY = false;
+
+ /**
+ * Enables or disables view recycler tracing. Any invoker of
+ * {@link #trace(View, android.view.ViewDebug.RecyclerTraceType, int[])} should first
+ * check that this value is set to true as not to affect performance.
+ */
+ public static final boolean TRACE_RECYCLER = false;
+
+ /**
+ * This annotation can be used to mark fields and methods to be dumped by
+ * the view server. Only non-void methods with no arguments can be annotated
+ * by this annotation.
+ */
+ @Target({ ElementType.FIELD, ElementType.METHOD })
+ @Retention(RetentionPolicy.RUNTIME)
+ public @interface ExportedProperty {
+ /**
+ * When resolveId is true, and if the annotated field/method return value
+ * is an int, the value is converted to an Android's resource name.
+ *
+ * @return true if the property's value must be transformed into an Android
+ * resource name, false otherwise
+ */
+ boolean resolveId() default false;
+
+ /**
+ * A mapping can be defined to map int values to specific strings. For
+ * instance, View.getVisibility() returns 0, 4 or 8. However, these values
+ * actually mean VISIBLE, INVISIBLE and GONE. A mapping can be used to see
+ * these human readable values:
+ *
+ * <pre>
+ * @ViewDebug.ExportedProperty(mapping = {
+ * @ViewDebug.IntToString(from = 0, to = "VISIBLE"),
+ * @ViewDebug.IntToString(from = 4, to = "INVISIBLE"),
+ * @ViewDebug.IntToString(from = 8, to = "GONE")
+ * })
+ * public int getVisibility() { ...
+ * <pre>
+ *
+ * @return An array of int to String mappings
+ *
+ * @see android.view.ViewDebug.IntToString
+ */
+ IntToString[] mapping() default { };
+
+ /**
+ * When deep export is turned on, this property is not dumped. Instead, the
+ * properties contained in this property are dumped. Each child property
+ * is prefixed with the name of this property.
+ *
+ * @return true if the properties of this property should be dumped
+ *
+ * @see #prefix()
+ */
+ boolean deepExport() default false;
+
+ /**
+ * The prefix to use on child properties when deep export is enabled
+ *
+ * @return a prefix as a String
+ *
+ * @see #deepExport()
+ */
+ String prefix() default "";
+ }
+
+ /**
+ * Defines a mapping from an int value to a String. Such a mapping can be used
+ * in a @ExportedProperty to provide more meaningful values to the end user.
+ *
+ * @see android.view.ViewDebug.ExportedProperty
+ */
+ @Target({ ElementType.TYPE })
+ @Retention(RetentionPolicy.RUNTIME)
+ public @interface IntToString {
+ /**
+ * The original int value to map to a String.
+ *
+ * @return An arbitrary int value.
+ */
+ int from();
+
+ /**
+ * The String to use in place of the original int value.
+ *
+ * @return An arbitrary non-null String.
+ */
+ String to();
+ }
+
+ // Maximum delay in ms after which we stop trying to capture a View's drawing
+ private static final int CAPTURE_TIMEOUT = 4000;
+
+ private static final String REMOTE_COMMAND_CAPTURE = "CAPTURE";
+ private static final String REMOTE_COMMAND_DUMP = "DUMP";
+ private static final String REMOTE_COMMAND_INVALIDATE = "INVALIDATE";
+ private static final String REMOTE_COMMAND_REQUEST_LAYOUT = "REQUEST_LAYOUT";
+
+ private static HashMap<Class<?>, Field[]> sFieldsForClasses;
+ private static HashMap<Class<?>, Method[]> sMethodsForClasses;
+
+ /**
+ * Defines the type of hierarhcy trace to output to the hierarchy traces file.
+ */
+ public enum HierarchyTraceType {
+ INVALIDATE,
+ INVALIDATE_CHILD,
+ INVALIDATE_CHILD_IN_PARENT,
+ REQUEST_LAYOUT,
+ ON_LAYOUT,
+ ON_MEASURE,
+ DRAW,
+ BUILD_CACHE
+ }
+
+ private static BufferedWriter sHierarchyTraces;
+ private static ViewRoot sHierarhcyRoot;
+ private static String sHierarchyTracePrefix;
+
+ /**
+ * Defines the type of recycler trace to output to the recycler traces file.
+ */
+ public enum RecyclerTraceType {
+ NEW_VIEW,
+ BIND_VIEW,
+ RECYCLE_FROM_ACTIVE_HEAP,
+ RECYCLE_FROM_SCRAP_HEAP,
+ MOVE_TO_ACTIVE_HEAP,
+ MOVE_TO_SCRAP_HEAP,
+ MOVE_FROM_ACTIVE_TO_SCRAP_HEAP
+ }
+
+ private static class RecyclerTrace {
+ public int view;
+ public RecyclerTraceType type;
+ public int position;
+ public int indexOnScreen;
+ }
+
+ private static View sRecyclerOwnerView;
+ private static List<View> sRecyclerViews;
+ private static List<RecyclerTrace> sRecyclerTraces;
+ private static String sRecyclerTracePrefix;
+
+ /**
+ * Returns the number of instanciated Views.
+ *
+ * @return The number of Views instanciated in the current process.
+ *
+ * @hide
+ */
+ public static long getViewInstanceCount() {
+ return View.sInstanceCount;
+ }
+
+ /**
+ * Returns the number of instanciated ViewRoots.
+ *
+ * @return The number of ViewRoots instanciated in the current process.
+ *
+ * @hide
+ */
+ public static long getViewRootInstanceCount() {
+ return ViewRoot.getInstanceCount();
+ }
+
+ /**
+ * Outputs a trace to the currently opened recycler traces. The trace records the type of
+ * recycler action performed on the supplied view as well as a number of parameters.
+ *
+ * @param view the view to trace
+ * @param type the type of the trace
+ * @param parameters parameters depending on the type of the trace
+ */
+ public static void trace(View view, RecyclerTraceType type, int... parameters) {
+ if (sRecyclerOwnerView == null || sRecyclerViews == null) {
+ return;
+ }
+
+ if (!sRecyclerViews.contains(view)) {
+ sRecyclerViews.add(view);
+ }
+
+ final int index = sRecyclerViews.indexOf(view);
+
+ RecyclerTrace trace = new RecyclerTrace();
+ trace.view = index;
+ trace.type = type;
+ trace.position = parameters[0];
+ trace.indexOnScreen = parameters[1];
+
+ sRecyclerTraces.add(trace);
+ }
+
+ /**
+ * Starts tracing the view recycler of the specified view. The trace is identified by a prefix,
+ * used to build the traces files names: <code>/tmp/view-recycler/PREFIX.traces</code> and
+ * <code>/tmp/view-recycler/PREFIX.recycler</code>.
+ *
+ * Only one view recycler can be traced at the same time. After calling this method, any
+ * other invocation will result in a <code>IllegalStateException</code> unless
+ * {@link #stopRecyclerTracing()} is invoked before.
+ *
+ * Traces files are created only after {@link #stopRecyclerTracing()} is invoked.
+ *
+ * This method will return immediately if TRACE_RECYCLER is false.
+ *
+ * @param prefix the traces files name prefix
+ * @param view the view whose recycler must be traced
+ *
+ * @see #stopRecyclerTracing()
+ * @see #trace(View, android.view.ViewDebug.RecyclerTraceType, int[])
+ */
+ public static void startRecyclerTracing(String prefix, View view) {
+ //noinspection PointlessBooleanExpression,ConstantConditions
+ if (!TRACE_RECYCLER) {
+ return;
+ }
+
+ if (sRecyclerOwnerView != null) {
+ throw new IllegalStateException("You must call stopRecyclerTracing() before running" +
+ " a new trace!");
+ }
+
+ sRecyclerTracePrefix = prefix;
+ sRecyclerOwnerView = view;
+ sRecyclerViews = new ArrayList<View>();
+ sRecyclerTraces = new LinkedList<RecyclerTrace>();
+ }
+
+ /**
+ * Stops the current view recycer tracing.
+ *
+ * Calling this method creates the file <code>/tmp/view-recycler/PREFIX.traces</code>
+ * containing all the traces (or method calls) relative to the specified view's recycler.
+ *
+ * Calling this method creates the file <code>/tmp/view-recycler/PREFIX.recycler</code>
+ * containing all of the views used by the recycler of the view supplied to
+ * {@link #startRecyclerTracing(String, View)}.
+ *
+ * This method will return immediately if TRACE_RECYCLER is false.
+ *
+ * @see #startRecyclerTracing(String, View)
+ * @see #trace(View, android.view.ViewDebug.RecyclerTraceType, int[])
+ */
+ public static void stopRecyclerTracing() {
+ //noinspection PointlessBooleanExpression,ConstantConditions
+ if (!TRACE_RECYCLER) {
+ return;
+ }
+
+ if (sRecyclerOwnerView == null || sRecyclerViews == null) {
+ throw new IllegalStateException("You must call startRecyclerTracing() before" +
+ " stopRecyclerTracing()!");
+ }
+
+ File recyclerDump = new File("/tmp/view-recycler/");
+ recyclerDump.mkdirs();
+
+ recyclerDump = new File(recyclerDump, sRecyclerTracePrefix + ".recycler");
+ try {
+ final BufferedWriter out = new BufferedWriter(new FileWriter(recyclerDump), 8 * 1024);
+
+ for (View view : sRecyclerViews) {
+ final String name = view.getClass().getName();
+ out.write(name);
+ out.newLine();
+ }
+
+ out.close();
+ } catch (IOException e) {
+ Log.e("View", "Could not dump recycler content");
+ return;
+ }
+
+ recyclerDump = new File("/tmp/view-recycler/");
+ recyclerDump = new File(recyclerDump, sRecyclerTracePrefix + ".traces");
+ try {
+ final FileOutputStream file = new FileOutputStream(recyclerDump);
+ final DataOutputStream out = new DataOutputStream(file);
+
+ for (RecyclerTrace trace : sRecyclerTraces) {
+ out.writeInt(trace.view);
+ out.writeInt(trace.type.ordinal());
+ out.writeInt(trace.position);
+ out.writeInt(trace.indexOnScreen);
+ out.flush();
+ }
+
+ out.close();
+ } catch (IOException e) {
+ Log.e("View", "Could not dump recycler traces");
+ return;
+ }
+
+ sRecyclerViews.clear();
+ sRecyclerViews = null;
+
+ sRecyclerTraces.clear();
+ sRecyclerTraces = null;
+
+ sRecyclerOwnerView = null;
+ }
+
+ /**
+ * Outputs a trace to the currently opened traces file. The trace contains the class name
+ * and instance's hashcode of the specified view as well as the supplied trace type.
+ *
+ * @param view the view to trace
+ * @param type the type of the trace
+ */
+ public static void trace(View view, HierarchyTraceType type) {
+ if (sHierarchyTraces == null) {
+ return;
+ }
+
+ try {
+ sHierarchyTraces.write(type.name());
+ sHierarchyTraces.write(' ');
+ sHierarchyTraces.write(view.getClass().getName());
+ sHierarchyTraces.write('@');
+ sHierarchyTraces.write(Integer.toHexString(view.hashCode()));
+ sHierarchyTraces.newLine();
+ } catch (IOException e) {
+ Log.w("View", "Error while dumping trace of type " + type + " for view " + view);
+ }
+ }
+
+ /**
+ * Starts tracing the view hierarchy of the specified view. The trace is identified by a prefix,
+ * used to build the traces files names: <code>/tmp/view-hierarchy/PREFIX.traces</code> and
+ * <code>/tmp/view-hierarchy/PREFIX.tree</code>.
+ *
+ * Only one view hierarchy can be traced at the same time. After calling this method, any
+ * other invocation will result in a <code>IllegalStateException</code> unless
+ * {@link #stopHierarchyTracing()} is invoked before.
+ *
+ * Calling this method creates the file <code>/tmp/view-hierarchy/PREFIX.traces</code>
+ * containing all the traces (or method calls) relative to the specified view's hierarchy.
+ *
+ * This method will return immediately if TRACE_HIERARCHY is false.
+ *
+ * @param prefix the traces files name prefix
+ * @param view the view whose hierarchy must be traced
+ *
+ * @see #stopHierarchyTracing()
+ * @see #trace(View, android.view.ViewDebug.HierarchyTraceType)
+ */
+ public static void startHierarchyTracing(String prefix, View view) {
+ //noinspection PointlessBooleanExpression,ConstantConditions
+ if (!TRACE_HIERARCHY) {
+ return;
+ }
+
+ if (sHierarhcyRoot != null) {
+ throw new IllegalStateException("You must call stopHierarchyTracing() before running" +
+ " a new trace!");
+ }
+
+ File hierarchyDump = new File("/tmp/view-hierarchy/");
+ hierarchyDump.mkdirs();
+
+ hierarchyDump = new File(hierarchyDump, prefix + ".traces");
+ sHierarchyTracePrefix = prefix;
+
+ try {
+ sHierarchyTraces = new BufferedWriter(new FileWriter(hierarchyDump), 8 * 1024);
+ } catch (IOException e) {
+ Log.e("View", "Could not dump view hierarchy");
+ return;
+ }
+
+ sHierarhcyRoot = (ViewRoot) view.getRootView().getParent();
+ }
+
+ /**
+ * Stops the current view hierarchy tracing. This method closes the file
+ * <code>/tmp/view-hierarchy/PREFIX.traces</code>.
+ *
+ * Calling this method creates the file <code>/tmp/view-hierarchy/PREFIX.tree</code> containing
+ * the view hierarchy of the view supplied to {@link #startHierarchyTracing(String, View)}.
+ *
+ * This method will return immediately if TRACE_HIERARCHY is false.
+ *
+ * @see #startHierarchyTracing(String, View)
+ * @see #trace(View, android.view.ViewDebug.HierarchyTraceType)
+ */
+ public static void stopHierarchyTracing() {
+ //noinspection PointlessBooleanExpression,ConstantConditions
+ if (!TRACE_HIERARCHY) {
+ return;
+ }
+
+ if (sHierarhcyRoot == null || sHierarchyTraces == null) {
+ throw new IllegalStateException("You must call startHierarchyTracing() before" +
+ " stopHierarchyTracing()!");
+ }
+
+ try {
+ sHierarchyTraces.close();
+ } catch (IOException e) {
+ Log.e("View", "Could not write view traces");
+ }
+ sHierarchyTraces = null;
+
+ File hierarchyDump = new File("/tmp/view-hierarchy/");
+ hierarchyDump.mkdirs();
+ hierarchyDump = new File(hierarchyDump, sHierarchyTracePrefix + ".tree");
+
+ BufferedWriter out;
+ try {
+ out = new BufferedWriter(new FileWriter(hierarchyDump), 8 * 1024);
+ } catch (IOException e) {
+ Log.e("View", "Could not dump view hierarchy");
+ return;
+ }
+
+ View view = sHierarhcyRoot.getView();
+ if (view instanceof ViewGroup) {
+ ViewGroup group = (ViewGroup) view;
+ dumpViewHierarchy(group, out, 0);
+ try {
+ out.close();
+ } catch (IOException e) {
+ Log.e("View", "Could not dump view hierarchy");
+ }
+ }
+
+ sHierarhcyRoot = null;
+ }
+
+ static void dispatchCommand(View view, String command, String parameters,
+ OutputStream clientStream) throws IOException {
+
+ // Paranoid but safe...
+ view = view.getRootView();
+
+ if (REMOTE_COMMAND_DUMP.equalsIgnoreCase(command)) {
+ dump(view, clientStream);
+ } else {
+ final String[] params = parameters.split(" ");
+ if (REMOTE_COMMAND_CAPTURE.equalsIgnoreCase(command)) {
+ capture(view, clientStream, params[0]);
+ } else if (REMOTE_COMMAND_INVALIDATE.equalsIgnoreCase(command)) {
+ invalidate(view, params[0]);
+ } else if (REMOTE_COMMAND_REQUEST_LAYOUT.equalsIgnoreCase(command)) {
+ requestLayout(view, params[0]);
+ }
+ }
+ }
+
+ private static View findViewByHashCode(View root, String parameter) {
+ final String[] ids = parameter.split("@");
+ final String className = ids[0];
+ final int hashCode = Integer.parseInt(ids[1], 16);
+
+ View view = root.getRootView();
+ if (view instanceof ViewGroup) {
+ return findView((ViewGroup) view, className, hashCode);
+ }
+
+ return null;
+ }
+
+ private static void invalidate(View root, String parameter) {
+ final View view = findViewByHashCode(root, parameter);
+ if (view != null) {
+ view.postInvalidate();
+ }
+ }
+
+ private static void requestLayout(View root, String parameter) {
+ final View view = findViewByHashCode(root, parameter);
+ if (view != null) {
+ root.post(new Runnable() {
+ public void run() {
+ view.requestLayout();
+ }
+ });
+ }
+ }
+
+ private static void capture(View root, final OutputStream clientStream, String parameter)
+ throws IOException {
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final View captureView = findViewByHashCode(root, parameter);
+
+ if (captureView != null) {
+ final Bitmap[] cache = new Bitmap[1];
+
+ final boolean hasCache = captureView.isDrawingCacheEnabled();
+ final boolean willNotCache = captureView.willNotCacheDrawing();
+
+ if (willNotCache) {
+ captureView.setWillNotCacheDrawing(false);
+ }
+
+ root.post(new Runnable() {
+ public void run() {
+ try {
+ if (!hasCache) {
+ captureView.buildDrawingCache();
+ }
+
+ cache[0] = captureView.getDrawingCache();
+ } finally {
+ latch.countDown();
+ }
+ }
+ });
+
+ try {
+ latch.await(CAPTURE_TIMEOUT, TimeUnit.MILLISECONDS);
+
+ if (cache[0] != null) {
+ BufferedOutputStream out = null;
+ try {
+ out = new BufferedOutputStream(clientStream, 32 * 1024);
+ cache[0].compress(Bitmap.CompressFormat.PNG, 100, out);
+ out.flush();
+ } finally {
+ if (out != null) {
+ out.close();
+ }
+ }
+ }
+ } catch (InterruptedException e) {
+ Log.w("View", "Could not complete the capture of the view " + captureView);
+ } finally {
+ if (willNotCache) {
+ captureView.setWillNotCacheDrawing(true);
+ }
+ if (!hasCache) {
+ captureView.destroyDrawingCache();
+ }
+ }
+ }
+ }
+
+ private static void dump(View root, OutputStream clientStream) throws IOException {
+ BufferedWriter out = null;
+ try {
+ out = new BufferedWriter(new OutputStreamWriter(clientStream), 32 * 1024);
+ View view = root.getRootView();
+ if (view instanceof ViewGroup) {
+ ViewGroup group = (ViewGroup) view;
+ dumpViewHierarchyWithProperties(group, out, 0);
+ }
+ out.write("DONE.");
+ out.newLine();
+ } finally {
+ if (out != null) {
+ out.close();
+ }
+ }
+ }
+
+ private static View findView(ViewGroup group, String className, int hashCode) {
+ if (isRequestedView(group, className, hashCode)) {
+ return group;
+ }
+
+ final int count = group.getChildCount();
+ for (int i = 0; i < count; i++) {
+ final View view = group.getChildAt(i);
+ if (view instanceof ViewGroup) {
+ final View found = findView((ViewGroup) view, className, hashCode);
+ if (found != null) {
+ return found;
+ }
+ } else if (isRequestedView(view, className, hashCode)) {
+ return view;
+ }
+ }
+
+ return null;
+ }
+
+ private static boolean isRequestedView(View view, String className, int hashCode) {
+ return view.getClass().getName().equals(className) && view.hashCode() == hashCode;
+ }
+
+ private static void dumpViewHierarchyWithProperties(ViewGroup group,
+ BufferedWriter out, int level) {
+ if (!dumpViewWithProperties(group, out, level)) {
+ return;
+ }
+
+ final int count = group.getChildCount();
+ for (int i = 0; i < count; i++) {
+ final View view = group.getChildAt(i);
+ if (view instanceof ViewGroup) {
+ dumpViewHierarchyWithProperties((ViewGroup) view, out, level + 1);
+ } else {
+ dumpViewWithProperties(view, out, level + 1);
+ }
+ }
+ }
+
+ private static boolean dumpViewWithProperties(View view, BufferedWriter out, int level) {
+ try {
+ for (int i = 0; i < level; i++) {
+ out.write(' ');
+ }
+ out.write(view.getClass().getName());
+ out.write('@');
+ out.write(Integer.toHexString(view.hashCode()));
+ out.write(' ');
+ dumpViewProperties(view, out);
+ out.newLine();
+ } catch (IOException e) {
+ Log.w("View", "Error while dumping hierarchy tree");
+ return false;
+ }
+ return true;
+ }
+
+ private static Field[] getExportedPropertyFields(Class<?> klass) {
+ if (sFieldsForClasses == null) {
+ sFieldsForClasses = new HashMap<Class<?>, Field[]>();
+ }
+ final HashMap<Class<?>, Field[]> map = sFieldsForClasses;
+
+ Field[] fields = map.get(klass);
+ if (fields != null) {
+ return fields;
+ }
+
+ final ArrayList<Field> foundFields = new ArrayList<Field>();
+ fields = klass.getDeclaredFields();
+
+ int count = fields.length;
+ for (int i = 0; i < count; i++) {
+ final Field field = fields[i];
+ if (field.isAnnotationPresent(ExportedProperty.class)) {
+ field.setAccessible(true);
+ foundFields.add(field);
+ }
+ }
+
+ fields = foundFields.toArray(new Field[foundFields.size()]);
+ map.put(klass, fields);
+
+ return fields;
+ }
+
+ private static Method[] getExportedPropertyMethods(Class<?> klass) {
+ if (sMethodsForClasses == null) {
+ sMethodsForClasses = new HashMap<Class<?>, Method[]>();
+ }
+ final HashMap<Class<?>, Method[]> map = sMethodsForClasses;
+
+ Method[] methods = map.get(klass);
+ if (methods != null) {
+ return methods;
+ }
+
+ final ArrayList<Method> foundMethods = new ArrayList<Method>();
+ methods = klass.getDeclaredMethods();
+
+ int count = methods.length;
+ for (int i = 0; i < count; i++) {
+ final Method method = methods[i];
+ if (method.getParameterTypes().length == 0 &&
+ method.isAnnotationPresent(ExportedProperty.class) &&
+ method.getReturnType() != Void.class) {
+ method.setAccessible(true);
+ foundMethods.add(method);
+ }
+ }
+
+ methods = foundMethods.toArray(new Method[foundMethods.size()]);
+ map.put(klass, methods);
+
+ return methods;
+ }
+
+ private static void dumpViewProperties(Object view, BufferedWriter out) throws IOException {
+ dumpViewProperties(view, out, "");
+ }
+
+ private static void dumpViewProperties(Object view, BufferedWriter out, String prefix)
+ throws IOException {
+ Class<?> klass = view.getClass();
+
+ do {
+ exportFields(view, out, klass, prefix);
+ exportMethods(view, out, klass, prefix);
+ klass = klass.getSuperclass();
+ } while (klass != Object.class);
+ }
+
+ private static void exportMethods(Object view, BufferedWriter out, Class<?> klass,
+ String prefix) throws IOException {
+
+ final Method[] methods = getExportedPropertyMethods(klass);
+
+ int count = methods.length;
+ for (int i = 0; i < count; i++) {
+ final Method method = methods[i];
+ //noinspection EmptyCatchBlock
+ try {
+ // TODO: This should happen on the UI thread
+ Object methodValue = method.invoke(view, (Object[]) null);
+ final Class<?> returnType = method.getReturnType();
+
+ if (returnType == int.class) {
+ ExportedProperty property = method.getAnnotation(ExportedProperty.class);
+ if (property.resolveId() && view instanceof View) {
+ final Resources resources = ((View) view).getContext().getResources();
+ final int id = (Integer) methodValue;
+ if (id >= 0) {
+ methodValue = resources.getResourceTypeName(id) + '/' +
+ resources.getResourceEntryName(id);
+ } else {
+ methodValue = "NO_ID";
+ }
+ } else {
+ final IntToString[] mapping = property.mapping();
+ if (mapping.length > 0) {
+ final int intValue = (Integer) methodValue;
+ boolean mapped = false;
+ int mappingCount = mapping.length;
+ for (int j = 0; j < mappingCount; j++) {
+ final IntToString mapper = mapping[j];
+ if (mapper.from() == intValue) {
+ methodValue = mapper.to();
+ mapped = true;
+ break;
+ }
+ }
+
+ if (!mapped) {
+ methodValue = intValue;
+ }
+ }
+ }
+ } else if (!returnType.isPrimitive()) {
+ ExportedProperty property = method.getAnnotation(ExportedProperty.class);
+ if (property.deepExport()) {
+ dumpViewProperties(methodValue, out, prefix + property.prefix());
+ continue;
+ }
+ }
+
+ out.write(prefix);
+ out.write(method.getName());
+ out.write("()=");
+
+ if (methodValue != null) {
+ final String value = methodValue.toString().replace("\n", "\\n");
+ out.write(String.valueOf(value.length()));
+ out.write(",");
+ out.write(value);
+ } else {
+ out.write("4,null");
+ }
+
+ out.write(' ');
+ } catch (IllegalAccessException e) {
+ } catch (InvocationTargetException e) {
+ }
+ }
+ }
+
+ private static void exportFields(Object view, BufferedWriter out, Class<?> klass, String prefix)
+ throws IOException {
+ final Field[] fields = getExportedPropertyFields(klass);
+
+ int count = fields.length;
+ for (int i = 0; i < count; i++) {
+ final Field field = fields[i];
+
+ //noinspection EmptyCatchBlock
+ try {
+ Object fieldValue = null;
+ final Class<?> type = field.getType();
+
+ if (type == int.class) {
+ ExportedProperty property = field.getAnnotation(ExportedProperty.class);
+ if (property.resolveId() && view instanceof View) {
+ final Resources resources = ((View) view).getContext().getResources();
+ final int id = field.getInt(view);
+ if (id >= 0) {
+ fieldValue = resources.getResourceTypeName(id) + '/' +
+ resources.getResourceEntryName(id);
+ } else {
+ fieldValue = "NO_ID";
+ }
+ } else {
+ final IntToString[] mapping = property.mapping();
+ if (mapping.length > 0) {
+ final int intValue = field.getInt(view);
+ int mappingCount = mapping.length;
+ for (int j = 0; j < mappingCount; j++) {
+ final IntToString mapped = mapping[j];
+ if (mapped.from() == intValue) {
+ fieldValue = mapped.to();
+ break;
+ }
+ }
+
+ if (fieldValue == null) {
+ fieldValue = intValue;
+ }
+ }
+ }
+ } else if (!type.isPrimitive()) {
+ ExportedProperty property = field.getAnnotation(ExportedProperty.class);
+ if (property.deepExport()) {
+ dumpViewProperties(field.get(view), out, prefix + property.prefix());
+ continue;
+ }
+ }
+
+ if (fieldValue == null) {
+ fieldValue = field.get(view);
+ }
+
+ out.write(prefix);
+ out.write(field.getName());
+ out.write('=');
+
+ if (fieldValue != null) {
+ final String value = fieldValue.toString().replace("\n", "\\n");
+ out.write(String.valueOf(value.length()));
+ out.write(",");
+ out.write(value);
+ } else {
+ out.write("4,null");
+ }
+
+ out.write(' ');
+ } catch (IllegalAccessException e) {
+ }
+ }
+ }
+
+ private static void dumpViewHierarchy(ViewGroup group, BufferedWriter out, int level) {
+ if (!dumpView(group, out, level)) {
+ return;
+ }
+
+ final int count = group.getChildCount();
+ for (int i = 0; i < count; i++) {
+ final View view = group.getChildAt(i);
+ if (view instanceof ViewGroup) {
+ dumpViewHierarchy((ViewGroup) view, out, level + 1);
+ } else {
+ dumpView(view, out, level + 1);
+ }
+ }
+ }
+
+ private static boolean dumpView(Object view, BufferedWriter out, int level) {
+ try {
+ for (int i = 0; i < level; i++) {
+ out.write(' ');
+ }
+ out.write(view.getClass().getName());
+ out.write('@');
+ out.write(Integer.toHexString(view.hashCode()));
+ out.newLine();
+ } catch (IOException e) {
+ Log.w("View", "Error while dumping hierarchy tree");
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java
new file mode 100644
index 0000000..9063821
--- /dev/null
+++ b/core/java/android/view/ViewGroup.java
@@ -0,0 +1,3389 @@
+/*
+ * 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.view;
+
+import com.android.internal.R;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.EventLog;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.view.animation.LayoutAnimationController;
+import android.view.animation.Transformation;
+
+import java.util.ArrayList;
+
+/**
+ * <p>
+ * A <code>ViewGroup</code> is a special view that can contain other views
+ * (called children.) The view group is the base class for layouts and views
+ * containers. This class also defines the
+ * {@link android.view.ViewGroup.LayoutParams} class which serves as the base
+ * class for layouts parameters.
+ * </p>
+ *
+ * <p>
+ * Also see {@link LayoutParams} for layout attributes.
+ * </p>
+ */
+public abstract class ViewGroup extends View implements ViewParent, ViewManager {
+ private static final boolean DBG = false;
+
+ /**
+ * Views which have been hidden or removed which need to be animated on
+ * their way out.
+ * This field should be made private, so it is hidden from the SDK.
+ * {@hide}
+ */
+ protected ArrayList<View> mDisappearingChildren;
+
+ /**
+ * Listener used to propagate events indicating when children are added
+ * and/or removed from a view group.
+ * This field should be made private, so it is hidden from the SDK.
+ * {@hide}
+ */
+ protected OnHierarchyChangeListener mOnHierarchyChangeListener;
+
+ // The view contained within this ViewGroup that has or contains focus.
+ private View mFocused;
+
+ // The current transformation to apply on the child being drawn
+ private final Transformation mChildTransformation = new Transformation();
+
+ // Target of Motion events
+ private View mMotionTarget;
+ private final Rect mTempRect = new Rect();
+
+ // Layout animation
+ private LayoutAnimationController mLayoutAnimationController;
+ private Animation.AnimationListener mAnimationListener;
+
+ /**
+ * Internal flags.
+ *
+ * This field should be made private, so it is hidden from the SDK.
+ * {@hide}
+ */
+ protected int mGroupFlags;
+
+ // When set, ViewGroup invalidates only the child's rectangle
+ // Set by default
+ private static final int FLAG_CLIP_CHILDREN = 0x1;
+
+ // When set, ViewGroup excludes the padding area from the invalidate rectangle
+ // Set by default
+ private static final int FLAG_CLIP_TO_PADDING = 0x2;
+
+ // When set, dispatchDraw() will invoke invalidate(); this is set by drawChild() when
+ // a child needs to be invalidated and FLAG_OPTIMIZE_INVALIDATE is set
+ private static final int FLAG_INVALIDATE_REQUIRED = 0x4;
+
+ // When set, dispatchDraw() will run the layout animation and unset the flag
+ private static final int FLAG_RUN_ANIMATION = 0x8;
+
+ // When set, there is either no layout animation on the ViewGroup or the layout
+ // animation is over
+ // Set by default
+ private static final int FLAG_ANIMATION_DONE = 0x10;
+
+ // If set, this ViewGroup has padding; if unset there is no padding and we don't need
+ // to clip it, even if FLAG_CLIP_TO_PADDING is set
+ private static final int FLAG_PADDING_NOT_NULL = 0x20;
+
+ // When set, this ViewGroup caches its children in a Bitmap before starting a layout animation
+ // Set by default
+ private static final int FLAG_ANIMATION_CACHE = 0x40;
+
+ // When set, this ViewGroup converts calls to invalidate(Rect) to invalidate() during a
+ // layout animation; this avoid clobbering the hierarchy
+ // Automatically set when the layout animation starts, depending on the animation's
+ // characteristics
+ private static final int FLAG_OPTIMIZE_INVALIDATE = 0x80;
+
+ // When set, the next call to drawChild() will clear mChildTransformation's matrix
+ private static final int FLAG_CLEAR_TRANSFORMATION = 0x100;
+
+ // When set, this ViewGroup invokes mAnimationListener.onAnimationEnd() and removes
+ // the children's Bitmap caches if necessary
+ // This flag is set when the layout animation is over (after FLAG_ANIMATION_DONE is set)
+ private static final int FLAG_NOTIFY_ANIMATION_LISTENER = 0x200;
+
+ /**
+ * When set, the drawing method will call {@link #getChildDrawingOrder(int, int)}
+ * to get the index of the child to draw for that iteration.
+ */
+ protected static final int FLAG_USE_CHILD_DRAWING_ORDER = 0x400;
+
+ /**
+ * When set, this ViewGroup supports static transformations on children; this causes
+ * {@link #getChildStaticTransformation(View, android.view.animation.Transformation)} to be
+ * invoked when a child is drawn.
+ *
+ * Any subclass overriding
+ * {@link #getChildStaticTransformation(View, android.view.animation.Transformation)} should
+ * set this flags in {@link #mGroupFlags}.
+ *
+ * This flag needs to be removed until we can add a setter for it. People
+ * can't be directly stuffing values in to mGroupFlags!!!
+ *
+ * {@hide}
+ */
+ protected static final int FLAG_SUPPORT_STATIC_TRANSFORMATIONS = 0x800;
+
+ // When the previous drawChild() invocation used an alpha value that was lower than
+ // 1.0 and set it in mCachePaint
+ private static final int FLAG_ALPHA_LOWER_THAN_ONE = 0x1000;
+
+ /**
+ * When set, this ViewGroup's drawable states also include those
+ * of its children.
+ */
+ private static final int FLAG_ADD_STATES_FROM_CHILDREN = 0x2000;
+
+ /**
+ * When set, this ViewGroup tries to always draw its children using their drawing cache.
+ */
+ private static final int FLAG_ALWAYS_DRAWN_WITH_CACHE = 0x4000;
+
+ /**
+ * When set, and if FLAG_ALWAYS_DRAWN_WITH_CACHE is not set, this ViewGroup will try to
+ * draw its children with their drawing cache.
+ */
+ private static final int FLAG_CHILDREN_DRAWN_WITH_CACHE = 0x8000;
+
+ /**
+ * When set, this group will go through its list of children to notify them of
+ * any drawable state change.
+ */
+ private static final int FLAG_NOTIFY_CHILDREN_ON_DRAWABLE_STATE_CHANGE = 0x10000;
+
+ private static final int FLAG_MASK_FOCUSABILITY = 0x60000;
+
+ /**
+ * This view will get focus before any of its descendants.
+ */
+ public static final int FOCUS_BEFORE_DESCENDANTS = 0x20000;
+
+ /**
+ * This view will get focus only if none of its descendants want it.
+ */
+ public static final int FOCUS_AFTER_DESCENDANTS = 0x40000;
+
+ /**
+ * This view will block any of its descendants from getting focus, even
+ * if they are focusable.
+ */
+ public static final int FOCUS_BLOCK_DESCENDANTS = 0x60000;
+
+ /**
+ * Used to map between enum in attrubutes and flag values.
+ */
+ private static final int[] DESCENDANT_FOCUSABILITY_FLAGS =
+ {FOCUS_BEFORE_DESCENDANTS, FOCUS_AFTER_DESCENDANTS,
+ FOCUS_BLOCK_DESCENDANTS};
+
+ /**
+ * When set, this ViewGroup should not intercept touch events.
+ */
+ private static final int FLAG_DISALLOW_INTERCEPT = 0x80000;
+
+ /**
+ * Indicates which types of drawing caches are to be kept in memory.
+ * This field should be made private, so it is hidden from the SDK.
+ * {@hide}
+ */
+ protected int mPersistentDrawingCache;
+
+ /**
+ * Used to indicate that no drawing cache should be kept in memory.
+ */
+ public static final int PERSISTENT_NO_CACHE = 0x0;
+
+ /**
+ * Used to indicate that the animation drawing cache should be kept in memory.
+ */
+ public static final int PERSISTENT_ANIMATION_CACHE = 0x1;
+
+ /**
+ * Used to indicate that the scrolling drawing cache should be kept in memory.
+ */
+ public static final int PERSISTENT_SCROLLING_CACHE = 0x2;
+
+ /**
+ * Used to indicate that all drawing caches should be kept in memory.
+ */
+ public static final int PERSISTENT_ALL_CACHES = 0x3;
+
+ /**
+ * We clip to padding when FLAG_CLIP_TO_PADDING and FLAG_PADDING_NOT_NULL
+ * are set at the same time.
+ */
+ protected static final int CLIP_TO_PADDING_MASK = FLAG_CLIP_TO_PADDING | FLAG_PADDING_NOT_NULL;
+
+ // Index of the child's left position in the mLocation array
+ private static final int CHILD_LEFT_INDEX = 0;
+ // Index of the child's top position in the mLocation array
+ private static final int CHILD_TOP_INDEX = 1;
+
+ // Child views of this ViewGroup
+ private View[] mChildren;
+ // Number of valid children in the mChildren array, the rest should be null or not
+ // considered as children
+ private int mChildrenCount;
+
+ private static final int ARRAY_INITIAL_CAPACITY = 12;
+ private static final int ARRAY_CAPACITY_INCREMENT = 12;
+
+ // Used to draw cached views
+ private final Paint mCachePaint = new Paint();
+
+ public ViewGroup(Context context) {
+ super(context);
+ initViewGroup();
+ }
+
+ public ViewGroup(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initViewGroup();
+ initFromAttributes(context, attrs);
+ }
+
+ public ViewGroup(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ initViewGroup();
+ initFromAttributes(context, attrs);
+ }
+
+ private void initViewGroup() {
+ // ViewGroup doesn't draw by default
+ setFlags(WILL_NOT_DRAW, DRAW_MASK);
+ mGroupFlags |= FLAG_CLIP_CHILDREN;
+ mGroupFlags |= FLAG_CLIP_TO_PADDING;
+ mGroupFlags |= FLAG_ANIMATION_DONE;
+ mGroupFlags |= FLAG_ANIMATION_CACHE;
+ mGroupFlags |= FLAG_ALWAYS_DRAWN_WITH_CACHE;
+
+ setDescendantFocusability(FOCUS_BEFORE_DESCENDANTS);
+
+ mChildren = new View[ARRAY_INITIAL_CAPACITY];
+ mChildrenCount = 0;
+
+ mCachePaint.setDither(false);
+
+ mPersistentDrawingCache = PERSISTENT_SCROLLING_CACHE;
+ }
+
+ private void initFromAttributes(Context context, AttributeSet attrs) {
+ TypedArray a = context.obtainStyledAttributes(attrs,
+ R.styleable.ViewGroup);
+
+ final int N = a.getIndexCount();
+ for (int i = 0; i < N; i++) {
+ int attr = a.getIndex(i);
+ switch (attr) {
+ case R.styleable.ViewGroup_clipChildren:
+ setClipChildren(a.getBoolean(attr, true));
+ break;
+ case R.styleable.ViewGroup_clipToPadding:
+ setClipToPadding(a.getBoolean(attr, true));
+ break;
+ case R.styleable.ViewGroup_animationCache:
+ setAnimationCacheEnabled(a.getBoolean(attr, true));
+ break;
+ case R.styleable.ViewGroup_persistentDrawingCache:
+ setPersistentDrawingCache(a.getInt(attr, PERSISTENT_SCROLLING_CACHE));
+ break;
+ case R.styleable.ViewGroup_addStatesFromChildren:
+ setAddStatesFromChildren(a.getBoolean(attr, false));
+ break;
+ case R.styleable.ViewGroup_alwaysDrawnWithCache:
+ setAlwaysDrawnWithCacheEnabled(a.getBoolean(attr, true));
+ break;
+ case R.styleable.ViewGroup_layoutAnimation:
+ int id = a.getResourceId(attr, -1);
+ if (id > 0) {
+ setLayoutAnimation(AnimationUtils.loadLayoutAnimation(mContext, id));
+ }
+ break;
+ case R.styleable.ViewGroup_descendantFocusability:
+ setDescendantFocusability(DESCENDANT_FOCUSABILITY_FLAGS[a.getInt(attr, 0)]);
+ break;
+ }
+ }
+
+ a.recycle();
+ }
+
+ /**
+ * Gets the descendant focusability of this view group. The descendant
+ * focusability defines the relationship between this view group and its
+ * descendants when looking for a view to take focus in
+ * {@link #requestFocus(int, android.graphics.Rect)}.
+ *
+ * @return one of {@link #FOCUS_BEFORE_DESCENDANTS}, {@link #FOCUS_AFTER_DESCENDANTS},
+ * {@link #FOCUS_BLOCK_DESCENDANTS}.
+ */
+ @ViewDebug.ExportedProperty(mapping = {
+ @ViewDebug.IntToString(from = FOCUS_BEFORE_DESCENDANTS, to = "FOCUS_BEFORE_DESCENDANTS"),
+ @ViewDebug.IntToString(from = FOCUS_AFTER_DESCENDANTS, to = "FOCUS_AFTER_DESCENDANTS"),
+ @ViewDebug.IntToString(from = FOCUS_BLOCK_DESCENDANTS, to = "FOCUS_BLOCK_DESCENDANTS")
+ })
+ public int getDescendantFocusability() {
+ return mGroupFlags & FLAG_MASK_FOCUSABILITY;
+ }
+
+ /**
+ * Set the descendant focusability of this view group. This defines the relationship
+ * between this view group and its descendants when looking for a view to
+ * take focus in {@link #requestFocus(int, android.graphics.Rect)}.
+ *
+ * @param focusability one of {@link #FOCUS_BEFORE_DESCENDANTS}, {@link #FOCUS_AFTER_DESCENDANTS},
+ * {@link #FOCUS_BLOCK_DESCENDANTS}.
+ */
+ public void setDescendantFocusability(int focusability) {
+ switch (focusability) {
+ case FOCUS_BEFORE_DESCENDANTS:
+ case FOCUS_AFTER_DESCENDANTS:
+ case FOCUS_BLOCK_DESCENDANTS:
+ break;
+ default:
+ throw new IllegalArgumentException("must be one of FOCUS_BEFORE_DESCENDANTS, "
+ + "FOCUS_AFTER_DESCENDANTS, FOCUS_BLOCK_DESCENDANTS");
+ }
+ mGroupFlags &= ~FLAG_MASK_FOCUSABILITY;
+ mGroupFlags |= (focusability & FLAG_MASK_FOCUSABILITY);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ void handleFocusGainInternal(int direction, Rect previouslyFocusedRect) {
+ if (mFocused != null) {
+ mFocused.unFocus();
+ mFocused = null;
+ }
+ super.handleFocusGainInternal(direction, previouslyFocusedRect);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void requestChildFocus(View child, View focused) {
+ if (DBG) {
+ System.out.println(this + " requestChildFocus()");
+ }
+ if (getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS) {
+ return;
+ }
+
+ // Unfocus us, if necessary
+ super.unFocus();
+
+ // We had a previous notion of who had focus. Clear it.
+ if (mFocused != child) {
+ if (mFocused != null) {
+ mFocused.unFocus();
+ }
+
+ mFocused = child;
+ }
+ if (mParent != null) {
+ mParent.requestChildFocus(this, focused);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void focusableViewAvailable(View v) {
+ if (mParent != null
+ // shortcut: don't report a new focusable view if we block our descendants from
+ // getting focus
+ && (getDescendantFocusability() != FOCUS_BLOCK_DESCENDANTS)
+ // shortcut: don't report a new focusable view if we already are focused
+ // (and we don't prefer our descendants)
+ //
+ // note: knowing that mFocused is non-null is not a good enough reason
+ // to break the traversal since in that case we'd actually have to find
+ // the focused view and make sure it wasn't FOCUS_AFTER_DESCENDANTS and
+ // an ancestor of v; this will get checked for at ViewRoot
+ && !(isFocused() && getDescendantFocusability() != FOCUS_AFTER_DESCENDANTS)) {
+ mParent.focusableViewAvailable(v);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean showContextMenuForChild(View originalView) {
+ return mParent != null && mParent.showContextMenuForChild(originalView);
+ }
+
+ /**
+ * Find the nearest view in the specified direction that wants to take
+ * focus.
+ *
+ * @param focused The view that currently has focus
+ * @param direction One of FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, and
+ * FOCUS_RIGHT, or 0 for not applicable.
+ */
+ public View focusSearch(View focused, int direction) {
+ if (isRootNamespace()) {
+ // root namespace means we should consider ourselves the top of the
+ // tree for focus searching; otherwise we could be focus searching
+ // into other tabs. see LocalActivityManager and TabHost for more info
+ return FocusFinder.getInstance().findNextFocus(this, focused, direction);
+ } else if (mParent != null) {
+ return mParent.focusSearch(focused, direction);
+ }
+ return null;
+ }
+
+ /**
+ * Called when a child of this group wants a particular rectangle to be
+ * positioned onto the screen. {@link ViewGroup}s overriding this can trust
+ * that:
+ * <ul>
+ * <li>child will be a direct child of this group</li>
+ * <li>rectangle will be in the child's coordinates</li>
+ * </ul>
+ *
+ * <p>{@link ViewGroup}s overriding this should uphold the contract:</p>
+ * <ul>
+ * <li>nothing will change if the rectangle is already visible</li>
+ * <li>the view port will be scrolled only just enough to make the
+ * rectangle visible</li>
+ * <ul>
+ *
+ * @param child The direct child making the request.
+ * @param rectangle The rectangle in the child's coordinates the child
+ * wishes to be on the screen.
+ * @param immediate True to forbid animated or delayed scrolling,
+ * false otherwise
+ * @return Whether the group scrolled to handle the operation
+ */
+ public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) {
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean dispatchUnhandledMove(View focused, int direction) {
+ return mFocused != null &&
+ mFocused.dispatchUnhandledMove(focused, direction);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void clearChildFocus(View child) {
+ if (DBG) {
+ System.out.println(this + " clearChildFocus()");
+ }
+
+ mFocused = null;
+ if (mParent != null) {
+ mParent.clearChildFocus(this);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void clearFocus() {
+ super.clearFocus();
+
+ // clear any child focus if it exists
+ if (mFocused != null) {
+ mFocused.clearFocus();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ void unFocus() {
+ if (DBG) {
+ System.out.println(this + " unFocus()");
+ }
+
+ super.unFocus();
+ if (mFocused != null) {
+ mFocused.unFocus();
+ }
+ mFocused = null;
+ }
+
+ /**
+ * Returns the focused child of this view, if any. The child may have focus
+ * or contain focus.
+ *
+ * @return the focused child or null.
+ */
+ public View getFocusedChild() {
+ return mFocused;
+ }
+
+ /**
+ * Returns true if this view has or contains focus
+ *
+ * @return true if this view has or contains focus
+ */
+ @Override
+ public boolean hasFocus() {
+ return (mPrivateFlags & FOCUSED) != 0 || mFocused != null;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see android.view.View#findFocus()
+ */
+ @Override
+ public View findFocus() {
+ if (DBG) {
+ System.out.println("Find focus in " + this + ": flags="
+ + isFocused() + ", child=" + mFocused);
+ }
+
+ if (isFocused()) {
+ return this;
+ }
+
+ if (mFocused != null) {
+ return mFocused.findFocus();
+ }
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean hasFocusable() {
+ if ((mViewFlags & VISIBILITY_MASK) != VISIBLE) {
+ return false;
+ }
+
+ if (isFocusable()) {
+ return true;
+ }
+
+ final int descendantFocusability = getDescendantFocusability();
+ if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) {
+ final int count = mChildrenCount;
+ final View[] children = mChildren;
+
+ for (int i = 0; i < count; i++) {
+ final View child = children[i];
+ if (child.hasFocusable()) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void addFocusables(ArrayList<View> views, int direction) {
+ final int focusableCount = views.size();
+
+ final int descendantFocusability = getDescendantFocusability();
+
+ if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) {
+ final int count = mChildrenCount;
+ final View[] children = mChildren;
+
+ for (int i = 0; i < count; i++) {
+ final View child = children[i];
+ if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
+ child.addFocusables(views, direction);
+ }
+ }
+ }
+
+ // we add ourselves (if focusable) in all cases except for when we are
+ // FOCUS_AFTER_DESCENDANTS and there are some descendants focusable. this is
+ // to avoid the focus search finding layouts when a more precise search
+ // among the focusable children would be more interesting.
+ if (
+ descendantFocusability != FOCUS_AFTER_DESCENDANTS ||
+ // No focusable descendants
+ (focusableCount == views.size())) {
+ super.addFocusables(views, direction);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void dispatchWindowFocusChanged(boolean hasFocus) {
+ super.dispatchWindowFocusChanged(hasFocus);
+ final int count = mChildrenCount;
+ final View[] children = mChildren;
+ for (int i = 0; i < count; i++) {
+ children[i].dispatchWindowFocusChanged(hasFocus);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void addTouchables(ArrayList<View> views) {
+ super.addTouchables(views);
+
+ final int count = mChildrenCount;
+ final View[] children = mChildren;
+
+ for (int i = 0; i < count; i++) {
+ final View child = children[i];
+ if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
+ child.addTouchables(views);
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void dispatchWindowVisibilityChanged(int visibility) {
+ super.dispatchWindowVisibilityChanged(visibility);
+ final int count = mChildrenCount;
+ final View[] children = mChildren;
+ for (int i = 0; i < count; i++) {
+ children[i].dispatchWindowVisibilityChanged(visibility);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void recomputeViewAttributes(View child) {
+ ViewParent parent = mParent;
+ if (parent != null) parent.recomputeViewAttributes(this);
+ }
+
+ @Override
+ void dispatchCollectViewAttributes(int visibility) {
+ visibility |= mViewFlags&VISIBILITY_MASK;
+ super.dispatchCollectViewAttributes(visibility);
+ final int count = mChildrenCount;
+ final View[] children = mChildren;
+ for (int i = 0; i < count; i++) {
+ children[i].dispatchCollectViewAttributes(visibility);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void bringChildToFront(View child) {
+ int index = indexOfChild(child);
+ if (index >= 0) {
+ removeFromArray(index);
+ addInArray(child, mChildrenCount);
+ child.mParent = this;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ if ((mPrivateFlags & (FOCUSED | HAS_BOUNDS)) == (FOCUSED | HAS_BOUNDS)) {
+ return super.dispatchKeyEvent(event);
+ } else if (mFocused != null && (mFocused.mPrivateFlags & HAS_BOUNDS) == HAS_BOUNDS) {
+ return mFocused.dispatchKeyEvent(event);
+ }
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean dispatchTrackballEvent(MotionEvent event) {
+ if ((mPrivateFlags & (FOCUSED | HAS_BOUNDS)) == (FOCUSED | HAS_BOUNDS)) {
+ return super.dispatchTrackballEvent(event);
+ } else if (mFocused != null && (mFocused.mPrivateFlags & HAS_BOUNDS) == HAS_BOUNDS) {
+ return mFocused.dispatchTrackballEvent(event);
+ }
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent ev) {
+ final int action = ev.getAction();
+ final float xf = ev.getX();
+ final float yf = ev.getY();
+ final float scrolledXFloat = xf + mScrollX;
+ final float scrolledYFloat = yf + mScrollY;
+ final Rect frame = mTempRect;
+
+ boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
+
+ if (action == MotionEvent.ACTION_DOWN) {
+ if (mMotionTarget != null) {
+ // this is weird, we got a pen down, but we thought it was
+ // already down!
+ // XXX: We should probably send an ACTION_UP to the current
+ // target.
+ mMotionTarget = null;
+ }
+ // If we're disallowing intercept or if we're allowing and we didn't
+ // intercept
+ if (disallowIntercept || !onInterceptTouchEvent(ev)) {
+ // reset this event's action (just to protect ourselves)
+ ev.setAction(MotionEvent.ACTION_DOWN);
+ // We know we want to dispatch the event down, find a child
+ // who can handle it, start with the front-most child.
+ final int scrolledXInt = (int) scrolledXFloat;
+ final int scrolledYInt = (int) scrolledYFloat;
+ final View[] children = mChildren;
+ final int count = mChildrenCount;
+ for (int i = count - 1; i >= 0; i--) {
+ final View child = children[i];
+ if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
+ || child.getAnimation() != null) {
+ child.getHitRect(frame);
+ if (frame.contains(scrolledXInt, scrolledYInt)) {
+ // offset the event to the view's coordinate system
+ final float xc = scrolledXFloat - child.mLeft;
+ final float yc = scrolledYFloat - child.mTop;
+ ev.setLocation(xc, yc);
+ if (child.dispatchTouchEvent(ev)) {
+ // Event handled, we have a target now.
+ mMotionTarget = child;
+ return true;
+ }
+ // The event didn't get handled, try the next view.
+ // Don't reset the event's location, it's not
+ // necessary here.
+ }
+ }
+ }
+ }
+ }
+
+ boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
+ (action == MotionEvent.ACTION_CANCEL);
+
+ if (isUpOrCancel) {
+ // Note, we've already copied the previous state to our local
+ // variable, so this takes effect on the next event
+ mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
+ }
+
+ // The event wasn't an ACTION_DOWN, dispatch it to our target if
+ // we have one.
+ final View target = mMotionTarget;
+ if (target == null) {
+ // We don't have a target, this means we're handling the
+ // event as a regular view.
+ ev.setLocation(xf, yf);
+ return super.dispatchTouchEvent(ev);
+ }
+
+ // if have a target, see if we're allowed to and want to intercept its
+ // events
+ if (!disallowIntercept && onInterceptTouchEvent(ev)) {
+ final float xc = scrolledXFloat - (float) target.mLeft;
+ final float yc = scrolledYFloat - (float) target.mTop;
+ ev.setAction(MotionEvent.ACTION_CANCEL);
+ ev.setLocation(xc, yc);
+ if (!target.dispatchTouchEvent(ev)) {
+ // target didn't handle ACTION_CANCEL. not much we can do
+ // but they should have.
+ }
+ // clear the target
+ mMotionTarget = null;
+ // Don't dispatch this event to our own view, because we already
+ // saw it when intercepting; we just want to give the following
+ // event to the normal onTouchEvent().
+ return true;
+ }
+
+ if (isUpOrCancel) {
+ mMotionTarget = null;
+ }
+
+ // finally offset the event to the target's coordinate system and
+ // dispatch the event.
+ final float xc = scrolledXFloat - (float) target.mLeft;
+ final float yc = scrolledYFloat - (float) target.mTop;
+ ev.setLocation(xc, yc);
+
+ return target.dispatchTouchEvent(ev);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
+
+ if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
+ // We're already in this state, assume our ancestors are too
+ return;
+ }
+
+ if (disallowIntercept) {
+ mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
+ } else {
+ mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
+ }
+
+ // Pass it up to our parent
+ if (mParent != null) {
+ mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
+ }
+ }
+
+ /**
+ * Implement this method to intercept all touch screen motion events. This
+ * allows you to watch events as they are dispatched to your children, and
+ * take ownership of the current gesture at any point.
+ *
+ * <p>Using this function takes some care, as it has a fairly complicated
+ * interaction with {@link View#onTouchEvent(MotionEvent)
+ * View.onTouchEvent(MotionEvent)}, and using it requires implementing
+ * that method as well as this one in the correct way. Events will be
+ * received in the following order:
+ *
+ * <ol>
+ * <li> You will receive the down event here.
+ * <li> The down event will be handled either by a child of this view
+ * group, or given to your own onTouchEvent() method to handle; this means
+ * you should implement onTouchEvent() to return true, so you will
+ * continue to see the rest of the gesture (instead of looking for
+ * a parent view to handle it). Also, by returning true from
+ * onTouchEvent(), you will not receive any following
+ * events in onInterceptTouchEvent() and all touch processing must
+ * happen in onTouchEvent() like normal.
+ * <li> For as long as you return false from this function, each following
+ * event (up to and including the final up) will be delivered first here
+ * and then to the target's onTouchEvent().
+ * <li> If you return true from here, you will not receive any
+ * following events: the target view will receive the same event but
+ * with the action {@link MotionEvent#ACTION_CANCEL}, and all further
+ * events will be delivered to your onTouchEvent() method and no longer
+ * appear here.
+ * </ol>
+ *
+ * @param ev The motion event being dispatched down the hierarchy.
+ * @return Return true to steal motion events from the children and have
+ * them dispatched to this ViewGroup through onTouchEvent().
+ * The current target will receive an ACTION_CANCEL event, and no further
+ * messages will be delivered here.
+ */
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Looks for a view to give focus to respecting the setting specified by
+ * {@link #getDescendantFocusability()}.
+ *
+ * Uses {@link #onRequestFocusInDescendants(int, android.graphics.Rect)} to
+ * find focus within the children of this group when appropriate.
+ *
+ * @see #FOCUS_BEFORE_DESCENDANTS
+ * @see #FOCUS_AFTER_DESCENDANTS
+ * @see #FOCUS_BLOCK_DESCENDANTS
+ * @see #onRequestFocusInDescendants
+ */
+ @Override
+ public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
+ if (DBG) {
+ System.out.println(this + " ViewGroup.requestFocus direction="
+ + direction);
+ }
+ int descendantFocusability = getDescendantFocusability();
+
+ switch (descendantFocusability) {
+ case FOCUS_BLOCK_DESCENDANTS:
+ return super.requestFocus(direction, previouslyFocusedRect);
+ case FOCUS_BEFORE_DESCENDANTS: {
+ final boolean took = super.requestFocus(direction, previouslyFocusedRect);
+ return took ? took : onRequestFocusInDescendants(direction, previouslyFocusedRect);
+ }
+ case FOCUS_AFTER_DESCENDANTS: {
+ final boolean took = onRequestFocusInDescendants(direction, previouslyFocusedRect);
+ return took ? took : super.requestFocus(direction, previouslyFocusedRect);
+ }
+ default:
+ throw new IllegalStateException("descendant focusability must be "
+ + "one of FOCUS_BEFORE_DESCENDANTS, FOCUS_AFTER_DESCENDANTS, FOCUS_BLOCK_DESCENDANTS "
+ + "but is " + descendantFocusability);
+ }
+ }
+
+ /**
+ * Look for a descendant to call {@link View#requestFocus} on.
+ * Called by {@link ViewGroup#requestFocus(int, android.graphics.Rect)}
+ * when it wants to request focus within its children. Override this to
+ * customize how your {@link ViewGroup} requests focus within its children.
+ * @param direction One of FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, and FOCUS_RIGHT
+ * @param previouslyFocusedRect The rectangle (in this View's coordinate system)
+ * to give a finer grained hint about where focus is coming from. May be null
+ * if there is no hint.
+ * @return Whether focus was taken.
+ */
+ @SuppressWarnings({"ConstantConditions"})
+ protected boolean onRequestFocusInDescendants(int direction,
+ Rect previouslyFocusedRect) {
+ int index;
+ int increment;
+ int end;
+ int count = mChildrenCount;
+ if ((direction & FOCUS_FORWARD) != 0) {
+ index = 0;
+ increment = 1;
+ end = count;
+ } else {
+ index = count - 1;
+ increment = -1;
+ end = -1;
+ }
+ final View[] children = mChildren;
+ for (int i = index; i != end; i += increment) {
+ View child = children[i];
+ if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
+ if (child.requestFocus(direction, previouslyFocusedRect)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ void dispatchAttachedToWindow(AttachInfo info, int visibility) {
+ super.dispatchAttachedToWindow(info, visibility);
+ visibility |= mViewFlags & VISIBILITY_MASK;
+ final int count = mChildrenCount;
+ final View[] children = mChildren;
+ for (int i = 0; i < count; i++) {
+ children[i].dispatchAttachedToWindow(info, visibility);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ void dispatchDetachedFromWindow() {
+ final int count = mChildrenCount;
+ final View[] children = mChildren;
+ for (int i = 0; i < count; i++) {
+ children[i].dispatchDetachedFromWindow();
+ }
+ super.dispatchDetachedFromWindow();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setPadding(int left, int top, int right, int bottom) {
+ super.setPadding(left, top, right, bottom);
+
+ if ((mPaddingLeft | mPaddingTop | mPaddingRight | mPaddingRight) != 0) {
+ mGroupFlags |= FLAG_PADDING_NOT_NULL;
+ } else {
+ mGroupFlags &= ~FLAG_PADDING_NOT_NULL;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
+ super.dispatchSaveInstanceState(container);
+ final int count = mChildrenCount;
+ final View[] children = mChildren;
+ for (int i = 0; i < count; i++) {
+ children[i].dispatchSaveInstanceState(container);
+ }
+ }
+
+ /**
+ * Perform dispatching of a {@link #saveHierarchyState freeze()} to only this view,
+ * not to its children. For use when overriding
+ * {@link #dispatchSaveInstanceState dispatchFreeze()} to allow subclasses to freeze
+ * their own state but not the state of their children.
+ *
+ * @param container the container
+ */
+ protected void dispatchFreezeSelfOnly(SparseArray<Parcelable> container) {
+ super.dispatchSaveInstanceState(container);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
+ super.dispatchRestoreInstanceState(container);
+ final int count = mChildrenCount;
+ final View[] children = mChildren;
+ for (int i = 0; i < count; i++) {
+ children[i].dispatchRestoreInstanceState(container);
+ }
+ }
+
+ /**
+ * Perform dispatching of a {@link #restoreHierarchyState thaw()} to only this view,
+ * not to its children. For use when overriding
+ * {@link #dispatchRestoreInstanceState dispatchThaw()} to allow subclasses to thaw
+ * their own state but not the state of their children.
+ *
+ * @param container the container
+ */
+ protected void dispatchThawSelfOnly(SparseArray<Parcelable> container) {
+ super.dispatchRestoreInstanceState(container);
+ }
+
+ /**
+ * Enables or disables the drawing cache for each child of this view group.
+ *
+ * @param enabled true to enable the cache, false to dispose of it
+ */
+ protected void setChildrenDrawingCacheEnabled(boolean enabled) {
+ if (enabled || (mPersistentDrawingCache & PERSISTENT_ALL_CACHES) != PERSISTENT_ALL_CACHES) {
+ final View[] children = mChildren;
+ final int count = mChildrenCount;
+ for (int i = 0; i < count; i++) {
+ children[i].setDrawingCacheEnabled(enabled);
+ }
+ }
+ }
+
+ @Override
+ protected void onAnimationStart() {
+ super.onAnimationStart();
+
+ // When this ViewGroup's animation starts, build the cache for the children
+ if ((mGroupFlags & FLAG_ANIMATION_CACHE) == FLAG_ANIMATION_CACHE) {
+ final int count = mChildrenCount;
+ final View[] children = mChildren;
+
+ for (int i = 0; i < count; i++) {
+ final View child = children[i];
+ if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
+ child.setDrawingCacheEnabled(true);
+ child.buildDrawingCache();
+ }
+ }
+
+ mGroupFlags |= FLAG_CHILDREN_DRAWN_WITH_CACHE;
+ }
+ }
+
+ @Override
+ protected void onAnimationEnd() {
+ super.onAnimationEnd();
+
+ // When this ViewGroup's animation ends, destroy the cache of the children
+ if ((mGroupFlags & FLAG_ANIMATION_CACHE) == FLAG_ANIMATION_CACHE) {
+ mGroupFlags &= ~FLAG_CHILDREN_DRAWN_WITH_CACHE;
+
+ if ((mPersistentDrawingCache & PERSISTENT_ANIMATION_CACHE) == 0) {
+ setChildrenDrawingCacheEnabled(false);
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ final int count = mChildrenCount;
+ final View[] children = mChildren;
+ int flags = mGroupFlags;
+
+ if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {
+ final boolean cache = (mGroupFlags & FLAG_ANIMATION_CACHE) == FLAG_ANIMATION_CACHE;
+
+ for (int i = 0; i < count; i++) {
+ final View child = children[i];
+ if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
+ final LayoutParams params = child.getLayoutParams();
+ attachLayoutAnimationParameters(child, params, i, count);
+ bindLayoutAnimation(child);
+ if (cache) {
+ child.setDrawingCacheEnabled(true);
+ child.buildDrawingCache();
+ }
+ }
+ }
+
+ final LayoutAnimationController controller = mLayoutAnimationController;
+ if (controller.willOverlap()) {
+ mGroupFlags |= FLAG_OPTIMIZE_INVALIDATE;
+ }
+
+ controller.start();
+
+ mGroupFlags &= ~FLAG_RUN_ANIMATION;
+ mGroupFlags &= ~FLAG_ANIMATION_DONE;
+
+ if (cache) {
+ mGroupFlags |= FLAG_CHILDREN_DRAWN_WITH_CACHE;
+ }
+
+ if (mAnimationListener != null) {
+ mAnimationListener.onAnimationStart(controller.getAnimation());
+ }
+ }
+
+ int saveCount = 0;
+ final boolean clipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK;
+ if (clipToPadding) {
+ saveCount = canvas.save();
+ final int scrollX = mScrollX;
+ final int scrollY = mScrollY;
+ canvas.clipRect(scrollX + mPaddingLeft, scrollY + mPaddingTop,
+ scrollX + mRight - mLeft - mPaddingRight,
+ scrollY + mBottom - mTop - mPaddingBottom);
+
+ }
+
+ mGroupFlags &= ~FLAG_INVALIDATE_REQUIRED;
+
+ boolean more = false;
+ final long drawingTime = getDrawingTime();
+
+ if ((flags & FLAG_USE_CHILD_DRAWING_ORDER) == 0) {
+ for (int i = 0; i < count; i++) {
+ final View child = children[i];
+ if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
+ more |= drawChild(canvas, child, drawingTime);
+ }
+ }
+ } else {
+ for (int i = 0; i < count; i++) {
+ final View child = children[getChildDrawingOrder(count, i)];
+ if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
+ more |= drawChild(canvas, child, drawingTime);
+ }
+ }
+ }
+
+ // Draw any disappearing views that have animations
+ if (mDisappearingChildren != null) {
+ final ArrayList<View> disappearingChildren = mDisappearingChildren;
+ final int disappearingCount = disappearingChildren.size() - 1;
+ // Go backwards -- we may delete as animations finish
+ for (int i = disappearingCount; i >= 0; i--) {
+ final View child = disappearingChildren.get(i);
+ more |= drawChild(canvas, child, drawingTime);
+ }
+ }
+
+ if (clipToPadding) {
+ canvas.restoreToCount(saveCount);
+ }
+
+ // mGroupFlags might have been updated by drawChild()
+ flags = mGroupFlags;
+
+ if ((flags & FLAG_INVALIDATE_REQUIRED) == FLAG_INVALIDATE_REQUIRED) {
+ invalidate();
+ }
+
+ if ((flags & FLAG_ANIMATION_DONE) == 0 && (flags & FLAG_NOTIFY_ANIMATION_LISTENER) == 0 &&
+ mLayoutAnimationController.isDone() && !more) {
+ // We want to erase the drawing cache and notify the listener after the
+ // next frame is drawn because one extra invalidate() is caused by
+ // drawChild() after the animation is over
+ mGroupFlags |= FLAG_NOTIFY_ANIMATION_LISTENER;
+ final Runnable end = new Runnable() {
+ public void run() {
+ notifyAnimationListener();
+ }
+ };
+ post(end);
+ }
+ }
+
+ /**
+ * Returns the index of the child to draw for this iteration. Override this
+ * if you want to change the drawing order of children. By default, it
+ * returns i.
+ * <p>
+ * NOTE: In order for this method to be called, the
+ * {@link #FLAG_USE_CHILD_DRAWING_ORDER} must be set.
+ *
+ * @param i The current iteration.
+ * @return The index of the child to draw this iteration.
+ */
+ protected int getChildDrawingOrder(int childCount, int i) {
+ return i;
+ }
+
+ private void notifyAnimationListener() {
+ mGroupFlags &= ~FLAG_NOTIFY_ANIMATION_LISTENER;
+ mGroupFlags |= FLAG_ANIMATION_DONE;
+
+ if (mAnimationListener != null) {
+ final Runnable end = new Runnable() {
+ public void run() {
+ mAnimationListener.onAnimationEnd(mLayoutAnimationController.getAnimation());
+ }
+ };
+ post(end);
+ }
+
+ if ((mGroupFlags & FLAG_ANIMATION_CACHE) == FLAG_ANIMATION_CACHE) {
+ mGroupFlags &= ~FLAG_CHILDREN_DRAWN_WITH_CACHE;
+ if ((mPersistentDrawingCache & PERSISTENT_ANIMATION_CACHE) == 0) {
+ setChildrenDrawingCacheEnabled(false);
+ }
+ }
+
+ invalidate();
+ }
+
+ /**
+ * Draw one child of this View Group. This method is responsible for getting
+ * the canvas in the right state. This includes clipping, translating so
+ * that the child's scrolled origin is at 0, 0, and applying any animation
+ * transformations.
+ *
+ * @param canvas The canvas on which to draw the child
+ * @param child Who to draw
+ * @param drawingTime The time at which draw is occuring
+ * @return True if an invalidate() was issued
+ */
+ protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
+ boolean more = false;
+
+ final int cl = child.mLeft;
+ final int ct = child.mTop;
+ final int cr = child.mRight;
+ final int cb = child.mBottom;
+
+ final int flags = mGroupFlags;
+
+ if ((flags & FLAG_CLEAR_TRANSFORMATION) == FLAG_CLEAR_TRANSFORMATION) {
+ mChildTransformation.clear();
+ mGroupFlags &= ~FLAG_CLEAR_TRANSFORMATION;
+ }
+
+ Transformation transformToApply = null;
+ final Animation a = child.getAnimation();
+ boolean concatMatrix = false;
+
+ if (a != null) {
+ if (!a.isInitialized()) {
+ a.initialize(cr - cl, cb - ct, getWidth(), getHeight());
+ child.onAnimationStart();
+ }
+
+ more = a.getTransformation(drawingTime, mChildTransformation);
+ transformToApply = mChildTransformation;
+
+ concatMatrix = a.willChangeTransformationMatrix();
+
+ if (more) {
+ if (!a.willChangeBounds()) {
+ if ((flags & (FLAG_OPTIMIZE_INVALIDATE | FLAG_ANIMATION_DONE)) ==
+ FLAG_OPTIMIZE_INVALIDATE) {
+ mGroupFlags |= FLAG_INVALIDATE_REQUIRED;
+ } else if ((flags & FLAG_INVALIDATE_REQUIRED) == 0) {
+ invalidate(cl, ct, cr, cb);
+ }
+ } else {
+ mGroupFlags |= FLAG_INVALIDATE_REQUIRED;
+ }
+ }
+ } else if ((flags & FLAG_SUPPORT_STATIC_TRANSFORMATIONS) ==
+ FLAG_SUPPORT_STATIC_TRANSFORMATIONS) {
+ final boolean hasTransform = getChildStaticTransformation(child, mChildTransformation);
+ if (hasTransform) {
+ final int transformType = mChildTransformation.getTransformationType();
+ transformToApply = transformType != Transformation.TYPE_IDENTITY ?
+ mChildTransformation : null;
+ concatMatrix = (transformType & Transformation.TYPE_MATRIX) != 0;
+ }
+ }
+
+ if (!concatMatrix && canvas.quickReject(cl, ct, cr, cb, Canvas.EdgeType.BW)) {
+ return more;
+ }
+
+ child.computeScroll();
+
+ final int sx = child.mScrollX;
+ final int sy = child.mScrollY;
+
+ Bitmap cache = null;
+ if ((flags & FLAG_CHILDREN_DRAWN_WITH_CACHE) == FLAG_CHILDREN_DRAWN_WITH_CACHE ||
+ (flags & FLAG_ALWAYS_DRAWN_WITH_CACHE) == FLAG_ALWAYS_DRAWN_WITH_CACHE) {
+ cache = child.getDrawingCache();
+ }
+
+ final boolean hasNoCache = cache == null;
+
+ final int restoreTo = canvas.save();
+ if (hasNoCache) {
+ canvas.translate(cl - sx, ct - sy);
+ } else {
+ canvas.translate(cl, ct);
+ }
+
+ float alpha = 1.0f;
+
+ if (transformToApply != null) {
+ if (concatMatrix) {
+ int transX = 0;
+ int transY = 0;
+ if (hasNoCache) {
+ transX = -sx;
+ transY = -sy;
+ }
+ // Undo the scroll translation, apply the transformation matrix,
+ // then redo the scroll translate to get the correct result.
+ canvas.translate(-transX, -transY);
+ canvas.concat(transformToApply.getMatrix());
+ canvas.translate(transX, transY);
+ mGroupFlags |= FLAG_CLEAR_TRANSFORMATION;
+ }
+
+ alpha = transformToApply.getAlpha();
+ if (alpha < 1.0f) {
+ mGroupFlags |= FLAG_CLEAR_TRANSFORMATION;
+ }
+
+ if (alpha < 1.0f && hasNoCache) {
+ final int multipliedAlpha = (int) (255 * alpha);
+ if (!child.onSetAlpha(multipliedAlpha)) {
+ canvas.saveLayerAlpha(sx, sy, sx + cr - cl, sy + cb - ct, multipliedAlpha,
+ Canvas.HAS_ALPHA_LAYER_SAVE_FLAG | Canvas.CLIP_TO_LAYER_SAVE_FLAG);
+ } else {
+ child.mPrivateFlags |= ALPHA_SET;
+ }
+ }
+ } else if ((child.mPrivateFlags & ALPHA_SET) == ALPHA_SET) {
+ child.onSetAlpha(255);
+ }
+
+ if ((flags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN) {
+ if (hasNoCache) {
+ canvas.clipRect(sx, sy, sx + cr - cl, sy + cb - ct);
+ } else {
+ canvas.clipRect(0, 0, cr - cl, cb - ct);
+ }
+ }
+
+ if (hasNoCache) {
+ // Fast path for layouts with no backgrounds
+ if ((child.mPrivateFlags & SKIP_DRAW) == SKIP_DRAW) {
+ child.mPrivateFlags |= DRAWN;
+ if (ViewDebug.TRACE_HIERARCHY) {
+ ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW);
+ }
+ child.dispatchDraw(canvas);
+ } else {
+ child.draw(canvas);
+ }
+ } else {
+ final Paint cachePaint = mCachePaint;
+ if (alpha < 1.0f) {
+ cachePaint.setAlpha((int) (alpha * 255));
+ mGroupFlags |= FLAG_ALPHA_LOWER_THAN_ONE;
+ } else if ((flags & FLAG_ALPHA_LOWER_THAN_ONE) == FLAG_ALPHA_LOWER_THAN_ONE) {
+ cachePaint.setAlpha(255);
+ mGroupFlags &= ~FLAG_ALPHA_LOWER_THAN_ONE;
+ }
+ child.mPrivateFlags |= DRAWN;
+ if (ViewRoot.PROFILE_DRAWING) {
+ EventLog.writeEvent(60003, hashCode());
+ }
+ canvas.drawBitmap(cache, 0.0f, 0.0f, cachePaint);
+ }
+
+ canvas.restoreToCount(restoreTo);
+
+ if (a != null && !more) {
+ child.onSetAlpha(255);
+ finishAnimatingView(child, a);
+ }
+
+ return more;
+ }
+
+ /**
+ * By default, children are clipped to their bounds before drawing. This
+ * allows view groups to override this behavior for animations, etc.
+ *
+ * @param clipChildren true to clip children to their bounds,
+ * false otherwise
+ * @attr ref android.R.styleable#ViewGroup_clipChildren
+ */
+ public void setClipChildren(boolean clipChildren) {
+ setBooleanFlag(FLAG_CLIP_CHILDREN, clipChildren);
+ }
+
+ /**
+ * By default, children are clipped to the padding of the ViewGroup. This
+ * allows view groups to override this behavior
+ *
+ * @param clipToPadding true to clip children to the padding of the
+ * group, false otherwise
+ * @attr ref android.R.styleable#ViewGroup_clipToPadding
+ */
+ public void setClipToPadding(boolean clipToPadding) {
+ setBooleanFlag(FLAG_CLIP_TO_PADDING, clipToPadding);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void dispatchSetSelected(boolean selected) {
+ final View[] children = mChildren;
+ final int count = mChildrenCount;
+ for (int i = 0; i < count; i++) {
+ children[i].setSelected(selected);
+ }
+ }
+
+ @Override
+ protected void dispatchSetPressed(boolean pressed) {
+ final View[] children = mChildren;
+ final int count = mChildrenCount;
+ for (int i = 0; i < count; i++) {
+ children[i].setPressed(pressed);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected boolean getChildStaticTransformation(View child, Transformation t) {
+ return false;
+ }
+
+ /**
+ * {@hide}
+ */
+ @Override
+ protected View findViewTraversal(int id) {
+ if (id == mID) {
+ return this;
+ }
+
+ final View[] where = mChildren;
+ final int len = mChildrenCount;
+
+ for (int i = 0; i < len; i++) {
+ View v = where[i];
+
+ if ((v.mPrivateFlags & IS_ROOT_NAMESPACE) == 0) {
+ v = v.findViewById(id);
+
+ if (v != null) {
+ return v;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * {@hide}
+ */
+ @Override
+ protected View findViewWithTagTraversal(Object tag) {
+ if (tag != null && tag.equals(mTag)) {
+ return this;
+ }
+
+ final View[] where = mChildren;
+ final int len = mChildrenCount;
+
+ for (int i = 0; i < len; i++) {
+ View v = where[i];
+
+ if ((v.mPrivateFlags & IS_ROOT_NAMESPACE) == 0) {
+ v = v.findViewWithTag(tag);
+
+ if (v != null) {
+ return v;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Adds a child view. If no layout parameters are already set on the child, the
+ * default parameters for this ViewGroup are set on the child.
+ *
+ * @param child the child view to add
+ *
+ * @see #generateDefaultLayoutParams()
+ */
+ public void addView(View child) {
+ addView(child, -1);
+ }
+
+ /**
+ * Adds a child view. If no layout parameters are already set on the child, the
+ * default parameters for this ViewGroup are set on the child.
+ *
+ * @param child the child view to add
+ * @param index the position at which to add the child
+ *
+ * @see #generateDefaultLayoutParams()
+ */
+ public void addView(View child, int index) {
+ LayoutParams params = child.getLayoutParams();
+ if (params == null) {
+ params = generateDefaultLayoutParams();
+ if (params == null) {
+ throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
+ }
+ }
+ addView(child, index, params);
+ }
+
+ /**
+ * Adds a child view with this ViewGroup's default layout parameters and the
+ * specified width and height.
+ *
+ * @param child the child view to add
+ */
+ public void addView(View child, int width, int height) {
+ final LayoutParams params = generateDefaultLayoutParams();
+ params.width = width;
+ params.height = height;
+ addView(child, -1, params);
+ }
+
+ /**
+ * Adds a child view with the specified layout parameters.
+ *
+ * @param child the child view to add
+ * @param params the layout parameters to set on the child
+ */
+ public void addView(View child, LayoutParams params) {
+ addView(child, -1, params);
+ }
+
+ /**
+ * Adds a child view with the specified layout parameters.
+ *
+ * @param child the child view to add
+ * @param index the position at which to add the child
+ * @param params the layout parameters to set on the child
+ */
+ public void addView(View child, int index, LayoutParams params) {
+ if (DBG) {
+ System.out.println(this + " addView");
+ }
+
+ // addViewInner() will call child.requestLayout() when setting the new LayoutParams
+ // therefore, we call requestLayout() on ourselves before, so that the child's request
+ // will be blocked at our level
+ requestLayout();
+ invalidate();
+ addViewInner(child, index, params, false);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
+ if (!checkLayoutParams(params)) {
+ throw new IllegalArgumentException("Invalid LayoutParams supplied to " + this);
+ }
+ if (view.mParent != this) {
+ throw new IllegalArgumentException("Given view not a child of " + this);
+ }
+ view.setLayoutParams(params);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+ return p != null;
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when the hierarchy
+ * within this view changed. The hierarchy changes whenever a child is added
+ * to or removed from this view.
+ */
+ public interface OnHierarchyChangeListener {
+ /**
+ * Called when a new child is added to a parent view.
+ *
+ * @param parent the view in which a child was added
+ * @param child the new child view added in the hierarchy
+ */
+ void onChildViewAdded(View parent, View child);
+
+ /**
+ * Called when a child is removed from a parent view.
+ *
+ * @param parent the view from which the child was removed
+ * @param child the child removed from the hierarchy
+ */
+ void onChildViewRemoved(View parent, View child);
+ }
+
+ /**
+ * Register a callback to be invoked when a child is added to or removed
+ * from this view.
+ *
+ * @param listener the callback to invoke on hierarchy change
+ */
+ public void setOnHierarchyChangeListener(OnHierarchyChangeListener listener) {
+ mOnHierarchyChangeListener = listener;
+ }
+
+ /**
+ * Adds a view during layout. This is useful if in your onLayout() method,
+ * you need to add more views (as does the list view for example).
+ *
+ * If index is negative, it means put it at the end of the list.
+ *
+ * @param child the view to add to the group
+ * @param index the index at which the child must be added
+ * @param params the layout parameters to associate with the child
+ * @return true if the child was added, false otherwise
+ */
+ protected boolean addViewInLayout(View child, int index, LayoutParams params) {
+ return addViewInLayout(child, index, params, false);
+ }
+
+ /**
+ * Adds a view during layout. This is useful if in your onLayout() method,
+ * you need to add more views (as does the list view for example).
+ *
+ * If index is negative, it means put it at the end of the list.
+ *
+ * @param child the view to add to the group
+ * @param index the index at which the child must be added
+ * @param params the layout parameters to associate with the child
+ * @param preventRequestLayout if true, calling this method will not trigger a
+ * layout request on child
+ * @return true if the child was added, false otherwise
+ */
+ protected boolean addViewInLayout(View child, int index, LayoutParams params,
+ boolean preventRequestLayout) {
+ child.mParent = null;
+ addViewInner(child, index, params, preventRequestLayout);
+ child.mPrivateFlags |= DRAWN;
+ return true;
+ }
+
+ /**
+ * Prevents the specified child to be laid out during the next layout pass.
+ *
+ * @param child the child on which to perform the cleanup
+ */
+ protected void cleanupLayoutState(View child) {
+ child.mPrivateFlags &= ~View.FORCE_LAYOUT;
+ }
+
+ private void addViewInner(View child, int index, LayoutParams params,
+ boolean preventRequestLayout) {
+
+ if (child.getParent() != null) {
+ throw new IllegalStateException("The specified child already has a parent. " +
+ "You must call removeView() on the child's parent first.");
+ }
+
+ if (!checkLayoutParams(params)) {
+ params = generateLayoutParams(params);
+ }
+
+ if (preventRequestLayout) {
+ child.mLayoutParams = params;
+ } else {
+ child.setLayoutParams(params);
+ }
+
+ if (index < 0) {
+ index = mChildrenCount;
+ }
+
+ addInArray(child, index);
+
+ // tell our children
+ if (preventRequestLayout) {
+ child.assignParent(this);
+ } else {
+ child.mParent = this;
+ }
+
+ if (child.hasFocus()) {
+ requestChildFocus(child, child.findFocus());
+ }
+
+ AttachInfo ai = mAttachInfo;
+ if (ai != null) {
+ boolean lastKeepOn = ai.mKeepScreenOn;
+ ai.mKeepScreenOn = false;
+ child.dispatchAttachedToWindow(mAttachInfo, (mViewFlags&VISIBILITY_MASK));
+ if (ai.mKeepScreenOn) {
+ needGlobalAttributesUpdate(true);
+ }
+ ai.mKeepScreenOn = lastKeepOn;
+ }
+
+ if (mOnHierarchyChangeListener != null) {
+ mOnHierarchyChangeListener.onChildViewAdded(this, child);
+ }
+
+ if ((child.mViewFlags & DUPLICATE_PARENT_STATE) == DUPLICATE_PARENT_STATE) {
+ mGroupFlags |= FLAG_NOTIFY_CHILDREN_ON_DRAWABLE_STATE_CHANGE;
+ }
+ }
+
+ private void addInArray(View child, int index) {
+ View[] children = mChildren;
+ final int count = mChildrenCount;
+ final int size = children.length;
+ if (index == count) {
+ if (size == count) {
+ mChildren = new View[size + ARRAY_CAPACITY_INCREMENT];
+ System.arraycopy(children, 0, mChildren, 0, size);
+ children = mChildren;
+ }
+ children[mChildrenCount++] = child;
+ } else if (index < count) {
+ if (size == count) {
+ mChildren = new View[size + ARRAY_CAPACITY_INCREMENT];
+ System.arraycopy(children, 0, mChildren, 0, index);
+ System.arraycopy(children, index, mChildren, index + 1, count - index);
+ children = mChildren;
+ } else {
+ System.arraycopy(children, index, children, index + 1, count - index);
+ }
+ children[index] = child;
+ mChildrenCount++;
+ } else {
+ throw new IndexOutOfBoundsException("index=" + index + " count=" + count);
+ }
+ }
+
+ // This method also sets the child's mParent to null
+ private void removeFromArray(int index) {
+ final View[] children = mChildren;
+ children[index].mParent = null;
+ final int count = mChildrenCount;
+ if (index == count - 1) {
+ children[--mChildrenCount] = null;
+ } else if (index >= 0 && index < count) {
+ System.arraycopy(children, index + 1, children, index, count - index - 1);
+ children[--mChildrenCount] = null;
+ } else {
+ throw new IndexOutOfBoundsException();
+ }
+ }
+
+ // This method also sets the children's mParent to null
+ private void removeFromArray(int start, int count) {
+ final View[] children = mChildren;
+ final int childrenCount = mChildrenCount;
+
+ start = Math.max(0, start);
+ final int end = Math.min(childrenCount, start + count);
+
+ if (start == end) {
+ return;
+ }
+
+ if (end == childrenCount) {
+ for (int i = start; i < end; i++) {
+ children[i].mParent = null;
+ children[i] = null;
+ }
+ } else {
+ for (int i = start; i < end; i++) {
+ children[i].mParent = null;
+ }
+
+ // Since we're looping above, we might as well do the copy, but is arraycopy()
+ // faster than the extra 2 bounds checks we would do in the loop?
+ System.arraycopy(children, end, children, start, childrenCount - end);
+
+ for (int i = childrenCount - (end - start); i < childrenCount; i++) {
+ children[i] = null;
+ }
+ }
+
+ mChildrenCount -= (end - start);
+ }
+
+ private void bindLayoutAnimation(View child) {
+ Animation a = mLayoutAnimationController.getAnimationForView(child);
+ child.setAnimation(a);
+ }
+
+ /**
+ * Subclasses should override this method to set layout animation
+ * parameters on the supplied child.
+ *
+ * @param child the child to associate with animation parameters
+ * @param params the child's layout parameters which hold the animation
+ * parameters
+ * @param index the index of the child in the view group
+ * @param count the number of children in the view group
+ */
+ protected void attachLayoutAnimationParameters(View child,
+ LayoutParams params, int index, int count) {
+ LayoutAnimationController.AnimationParameters animationParams =
+ params.layoutAnimationParameters;
+ if (animationParams == null) {
+ animationParams =
+ new LayoutAnimationController.AnimationParameters();
+ params.layoutAnimationParameters = animationParams;
+ }
+
+ animationParams.count = count;
+ animationParams.index = index;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void removeView(View view) {
+ removeViewInternal(view);
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Removes a view during layout. This is useful if in your onLayout() method,
+ * you need to remove more views.
+ *
+ * @param view the view to remove from the group
+ */
+ public void removeViewInLayout(View view) {
+ removeViewInternal(view);
+ }
+
+ /**
+ * Removes a range of views during layout. This is useful if in your onLayout() method,
+ * you need to remove more views.
+ *
+ * @param start the index of the first view to remove from the group
+ * @param count the number of views to remove from the group
+ */
+ public void removeViewsInLayout(int start, int count) {
+ removeViewsInternal(start, count);
+ }
+
+ /**
+ * Removes the view at the specified position in the group.
+ *
+ * @param index the position in the group of the view to remove
+ */
+ public void removeViewAt(int index) {
+ removeViewInternal(index, getChildAt(index));
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Removes the specified range of views from the group.
+ *
+ * @param start the first position in the group of the range of views to remove
+ * @param count the number of views to remove
+ */
+ public void removeViews(int start, int count) {
+ removeViewsInternal(start, count);
+ requestLayout();
+ invalidate();
+ }
+
+ private void removeViewInternal(View view) {
+ final int index = indexOfChild(view);
+ if (index >= 0) {
+ removeViewInternal(index, view);
+ }
+ }
+
+ private void removeViewInternal(int index, View view) {
+ boolean clearChildFocus = false;
+ if (view == mFocused) {
+ view.clearFocusForRemoval();
+ clearChildFocus = true;
+ }
+
+ if (view.getAnimation() != null) {
+ addDisappearingView(view);
+ } else if (mAttachInfo != null) {
+ view.dispatchDetachedFromWindow();
+ }
+
+ if (mOnHierarchyChangeListener != null) {
+ mOnHierarchyChangeListener.onChildViewRemoved(this, view);
+ }
+
+ needGlobalAttributesUpdate(false);
+
+ removeFromArray(index);
+
+ if (clearChildFocus) {
+ clearChildFocus(view);
+ }
+ }
+
+ private void removeViewsInternal(int start, int count) {
+ final OnHierarchyChangeListener onHierarchyChangeListener = mOnHierarchyChangeListener;
+ final boolean notifyListener = onHierarchyChangeListener != null;
+ final View focused = mFocused;
+ final boolean detach = mAttachInfo != null;
+ View clearChildFocus = null;
+
+ final View[] children = mChildren;
+ final int end = start + count;
+
+ for (int i = start; i < end; i++) {
+ final View view = children[i];
+
+ if (view == focused) {
+ view.clearFocusForRemoval();
+ clearChildFocus = view;
+ }
+
+ if (view.getAnimation() != null) {
+ addDisappearingView(view);
+ } else if (detach) {
+ view.dispatchDetachedFromWindow();
+ }
+
+ needGlobalAttributesUpdate(false);
+
+ if (notifyListener) {
+ onHierarchyChangeListener.onChildViewRemoved(this, view);
+ }
+ }
+
+ removeFromArray(start, count);
+
+ if (clearChildFocus != null) {
+ clearChildFocus(clearChildFocus);
+ }
+ }
+
+ /**
+ * Call this method to remove all child views from the
+ * ViewGroup.
+ */
+ public void removeAllViews() {
+ removeAllViewsInLayout();
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Called by a ViewGroup subclass to remove child views from itself,
+ * when it must first know its size on screen before it can calculate how many
+ * child views it will render. An example is a Gallery or a ListView, which
+ * may "have" 50 children, but actually only render the number of children
+ * that can currently fit inside the object on screen. Do not call
+ * this method unless you are extending ViewGroup and understand the
+ * view measuring and layout pipeline.
+ */
+ public void removeAllViewsInLayout() {
+ final int count = mChildrenCount;
+ if (count <= 0) {
+ return;
+ }
+
+ final View[] children = mChildren;
+ mChildrenCount = 0;
+
+ final OnHierarchyChangeListener listener = mOnHierarchyChangeListener;
+ final boolean notify = listener != null;
+ final View focused = mFocused;
+ final boolean detach = mAttachInfo != null;
+ View clearChildFocus = null;
+
+ needGlobalAttributesUpdate(false);
+
+ for (int i = count - 1; i >= 0; i--) {
+ final View view = children[i];
+
+ if (view == focused) {
+ view.clearFocusForRemoval();
+ clearChildFocus = view;
+ }
+
+ if (view.getAnimation() != null) {
+ addDisappearingView(view);
+ } else if (detach) {
+ view.dispatchDetachedFromWindow();
+ }
+
+ if (notify) {
+ listener.onChildViewRemoved(this, view);
+ }
+
+ view.mParent = null;
+ children[i] = null;
+ }
+
+ if (clearChildFocus != null) {
+ clearChildFocus(clearChildFocus);
+ }
+ }
+
+ /**
+ * Finishes the removal of a detached view. This method will dispatch the detached from
+ * window event and notify the hierarchy change listener.
+ *
+ * @param child the child to be definitely removed from the view hierarchy
+ * @param animate if true and the view has an animation, the view is placed in the
+ * disappearing views list, otherwise, it is detached from the window
+ *
+ * @see #attachViewToParent(View, int, android.view.ViewGroup.LayoutParams)
+ * @see #detachAllViewsFromParent()
+ * @see #detachViewFromParent(View)
+ * @see #detachViewFromParent(int)
+ */
+ protected void removeDetachedView(View child, boolean animate) {
+ if (child == mFocused) {
+ child.clearFocus();
+ }
+
+ if (animate && child.getAnimation() != null) {
+ addDisappearingView(child);
+ } else if (mAttachInfo != null) {
+ child.dispatchDetachedFromWindow();
+ }
+
+ if (mOnHierarchyChangeListener != null) {
+ mOnHierarchyChangeListener.onChildViewRemoved(this, child);
+ }
+ }
+
+ /**
+ * Attaches a view to this view group. Attaching a view assigns this group as the parent,
+ * sets the layout parameters and puts the view in the list of children so it can be retrieved
+ * by calling {@link #getChildAt(int)}.
+ *
+ * This method should be called only for view which were detached from their parent.
+ *
+ * @param child the child to attach
+ * @param index the index at which the child should be attached
+ * @param params the layout parameters of the child
+ *
+ * @see #removeDetachedView(View, boolean)
+ * @see #detachAllViewsFromParent()
+ * @see #detachViewFromParent(View)
+ * @see #detachViewFromParent(int)
+ */
+ protected void attachViewToParent(View child, int index, LayoutParams params) {
+ child.mLayoutParams = params;
+
+ if (index < 0) {
+ index = mChildrenCount;
+ }
+
+ addInArray(child, index);
+
+ child.mParent = this;
+ child.mPrivateFlags |= DRAWN;
+
+ if (child.hasFocus()) {
+ requestChildFocus(child, child.findFocus());
+ }
+ }
+
+ /**
+ * Detaches a view from its parent. Detaching a view should be temporary and followed
+ * either by a call to {@link #attachViewToParent(View, int, android.view.ViewGroup.LayoutParams)}
+ * or a call to {@link #removeDetachedView(View, boolean)}. When a view is detached,
+ * its parent is null and cannot be retrieved by a call to {@link #getChildAt(int)}.
+ *
+ * @param child the child to detach
+ *
+ * @see #detachViewFromParent(int)
+ * @see #detachViewsFromParent(int, int)
+ * @see #detachAllViewsFromParent()
+ * @see #attachViewToParent(View, int, android.view.ViewGroup.LayoutParams)
+ * @see #removeDetachedView(View, boolean)
+ */
+ protected void detachViewFromParent(View child) {
+ removeFromArray(indexOfChild(child));
+ }
+
+ /**
+ * Detaches a view from its parent. Detaching a view should be temporary and followed
+ * either by a call to {@link #attachViewToParent(View, int, android.view.ViewGroup.LayoutParams)}
+ * or a call to {@link #removeDetachedView(View, boolean)}. When a view is detached,
+ * its parent is null and cannot be retrieved by a call to {@link #getChildAt(int)}.
+ *
+ * @param index the index of the child to detach
+ *
+ * @see #detachViewFromParent(View)
+ * @see #detachAllViewsFromParent()
+ * @see #detachViewsFromParent(int, int)
+ * @see #attachViewToParent(View, int, android.view.ViewGroup.LayoutParams)
+ * @see #removeDetachedView(View, boolean)
+ */
+ protected void detachViewFromParent(int index) {
+ removeFromArray(index);
+ }
+
+ /**
+ * Detaches a range of view from their parent. Detaching a view should be temporary and followed
+ * either by a call to {@link #attachViewToParent(View, int, android.view.ViewGroup.LayoutParams)}
+ * or a call to {@link #removeDetachedView(View, boolean)}. When a view is detached, its
+ * parent is null and cannot be retrieved by a call to {@link #getChildAt(int)}.
+ *
+ * @param start the first index of the childrend range to detach
+ * @param count the number of children to detach
+ *
+ * @see #detachViewFromParent(View)
+ * @see #detachViewFromParent(int)
+ * @see #detachAllViewsFromParent()
+ * @see #attachViewToParent(View, int, android.view.ViewGroup.LayoutParams)
+ * @see #removeDetachedView(View, boolean)
+ */
+ protected void detachViewsFromParent(int start, int count) {
+ removeFromArray(start, count);
+ }
+
+ /**
+ * Detaches all views from theparent. Detaching a view should be temporary and followed
+ * either by a call to {@link #attachViewToParent(View, int, android.view.ViewGroup.LayoutParams)}
+ * or a call to {@link #removeDetachedView(View, boolean)}. When a view is detached,
+ * its parent is null and cannot be retrieved by a call to {@link #getChildAt(int)}.
+ *
+ * @see #detachViewFromParent(View)
+ * @see #detachViewFromParent(int)
+ * @see #detachViewsFromParent(int, int)
+ * @see #attachViewToParent(View, int, android.view.ViewGroup.LayoutParams)
+ * @see #removeDetachedView(View, boolean)
+ */
+ protected void detachAllViewsFromParent() {
+ final int count = mChildrenCount;
+ if (count <= 0) {
+ return;
+ }
+
+ final View[] children = mChildren;
+ mChildrenCount = 0;
+
+ for (int i = count - 1; i >= 0; i--) {
+ children[i].mParent = null;
+ children[i] = null;
+ }
+ }
+
+ /**
+ * Don't call or override this method. It is used for the implementation of
+ * the view hierarchy.
+ */
+ public final void invalidateChild(View child, final Rect dirty) {
+ if (ViewDebug.TRACE_HIERARCHY) {
+ ViewDebug.trace(this, ViewDebug.HierarchyTraceType.INVALIDATE_CHILD);
+ }
+
+ ViewParent parent = this;
+
+ final int[] location = mLocation;
+ location[CHILD_LEFT_INDEX] = child.mLeft;
+ location[CHILD_TOP_INDEX] = child.mTop;
+
+ do {
+ parent = parent.invalidateChildInParent(location, dirty);
+ } while (parent != null);
+ }
+
+ /**
+ * Don't call or override this method. It is used for the implementation of
+ * the view hierarchy.
+ *
+ * This implementation returns null if this ViewGroup does not have a parent,
+ * if this ViewGroup is already fully invalidated or if the dirty rectangle
+ * does not intersect with this ViewGroup's bounds.
+ */
+ public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
+ if (ViewDebug.TRACE_HIERARCHY) {
+ ViewDebug.trace(this, ViewDebug.HierarchyTraceType.INVALIDATE_CHILD_IN_PARENT);
+ }
+
+ if ((mPrivateFlags & DRAWN) == DRAWN) {
+ if ((mGroupFlags & (FLAG_OPTIMIZE_INVALIDATE | FLAG_ANIMATION_DONE)) !=
+ FLAG_OPTIMIZE_INVALIDATE) {
+ dirty.offset(location[CHILD_LEFT_INDEX] - mScrollX,
+ location[CHILD_TOP_INDEX] - mScrollY);
+
+ final int left = mLeft;
+ final int top = mTop;
+
+ if (dirty.intersect(0, 0, mRight - left, mBottom - top)) {
+ mPrivateFlags &= ~DRAWING_CACHE_VALID;
+
+ location[CHILD_LEFT_INDEX] = left;
+ location[CHILD_TOP_INDEX] = top;
+
+ return mParent;
+ }
+ } else {
+ mPrivateFlags &= ~DRAWN & ~DRAWING_CACHE_VALID;
+
+ location[CHILD_LEFT_INDEX] = mLeft;
+ location[CHILD_TOP_INDEX] = mTop;
+
+ dirty.set(0, 0, mRight - location[CHILD_LEFT_INDEX],
+ mBottom - location[CHILD_TOP_INDEX]);
+
+ return mParent;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Offset a rectangle that is in a descendant's coordinate
+ * space into our coordinate space.
+ * @param descendant A descendant of this view
+ * @param rect A rectangle defined in descendant's coordinate space.
+ */
+ public final void offsetDescendantRectToMyCoords(View descendant, Rect rect) {
+ offsetRectBetweenParentAndChild(descendant, rect, true, false);
+ }
+
+ /**
+ * Offset a rectangle that is in our coordinate space into an ancestor's
+ * coordinate space.
+ * @param descendant A descendant of this view
+ * @param rect A rectangle defined in descendant's coordinate space.
+ */
+ public final void offsetRectIntoDescendantCoords(View descendant, Rect rect) {
+ offsetRectBetweenParentAndChild(descendant, rect, false, false);
+ }
+
+ /**
+ * Helper method that offsets a rect either from parent to descendant or
+ * descendant to parent.
+ */
+ void offsetRectBetweenParentAndChild(View descendant, Rect rect,
+ boolean offsetFromChildToParent, boolean clipToBounds) {
+
+ // already in the same coord system :)
+ if (descendant == this) {
+ return;
+ }
+
+ ViewParent theParent = descendant.mParent;
+
+ // search and offset up to the parent
+ while ((theParent != null)
+ && (theParent instanceof View)
+ && (theParent != this)) {
+
+ if (offsetFromChildToParent) {
+ rect.offset(descendant.mLeft - descendant.mScrollX,
+ descendant.mTop - descendant.mScrollY);
+ if (clipToBounds) {
+ View p = (View) theParent;
+ rect.intersect(0, 0, p.mRight - p.mLeft, p.mBottom - p.mTop);
+ }
+ } else {
+ if (clipToBounds) {
+ View p = (View) theParent;
+ rect.intersect(0, 0, p.mRight - p.mLeft, p.mBottom - p.mTop);
+ }
+ rect.offset(descendant.mScrollX - descendant.mLeft,
+ descendant.mScrollY - descendant.mTop);
+ }
+
+ descendant = (View) theParent;
+ theParent = descendant.mParent;
+ }
+
+ // now that we are up to this view, need to offset one more time
+ // to get into our coordinate space
+ if (theParent == this) {
+ if (offsetFromChildToParent) {
+ rect.offset(descendant.mLeft - descendant.mScrollX,
+ descendant.mTop - descendant.mScrollY);
+ } else {
+ rect.offset(descendant.mScrollX - descendant.mLeft,
+ descendant.mScrollY - descendant.mTop);
+ }
+ } else {
+ throw new IllegalArgumentException("parameter must be a descendant of this view");
+ }
+ }
+
+ /**
+ * Offset the vertical location of all children of this view by the specified number of pixels.
+ *
+ * @param offset the number of pixels to offset
+ *
+ * @hide
+ */
+ public void offsetChildrenTopAndBottom(int offset) {
+ final int count = mChildrenCount;
+ final View[] children = mChildren;
+
+ for (int i = 0; i < count; i++) {
+ final View v = children[i];
+ v.mTop += offset;
+ v.mBottom += offset;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean getChildVisibleRect(View child, Rect r, android.graphics.Point offset) {
+ int dx = child.mLeft - mScrollX;
+ int dy = child.mTop - mScrollY;
+ if (offset != null) {
+ offset.x += dx;
+ offset.y += dy;
+ }
+ r.offset(dx, dy);
+ return r.intersect(0, 0, mRight - mLeft, mBottom - mTop) &&
+ (mParent == null || mParent.getChildVisibleRect(this, r, offset));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected abstract void onLayout(boolean changed,
+ int l, int t, int r, int b);
+
+ /**
+ * Indicates whether the view group has the ability to animate its children
+ * after the first layout.
+ *
+ * @return true if the children can be animated, false otherwise
+ */
+ protected boolean canAnimate() {
+ return mLayoutAnimationController != null;
+ }
+
+ /**
+ * Runs the layout animation. Calling this method triggers a relayout of
+ * this view group.
+ */
+ public void startLayoutAnimation() {
+ if (mLayoutAnimationController != null) {
+ mGroupFlags |= FLAG_RUN_ANIMATION;
+ requestLayout();
+ }
+ }
+
+ /**
+ * Schedules the layout animation to be played after the next layout pass
+ * of this view group. This can be used to restart the layout animation
+ * when the content of the view group changes or when the activity is
+ * paused and resumed.
+ */
+ public void scheduleLayoutAnimation() {
+ mGroupFlags |= FLAG_RUN_ANIMATION;
+ }
+
+ /**
+ * Sets the layout animation controller used to animate the group's
+ * children after the first layout.
+ *
+ * @param controller the animation controller
+ */
+ public void setLayoutAnimation(LayoutAnimationController controller) {
+ mLayoutAnimationController = controller;
+ if (mLayoutAnimationController != null) {
+ mGroupFlags |= FLAG_RUN_ANIMATION;
+ }
+ }
+
+ /**
+ * Returns the layout animation controller used to animate the group's
+ * children.
+ *
+ * @return the current animation controller
+ */
+ public LayoutAnimationController getLayoutAnimation() {
+ return mLayoutAnimationController;
+ }
+
+ /**
+ * Indicates whether the children's drawing cache is used during a layout
+ * animation. By default, the drawing cache is enabled but this will prevent
+ * nested layout animations from working. To nest animations, you must disable
+ * the cache.
+ *
+ * @return true if the animation cache is enabled, false otherwise
+ *
+ * @see #setAnimationCacheEnabled(boolean)
+ * @see View#setDrawingCacheEnabled(boolean)
+ */
+ @ViewDebug.ExportedProperty
+ public boolean isAnimationCacheEnabled() {
+ return (mGroupFlags & FLAG_ANIMATION_CACHE) == FLAG_ANIMATION_CACHE;
+ }
+
+ /**
+ * Enables or disables the children's drawing cache during a layout animation.
+ * By default, the drawing cache is enabled but this will prevent nested
+ * layout animations from working. To nest animations, you must disable the
+ * cache.
+ *
+ * @param enabled true to enable the animation cache, false otherwise
+ *
+ * @see #isAnimationCacheEnabled()
+ * @see View#setDrawingCacheEnabled(boolean)
+ */
+ public void setAnimationCacheEnabled(boolean enabled) {
+ setBooleanFlag(FLAG_ANIMATION_CACHE, enabled);
+ }
+
+ /**
+ * Indicates whether this ViewGroup will always try to draw its children using their
+ * drawing cache. By default this property is enabled.
+ *
+ * @return true if the animation cache is enabled, false otherwise
+ *
+ * @see #setAlwaysDrawnWithCacheEnabled(boolean)
+ * @see #setChildrenDrawnWithCacheEnabled(boolean)
+ * @see View#setDrawingCacheEnabled(boolean)
+ */
+ @ViewDebug.ExportedProperty
+ public boolean isAlwaysDrawnWithCacheEnabled() {
+ return (mGroupFlags & FLAG_ALWAYS_DRAWN_WITH_CACHE) == FLAG_ALWAYS_DRAWN_WITH_CACHE;
+ }
+
+ /**
+ * Indicates whether this ViewGroup will always try to draw its children using their
+ * drawing cache. This property can be set to true when the cache rendering is
+ * slightly different from the children's normal rendering. Renderings can be different,
+ * for instance, when the cache's quality is set to low.
+ *
+ * When this property is disabled, the ViewGroup will use the drawing cache of its
+ * children only when asked to. It's usually the task of subclasses to tell ViewGroup
+ * when to start using the drawing cache and when to stop using it.
+ *
+ * @param always true to always draw with the drawing cache, false otherwise
+ *
+ * @see #isAlwaysDrawnWithCacheEnabled()
+ * @see #setChildrenDrawnWithCacheEnabled(boolean)
+ * @see View#setDrawingCacheEnabled(boolean)
+ * @see View#setDrawingCacheQuality(int)
+ */
+ public void setAlwaysDrawnWithCacheEnabled(boolean always) {
+ setBooleanFlag(FLAG_ALWAYS_DRAWN_WITH_CACHE, always);
+ }
+
+ /**
+ * Indicates whether the ViewGroup is currently drawing its children using
+ * their drawing cache.
+ *
+ * @return true if children should be drawn with their cache, false otherwise
+ *
+ * @see #setAlwaysDrawnWithCacheEnabled(boolean)
+ * @see #setChildrenDrawnWithCacheEnabled(boolean)
+ */
+ @ViewDebug.ExportedProperty
+ protected boolean isChildrenDrawnWithCacheEnabled() {
+ return (mGroupFlags & FLAG_CHILDREN_DRAWN_WITH_CACHE) == FLAG_CHILDREN_DRAWN_WITH_CACHE;
+ }
+
+ /**
+ * Tells the ViewGroup to draw its children using their drawing cache. This property
+ * is ignored when {@link #isAlwaysDrawnWithCacheEnabled()} is true. A child's drawing cache
+ * will be used only if it has been enabled.
+ *
+ * Subclasses should call this method to start and stop using the drawing cache when
+ * they perform performance sensitive operations, like scrolling or animating.
+ *
+ * @param enabled true if children should be drawn with their cache, false otherwise
+ *
+ * @see #setAlwaysDrawnWithCacheEnabled(boolean)
+ * @see #isChildrenDrawnWithCacheEnabled()
+ */
+ protected void setChildrenDrawnWithCacheEnabled(boolean enabled) {
+ setBooleanFlag(FLAG_CHILDREN_DRAWN_WITH_CACHE, enabled);
+ }
+
+ private void setBooleanFlag(int flag, boolean value) {
+ if (value) {
+ mGroupFlags |= flag;
+ } else {
+ mGroupFlags &= ~flag;
+ }
+ }
+
+ /**
+ * Returns an integer indicating what types of drawing caches are kept in memory.
+ *
+ * @see #setPersistentDrawingCache(int)
+ * @see #setAnimationCacheEnabled(boolean)
+ *
+ * @return one or a combination of {@link #PERSISTENT_NO_CACHE},
+ * {@link #PERSISTENT_ANIMATION_CACHE}, {@link #PERSISTENT_SCROLLING_CACHE}
+ * and {@link #PERSISTENT_ALL_CACHES}
+ */
+ @ViewDebug.ExportedProperty(mapping = {
+ @ViewDebug.IntToString(from = PERSISTENT_NO_CACHE, to = "NONE"),
+ @ViewDebug.IntToString(from = PERSISTENT_ALL_CACHES, to = "ANIMATION"),
+ @ViewDebug.IntToString(from = PERSISTENT_SCROLLING_CACHE, to = "SCROLLING"),
+ @ViewDebug.IntToString(from = PERSISTENT_ALL_CACHES, to = "ALL")
+ })
+ public int getPersistentDrawingCache() {
+ return mPersistentDrawingCache;
+ }
+
+ /**
+ * Indicates what types of drawing caches should be kept in memory after
+ * they have been created.
+ *
+ * @see #getPersistentDrawingCache()
+ * @see #setAnimationCacheEnabled(boolean)
+ *
+ * @param drawingCacheToKeep one or a combination of {@link #PERSISTENT_NO_CACHE},
+ * {@link #PERSISTENT_ANIMATION_CACHE}, {@link #PERSISTENT_SCROLLING_CACHE}
+ * and {@link #PERSISTENT_ALL_CACHES}
+ */
+ public void setPersistentDrawingCache(int drawingCacheToKeep) {
+ mPersistentDrawingCache = drawingCacheToKeep & PERSISTENT_ALL_CACHES;
+ }
+
+ /**
+ * Returns a new set of layout parameters based on the supplied attributes set.
+ *
+ * @param attrs the attributes to build the layout parameters from
+ *
+ * @return an instance of {@link android.view.ViewGroup.LayoutParams} or one
+ * of its descendants
+ */
+ public LayoutParams generateLayoutParams(AttributeSet attrs) {
+ return new LayoutParams(getContext(), attrs);
+ }
+
+ /**
+ * Returns a safe set of layout parameters based on the supplied layout params.
+ * When a ViewGroup is passed a View whose layout params do not pass the test of
+ * {@link #checkLayoutParams(android.view.ViewGroup.LayoutParams)}, this method
+ * is invoked. This method should return a new set of layout params suitable for
+ * this ViewGroup, possibly by copying the appropriate attributes from the
+ * specified set of layout params.
+ *
+ * @param p The layout parameters to convert into a suitable set of layout parameters
+ * for this ViewGroup.
+ *
+ * @return an instance of {@link android.view.ViewGroup.LayoutParams} or one
+ * of its descendants
+ */
+ protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
+ return p;
+ }
+
+ /**
+ * Returns a set of default layout parameters. These parameters are requested
+ * when the View passed to {@link #addView(View)} has no layout parameters
+ * already set. If null is returned, an exception is thrown from addView.
+ *
+ * @return a set of default layout parameters or null
+ */
+ protected LayoutParams generateDefaultLayoutParams() {
+ return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void debug(int depth) {
+ super.debug(depth);
+ String output;
+
+ if (mFocused != null) {
+ output = debugIndent(depth);
+ output += "mFocused";
+ Log.d(VIEW_LOG_TAG, output);
+ }
+ if (mChildrenCount != 0) {
+ output = debugIndent(depth);
+ output += "{";
+ Log.d(VIEW_LOG_TAG, output);
+ }
+ int count = mChildrenCount;
+ for (int i = 0; i < count; i++) {
+ View child = mChildren[i];
+ child.debug(depth + 1);
+ }
+
+ if (mChildrenCount != 0) {
+ output = debugIndent(depth);
+ output += "}";
+ Log.d(VIEW_LOG_TAG, output);
+ }
+ }
+
+ /**
+ * Returns the position in the group of the specified child view.
+ *
+ * @param child the view for which to get the position
+ * @return a positive integer representing the position of the view in the
+ * group, or -1 if the view does not exist in the group
+ */
+ public int indexOfChild(View child) {
+ final int count = mChildrenCount;
+ final View[] children = mChildren;
+ for (int i = 0; i < count; i++) {
+ if (children[i] == child) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Returns the number of children in the group.
+ *
+ * @return a positive integer representing the number of children in
+ * the group
+ */
+ public int getChildCount() {
+ return mChildrenCount;
+ }
+
+ /**
+ * Returns the view at the specified position in the group.
+ *
+ * @param index the position at which to get the view from
+ * @return the view at the specified position or null if the position
+ * does not exist within the group
+ */
+ public View getChildAt(int index) {
+ try {
+ return mChildren[index];
+ } catch (IndexOutOfBoundsException ex) {
+ return null;
+ }
+ }
+
+ /**
+ * Ask all of the children of this view to measure themselves, taking into
+ * account both the MeasureSpec requirements for this view and its padding.
+ * We skip children that are in the GONE state The heavy lifting is done in
+ * getChildMeasureSpec.
+ *
+ * @param widthMeasureSpec The width requirements for this view
+ * @param heightMeasureSpec The height requirements for this view
+ */
+ protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
+ final int size = mChildrenCount;
+ final View[] children = mChildren;
+ for (int i = 0; i < size; ++i) {
+ final View child = children[i];
+ if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
+ measureChild(child, widthMeasureSpec, heightMeasureSpec);
+ }
+ }
+ }
+
+ /**
+ * Ask one of the children of this view to measure itself, taking into
+ * account both the MeasureSpec requirements for this view and its padding.
+ * The heavy lifting is done in getChildMeasureSpec.
+ *
+ * @param child The child to measure
+ * @param parentWidthMeasureSpec The width requirements for this view
+ * @param parentHeightMeasureSpec The height requirements for this view
+ */
+ protected void measureChild(View child, int parentWidthMeasureSpec,
+ int parentHeightMeasureSpec) {
+ final LayoutParams lp = child.getLayoutParams();
+
+ final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
+ mPaddingLeft + mPaddingRight, lp.width);
+ final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
+ mPaddingTop + mPaddingBottom, lp.height);
+
+ child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+ }
+
+ /**
+ * Ask one of the children of this view to measure itself, taking into
+ * account both the MeasureSpec requirements for this view and its padding
+ * and margins. The child must have MarginLayoutParams The heavy lifting is
+ * done in getChildMeasureSpec.
+ *
+ * @param child The child to measure
+ * @param parentWidthMeasureSpec The width requirements for this view
+ * @param widthUsed Extra space that has been used up by the parent
+ * horizontally (possibly by other children of the parent)
+ * @param parentHeightMeasureSpec The height requirements for this view
+ * @param heightUsed Extra space that has been used up by the parent
+ * vertically (possibly by other children of the parent)
+ */
+ protected void measureChildWithMargins(View child,
+ int parentWidthMeasureSpec, int widthUsed,
+ int parentHeightMeasureSpec, int heightUsed) {
+ final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
+
+ final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
+ mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ + widthUsed, lp.width);
+ final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
+ mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ + heightUsed, lp.height);
+
+ child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+ }
+
+ /**
+ * Does the hard part of measureChildren: figuring out the MeasureSpec to
+ * pass to a particular child. This method figures out the right MeasureSpec
+ * for one dimension (height or width) of one child view.
+ *
+ * The goal is to combine information from our MeasureSpec with the
+ * LayoutParams of the child to get the best possible results. For example,
+ * if the this view knows its size (because its MeasureSpec has a mode of
+ * EXACTLY), and the child has indicated in its LayoutParams that it wants
+ * to be the same size as the parent, the parent should ask the child to
+ * layout given an exact size.
+ *
+ * @param spec The requirements for this view
+ * @param padding The padding of this view for the current dimension and
+ * margins, if applicable
+ * @param childDimension How big the child wants to be in the current
+ * dimension
+ * @return a MeasureSpec integer for the child
+ */
+ public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
+ int specMode = MeasureSpec.getMode(spec);
+ int specSize = MeasureSpec.getSize(spec);
+
+ int size = Math.max(0, specSize - padding);
+
+ int resultSize = 0;
+ int resultMode = 0;
+
+ switch (specMode) {
+ // Parent has imposed an exact size on us
+ case MeasureSpec.EXACTLY:
+ if (childDimension >= 0) {
+ resultSize = childDimension;
+ resultMode = MeasureSpec.EXACTLY;
+ } else if (childDimension == LayoutParams.FILL_PARENT) {
+ // Child wants to be our size. So be it.
+ resultSize = size;
+ resultMode = MeasureSpec.EXACTLY;
+ } else if (childDimension == LayoutParams.WRAP_CONTENT) {
+ // Child wants to determine its own size. It can't be
+ // bigger than us.
+ resultSize = size;
+ resultMode = MeasureSpec.AT_MOST;
+ }
+ break;
+
+ // Parent has imposed a maximum size on us
+ case MeasureSpec.AT_MOST:
+ if (childDimension >= 0) {
+ // Child wants a specific size... so be it
+ resultSize = childDimension;
+ resultMode = MeasureSpec.EXACTLY;
+ } else if (childDimension == LayoutParams.FILL_PARENT) {
+ // Child wants to be our size, but our size is not fixed.
+ // Constrain child to not be bigger than us.
+ resultSize = size;
+ resultMode = MeasureSpec.AT_MOST;
+ } else if (childDimension == LayoutParams.WRAP_CONTENT) {
+ // Child wants to determine its own size. It can't be
+ // bigger than us.
+ resultSize = size;
+ resultMode = MeasureSpec.AT_MOST;
+ }
+ break;
+
+ // Parent asked to see how big we want to be
+ case MeasureSpec.UNSPECIFIED:
+ if (childDimension >= 0) {
+ // Child wants a specific size... let him have it
+ resultSize = childDimension;
+ resultMode = MeasureSpec.EXACTLY;
+ } else if (childDimension == LayoutParams.FILL_PARENT) {
+ // Child wants to be our size... find out how big it should
+ // be
+ resultSize = 0;
+ resultMode = MeasureSpec.UNSPECIFIED;
+ } else if (childDimension == LayoutParams.WRAP_CONTENT) {
+ // Child wants to determine its own size.... find out how
+ // big it should be
+ resultSize = 0;
+ resultMode = MeasureSpec.UNSPECIFIED;
+ }
+ break;
+ }
+ return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
+ }
+
+
+ /**
+ * Removes any pending animations for views that have been removed. Call
+ * this if you don't want animations for exiting views to stack up.
+ */
+ public void clearDisappearingChildren() {
+ if (mDisappearingChildren != null) {
+ mDisappearingChildren.clear();
+ }
+ }
+
+ /**
+ * Add a view which is removed from mChildren but still needs animation
+ *
+ * @param v View to add
+ */
+ private void addDisappearingView(View v) {
+ ArrayList<View> disappearingChildren = mDisappearingChildren;
+
+ if (disappearingChildren == null) {
+ disappearingChildren = mDisappearingChildren = new ArrayList<View>();
+ }
+
+ disappearingChildren.add(v);
+ }
+
+ /**
+ * Cleanup a view when its animation is done. This may mean removing it from
+ * the list of disappearing views.
+ *
+ * @param view The view whose animation has finished
+ * @param animation The animation, cannot be null
+ */
+ private void finishAnimatingView(final View view, Animation animation) {
+ final ArrayList<View> disappearingChildren = mDisappearingChildren;
+ if (disappearingChildren != null) {
+ if (disappearingChildren.contains(view)) {
+ disappearingChildren.remove(view);
+
+ if (mAttachInfo != null) {
+ view.dispatchDetachedFromWindow();
+ }
+
+ view.clearAnimation();
+ mGroupFlags |= FLAG_INVALIDATE_REQUIRED;
+ }
+ }
+
+ if (animation != null && !animation.getFillAfter()) {
+ view.clearAnimation();
+ }
+
+ if ((view.mPrivateFlags & ANIMATION_STARTED) == ANIMATION_STARTED) {
+ view.onAnimationEnd();
+ // Should be performed by onAnimationEnd() but this avoid an infinite loop,
+ // so we'd rather be safe than sorry
+ view.mPrivateFlags &= ~ANIMATION_STARTED;
+ // Draw one more frame after the animation is done
+ mGroupFlags |= FLAG_INVALIDATE_REQUIRED;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean gatherTransparentRegion(Region region) {
+ // If no transparent regions requested, we are always opaque.
+ final boolean meOpaque = (mPrivateFlags & View.REQUEST_TRANSPARENT_REGIONS) == 0;
+ if (meOpaque && region == null) {
+ // The caller doesn't care about the region, so stop now.
+ return true;
+ }
+ super.gatherTransparentRegion(region);
+ final View[] children = mChildren;
+ final int count = mChildrenCount;
+ boolean noneOfTheChildrenAreTransparent = true;
+ for (int i = 0; i < count; i++) {
+ final View child = children[i];
+ if ((child.mViewFlags & VISIBILITY_MASK) != GONE || child.getAnimation() != null) {
+ if (!child.gatherTransparentRegion(region)) {
+ noneOfTheChildrenAreTransparent = false;
+ }
+ }
+ }
+ return meOpaque || noneOfTheChildrenAreTransparent;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void requestTransparentRegion(View child) {
+ if (child != null) {
+ child.mPrivateFlags |= View.REQUEST_TRANSPARENT_REGIONS;
+ if (mParent != null) {
+ mParent.requestTransparentRegion(this);
+ }
+ }
+ }
+
+
+ @Override
+ protected boolean fitSystemWindows(Rect insets) {
+ boolean done = super.fitSystemWindows(insets);
+ if (!done) {
+ final int count = mChildrenCount;
+ final View[] children = mChildren;
+ for (int i = 0; i < count; i++) {
+ done = children[i].fitSystemWindows(insets);
+ if (done) {
+ break;
+ }
+ }
+ }
+ return done;
+ }
+
+ /**
+ * Returns the animation listener to which layout animation events are
+ * sent.
+ *
+ * @return an {@link android.view.animation.Animation.AnimationListener}
+ */
+ public Animation.AnimationListener getLayoutAnimationListener() {
+ return mAnimationListener;
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+
+ if ((mGroupFlags & FLAG_NOTIFY_CHILDREN_ON_DRAWABLE_STATE_CHANGE) != 0) {
+ if ((mGroupFlags & FLAG_ADD_STATES_FROM_CHILDREN) != 0) {
+ throw new IllegalStateException("addStateFromChildren cannot be enabled if a"
+ + " child has duplicateParentState set to true");
+ }
+
+ final View[] children = mChildren;
+ final int count = mChildrenCount;
+
+ for (int i = 0; i < count; i++) {
+ final View child = children[i];
+ if ((child.mViewFlags & DUPLICATE_PARENT_STATE) != 0) {
+ child.refreshDrawableState();
+ }
+ }
+ }
+ }
+
+ @Override
+ protected int[] onCreateDrawableState(int extraSpace) {
+ if ((mGroupFlags & FLAG_ADD_STATES_FROM_CHILDREN) == 0) {
+ return super.onCreateDrawableState(extraSpace);
+ }
+
+ int need = 0;
+ int n = getChildCount();
+ for (int i = 0; i < n; i++) {
+ int[] childState = getChildAt(i).getDrawableState();
+
+ if (childState != null) {
+ need += childState.length;
+ }
+ }
+
+ int[] state = super.onCreateDrawableState(extraSpace + need);
+
+ for (int i = 0; i < n; i++) {
+ int[] childState = getChildAt(i).getDrawableState();
+
+ if (childState != null) {
+ state = mergeDrawableStates(state, childState);
+ }
+ }
+
+ return state;
+ }
+
+ /**
+ * Sets whether this ViewGroup's drawable states also include
+ * its children's drawable states. This is used, for example, to
+ * make a group appear to be focused when its child EditText or button
+ * is focused.
+ */
+ public void setAddStatesFromChildren(boolean addsStates) {
+ if (addsStates) {
+ mGroupFlags |= FLAG_ADD_STATES_FROM_CHILDREN;
+ } else {
+ mGroupFlags &= ~FLAG_ADD_STATES_FROM_CHILDREN;
+ }
+
+ refreshDrawableState();
+ }
+
+ /**
+ * Returns whether this ViewGroup's drawable states also include
+ * its children's drawable states. This is used, for example, to
+ * make a group appear to be focused when its child EditText or button
+ * is focused.
+ */
+ public boolean addStatesFromChildren() {
+ return (mGroupFlags & FLAG_ADD_STATES_FROM_CHILDREN) != 0;
+ }
+
+ /**
+ * If {link #addStatesFromChildren} is true, refreshes this group's
+ * drawable state (to include the states from its children).
+ */
+ public void childDrawableStateChanged(View child) {
+ if ((mGroupFlags & FLAG_ADD_STATES_FROM_CHILDREN) != 0) {
+ refreshDrawableState();
+ }
+ }
+
+ /**
+ * Specifies the animation listener to which layout animation events must
+ * be sent. Only
+ * {@link android.view.animation.Animation.AnimationListener#onAnimationStart(Animation)}
+ * and
+ * {@link android.view.animation.Animation.AnimationListener#onAnimationEnd(Animation)}
+ * are invoked.
+ *
+ * @param animationListener the layout animation listener
+ */
+ public void setLayoutAnimationListener(Animation.AnimationListener animationListener) {
+ mAnimationListener = animationListener;
+ }
+
+ /**
+ * LayoutParams are used by views to tell their parents how they want to be
+ * laid out. See
+ * {@link android.R.styleable#ViewGroup_Layout ViewGroup Layout Attributes}
+ * for a list of all child view attributes that this class supports.
+ *
+ * <p>
+ * The base LayoutParams class just describes how big the view wants to be
+ * for both width and height. For each dimension, it can specify one of:
+ * <ul>
+ * <li> an exact number
+ * <li>FILL_PARENT, which means the view wants to be as big as its parent
+ * (minus padding)
+ * <li> WRAP_CONTENT, which means that the view wants to be just big enough
+ * to enclose its content (plus padding)
+ * </ul>
+ * There are subclasses of LayoutParams for different subclasses of
+ * ViewGroup. For example, AbsoluteLayout has its own subclass of
+ * LayoutParams which adds an X and Y value.
+ *
+ * @attr ref android.R.styleable#ViewGroup_Layout_layout_height
+ * @attr ref android.R.styleable#ViewGroup_Layout_layout_width
+ */
+ public static class LayoutParams {
+ /**
+ * Special value for the height or width requested by a View.
+ * FILL_PARENT means that the view wants to fill the available space
+ * within the parent, taking the parent's padding into account.
+ */
+ public static final int FILL_PARENT = -1;
+
+ /**
+ * Special value for the height or width requested by a View.
+ * WRAP_CONTENT means that the view wants to be just large enough to fit
+ * its own internal content, taking its own padding into account.
+ */
+ public static final int WRAP_CONTENT = -2;
+
+ /**
+ * Information about how wide the view wants to be. Can be an exact
+ * size, or one of the constants FILL_PARENT or WRAP_CONTENT.
+ */
+ @ViewDebug.ExportedProperty(mapping = {
+ @ViewDebug.IntToString(from = FILL_PARENT, to = "FILL_PARENT"),
+ @ViewDebug.IntToString(from = WRAP_CONTENT, to = "WRAP_CONTENT")
+ })
+ public int width;
+
+ /**
+ * Information about how tall the view wants to be. Can be an exact
+ * size, or one of the constants FILL_PARENT or WRAP_CONTENT.
+ */
+ @ViewDebug.ExportedProperty(mapping = {
+ @ViewDebug.IntToString(from = FILL_PARENT, to = "FILL_PARENT"),
+ @ViewDebug.IntToString(from = WRAP_CONTENT, to = "WRAP_CONTENT")
+ })
+ public int height;
+
+ /**
+ * Used to animate layouts.
+ */
+ public LayoutAnimationController.AnimationParameters layoutAnimationParameters;
+
+ /**
+ * Creates a new set of layout parameters. The values are extracted from
+ * the supplied attributes set and context. The XML attributes mapped
+ * to this set of layout parameters are:
+ *
+ * <ul>
+ * <li><code>layout_width</code>: the width, either an exact value,
+ * {@link #WRAP_CONTENT} or {@link #FILL_PARENT}</li>
+ * <li><code>layout_height</code>: the height, either an exact value,
+ * {@link #WRAP_CONTENT} or {@link #FILL_PARENT}</li>
+ * </ul>
+ *
+ * @param c the application environment
+ * @param attrs the set of attributes from which to extract the layout
+ * parameters' values
+ */
+ public LayoutParams(Context c, AttributeSet attrs) {
+ TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
+ setBaseAttributes(a,
+ R.styleable.ViewGroup_Layout_layout_width,
+ R.styleable.ViewGroup_Layout_layout_height);
+ a.recycle();
+ }
+
+ /**
+ * Creates a new set of layout parameters with the specified width
+ * and height.
+ *
+ * @param width the width, either {@link #FILL_PARENT},
+ * {@link #WRAP_CONTENT} or a fixed size in pixels
+ * @param height the height, either {@link #FILL_PARENT},
+ * {@link #WRAP_CONTENT} or a fixed size in pixels
+ */
+ public LayoutParams(int width, int height) {
+ this.width = width;
+ this.height = height;
+ }
+
+ /**
+ * Copy constructor. Clones the width and height values of the source.
+ *
+ * @param source The layout params to copy from.
+ */
+ public LayoutParams(LayoutParams source) {
+ this.width = source.width;
+ this.height = source.height;
+ }
+
+ /**
+ * Used internally by MarginLayoutParams.
+ * @hide
+ */
+ LayoutParams() {
+ }
+
+ /**
+ * Extracts the layout parameters from the supplied attributes.
+ *
+ * @param a the style attributes to extract the parameters from
+ * @param widthAttr the identifier of the width attribute
+ * @param heightAttr the identifier of the height attribute
+ */
+ protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
+ width = a.getLayoutDimension(widthAttr, "layout_width");
+ height = a.getLayoutDimension(heightAttr, "layout_height");
+ }
+
+ /**
+ * Returns a String representation of this set of layout parameters.
+ *
+ * @param output the String to prepend to the internal representation
+ * @return a String with the following format: output +
+ * "ViewGroup.LayoutParams={ width=WIDTH, height=HEIGHT }"
+ *
+ * @hide
+ */
+ public String debug(String output) {
+ return output + "ViewGroup.LayoutParams={ width="
+ + sizeToString(width) + ", height=" + sizeToString(height) + " }";
+ }
+
+ /**
+ * Converts the specified size to a readable String.
+ *
+ * @param size the size to convert
+ * @return a String instance representing the supplied size
+ *
+ * @hide
+ */
+ protected static String sizeToString(int size) {
+ if (size == WRAP_CONTENT) {
+ return "wrap-content";
+ }
+ if (size == FILL_PARENT) {
+ return "fill-parent";
+ }
+ return String.valueOf(size);
+ }
+ }
+
+ /**
+ * Per-child layout information for layouts that support margins.
+ * See
+ * {@link android.R.styleable#ViewGroup_MarginLayout ViewGroup Margin Layout Attributes}
+ * for a list of all child view attributes that this class supports.
+ */
+ public static class MarginLayoutParams extends ViewGroup.LayoutParams {
+ /**
+ * The left margin in pixels of the child.
+ */
+ @ViewDebug.ExportedProperty
+ public int leftMargin;
+
+ /**
+ * The top margin in pixels of the child.
+ */
+ @ViewDebug.ExportedProperty
+ public int topMargin;
+
+ /**
+ * The right margin in pixels of the child.
+ */
+ @ViewDebug.ExportedProperty
+ public int rightMargin;
+
+ /**
+ * The bottom margin in pixels of the child.
+ */
+ @ViewDebug.ExportedProperty
+ public int bottomMargin;
+
+ /**
+ * Creates a new set of layout parameters. The values are extracted from
+ * the supplied attributes set and context.
+ *
+ * @param c the application environment
+ * @param attrs the set of attributes from which to extract the layout
+ * parameters' values
+ */
+ public MarginLayoutParams(Context c, AttributeSet attrs) {
+ super();
+
+ TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_MarginLayout);
+ setBaseAttributes(a,
+ R.styleable.ViewGroup_MarginLayout_layout_width,
+ R.styleable.ViewGroup_MarginLayout_layout_height);
+
+ int margin = a.getDimensionPixelSize(
+ com.android.internal.R.styleable.ViewGroup_MarginLayout_layout_margin, -1);
+ if (margin >= 0) {
+ leftMargin = margin;
+ topMargin = margin;
+ rightMargin= margin;
+ bottomMargin = margin;
+ } else {
+ leftMargin = a.getDimensionPixelSize(
+ R.styleable.ViewGroup_MarginLayout_layout_marginLeft, 0);
+ topMargin = a.getDimensionPixelSize(
+ R.styleable.ViewGroup_MarginLayout_layout_marginTop, 0);
+ rightMargin = a.getDimensionPixelSize(
+ R.styleable.ViewGroup_MarginLayout_layout_marginRight, 0);
+ bottomMargin = a.getDimensionPixelSize(
+ R.styleable.ViewGroup_MarginLayout_layout_marginBottom, 0);
+ }
+
+ a.recycle();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public MarginLayoutParams(int width, int height) {
+ super(width, height);
+ }
+
+ /**
+ * Copy constructor. Clones the width, height and margin values of the source.
+ *
+ * @param source The layout params to copy from.
+ */
+ public MarginLayoutParams(MarginLayoutParams source) {
+ this.width = source.width;
+ this.height = source.height;
+
+ this.leftMargin = source.leftMargin;
+ this.topMargin = source.topMargin;
+ this.rightMargin = source.rightMargin;
+ this.bottomMargin = source.bottomMargin;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public MarginLayoutParams(LayoutParams source) {
+ super(source);
+ }
+
+ /**
+ * Sets the margins, in pixels.
+ *
+ * @param left the left margin size
+ * @param top the top margin size
+ * @param right the right margin size
+ * @param bottom the bottom margin size
+ *
+ * @attr ref android.R.styleable#ViewGroup_MarginLayout_layout_marginLeft
+ * @attr ref android.R.styleable#ViewGroup_MarginLayout_layout_marginTop
+ * @attr ref android.R.styleable#ViewGroup_MarginLayout_layout_marginRight
+ * @attr ref android.R.styleable#ViewGroup_MarginLayout_layout_marginBottom
+ */
+ public void setMargins(int left, int top, int right, int bottom) {
+ leftMargin = left;
+ topMargin = top;
+ rightMargin = right;
+ bottomMargin = bottom;
+ }
+ }
+}
diff --git a/core/java/android/view/ViewManager.java b/core/java/android/view/ViewManager.java
new file mode 100644
index 0000000..7f318c1
--- /dev/null
+++ b/core/java/android/view/ViewManager.java
@@ -0,0 +1,27 @@
+/*
+ * 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.view;
+
+/** Interface to let you add and remove child views to an Activity. To get an instance
+ * of this class, call {@link android.content.Context#getSystemService(java.lang.String) Context.getSystemService()}.
+ */
+public interface ViewManager
+{
+ public void addView(View view, ViewGroup.LayoutParams params);
+ public void updateViewLayout(View view, ViewGroup.LayoutParams params);
+ public void removeView(View view);
+}
diff --git a/core/java/android/view/ViewParent.java b/core/java/android/view/ViewParent.java
new file mode 100644
index 0000000..1a5d495
--- /dev/null
+++ b/core/java/android/view/ViewParent.java
@@ -0,0 +1,185 @@
+/*
+ * 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.view;
+
+import android.graphics.Rect;
+
+/**
+ * Defines the responsibilities for a class that will be a parent of a View.
+ * This is the API that a view sees when it wants to interact with its parent.
+ *
+ */
+public interface ViewParent {
+ /**
+ * Called when something has changed which has invalidated the layout of a
+ * child of this view parent. This will schedule a layout pass of the view
+ * tree.
+ */
+ public void requestLayout();
+
+ /**
+ * Indicates whether layout was requested on this view parent.
+ *
+ * @return true if layout was requested, false otherwise
+ */
+ public boolean isLayoutRequested();
+
+ /**
+ * Called when a child wants the view hierarchy to gather and report
+ * transparent regions to the window compositor. Views that "punch" holes in
+ * the view hierarchy, such as SurfaceView can use this API to improve
+ * performance of the system. When no such a view is present in the
+ * hierarchy, this optimization in unnecessary and might slightly reduce the
+ * view hierarchy performance.
+ *
+ * @param child the view requesting the transparent region computation
+ *
+ */
+ public void requestTransparentRegion(View child);
+
+ /**
+ * All or part of a child is dirty and needs to be redrawn.
+ *
+ * @param child The child which is dirty
+ * @param r The area within the child that is invalid
+ */
+ public void invalidateChild(View child, Rect r);
+
+ /**
+ * All or part of a child is dirty and needs to be redrawn.
+ *
+ * The location array is an array of two int values which respectively
+ * define the left and the top position of the dirty child.
+ *
+ * This method must return the parent of this ViewParent if the specified
+ * rectangle must be invalidated in the parent. If the specified rectangle
+ * does not require invalidation in the parent or if the parent does not
+ * exist, this method must return null.
+ *
+ * When this method returns a non-null value, the location array must
+ * have been updated with the left and top coordinates of this ViewParent.
+ *
+ * @param location An array of 2 ints containing the left and top
+ * coordinates of the child to invalidate
+ * @param r The area within the child that is invalid
+ *
+ * @return the parent of this ViewParent or null
+ */
+ public ViewParent invalidateChildInParent(int[] location, Rect r);
+
+ /**
+ * Returns the parent if it exists, or null.
+ *
+ * @return a ViewParent or null if this ViewParent does not have a parent
+ */
+ public ViewParent getParent();
+
+ /**
+ * Called when a child of this parent wants focus
+ *
+ * @param child The child of this ViewParent that wants focus. This view
+ * will contain the focused view. It is not necessarily the view that
+ * actually has focus.
+ * @param focused The view that is a descendant of child that actually has
+ * focus
+ */
+ public void requestChildFocus(View child, View focused);
+
+ /**
+ * Tell view hierarchy that the global view attributes need to be
+ * re-evaluated.
+ *
+ * @param child View whose attributes have changed.
+ */
+ public void recomputeViewAttributes(View child);
+
+ /**
+ * Called when a child of this parent is giving up focus
+ *
+ * @param child The view that is giving up focus
+ */
+ public void clearChildFocus(View child);
+
+ public boolean getChildVisibleRect(View child, Rect r, android.graphics.Point offset);
+
+ /**
+ * Find the nearest view in the specified direction that wants to take focus
+ *
+ * @param v The view that currently has focus
+ * @param direction One of FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, and FOCUS_RIGHT
+ */
+ public View focusSearch(View v, int direction);
+
+ /**
+ * Change the z order of the child so it's on top of all other children
+ *
+ * @param child
+ */
+ public void bringChildToFront(View child);
+
+ /**
+ * Tells the parent that a new focusable view has become available. This is
+ * to handle transitions from the case where there are no focusable views to
+ * the case where the first focusable view appears.
+ *
+ * @param v The view that has become newly focusable
+ */
+ public void focusableViewAvailable(View v);
+
+ /**
+ * Bring up a context menu for the specified view or its ancestors.
+ * <p>
+ * In most cases, a subclass does not need to override this. However, if
+ * the subclass is added directly to the window manager (for example,
+ * {@link ViewManager#addView(View, android.view.ViewGroup.LayoutParams)})
+ * then it should override this and show the context menu.
+ *
+ * @param originalView The source view where the context menu was first invoked
+ * @return true if a context menu was displayed
+ */
+ public boolean showContextMenuForChild(View originalView);
+
+ /**
+ * Have the parent populate the specified context menu if it has anything to
+ * add (and then recurse on its parent).
+ *
+ * @param menu The menu to populate
+ */
+ public void createContextMenu(ContextMenu menu);
+
+ /**
+ * This method is called on the parent when a child's drawable state
+ * has changed.
+ *
+ * @param child The child whose drawable state has changed.
+ */
+ public void childDrawableStateChanged(View child);
+
+ /**
+ * Called when a child does not want this parent and its ancestors to
+ * intercept touch events with
+ * {@link ViewGroup#onInterceptTouchEvent(MotionEvent)}.
+ * <p>
+ * This parent should pass this call onto its parents. This parent must obey
+ * this request for the duration of the touch (that is, only clear the flag
+ * after this parent has received an up or a cancel.
+ *
+ * @param disallowIntercept True if the child does not want the parent to
+ * intercept touch events.
+ */
+ public void requestDisallowInterceptTouchEvent(boolean disallowIntercept);
+}
diff --git a/core/java/android/view/ViewRoot.java b/core/java/android/view/ViewRoot.java
new file mode 100644
index 0000000..ca67404
--- /dev/null
+++ b/core/java/android/view/ViewRoot.java
@@ -0,0 +1,2212 @@
+/*
+ * 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.view;
+
+import android.graphics.Canvas;
+import android.graphics.PixelFormat;
+import android.graphics.PorterDuff;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.os.*;
+import android.os.Process;
+import android.util.AndroidRuntimeException;
+import android.util.Config;
+import android.util.Log;
+import android.util.EventLog;
+import android.view.View.MeasureSpec;
+import android.content.pm.PackageManager;
+import android.content.Context;
+import android.app.ActivityManagerNative;
+import android.Manifest;
+import android.media.AudioManager;
+
+import java.lang.ref.WeakReference;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+
+import javax.microedition.khronos.egl.*;
+import javax.microedition.khronos.opengles.*;
+import static javax.microedition.khronos.opengles.GL10.*;
+
+/**
+ * The top of a view hierarchy, implementing the needed protocol between View
+ * and the WindowManager. This is for the most part an internal implementation
+ * detail of {@link WindowManagerImpl}.
+ *
+ * {@hide}
+ */
+@SuppressWarnings({"EmptyCatchBlock"})
+final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.SoundEffectPlayer {
+ private static final String TAG = "ViewRoot";
+ private static final boolean DBG = false;
+ @SuppressWarnings({"ConstantConditionalExpression"})
+ private static final boolean LOCAL_LOGV = false ? Config.LOGD : Config.LOGV;
+ /** @noinspection PointlessBooleanExpression*/
+ private static final boolean DEBUG_DRAW = false || LOCAL_LOGV;
+ private static final boolean DEBUG_ORIENTATION = false || LOCAL_LOGV;
+ private static final boolean DEBUG_TRACKBALL = LOCAL_LOGV;
+ private static final boolean WATCH_POINTER = false;
+
+ static final boolean PROFILE_DRAWING = false;
+ private static final boolean PROFILE_LAYOUT = false;
+ // profiles real fps (times between draws) and displays the result
+ private static final boolean SHOW_FPS = false;
+ // used by SHOW_FPS
+ private static int sDrawTime;
+
+ /**
+ * Maximum time we allow the user to roll the trackball enough to generate
+ * a key event, before resetting the counters.
+ */
+ static final int MAX_TRACKBALL_DELAY = 250;
+
+ private static long sInstanceCount = 0;
+
+ private static IWindowSession sWindowSession;
+
+ private static final Object mStaticInit = new Object();
+ private static boolean mInitialized = false;
+
+ static final ThreadLocal<Handler> sUiThreads = new ThreadLocal<Handler>();
+ static final RunQueue sRunQueue = new RunQueue();
+
+ private long mLastTrackballTime = 0;
+ private final TrackballAxis mTrackballAxisX = new TrackballAxis();
+ private final TrackballAxis mTrackballAxisY = new TrackballAxis();
+
+ private final Thread mThread;
+
+ private final WindowLeaked mLocation;
+
+ private final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams();
+
+ final W mWindow;
+
+ private View mView;
+ private View mFocusedView;
+ private int mViewVisibility;
+ private boolean mAppVisible = true;
+
+ private final Region mTransparentRegion;
+ private final Region mPreviousTransparentRegion;
+
+ private int mWidth;
+ private int mHeight;
+ private Rect mDirty; // will be a graphics.Region soon
+
+ private final View.AttachInfo mAttachInfo;
+
+ private final Rect mTempRect; // used in the transaction to not thrash the heap.
+
+ private boolean mTraversalScheduled;
+ private boolean mWillDrawSoon;
+ private boolean mLayoutRequested;
+ private boolean mFirst;
+ private boolean mReportNextDraw;
+ private boolean mFullRedrawNeeded;
+ private boolean mNewSurfaceNeeded;
+
+ private boolean mWindowAttributesChanged = false;
+
+ // These can be accessed by any thread, must be protected with a lock.
+ private Surface mSurface;
+
+ private boolean mAdded;
+ private boolean mAddedTouchMode;
+
+ /*package*/ int mAddNesting;
+
+ // These are accessed by multiple threads.
+ private final Rect mWinFrame; // frame given by window manager.
+
+ private final Rect mCoveredInsets = new Rect();
+ private final Rect mNewCoveredInsets = new Rect();
+
+ private EGL10 mEgl;
+ private EGLDisplay mEglDisplay;
+ private EGLContext mEglContext;
+ private EGLSurface mEglSurface;
+ private GL11 mGL;
+ private Canvas mGlCanvas;
+ private boolean mUseGL;
+ private boolean mGlWanted;
+
+ /**
+ * see {@link #playSoundEffect(int)}
+ */
+ private AudioManager mAudioManager;
+
+
+
+ public ViewRoot() {
+ super();
+
+ ++sInstanceCount;
+
+ // Initialize the statics when this class is first instantiated. This is
+ // done here instead of in the static block because Zygote does not
+ // allow the spawning of threads.
+ synchronized (mStaticInit) {
+ if (!mInitialized) {
+ try {
+ sWindowSession = IWindowManager.Stub.asInterface(
+ ServiceManager.getService("window"))
+ .openSession(new Binder());
+ mInitialized = true;
+ } catch (RemoteException e) {
+ }
+ }
+ }
+
+ mThread = Thread.currentThread();
+ mLocation = new WindowLeaked(null);
+ mLocation.fillInStackTrace();
+ mWidth = -1;
+ mHeight = -1;
+ mDirty = new Rect();
+ mTempRect = new Rect();
+ mWinFrame = new Rect();
+ mWindow = new W(this);
+ mViewVisibility = View.GONE;
+ mTransparentRegion = new Region();
+ mPreviousTransparentRegion = new Region();
+ mFirst = true; // true for the first time the view is added
+ mSurface = new Surface();
+ mAdded = false;
+
+ Handler handler = sUiThreads.get();
+ if (handler == null) {
+ handler = new RootHandler();
+ sUiThreads.set(handler);
+ }
+ mAttachInfo = new View.AttachInfo(handler, this);
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ super.finalize();
+ --sInstanceCount;
+ }
+
+ public static long getInstanceCount() {
+ return sInstanceCount;
+ }
+
+ // FIXME for perf testing only
+ private boolean mProfile = false;
+
+ /**
+ * Call this to profile the next traversal call.
+ * FIXME for perf testing only. Remove eventually
+ */
+ public void profile() {
+ mProfile = true;
+ }
+
+ /**
+ * Indicates whether we are in touch mode. Calling this method triggers an IPC
+ * call and should be avoided whenever possible.
+ *
+ * @return True, if the device is in touch mode, false otherwise.
+ *
+ * @hide
+ */
+ static boolean isInTouchMode() {
+ if (mInitialized) {
+ try {
+ return sWindowSession.getInTouchMode();
+ } catch (RemoteException e) {
+ }
+ }
+ return false;
+ }
+
+ private void initializeGL() {
+ initializeGLInner();
+ int err = mEgl.eglGetError();
+ if (err != EGL10.EGL_SUCCESS) {
+ // give-up on using GL
+ destroyGL();
+ mGlWanted = false;
+ }
+ }
+
+ private void initializeGLInner() {
+ final EGL10 egl = (EGL10) EGLContext.getEGL();
+ mEgl = egl;
+
+ /*
+ * Get to the default display.
+ */
+ final EGLDisplay eglDisplay = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
+ mEglDisplay = eglDisplay;
+
+ /*
+ * We can now initialize EGL for that display
+ */
+ int[] version = new int[2];
+ egl.eglInitialize(eglDisplay, version);
+
+ /*
+ * Specify a configuration for our opengl session
+ * and grab the first configuration that matches is
+ */
+ final int[] configSpec = {
+ EGL10.EGL_RED_SIZE, 5,
+ EGL10.EGL_GREEN_SIZE, 6,
+ EGL10.EGL_BLUE_SIZE, 5,
+ EGL10.EGL_DEPTH_SIZE, 0,
+ EGL10.EGL_NONE
+ };
+ final EGLConfig[] configs = new EGLConfig[1];
+ final int[] num_config = new int[1];
+ egl.eglChooseConfig(eglDisplay, configSpec, configs, 1, num_config);
+ final EGLConfig config = configs[0];
+
+ /*
+ * Create an OpenGL ES context. This must be done only once, an
+ * OpenGL context is a somewhat heavy object.
+ */
+ final EGLContext context = egl.eglCreateContext(eglDisplay, config,
+ EGL10.EGL_NO_CONTEXT, null);
+ mEglContext = context;
+
+ /*
+ * Create an EGL surface we can render into.
+ */
+ final EGLSurface surface = egl.eglCreateWindowSurface(eglDisplay, config, mHolder, null);
+ mEglSurface = surface;
+
+ /*
+ * Before we can issue GL commands, we need to make sure
+ * the context is current and bound to a surface.
+ */
+ egl.eglMakeCurrent(eglDisplay, surface, surface, context);
+
+ /*
+ * Get to the appropriate GL interface.
+ * This is simply done by casting the GL context to either
+ * GL10 or GL11.
+ */
+ final GL11 gl = (GL11) context.getGL();
+ mGL = gl;
+ mGlCanvas = new Canvas(gl);
+ mUseGL = true;
+ }
+
+ private void destroyGL() {
+ // inform skia that the context is gone
+ nativeAbandonGlCaches();
+
+ mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE,
+ EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT);
+ mEgl.eglDestroyContext(mEglDisplay, mEglContext);
+ mEgl.eglDestroySurface(mEglDisplay, mEglSurface);
+ mEgl.eglTerminate(mEglDisplay);
+ mEglContext = null;
+ mEglSurface = null;
+ mEglDisplay = null;
+ mEgl = null;
+ mGlCanvas = null;
+ mGL = null;
+ mUseGL = false;
+ }
+
+ private void checkEglErrors() {
+ if (mUseGL) {
+ int err = mEgl.eglGetError();
+ if (err != EGL10.EGL_SUCCESS) {
+ // something bad has happened revert to
+ // normal rendering.
+ destroyGL();
+ if (err != EGL11.EGL_CONTEXT_LOST) {
+ // we'll try again if it was context lost
+ mGlWanted = false;
+ }
+ }
+ }
+ }
+
+ /**
+ * We have one child
+ */
+ public void setView(View view, WindowManager.LayoutParams attrs,
+ View panelParentView) {
+ synchronized (this) {
+ if (mView == null) {
+ mWindowAttributes.copyFrom(attrs);
+ mWindowAttributesChanged = true;
+ mView = view;
+ if (panelParentView != null) {
+ mAttachInfo.mPanelParentWindowToken
+ = panelParentView.getApplicationWindowToken();
+ }
+ mAdded = true;
+ int res; /* = WindowManagerImpl.ADD_OKAY; */
+
+ // Schedule the first layout -before- adding to the window
+ // manager, to make sure we do the relayout before receiving
+ // any other events from the system.
+ requestLayout();
+
+ try {
+ res = sWindowSession.add(mWindow, attrs,
+ getHostVisibility(), mCoveredInsets);
+ } catch (RemoteException e) {
+ mAdded = false;
+ mView = null;
+ unscheduleTraversals();
+ throw new RuntimeException("Adding window failed", e);
+ }
+ if (Config.LOGV) Log.v("ViewRoot", "Added window " + mWindow);
+ if (res < WindowManagerImpl.ADD_OKAY) {
+ mView = null;
+ mAdded = false;
+ unscheduleTraversals();
+ switch (res) {
+ case WindowManagerImpl.ADD_BAD_APP_TOKEN:
+ case WindowManagerImpl.ADD_BAD_SUBWINDOW_TOKEN:
+ throw new WindowManagerImpl.BadTokenException(
+ "Unable to add window -- token " + attrs.token
+ + " is not valid; is your activity running?");
+ case WindowManagerImpl.ADD_NOT_APP_TOKEN:
+ throw new WindowManagerImpl.BadTokenException(
+ "Unable to add window -- token " + attrs.token
+ + " is not for an application");
+ case WindowManagerImpl.ADD_APP_EXITING:
+ throw new WindowManagerImpl.BadTokenException(
+ "Unable to add window -- app for token " + attrs.token
+ + " is exiting");
+ case WindowManagerImpl.ADD_DUPLICATE_ADD:
+ throw new WindowManagerImpl.BadTokenException(
+ "Unable to add window -- window " + mWindow
+ + " has already been added");
+ case WindowManagerImpl.ADD_STARTING_NOT_NEEDED:
+ // Silently ignore -- we would have just removed it
+ // right away, anyway.
+ return;
+ case WindowManagerImpl.ADD_MULTIPLE_SINGLETON:
+ throw new WindowManagerImpl.BadTokenException(
+ "Unable to add window " + mWindow +
+ " -- another window of this type already exists");
+ case WindowManagerImpl.ADD_PERMISSION_DENIED:
+ throw new WindowManagerImpl.BadTokenException(
+ "Unable to add window " + mWindow +
+ " -- permission denied for this window type");
+ }
+ throw new RuntimeException(
+ "Unable to add window -- unknown error code " + res);
+ }
+ view.assignParent(this);
+ mAddedTouchMode = (res&WindowManagerImpl.ADD_FLAG_IN_TOUCH_MODE) != 0;
+ mAppVisible = (res&WindowManagerImpl.ADD_FLAG_APP_VISIBLE) != 0;
+ }
+ }
+ }
+
+ public View getView() {
+ return mView;
+ }
+
+ final WindowLeaked getLocation() {
+ return mLocation;
+ }
+
+ public void setLayoutParams(WindowManager.LayoutParams attrs) {
+ synchronized (this) {
+ mWindowAttributes.copyFrom(attrs);
+ mWindowAttributesChanged = true;
+ scheduleTraversals();
+ }
+ }
+
+ void handleAppVisibility(boolean visible) {
+ if (mAppVisible != visible) {
+ mAppVisible = visible;
+ scheduleTraversals();
+ }
+ }
+
+ void handleGetNewSurface() {
+ mNewSurfaceNeeded = true;
+ mFullRedrawNeeded = true;
+ scheduleTraversals();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void requestLayout() {
+ checkThread();
+ mLayoutRequested = true;
+ scheduleTraversals();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean isLayoutRequested() {
+ return mLayoutRequested;
+ }
+
+ public void invalidateChild(View child, Rect dirty) {
+ checkThread();
+ if (LOCAL_LOGV) Log.v(TAG, "Invalidate child: " + dirty);
+ mDirty.union(dirty);
+ if (!mWillDrawSoon) {
+ scheduleTraversals();
+ }
+ }
+
+ public ViewParent getParent() {
+ return null;
+ }
+
+ public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
+ invalidateChild(null, dirty);
+ return null;
+ }
+
+ public boolean getChildVisibleRect(View child, Rect r, android.graphics.Point offset) {
+ if (child != mView) {
+ throw new RuntimeException("child is not mine, honest!");
+ }
+ return r.intersect(0, 0, mWidth, mHeight);
+ }
+
+ public void bringChildToFront(View child) {
+ }
+
+ public void scheduleTraversals() {
+ if (!mTraversalScheduled) {
+ mTraversalScheduled = true;
+ sendEmptyMessage(DO_TRAVERSAL);
+ }
+ }
+
+ public void unscheduleTraversals() {
+ if (mTraversalScheduled) {
+ mTraversalScheduled = false;
+ removeMessages(DO_TRAVERSAL);
+ }
+ }
+
+ int getHostVisibility() {
+ return mAppVisible ? mView.getVisibility() : View.GONE;
+ }
+
+ private void performTraversals() {
+ // cache mView since it is used so much below...
+ final View host = mView;
+
+ if (DBG) {
+ System.out.println("======================================");
+ System.out.println("performTraversals");
+ host.debug();
+ }
+
+ if (host == null || !mAdded)
+ return;
+
+ mTraversalScheduled = false;
+ mWillDrawSoon = true;
+ boolean windowResizesToFitContent = false;
+ boolean fullRedrawNeeded = mFullRedrawNeeded;
+ boolean newSurface = false;
+ WindowManager.LayoutParams lp = (WindowManager.LayoutParams) host.getLayoutParams();
+
+ int desiredWindowWidth;
+ int desiredWindowHeight;
+ int childWidthMeasureSpec;
+ int childHeightMeasureSpec;
+
+ final View.AttachInfo attachInfo = mAttachInfo;
+
+ final int viewVisibility = getHostVisibility();
+ boolean viewVisibilityChanged = mViewVisibility != viewVisibility
+ || mNewSurfaceNeeded;
+
+ WindowManager.LayoutParams params = null;
+ if (mWindowAttributesChanged) {
+ mWindowAttributesChanged = false;
+ params = mWindowAttributes;
+ }
+
+ if (mFirst) {
+ fullRedrawNeeded = true;
+ mLayoutRequested = true;
+
+ Display d = new Display(0);
+ desiredWindowWidth = d.getWidth();
+ desiredWindowHeight = d.getHeight();
+
+ // For the very first time, tell the view hierarchy that it
+ // is attached to the window. Note that at this point the surface
+ // object is not initialized to its backing store, but soon it
+ // will be (assuming the window is visible).
+ attachInfo.mWindowToken = mWindow.asBinder();
+ attachInfo.mSurface = mSurface;
+ attachInfo.mSession = sWindowSession;
+ attachInfo.mHasWindowFocus = false;
+ attachInfo.mWindowVisibility = viewVisibility;
+ attachInfo.mRecomputeGlobalAttributes = false;
+ attachInfo.mKeepScreenOn = false;
+ viewVisibilityChanged = false;
+ host.dispatchAttachedToWindow(attachInfo, 0);
+ sRunQueue.executeActions(attachInfo.mHandler);
+ //Log.i(TAG, "Screen on initialized: " + attachInfo.mKeepScreenOn);
+ } else {
+ desiredWindowWidth = mWinFrame.width();
+ desiredWindowHeight = mWinFrame.height();
+ if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) {
+ if (DEBUG_ORIENTATION) Log.v("ViewRoot",
+ "View " + host + " resized to: " + mWinFrame);
+ fullRedrawNeeded = true;
+ mLayoutRequested = true;
+ windowResizesToFitContent = true;
+ }
+ }
+
+ if (viewVisibilityChanged) {
+ attachInfo.mWindowVisibility = viewVisibility;
+ host.dispatchWindowVisibilityChanged(viewVisibility);
+ if (viewVisibility != View.VISIBLE || mNewSurfaceNeeded) {
+ if (mUseGL) {
+ destroyGL();
+ }
+ }
+ }
+
+ if (mLayoutRequested) {
+ if (mFirst) {
+ host.fitSystemWindows(mCoveredInsets);
+ // make sure touch mode code executes by setting cached value
+ // to opposite of the added touch mode.
+ mAttachInfo.mInTouchMode = !mAddedTouchMode;
+ ensureTouchModeLocally(mAddedTouchMode);
+ } else {
+ if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT
+ || lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
+ windowResizesToFitContent = true;
+
+ Display d = new Display(0);
+ desiredWindowWidth = d.getWidth();
+ desiredWindowHeight = d.getHeight();
+ }
+ }
+
+ childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
+ childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
+
+ // Ask host how big it wants to be
+ if (DEBUG_ORIENTATION) Log.v("ViewRoot",
+ "Measuring " + host + " in display " + desiredWindowWidth
+ + "x" + desiredWindowHeight + "...");
+ host.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+
+ if (DBG) {
+ System.out.println("======================================");
+ System.out.println("performTraversals -- after measure");
+ host.debug();
+ }
+ }
+
+ if (attachInfo.mRecomputeGlobalAttributes) {
+ //Log.i(TAG, "Computing screen on!");
+ attachInfo.mRecomputeGlobalAttributes = false;
+ boolean oldVal = attachInfo.mKeepScreenOn;
+ attachInfo.mKeepScreenOn = false;
+ host.dispatchCollectViewAttributes(0);
+ if (attachInfo.mKeepScreenOn != oldVal) {
+ params = mWindowAttributes;
+ //Log.i(TAG, "Keep screen on changed: " + attachInfo.mKeepScreenOn);
+ }
+ }
+
+ if (params != null && (host.mPrivateFlags & View.REQUEST_TRANSPARENT_REGIONS) != 0) {
+ if (!PixelFormat.formatHasAlpha(params.format)) {
+ params.format = PixelFormat.TRANSLUCENT;
+ }
+ }
+
+ boolean windowShouldResize = mLayoutRequested && windowResizesToFitContent
+ && (mWidth != host.mMeasuredWidth || mHeight != host.mMeasuredHeight);
+
+ int relayoutResult = 0;
+ if (mFirst || windowShouldResize || viewVisibilityChanged || params != null) {
+
+ if (viewVisibility == View.VISIBLE) {
+ if (mWindowAttributes.memoryType == WindowManager.LayoutParams.MEMORY_TYPE_GPU) {
+ if (params == null) {
+ params = mWindowAttributes;
+ }
+ mGlWanted = true;
+ }
+ }
+
+ final Rect frame = mWinFrame;
+ boolean initialized = false;
+ boolean coveredInsetsChanged = false;
+ try {
+ boolean hadSurface = mSurface.isValid();
+ int fl = 0;
+ if (params != null) {
+ fl = params.flags;
+ if (attachInfo.mKeepScreenOn) {
+ params.flags |= WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
+ }
+ }
+ relayoutResult = sWindowSession.relayout(
+ mWindow, params, host.mMeasuredWidth, host.mMeasuredHeight,
+ viewVisibility, frame, mNewCoveredInsets, mSurface);
+ if (params != null) {
+ params.flags = fl;
+ }
+
+ coveredInsetsChanged = !mNewCoveredInsets.equals(mCoveredInsets);
+ if (coveredInsetsChanged) {
+ mCoveredInsets.set(mNewCoveredInsets);
+ host.fitSystemWindows(mCoveredInsets);
+ }
+
+ if (!hadSurface && mSurface.isValid()) {
+ // If we are creating a new surface, then we need to
+ // completely redraw it. Also, when we get to the
+ // point of drawing it we will hold off and schedule
+ // a new traversal instead. This is so we can tell the
+ // window manager about all of the windows being displayed
+ // before actually drawing them, so it can display then
+ // all at once.
+ newSurface = true;
+ fullRedrawNeeded = true;
+
+ if (mGlWanted && !mUseGL) {
+ initializeGL();
+ initialized = mGlCanvas != null;
+ }
+ }
+ } catch (RemoteException e) {
+ }
+ if (DEBUG_ORIENTATION) Log.v(
+ "ViewRoot", "Relayout returned: frame=" + mWinFrame + ", surface=" + mSurface);
+
+ attachInfo.mWindowLeft = frame.left;
+ attachInfo.mWindowTop = frame.top;
+
+ // !!FIXME!! This next section handles the case where we did not get the
+ // window size we asked for. We should avoid this by getting a maximum size from
+ // the window session beforehand.
+ mWidth = frame.width();
+ mHeight = frame.height();
+
+ if (initialized) {
+ mGlCanvas.setViewport(mWidth, mHeight);
+ }
+
+ boolean focusChangedDueToTouchMode = ensureTouchModeLocally(
+ (relayoutResult&WindowManagerImpl.RELAYOUT_IN_TOUCH_MODE) != 0);
+ if (focusChangedDueToTouchMode || mWidth != host.mMeasuredWidth
+ || mHeight != host.mMeasuredHeight || coveredInsetsChanged) {
+ childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
+ childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
+
+ // Ask host how big it wants to be
+ host.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+
+ // Implementation of weights from WindowManager.LayoutParams
+ // We just grow the dimensions as needed and re-measure if
+ // needs be
+ int width = host.mMeasuredWidth;
+ int height = host.mMeasuredHeight;
+ boolean measureAgain = false;
+
+ if (lp.horizontalWeight > 0.0f) {
+ width += (int) ((mWidth - width) * lp.horizontalWeight);
+ childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width,
+ MeasureSpec.EXACTLY);
+ measureAgain = true;
+ }
+ if (lp.verticalWeight > 0.0f) {
+ height += (int) ((mHeight - height) * lp.verticalWeight);
+ childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,
+ MeasureSpec.EXACTLY);
+ measureAgain = true;
+ }
+
+ if (measureAgain) {
+ host.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+ }
+
+ mLayoutRequested = true;
+ }
+ }
+
+ boolean triggerGlobalLayoutListener = mLayoutRequested
+ || attachInfo.mRecomputeGlobalAttributes;
+ if (mLayoutRequested) {
+ mLayoutRequested = false;
+ if (DEBUG_ORIENTATION) Log.v(
+ "ViewRoot", "Setting frame " + host + " to (" +
+ host.mMeasuredWidth + ", " + host.mMeasuredHeight + ")");
+ long startTime;
+ if (PROFILE_LAYOUT) {
+ startTime = SystemClock.elapsedRealtime();
+ }
+
+ host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight);
+
+ if (PROFILE_LAYOUT) {
+ EventLog.writeEvent(60001, SystemClock.elapsedRealtime() - startTime);
+ }
+
+ // By this point all views have been sized and positionned
+ // We can compute the transparent area
+
+ if ((host.mPrivateFlags & View.REQUEST_TRANSPARENT_REGIONS) != 0) {
+ // start out transparent
+ // TODO: AVOID THAT CALL BY CACHING THE RESULT?
+ host.getLocationInWindow(host.mLocation);
+ mTransparentRegion.set(host.mLocation[0], host.mLocation[1],
+ host.mLocation[0] + host.mRight - host.mLeft,
+ host.mLocation[1] + host.mBottom - host.mTop);
+
+ host.gatherTransparentRegion(mTransparentRegion);
+ if (!mTransparentRegion.equals(mPreviousTransparentRegion)) {
+ mPreviousTransparentRegion.set(mTransparentRegion);
+ // reconfigure window manager
+ try {
+ sWindowSession.setTransparentRegion(mWindow, mTransparentRegion);
+ } catch (RemoteException e) {
+ }
+ }
+ }
+
+
+ if (DBG) {
+ System.out.println("======================================");
+ System.out.println("performTraversals -- after setFrame");
+ host.debug();
+ }
+ }
+
+ if (triggerGlobalLayoutListener) {
+ attachInfo.mRecomputeGlobalAttributes = false;
+ attachInfo.mTreeObserver.dispatchOnGlobalLayout();
+ }
+
+ if (mFirst) {
+ // handle first focus request
+ if (mView != null && !mView.hasFocus()) {
+ mView.requestFocus(View.FOCUS_FORWARD);
+ mFocusedView = mView.findFocus();
+ }
+ }
+
+ mFirst = false;
+ mWillDrawSoon = false;
+ mNewSurfaceNeeded = false;
+ mViewVisibility = viewVisibility;
+
+ boolean cancelDraw = attachInfo.mTreeObserver.dispatchOnPreDraw();
+
+ if (!cancelDraw && !newSurface) {
+ mFullRedrawNeeded = false;
+ draw(fullRedrawNeeded);
+
+ if ((relayoutResult&WindowManagerImpl.RELAYOUT_FIRST_TIME) != 0
+ || mReportNextDraw) {
+ if (LOCAL_LOGV) {
+ Log.v("ViewRoot", "FINISHED DRAWING: " + mWindowAttributes.getTitle());
+ }
+ mReportNextDraw = false;
+ try {
+ sWindowSession.finishDrawing(mWindow);
+ } catch (RemoteException e) {
+ }
+ }
+ } else {
+ // We were supposed to report when we are done drawing. Since we canceled the
+ // draw, rememeber it here.
+ if ((relayoutResult&WindowManagerImpl.RELAYOUT_FIRST_TIME) != 0) {
+ mReportNextDraw = true;
+ }
+ if (fullRedrawNeeded) {
+ mFullRedrawNeeded = true;
+ }
+ // Try again
+ scheduleTraversals();
+ }
+ }
+
+ public void requestTransparentRegion(View child) {
+ // the test below should not fail unless someone is messing with us
+ checkThread();
+ if (mView == child) {
+ mView.mPrivateFlags |= View.REQUEST_TRANSPARENT_REGIONS;
+ // Need to make sure we re-evaluate the window attributes next
+ // time around, to ensure the window has the correct format.
+ mWindowAttributesChanged = true;
+ }
+ }
+
+ /**
+ * Figures out the measure spec for the root view in a window based on it's
+ * layout params.
+ *
+ * @param windowSize
+ * The available width or height of the window
+ *
+ * @param rootDimension
+ * The layout params for one dimension (width or height) of the
+ * window.
+ *
+ * @return The measure spec to use to measure the root view.
+ */
+ private int getRootMeasureSpec(int windowSize, int rootDimension) {
+ int measureSpec;
+ switch (rootDimension) {
+
+ case ViewGroup.LayoutParams.FILL_PARENT:
+ // Window can't resize. Force root view to be windowSize.
+ measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
+ break;
+ case ViewGroup.LayoutParams.WRAP_CONTENT:
+ // Window can resize. Set max size for root view.
+ measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
+ break;
+ default:
+ // Window wants to be an exact size. Force root view to be that size.
+ measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
+ break;
+ }
+ return measureSpec;
+ }
+
+ private void draw(boolean fullRedrawNeeded) {
+ Surface surface = mSurface;
+ if (surface == null || !surface.isValid()) {
+ return;
+ }
+
+ Rect dirty = mDirty;
+ if (mUseGL) {
+ if (!dirty.isEmpty()) {
+ Canvas canvas = mGlCanvas;
+ if (mGL!=null && canvas != null) {
+ mGL.glDisable(GL_SCISSOR_TEST);
+ mGL.glClearColor(0, 0, 0, 0);
+ mGL.glClear(GL_COLOR_BUFFER_BIT);
+ mGL.glEnable(GL_SCISSOR_TEST);
+
+ mAttachInfo.mDrawingTime = SystemClock.uptimeMillis();
+ mView.draw(canvas);
+
+ mEgl.eglSwapBuffers(mEglDisplay, mEglSurface);
+ checkEglErrors();
+
+ if (SHOW_FPS) {
+ int now = (int)SystemClock.elapsedRealtime();
+ if (sDrawTime != 0) {
+ nativeShowFPS(canvas, now - sDrawTime);
+ }
+ sDrawTime = now;
+ }
+ }
+ }
+ return;
+ }
+
+
+ if (fullRedrawNeeded)
+ dirty.union(0, 0, mWidth, mHeight);
+
+ if (DEBUG_ORIENTATION || DEBUG_DRAW) {
+ Log.v("ViewRoot", "Draw " + mView + "/"
+ + mWindowAttributes.getTitle()
+ + ": dirty={" + dirty.left + "," + dirty.top
+ + "," + dirty.right + "," + dirty.bottom + "} surface="
+ + surface + " surface.isValid()=" + surface.isValid());
+ }
+
+ if (!dirty.isEmpty()) {
+ Canvas canvas;
+ try {
+ canvas = surface.lockCanvas(dirty);
+ } catch (Surface.OutOfResourcesException e) {
+ Log.e("ViewRoot", "OutOfResourcesException locking surface", e);
+ // TODO: we should ask the window manager to do something!
+ // for now we just do nothing
+ return;
+ }
+
+ long startTime;
+
+ try {
+ if (DEBUG_ORIENTATION || DEBUG_DRAW) {
+ Log.v("ViewRoot", "Surface " + surface + " drawing to bitmap w="
+ + canvas.getWidth() + ", h=" + canvas.getHeight());
+ //canvas.drawARGB(255, 255, 0, 0);
+ }
+
+ if (PROFILE_DRAWING) {
+ startTime = SystemClock.elapsedRealtime();
+ }
+
+ // If this bitmap's format includes an alpha channel, we
+ // need to clear it before drawing so that the child will
+ // properly re-composite its drawing on a transparent
+ // background. This automatically respects the clip/dirty region
+ if (!canvas.isOpaque()) {
+ canvas.drawColor(0, PorterDuff.Mode.CLEAR);
+ }
+
+ dirty.setEmpty();
+ mAttachInfo.mDrawingTime = SystemClock.uptimeMillis();
+ mView.draw(canvas);
+
+ if (SHOW_FPS) {
+ int now = (int)SystemClock.elapsedRealtime();
+ if (sDrawTime != 0) {
+ nativeShowFPS(canvas, now - sDrawTime);
+ }
+ sDrawTime = now;
+ }
+
+ } finally {
+ surface.unlockCanvasAndPost(canvas);
+ }
+
+ if (PROFILE_DRAWING) {
+ EventLog.writeEvent(60000, SystemClock.elapsedRealtime() - startTime);
+ }
+
+ if (LOCAL_LOGV) {
+ Log.v("ViewRoot", "Surface " + surface + " unlockCanvasAndPost");
+ }
+ }
+ }
+
+ public void requestChildFocus(View child, View focused) {
+ checkThread();
+ if (mFocusedView != focused) {
+ mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(mFocusedView, focused);
+ }
+ mFocusedView = focused;
+ }
+
+ public void clearChildFocus(View child) {
+ checkThread();
+
+ View oldFocus = mFocusedView;
+
+ mFocusedView = null;
+ if (mView != null && !mView.hasFocus()) {
+ // If a view gets the focus, the listener will be invoked from requestChildFocus()
+ if (!mView.requestFocus(View.FOCUS_FORWARD)) {
+ mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, null);
+ }
+ } else if (oldFocus != null) {
+ mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, null);
+ }
+ }
+
+
+ public void focusableViewAvailable(View v) {
+ checkThread();
+
+ if (mView != null && !mView.hasFocus()) {
+ v.requestFocus();
+ } else {
+ // the one case where will transfer focus away from the current one
+ // is if the current view is a view group that prefers to give focus
+ // to its children first AND the view is a descendant of it.
+ mFocusedView = mView.findFocus();
+ boolean descendantsHaveDibsOnFocus =
+ (mFocusedView instanceof ViewGroup) &&
+ (((ViewGroup) mFocusedView).getDescendantFocusability() ==
+ ViewGroup.FOCUS_AFTER_DESCENDANTS);
+ if (descendantsHaveDibsOnFocus && isViewDescendantOf(v, mFocusedView)) {
+ // If a view gets the focus, the listener will be invoked from requestChildFocus()
+ v.requestFocus();
+ }
+ }
+ }
+
+ public void recomputeViewAttributes(View child) {
+ checkThread();
+ if (mView == child) {
+ mAttachInfo.mRecomputeGlobalAttributes = true;
+ if (!mWillDrawSoon) {
+ scheduleTraversals();
+ }
+ }
+ }
+
+ void dispatchDetachedFromWindow() {
+ if (Config.LOGV) Log.v("ViewRoot", "Detaching in " + this + " of " + mSurface);
+ if (mView != null) {
+ mView.dispatchDetachedFromWindow();
+ }
+ mView = null;
+ if (mUseGL) {
+ destroyGL();
+ }
+ }
+
+ /**
+ * Return true if child is an ancestor of parent, (or equal to the parent).
+ */
+ private static boolean isViewDescendantOf(View child, View parent) {
+ if (child == parent) {
+ return true;
+ }
+
+ final ViewParent theParent = child.getParent();
+ return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent);
+ }
+
+
+ private final static int DO_TRAVERSAL = 1000;
+ private final static int DIE = 1001;
+ private final static int RESIZED = 1002;
+ private final static int RESIZED_REPORT = 1003;
+ private final static int WINDOW_FOCUS_CHANGED = 1004;
+ private final static int DISPATCH_KEY = 1005;
+ private final static int DISPATCH_POINTER = 1006;
+ private final static int DISPATCH_TRACKBALL = 1007;
+ private final static int DISPATCH_APP_VISIBILITY = 1008;
+ private final static int DISPATCH_GET_NEW_SURFACE = 1009;
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case DO_TRAVERSAL:
+ if (mProfile) {
+ Debug.startMethodTracing("ViewRoot");
+ }
+
+ performTraversals();
+
+ if (mProfile) {
+ Debug.stopMethodTracing();
+ mProfile = false;
+ }
+ break;
+ case DISPATCH_KEY:
+ if (LOCAL_LOGV) Log.v(
+ "ViewRoot", "Dispatching key "
+ + msg.obj + " to " + mView);
+ deliverKeyEvent((KeyEvent)msg.obj, true);
+ break;
+ case DISPATCH_POINTER:
+ MotionEvent event = (MotionEvent)msg.obj;
+
+ boolean didFinish;
+ if (event == null) {
+ try {
+ event = sWindowSession.getPendingPointerMove(mWindow);
+ } catch (RemoteException e) {
+ }
+ didFinish = true;
+ } else {
+ didFinish = false;
+ }
+
+ try {
+ boolean handled;
+ if (mView != null && mAdded && event != null) {
+
+ // enter touch mode on the down
+ boolean isDown = event.getAction() == MotionEvent.ACTION_DOWN;
+ if (isDown) {
+ ensureTouchMode(true);
+ }
+
+ handled = mView.dispatchTouchEvent(event);
+ if (!handled && isDown) {
+ int edgeSlop = ViewConfiguration.getEdgeSlop();
+
+ final int edgeFlags = event.getEdgeFlags();
+ int direction = View.FOCUS_UP;
+ int x = (int)event.getX();
+ int y = (int)event.getY();
+ final int[] deltas = new int[2];
+
+ if ((edgeFlags & MotionEvent.EDGE_TOP) != 0) {
+ direction = View.FOCUS_DOWN;
+ if ((edgeFlags & MotionEvent.EDGE_LEFT) != 0) {
+ deltas[0] = edgeSlop;
+ x += edgeSlop;
+ } else if ((edgeFlags & MotionEvent.EDGE_RIGHT) != 0) {
+ deltas[0] = -edgeSlop;
+ x -= edgeSlop;
+ }
+ } else if ((edgeFlags & MotionEvent.EDGE_BOTTOM) != 0) {
+ direction = View.FOCUS_UP;
+ if ((edgeFlags & MotionEvent.EDGE_LEFT) != 0) {
+ deltas[0] = edgeSlop;
+ x += edgeSlop;
+ } else if ((edgeFlags & MotionEvent.EDGE_RIGHT) != 0) {
+ deltas[0] = -edgeSlop;
+ x -= edgeSlop;
+ }
+ } else if ((edgeFlags & MotionEvent.EDGE_LEFT) != 0) {
+ direction = View.FOCUS_RIGHT;
+ } else if ((edgeFlags & MotionEvent.EDGE_RIGHT) != 0) {
+ direction = View.FOCUS_LEFT;
+ }
+
+ if (edgeFlags != 0 && mView instanceof ViewGroup) {
+ View nearest = FocusFinder.getInstance().findNearestTouchable(
+ ((ViewGroup) mView), x, y, direction, deltas);
+ if (nearest != null) {
+ event.offsetLocation(deltas[0], deltas[1]);
+ event.setEdgeFlags(0);
+ mView.dispatchTouchEvent(event);
+ }
+ }
+ }
+ }
+ } finally {
+ if (!didFinish) {
+ try {
+ sWindowSession.finishKey(mWindow);
+ } catch (RemoteException e) {
+ }
+ }
+ if (event != null) {
+ event.recycle();
+ }
+ if (LOCAL_LOGV || WATCH_POINTER) Log.i(TAG, "Done dispatching!");
+ // Let the exception fall through -- the looper will catch
+ // it and take care of the bad app for us.
+ }
+ break;
+ case DISPATCH_TRACKBALL:
+ deliverTrackballEvent((MotionEvent)msg.obj);
+ break;
+ case DISPATCH_APP_VISIBILITY:
+ handleAppVisibility(msg.arg1 != 0);
+ break;
+ case DISPATCH_GET_NEW_SURFACE:
+ handleGetNewSurface();
+ break;
+ case RESIZED:
+ if (mWinFrame.width() == msg.arg1 && mWinFrame.height() == msg.arg2) {
+ break;
+ }
+ // fall through...
+ case RESIZED_REPORT:
+ if (mAdded) {
+ mWinFrame.left = 0;
+ mWinFrame.right = msg.arg1;
+ mWinFrame.top = 0;
+ mWinFrame.bottom = msg.arg2;
+ if (msg.what == RESIZED_REPORT) {
+ mReportNextDraw = true;
+ }
+ requestLayout();
+ }
+ break;
+ case WINDOW_FOCUS_CHANGED: {
+ if (mAdded) {
+ boolean hasWindowFocus = msg.arg1 != 0;
+ mAttachInfo.mHasWindowFocus = hasWindowFocus;
+ if (hasWindowFocus) {
+ boolean inTouchMode = msg.arg2 != 0;
+ ensureTouchModeLocally(inTouchMode);
+
+ if (mGlWanted) {
+ checkEglErrors();
+ // we lost the gl context, so recreate it.
+ if (mGlWanted && !mUseGL) {
+ initializeGL();
+ if (mGlCanvas != null) {
+ mGlCanvas.setViewport(mWidth, mHeight);
+ }
+ }
+ }
+ }
+ if (mView != null) {
+ mView.dispatchWindowFocusChanged(hasWindowFocus);
+ }
+ }
+ } break;
+ case DIE:
+ dispatchDetachedFromWindow();
+ break;
+ }
+ }
+
+ /**
+ * 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.
+ *
+ * If the touch mode has changed, tell the window manager, and handle it locally.
+ *
+ * @param inTouchMode Whether we want to be in touch mode.
+ * @return True if the touch mode changed and focus changed was changed as a result
+ */
+ boolean ensureTouchMode(boolean inTouchMode) {
+ if (DBG) Log.d("touchmode", "ensureTouchMode(" + inTouchMode + "), current "
+ + "touch mode is " + mAttachInfo.mInTouchMode);
+ if (mAttachInfo.mInTouchMode == inTouchMode) return false;
+
+ // tell the window manager
+ try {
+ sWindowSession.setInTouchMode(inTouchMode);
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ }
+
+ // handle the change
+ return ensureTouchModeLocally(inTouchMode);
+ }
+
+ /**
+ * Ensure that the touch mode for this window is set, and if it is changing,
+ * take the appropriate action.
+ * @param inTouchMode Whether we want to be in touch mode.
+ * @return True if the touch mode changed and focus changed was changed as a result
+ */
+ private boolean ensureTouchModeLocally(boolean inTouchMode) {
+ if (DBG) Log.d("touchmode", "ensureTouchModeLocally(" + inTouchMode + "), current "
+ + "touch mode is " + mAttachInfo.mInTouchMode);
+
+ if (mAttachInfo.mInTouchMode == inTouchMode) return false;
+
+ mAttachInfo.mInTouchMode = inTouchMode;
+ mAttachInfo.mTreeObserver.dispatchOnTouchModeChanged(inTouchMode);
+
+ return (inTouchMode) ? enterTouchMode() : leaveTouchMode();
+ }
+
+ private boolean enterTouchMode() {
+ if (mView != null) {
+ if (mView.hasFocus()) {
+ // note: not relying on mFocusedView here because this could
+ // be when the window is first being added, and mFocused isn't
+ // set yet.
+ final View focused = mView.findFocus();
+ if (focused != null && !focused.isFocusableInTouchMode()) {
+
+ final ViewGroup ancestorToTakeFocus =
+ findAncestorToTakeFocusInTouchMode(focused);
+ if (ancestorToTakeFocus != null) {
+ // there is an ancestor that wants focus after its descendants that
+ // is focusable in touch mode.. give it focus
+ return ancestorToTakeFocus.requestFocus();
+ } else {
+ // nothing appropriate to have focus in touch mode, clear it out
+ mView.unFocus();
+ mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(focused, null);
+ mFocusedView = null;
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+
+ /**
+ * Find an ancestor of focused that wants focus after its descendants and is
+ * focusable in touch mode.
+ * @param focused The currently focused view.
+ * @return An appropriate view, or null if no such view exists.
+ */
+ private ViewGroup findAncestorToTakeFocusInTouchMode(View focused) {
+ ViewParent parent = focused.getParent();
+ while (parent instanceof ViewGroup) {
+ final ViewGroup vgParent = (ViewGroup) parent;
+ if (vgParent.getDescendantFocusability() == ViewGroup.FOCUS_AFTER_DESCENDANTS
+ && vgParent.isFocusableInTouchMode()) {
+ return vgParent;
+ }
+ if (vgParent.isRootNamespace()) {
+ return null;
+ } else {
+ parent = vgParent.getParent();
+ }
+ }
+ return null;
+ }
+
+ private boolean leaveTouchMode() {
+ if (mView != null) {
+ if (mView.hasFocus()) {
+ // i learned the hard way to not trust mFocusedView :)
+ mFocusedView = mView.findFocus();
+ if (!(mFocusedView instanceof ViewGroup)) {
+ // some view has focus, let it keep it
+ return false;
+ } else if (((ViewGroup)mFocusedView).getDescendantFocusability() !=
+ ViewGroup.FOCUS_AFTER_DESCENDANTS) {
+ // some view group has focus, and doesn't prefer its children
+ // over itself for focus, so let them keep it.
+ return false;
+ }
+ }
+
+ // find the best view to give focus to in this brave new non-touch-mode
+ // world
+ final View focused = focusSearch(null, View.FOCUS_DOWN);
+ if (focused != null) {
+ return focused.requestFocus(View.FOCUS_DOWN);
+ }
+ }
+ return false;
+ }
+
+
+ private void deliverTrackballEvent(MotionEvent event) {
+ boolean didFinish;
+ if (event == null) {
+ try {
+ event = sWindowSession.getPendingTrackballMove(mWindow);
+ } catch (RemoteException e) {
+ }
+ didFinish = true;
+ } else {
+ didFinish = false;
+ }
+
+ //Log.i("foo", "Motion event:" + event);
+
+ boolean handled = false;
+ try {
+ if (event == null) {
+ handled = true;
+ } else if (mView != null && mAdded) {
+ handled = mView.dispatchTrackballEvent(event);
+ if (!handled) {
+ // we could do something here, like changing the focus
+ // or someting?
+ }
+ }
+ } finally {
+ if (handled) {
+ if (!didFinish) {
+ try {
+ sWindowSession.finishKey(mWindow);
+ } catch (RemoteException e) {
+ }
+ }
+ if (event != null) {
+ event.recycle();
+ }
+ //noinspection ReturnInsideFinallyBlock
+ return;
+ }
+ // Let the exception fall through -- the looper will catch
+ // it and take care of the bad app for us.
+ }
+
+ final TrackballAxis x = mTrackballAxisX;
+ final TrackballAxis y = mTrackballAxisY;
+
+ long curTime = SystemClock.uptimeMillis();
+ if ((mLastTrackballTime+MAX_TRACKBALL_DELAY) < curTime) {
+ // It has been too long since the last movement,
+ // so restart at the beginning.
+ x.reset(0);
+ y.reset(0);
+ mLastTrackballTime = curTime;
+ }
+
+ try {
+ final int action = event.getAction();
+ final int metastate = event.getMetaState();
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ x.reset(2);
+ y.reset(2);
+ deliverKeyEvent(new KeyEvent(curTime, curTime,
+ KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_CENTER,
+ 0, metastate), false);
+ break;
+ case MotionEvent.ACTION_UP:
+ x.reset(2);
+ y.reset(2);
+ deliverKeyEvent(new KeyEvent(curTime, curTime,
+ KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DPAD_CENTER,
+ 0, metastate), false);
+ break;
+ }
+
+ if (DEBUG_TRACKBALL) Log.v(TAG, "TB X=" + x.position + " step="
+ + x.step + " dir=" + x.dir + " acc=" + x.acceleration
+ + " move=" + event.getX()
+ + " / Y=" + y.position + " step="
+ + y.step + " dir=" + y.dir + " acc=" + y.acceleration
+ + " move=" + event.getY());
+ final float xOff = x.collect(event.getX(), "X");
+ final float yOff = y.collect(event.getY(), "Y");
+
+ // Generate DPAD events based on the trackball movement.
+ // We pick the axis that has moved the most as the direction of
+ // the DPAD. When we generate DPAD events for one axis, then the
+ // other axis is reset -- we don't want to perform DPAD jumps due
+ // to slight movements in the trackball when making major movements
+ // along the other axis.
+ int keycode = 0;
+ int movement = 0;
+ float accel = 1;
+ if (xOff > yOff) {
+ movement = x.generate((2/event.getXPrecision()));
+ if (movement != 0) {
+ keycode = movement > 0 ? KeyEvent.KEYCODE_DPAD_RIGHT
+ : KeyEvent.KEYCODE_DPAD_LEFT;
+ accel = x.acceleration;
+ y.reset(2);
+ }
+ } else if (yOff > 0) {
+ movement = y.generate((2/event.getYPrecision()));
+ if (movement != 0) {
+ keycode = movement > 0 ? KeyEvent.KEYCODE_DPAD_DOWN
+ : KeyEvent.KEYCODE_DPAD_UP;
+ accel = y.acceleration;
+ x.reset(2);
+ }
+ }
+
+ if (keycode != 0) {
+ if (movement < 0) movement = -movement;
+ int accelMovement = (int)(movement * accel);
+ //Log.i(TAG, "Move: movement=" + movement
+ // + " accelMovement=" + accelMovement
+ // + " accel=" + accel);
+ if (accelMovement > movement) {
+ if (DEBUG_TRACKBALL) Log.v("foo", "Delivering fake DPAD: "
+ + keycode);
+ movement--;
+ deliverKeyEvent(new KeyEvent(curTime, curTime,
+ KeyEvent.ACTION_MULTIPLE, keycode,
+ accelMovement-movement, metastate), false);
+ }
+ while (movement > 0) {
+ if (DEBUG_TRACKBALL) Log.v("foo", "Delivering fake DPAD: "
+ + keycode);
+ movement--;
+ curTime = SystemClock.uptimeMillis();
+ deliverKeyEvent(new KeyEvent(curTime, curTime,
+ KeyEvent.ACTION_DOWN, keycode, 0, event.getMetaState()), false);
+ deliverKeyEvent(new KeyEvent(curTime, curTime,
+ KeyEvent.ACTION_UP, keycode, 0, metastate), false);
+ }
+ mLastTrackballTime = curTime;
+ }
+ } finally {
+ if (!didFinish) {
+ try {
+ sWindowSession.finishKey(mWindow);
+ } catch (RemoteException e) {
+ }
+ if (event != null) {
+ event.recycle();
+ }
+ }
+ // Let the exception fall through -- the looper will catch
+ // it and take care of the bad app for us.
+ }
+ }
+
+ /**
+ * @param keyCode The key code
+ * @return True if the key is directional.
+ */
+ static boolean isDirectional(int keyCode) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ case KeyEvent.KEYCODE_DPAD_UP:
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if this key is a keyboard key.
+ * @param keyEvent The key event.
+ * @return whether this key is a keyboard key.
+ */
+ private static boolean isKeyboardKey(KeyEvent keyEvent) {
+ final int convertedKey = keyEvent.getUnicodeChar();
+ return convertedKey > 0;
+ }
+
+
+
+ /**
+ * See if the key event means we should leave touch mode (and leave touch
+ * mode if so).
+ * @param event The key event.
+ * @return Whether this key event should be consumed (meaning the act of
+ * leaving touch mode alone is considered the event).
+ */
+ private boolean checkForLeavingTouchModeAndConsume(KeyEvent event) {
+ if (event.getAction() != KeyEvent.ACTION_DOWN) {
+ return false;
+ }
+
+ // only relevant if we are in touch mode
+ if (!mAttachInfo.mInTouchMode) {
+ return false;
+ }
+
+ // if something like an edit text has focus and the user is typing,
+ // leave touch mode
+ //
+ // note: the condition of not being a keyboard key is kind of a hacky
+ // approximation of whether we think the focused view will want the
+ // key; if we knew for sure whether the focused view would consume
+ // the event, that would be better.
+ if (isKeyboardKey(event) && mView != null && mView.hasFocus()) {
+ mFocusedView = mView.findFocus();
+ if ((mFocusedView instanceof ViewGroup)
+ && ((ViewGroup) mFocusedView).getDescendantFocusability() ==
+ ViewGroup.FOCUS_AFTER_DESCENDANTS) {
+ // something has focus, but is holding it weakly as a container
+ return false;
+ }
+ if (ensureTouchMode(false)) {
+ throw new IllegalStateException("should not have changed focus "
+ + "when leaving touch mode while a view has focus.");
+ }
+ return false;
+ }
+
+ if (isDirectional(event.getKeyCode())) {
+ // no view has focus, so we leave touch mode (and find something
+ // to give focus to). the event is consumed if we were able to
+ // find something to give focus to.
+ return ensureTouchMode(false);
+ }
+ return false;
+ }
+
+
+ private void deliverKeyEvent(KeyEvent event, boolean sendDone) {
+ try {
+ if (mView != null && mAdded) {
+ final int action = event.getAction();
+ boolean isDown = (action == KeyEvent.ACTION_DOWN);
+
+ if (checkForLeavingTouchModeAndConsume(event)) {
+ return;
+ }
+
+ boolean keyHandled = mView.dispatchKeyEvent(event);
+
+ if ((!keyHandled && isDown) || (action == KeyEvent.ACTION_MULTIPLE)) {
+ int direction = 0;
+ switch (event.getKeyCode()) {
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ direction = View.FOCUS_LEFT;
+ break;
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ direction = View.FOCUS_RIGHT;
+ break;
+ case KeyEvent.KEYCODE_DPAD_UP:
+ direction = View.FOCUS_UP;
+ break;
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ direction = View.FOCUS_DOWN;
+ break;
+ }
+
+ if (direction != 0) {
+
+ View focused = mView != null ? mView.findFocus() : null;
+ if (focused != null) {
+ View v = focused.focusSearch(direction);
+ boolean focusPassed = false;
+ if (v != null && v != focused) {
+ // do the math the get the interesting rect
+ // of previous focused into the coord system of
+ // newly focused view
+ focused.getFocusedRect(mTempRect);
+ ((ViewGroup) mView).offsetDescendantRectToMyCoords(focused, mTempRect);
+ ((ViewGroup) mView).offsetRectIntoDescendantCoords(v, mTempRect);
+ focusPassed = v.requestFocus(direction, mTempRect);
+ }
+
+ if (!focusPassed) {
+ mView.dispatchUnhandledMove(focused, direction);
+ } else {
+ playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));
+ }
+ }
+ }
+ }
+ }
+
+ } finally {
+ if (sendDone) {
+ if (LOCAL_LOGV) Log.v(
+ "ViewRoot", "Telling window manager key is finished");
+ try {
+ sWindowSession.finishKey(mWindow);
+ } catch (RemoteException e) {
+ }
+ }
+ // Let the exception fall through -- the looper will catch
+ // it and take care of the bad app for us.
+ }
+ }
+
+ private AudioManager getAudioManager() {
+ if (mView == null) {
+ throw new IllegalStateException("getAudioManager called when there is no mView");
+ }
+ if (mAudioManager == null) {
+ mAudioManager = (AudioManager) mView.getContext().getSystemService(Context.AUDIO_SERVICE);
+ }
+ return mAudioManager;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void playSoundEffect(int effectId) {
+ checkThread();
+
+ final AudioManager audioManager = getAudioManager();
+
+ switch (effectId) {
+ case SoundEffectConstants.CLICK:
+ audioManager.playSoundEffect(AudioManager.FX_KEY_CLICK);
+ return;
+ case SoundEffectConstants.NAVIGATION_DOWN:
+ audioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_DOWN);
+ return;
+ case SoundEffectConstants.NAVIGATION_LEFT:
+ audioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_LEFT);
+ return;
+ case SoundEffectConstants.NAVIGATION_RIGHT:
+ audioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_RIGHT);
+ return;
+ case SoundEffectConstants.NAVIGATION_UP:
+ audioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_UP);
+ return;
+ default:
+ throw new IllegalArgumentException("unknown effect id " + effectId +
+ " not defined in " + SoundEffectConstants.class.getCanonicalName());
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public View focusSearch(View focused, int direction) {
+ checkThread();
+ if (!(mView instanceof ViewGroup)) {
+ return null;
+ }
+ return FocusFinder.getInstance().findNextFocus((ViewGroup) mView, focused, direction);
+ }
+
+ public void debug() {
+ mView.debug();
+ }
+
+ public void die(boolean immediate) {
+ checkThread();
+ if (Config.LOGV) Log.v("ViewRoot", "DIE in " + this + " of " + mSurface);
+ synchronized (this) {
+ if (mAdded && !mFirst) {
+ int viewVisibility = mView.getVisibility();
+ boolean viewVisibilityChanged = mViewVisibility != viewVisibility;
+ if (mWindowAttributesChanged || viewVisibilityChanged) {
+ // If layout params have been changed, first give them
+ // to the window manager to make sure it has the correct
+ // animation info.
+ try {
+ if ((sWindowSession.relayout(
+ mWindow, mWindowAttributes,
+ mView.mMeasuredWidth, mView.mMeasuredHeight,
+ viewVisibility, mWinFrame, mCoveredInsets, mSurface)
+ &WindowManagerImpl.RELAYOUT_FIRST_TIME) != 0) {
+ sWindowSession.finishDrawing(mWindow);
+ }
+ } catch (RemoteException e) {
+ }
+ }
+
+ mSurface = null;
+ }
+ if (mAdded) {
+ mAdded = false;
+ try {
+ sWindowSession.remove(mWindow);
+ } catch (RemoteException e) {
+ }
+ if (immediate) {
+ dispatchDetachedFromWindow();
+ } else if (mView != null) {
+ sendEmptyMessage(DIE);
+ }
+ }
+ }
+ }
+
+ public void dispatchResized(int w, int h, boolean reportDraw) {
+ if (DEBUG_DRAW) Log.v(TAG, "Resized " + this + ": w=" + w
+ + " h=" + h + " reportDraw=" + reportDraw);
+ Message msg = obtainMessage(reportDraw ? RESIZED_REPORT : RESIZED);
+ msg.arg1 = w;
+ msg.arg2 = h;
+ sendMessage(msg);
+ }
+
+ public void dispatchKey(KeyEvent event) {
+ if (event.getAction() == KeyEvent.ACTION_DOWN) {
+ //noinspection ConstantConditions
+ if (false && event.getKeyCode() == KeyEvent.KEYCODE_CAMERA) {
+ if (Config.LOGD) Log.d("keydisp",
+ "===================================================");
+ if (Config.LOGD) Log.d("keydisp", "Focused view Hierarchy is:");
+ debug();
+
+ if (Config.LOGD) Log.d("keydisp",
+ "===================================================");
+ }
+ }
+
+ Message msg = obtainMessage(DISPATCH_KEY);
+ msg.obj = event;
+
+ if (LOCAL_LOGV) Log.v(
+ "ViewRoot", "sending key " + event + " to " + mView);
+
+ sendMessageAtTime(msg, event.getEventTime());
+ }
+
+ public void dispatchPointer(MotionEvent event, long eventTime) {
+ Message msg = obtainMessage(DISPATCH_POINTER);
+ msg.obj = event;
+ sendMessageAtTime(msg, eventTime);
+ }
+
+ public void dispatchTrackball(MotionEvent event, long eventTime) {
+ Message msg = obtainMessage(DISPATCH_TRACKBALL);
+ msg.obj = event;
+ sendMessageAtTime(msg, eventTime);
+ }
+
+ public void dispatchAppVisibility(boolean visible) {
+ Message msg = obtainMessage(DISPATCH_APP_VISIBILITY);
+ msg.arg1 = visible ? 1 : 0;
+ sendMessage(msg);
+ }
+
+ public void dispatchGetNewSurface() {
+ Message msg = obtainMessage(DISPATCH_GET_NEW_SURFACE);
+ sendMessage(msg);
+ }
+
+ public void windowFocusChanged(boolean hasFocus, boolean inTouchMode) {
+ Message msg = Message.obtain();
+ msg.what = WINDOW_FOCUS_CHANGED;
+ msg.arg1 = hasFocus ? 1 : 0;
+ msg.arg2 = inTouchMode ? 1 : 0;
+ sendMessage(msg);
+ }
+
+ public boolean showContextMenuForChild(View originalView) {
+ return false;
+ }
+
+ public void createContextMenu(ContextMenu menu) {
+ }
+
+ public void childDrawableStateChanged(View child) {
+ }
+
+ protected Rect getWindowFrame() {
+ return mWinFrame;
+ }
+
+ void checkThread() {
+ if (mThread != Thread.currentThread()) {
+ throw new CalledFromWrongThreadException(
+ "Only the original thread that created a view hierarchy can touch its views.");
+ }
+ }
+
+ public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
+ // ViewRoot never intercepts touch event, so this can be a no-op
+ }
+
+ static class W extends IWindow.Stub {
+ private WeakReference<ViewRoot> mViewRoot;
+
+ public W(ViewRoot viewRoot) {
+ mViewRoot = new WeakReference<ViewRoot>(viewRoot);
+ }
+
+ public void resized(int w, int h, boolean reportDraw) {
+ final ViewRoot viewRoot = mViewRoot.get();
+ if (viewRoot != null) {
+ viewRoot.dispatchResized(w, h, reportDraw);
+ }
+ }
+
+ public void dispatchKey(KeyEvent event) {
+ final ViewRoot viewRoot = mViewRoot.get();
+ if (viewRoot != null) {
+ viewRoot.dispatchKey(event);
+ }
+ }
+
+ public void dispatchPointer(MotionEvent event, long eventTime) {
+ final ViewRoot viewRoot = mViewRoot.get();
+ if (viewRoot != null) {
+ viewRoot.dispatchPointer(event, eventTime);
+ }
+ }
+
+ public void dispatchTrackball(MotionEvent event, long eventTime) {
+ final ViewRoot viewRoot = mViewRoot.get();
+ if (viewRoot != null) {
+ viewRoot.dispatchTrackball(event, eventTime);
+ }
+ }
+
+ public void dispatchAppVisibility(boolean visible) {
+ final ViewRoot viewRoot = mViewRoot.get();
+ if (viewRoot != null) {
+ viewRoot.dispatchAppVisibility(visible);
+ }
+ }
+
+ public void dispatchGetNewSurface() {
+ final ViewRoot viewRoot = mViewRoot.get();
+ if (viewRoot != null) {
+ viewRoot.dispatchGetNewSurface();
+ }
+ }
+
+ public void windowFocusChanged(boolean hasFocus, boolean inTouchMode) {
+ final ViewRoot viewRoot = mViewRoot.get();
+ if (viewRoot != null) {
+ viewRoot.windowFocusChanged(hasFocus, inTouchMode);
+ }
+ }
+
+ private static int checkCallingPermission(String permission) {
+ if (!Process.supportsProcesses()) {
+ return PackageManager.PERMISSION_GRANTED;
+ }
+
+ try {
+ return ActivityManagerNative.getDefault().checkPermission(
+ permission, Binder.getCallingPid(), Binder.getCallingUid());
+ } catch (RemoteException e) {
+ return PackageManager.PERMISSION_DENIED;
+ }
+ }
+
+ public void executeCommand(String command, String parameters, ParcelFileDescriptor out) {
+ final ViewRoot viewRoot = mViewRoot.get();
+ if (viewRoot != null) {
+ final View view = viewRoot.mView;
+ if (view != null) {
+ if (checkCallingPermission(Manifest.permission.DUMP) !=
+ PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("Insufficient permissions to invoke"
+ + " executeCommand() from pid=" + Binder.getCallingPid()
+ + ", uid=" + Binder.getCallingUid());
+ }
+
+ OutputStream clientStream = null;
+ try {
+ clientStream = new ParcelFileDescriptor.AutoCloseOutputStream(out);
+ ViewDebug.dispatchCommand(view, command, parameters, clientStream);
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ if (clientStream != null) {
+ try {
+ clientStream.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Maintains state information for a single trackball axis, generating
+ * discrete (DPAD) movements based on raw trackball motion.
+ */
+ static final class TrackballAxis {
+ float position;
+ float absPosition;
+ float acceleration = 1;
+ int step;
+ int dir;
+ int nonAccelMovement;
+
+ void reset(int _step) {
+ position = 0;
+ acceleration = 1;
+ step = _step;
+ dir = 0;
+ }
+
+ /**
+ * Add trackball movement into the state. If the direction of movement
+ * has been reversed, the state is reset before adding the
+ * movement (so that you don't have to compensate for any previously
+ * collected movement before see the result of the movement in the
+ * new direction).
+ *
+ * @return Returns the absolute value of the amount of movement
+ * collected so far.
+ */
+ float collect(float off, String axis) {
+ if (off > 0) {
+ if (dir < 0) {
+ if (DEBUG_TRACKBALL) Log.v(TAG, axis + " reversed to positive!");
+ position = 0;
+ step = 0;
+ acceleration = 1;
+ }
+ dir = 1;
+ } else if (off < 0) {
+ if (dir > 0) {
+ if (DEBUG_TRACKBALL) Log.v(TAG, axis + " reversed to negative!");
+ position = 0;
+ step = 0;
+ acceleration = 1;
+ }
+ dir = -1;
+ }
+ position += off;
+ return (absPosition = Math.abs(position));
+ }
+
+ /**
+ * Generate the number of discrete movement events appropriate for
+ * the currently collected trackball movement.
+ *
+ * @param precision The minimum movement required to generate the
+ * first discrete movement.
+ *
+ * @return Returns the number of discrete movements, either positive
+ * or negative, or 0 if there is not enough trackball movement yet
+ * for a discrete movement.
+ */
+ int generate(float precision) {
+ int movement = 0;
+ nonAccelMovement = 0;
+ do {
+ final int dir = position >= 0 ? 1 : -1;
+ switch (step) {
+ // If we are going to execute the first step, then we want
+ // to do this as soon as possible instead of waiting for
+ // a full movement, in order to make things look responsive.
+ case 0:
+ if (absPosition < precision) {
+ return movement;
+ }
+ movement += dir;
+ nonAccelMovement += dir;
+ step = 1;
+ break;
+ // If we have generated the first movement, then we need
+ // to wait for the second complete trackball motion before
+ // generating the second discrete movement.
+ case 1:
+ if (absPosition < 2) {
+ return movement;
+ }
+ movement += dir;
+ nonAccelMovement += dir;
+ position += dir > 0 ? -2 : 2;
+ absPosition = Math.abs(position);
+ step = 2;
+ break;
+ // After the first two, we generate discrete movements
+ // consistently with the trackball, applying an acceleration
+ // if the trackball is moving quickly. The acceleration is
+ // currently very simple, just reducing the amount of
+ // trackball motion required as more discrete movements are
+ // generated. This should probably be changed to take time
+ // more into account, so that quick trackball movements will
+ // have increased acceleration.
+ default:
+ if (absPosition < 1) {
+ return movement;
+ }
+ movement += dir;
+ position += dir >= 0 ? -1 : 1;
+ absPosition = Math.abs(position);
+ float acc = acceleration;
+ acc *= 1.1f;
+ acceleration = acc < 20 ? acc : acceleration;
+ break;
+ }
+ } while (true);
+ }
+ }
+
+ public static final class CalledFromWrongThreadException extends AndroidRuntimeException {
+ public CalledFromWrongThreadException(String msg) {
+ super(msg);
+ }
+ }
+
+ private static final class RootHandler extends Handler {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case View.AttachInfo.INVALIDATE_MSG:
+ ((View) msg.obj).invalidate();
+ break;
+ case View.AttachInfo.INVALIDATE_RECT_MSG:
+ int left = msg.arg1 >>> 16;
+ int top = msg.arg1 & 0xFFFF;
+ int right = msg.arg2 >>> 16;
+ int bottom = msg.arg2 & 0xFFFF;
+ ((View) msg.obj).invalidate(left, top, right, bottom);
+ break;
+ }
+ }
+ }
+
+ private SurfaceHolder mHolder = new SurfaceHolder() {
+ // we only need a SurfaceHolder for opengl. it would be nice
+ // to implement everything else though, especially the callback
+ // support (opengl doesn't make use of it right now, but eventually
+ // will).
+ public Surface getSurface() {
+ return mSurface;
+ }
+
+ public boolean isCreating() {
+ return false;
+ }
+
+ public void addCallback(Callback callback) {
+ }
+
+ public void removeCallback(Callback callback) {
+ }
+
+ public void setFixedSize(int width, int height) {
+ }
+
+ public void setSizeFromLayout() {
+ }
+
+ public void setFormat(int format) {
+ }
+
+ public void setType(int type) {
+ }
+
+ public void setKeepScreenOn(boolean screenOn) {
+ }
+
+ public Canvas lockCanvas() {
+ return null;
+ }
+
+ public Canvas lockCanvas(Rect dirty) {
+ return null;
+ }
+
+ public void unlockCanvasAndPost(Canvas canvas) {
+ }
+ public Rect getSurfaceFrame() {
+ return null;
+ }
+ };
+
+ /**
+ * @hide
+ */
+ static final class RunQueue {
+ private final ArrayList<HandlerAction> mActions = new ArrayList<HandlerAction>();
+
+ void post(Runnable action) {
+ postDelayed(action, 0);
+ }
+
+ void postDelayed(Runnable action, long delayMillis) {
+ HandlerAction handlerAction = new HandlerAction();
+ handlerAction.action = action;
+ handlerAction.delay = delayMillis;
+
+ synchronized (mActions) {
+ mActions.add(handlerAction);
+ }
+ }
+
+ void removeCallbacks(Runnable action) {
+ final HandlerAction handlerAction = new HandlerAction();
+ handlerAction.action = action;
+
+ synchronized (mActions) {
+ final ArrayList<HandlerAction> actions = mActions;
+ final int count = actions.size();
+
+ while (actions.remove(handlerAction)) {
+ // Keep going
+ }
+ }
+ }
+
+ void executeActions(Handler handler) {
+ synchronized (mActions) {
+ final ArrayList<HandlerAction> actions = mActions;
+ final int count = actions.size();
+
+ for (int i = 0; i < count; i++) {
+ final HandlerAction handlerAction = actions.get(i);
+ handler.postDelayed(handlerAction.action, handlerAction.delay);
+ }
+
+ mActions.clear();
+ }
+ }
+
+ private static class HandlerAction {
+ Runnable action;
+ long delay;
+
+ @Override
+ public boolean equals(Object o) {
+ return action.equals(o);
+ }
+ }
+ }
+
+ private static native void nativeShowFPS(Canvas canvas, int durationMillis);
+
+ // inform skia to just abandon its texture cache IDs
+ // doesn't call glDeleteTextures
+ private static native void nativeAbandonGlCaches();
+}
diff --git a/core/java/android/view/ViewStub.java b/core/java/android/view/ViewStub.java
new file mode 100644
index 0000000..e159de4
--- /dev/null
+++ b/core/java/android/view/ViewStub.java
@@ -0,0 +1,279 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.util.AttributeSet;
+
+import com.android.internal.R;
+
+/**
+ * A ViewStub is an invisible, zero-sized View that can be used to lazily inflate
+ * layout resources at runtime.
+ *
+ * When a ViewStub is made visible, or when {@link #inflate()} is invoked, the layout resource
+ * is inflated. The ViewStub then replaces itself in its parent with the inflated View or Views.
+ * Therefore, the ViewStub exists in the view hierarchy until {@link #setVisibility(int)} or
+ * {@link #inflate()} is invoked.
+ *
+ * The inflated View is added to the ViewStub's parent with the ViewStub's layout
+ * parameters. Similarly, you can define/override the inflate View's id by using the
+ * ViewStub's inflatedId property. For instance:
+ *
+ * <pre>
+ * &lt;ViewStub android:id="@+id/stub"
+ * android:inflatedId="@+id/subTree"
+ * android:layout="@layout/mySubTree"
+ * android:layout_width="120dip"
+ * android:layout_height="40dip" /&gt;
+ * </pre>
+ *
+ * The ViewStub thus defined can be found using the id "stub." After inflation of
+ * the layout resource "mySubTree," the ViewStub is removed from its parent. The
+ * View created by inflating the layout resource "mySubTree" can be found using the
+ * id "subTree," specified by the inflatedId property. The inflated View is finally
+ * assigned a width of 120dip and a height of 40dip.
+ *
+ * The preferred way to perform the inflation of the layout resource is the following:
+ *
+ * <pre>
+ * ViewStub stub = (ViewStub) findViewById(R.id.stub);
+ * View inflated = stub.inflate();
+ * </pre>
+ *
+ * When {@link #inflate()} is invoked, the ViewStub is replaced by the inflated View
+ * and the inflated View is returned. This lets applications get a reference to the
+ * inflated View without executing an extra findViewById().
+ *
+ * @attr ref android.R.styleable#ViewStub_inflatedId
+ * @attr ref android.R.styleable#ViewStub_layout
+ */
+public final class ViewStub extends View {
+ private int mLayoutResource = 0;
+ private int mInflatedId;
+
+ private OnInflateListener mInflateListener;
+
+ public ViewStub(Context context) {
+ initialize(context);
+ }
+
+ /**
+ * Creates a new ViewStub with the specified layout resource.
+ *
+ * @param context The application's environment.
+ * @param layoutResource The reference to a layout resource that will be inflated.
+ */
+ public ViewStub(Context context, int layoutResource) {
+ mLayoutResource = layoutResource;
+ initialize(context);
+ }
+
+ public ViewStub(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ @SuppressWarnings({"UnusedDeclaration"})
+ public ViewStub(Context context, AttributeSet attrs, int defStyle) {
+ TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.ViewStub,
+ defStyle, 0);
+
+ mInflatedId = a.getResourceId(R.styleable.ViewStub_inflatedId, NO_ID);
+ mLayoutResource = a.getResourceId(R.styleable.ViewStub_layout, 0);
+
+ a.recycle();
+
+ a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.View, defStyle, 0);
+ mID = a.getResourceId(R.styleable.View_id, NO_ID);
+ a.recycle();
+
+ initialize(context);
+ }
+
+ private void initialize(Context context) {
+ mContext = context;
+ setVisibility(GONE);
+ setWillNotDraw(true);
+ }
+
+ /**
+ * Returns the id taken by the inflated view. If the inflated id is
+ * {@link View#NO_ID}, the inflated view keeps its original id.
+ *
+ * @return A positive integer used to identify the inflated view or
+ * {@link #NO_ID} if the inflated view should keep its id.
+ *
+ * @see #setInflatedId(int)
+ * @attr ref android.R.styleable#ViewStub_inflatedId
+ */
+ public int getInflatedId() {
+ return mInflatedId;
+ }
+
+ /**
+ * Defines the id taken by the inflated view. If the inflated id is
+ * {@link View#NO_ID}, the inflated view keeps its original id.
+ *
+ * @param inflatedId A positive integer used to identify the inflated view or
+ * {@link #NO_ID} if the inflated view should keep its id.
+ *
+ * @see #getInflatedId()
+ * @attr ref android.R.styleable#ViewStub_inflatedId
+ */
+ public void setInflatedId(int inflatedId) {
+ mInflatedId = inflatedId;
+ }
+
+ /**
+ * Returns the layout resource that will be used by {@link #setVisibility(int)} or
+ * {@link #inflate()} to replace this StubbedView
+ * in its parent by another view.
+ *
+ * @return The layout resource identifier used to inflate the new View.
+ *
+ * @see #setLayoutResource(int)
+ * @see #setVisibility(int)
+ * @see #inflate()
+ * @attr ref android.R.styleable#ViewStub_layout
+ */
+ public int getLayoutResource() {
+ return mLayoutResource;
+ }
+
+ /**
+ * Specifies the layout resource to inflate when this StubbedView becomes visible or invisible
+ * or when {@link #inflate()} is invoked. The View created by inflating the layout resource is
+ * used to replace this StubbedView in its parent.
+ *
+ * @param layoutResource A valid layout resource identifier (different from 0.)
+ *
+ * @see #getLayoutResource()
+ * @see #setVisibility(int)
+ * @see #inflate()
+ * @attr ref android.R.styleable#ViewStub_layout
+ */
+ public void setLayoutResource(int layoutResource) {
+ mLayoutResource = layoutResource;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ setMeasuredDimension(0, 0);
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ }
+
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ }
+
+ /**
+ * When visibility is set to {@link #VISIBLE} or {@link #INVISIBLE},
+ * {@link #inflate()} is invoked and this StubbedView is replaced in its parent
+ * by the inflated layout resource.
+ *
+ * @param visibility One of {@link #VISIBLE}, {@link #INVISIBLE}, or {@link #GONE}.
+ *
+ * @see #inflate()
+ */
+ @Override
+ public void setVisibility(int visibility) {
+ super.setVisibility(visibility);
+
+ if (visibility == VISIBLE || visibility == INVISIBLE) {
+ inflate();
+ }
+ }
+
+ /**
+ * Inflates the layout resource identified by {@link #getLayoutResource()}
+ * and replaces this StubbedView in its parent by the inflated layout resource.
+ *
+ * @return The inflated layout resource.
+ *
+ */
+ public View inflate() {
+ final ViewParent viewParent = getParent();
+
+ if (viewParent != null && viewParent instanceof ViewGroup) {
+ if (mLayoutResource != 0) {
+ final ViewGroup parent = (ViewGroup) viewParent;
+ final LayoutInflater factory = LayoutInflater.from(mContext);
+ final View view = factory.inflate(mLayoutResource, parent,
+ false);
+
+ if (mInflatedId != NO_ID) {
+ view.setId(mInflatedId);
+ }
+
+ final int index = parent.indexOfChild(this);
+ parent.removeViewInLayout(this);
+
+ final ViewGroup.LayoutParams layoutParams = getLayoutParams();
+ if (layoutParams != null) {
+ parent.addView(view, index, layoutParams);
+ } else {
+ parent.addView(view, index);
+ }
+
+ if (mInflateListener != null) {
+ mInflateListener.onInflate(this, view);
+ }
+
+ return view;
+ } else {
+ throw new IllegalArgumentException("ViewStub must have a valid layoutResource");
+ }
+ } else {
+ throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
+ }
+ }
+
+ /**
+ * Specifies the inflate listener to be notified after this ViewStub successfully
+ * inflated its layout resource.
+ *
+ * @param inflateListener The OnInflateListener to notify of successful inflation.
+ *
+ * @see android.view.ViewStub.OnInflateListener
+ */
+ public void setOnInflateListener(OnInflateListener inflateListener) {
+ mInflateListener = inflateListener;
+ }
+
+ /**
+ * Listener used to receive a notification after a ViewStub has successfully
+ * inflated its layout resource.
+ *
+ * @see android.view.ViewStub#setOnInflateListener(android.view.ViewStub.OnInflateListener)
+ */
+ public static interface OnInflateListener {
+ /**
+ * Invoked after a ViewStub successfully inflated its layout resource.
+ * This method is invoked after the inflated view was added to the
+ * hierarchy but before the layout pass.
+ *
+ * @param stub The ViewStub that initiated the inflation.
+ * @param inflated The inflated View.
+ */
+ void onInflate(ViewStub stub, View inflated);
+ }
+}
diff --git a/core/java/android/view/ViewTreeObserver.java b/core/java/android/view/ViewTreeObserver.java
new file mode 100644
index 0000000..2e1e01a
--- /dev/null
+++ b/core/java/android/view/ViewTreeObserver.java
@@ -0,0 +1,376 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import java.util.ArrayList;
+
+/**
+ * A view tree observer is used to register listeners that can be notified of global
+ * changes in the view tree. Such global events include, but are not limited to,
+ * layout of the whole tree, beginning of the drawing pass, touch mode change....
+ *
+ * A ViewTreeObserver should never be instantiated by applications as it is provided
+ * by the views hierarchy. Refer to {@link android.view.View#getViewTreeObserver()}
+ * for more information.
+ */
+public final class ViewTreeObserver {
+ private ArrayList<OnGlobalFocusChangeListener> mOnGlobalFocusListeners;
+ private ArrayList<OnGlobalLayoutListener> mOnGlobalLayoutListeners;
+ private ArrayList<OnPreDrawListener> mOnPreDrawListeners;
+ private ArrayList<OnTouchModeChangeListener> mOnTouchModeChangeListeners;
+
+ private boolean mAlive = true;
+
+ /**
+ * Interface definition for a callback to be invoked when the focus state within
+ * the view tree changes.
+ */
+ public interface OnGlobalFocusChangeListener {
+ /**
+ * Callback method to be invoked when the focus changes in the view tree. When
+ * the view tree transitions from touch mode to non-touch mode, oldFocus is null.
+ * When the view tree transitions from non-touch mode to touch mode, newFocus is
+ * null. When focus changes in non-touch mode (without transition from or to
+ * touch mode) either oldFocus or newFocus can be null.
+ *
+ * @param oldFocus The previously focused view, if any.
+ * @param newFocus The newly focused View, if any.
+ */
+ public void onGlobalFocusChanged(View oldFocus, View newFocus);
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when the global layout state
+ * or the visibility of views within the view tree changes.
+ */
+ public interface OnGlobalLayoutListener {
+ /**
+ * Callback method to be invoked when the global layout state or the visibility of views
+ * within the view tree changes
+ */
+ public void onGlobalLayout();
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when the view tree is about to be drawn.
+ */
+ public interface OnPreDrawListener {
+ /**
+ * Callback method to be invoked when the view tree is about to be drawn. At this point, all
+ * views in the tree have been measured and given a frame. Clients can use this to adjust
+ * their scroll bounds or even to request a new layout before drawing occurs.
+ *
+ * @return Return true to proceed with the current drawing pass, or false to cancel.
+ *
+ * @see android.view.View#onMeasure
+ * @see android.view.View#onLayout
+ * @see android.view.View#onDraw
+ */
+ public boolean onPreDraw();
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when the touch mode changes.
+ */
+ public interface OnTouchModeChangeListener {
+ /**
+ * Callback method to be invoked when the touch mode changes.
+ *
+ * @param isInTouchMode True if the view hierarchy is now in touch mode, false otherwise.
+ */
+ public void onTouchModeChanged(boolean isInTouchMode);
+ }
+
+ /**
+ * Creates a new ViewTreeObserver. This constructor should not be called
+ */
+ ViewTreeObserver() {
+ }
+
+ /**
+ * Merges all the listeners registered on the specified observer with the listeners
+ * registered on this object. After this method is invoked, the specified observer
+ * will return false in {@link #isAlive()} and should not be used anymore.
+ *
+ * @param observer The ViewTreeObserver whose listeners must be added to this observer
+ */
+ void merge(ViewTreeObserver observer) {
+ if (observer.mOnGlobalFocusListeners != null) {
+ if (mOnGlobalFocusListeners != null) {
+ mOnGlobalFocusListeners.addAll(observer.mOnGlobalFocusListeners);
+ } else {
+ mOnGlobalFocusListeners = observer.mOnGlobalFocusListeners;
+ }
+ }
+
+ if (observer.mOnGlobalLayoutListeners != null) {
+ if (mOnGlobalLayoutListeners != null) {
+ mOnGlobalLayoutListeners.addAll(observer.mOnGlobalLayoutListeners);
+ } else {
+ mOnGlobalLayoutListeners = observer.mOnGlobalLayoutListeners;
+ }
+ }
+
+ if (observer.mOnPreDrawListeners != null) {
+ if (mOnPreDrawListeners != null) {
+ mOnPreDrawListeners.addAll(observer.mOnPreDrawListeners);
+ } else {
+ mOnPreDrawListeners = observer.mOnPreDrawListeners;
+ }
+ }
+
+ if (observer.mOnTouchModeChangeListeners != null) {
+ if (mOnTouchModeChangeListeners != null) {
+ mOnTouchModeChangeListeners.addAll(observer.mOnTouchModeChangeListeners);
+ } else {
+ mOnTouchModeChangeListeners = observer.mOnTouchModeChangeListeners;
+ }
+ }
+
+ observer.kill();
+ }
+
+ /**
+ * Register a callback to be invoked when the focus state within the view tree changes.
+ *
+ * @param listener The callback to add
+ *
+ * @throws IllegalStateException If {@link #isAlive()} returns false
+ */
+ public void addOnGlobalFocusChangeListener(OnGlobalFocusChangeListener listener) {
+ checkIsAlive();
+
+ if (mOnGlobalFocusListeners == null) {
+ mOnGlobalFocusListeners = new ArrayList<OnGlobalFocusChangeListener>();
+ }
+
+ mOnGlobalFocusListeners.add(listener);
+ }
+
+ /**
+ * Remove a previously installed focus change callback.
+ *
+ * @param victim The callback to remove
+ *
+ * @throws IllegalStateException If {@link #isAlive()} returns false
+ *
+ * @see #addOnGlobalFocusChangeListener(OnGlobalFocusChangeListener)
+ */
+ public void removeOnGlobalFocusChangeListener(OnGlobalFocusChangeListener victim) {
+ checkIsAlive();
+ if (mOnGlobalFocusListeners == null) {
+ return;
+ }
+ mOnGlobalFocusListeners.remove(victim);
+ }
+
+ /**
+ * Register a callback to be invoked when the global layout state or the visibility of views
+ * within the view tree changes
+ *
+ * @param listener The callback to add
+ *
+ * @throws IllegalStateException If {@link #isAlive()} returns false
+ */
+ public void addOnGlobalLayoutListener(OnGlobalLayoutListener listener) {
+ checkIsAlive();
+
+ if (mOnGlobalLayoutListeners == null) {
+ mOnGlobalLayoutListeners = new ArrayList<OnGlobalLayoutListener>();
+ }
+
+ mOnGlobalLayoutListeners.add(listener);
+ }
+
+ /**
+ * 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 removeGlobalOnLayoutListener(OnGlobalLayoutListener victim) {
+ checkIsAlive();
+ if (mOnGlobalLayoutListeners == null) {
+ return;
+ }
+ mOnGlobalLayoutListeners.remove(victim);
+ }
+
+ /**
+ * Register a callback to be invoked when the view tree is about to be drawn
+ *
+ * @param listener The callback to add
+ *
+ * @throws IllegalStateException If {@link #isAlive()} returns false
+ */
+ public void addOnPreDrawListener(OnPreDrawListener listener) {
+ checkIsAlive();
+
+ if (mOnPreDrawListeners == null) {
+ mOnPreDrawListeners = new ArrayList<OnPreDrawListener>();
+ }
+
+ mOnPreDrawListeners.add(listener);
+ }
+
+ /**
+ * Remove a previously installed pre-draw callback
+ *
+ * @param victim The callback to remove
+ *
+ * @throws IllegalStateException If {@link #isAlive()} returns false
+ *
+ * @see #addOnPreDrawListener(OnPreDrawListener)
+ */
+ public void removeOnPreDrawListener(OnPreDrawListener victim) {
+ checkIsAlive();
+ if (mOnPreDrawListeners == null) {
+ return;
+ }
+ mOnPreDrawListeners.remove(victim);
+ }
+
+ /**
+ * Register a callback to be invoked when the invoked when the touch mode changes.
+ *
+ * @param listener The callback to add
+ *
+ * @throws IllegalStateException If {@link #isAlive()} returns false
+ */
+ public void addOnTouchModeChangeListener(OnTouchModeChangeListener listener) {
+ checkIsAlive();
+
+ if (mOnTouchModeChangeListeners == null) {
+ mOnTouchModeChangeListeners = new ArrayList<OnTouchModeChangeListener>();
+ }
+
+ mOnTouchModeChangeListeners.add(listener);
+ }
+
+ /**
+ * Remove a previously installed touch mode change callback
+ *
+ * @param victim The callback to remove
+ *
+ * @throws IllegalStateException If {@link #isAlive()} returns false
+ *
+ * @see #addOnTouchModeChangeListener(OnTouchModeChangeListener)
+ */
+ public void removeOnTouchModeChangeListener(OnTouchModeChangeListener victim) {
+ checkIsAlive();
+ if (mOnTouchModeChangeListeners == null) {
+ return;
+ }
+ mOnTouchModeChangeListeners.remove(victim);
+ }
+
+ private void checkIsAlive() {
+ if (!mAlive) {
+ throw new IllegalStateException("This ViewTreeObserver is not alive, call "
+ + "getViewTreeObserver() again");
+ }
+ }
+
+ /**
+ * Indicates whether this ViewTreeObserver is alive. When an observer is not alive,
+ * any call to a method (except this one) will throw an exception.
+ *
+ * If an application keeps a long-lived reference to this ViewTreeObserver, it should
+ * always check for the result of this method before calling any other method.
+ *
+ * @return True if this object is alive and be used, false otherwise.
+ */
+ public boolean isAlive() {
+ return mAlive;
+ }
+
+ /**
+ * Marks this ViewTreeObserver as not alive. After invoking this method, invoking
+ * any other method but {@link #isAlive()} and {@link #kill()} will throw an Exception.
+ *
+ * @hide
+ */
+ private void kill() {
+ mAlive = false;
+ }
+
+ /**
+ * Notifies registered listeners that focus has changed.
+ */
+ final void dispatchOnGlobalFocusChange(View oldFocus, View newFocus) {
+ final ArrayList<OnGlobalFocusChangeListener> globaFocusListeners = mOnGlobalFocusListeners;
+ if (globaFocusListeners != null) {
+ final int count = globaFocusListeners.size();
+ for (int i = count - 1; i >= 0; i--) {
+ globaFocusListeners.get(i).onGlobalFocusChanged(oldFocus, newFocus);
+ }
+ }
+ }
+
+ /**
+ * Notifies registered listeners that a global layout happened. This can be called
+ * manually if you are forcing a layout on a View or a hierarchy of Views that are
+ * not attached to a Window or in the GONE state.
+ */
+ public final void dispatchOnGlobalLayout() {
+ final ArrayList<OnGlobalLayoutListener> globaLayoutListeners = mOnGlobalLayoutListeners;
+ if (globaLayoutListeners != null) {
+ final int count = globaLayoutListeners.size();
+ for (int i = count - 1; i >= 0; i--) {
+ globaLayoutListeners.get(i).onGlobalLayout();
+ }
+ }
+ }
+
+ /**
+ * Notifies registered listeners that the drawing pass is about to start. If a
+ * listener returns true, then the drawing pass is canceled and rescheduled. This can
+ * be called manually if you are forcing the drawing on a View or a hierarchy of Views
+ * that are not attached to a Window or in the GONE state.
+ *
+ * @return True if the current draw should be canceled and resceduled, false otherwise.
+ */
+ public final boolean dispatchOnPreDraw() {
+ boolean cancelDraw = false;
+ final ArrayList<OnPreDrawListener> preDrawListeners = mOnPreDrawListeners;
+ if (preDrawListeners != null) {
+ final int count = preDrawListeners.size();
+ for (int i = count - 1; i >= 0; i--) {
+ cancelDraw |= !preDrawListeners.get(i).onPreDraw();
+ }
+ }
+ return cancelDraw;
+ }
+
+ /**
+ * Notifies registered listeners that the touch mode has changed.
+ *
+ * @param inTouchMode True if the touch mode is now enabled, false otherwise.
+ */
+ final void dispatchOnTouchModeChanged(boolean inTouchMode) {
+ final ArrayList<OnTouchModeChangeListener> touchModeListeners = mOnTouchModeChangeListeners;
+ if (touchModeListeners != null) {
+ final int count = touchModeListeners.size();
+ for (int i = count - 1; i >= 0; i--) {
+ touchModeListeners.get(i).onTouchModeChanged(inTouchMode);
+ }
+ }
+ }
+}
diff --git a/core/java/android/view/VolumePanel.java b/core/java/android/view/VolumePanel.java
new file mode 100644
index 0000000..24f4853
--- /dev/null
+++ b/core/java/android/view/VolumePanel.java
@@ -0,0 +1,341 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.media.ToneGenerator;
+import android.media.AudioManager;
+import android.media.AudioService;
+import android.content.Context;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Vibrator;
+import android.util.Config;
+import android.util.Log;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import android.widget.Toast;
+
+/**
+ * Handle the volume up and down keys.
+ *
+ * This code really should be moved elsewhere.
+ *
+ * @hide
+ */
+public class VolumePanel extends Handler
+{
+ private static final String TAG = "VolumePanel";
+ private static boolean LOGD = false || Config.LOGD;
+
+ /**
+ * The delay before playing a sound. This small period exists so the user
+ * can press another key (non-volume keys, too) to have it NOT be audible.
+ * <p>
+ * PhoneWindow will implement this part.
+ */
+ public static final int PLAY_SOUND_DELAY = 300;
+
+ /**
+ * The delay before vibrating. This small period exists so if the user is
+ * moving to silent mode, it will not emit a short vibrate (it normally
+ * would since vibrate is between normal mode and silent mode using hardware
+ * keys).
+ */
+ public static final int VIBRATE_DELAY = 300;
+
+ private static final int VIBRATE_DURATION = 300;
+ private static final int BEEP_DURATION = 150;
+ private static final int MAX_VOLUME = 100;
+ private static final int FREE_DELAY = 10000;
+
+ private static final int MSG_VOLUME_CHANGED = 0;
+ private static final int MSG_FREE_RESOURCES = 1;
+ private static final int MSG_PLAY_SOUND = 2;
+ private static final int MSG_STOP_SOUNDS = 3;
+ private static final int MSG_VIBRATE = 4;
+
+ private final String RINGTONE_VOLUME_TEXT;
+ private final String MUSIC_VOLUME_TEXT;
+ private final String INCALL_VOLUME_TEXT;
+ private final String ALARM_VOLUME_TEXT;
+ private final String UNKNOWN_VOLUME_TEXT;
+
+ protected Context mContext;
+ protected AudioService mAudioService;
+
+ private Toast mToast;
+ private View mView;
+ private TextView mMessage;
+ private ImageView mOtherStreamIcon;
+ private ImageView mRingerStreamIcon;
+ private ProgressBar mLevel;
+
+ // Synchronize when accessing this
+ private ToneGenerator mToneGenerators[];
+ private Vibrator mVibrator;
+
+ public VolumePanel(Context context, AudioService volumeService) {
+ mContext = context;
+ mAudioService = volumeService;
+ mToast = new Toast(context);
+
+ RINGTONE_VOLUME_TEXT = context.getString(com.android.internal.R.string.volume_ringtone);
+ MUSIC_VOLUME_TEXT = context.getString(com.android.internal.R.string.volume_music);
+ INCALL_VOLUME_TEXT = context.getString(com.android.internal.R.string.volume_call);
+ ALARM_VOLUME_TEXT = context.getString(com.android.internal.R.string.volume_alarm);
+ UNKNOWN_VOLUME_TEXT = context.getString(com.android.internal.R.string.volume_unknown);
+
+ LayoutInflater inflater = (LayoutInflater) context
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View view = mView = inflater.inflate(com.android.internal.R.layout.volume_adjust, null);
+ mMessage = (TextView) view.findViewById(com.android.internal.R.id.message);
+ mOtherStreamIcon = (ImageView) view.findViewById(com.android.internal.R.id.other_stream_icon);
+ mRingerStreamIcon = (ImageView) view.findViewById(com.android.internal.R.id.ringer_stream_icon);
+ mLevel = (ProgressBar) view.findViewById(com.android.internal.R.id.level);
+
+ mToneGenerators = new ToneGenerator[AudioManager.NUM_STREAMS];
+ mVibrator = new Vibrator();
+ }
+
+ public void postVolumeChanged(int streamType, int flags) {
+ if (hasMessages(MSG_VOLUME_CHANGED)) return;
+ removeMessages(MSG_FREE_RESOURCES);
+ obtainMessage(MSG_VOLUME_CHANGED, streamType, flags).sendToTarget();
+ }
+
+ /**
+ * Override this if you have other work to do when the volume changes (for
+ * example, vibrating, playing a sound, etc.). Make sure to call through to
+ * the superclass implementation.
+ */
+ protected void onVolumeChanged(int streamType, int flags) {
+
+ if (LOGD) Log.d(TAG, "onVolumeChanged(streamType: " + streamType + ", flags: " + flags + ")");
+
+ if ((flags & AudioManager.FLAG_SHOW_UI) != 0) {
+ onShowVolumeChanged(streamType, flags);
+ }
+
+ if ((flags & AudioManager.FLAG_PLAY_SOUND) != 0) {
+ removeMessages(MSG_PLAY_SOUND);
+ sendMessageDelayed(obtainMessage(MSG_PLAY_SOUND, streamType, flags), PLAY_SOUND_DELAY);
+ }
+
+ if ((flags & AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE) != 0) {
+ removeMessages(MSG_PLAY_SOUND);
+ removeMessages(MSG_VIBRATE);
+ onStopSounds();
+ }
+
+ removeMessages(MSG_FREE_RESOURCES);
+ sendMessageDelayed(obtainMessage(MSG_FREE_RESOURCES), FREE_DELAY);
+ }
+
+ protected void onShowVolumeChanged(int streamType, int flags) {
+ int index = mAudioService.getStreamVolume(streamType);
+ String message = UNKNOWN_VOLUME_TEXT;
+
+ if (LOGD) {
+ Log.d(TAG, "onShowVolumeChanged(streamType: " + streamType
+ + ", flags: " + flags + "), index: " + index);
+ }
+
+ switch (streamType) {
+
+ case AudioManager.STREAM_RING: {
+ message = RINGTONE_VOLUME_TEXT;
+ setRingerIcon(index);
+ break;
+ }
+
+ case AudioManager.STREAM_MUSIC: {
+ message = MUSIC_VOLUME_TEXT;
+ setOtherIcon(index);
+ break;
+ }
+
+ case AudioManager.STREAM_VOICE_CALL: {
+ message = INCALL_VOLUME_TEXT;
+ /*
+ * For in-call voice call volume, there is no inaudible volume
+ * level, so never show the mute icon
+ */
+ setOtherIcon(index == 0 ? 1 : index);
+ break;
+ }
+
+ case AudioManager.STREAM_ALARM: {
+ message = ALARM_VOLUME_TEXT;
+ setOtherIcon(index);
+ break;
+ }
+ }
+
+ if (!mMessage.getText().equals(message)) {
+ mMessage.setText(message);
+ }
+
+ int max = mAudioService.getStreamMaxVolume(streamType);
+ if (max != mLevel.getMax()) {
+ mLevel.setMax(max);
+ }
+ mLevel.setProgress(index);
+
+ mToast.setView(mView);
+ mToast.setDuration(Toast.LENGTH_SHORT);
+ mToast.setGravity(Gravity.TOP, 0, 0);
+ mToast.show();
+
+ // Do a little vibrate if applicable (only when going into vibrate mode)
+ if ((flags & AudioManager.FLAG_VIBRATE) != 0 &&
+ mAudioService.isStreamAffectedByRingerMode(streamType) &&
+ mAudioService.getRingerMode() == AudioManager.RINGER_MODE_VIBRATE &&
+ mAudioService.shouldVibrate(AudioManager.VIBRATE_TYPE_RINGER)) {
+ sendMessageDelayed(obtainMessage(MSG_VIBRATE), VIBRATE_DELAY);
+ }
+
+ }
+
+ protected void onPlaySound(int streamType, int flags) {
+
+ if (hasMessages(MSG_STOP_SOUNDS)) {
+ removeMessages(MSG_STOP_SOUNDS);
+ // Force stop right now
+ onStopSounds();
+ }
+
+ synchronized (this) {
+ ToneGenerator toneGen = getOrCreateToneGenerator(streamType);
+ toneGen.startTone(ToneGenerator.TONE_PROP_BEEP);
+ }
+
+ sendMessageDelayed(obtainMessage(MSG_STOP_SOUNDS), BEEP_DURATION);
+ }
+
+ protected void onStopSounds() {
+
+ synchronized (this) {
+ for (int i = AudioManager.NUM_STREAMS - 1; i >= 0; i--) {
+ ToneGenerator toneGen = mToneGenerators[i];
+ if (toneGen != null) {
+ toneGen.stopTone();
+ }
+ }
+ }
+ }
+
+ protected void onVibrate() {
+
+ // Make sure we ended up in vibrate ringer mode
+ if (mAudioService.getRingerMode() != AudioManager.RINGER_MODE_VIBRATE) {
+ return;
+ }
+
+ mVibrator.vibrate(VIBRATE_DURATION);
+ }
+
+ /**
+ * Lock on this VolumePanel instance as long as you use the returned ToneGenerator.
+ */
+ private ToneGenerator getOrCreateToneGenerator(int streamType) {
+ synchronized (this) {
+ if (mToneGenerators[streamType] == null) {
+ return mToneGenerators[streamType] = new ToneGenerator(streamType, MAX_VOLUME);
+ } else {
+ return mToneGenerators[streamType];
+ }
+ }
+ }
+
+ private void setOtherIcon(int index) {
+ mRingerStreamIcon.setVisibility(View.GONE);
+ mOtherStreamIcon.setVisibility(View.VISIBLE);
+
+ mOtherStreamIcon.setImageResource(index == 0
+ ? com.android.internal.R.drawable.ic_volume_off_small
+ : com.android.internal.R.drawable.ic_volume_small);
+ }
+
+ private void setRingerIcon(int index) {
+ mOtherStreamIcon.setVisibility(View.GONE);
+ mRingerStreamIcon.setVisibility(View.VISIBLE);
+
+ int ringerMode = mAudioService.getRingerMode();
+ int icon;
+
+ if (LOGD) Log.d(TAG, "setRingerIcon(index: " + index+ "), ringerMode: " + ringerMode);
+
+ if (ringerMode == AudioManager.RINGER_MODE_SILENT) {
+ icon = com.android.internal.R.drawable.ic_volume_off;
+ } else if (ringerMode == AudioManager.RINGER_MODE_VIBRATE) {
+ icon = com.android.internal.R.drawable.ic_vibrate;
+ } else {
+ icon = com.android.internal.R.drawable.ic_volume;
+ }
+ mRingerStreamIcon.setImageResource(icon);
+ }
+
+ protected void onFreeResources() {
+ // We'll keep the views, just ditch the cached drawable and hence
+ // bitmaps
+ mOtherStreamIcon.setImageDrawable(null);
+ mRingerStreamIcon.setImageDrawable(null);
+
+ synchronized (this) {
+ for (int i = mToneGenerators.length - 1; i >= 0; i--) {
+ if (mToneGenerators[i] != null) {
+ mToneGenerators[i].release();
+ }
+ mToneGenerators[i] = null;
+ }
+ }
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+
+ case MSG_VOLUME_CHANGED: {
+ onVolumeChanged(msg.arg1, msg.arg2);
+ break;
+ }
+
+ case MSG_FREE_RESOURCES: {
+ onFreeResources();
+ break;
+ }
+
+ case MSG_STOP_SOUNDS: {
+ onStopSounds();
+ break;
+ }
+
+ case MSG_PLAY_SOUND: {
+ onPlaySound(msg.arg1, msg.arg2);
+ break;
+ }
+
+ case MSG_VIBRATE: {
+ onVibrate();
+ break;
+ }
+
+ }
+ }
+
+}
diff --git a/core/java/android/view/Window.java b/core/java/android/view/Window.java
new file mode 100644
index 0000000..4aeab2d
--- /dev/null
+++ b/core/java/android/view/Window.java
@@ -0,0 +1,962 @@
+/*
+ * 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.view;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.TypedArray;
+import android.graphics.PixelFormat;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.IBinder;
+
+/**
+ * Abstract base class for a top-level window look and behavior policy. An
+ * instance of this class should be used as the top-level view added to the
+ * window manager. It provides standard UI policies such as a background, title
+ * area, default key processing, etc.
+ *
+ * <p>The only existing implementation of this abstract class is
+ * android.policy.PhoneWindow, which you should instantiate when needing a
+ * Window. Eventually that class will be refactored and a factory method
+ * added for creating Window instances without knowing about a particular
+ * implementation.
+ */
+public abstract class Window {
+ /** Flag for the "options panel" feature. This is enabled by default. */
+ public static final int FEATURE_OPTIONS_PANEL = 0;
+ /** Flag for the "no title" feature, turning off the title at the top
+ * of the screen. */
+ public static final int FEATURE_NO_TITLE = 1;
+ /** Flag for the progress indicator feature */
+ public static final int FEATURE_PROGRESS = 2;
+ /** Flag for having an icon on the left side of the title bar */
+ public static final int FEATURE_LEFT_ICON = 3;
+ /** Flag for having an icon on the right side of the title bar */
+ public static final int FEATURE_RIGHT_ICON = 4;
+ /** Flag for indeterminate progress */
+ public static final int FEATURE_INDETERMINATE_PROGRESS = 5;
+ /** Flag for the context menu. This is enabled by default. */
+ public static final int FEATURE_CONTEXT_MENU = 6;
+ /** Flag for custom title. You cannot combine this feature with other title features. */
+ public static final int FEATURE_CUSTOM_TITLE = 7;
+ /* Flag for asking for an OpenGL enabled window.
+ All 2D graphics will be handled by OpenGL ES.
+ Private for now, until it is better tested (not shipping in 1.0)
+ */
+ private static final int FEATURE_OPENGL = 8;
+ /** Flag for setting the progress bar's visibility to VISIBLE */
+ public static final int PROGRESS_VISIBILITY_ON = -1;
+ /** Flag for setting the progress bar's visibility to GONE */
+ public static final int PROGRESS_VISIBILITY_OFF = -2;
+ /** Flag for setting the progress bar's indeterminate mode on */
+ public static final int PROGRESS_INDETERMINATE_ON = -3;
+ /** Flag for setting the progress bar's indeterminate mode off */
+ public static final int PROGRESS_INDETERMINATE_OFF = -4;
+ /** Starting value for the (primary) progress */
+ public static final int PROGRESS_START = 0;
+ /** Ending value for the (primary) progress */
+ public static final int PROGRESS_END = 10000;
+ /** Lowest possible value for the secondary progress */
+ public static final int PROGRESS_SECONDARY_START = 20000;
+ /** Highest possible value for the secondary progress */
+ public static final int PROGRESS_SECONDARY_END = 30000;
+
+ /** The default features enabled */
+ @SuppressWarnings({"PointlessBitwiseExpression"})
+ protected static final int DEFAULT_FEATURES = (1 << FEATURE_OPTIONS_PANEL) |
+ (1 << FEATURE_CONTEXT_MENU);
+
+ /**
+ * The ID that the main layout in the XML layout file should have.
+ */
+ public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;
+
+ private final Context mContext;
+
+ private TypedArray mWindowStyle;
+ private Callback mCallback;
+ private WindowManager mWindowManager;
+ private IBinder mAppToken;
+ private String mAppName;
+ private Window mContainer;
+ private Window mActiveChild;
+ private boolean mIsActive = false;
+ private boolean mHasChildren = false;
+ private int mForcedWindowFlags = 0;
+
+ private int mFeatures = DEFAULT_FEATURES;
+ private int mLocalFeatures = DEFAULT_FEATURES;
+
+ private boolean mHaveWindowFormat = false;
+ private int mDefaultWindowFormat = PixelFormat.OPAQUE;
+
+ // The current window attributes.
+ private final WindowManager.LayoutParams mWindowAttributes =
+ new WindowManager.LayoutParams();
+
+ /**
+ * API from a Window back to its caller. This allows the client to
+ * intercept key dispatching, panels and menus, etc.
+ */
+ public interface Callback {
+ /**
+ * Called to process key events. At the very least your
+ * implementation must call
+ * {@link android.view.Window#superDispatchKeyEvent} to do the
+ * standard key processing.
+ *
+ * @param event The key event.
+ *
+ * @return boolean Return true if this event was consumed.
+ */
+ public boolean dispatchKeyEvent(KeyEvent event);
+
+ /**
+ * Called to process touch screen events. At the very least your
+ * implementation must call
+ * {@link android.view.Window#superDispatchTouchEvent} to do the
+ * standard touch screen processing.
+ *
+ * @param event The touch screen event.
+ *
+ * @return boolean Return true if this event was consumed.
+ */
+ public boolean dispatchTouchEvent(MotionEvent event);
+
+ /**
+ * Called to process trackball events. At the very least your
+ * implementation must call
+ * {@link android.view.Window#superDispatchTrackballEvent} to do the
+ * standard trackball processing.
+ *
+ * @param event The trackball event.
+ *
+ * @return boolean Return true if this event was consumed.
+ */
+ public boolean dispatchTrackballEvent(MotionEvent event);
+
+ /**
+ * Instantiate the view to display in the panel for 'featureId'.
+ * You can return null, in which case the default content (typically
+ * a menu) will be created for you.
+ *
+ * @param featureId Which panel is being created.
+ *
+ * @return view The top-level view to place in the panel.
+ *
+ * @see #onPreparePanel
+ */
+ public View onCreatePanelView(int featureId);
+
+ /**
+ * Initialize the contents of the menu for panel 'featureId'. This is
+ * called if onCreatePanelView() returns null, giving you a standard
+ * menu in which you can place your items. It is only called once for
+ * the panel, the first time it is shown.
+ *
+ * <p>You can safely hold on to <var>menu</var> (and any items created
+ * from it), making modifications to it as desired, until the next
+ * time onCreatePanelMenu() is called for this feature.
+ *
+ * @param featureId The panel being created.
+ * @param menu The menu inside the panel.
+ *
+ * @return boolean You must return true for the panel to be displayed;
+ * if you return false it will not be shown.
+ */
+ public boolean onCreatePanelMenu(int featureId, Menu menu);
+
+ /**
+ * Prepare a panel to be displayed. This is called right before the
+ * panel window is shown, every time it is shown.
+ *
+ * @param featureId The panel that is being displayed.
+ * @param view The View that was returned by onCreatePanelView().
+ * @param menu If onCreatePanelView() returned null, this is the Menu
+ * being displayed in the panel.
+ *
+ * @return boolean You must return true for the panel to be displayed;
+ * if you return false it will not be shown.
+ *
+ * @see #onCreatePanelView
+ */
+ public boolean onPreparePanel(int featureId, View view, Menu menu);
+
+ /**
+ * Called when a panel's menu is opened by the user. This may also be
+ * called when the menu is changing from one type to another (for
+ * example, from the icon menu to the expanded menu).
+ *
+ * @param featureId The panel that the menu is in.
+ * @param menu The menu that is opened.
+ * @return Return true to allow the menu to open, or false to prevent
+ * the menu from opening.
+ */
+ public boolean onMenuOpened(int featureId, Menu menu);
+
+ /**
+ * Called when a panel's menu item has been selected by the user.
+ *
+ * @param featureId The panel that the menu is in.
+ * @param item The menu item that was selected.
+ *
+ * @return boolean Return true to finish processing of selection, or
+ * false to perform the normal menu handling (calling its
+ * Runnable or sending a Message to its target Handler).
+ */
+ public boolean onMenuItemSelected(int featureId, MenuItem item);
+
+ /**
+ * This is called whenever the current window attributes change.
+ *
+
+ */
+ public void onWindowAttributesChanged(WindowManager.LayoutParams attrs);
+
+ /**
+ * This hook is called whenever the content view of the screen changes
+ * (due to a call to
+ * {@link Window#setContentView(View, android.view.ViewGroup.LayoutParams)
+ * Window.setContentView} or
+ * {@link Window#addContentView(View, android.view.ViewGroup.LayoutParams)
+ * Window.addContentView}).
+ */
+ public void onContentChanged();
+
+ /**
+ * This hook is called whenever the window focus changes.
+ *
+ * @param hasFocus Whether the window now has focus.
+ */
+ public void onWindowFocusChanged(boolean hasFocus);
+
+ /**
+ * Called when a panel is being closed. If another logical subsequent
+ * panel is being opened (and this panel is being closed to make room for the subsequent
+ * panel), this method will NOT be called.
+ *
+ * @param featureId The panel that is being displayed.
+ * @param menu If onCreatePanelView() returned null, this is the Menu
+ * being displayed in the panel.
+ */
+ public void onPanelClosed(int featureId, Menu menu);
+
+ /**
+ * Called when the user signals the desire to start a search.
+ *
+ * @return true if search launched, false if activity refuses (blocks)
+ *
+ * @see android.app.Activity#onSearchRequested()
+ */
+ public boolean onSearchRequested();
+ }
+
+ public Window(Context context) {
+ mContext = context;
+ }
+
+ /**
+ * Return the Context this window policy is running in, for retrieving
+ * resources and other information.
+ *
+ * @return Context The Context that was supplied to the constructor.
+ */
+ public final Context getContext() {
+ return mContext;
+ }
+
+ /**
+ * Return the {@link android.R.styleable#Window} attributes from this
+ * window's theme.
+ */
+ public final TypedArray getWindowStyle() {
+ synchronized (this) {
+ if (mWindowStyle == null) {
+ mWindowStyle = mContext.obtainStyledAttributes(
+ com.android.internal.R.styleable.Window);
+ }
+ return mWindowStyle;
+ }
+ }
+
+ /**
+ * Set the container for this window. If not set, the DecorWindow
+ * operates as a top-level window; otherwise, it negotiates with the
+ * container to display itself appropriately.
+ *
+ * @param container The desired containing Window.
+ */
+ public void setContainer(Window container) {
+ mContainer = container;
+ if (container != null) {
+ // Embedded screens never have a title.
+ mFeatures |= 1<<FEATURE_NO_TITLE;
+ mLocalFeatures |= 1<<FEATURE_NO_TITLE;
+ container.mHasChildren = true;
+ }
+ }
+
+ /**
+ * Return the container for this Window.
+ *
+ * @return Window The containing window, or null if this is a
+ * top-level window.
+ */
+ public final Window getContainer() {
+ return mContainer;
+ }
+
+ public final boolean hasChildren() {
+ return mHasChildren;
+ }
+
+ /**
+ * Set the window manager for use by this Window to, for example,
+ * display panels. This is <em>not</em> used for displaying the
+ * Window itself -- that must be done by the client.
+ *
+ * @param wm The ViewManager for adding new windows.
+ */
+ public void setWindowManager(WindowManager wm,
+ IBinder appToken, String appName) {
+ mAppToken = appToken;
+ mAppName = appName;
+ if (wm == null) {
+ wm = WindowManagerImpl.getDefault();
+ }
+ mWindowManager = new LocalWindowManager(wm);
+ }
+
+ private class LocalWindowManager implements WindowManager {
+ LocalWindowManager(WindowManager wm) {
+ mWindowManager = wm;
+ }
+
+ public final void addView(View view, ViewGroup.LayoutParams params) {
+ // Let this throw an exception on a bad params.
+ WindowManager.LayoutParams wp = (WindowManager.LayoutParams)params;
+ CharSequence curTitle = wp.getTitle();
+ if (wp.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
+ wp.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
+ if (wp.token == null) {
+ View decor = peekDecorView();
+ if (decor != null) {
+ wp.token = decor.getWindowToken();
+ }
+ }
+ if (curTitle == null || curTitle.length() == 0) {
+ String title;
+ if (wp.type == WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA) {
+ title="Media";
+ } else if (wp.type == WindowManager.LayoutParams.TYPE_APPLICATION_PANEL) {
+ title="Panel";
+ } else {
+ title=Integer.toString(wp.type);
+ }
+ if (mAppName != null) {
+ title += ":" + mAppName;
+ }
+ wp.setTitle(title);
+ }
+ } else {
+ if (wp.token == null) {
+ wp.token = mContainer == null ? mAppToken : mContainer.mAppToken;
+ }
+ if ((curTitle == null || curTitle.length() == 0)
+ && mAppName != null) {
+ wp.setTitle(mAppName);
+ }
+ if (wp.windowAnimations == 0) {
+ wp.windowAnimations = getWindowStyle().getResourceId(
+ com.android.internal.R.styleable.Window_windowAnimationStyle, 0);
+ }
+ }
+ if (wp.packageName == null) {
+ wp.packageName = mContext.getPackageName();
+ }
+ mWindowManager.addView(view, params);
+ }
+
+ public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
+ mWindowManager.updateViewLayout(view, params);
+ }
+
+ public final void removeView(View view) {
+ mWindowManager.removeView(view);
+ }
+
+ public final void removeViewImmediate(View view) {
+ mWindowManager.removeViewImmediate(view);
+ }
+
+ public Display getDefaultDisplay() {
+ return mWindowManager.getDefaultDisplay();
+ }
+
+ WindowManager mWindowManager;
+ }
+
+ /**
+ * Return the window manager allowing this Window to display its own
+ * windows.
+ *
+ * @return WindowManager The ViewManager.
+ */
+ public WindowManager getWindowManager() {
+ return mWindowManager;
+ }
+
+ /**
+ * Set the Callback interface for this window, used to intercept key
+ * events and other dynamic operations in the window.
+ *
+ * @param callback The desired Callback interface.
+ */
+ public void setCallback(Callback callback) {
+ mCallback = callback;
+ }
+
+ /**
+ * Return the current Callback interface for this window.
+ */
+ public final Callback getCallback() {
+ return mCallback;
+ }
+
+ /**
+ * Return whether this window is being displayed with a floating style
+ * (based on the {@link android.R.attr#windowIsFloating} attribute in
+ * the style/theme).
+ *
+ * @return Returns true if the window is configured to be displayed floating
+ * on top of whatever is behind it.
+ */
+ public abstract boolean isFloating();
+
+ /**
+ * Set the width and height layout parameters of the window. The default
+ * for both of these is FILL_PARENT; you can change them to WRAP_CONTENT to
+ * make a window that is not full-screen.
+ *
+ * @param width The desired layout width of the window.
+ * @param height The desired layout height of the window.
+ */
+ public void setLayout(int width, int height)
+ {
+ final WindowManager.LayoutParams attrs = getAttributes();
+ attrs.width = width;
+ attrs.height = height;
+ if (mCallback != null) {
+ mCallback.onWindowAttributesChanged(attrs);
+ }
+ }
+
+ /**
+ * Set the gravity of the window, as per the Gravity constants. This
+ * controls how the window manager is positioned in the overall window; it
+ * is only useful when using WRAP_CONTENT for the layout width or height.
+ *
+ * @param gravity The desired gravity constant.
+ *
+ * @see Gravity
+ * @see #setLayout
+ */
+ public void setGravity(int gravity)
+ {
+ final WindowManager.LayoutParams attrs = getAttributes();
+ attrs.gravity = gravity;
+ if (mCallback != null) {
+ mCallback.onWindowAttributesChanged(attrs);
+ }
+ }
+
+ /**
+ * Set the type of the window, as per the WindowManager.LayoutParams
+ * types.
+ *
+ * @param type The new window type (see WindowManager.LayoutParams).
+ */
+ public void setType(int type) {
+ final WindowManager.LayoutParams attrs = getAttributes();
+ attrs.type = type;
+ if (mCallback != null) {
+ mCallback.onWindowAttributesChanged(attrs);
+ }
+ }
+
+ /**
+ * Set the format of window, as per the PixelFormat types. This overrides
+ * the default format that is selected by the Window based on its
+ * window decorations.
+ *
+ * @param format The new window format (see PixelFormat). Use
+ * PixelFormat.UNKNOWN to allow the Window to select
+ * the format.
+ *
+ * @see PixelFormat
+ */
+ public void setFormat(int format) {
+ final WindowManager.LayoutParams attrs = getAttributes();
+ if (format != PixelFormat.UNKNOWN) {
+ attrs.format = format;
+ mHaveWindowFormat = true;
+ } else {
+ attrs.format = mDefaultWindowFormat;
+ mHaveWindowFormat = false;
+ }
+ if (mCallback != null) {
+ mCallback.onWindowAttributesChanged(attrs);
+ }
+ }
+
+ /**
+ * Convenience function to set the flag bits as specified in flags, as
+ * per {@link #setFlags}.
+ * @param flags The flag bits to be set.
+ * @see #setFlags
+ */
+ public void addFlags(int flags) {
+ setFlags(flags, flags);
+ }
+
+ /**
+ * Convenience function to clear the flag bits as specified in flags, as
+ * per {@link #setFlags}.
+ * @param flags The flag bits to be cleared.
+ * @see #setFlags
+ */
+ public void clearFlags(int flags) {
+ setFlags(0, flags);
+ }
+
+ /**
+ * Set the flags of the window, as per the
+ * {@link WindowManager.LayoutParams WindowManager.LayoutParams}
+ * flags.
+ *
+ * <p>Note that some flags must be set before the window decoration is
+ * created (by the first call to
+ * {@link #setContentView(View, android.view.ViewGroup.LayoutParams)} or
+ * {@link #getDecorView()}:
+ * {@link WindowManager.LayoutParams#FLAG_LAYOUT_IN_SCREEN} and
+ * {@link WindowManager.LayoutParams#FLAG_LAYOUT_INSET_DECOR}. These
+ * will be set for you based on the {@link android.R.attr#windowIsFloating}
+ * attribute.
+ *
+ * @param flags The new window flags (see WindowManager.LayoutParams).
+ * @param mask Which of the window flag bits to modify.
+ */
+ public void setFlags(int flags, int mask) {
+ final WindowManager.LayoutParams attrs = getAttributes();
+ attrs.flags = (attrs.flags&~mask) | (flags&mask);
+ mForcedWindowFlags |= mask;
+ if (mCallback != null) {
+ mCallback.onWindowAttributesChanged(attrs);
+ }
+ }
+
+ /**
+ * Specify custom window attributes.
+ *
+ * @param a The new window attributes, which will completely override any
+ * current values.
+ */
+ public void setAttributes(WindowManager.LayoutParams a) {
+ mWindowAttributes.copyFrom(a);
+ mForcedWindowFlags = 0xffffffff;
+ if (mCallback != null) {
+ mCallback.onWindowAttributesChanged(mWindowAttributes);
+ }
+ }
+
+ /**
+ * Retrieve the current window attributes associated with this panel.
+ *
+ * @return WindowManager.LayoutParams Either the existing window
+ * attributes object, or a freshly created one if there is none.
+ */
+ public final WindowManager.LayoutParams getAttributes() {
+ return mWindowAttributes;
+ }
+
+ /**
+ * Return the window flags that have been explicitly set by the client,
+ * so will not be modified by {@link #getDecorView}.
+ */
+ protected final int getForcedWindowFlags() {
+ return mForcedWindowFlags;
+ }
+
+ /**
+ * Enable extended screen features. This must be called before
+ * setContentView(). May be called as many times as desired as long as it
+ * is before setContentView(). If not called, no extended features
+ * will be available. You can not turn off a feature once it is requested.
+ * You canot use other title features with {@link #FEATURE_CUSTOM_TITLE}.
+ *
+ * @param featureId The desired features, defined as constants by Window.
+ * @return The features that are now set.
+ */
+ public boolean requestFeature(int featureId) {
+ final int flag = 1<<featureId;
+ mFeatures |= flag;
+ mLocalFeatures |= mContainer != null ? (flag&~mContainer.mFeatures) : flag;
+ return (mFeatures&flag) != 0;
+ }
+
+ public final void makeActive() {
+ if (mContainer != null) {
+ if (mContainer.mActiveChild != null) {
+ mContainer.mActiveChild.mIsActive = false;
+ }
+ mContainer.mActiveChild = this;
+ }
+ mIsActive = true;
+ onActive();
+ }
+
+ public final boolean isActive()
+ {
+ return mIsActive;
+ }
+
+ /**
+ * Finds a view that was identified by the id attribute from the XML that
+ * was processed in {@link android.app.Activity#onCreate}. This will
+ * implicitly call {@link #getDecorView} for you, with all of the
+ * associated side-effects.
+ *
+ * @return The view if found or null otherwise.
+ */
+ public View findViewById(int id) {
+ return getDecorView().findViewById(id);
+ }
+
+ /**
+ * Convenience for
+ * {@link #setContentView(View, android.view.ViewGroup.LayoutParams)}
+ * to set the screen content from a layout resource. The resource will be
+ * inflated, adding all top-level views to the screen.
+ *
+ * @param layoutResID Resource ID to be inflated.
+ * @see #setContentView(View, android.view.ViewGroup.LayoutParams)
+ */
+ public abstract void setContentView(int layoutResID);
+
+ /**
+ * Convenience for
+ * {@link #setContentView(View, android.view.ViewGroup.LayoutParams)}
+ * set the screen content to an explicit view. This view is placed
+ * directly into the screen's view hierarchy. It can itself be a complex
+ * view hierarhcy.
+ *
+ * @param view The desired content to display.
+ * @see #setContentView(View, android.view.ViewGroup.LayoutParams)
+ */
+ public abstract void setContentView(View view);
+
+ /**
+ * Set the screen content to an explicit view. This view is placed
+ * directly into the screen's view hierarchy. It can itself be a complex
+ * view hierarchy.
+ *
+ * <p>Note that calling this function "locks in" various characteristics
+ * of the window that can not, from this point forward, be changed: the
+ * features that have been requested with {@link #requestFeature(int)},
+ * and certain window flags as described in {@link #setFlags(int, int)}.
+ *
+ * @param view The desired content to display.
+ * @param params Layout parameters for the view.
+ */
+ public abstract void setContentView(View view, ViewGroup.LayoutParams params);
+
+ /**
+ * Variation on
+ * {@link #setContentView(View, android.view.ViewGroup.LayoutParams)}
+ * to add an additional content view to the screen. Added after any existing
+ * ones in the screen -- existing views are NOT removed.
+ *
+ * @param view The desired content to display.
+ * @param params Layout parameters for the view.
+ */
+ public abstract void addContentView(View view, ViewGroup.LayoutParams params);
+
+ /**
+ * Return the view in this Window that currently has focus, or null if
+ * there are none. Note that this does not look in any containing
+ * Window.
+ *
+ * @return View The current View with focus or null.
+ */
+ public abstract View getCurrentFocus();
+
+ /**
+ * Quick access to the {@link LayoutInflater} instance that this Window
+ * retrieved from its Context.
+ *
+ * @return LayoutInflater The shared LayoutInflater.
+ */
+ public abstract LayoutInflater getLayoutInflater();
+
+ public abstract void setTitle(CharSequence title);
+
+ public abstract void setTitleColor(int textColor);
+
+ public abstract void openPanel(int featureId, KeyEvent event);
+
+ public abstract void closePanel(int featureId);
+
+ public abstract void togglePanel(int featureId, KeyEvent event);
+
+ public abstract boolean performPanelShortcut(int featureId,
+ int keyCode,
+ KeyEvent event,
+ int flags);
+ public abstract boolean performPanelIdentifierAction(int featureId,
+ int id,
+ int flags);
+
+ public abstract void closeAllPanels();
+
+ public abstract boolean performContextMenuIdentifierAction(int id, int flags);
+
+ /**
+ * Should be called when the configuration is changed.
+ *
+ * @param newConfig The new configuration.
+ */
+ public abstract void onConfigurationChanged(Configuration newConfig);
+
+ /**
+ * Change the background of this window to a Drawable resource. Setting the
+ * background to null will make the window be opaque. To make the window
+ * transparent, you can use an empty drawable (for instance a ColorDrawable
+ * with the color 0 or the system drawable android:drawable/empty.)
+ *
+ * @param resid The resource identifier of a drawable resource which will be
+ * installed as the new background.
+ */
+ public void setBackgroundDrawableResource(int resid)
+ {
+ setBackgroundDrawable(mContext.getResources().getDrawable(resid));
+ }
+
+ /**
+ * Change the background of this window to a custom Drawable. Setting the
+ * background to null will make the window be opaque. To make the window
+ * transparent, you can use an empty drawable (for instance a ColorDrawable
+ * with the color 0 or the system drawable android:drawable/empty.)
+ *
+ * @param drawable The new Drawable to use for this window's background.
+ */
+ public abstract void setBackgroundDrawable(Drawable drawable);
+
+ /**
+ * Set the value for a drawable feature of this window, from a resource
+ * identifier. You must have called requestFeauture(featureId) before
+ * calling this function.
+ *
+ * @see android.content.res.Resources#getDrawable(int)
+ *
+ * @param featureId The desired drawable feature to change, defined as a
+ * constant by Window.
+ * @param resId Resource identifier of the desired image.
+ */
+ public abstract void setFeatureDrawableResource(int featureId, int resId);
+
+ /**
+ * Set the value for a drawable feature of this window, from a URI. You
+ * must have called requestFeature(featureId) before calling this
+ * function.
+ *
+ * <p>The only URI currently supported is "content:", specifying an image
+ * in a content provider.
+ *
+ * @see android.widget.ImageView#setImageURI
+ *
+ * @param featureId The desired drawable feature to change. Features are
+ * constants defined by Window.
+ * @param uri The desired URI.
+ */
+ public abstract void setFeatureDrawableUri(int featureId, Uri uri);
+
+ /**
+ * Set an explicit Drawable value for feature of this window. You must
+ * have called requestFeature(featureId) before calling this function.
+ *
+ * @param featureId The desired drawable feature to change.
+ * Features are constants defined by Window.
+ * @param drawable A Drawable object to display.
+ */
+ public abstract void setFeatureDrawable(int featureId, Drawable drawable);
+
+ /**
+ * Set a custom alpha value for the given drawale feature, controlling how
+ * much the background is visible through it.
+ *
+ * @param featureId The desired drawable feature to change.
+ * Features are constants defined by Window.
+ * @param alpha The alpha amount, 0 is completely transparent and 255 is
+ * completely opaque.
+ */
+ public abstract void setFeatureDrawableAlpha(int featureId, int alpha);
+
+ /**
+ * Set the integer value for a feature. The range of the value depends on
+ * the feature being set. For FEATURE_PROGRESSS, it should go from 0 to
+ * 10000. At 10000 the progress is complete and the indicator hidden.
+ *
+ * @param featureId The desired feature to change.
+ * Features are constants defined by Window.
+ * @param value The value for the feature. The interpretation of this
+ * value is feature-specific.
+ */
+ public abstract void setFeatureInt(int featureId, int value);
+
+ /**
+ * Request that key events come to this activity. Use this if your
+ * activity has no views with focus, but the activity still wants
+ * a chance to process key events.
+ */
+ public abstract void takeKeyEvents(boolean get);
+
+ /**
+ * Used by custom windows, such as Dialog, to pass the key press event
+ * further down the view hierarchy. Application developers should
+ * not need to implement or call this.
+ *
+ */
+ public abstract boolean superDispatchKeyEvent(KeyEvent event);
+
+ /**
+ * Used by custom windows, such as Dialog, to pass the touch screen event
+ * further down the view hierarchy. Application developers should
+ * not need to implement or call this.
+ *
+ */
+ public abstract boolean superDispatchTouchEvent(MotionEvent event);
+
+ /**
+ * Used by custom windows, such as Dialog, to pass the trackball event
+ * further down the view hierarchy. Application developers should
+ * not need to implement or call this.
+ *
+ */
+ public abstract boolean superDispatchTrackballEvent(MotionEvent event);
+
+ /**
+ * Retrieve the top-level window decor view (containing the standard
+ * window frame/decorations and the client's content inside of that), which
+ * can be added as a window to the window manager.
+ *
+ * <p><em>Note that calling this function for the first time "locks in"
+ * various window characteristics as described in
+ * {@link #setContentView(View, android.view.ViewGroup.LayoutParams)}.</em></p>
+ *
+ * @return Returns the top-level window decor view.
+ */
+ public abstract View getDecorView();
+
+ /**
+ * Retrieve the current decor view, but only if it has already been created;
+ * otherwise returns null.
+ *
+ * @return Returns the top-level window decor or null.
+ * @see #getDecorView
+ */
+ public abstract View peekDecorView();
+
+ public abstract Bundle saveHierarchyState();
+
+ public abstract void restoreHierarchyState(Bundle savedInstanceState);
+
+ protected abstract void onActive();
+
+ /**
+ * Return the feature bits that are enabled. This is the set of features
+ * that were given to requestFeature(), and are being handled by this
+ * Window itself or its container. That is, it is the set of
+ * requested features that you can actually use.
+ *
+ * <p>To do: add a public version of this API that allows you to check for
+ * features by their feature ID.
+ *
+ * @return int The feature bits.
+ */
+ protected final int getFeatures()
+ {
+ return mFeatures;
+ }
+
+ /**
+ * Return the feature bits that are being implemented by this Window.
+ * This is the set of features that were given to requestFeature(), and are
+ * being handled by only this Window itself, not by its containers.
+ *
+ * @return int The feature bits.
+ */
+ protected final int getLocalFeatures()
+ {
+ return mLocalFeatures;
+ }
+
+ /**
+ * Set the default format of window, as per the PixelFormat types. This
+ * is the format that will be used unless the client specifies in explicit
+ * format with setFormat();
+ *
+ * @param format The new window format (see PixelFormat).
+ *
+ * @see #setFormat
+ * @see PixelFormat
+ */
+ protected void setDefaultWindowFormat(int format)
+ {
+ mDefaultWindowFormat = format;
+ if (!mHaveWindowFormat) {
+ final WindowManager.LayoutParams attrs = getAttributes();
+ attrs.format = format;
+ if (mCallback != null) {
+ mCallback.onWindowAttributesChanged(attrs);
+ }
+ }
+ }
+
+ public abstract void setChildDrawable(int featureId, Drawable drawable);
+
+ public abstract void setChildInt(int featureId, int value);
+
+ /**
+ * Is a keypress one of the defined shortcut keys for this window.
+ * @param keyCode the key code from {@link android.view.KeyEvent} to check.
+ * @param event the {@link android.view.KeyEvent} to use to help check.
+ */
+ public abstract boolean isShortcutKey(int keyCode, KeyEvent event);
+
+ /**
+ * @see android.app.Activity#setVolumeControlStream(int)
+ */
+ public abstract void setVolumeControlStream(int streamType);
+
+ /**
+ * @see android.app.Activity#getVolumeControlStream()
+ */
+ public abstract int getVolumeControlStream();
+
+}
diff --git a/core/java/android/view/WindowManager.aidl b/core/java/android/view/WindowManager.aidl
new file mode 100755
index 0000000..556dc72
--- /dev/null
+++ b/core/java/android/view/WindowManager.aidl
@@ -0,0 +1,21 @@
+/* //device/java/android/android/view/WindowManager.aidl
+**
+** Copyright 2007, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.view;
+
+parcelable WindowManager.LayoutParams;
+
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
new file mode 100644
index 0000000..4c1dec5
--- /dev/null
+++ b/core/java/android/view/WindowManager.java
@@ -0,0 +1,693 @@
+/*
+ * 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.view;
+
+import android.graphics.PixelFormat;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.Log;
+
+
+/**
+ * The interface that apps use to talk to the window manager.
+ * <p>
+ * Use <code>Context.getSystemService(Context.WINDOW_SERVICE)</code> to get one of these.
+ *
+ * @see android.content.Context#getSystemService
+ * @see android.content.Context#WINDOW_SERVICE
+ */
+public interface WindowManager extends ViewManager {
+ /**
+ * Exception that is thrown when trying to add view whose
+ * {@link WindowManager.LayoutParams} {@link WindowManager.LayoutParams#token}
+ * is invalid.
+ */
+ public static class BadTokenException extends RuntimeException {
+ public BadTokenException() {
+ }
+
+ public BadTokenException(String name) {
+ super(name);
+ }
+ }
+
+ /**
+ * Use this method to get the default Display object.
+ *
+ * @return default Display object
+ */
+ public Display getDefaultDisplay();
+
+ /**
+ * Special variation of {@link #removeView} that immediately invokes
+ * the given view hierarchy's {@link View#onDetachedFromWindow()
+ * View.onDetachedFromWindow()} methods before returning. This is not
+ * for normal applications; using it correctly requires great care.
+ *
+ * @param view The view to be removed.
+ */
+ public void removeViewImmediate(View view);
+
+ public static class LayoutParams extends ViewGroup.LayoutParams
+ implements Parcelable {
+ /**
+ * X position for this window. With the default gravity it is ignored.
+ * When using {@link Gravity#LEFT} or {@link Gravity#RIGHT} it provides
+ * an offset from the given edge.
+ */
+ public int x;
+
+ /**
+ * Y position for this window. With the default gravity it is ignored.
+ * When using {@link Gravity#TOP} or {@link Gravity#BOTTOM} it provides
+ * an offset from the given edge.
+ */
+ public int y;
+
+ /**
+ * Indicates how much of the extra space will be allocated horizontally
+ * to the view associated with these LayoutParams. Specify 0 if the view
+ * should not be stretched. Otherwise the extra pixels will be pro-rated
+ * among all views whose weight is greater than 0.
+ */
+ public float horizontalWeight;
+
+ /**
+ * Indicates how much of the extra space will be allocated vertically
+ * to the view associated with these LayoutParams. Specify 0 if the view
+ * should not be stretched. Otherwise the extra pixels will be pro-rated
+ * among all views whose weight is greater than 0.
+ */
+ public float verticalWeight;
+
+ /**
+ * The general type of window. There are three main classes of
+ * window types:
+ * <ul>
+ * <li> <strong>Application windows</strong> (ranging from
+ * {@link #FIRST_APPLICATION_WINDOW} to
+ * {@link #LAST_APPLICATION_WINDOW}) are normal top-level application
+ * windows. For these types of windows, the {@link #token} must be
+ * set to the token of the activity they are a part of (this will
+ * normally be done for you if {@link #token} is null).
+ * <li> <strong>Sub-windows</strong> (ranging from
+ * {@link #FIRST_SUB_WINDOW} to
+ * {@link #LAST_SUB_WINDOW}) are associated with another top-level
+ * window. For these types of windows, the {@link #token} must be
+ * the token of the window it is attached to.
+ * <li> <strong>System windows</strong> (ranging from
+ * {@link #FIRST_SYSTEM_WINDOW} to
+ * {@link #LAST_SYSTEM_WINDOW}) are special types of windows for
+ * use by the system for specific purposes. They should not normally
+ * be used by applications, and a special permission is required
+ * to use them.
+ * </ul>
+ *
+ * @see #TYPE_BASE_APPLICATION
+ * @see #TYPE_APPLICATION
+ * @see #TYPE_APPLICATION_STARTING
+ * @see #TYPE_APPLICATION_PANEL
+ * @see #TYPE_APPLICATION_MEDIA
+ * @see #TYPE_APPLICATION_SUB_PANEL
+ * @see #TYPE_STATUS_BAR
+ * @see #TYPE_SEARCH_BAR
+ * @see #TYPE_PHONE
+ * @see #TYPE_SYSTEM_ALERT
+ * @see #TYPE_KEYGUARD
+ * @see #TYPE_TOAST
+ * @see #TYPE_SYSTEM_OVERLAY
+ * @see #TYPE_PRIORITY_PHONE
+ */
+ public int type;
+
+ /**
+ * Start of window types that represent normal application windows.
+ */
+ public static final int FIRST_APPLICATION_WINDOW = 1;
+
+ /**
+ * Window type: an application window that serves as the "base" window
+ * of the overall application; all other application windows will
+ * appear on top of it.
+ */
+ public static final int TYPE_BASE_APPLICATION = 1;
+
+ /**
+ * Window type: a normal application window. The {@link #token} must be
+ * an Activity token identifying who the window belongs to.
+ */
+ public static final int TYPE_APPLICATION = 2;
+
+ /**
+ * Window type: special application window that is displayed while the
+ * application is starting. Not for use by applications themselves;
+ * this is used by the system to display something until the
+ * application can show its own windows.
+ */
+ public static final int TYPE_APPLICATION_STARTING = 3;
+
+ /**
+ * End of types of application windows.
+ */
+ public static final int LAST_APPLICATION_WINDOW = 99;
+
+ /**
+ * Start of types of sub-windows. The {@link #token} of these windows
+ * must be set to the window they are attached to. These types of
+ * windows are kept next to their attached window in Z-order, and their
+ * coordinate space is relative to their attached window.
+ */
+ public static final int FIRST_SUB_WINDOW = 1000;
+
+ /**
+ * Window type: a panel on top of an application window. These windows
+ * appear on top of their attached window.
+ */
+ public static final int TYPE_APPLICATION_PANEL = FIRST_SUB_WINDOW;
+
+ /**
+ * Window type: window for showing media (e.g. video). These windows
+ * are displayed behind their attached window.
+ */
+ public static final int TYPE_APPLICATION_MEDIA = FIRST_SUB_WINDOW+1;
+
+ /**
+ * Window type: a sub-panel on top of an application window. These
+ * windows are displayed on top their attached window and any
+ * {@link #TYPE_APPLICATION_PANEL} panels.
+ */
+ public static final int TYPE_APPLICATION_SUB_PANEL = FIRST_SUB_WINDOW+2;
+
+ /**
+ * End of types of sub-windows.
+ */
+ public static final int LAST_SUB_WINDOW = 1999;
+
+ /**
+ * Start of system-specific window types. These are not normally
+ * created by applications.
+ */
+ public static final int FIRST_SYSTEM_WINDOW = 2000;
+
+ /**
+ * Window type: the status bar. There can be only one status bar
+ * window; it is placed at the top of the screen, and all other
+ * windows are shifted down so they are below it.
+ */
+ public static final int TYPE_STATUS_BAR = FIRST_SYSTEM_WINDOW;
+
+ /**
+ * Window type: the search bar. There can be only one search bar
+ * window; it is placed at the top of the screen.
+ */
+ public static final int TYPE_SEARCH_BAR = FIRST_SYSTEM_WINDOW+1;
+
+ /**
+ * Window type: phone. These are non-application windows providing
+ * user interaction with the phone (in particular incoming calls).
+ * These windows are normally placed above all applications, but behind
+ * the status bar.
+ */
+ public static final int TYPE_PHONE = FIRST_SYSTEM_WINDOW+2;
+
+ /**
+ * Window type: system window, such as low power alert. These windows
+ * are always on top of application windows.
+ */
+ public static final int TYPE_SYSTEM_ALERT = FIRST_SYSTEM_WINDOW+3;
+
+ /**
+ * Window type: keyguard window.
+ */
+ public static final int TYPE_KEYGUARD = FIRST_SYSTEM_WINDOW+4;
+
+ /**
+ * Window type: transient notifications.
+ */
+ public static final int TYPE_TOAST = FIRST_SYSTEM_WINDOW+5;
+
+ /**
+ * Window type: system overlay windows, which need to be displayed
+ * on top of everything else. These windows must not take input
+ * focus, or they will interfere with the keyguard.
+ */
+ public static final int TYPE_SYSTEM_OVERLAY = FIRST_SYSTEM_WINDOW+6;
+
+ /**
+ * Window type: priority phone UI, which needs to be displayed even if
+ * the keyguard is active. These windows must not take input
+ * focus, or they will interfere with the keyguard.
+ */
+ public static final int TYPE_PRIORITY_PHONE = FIRST_SYSTEM_WINDOW+7;
+
+ /**
+ * Window type: panel that slides out from the status bar
+ */
+ public static final int TYPE_STATUS_BAR_PANEL = FIRST_SYSTEM_WINDOW+8;
+
+ /**
+ * Window type: panel that slides out from the status bar
+ */
+ public static final int TYPE_SYSTEM_DIALOG = FIRST_SYSTEM_WINDOW+8;
+
+ /**
+ * Window type: dialogs that the keyguard shows
+ */
+ public static final int TYPE_KEYGUARD_DIALOG = FIRST_SYSTEM_WINDOW+9;
+
+ /**
+ * Window type: internal system error windows, appear on top of
+ * everything they can.
+ */
+ public static final int TYPE_SYSTEM_ERROR = FIRST_SYSTEM_WINDOW+10;
+
+ /**
+ * End of types of system windows.
+ */
+ public static final int LAST_SYSTEM_WINDOW = 2999;
+
+ /**
+ * Specifies what type of memory buffers should be used by this window.
+ * Default is normal.
+ *
+ * @see #MEMORY_TYPE_NORMAL
+ * @see #MEMORY_TYPE_HARDWARE
+ * @see #MEMORY_TYPE_GPU
+ * @see #MEMORY_TYPE_PUSH_BUFFERS
+ */
+ public int memoryType;
+
+ /** Memory type: The window's surface is allocated in main memory. */
+ public static final int MEMORY_TYPE_NORMAL = 0;
+ /** Memory type: The window's surface is configured to be accessible
+ * by DMA engines and hardware accelerators. */
+ public static final int MEMORY_TYPE_HARDWARE = 1;
+ /** Memory type: The window's surface is configured to be accessible
+ * by graphics accelerators. */
+ public static final int MEMORY_TYPE_GPU = 2;
+ /** Memory type: The window's surface doesn't own its buffers and
+ * therefore cannot be locked. Instead the buffers are pushed to
+ * it through native binder calls. */
+ public static final int MEMORY_TYPE_PUSH_BUFFERS = 3;
+
+ /**
+ * Various behavioral options/flags. Default is none.
+ *
+ * @see #FLAG_BLUR_BEHIND
+ * @see #FLAG_DIM_BEHIND
+ * @see #FLAG_NOT_FOCUSABLE
+ * @see #FLAG_NOT_TOUCHABLE
+ * @see #FLAG_NOT_TOUCH_MODAL
+ * @see #FLAG_LAYOUT_IN_SCREEN
+ * @see #FLAG_DITHER
+ * @see #FLAG_KEEP_SCREEN_ON
+ * @see #FLAG_FULLSCREEN
+ * @see #FLAG_FORCE_NOT_FULLSCREEN
+ * @see #FLAG_IGNORE_CHEEK_PRESSES
+ */
+ public int flags;
+
+ /** Window flag: everything behind this window will be dimmed.
+ * Use {@link #dimAmount} to control the amount of dim. */
+ public static final int FLAG_DIM_BEHIND = 0x00000002;
+
+ /** Window flag: blur everything behind this window. */
+ public static final int FLAG_BLUR_BEHIND = 0x00000004;
+
+ /** Window flag: this window won't ever get focus. */
+ public static final int FLAG_NOT_FOCUSABLE = 0x00000008;
+
+ /** Window flag: this window can never receive touch events. */
+ public static final int FLAG_NOT_TOUCHABLE = 0x00000010;
+
+ /** Window flag: Even when this window is focusable (its
+ * {@link #FLAG_NOT_FOCUSABLE is not set), allow any pointer events
+ * outside of the window to be sent to the windows behind it. Otherwise
+ * it will consume all pointer events itself, regardless of whether they
+ * are inside of the window. */
+ public static final int FLAG_NOT_TOUCH_MODAL = 0x00000020;
+
+ /** Window flag: When set, if the device is asleep when the touch
+ * screen is pressed, you will receive this first touch event. Usually
+ * the first touch event is consumed by the system since the user can
+ * not see what they are pressing on.
+ */
+ public static final int FLAG_TOUCHABLE_WHEN_WAKING = 0x00000040;
+
+ /** Window flag: as long as this window is visible to the user, keep
+ * the device's screen turned on and bright. */
+ public static final int FLAG_KEEP_SCREEN_ON = 0x00000080;
+
+ /** Window flag: place the window within the entire screen, ignoring
+ * decorations around the border (a.k.a. the status bar). The
+ * window must correctly position its contents to take the screen
+ * decoration into account. This flag is normally set for you
+ * by Window as described in {@link Window#setFlags}. */
+ public static final int FLAG_LAYOUT_IN_SCREEN = 0x00000100;
+
+ /** Window flag: allow window to extend outside of the screen. */
+ public static final int FLAG_LAYOUT_NO_LIMITS = 0x00000200;
+
+ /** Window flag: Hide all screen decorations (e.g. status bar) while
+ * this window is displayed. This allows the window to use the entire
+ * display space for itself -- the status bar will be hidden when
+ * an app window with this flag set is on the top layer. */
+ public static final int FLAG_FULLSCREEN = 0x00000400;
+
+ /** Window flag: Override {@link #FLAG_FULLSCREEN and force the
+ * screen decorations (such as status bar) to be shown. */
+ public static final int FLAG_FORCE_NOT_FULLSCREEN = 0x00000800;
+
+ /** Window flag: turn on dithering when compositing this window to
+ * the screen. */
+ public static final int FLAG_DITHER = 0x00001000;
+
+ /** Window flag: don't allow screen shots while this window is
+ * displayed. */
+ public static final int FLAG_SECURE = 0x00002000;
+
+ /** Window flag: a special mode where the layout parameters are used
+ * to perform scaling of the surface when it is composited to the
+ * screen. */
+ public static final int FLAG_SCALED = 0x00004000;
+
+ /** Window flag: intended for windows that will often be used when the user is
+ * holding the screen against their face, it will aggressively filter the event
+ * stream to prevent unintended presses in this situation that may not be
+ * desired for a particular window, when such an event stream is detected, the
+ * application will receive a CANCEL motion event to indicate this so applications
+ * can handle this accordingly by taking no action on the event
+ * until the finger is released. */
+ public static final int FLAG_IGNORE_CHEEK_PRESSES = 0x00008000;
+
+ /** Window flag: a special option only for use in combination with
+ * {@link #FLAG_LAYOUT_IN_SCREEN}. When requesting layout in the
+ * screen your window may appear on top of or behind screen decorations
+ * such as the status bar. By also including this flag, the window
+ * manager will report the inset rectangle needed to ensure your
+ * content is not covered by screen decorations. This flag is normally
+ * set for you by Window as described in {@link Window#setFlags}.*/
+ public static final int FLAG_LAYOUT_INSET_DECOR = 0x00010000;
+
+ /** Window flag: a special option intended for system dialogs. When
+ * this flag is set, the window will demand focus unconditionally when
+ * it is created.
+ * {@hide} */
+ public static final int FLAG_SYSTEM_ERROR = 0x40000000;
+
+ /**
+ * Placement of window within the screen as per {@link Gravity}
+ *
+ * @see Gravity
+ */
+ public int gravity;
+
+ /**
+ * The horizontal margin, as a percentage of the container's width,
+ * between the container and the widget.
+ */
+ public float horizontalMargin;
+
+ /**
+ * The vertical margin, as a percentage of the container's height,
+ * between the container and the widget.
+ */
+ public float verticalMargin;
+
+ /**
+ * The desired bitmap format. May be one of the constants in
+ * {@link android.graphics.PixelFormat}. Default is OPAQUE.
+ */
+ public int format;
+
+ /**
+ * A style resource defining the animations to use for this window.
+ * This must be a system resource; it can not be an application resource
+ * because the window manager does not have access to applications.
+ */
+ public int windowAnimations;
+
+ /**
+ * An alpha value to apply to this entire window.
+ * An alpha of 1.0 means fully opaque and 0.0 means fully transparent
+ */
+ public float alpha = 1.0f;
+
+ /**
+ * When {@link #FLAG_DIM_BEHIND} is set, this is the amount of dimming
+ * to apply. Range is from 1.0 for completely opaque to 0.0 for no
+ * dim.
+ */
+ public float dimAmount = 1.0f;
+
+ /**
+ * Identifier for this window. This will usually be filled in for
+ * you.
+ */
+ public IBinder token = null;
+
+ /**
+ * Name of the package owning this window.
+ */
+ public String packageName = null;
+
+ public LayoutParams() {
+ super(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);
+ type = TYPE_APPLICATION;
+ format = PixelFormat.OPAQUE;
+ }
+
+ public LayoutParams(int _type) {
+ super(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);
+ type = _type;
+ format = PixelFormat.OPAQUE;
+ }
+
+ public LayoutParams(int _type, int _flags) {
+ super(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);
+ type = _type;
+ flags = _flags;
+ format = PixelFormat.OPAQUE;
+ }
+
+ public LayoutParams(int _type, int _flags, int _format) {
+ super(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);
+ type = _type;
+ flags = _flags;
+ format = _format;
+ }
+
+ public LayoutParams(int w, int h, int _type, int _flags, int _format) {
+ super(w, h);
+ type = _type;
+ flags = _flags;
+ format = _format;
+ }
+
+ public LayoutParams(int w, int h, int xpos, int ypos, int _type,
+ int _flags, int _format) {
+ super(w, h);
+ x = xpos;
+ y = ypos;
+ type = _type;
+ flags = _flags;
+ format = _format;
+ }
+
+ public final void setTitle(CharSequence title) {
+ if (null == title)
+ title = "";
+
+ mTitle = TextUtils.stringOrSpannedString(title);
+ }
+
+ public final CharSequence getTitle() {
+ return mTitle;
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel out, int parcelableFlags) {
+ out.writeInt(width);
+ out.writeInt(height);
+ out.writeInt(x);
+ out.writeInt(y);
+ out.writeInt(type);
+ out.writeInt(memoryType);
+ out.writeInt(flags);
+ out.writeInt(gravity);
+ out.writeFloat(horizontalMargin);
+ out.writeFloat(verticalMargin);
+ out.writeInt(format);
+ out.writeInt(windowAnimations);
+ out.writeFloat(alpha);
+ out.writeFloat(dimAmount);
+ out.writeStrongBinder(token);
+ out.writeString(packageName);
+ TextUtils.writeToParcel(mTitle, out, parcelableFlags);
+ }
+
+ public static final Parcelable.Creator<LayoutParams> CREATOR
+ = new Parcelable.Creator<LayoutParams>() {
+ public LayoutParams createFromParcel(Parcel in) {
+ return new LayoutParams(in);
+ }
+
+ public LayoutParams[] newArray(int size) {
+ return new LayoutParams[size];
+ }
+ };
+
+
+ public LayoutParams(Parcel in) {
+ width = in.readInt();
+ height = in.readInt();
+ x = in.readInt();
+ y = in.readInt();
+ type = in.readInt();
+ memoryType = in.readInt();
+ flags = in.readInt();
+ gravity = in.readInt();
+ horizontalMargin = in.readFloat();
+ verticalMargin = in.readFloat();
+ format = in.readInt();
+ windowAnimations = in.readInt();
+ alpha = in.readFloat();
+ dimAmount = in.readFloat();
+ token = in.readStrongBinder();
+ packageName = in.readString();
+ mTitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
+ }
+
+ public static final int LAYOUT_CHANGED = 1<<0;
+ public static final int TYPE_CHANGED = 1<<1;
+ public static final int FLAGS_CHANGED = 1<<2;
+ public static final int FORMAT_CHANGED = 1<<3;
+ public static final int ANIMATION_CHANGED = 1<<4;
+ public static final int DIM_AMOUNT_CHANGED = 1<<5;
+ public static final int TITLE_CHANGED = 1<<6;
+ public static final int ALPHA_CHANGED = 1<<7;
+ public static final int MEMORY_TYPE_CHANGED = 1<<8;
+
+ public final int copyFrom(LayoutParams o) {
+ int changes = 0;
+
+ if (width != o.width) {
+ width = o.width;
+ changes |= LAYOUT_CHANGED;
+ }
+ if (height != o.height) {
+ height = o.height;
+ changes |= LAYOUT_CHANGED;
+ }
+ if (x != o.x) {
+ x = o.x;
+ changes |= LAYOUT_CHANGED;
+ }
+ if (y != o.y) {
+ y = o.y;
+ changes |= LAYOUT_CHANGED;
+ }
+ if (type != o.type) {
+ type = o.type;
+ changes |= TYPE_CHANGED;
+ }
+ if (memoryType != o.memoryType) {
+ memoryType = o.memoryType;
+ changes |= MEMORY_TYPE_CHANGED;
+ }
+ if (flags != o.flags) {
+ flags = o.flags;
+ changes |= FLAGS_CHANGED;
+ }
+ if (gravity != o.gravity) {
+ gravity = o.gravity;
+ changes |= LAYOUT_CHANGED;
+ }
+ if (horizontalMargin != o.horizontalMargin) {
+ horizontalMargin = o.horizontalMargin;
+ changes |= LAYOUT_CHANGED;
+ }
+ if (verticalMargin != o.verticalMargin) {
+ verticalMargin = o.verticalMargin;
+ changes |= LAYOUT_CHANGED;
+ }
+ if (format != o.format) {
+ format = o.format;
+ changes |= FORMAT_CHANGED;
+ }
+ if (windowAnimations != o.windowAnimations) {
+ windowAnimations = o.windowAnimations;
+ changes |= ANIMATION_CHANGED;
+ }
+ if (token == null) {
+ // NOTE: token only copied if the recipient doesn't
+ // already have one.
+ token = o.token;
+ }
+ if (packageName == null) {
+ // NOTE: packageName only copied if the recipient doesn't
+ // already have one.
+ packageName = o.packageName;
+ }
+ if (!mTitle.equals(o.mTitle)) {
+ mTitle = o.mTitle;
+ changes |= TITLE_CHANGED;
+ }
+ if (alpha != o.alpha) {
+ alpha = o.alpha;
+ changes |= ALPHA_CHANGED;
+ }
+ if (dimAmount != o.dimAmount) {
+ dimAmount = o.dimAmount;
+ changes |= DIM_AMOUNT_CHANGED;
+ }
+
+ return changes;
+ }
+
+ @Override
+ public String debug(String output) {
+ output += "Contents of " + this + ":";
+ Log.d("Debug", output);
+ output = super.debug("");
+ Log.d("Debug", output);
+ Log.d("Debug", "");
+ Log.d("Debug", "WindowManager.LayoutParams={title=" + mTitle + "}");
+ return "";
+ }
+
+ @Override
+ public String toString() {
+ return "WM.LayoutParams{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " (" + x + "," + y + ")("
+ + (width==FILL_PARENT?"fill_parent":(width==WRAP_CONTENT?"wrap_content":width))
+ + "x"
+ + (height==FILL_PARENT?"fill_parent":(height==WRAP_CONTENT?"wrap_content":height))
+ + ") gr=#" + Integer.toHexString(gravity)
+ + " ty=" + type + " fl=#" + Integer.toHexString(flags)
+ + " fmt=" + format + "}";
+ }
+
+ private CharSequence mTitle = "";
+ }
+}
diff --git a/core/java/android/view/WindowManagerImpl.java b/core/java/android/view/WindowManagerImpl.java
new file mode 100644
index 0000000..fbecf46
--- /dev/null
+++ b/core/java/android/view/WindowManagerImpl.java
@@ -0,0 +1,339 @@
+/*
+ * 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.view;
+
+import android.graphics.PixelFormat;
+import android.os.IBinder;
+import android.util.AndroidRuntimeException;
+import android.util.Config;
+import android.util.Log;
+import android.view.WindowManager;
+
+final class WindowLeaked extends AndroidRuntimeException {
+ public WindowLeaked(String msg) {
+ super(msg);
+ }
+}
+
+/**
+ * Low-level communication with the global system window manager. It implements
+ * the ViewManager interface, allowing you to add any View subclass as a
+ * top-level window on the screen. Additional window manager specific layout
+ * parameters are defined for control over how windows are displayed.
+ * It also implemens the WindowManager interface, allowing you to control the
+ * displays attached to the device.
+ *
+ * <p>Applications will not normally use WindowManager directly, instead relying
+ * on the higher-level facilities in {@link android.app.Activity} and
+ * {@link android.app.Dialog}.
+ *
+ * <p>Even for low-level window manager access, it is almost never correct to use
+ * this class. For example, {@link android.app.Activity#getWindowManager}
+ * provides a ViewManager for adding windows that are associated with that
+ * activity -- the window manager will not normally allow you to add arbitrary
+ * windows that are not associated with an activity.
+ *
+ * @hide
+ */
+public class WindowManagerImpl implements WindowManager {
+ /**
+ * The user is navigating with keys (not the touch screen), so
+ * navigational focus should be shown.
+ */
+ public static final int RELAYOUT_IN_TOUCH_MODE = 0x1;
+ /**
+ * This is the first time the window is being drawn,
+ * so the client must call drawingFinished() when done
+ */
+ public static final int RELAYOUT_FIRST_TIME = 0x2;
+
+ public static final int ADD_FLAG_APP_VISIBLE = 0x2;
+ public static final int ADD_FLAG_IN_TOUCH_MODE = RELAYOUT_IN_TOUCH_MODE;
+
+ public static final int ADD_OKAY = 0;
+ public static final int ADD_BAD_APP_TOKEN = -1;
+ public static final int ADD_BAD_SUBWINDOW_TOKEN = -2;
+ public static final int ADD_NOT_APP_TOKEN = -3;
+ public static final int ADD_APP_EXITING = -4;
+ public static final int ADD_DUPLICATE_ADD = -5;
+ public static final int ADD_STARTING_NOT_NEEDED = -6;
+ public static final int ADD_MULTIPLE_SINGLETON = -7;
+ public static final int ADD_PERMISSION_DENIED = -8;
+
+ public static WindowManagerImpl getDefault()
+ {
+ return mWindowManager;
+ }
+
+ public void addView(View view)
+ {
+ addView(view, new WindowManager.LayoutParams(
+ WindowManager.LayoutParams.TYPE_APPLICATION, 0, PixelFormat.OPAQUE));
+ }
+
+ public void addView(View view, ViewGroup.LayoutParams params)
+ {
+ addView(view, params, false);
+ }
+
+ public void addViewNesting(View view, ViewGroup.LayoutParams params)
+ {
+ addView(view, params, false);
+ }
+
+ private void addView(View view, ViewGroup.LayoutParams params, boolean nest)
+ {
+ if (Config.LOGV) Log.v("WindowManager", "addView view=" + view);
+
+ if (!(params instanceof WindowManager.LayoutParams)) {
+ throw new IllegalArgumentException(
+ "Params must be WindowManager.LayoutParams");
+ }
+
+ final WindowManager.LayoutParams wparams
+ = (WindowManager.LayoutParams)params;
+
+ ViewRoot root;
+ View panelParentView = null;
+
+ synchronized (this) {
+ // Here's an odd/questionable case: if someone tries to add a
+ // view multiple times, then we simply bump up a nesting count
+ // and they need to remove the view the corresponding number of
+ // times to have it actually removed from the window manager.
+ // This is useful specifically for the notification manager,
+ // which can continually add/remove the same view as a
+ // notification gets updated.
+ int index = findViewLocked(view, false);
+ if (index >= 0) {
+ if (!nest) {
+ throw new IllegalStateException("View " + view
+ + " has already been added to the window manager.");
+ }
+ root = mRoots[index];
+ root.mAddNesting++;
+ // Update layout parameters.
+ view.setLayoutParams(wparams);
+ root.setLayoutParams(wparams);
+ return;
+ }
+
+ // If this is a panel window, then find the window it is being
+ // attached to for future reference.
+ if (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
+ wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
+ final int count = mViews != null ? mViews.length : 0;
+ for (int i=0; i<count; i++) {
+ if (mRoots[i].mWindow.asBinder() == wparams.token) {
+ panelParentView = mViews[i];
+ }
+ }
+ }
+
+ root = new ViewRoot();
+ root.mAddNesting = 1;
+
+ view.setLayoutParams(wparams);
+
+ if (mViews == null) {
+ index = 1;
+ mViews = new View[1];
+ mRoots = new ViewRoot[1];
+ mParams = new WindowManager.LayoutParams[1];
+ } else {
+ index = mViews.length + 1;
+ Object[] old = mViews;
+ mViews = new View[index];
+ System.arraycopy(old, 0, mViews, 0, index-1);
+ old = mRoots;
+ mRoots = new ViewRoot[index];
+ System.arraycopy(old, 0, mRoots, 0, index-1);
+ old = mParams;
+ mParams = new WindowManager.LayoutParams[index];
+ System.arraycopy(old, 0, mParams, 0, index-1);
+ }
+ index--;
+
+ mViews[index] = view;
+ mRoots[index] = root;
+ mParams[index] = wparams;
+ }
+
+ // do this last because it fires off messages to start doing things
+ root.setView(view, wparams, panelParentView);
+ }
+
+ public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
+ if (!(params instanceof WindowManager.LayoutParams)) {
+ throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
+ }
+
+ final WindowManager.LayoutParams wparams
+ = (WindowManager.LayoutParams)params;
+
+ view.setLayoutParams(wparams);
+
+ synchronized (this) {
+ int index = findViewLocked(view, true);
+ ViewRoot root = mRoots[index];
+ mParams[index] = wparams;
+ root.setLayoutParams(wparams);
+ }
+ }
+
+ public void removeView(View view) {
+ synchronized (this) {
+ int index = findViewLocked(view, true);
+ View curView = removeViewLocked(index);
+ if (curView == view) {
+ return;
+ }
+
+ throw new IllegalStateException("Calling with view " + view
+ + " but the ViewRoot is attached to " + curView);
+ }
+ }
+
+ public void removeViewImmediate(View view) {
+ synchronized (this) {
+ int index = findViewLocked(view, true);
+ ViewRoot root = mRoots[index];
+ View curView = root.getView();
+
+ root.mAddNesting = 0;
+ root.die(true);
+ finishRemoveViewLocked(curView, index);
+ if (curView == view) {
+ return;
+ }
+
+ throw new IllegalStateException("Calling with view " + view
+ + " but the ViewRoot is attached to " + curView);
+ }
+ }
+
+ View removeViewLocked(int index) {
+ ViewRoot root = mRoots[index];
+ View view = root.getView();
+
+ // Don't really remove until we have matched all calls to add().
+ root.mAddNesting--;
+ if (root.mAddNesting > 0) {
+ return view;
+ }
+
+ root.die(false);
+ finishRemoveViewLocked(view, index);
+ return view;
+ }
+
+ void finishRemoveViewLocked(View view, int index) {
+ final int count = mViews.length;
+
+ // remove it from the list
+ View[] tmpViews = new View[count-1];
+ removeItem(tmpViews, mViews, index);
+ mViews = tmpViews;
+
+ ViewRoot[] tmpRoots = new ViewRoot[count-1];
+ removeItem(tmpRoots, mRoots, index);
+ mRoots = tmpRoots;
+
+ WindowManager.LayoutParams[] tmpParams
+ = new WindowManager.LayoutParams[count-1];
+ removeItem(tmpParams, mParams, index);
+ mParams = tmpParams;
+
+ view.assignParent(null);
+ // func doesn't allow null... does it matter if we clear them?
+ //view.setLayoutParams(null);
+ }
+
+ public void closeAll(IBinder token, String who, String what) {
+ synchronized (this) {
+ if (mViews == null)
+ return;
+
+ int count = mViews.length;
+ //Log.i("foo", "Closing all windows of " + token);
+ for (int i=0; i<count; i++) {
+ //Log.i("foo", "@ " + i + " token " + mParams[i].token
+ // + " view " + mRoots[i].getView());
+ if (token == null || mParams[i].token == token) {
+ ViewRoot root = mRoots[i];
+ root.mAddNesting = 1;
+
+ //Log.i("foo", "Force closing " + root);
+ if (who != null) {
+ WindowLeaked leak = new WindowLeaked(
+ what + " " + who + " has leaked window "
+ + root.getView() + " that was originally added here");
+ leak.setStackTrace(root.getLocation().getStackTrace());
+ Log.e("WindowManager", leak.getMessage(), leak);
+ }
+
+ removeViewLocked(i);
+ i--;
+ count--;
+ }
+ }
+ }
+ }
+
+ public void closeAll() {
+ closeAll(null, null, null);
+ }
+
+ public Display getDefaultDisplay() {
+ return new Display(Display.DEFAULT_DISPLAY);
+ }
+
+ private View[] mViews;
+ private ViewRoot[] mRoots;
+ private WindowManager.LayoutParams[] mParams;
+
+ private static void removeItem(Object[] dst, Object[] src, int index)
+ {
+ if (dst.length > 0) {
+ if (index > 0) {
+ System.arraycopy(src, 0, dst, 0, index);
+ }
+ if (index < dst.length) {
+ System.arraycopy(src, index+1, dst, index, src.length-index-1);
+ }
+ }
+ }
+
+ private int findViewLocked(View view, boolean required)
+ {
+ synchronized (this) {
+ final int count = mViews != null ? mViews.length : 0;
+ for (int i=0; i<count; i++) {
+ if (mViews[i] == view) {
+ return i;
+ }
+ }
+ if (required) {
+ throw new IllegalArgumentException(
+ "View not attached to window manager");
+ }
+ return -1;
+ }
+ }
+
+ private static WindowManagerImpl mWindowManager = new WindowManagerImpl();
+}
diff --git a/core/java/android/view/WindowManagerPolicy.java b/core/java/android/view/WindowManagerPolicy.java
new file mode 100644
index 0000000..93e1a0b
--- /dev/null
+++ b/core/java/android/view/WindowManagerPolicy.java
@@ -0,0 +1,717 @@
+/*
+ * 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.view;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.os.IBinder;
+import android.os.LocalPowerManager;
+
+/**
+ * This interface supplies all UI-specific behavior of the window manager. An
+ * instance of it is created by the window manager when it starts up, and allows
+ * customization of window layering, special window types, key dispatching, and
+ * layout.
+ *
+ * <p>Because this provides deep interaction with the system window manager,
+ * specific methods on this interface can be called from a variety of contexts
+ * with various restrictions on what they can do. These are encoded through
+ * a suffixes at the end of a method encoding the thread the method is called
+ * from and any locks that are held when it is being called; if no suffix
+ * is attached to a method, then it is not called with any locks and may be
+ * called from the main window manager thread or another thread calling into
+ * the window manager.
+ *
+ * <p>The current suffixes are:
+ *
+ * <dl>
+ * <dt> Ti <dd> Called from the input thread. This is the thread that
+ * collects pending input events and dispatches them to the appropriate window.
+ * It may block waiting for events to be processed, so that the input stream is
+ * properly serialized.
+ * <dt> Tq <dd> Called from the low-level input queue thread. This is the
+ * thread that reads events out of the raw input devices and places them
+ * into the global input queue that is read by the <var>Ti</var> thread.
+ * This thread should not block for a long period of time on anything but the
+ * key driver.
+ * <dt> Lw <dd> Called with the main window manager lock held. Because the
+ * window manager is a very low-level system service, there are few other
+ * system services you can call with this lock held. It is explicitly okay to
+ * make calls into the package manager and power manager; it is explicitly not
+ * okay to make calls into the activity manager. Note that
+ * {@link android.content.Context#checkPermission(String, int, int)} and
+ * variations require calling into the activity manager.
+ * <dt> Li <dd> Called with the input thread lock held. This lock can be
+ * acquired by the window manager while it holds the window lock, so this is
+ * even more restrictive than <var>Lw</var>.
+ * </dl>
+ *
+ * @hide
+ */
+public interface WindowManagerPolicy {
+ public final static int FLAG_WAKE = 0x00000001;
+ public final static int FLAG_WAKE_DROPPED = 0x00000002;
+ public final static int FLAG_SHIFT = 0x00000004;
+ public final static int FLAG_CAPS_LOCK = 0x00000008;
+ public final static int FLAG_ALT = 0x00000010;
+ public final static int FLAG_ALT_GR = 0x00000020;
+ public final static int FLAG_MENU = 0x00000040;
+ public final static int FLAG_LAUNCHER = 0x00000080;
+
+ public final static int FLAG_WOKE_HERE = 0x10000000;
+ public final static int FLAG_BRIGHT_HERE = 0x20000000;
+
+ public final static boolean WATCH_POINTER = false;
+
+ // flags for interceptKeyTq
+ /**
+ * Pass this event to the user / app. To be returned from {@link #interceptKeyTq}.
+ */
+ public final static int ACTION_PASS_TO_USER = 0x00000001;
+
+ /**
+ * This key event should extend the user activity timeout and turn the lights on.
+ * To be returned from {@link #interceptKeyTq}. Do not return this and
+ * {@link #ACTION_GO_TO_SLEEP} or {@link #ACTION_PASS_TO_USER}.
+ */
+ public final static int ACTION_POKE_USER_ACTIVITY = 0x00000002;
+
+ /**
+ * This key event should put the device to sleep (and engage keyguard if necessary)
+ * To be returned from {@link #interceptKeyTq}. Do not return this and
+ * {@link #ACTION_POKE_USER_ACTIVITY} or {@link #ACTION_PASS_TO_USER}.
+ */
+ public final static int ACTION_GO_TO_SLEEP = 0x00000004;
+
+ /**
+ * Interface to the Window Manager state associated with a particular
+ * window. You can hold on to an instance of this interface from the call
+ * to prepareAddWindow() until removeWindow().
+ */
+ public interface WindowState {
+ /**
+ * Perform standard frame computation. The result can be obtained with
+ * getFrame() if so desired. Must be called with the window manager
+ * lock held.
+ *
+ * @param parentLeft The left edge of the parent container this window
+ * is in, used for computing its position.
+ * @param parentTop The top edge of the parent container this window
+ * is in, used for computing its position.
+ * @param parentRight The right edge of the parent container this window
+ * is in, used for computing its position.
+ * @param parentBottom The bottom edge of the parent container this window
+ * is in, used for computing its position.
+ * @param displayLeft The left edge of the available display, used
+ * for constraining the overall dimensions of the window.
+ * @param displayTop The left edge of the available display, used
+ * for constraining the overall dimensions of the window.
+ * @param displayRight The right edge of the available display, used
+ * for constraining the overall dimensions of the window.
+ * @param displayBottom The bottom edge of the available display, used
+ * for constraining the overall dimensions of the window.
+ */
+ public void computeFrameLw(int parentLeft, int parentRight, int parentBottom,
+ int parentHeight, int displayLeft, int displayTop,
+ int displayRight, int displayBottom);
+
+ /**
+ * Set the window's frame to an exact value. Must be called with the
+ * window manager lock held.
+ *
+ * @param left Left edge of the window.
+ * @param top Top edge of the window.
+ * @param right Right edge (exclusive) of the window.
+ * @param bottom Bottom edge (exclusive) of the window.
+ */
+ public void setFrameLw(int left, int top, int right, int bottom);
+
+ /**
+ * Retrieve the current frame of the window. Must be called with the
+ * window manager lock held.
+ *
+ * @return Rect The rectangle holding the window frame.
+ */
+ public Rect getFrameLw();
+
+ /**
+ * Retrieve the current LayoutParams of the window.
+ *
+ * @return WindowManager.LayoutParams The window's internal LayoutParams
+ * instance.
+ */
+ public WindowManager.LayoutParams getAttrs();
+
+ /**
+ * Return the token for the application (actually activity) that owns
+ * this window. May return null for system windows.
+ *
+ * @return An IApplicationToken identifying the owning activity.
+ */
+ public IApplicationToken getAppToken();
+
+ /**
+ * Return true if, at any point, the application token associated with
+ * this window has actually displayed any windows. This is most useful
+ * with the "starting up" window to determine if any windows were
+ * displayed when it is closed.
+ *
+ * @return Returns true if one or more windows have been displayed,
+ * else false.
+ */
+ public boolean hasAppShownWindows();
+
+ /**
+ * Return true if the application token has been asked to display an
+ * app starting icon as the application is starting up.
+ *
+ * @return Returns true if setAppStartingIcon() was called for this
+ * window's token.
+ */
+ public boolean hasAppStartingIcon();
+
+ /**
+ * Return the Window that is being displayed as this window's
+ * application token is being started.
+ *
+ * @return Returns the currently displayed starting window, or null if
+ * it was not requested, has not yet been displayed, or has
+ * been removed.
+ */
+ public WindowState getAppStartingWindow();
+
+ /**
+ * Is this window currently visible to the user on-screen? It is
+ * displayed either if it is visible or it is currently running an
+ * animation before no longer being visible. Must be called with the
+ * window manager lock held.
+ */
+ boolean isDisplayedLw();
+
+ /**
+ * Returns true if the window is both full screen and opaque. Must be
+ * called with the window manager lock held.
+ *
+ * @param width The width of the screen
+ * @param height The height of the screen
+ * @param shownFrame If true, this is based on the actual shown frame of
+ * the window (taking into account animations); if
+ * false, this is based on the currently requested
+ * frame, which any current animation will be moving
+ * towards.
+ * @return Returns true if the window is both full screen and opaque
+ */
+ public boolean fillsScreenLw(int width, int height, boolean shownFrame);
+
+ /**
+ * Returns true if this window has been shown on screen at some time in
+ * the past. Must be called with the window manager lock held.
+ *
+ * @return boolean
+ */
+ public boolean hasDrawnLw();
+
+ /**
+ * Can be called by the policy to force a window to be hidden,
+ * regardless of whether the client or window manager would like
+ * it shown. Must be called with the window manager lock held.
+ */
+ public void hideLw();
+
+ /**
+ * Can be called to undo the effect of {@link #hideLw}, allowing a
+ * window to be shown as long as the window manager and client would
+ * also like it to be shown. Must be called with the window manager
+ * lock held.
+ */
+ public void showLw();
+
+ /**
+ * Sets insets on the window that represent the area within the window that is covered
+ * by system windows (e.g. status bar). Must be called with the window
+ * manager lock held.
+ *
+ * @param l
+ * @param t
+ * @param r
+ * @param b
+ */
+ public void setCoveredInsetsLw(int l, int t, int r, int b);
+ }
+
+ /** No transition happening. */
+ public final int TRANSIT_NONE = 0;
+ /** Window has been added to the screen. */
+ public final int TRANSIT_ENTER = 1;
+ /** Window has been removed from the screen. */
+ public final int TRANSIT_EXIT = 2;
+ /** Window has been made visible. */
+ public final int TRANSIT_SHOW = 3;
+ /** Window has been made invisible. */
+ public final int TRANSIT_HIDE = 4;
+ /** The "application starting" preview window is no longer needed, and will
+ * animate away to show the real window. */
+ public final int TRANSIT_PREVIEW_DONE = 5;
+ /** A window in a new activity is being opened on top of an existing one
+ * in the same task. */
+ public final int TRANSIT_ACTIVITY_OPEN = 6;
+ /** The window in the top-most activity is being closed to reveal the
+ * previous activity in the same task. */
+ public final int TRANSIT_ACTIVITY_CLOSE = 7;
+ /** A window in a new task is being opened on top of an existing one
+ * in another activity's task. */
+ public final int TRANSIT_TASK_OPEN = 8;
+ /** A window in the top-most activity is being closed to reveal the
+ * previous activity in a different task. */
+ public final int TRANSIT_TASK_CLOSE = 9;
+ /** A window in an existing task is being displayed on top of an existing one
+ * in another activity's task. */
+ public final int TRANSIT_TASK_TO_FRONT = 10;
+ /** A window in an existing task is being put below all other tasks. */
+ public final int TRANSIT_TASK_TO_BACK = 11;
+
+ /** Screen turned off because of power button */
+ public final int OFF_BECAUSE_OF_USER = 1;
+ /** Screen turned off because of timeout */
+ public final int OFF_BECAUSE_OF_TIMEOUT = 2;
+
+ /**
+ * Magic constant to {@link IWindowManager#setRotation} to not actually
+ * modify the rotation.
+ */
+ public final int USE_LAST_ROTATION = -1000;
+
+ /**
+ * Perform initialization of the policy.
+ *
+ * @param context The system context we are running in.
+ * @param powerManager
+ */
+ public void init(Context context, IWindowManager windowManager,
+ LocalPowerManager powerManager);
+
+ /**
+ * Check permissions when adding a window.
+ *
+ * @param attrs The window's LayoutParams.
+ *
+ * @return {@link WindowManagerImpl#ADD_OKAY} if the add can proceed;
+ * else an error code, usually
+ * {@link WindowManagerImpl#ADD_PERMISSION_DENIED}, to abort the add.
+ */
+ public int checkAddPermission(WindowManager.LayoutParams attrs);
+
+ /**
+ * Sanitize the layout parameters coming from a client. Allows the policy
+ * to do things like ensure that windows of a specific type can't take
+ * input focus.
+ *
+ * @param attrs The window layout parameters to be modified. These values
+ * are modified in-place.
+ */
+ public void adjustWindowParamsLw(WindowManager.LayoutParams attrs);
+
+ /**
+ * After the window manager has computed the current configuration based
+ * on its knowledge of the display and input devices, it gives the policy
+ * a chance to adjust the information contained in it. If you want to
+ * leave it as-is, simply do nothing.
+ *
+ * <p>This method may be called by any thread in the window manager, but
+ * no internal locks in the window manager will be held.
+ *
+ * @param config The Configuration being computed, for you to change as
+ * desired.
+ */
+ public void adjustConfigurationLw(Configuration config);
+
+ /**
+ * Assign a window type to a layer. Allows you to control how different
+ * kinds of windows are ordered on-screen.
+ *
+ * @param type The type of window being assigned.
+ *
+ * @return int An arbitrary integer used to order windows, with lower
+ * numbers below higher ones.
+ */
+ public int windowTypeToLayerLw(int type);
+
+ /**
+ * Return how to Z-order sub-windows in relation to the window they are
+ * attached to. Return positive to have them ordered in front, negative for
+ * behind.
+ *
+ * @param type The sub-window type code.
+ *
+ * @return int Layer in relation to the attached window, where positive is
+ * above and negative is below.
+ */
+ public int subWindowTypeToLayerLw(int type);
+
+ /**
+ * Called when the system would like to show a UI to indicate that an
+ * application is starting. You can use this to add a
+ * APPLICATION_STARTING_TYPE window with the given appToken to the window
+ * manager (using the normal window manager APIs) that will be shown until
+ * the application displays its own window. This is called without the
+ * window manager locked so that you can call back into it.
+ *
+ * @param appToken Token of the application being started.
+ * @param packageName The name of the application package being started.
+ * @param theme Resource defining the application's overall visual theme.
+ * @param nonLocalizedLabel The default title label of the application if
+ * no data is found in the resource.
+ * @param labelRes The resource ID the application would like to use as its name.
+ * @param icon The resource ID the application would like to use as its icon.
+ *
+ * @return Optionally you can return the View that was used to create the
+ * window, for easy removal in removeStartingWindow.
+ *
+ * @see #removeStartingWindow
+ */
+ public View addStartingWindow(IBinder appToken, String packageName,
+ int theme, CharSequence nonLocalizedLabel,
+ int labelRes, int icon);
+
+ /**
+ * Called when the first window of an application has been displayed, while
+ * {@link #addStartingWindow} has created a temporary initial window for
+ * that application. You should at this point remove the window from the
+ * window manager. This is called without the window manager locked so
+ * that you can call back into it.
+ *
+ * <p>Note: due to the nature of these functions not being called with the
+ * window manager locked, you must be prepared for this function to be
+ * called multiple times and/or an initial time with a null View window
+ * even if you previously returned one.
+ *
+ * @param appToken Token of the application that has started.
+ * @param window Window View that was returned by createStartingWindow.
+ *
+ * @see #addStartingWindow
+ */
+ public void removeStartingWindow(IBinder appToken, View window);
+
+ /**
+ * Prepare for a window being added to the window manager. You can throw an
+ * exception here to prevent the window being added, or do whatever setup
+ * you need to keep track of the window.
+ *
+ * @param win The window being added.
+ * @param attrs The window's LayoutParams.
+ *
+ * @return {@link WindowManagerImpl#ADD_OKAY} if the add can proceed, else an
+ * error code to abort the add.
+ */
+ public int prepareAddWindowLw(WindowState win,
+ WindowManager.LayoutParams attrs);
+
+ /**
+ * Called when a window is being removed from a window manager. Must not
+ * throw an exception -- clean up as much as possible.
+ *
+ * @param win The window being removed.
+ */
+ public void removeWindowLw(WindowState win);
+
+ /**
+ * Control the animation to run when a window's state changes. Return a
+ * non-0 number to force the animation to a specific resource ID, or 0
+ * to use the default animation.
+ *
+ * @param win The window that is changing.
+ * @param transit What is happening to the window: {@link #TRANSIT_ENTER},
+ * {@link #TRANSIT_EXIT}, {@link #TRANSIT_SHOW}, or
+ * {@link #TRANSIT_HIDE}.
+ *
+ * @return Resource ID of the actual animation to use, or 0 for none.
+ */
+ public int selectAnimationLw(WindowState win, int transit);
+
+ /**
+ * Called from the key queue thread before a key is dispatched to the
+ * input thread.
+ *
+ * <p>There are some actions that need to be handled here because they
+ * affect the power state of the device, for example, the power keys.
+ * Generally, it's best to keep as little as possible in the queue thread
+ * because it's the most fragile.
+ *
+ * @param event the raw input event as read from the driver
+ * @param screenIsOn true if the screen is already on
+ * @return The bitwise or of the {@link #ACTION_PASS_TO_USER},
+ * {@link #ACTION_POKE_USER_ACTIVITY} and {@link #ACTION_GO_TO_SLEEP} flags.
+ */
+ public int interceptKeyTq(RawInputEvent event, boolean screenIsOn);
+
+ /**
+ * Called from the input thread before a key is dispatched to a window.
+ *
+ * <p>Allows you to define
+ * behavior for keys that can not be overridden by applications or redirect
+ * key events to a different window. This method is called from the
+ * input thread, with no locks held.
+ *
+ * <p>Note that if you change the window a key is dispatched to, the new
+ * target window will receive the key event without having input focus.
+ *
+ * @param win The window that currently has focus. This is where the key
+ * event will normally go.
+ * @param code Key code.
+ * @param metaKeys TODO
+ * @param down Is this a key press (true) or release (false)?
+ * @param repeatCount Number of times a key down has repeated.
+ * @return Returns true if the policy consumed the event and it should
+ * not be further dispatched.
+ */
+ public boolean interceptKeyTi(WindowState win, int code,
+ int metaKeys, boolean down, int repeatCount);
+
+ /**
+ * Called when layout of the windows is about to start.
+ *
+ * @param displayWidth The current full width of the screen.
+ * @param displayHeight The current full height of the screen.
+ */
+ public void beginLayoutLw(int displayWidth, int displayHeight);
+
+ /**
+ * Called for each window attached to the window manager as layout is
+ * proceeding. The implementation of this function must take care of
+ * setting the window's frame, either here or in finishLayout().
+ *
+ * @param win The window being positioned.
+ * @param attrs The LayoutParams of the window.
+ * @param attached For sub-windows, the window it is attached to; this
+ * window will already have had layoutWindow() called on it
+ * so you can use its Rect. Otherwise null.
+ */
+ public void layoutWindowLw(WindowState win,
+ WindowManager.LayoutParams attrs, WindowState attached);
+
+
+ /**
+ * Return the insets for the areas covered by system windows. These values are computed on the
+ * mose recent layout, so they are not guaranteed to be correct.
+ *
+ * @param attrs The LayoutParams of the window.
+ * @param coveredInset The areas covered by system windows, expressed as positive insets
+ *
+ */
+ public void getCoveredInsetHintLw(WindowManager.LayoutParams attrs, Rect coveredInset);
+
+ /**
+ * Called when layout of the windows is finished. After this function has
+ * returned, all windows given to layoutWindow() <em>must</em> have had a
+ * frame assigned.
+ */
+ public void finishLayoutLw();
+
+ /**
+ * Called when animation of the windows is about to start.
+ *
+ * @param displayWidth The current full width of the screen.
+ * @param displayHeight The current full height of the screen.
+ */
+ public void beginAnimationLw(int displayWidth, int displayHeight);
+
+ /**
+ * Called each time a window is animating.
+ *
+ * @param win The window being positioned.
+ * @param attrs The LayoutParams of the window.
+ */
+ public void animatingWindowLw(WindowState win,
+ WindowManager.LayoutParams attrs);
+
+ /**
+ * Called when animation of the windows is finished. If in this function you do
+ * something that may have modified the animation state of another window,
+ * be sure to return true in order to perform another animation frame.
+ *
+ * @return Return true if animation state may have changed (so that another
+ * frame of animation will be run).
+ */
+ public boolean finishAnimationLw();
+
+ /**
+ * Called after the screen turns off.
+ *
+ * @param why {@link #OFF_BECAUSE_OF_USER} or
+ * {@link #OFF_BECAUSE_OF_TIMEOUT}.
+ */
+ public void screenTurnedOff(int why);
+
+ /**
+ * Called after the screen turns on.
+ */
+ public void screenTurnedOn();
+
+ /**
+ * Perform any initial processing of a low-level input event before the
+ * window manager handles special keys and generates a high-level event
+ * that is dispatched to the application.
+ *
+ * @param event The input event that has occurred.
+ *
+ * @return Return true if you have consumed the event and do not want
+ * further processing to occur; return false for normal processing.
+ */
+ public boolean preprocessInputEventTq(RawInputEvent event);
+
+ /**
+ * Determine whether a given key code is used to cause an app switch
+ * to occur (most often the HOME key, also often ENDCALL). If you return
+ * true, then the system will go into a special key processing state
+ * where it drops any pending events that it cans and adjusts timeouts to
+ * try to get to this key as quickly as possible.
+ *
+ * <p>Note that this function is called from the low-level input queue
+ * thread, with either/or the window or input lock held; be very careful
+ * about what you do here. You absolutely should never acquire a lock
+ * that you would ever hold elsewhere while calling out into the window
+ * manager or view hierarchy.
+ *
+ * @param keycode The key that should be checked for performing an
+ * app switch before delivering to the application.
+ *
+ * @return Return true if this is an app switch key and special processing
+ * should happen; return false for normal processing.
+ */
+ public boolean isAppSwitchKeyTqTiLwLi(int keycode);
+
+ /**
+ * Determine whether a given key code is used for movement within a UI,
+ * and does not generally cause actions to be performed (normally the DPAD
+ * movement keys, NOT the DPAD center press key). This is called
+ * when {@link #isAppSwitchKeyTiLi} returns true to remove any pending events
+ * in the key queue that are not needed to switch applications.
+ *
+ * <p>Note that this function is called from the low-level input queue
+ * thread; be very careful about what you do here.
+ *
+ * @param keycode The key that is waiting to be delivered to the
+ * application.
+ *
+ * @return Return true if this is a purely navigation key and can be
+ * dropped without negative consequences; return false to keep it.
+ */
+ public boolean isMovementKeyTi(int keycode);
+
+ /**
+ * Given the current state of the world, should this relative movement
+ * wake up the device?
+ *
+ * @param device The device the movement came from.
+ * @param classes The input classes associated with the device.
+ * @param event The input event that occurred.
+ * @return
+ */
+ public boolean isWakeRelMovementTq(int device, int classes,
+ RawInputEvent event);
+
+ /**
+ * Given the current state of the world, should this absolute movement
+ * wake up the device?
+ *
+ * @param device The device the movement came from.
+ * @param classes The input classes associated with the device.
+ * @param event The input event that occurred.
+ * @return
+ */
+ public boolean isWakeAbsMovementTq(int device, int classes,
+ RawInputEvent event);
+
+ /**
+ * Tell the policy if anyone is requesting that keyguard not come on.
+ *
+ * @param enabled Whether keyguard can be on or not. does not actually
+ * turn it on, unless it was previously disabled with this function.
+ *
+ * @see android.app.KeyguardManager.KeyguardLock#disableKeyguard()
+ * @see android.app.KeyguardManager.KeyguardLock#reenableKeyguard()
+ */
+ public void enableKeyguard(boolean enabled);
+
+ /**
+ * Callback used by {@link WindowManagerPolicy#exitKeyguardSecurely}
+ */
+ interface OnKeyguardExitResult {
+ void onKeyguardExitResult(boolean success);
+ }
+
+ /**
+ * Tell the policy if anyone is requesting the keyguard to exit securely
+ * (this would be called after the keyguard was disabled)
+ * @param callback Callback to send the result back.
+ * @see android.app.KeyguardManager#exitKeyguardSecurely(android.app.KeyguardManager.OnKeyguardExitResult)
+ */
+ void exitKeyguardSecurely(OnKeyguardExitResult callback);
+
+ /**
+ * Return if keyguard is currently showing.
+ */
+ public boolean keyguardIsShowingTq();
+
+ /**
+ * inKeyguardRestrictedKeyInputMode
+ *
+ * if keyguard screen is showing or in restricted key input mode (i.e. in
+ * keyguard password emergency screen). When in such mode, certain keys,
+ * such as the Home key and the right soft keys, don't work.
+ *
+ * @return true if in keyguard restricted input mode.
+ */
+ public boolean inKeyguardRestrictedKeyInputMode();
+
+ /**
+ * Given an orientation constant
+ * ({@link android.content.pm.ActivityInfo#SCREEN_ORIENTATION_LANDSCAPE
+ * ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE} or
+ * {@link android.content.pm.ActivityInfo#SCREEN_ORIENTATION_PORTRAIT
+ * ActivityInfo.SCREEN_ORIENTATION_PORTRAIT}), return a surface
+ * rotation.
+ */
+ public int rotationForOrientation(int orientation);
+
+ /**
+ * Called when the system is mostly done booting
+ */
+ public void systemReady();
+
+ /**
+ * Called when we have finished booting and can now display the home
+ * screen to the user. This wilWl happen after systemReady(), and at
+ * this point the display is active.
+ */
+ public void enableScreenAfterBoot();
+
+ /**
+ * Returns true if the user's cheek has been pressed against the phone. This is
+ * determined by comparing the event's size attribute with a threshold value.
+ * For example for a motion event like down or up or move, if the size exceeds
+ * the threshold, it is considered as cheek press.
+ * @param ev the motion event generated when the cheek is pressed
+ * against the phone
+ * @return Returns true if the user's cheek has been pressed against the phone
+ * screen resulting in an invalid motion event
+ */
+ public boolean isCheekPressedAgainstScreen(MotionEvent ev);
+
+ public void setCurrentOrientation(int newOrientation);
+}
diff --git a/core/java/android/view/animation/AccelerateDecelerateInterpolator.java b/core/java/android/view/animation/AccelerateDecelerateInterpolator.java
new file mode 100644
index 0000000..fdb6f9d
--- /dev/null
+++ b/core/java/android/view/animation/AccelerateDecelerateInterpolator.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.animation;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+/**
+ * An interpolator where the rate of change starts and ends slowly but
+ * accelerates through the middle.
+ *
+ */
+public class AccelerateDecelerateInterpolator implements Interpolator {
+ public AccelerateDecelerateInterpolator() {
+ }
+
+ public AccelerateDecelerateInterpolator(Context context, AttributeSet attrs) {
+ }
+
+ public float getInterpolation(float input) {
+ return (float)(Math.cos((input + 1) * Math.PI) / 2.0f) + 0.5f;
+ }
+}
diff --git a/core/java/android/view/animation/AccelerateInterpolator.java b/core/java/android/view/animation/AccelerateInterpolator.java
new file mode 100644
index 0000000..b9e293f
--- /dev/null
+++ b/core/java/android/view/animation/AccelerateInterpolator.java
@@ -0,0 +1,62 @@
+/*
+ * 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.view.animation;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+
+/**
+ * An interpolator where the rate of change starts out slowly and
+ * and then accelerates.
+ *
+ */
+public class AccelerateInterpolator implements Interpolator {
+ public AccelerateInterpolator() {
+ }
+
+ /**
+ * Constructor
+ *
+ * @param factor Degree to which the animation should be eased. Seting
+ * factor to 1.0f produces a y=x^2 parabola. Increasing factor above
+ * 1.0f exaggerates the ease-in effect (i.e., it starts even
+ * slower and ends evens faster)
+ */
+ public AccelerateInterpolator(float factor) {
+ mFactor = factor;
+ }
+
+ public AccelerateInterpolator(Context context, AttributeSet attrs) {
+ TypedArray a =
+ context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.AccelerateInterpolator);
+
+ mFactor = a.getFloat(com.android.internal.R.styleable.AccelerateInterpolator_factor, 1.0f);
+
+ a.recycle();
+ }
+
+ public float getInterpolation(float input) {
+ if (mFactor == 1.0f) {
+ return (float)(input * input);
+ } else {
+ return (float)Math.pow(input, 2 * mFactor);
+ }
+ }
+
+ private float mFactor = 1.0f;
+}
diff --git a/core/java/android/view/animation/AlphaAnimation.java b/core/java/android/view/animation/AlphaAnimation.java
new file mode 100644
index 0000000..16a10a4
--- /dev/null
+++ b/core/java/android/view/animation/AlphaAnimation.java
@@ -0,0 +1,81 @@
+/*
+ * 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.view.animation;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+
+/**
+ * An animation that controls the alpha level of an object.
+ * Useful for fading things in and out. This animation ends up
+ * changing the alpha property of a {@link Transformation}
+ *
+ */
+public class AlphaAnimation extends Animation {
+ private float mFromAlpha;
+ private float mToAlpha;
+
+ /**
+ * Constructor used whan an AlphaAnimation is loaded from a resource.
+ *
+ * @param context Application context to use
+ * @param attrs Attribute set from which to read values
+ */
+ public AlphaAnimation(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ TypedArray a =
+ context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.AlphaAnimation);
+
+ mFromAlpha = a.getFloat(com.android.internal.R.styleable.AlphaAnimation_fromAlpha, 1.0f);
+ mToAlpha = a.getFloat(com.android.internal.R.styleable.AlphaAnimation_toAlpha, 1.0f);
+
+ a.recycle();
+ }
+
+ /**
+ * Constructor to use when building an AlphaAnimation from code
+ *
+ * @param fromAlpha Starting alpha value for the animation, where 1.0 means
+ * fully opaque and 0.0 means fully transparent.
+ * @param toAlpha Ending alpha value for the animation.
+ */
+ public AlphaAnimation(float fromAlpha, float toAlpha) {
+ mFromAlpha = fromAlpha;
+ mToAlpha = toAlpha;
+ }
+
+ /**
+ * Changes the alpha property of the supplied {@link Transformation}
+ */
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ final float alpha = mFromAlpha;
+ t.setAlpha(alpha + ((mToAlpha - alpha) * interpolatedTime));
+ }
+
+ @Override
+ public boolean willChangeTransformationMatrix() {
+ return false;
+ }
+
+ @Override
+ public boolean willChangeBounds() {
+ return false;
+ }
+}
diff --git a/core/java/android/view/animation/Animation.java b/core/java/android/view/animation/Animation.java
new file mode 100644
index 0000000..39fe561
--- /dev/null
+++ b/core/java/android/view/animation/Animation.java
@@ -0,0 +1,796 @@
+/*
+ * 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.view.animation;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+
+/**
+ * Abstraction for an Animation that can be applied to Views, Surfaces, or
+ * other objects. See the {@link android.view.animation animation package
+ * description file}.
+ */
+public abstract class Animation {
+ /**
+ * Repeat the animation indefinitely.
+ */
+ public static final int INFINITE = -1;
+
+ /**
+ * When the animation reaches the end and the repeat count is INFINTE_REPEAT
+ * or a positive value, the animation restarts from the beginning.
+ */
+ public static final int RESTART = 1;
+
+ /**
+ * When the animation reaches the end and the repeat count is INFINTE_REPEAT
+ * or a positive value, the animation plays backward (and then forward again).
+ */
+ public static final int REVERSE = 2;
+
+ /**
+ * Can be used as the start time to indicate the start time should be the current
+ * time when {@link #getTransformation(long, Transformation)} is invoked for the
+ * first animation frame. This can is useful for short animations.
+ */
+ public static final int START_ON_FIRST_FRAME = -1;
+
+ /**
+ * The specified dimension is an absolute number of pixels.
+ */
+ public static final int ABSOLUTE = 0;
+
+ /**
+ * The specified dimension holds a float and should be multiplied by the
+ * height or width of the object being animated.
+ */
+ public static final int RELATIVE_TO_SELF = 1;
+
+ /**
+ * The specified dimension holds a float and should be multiplied by the
+ * height or width of the parent of the object being animated.
+ */
+ public static final int RELATIVE_TO_PARENT = 2;
+
+ /**
+ * Requests that the content being animated be kept in its current Z
+ * order.
+ */
+ public static final int ZORDER_NORMAL = 0;
+
+ /**
+ * Requests that the content being animated be forced on top of all other
+ * content for the duration of the animation.
+ */
+ public static final int ZORDER_TOP = 1;
+
+ /**
+ * Requests that the content being animated be forced under all other
+ * content for the duration of the animation.
+ */
+ public static final int ZORDER_BOTTOM = -1;
+
+ /**
+ * Set by {@link #getTransformation(long, Transformation)} when the animation ends.
+ */
+ boolean mEnded = false;
+
+ /**
+ * Set by {@link #getTransformation(long, Transformation)} when the animation starts.
+ */
+ boolean mStarted = false;
+
+ /**
+ * Set by {@link #getTransformation(long, Transformation)} when the animation repeats
+ * in REVERSE mode.
+ */
+ boolean mCycleFlip = false;
+
+ /**
+ * This value must be set to true by {@link #initialize(int, int, int, int)}. It
+ * indicates the animation was successfully initialized and can be played.
+ */
+ boolean mInitialized = false;
+
+ /**
+ * Indicates whether the animation transformation should be applied before the
+ * animation starts.
+ */
+ boolean mFillBefore = true;
+
+ /**
+ * Indicates whether the animation transformation should be applied after the
+ * animation ends.
+ */
+ boolean mFillAfter = false;
+
+ /**
+ * The time in milliseconds at which the animation must start;
+ */
+ long mStartTime = -1;
+
+ /**
+ * The delay in milliseconds after which the animation must start. When the
+ * start offset is > 0, the start time of the animation is startTime + startOffset.
+ */
+ long mStartOffset;
+
+ /**
+ * The duration of one animation cycle in milliseconds.
+ */
+ long mDuration;
+
+ /**
+ * The number of times the animation must repeat. By default, an animation repeats
+ * indefinitely.
+ */
+ int mRepeatCount = 0;
+
+ /**
+ * Indicates how many times the animation was repeated.
+ */
+ int mRepeated = 0;
+
+ /**
+ * The behavior of the animation when it repeats. The repeat mode is either
+ * {@link #RESTART} or {@link #REVERSE}.
+ *
+ */
+ int mRepeatMode = RESTART;
+
+ /**
+ * The interpolator used by the animation to smooth the movement.
+ */
+ Interpolator mInterpolator;
+
+ /**
+ * The animation listener to be notified when the animation starts, ends or repeats.
+ */
+ AnimationListener mListener;
+
+ /**
+ * Desired Z order mode during animation.
+ */
+ private int mZAdjustment;
+
+ // Indicates what was the last value returned by getTransformation()
+ private boolean mMore = true;
+
+ /**
+ * Creates a new animation with a duration of 0ms, the default interpolator, with
+ * fillBefore set to true and fillAfter set to false
+ */
+ public Animation() {
+ ensureInterpolator();
+ }
+
+ /**
+ * Creates a new animation whose parameters come from the specified context and
+ * attributes set.
+ *
+ * @param context the application environment
+ * @param attrs the set of attributes holding the animation parameters
+ */
+ public Animation(Context context, AttributeSet attrs) {
+ TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.Animation);
+
+ setDuration((long) a.getInt(com.android.internal.R.styleable.Animation_duration, 0));
+ setStartOffset((long) a.getInt(com.android.internal.R.styleable.Animation_startOffset, 0));
+
+ setFillBefore(a.getBoolean(com.android.internal.R.styleable.Animation_fillBefore, mFillBefore));
+ setFillAfter(a.getBoolean(com.android.internal.R.styleable.Animation_fillAfter, mFillAfter));
+
+ final int resID = a.getResourceId(com.android.internal.R.styleable.Animation_interpolator, 0);
+ if (resID > 0) {
+ setInterpolator(context, resID);
+ }
+
+ setRepeatCount(a.getInt(com.android.internal.R.styleable.Animation_repeatCount, mRepeatCount));
+ setRepeatMode(a.getInt(com.android.internal.R.styleable.Animation_repeatMode, RESTART));
+
+ setZAdjustment(a.getInt(com.android.internal.R.styleable.Animation_zAdjustment, ZORDER_NORMAL));
+
+ ensureInterpolator();
+
+ a.recycle();
+ }
+
+ /**
+ * Reset the initialization state of this animation.
+ *
+ * @see #initialize(int, int, int, int)
+ */
+ public void reset() {
+ mInitialized = false;
+ mCycleFlip = false;
+ mRepeated = 0;
+ mMore = true;
+ }
+
+ /**
+ * Whether or not the animation has been initialized.
+ *
+ * @return Has this animation been initialized.
+ * @see #initialize(int, int, int, int)
+ */
+ public boolean isInitialized() {
+ return mInitialized;
+ }
+
+ /**
+ * 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.)
+ *
+ * <p>Objects that interpret a Animations should call this method when
+ * the sizes of the object being animated and its parent are known, and
+ * before calling {@link #getTransformation}.
+ *
+ *
+ * @param width Width of the object being animated
+ * @param height Height of the object being animated
+ * @param parentWidth Width of the animated object's parent
+ * @param parentHeight Height of the animated object's parent
+ */
+ public void initialize(int width, int height, int parentWidth, int parentHeight) {
+ mInitialized = true;
+ mCycleFlip = false;
+ mRepeated = 0;
+ mMore = true;
+ }
+
+ /**
+ * Sets the acceleration curve for this animation. The interpolator is loaded as
+ * a resource from the specified context.
+ *
+ * @param context The application environment
+ * @param resID The resource identifier of the interpolator to load
+ * @attr ref android.R.styleable#Animation_interpolator
+ */
+ public void setInterpolator(Context context, int resID) {
+ setInterpolator(AnimationUtils.loadInterpolator(context, resID));
+ }
+
+ /**
+ * Sets the acceleration curve for this animation. Defaults to a linear
+ * interpolation.
+ *
+ * @param i The interpolator which defines the acceleration curve
+ * @attr ref android.R.styleable#Animation_interpolator
+ */
+ public void setInterpolator(Interpolator i) {
+ mInterpolator = i;
+ }
+
+ /**
+ * When this animation should start relative to the start time. This is most
+ * useful when composing complex animations using an {@link AnimationSet }
+ * where some of the animations components start at different times.
+ *
+ * @param startOffset When this Animation should start, in milliseconds from
+ * the start time of the root AnimationSet.
+ * @attr ref android.R.styleable#Animation_startOffset
+ */
+ public void setStartOffset(long startOffset) {
+ mStartOffset = startOffset;
+ }
+
+ /**
+ * How long this animation should last.
+ *
+ * @param durationMillis Duration in milliseconds
+ * @attr ref android.R.styleable#Animation_duration
+ */
+ public void setDuration(long durationMillis) {
+ mDuration = durationMillis;
+ }
+
+ /**
+ * Ensure that the duration that this animation will run is not longer
+ * than <var>durationMillis</var>. In addition to adjusting the duration
+ * itself, this ensures that the repeat count also will not make it run
+ * longer than the given time.
+ *
+ * @param durationMillis The maximum duration the animation is allowed
+ * to run.
+ */
+ public void restrictDuration(long durationMillis) {
+ if (mStartOffset > durationMillis) {
+ mStartOffset = durationMillis;
+ mDuration = 0;
+ mRepeatCount = 0;
+ return;
+ }
+
+ long dur = mDuration + mStartOffset;
+ if (dur > durationMillis) {
+ mDuration = dur = durationMillis-mStartOffset;
+ }
+ if (mRepeatCount < 0 || mRepeatCount > durationMillis
+ || (dur*mRepeatCount) > durationMillis) {
+ mRepeatCount = (int)(durationMillis/dur);
+ }
+ }
+
+ /**
+ * How much to scale the duration by.
+ *
+ * @param scale The amount to scale the duration.
+ */
+ public void scaleCurrentDuration(float scale) {
+ mDuration = (long) (mDuration * scale);
+ }
+
+ /**
+ * When this animation should start. When the start time is set to
+ * {@link #START_ON_FIRST_FRAME}, the animation will start the first time
+ * {@link #getTransformation(long, Transformation)} is invoked. The time passed
+ * to this method should be obtained by calling
+ * {@link AnimationUtils#currentAnimationTimeMillis()} instead of
+ * {@link System#currentTimeMillis()}.
+ *
+ * @param startTimeMillis the start time in milliseconds
+ */
+ public void setStartTime(long startTimeMillis) {
+ mStartTime = startTimeMillis;
+ mStarted = mEnded = false;
+ mCycleFlip = false;
+ mRepeated = 0;
+ mMore = true;
+ }
+
+ /**
+ * Convenience method to start the animation the first time
+ * {@link #getTransformation(long, Transformation)} is invoked.
+ */
+ public void start() {
+ setStartTime(-1);
+ }
+
+ /**
+ * Convenience method to start the animation at the current time in
+ * milliseconds.
+ */
+ public void startNow() {
+ setStartTime(AnimationUtils.currentAnimationTimeMillis());
+ }
+
+ /**
+ * Defines what this animation should do when it reaches the end. This
+ * setting is applied only when the repeat count is either greater than
+ * 0 or {@link #INFINITE}. Defaults to {@link #RESTART}.
+ *
+ * @param repeatMode {@link #RESTART} or {@link #REVERSE}
+ * @attr ref android.R.styleable#Animation_repeatMode
+ */
+ public void setRepeatMode(int repeatMode) {
+ mRepeatMode = repeatMode;
+ }
+
+ /**
+ * Sets how many times the animation should be repeated. If the repeat
+ * count is 0, the animation is never repeated. If the repeat count is
+ * greater than 0 or {@link #INFINITE}, the repeat mode will be taken
+ * into account. The repeat count if 0 by default.
+ *
+ * @param repeatCount the number of times the animation should be repeated
+ * @attr ref android.R.styleable#Animation_repeatCount
+ */
+ public void setRepeatCount(int repeatCount) {
+ if (repeatCount < 0) {
+ repeatCount = INFINITE;
+ }
+ mRepeatCount = repeatCount;
+ }
+
+ /**
+ * If fillBefore is true, this animation will apply its transformation
+ * before the start time of the animation. Defaults to false if not set.
+ * Note that this applies when using an {@link
+ * android.view.animation.AnimationSet AnimationSet} to chain
+ * animations. The transformation is not applied before the AnimationSet
+ * itself starts.
+ *
+ * @param fillBefore true if the animation should apply its transformation before it starts
+ * @attr ref android.R.styleable#Animation_fillBefore
+ */
+ public void setFillBefore(boolean fillBefore) {
+ mFillBefore = fillBefore;
+ }
+
+ /**
+ * If fillAfter is true, the transformation that this animation performed
+ * will persist when it is finished. Defaults to false if not set.
+ * Note that this applies when using an {@link
+ * android.view.animation.AnimationSet AnimationSet} to chain
+ * animations. The transformation is not applied before the AnimationSet
+ * itself starts.
+ *
+ * @param fillAfter true if the animation should apply its transformation after it ends
+ * @attr ref android.R.styleable#Animation_fillAfter
+ */
+ public void setFillAfter(boolean fillAfter) {
+ mFillAfter = fillAfter;
+ }
+
+ /**
+ * Set the Z ordering mode to use while running the animation.
+ *
+ * @param zAdjustment The desired mode, one of {@link #ZORDER_NORMAL},
+ * {@link #ZORDER_TOP}, or {@link #ZORDER_BOTTOM}.
+ * @attr ref android.R.styleable#Animation_zAdjustment
+ */
+ public void setZAdjustment(int zAdjustment) {
+ mZAdjustment = zAdjustment;
+ }
+
+ /**
+ * Gets the acceleration curve type for this animation.
+ *
+ * @return the {@link Interpolator} associated to this animation
+ * @attr ref android.R.styleable#Animation_interpolator
+ */
+ public Interpolator getInterpolator() {
+ return mInterpolator;
+ }
+
+ /**
+ * When this animation should start. If the animation has not startet yet,
+ * this method might return {@link #START_ON_FIRST_FRAME}.
+ *
+ * @return the time in milliseconds when the animation should start or
+ * {@link #START_ON_FIRST_FRAME}
+ */
+ public long getStartTime() {
+ return mStartTime;
+ }
+
+ /**
+ * How long this animation should last
+ *
+ * @return the duration in milliseconds of the animation
+ * @attr ref android.R.styleable#Animation_duration
+ */
+ public long getDuration() {
+ return mDuration;
+ }
+
+ /**
+ * When this animation should start, relative to StartTime
+ *
+ * @return the start offset in milliseconds
+ * @attr ref android.R.styleable#Animation_startOffset
+ */
+ public long getStartOffset() {
+ return mStartOffset;
+ }
+
+ /**
+ * Defines what this animation should do when it reaches the end.
+ *
+ * @return either one of {@link #REVERSE} or {@link #RESTART}
+ * @attr ref android.R.styleable#Animation_repeatMode
+ */
+ public int getRepeatMode() {
+ return mRepeatMode;
+ }
+
+ /**
+ * Defines how many times the animation should repeat. The default value
+ * is 0.
+ *
+ * @return the number of times the animation should repeat, or {@link #INFINITE}
+ * @attr ref android.R.styleable#Animation_repeatCount
+ */
+ public int getRepeatCount() {
+ return mRepeatCount;
+ }
+
+ /**
+ * If fillBefore is true, this animation will apply its transformation
+ * before the start time of the animation.
+ *
+ * @return true if the animation applies its transformation before it starts
+ * @attr ref android.R.styleable#Animation_fillBefore
+ */
+ public boolean getFillBefore() {
+ return mFillBefore;
+ }
+
+ /**
+ * If fillAfter is true, this animation will apply its transformation
+ * after the end time of the animation.
+ *
+ * @return true if the animation applies its transformation after it ends
+ * @attr ref android.R.styleable#Animation_fillAfter
+ */
+ public boolean getFillAfter() {
+ return mFillAfter;
+ }
+
+ /**
+ * Returns the Z ordering mode to use while running the animation as
+ * previously set by {@link #setZAdjustment}.
+ *
+ * @return Returns one of {@link #ZORDER_NORMAL},
+ * {@link #ZORDER_TOP}, or {@link #ZORDER_BOTTOM}.
+ * @attr ref android.R.styleable#Animation_zAdjustment
+ */
+ public int getZAdjustment() {
+ return mZAdjustment;
+ }
+
+ /**
+ * <p>Indicates whether or not this animation will affect the transformation
+ * matrix. For instance, a fade animation will not affect the matrix whereas
+ * a scale animation will.</p>
+ *
+ * @return true if this animation will change the transformation matrix
+ */
+ public boolean willChangeTransformationMatrix() {
+ // assume we will change the matrix
+ return true;
+ }
+
+ /**
+ * <p>Indicates whether or not this animation will affect the bounds of the
+ * animated view. For instance, a fade animation will not affect the bounds
+ * whereas a 200% scale animation will.</p>
+ *
+ * @return true if this animation will change the view's bounds
+ */
+ public boolean willChangeBounds() {
+ // assume we will change the bounds
+ return true;
+ }
+
+ /**
+ * <p>Binds an animation listener to this animation. The animation listener
+ * is notified of animation events such as the end of the animation or the
+ * repetition of the animation.</p>
+ *
+ * @param listener the animation listener to be notified
+ */
+ public void setAnimationListener(AnimationListener listener) {
+ mListener = listener;
+ }
+
+ /**
+ * Gurantees that this animation has an interpolator. Will use
+ * a AccelerateDecelerateInterpolator is nothing else was specified.
+ */
+ protected void ensureInterpolator() {
+ if (mInterpolator == null) {
+ mInterpolator = new AccelerateDecelerateInterpolator();
+ }
+ }
+
+ /**
+ * Gets the transformation to apply at a specified point in time. Implementations of this
+ * method should always replace the specified Transformation or document they are doing
+ * otherwise.
+ *
+ * @param currentTime Where we are in the animation. This is wall clock time.
+ * @param outTransformation A tranformation object that is provided by the
+ * caller and will be filled in by the animation.
+ * @return True if the animation is still running
+ */
+ public boolean getTransformation(long currentTime, Transformation outTransformation) {
+ if (mStartTime == -1) {
+ mStartTime = currentTime;
+ }
+
+ final long startOffset = getStartOffset();
+ float normalizedTime = ((float) (currentTime - (mStartTime + startOffset))) /
+ (float) mDuration;
+
+ boolean expired = normalizedTime >= 1.0f;
+ // Pin time to 0.0 to 1.0 range
+ normalizedTime = Math.max(Math.min(normalizedTime, 1.0f), 0.0f);
+ mMore = !expired;
+
+ if ((normalizedTime >= 0.0f || mFillBefore) && (normalizedTime <= 1.0f || mFillAfter)) {
+ if (!mStarted) {
+ if (mListener != null) {
+ mListener.onAnimationStart(this);
+ }
+ mStarted = true;
+ }
+
+ if (mCycleFlip) {
+ normalizedTime = 1.0f - normalizedTime;
+ }
+
+ final float interpolatedTime = mInterpolator.getInterpolation(normalizedTime);
+ applyTransformation(interpolatedTime, outTransformation);
+ }
+
+ if (expired) {
+ if (mRepeatCount == mRepeated) {
+ if (!mEnded) {
+ if (mListener != null) {
+ mListener.onAnimationEnd(this);
+ }
+ mEnded = true;
+ }
+ } else {
+ if (mRepeatCount > 0) {
+ mRepeated++;
+ }
+
+ if (mRepeatMode == REVERSE) {
+ mCycleFlip = !mCycleFlip;
+ }
+
+ mStartTime = -1;
+ mMore = true;
+
+ if (mListener != null) {
+ mListener.onAnimationRepeat(this);
+ }
+ }
+ }
+
+ return mMore;
+ }
+
+ /**
+ * <p>Indicates whether this animation has started or not.</p>
+ *
+ * @return true if the animation has started, false otherwise
+ */
+ public boolean hasStarted() {
+ return mStarted;
+ }
+
+ /**
+ * <p>Indicates whether this animation has ended or not.</p>
+ *
+ * @return true if the animation has ended, false otherwise
+ */
+ public boolean hasEnded() {
+ return mEnded;
+ }
+
+ /**
+ * Helper for getTransformation. Subclasses should implement this to apply
+ * their transforms given an interpolation value. Implementations of this
+ * method should always replace the specified Transformation or document
+ * they are doing otherwise.
+ *
+ * @param interpolatedTime The value of the normalized time (0.0 to 1.0)
+ * after it has been run through the interpolation function.
+ * @param t The Transofrmation object to fill in with the current
+ * transforms.
+ */
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ }
+
+ /**
+ * Convert the information in the description of a size to an actual
+ * dimension
+ *
+ * @param type One of Animation.ABSOLUTE, Animation.RELATIVE_TO_SELF, or
+ * Animation.RELATIVE_TO_PARENT.
+ * @param value The dimension associated with the type parameter
+ * @param size The size of the object being animated
+ * @param parentSize The size of the parent of the object being animated
+ * @return The dimension to use for the animation
+ */
+ protected float resolveSize(int type, float value, int size, int parentSize) {
+ switch (type) {
+ case ABSOLUTE:
+ return value;
+ case RELATIVE_TO_SELF:
+ return size * value;
+ case RELATIVE_TO_PARENT:
+ return parentSize * value;
+ default:
+ return value;
+ }
+ }
+
+ /**
+ * Utility class to parse a string description of a size.
+ */
+ protected static class Description {
+ /**
+ * One of Animation.ABSOLUTE, Animation.RELATIVE_TO_SELF, or
+ * Animation.RELATIVE_TO_PARENT.
+ */
+ public int type;
+
+ /**
+ * The absolute or relative dimension for this Description.
+ */
+ public float value;
+
+ /**
+ * Size descriptions can appear inthree forms:
+ * <ol>
+ * <li>An absolute size. This is represented by a number.</li>
+ * <li>A size relative to the size of the object being animated. This
+ * is represented by a number followed by "%".</li> *
+ * <li>A size relative to the size of the parent of object being
+ * animated. This is represented by a number followed by "%p".</li>
+ * </ol>
+ * @param value The typed value to parse
+ * @return The parsed version of the description
+ */
+ static Description parseValue(TypedValue value) {
+ Description d = new Description();
+ if (value == null) {
+ d.type = ABSOLUTE;
+ d.value = 0;
+ } else {
+ if (value.type == TypedValue.TYPE_FRACTION) {
+ d.type = (value.data & TypedValue.COMPLEX_UNIT_MASK) ==
+ TypedValue.COMPLEX_UNIT_FRACTION_PARENT ?
+ RELATIVE_TO_PARENT : RELATIVE_TO_SELF;
+ d.value = TypedValue.complexToFloat(value.data);
+ return d;
+ } else if (value.type == TypedValue.TYPE_FLOAT) {
+ d.type = ABSOLUTE;
+ d.value = value.getFloat();
+ return d;
+ } else if (value.type >= TypedValue.TYPE_FIRST_INT &&
+ value.type <= TypedValue.TYPE_LAST_INT) {
+ d.type = ABSOLUTE;
+ d.value = value.data;
+ return d;
+ }
+ }
+
+ d.type = ABSOLUTE;
+ d.value = 0.0f;
+
+ return d;
+ }
+ }
+
+ /**
+ * <p>An animation listener receives notifications from an animation.
+ * Notifications indicate animation related events, such as the end or the
+ * repetition of the animation.</p>
+ */
+ public static interface AnimationListener {
+ /**
+ * <p>Notifies the start of the animation.</p>
+ *
+ * @param animation The started animation.
+ */
+ void onAnimationStart(Animation animation);
+
+ /**
+ * <p>Notifies the end of the animation. This callback is invoked
+ * only for animation with repeat mode set to NO_REPEAT.</p>
+ *
+ * @param animation The animation which reached its end.
+ */
+ void onAnimationEnd(Animation animation);
+
+ /**
+ * <p>Notifies the repetition of the animation. This callback is invoked
+ * only for animation with repeat mode set to RESTART or REVERSE.</p>
+ *
+ * @param animation The animation which was repeated.
+ */
+ void onAnimationRepeat(Animation animation);
+ }
+}
diff --git a/core/java/android/view/animation/AnimationSet.java b/core/java/android/view/animation/AnimationSet.java
new file mode 100644
index 0000000..3c5920f
--- /dev/null
+++ b/core/java/android/view/animation/AnimationSet.java
@@ -0,0 +1,380 @@
+/*
+ * 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.view.animation;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents a group of Animations that should be played together.
+ * The transformation of each individual animation are composed
+ * together into a single transform.
+ * If AnimationSet sets any properties that its children also set
+ * (for example, duration or fillBefore), the values of AnimationSet
+ * override the child values.
+ */
+public class AnimationSet extends Animation {
+ private static final int PROPERTY_FILL_AFTER_MASK = 0x1;
+ private static final int PROPERTY_FILL_BEFORE_MASK = 0x2;
+ private static final int PROPERTY_REPEAT_MODE_MASK = 0x4;
+ private static final int PROPERTY_START_OFFSET_MASK = 0x8;
+ private static final int PROPERTY_SHARE_INTERPOLATOR_MASK = 0x10;
+ private static final int PROPERTY_DURATION_MASK = 0x20;
+ private static final int PROPERTY_MORPH_MATRIX_MASK = 0x40;
+
+ private int mFlags = 0;
+
+ private ArrayList<Animation> mAnimations = new ArrayList<Animation>();
+
+ private Transformation mTempTransformation = new Transformation();
+
+ private long mLastEnd;
+
+ private long[] mStoredOffsets;
+
+ /**
+ * Constructor used whan an AnimationSet is loaded from a resource.
+ *
+ * @param context Application context to use
+ * @param attrs Attribute set from which to read values
+ */
+ public AnimationSet(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ TypedArray a =
+ context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.AnimationSet);
+
+ setFlag(PROPERTY_SHARE_INTERPOLATOR_MASK,
+ a.getBoolean(com.android.internal.R.styleable.AnimationSet_shareInterpolator, true));
+ init();
+
+ a.recycle();
+ }
+
+
+ /**
+ * Constructor to use when building an AnimationSet from code
+ *
+ * @param shareInterpolator Pass true if all of the animations in this set
+ * should use the interpolator assocciated with this AnimationSet.
+ * Pass false if each animation should use its own interpolator.
+ */
+ public AnimationSet(boolean shareInterpolator) {
+ setFlag(PROPERTY_SHARE_INTERPOLATOR_MASK, shareInterpolator);
+ init();
+ }
+
+ private void setFlag(int mask, boolean value) {
+ if (value) {
+ mFlags |= mask;
+ } else {
+ mFlags &= ~mask;
+ }
+ }
+
+ private void init() {
+ mStartTime = 0;
+ mDuration = 0;
+ }
+
+ @Override
+ public void setFillAfter(boolean fillAfter) {
+ mFlags |= PROPERTY_FILL_AFTER_MASK;
+ super.setFillAfter(fillAfter);
+ }
+
+ @Override
+ public void setFillBefore(boolean fillBefore) {
+ mFlags |= PROPERTY_FILL_BEFORE_MASK;
+ super.setFillBefore(fillBefore);
+ }
+
+ @Override
+ public void setRepeatMode(int repeatMode) {
+ mFlags |= PROPERTY_REPEAT_MODE_MASK;
+ super.setRepeatMode(repeatMode);
+ }
+
+ @Override
+ public void setStartOffset(long startOffset) {
+ mFlags |= PROPERTY_START_OFFSET_MASK;
+ super.setStartOffset(startOffset);
+ }
+
+ /**
+ * <p>Sets the duration of every child animation.</p>
+ *
+ * @param durationMillis the duration of the animation, in milliseconds, for
+ * every child in this set
+ */
+ @Override
+ public void setDuration(long durationMillis) {
+ mFlags |= PROPERTY_DURATION_MASK;
+ super.setDuration(durationMillis);
+ }
+
+ /**
+ * Add a child animation to this animation set.
+ * The transforms of the child animations are applied in the order
+ * that they were added
+ * @param a Animation to add.
+ */
+ public void addAnimation(Animation a) {
+ mAnimations.add(a);
+
+ boolean noMatrix = (mFlags & PROPERTY_MORPH_MATRIX_MASK) == 0;
+ if (noMatrix && a.willChangeTransformationMatrix()) {
+ mFlags |= PROPERTY_MORPH_MATRIX_MASK;
+ }
+
+ if (mAnimations.size() == 1) {
+ mDuration = a.getStartOffset() + a.getDuration();
+ mLastEnd = mStartOffset + mDuration;
+ } else {
+ mLastEnd = Math.max(mLastEnd, a.getStartOffset() + a.getDuration());
+ mDuration = mLastEnd - mStartOffset;
+ }
+ }
+
+ /**
+ * Sets the start time of this animation and all child animations
+ *
+ * @see android.view.animation.Animation#setStartTime(long)
+ */
+ @Override
+ public void setStartTime(long startTimeMillis) {
+ super.setStartTime(startTimeMillis);
+
+ final int count = mAnimations.size();
+ final ArrayList<Animation> animations = mAnimations;
+
+ for (int i = 0; i < count; i++) {
+ Animation a = animations.get(i);
+ a.setStartTime(startTimeMillis);
+ }
+ }
+
+ @Override
+ public long getStartTime() {
+ long startTime = Long.MAX_VALUE;
+
+ final int count = mAnimations.size();
+ final ArrayList<Animation> animations = mAnimations;
+
+ for (int i = 0; i < count; i++) {
+ Animation a = animations.get(i);
+ startTime = Math.min(startTime, a.getStartTime());
+ }
+
+ return startTime;
+ }
+
+ @Override
+ public void restrictDuration(long durationMillis) {
+ super.restrictDuration(durationMillis);
+
+ final ArrayList<Animation> animations = mAnimations;
+ int count = animations.size();
+
+ for (int i = 0; i < count; i++) {
+ animations.get(i).restrictDuration(durationMillis);
+ }
+ }
+
+ /**
+ * The duration of an AnimationSet is defined to be the
+ * duration of the longest child animation.
+ *
+ * @see android.view.animation.Animation#getDuration()
+ */
+ @Override
+ public long getDuration() {
+ final ArrayList<Animation> animations = mAnimations;
+ final int count = animations.size();
+ long duration = 0;
+
+ boolean durationSet = (mFlags & PROPERTY_DURATION_MASK) == PROPERTY_DURATION_MASK;
+ if (durationSet) {
+ duration = mDuration;
+ } else {
+ for (int i = 0; i < count; i++) {
+ duration = Math.max(duration, animations.get(i).getDuration());
+ }
+ }
+
+ return duration;
+ }
+
+ /**
+ * The transformation of an animation set is the concatenation of all of its
+ * component animations.
+ *
+ * @see android.view.animation.Animation#getTransformation
+ */
+ @Override
+ public boolean getTransformation(long currentTime, Transformation t) {
+ final int count = mAnimations.size();
+ final ArrayList<Animation> animations = mAnimations;
+ final Transformation temp = mTempTransformation;
+
+ boolean more = false;
+ boolean started = false;
+ boolean ended = true;
+
+ t.clear();
+
+ for (int i = count - 1; i >= 0; --i) {
+ final Animation a = animations.get(i);
+
+ temp.clear();
+ more = a.getTransformation(currentTime, temp) || more;
+ t.compose(temp);
+
+ started = started || a.hasStarted();
+ ended = a.hasEnded() && ended;
+ }
+
+ if (started && !mStarted) {
+ if (mListener != null) {
+ mListener.onAnimationStart(this);
+ }
+ mStarted = true;
+ }
+
+ if (ended != mEnded) {
+ if (mListener != null) {
+ mListener.onAnimationEnd(this);
+ }
+ mEnded = ended;
+ }
+
+ return more;
+ }
+
+ /**
+ * @see android.view.animation.Animation#scaleCurrentDuration(float)
+ */
+ @Override
+ public void scaleCurrentDuration(float scale) {
+ final ArrayList<Animation> animations = mAnimations;
+ int count = animations.size();
+ for (int i = 0; i < count; i++) {
+ animations.get(i).scaleCurrentDuration(scale);
+ }
+ }
+
+ /**
+ * @see android.view.animation.Animation#initialize(int, int, int, int)
+ */
+ @Override
+ public void initialize(int width, int height, int parentWidth, int parentHeight) {
+ super.initialize(width, height, parentWidth, parentHeight);
+
+ boolean durationSet = (mFlags & PROPERTY_DURATION_MASK) == PROPERTY_DURATION_MASK;
+ boolean fillAfterSet = (mFlags & PROPERTY_FILL_AFTER_MASK) == PROPERTY_FILL_AFTER_MASK;
+ boolean fillBeforeSet = (mFlags & PROPERTY_FILL_BEFORE_MASK) == PROPERTY_FILL_BEFORE_MASK;
+ boolean repeatModeSet = (mFlags & PROPERTY_REPEAT_MODE_MASK) == PROPERTY_REPEAT_MODE_MASK;
+ boolean shareInterpolator = (mFlags & PROPERTY_SHARE_INTERPOLATOR_MASK)
+ == PROPERTY_SHARE_INTERPOLATOR_MASK;
+ boolean startOffsetSet = (mFlags & PROPERTY_START_OFFSET_MASK)
+ == PROPERTY_START_OFFSET_MASK;
+
+ if (shareInterpolator) {
+ ensureInterpolator();
+ }
+
+ final ArrayList<Animation> children = mAnimations;
+ final int count = children.size();
+
+ final long duration = mDuration;
+ final boolean fillAfter = mFillAfter;
+ final boolean fillBefore = mFillBefore;
+ final int repeatMode = mRepeatMode;
+ final Interpolator interpolator = mInterpolator;
+ final long startOffset = mStartOffset;
+
+ for (int i = 0; i < count; i++) {
+ Animation a = children.get(i);
+ if (durationSet) {
+ a.setDuration(duration);
+ }
+ if (fillAfterSet) {
+ a.setFillAfter(fillAfter);
+ }
+ if (fillBeforeSet) {
+ a.setFillBefore(fillBefore);
+ }
+ if (repeatModeSet) {
+ a.setRepeatMode(repeatMode);
+ }
+ if (shareInterpolator) {
+ a.setInterpolator(interpolator);
+ }
+ if (startOffsetSet) {
+ a.setStartOffset(startOffset);
+ }
+ a.initialize(width, height, parentWidth, parentHeight);
+ }
+ }
+
+ /**
+ * @hide
+ * @param startOffset the startOffset to add to the children's startOffset
+ */
+ void saveChildrenStartOffset(long startOffset) {
+ final ArrayList<Animation> children = mAnimations;
+ final int count = children.size();
+ long[] storedOffsets = mStoredOffsets = new long[count];
+
+ for (int i = 0; i < count; i++) {
+ Animation animation = children.get(i);
+ long offset = animation.getStartOffset();
+ animation.setStartOffset(offset + startOffset);
+ storedOffsets[i] = offset;
+ }
+ }
+
+ /**
+ * @hide
+ */
+ void restoreChildrenStartOffset() {
+ final ArrayList<Animation> children = mAnimations;
+ final int count = children.size();
+ final long[] offsets = mStoredOffsets;
+
+ for (int i = 0; i < count; i++) {
+ children.get(i).setStartOffset(offsets[i]);
+ }
+ }
+
+ /**
+ * @return All the child animations in this AnimationSet. Note that
+ * this may include other AnimationSets, which are not expanded.
+ */
+ public List<Animation> getAnimations() {
+ return mAnimations;
+ }
+
+ @Override
+ public boolean willChangeTransformationMatrix() {
+ return (mFlags & PROPERTY_MORPH_MATRIX_MASK) == PROPERTY_MORPH_MATRIX_MASK;
+ }
+}
diff --git a/core/java/android/view/animation/AnimationUtils.java b/core/java/android/view/animation/AnimationUtils.java
new file mode 100644
index 0000000..ce3cdc5
--- /dev/null
+++ b/core/java/android/view/animation/AnimationUtils.java
@@ -0,0 +1,329 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.animation;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.content.Context;
+import android.content.res.XmlResourceParser;
+import android.content.res.Resources.NotFoundException;
+import android.util.AttributeSet;
+import android.util.Xml;
+import android.os.SystemClock;
+
+import java.io.IOException;
+
+/**
+ * Defines common utilities for working with animations.
+ *
+ */
+public class AnimationUtils {
+ /**
+ * Returns the current animation time in milliseconds. This time should be used when invoking
+ * {@link Animation#setStartTime(long)}. Refer to {@link android.os.SystemClock} for more
+ * information about the different available clocks. The clock used by this method is
+ * <em>not</em> the "wall" clock (it is not {@link System#currentTimeMillis}).
+ *
+ * @return the current animation time in milliseconds
+ *
+ * @see android.os.SystemClock
+ */
+ public static long currentAnimationTimeMillis() {
+ return SystemClock.uptimeMillis();
+ }
+
+ /**
+ * Loads an {@link Animation} 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 animation cannot be loaded
+ */
+ public static Animation loadAnimation(Context context, int id)
+ throws NotFoundException {
+
+ XmlResourceParser parser = null;
+ try {
+ parser = context.getResources().getAnimation(id);
+ return createAnimationFromXml(context, parser);
+ } catch (XmlPullParserException ex) {
+ NotFoundException rnf = new NotFoundException(
+ "Can't load animation resource ID #0x"
+ + Integer.toHexString(id));
+ rnf.initCause(ex);
+ throw rnf;
+ } catch (IOException ex) {
+ NotFoundException rnf = new NotFoundException(
+ "Can't load animation resource ID #0x"
+ + Integer.toHexString(id));
+ rnf.initCause(ex);
+ throw rnf;
+ } finally {
+ if (parser != null) parser.close();
+ }
+ }
+
+ private static Animation createAnimationFromXml(Context c, XmlPullParser parser)
+ throws XmlPullParserException, IOException {
+ return createAnimationFromXml(c, parser, null, Xml.asAttributeSet(parser));
+ }
+
+ private static Animation createAnimationFromXml(Context c, XmlPullParser parser, AnimationSet parent, AttributeSet attrs)
+ throws XmlPullParserException, IOException {
+
+ Animation anim = null;
+
+ // Make sure we are on a start tag.
+ int type = parser.getEventType();
+ int depth = parser.getDepth();
+
+ while (((type=parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
+ && type != XmlPullParser.END_DOCUMENT) {
+
+ if (type != XmlPullParser.START_TAG) {
+ continue;
+ }
+
+ String name = parser.getName();
+
+ if (name.equals("set")) {
+ anim = new AnimationSet(c, attrs);
+ createAnimationFromXml(c, parser, (AnimationSet)anim, attrs);
+ } else if (name.equals("alpha")) {
+ anim = new AlphaAnimation(c, attrs);
+ } else if (name.equals("scale")) {
+ anim = new ScaleAnimation(c, attrs);
+ } else if (name.equals("rotate")) {
+ anim = new RotateAnimation(c, attrs);
+ } else if (name.equals("translate")) {
+ anim = new TranslateAnimation(c, attrs);
+ } else {
+ throw new RuntimeException("Unknown animation name: " + parser.getName());
+ }
+
+ if (parent != null) {
+ parent.addAnimation(anim);
+ }
+ }
+
+ return anim;
+
+ }
+
+ public static LayoutAnimationController loadLayoutAnimation(
+ Context context, int id) throws NotFoundException {
+
+ XmlResourceParser parser = null;
+ try {
+ parser = context.getResources().getAnimation(id);
+ return createLayoutAnimationFromXml(context, parser);
+ } catch (XmlPullParserException ex) {
+ NotFoundException rnf = new NotFoundException(
+ "Can't load animation resource ID #0x" +
+ Integer.toHexString(id));
+ rnf.initCause(ex);
+ throw rnf;
+ } catch (IOException ex) {
+ NotFoundException rnf = new NotFoundException(
+ "Can't load animation resource ID #0x" +
+ Integer .toHexString(id));
+ rnf.initCause(ex);
+ throw rnf;
+ } finally {
+ if (parser != null) parser.close();
+ }
+ }
+
+ private static LayoutAnimationController createLayoutAnimationFromXml(
+ Context c, XmlPullParser parser)
+ throws XmlPullParserException, IOException {
+ return createLayoutAnimationFromXml(c, parser,
+ Xml.asAttributeSet(parser));
+ }
+
+ private static LayoutAnimationController createLayoutAnimationFromXml(
+ Context c, XmlPullParser parser, AttributeSet attrs)
+ throws XmlPullParserException, IOException {
+
+ LayoutAnimationController controller = null;
+
+ int type;
+ int depth = parser.getDepth();
+
+ while (((type = parser.next()) != XmlPullParser.END_TAG
+ || parser.getDepth() > depth)
+ && type != XmlPullParser.END_DOCUMENT) {
+
+ if (type != XmlPullParser.START_TAG) {
+ continue;
+ }
+
+ String name = parser.getName();
+
+ if ("layoutAnimation".equals(name)) {
+ controller = new LayoutAnimationController(c, attrs);
+ } else if ("gridLayoutAnimation".equals(name)) {
+ controller = new GridLayoutAnimationController(c, attrs);
+ } else {
+ throw new RuntimeException("Unknown layout animation name: " +
+ name);
+ }
+ }
+
+ return controller;
+ }
+
+ /**
+ * Make an animation for objects becoming visible. Uses a slide and fade
+ * effect.
+ *
+ * @param c Context for loading resources
+ * @param fromLeft is the object to be animated coming from the left
+ * @return The new animation
+ */
+ public static Animation makeInAnimation(Context c, boolean fromLeft)
+ {
+
+ Animation a;
+ if (fromLeft) {
+ a = AnimationUtils.loadAnimation(c, com.android.internal.R.anim.slide_in_left);
+ } else {
+ a = AnimationUtils.loadAnimation(c, com.android.internal.R.anim.slide_in_right);
+ }
+
+ a.setInterpolator(new DecelerateInterpolator());
+ a.setStartTime(currentAnimationTimeMillis());
+ return a;
+ }
+
+ /**
+ * Make an animation for objects becoming invisible. Uses a slide and fade
+ * effect.
+ *
+ * @param c Context for loading resources
+ * @param toRight is the object to be animated exiting to the right
+ * @return The new animation
+ */
+ public static Animation makeOutAnimation(Context c, boolean toRight)
+ {
+
+ Animation a;
+ if (toRight) {
+ a = AnimationUtils.loadAnimation(c, com.android.internal.R.anim.slide_out_right);
+ } else {
+ a = AnimationUtils.loadAnimation(c, com.android.internal.R.anim.slide_out_left);
+ }
+
+ a.setInterpolator(new AccelerateInterpolator());
+ a.setStartTime(currentAnimationTimeMillis());
+ return a;
+ }
+
+
+ /**
+ * Make an animation for objects becoming visible. Uses a slide up and fade
+ * effect.
+ *
+ * @param c Context for loading resources
+ * @return The new animation
+ */
+ public static Animation makeInChildBottomAnimation(Context c)
+ {
+
+ Animation a;
+ a = AnimationUtils.loadAnimation(c, com.android.internal.R.anim.slide_in_child_bottom);
+ a.setInterpolator(new AccelerateInterpolator());
+ a.setStartTime(currentAnimationTimeMillis());
+ return a;
+ }
+
+ /**
+ * Loads an {@link Interpolator} 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
+ */
+ public static Interpolator loadInterpolator(Context context, int id)
+ throws NotFoundException {
+
+ XmlResourceParser parser = null;
+ try {
+ parser = context.getResources().getAnimation(id);
+ return createInterpolatorFromXml(context, parser);
+ } catch (XmlPullParserException ex) {
+ NotFoundException rnf = new NotFoundException(
+ "Can't load animation resource ID #0x"
+ + Integer.toHexString(id));
+ rnf.initCause(ex);
+ throw rnf;
+ } catch (IOException ex) {
+ NotFoundException rnf = new NotFoundException(
+ "Can't load animation resource ID #0x"
+ + Integer.toHexString(id));
+ rnf.initCause(ex);
+ throw rnf;
+ } finally {
+ if (parser != null) parser.close();
+ }
+
+ }
+
+ private static Interpolator createInterpolatorFromXml(Context c, XmlPullParser parser)
+ throws XmlPullParserException, IOException {
+
+ Interpolator interpolator = null;
+
+ // Make sure we are on a start tag.
+ int type = parser.getEventType();
+ int depth = parser.getDepth();
+
+ while (((type=parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
+ && type != XmlPullParser.END_DOCUMENT) {
+
+ if (type != XmlPullParser.START_TAG) {
+ continue;
+ }
+
+ AttributeSet attrs = Xml.asAttributeSet(parser);
+
+ String name = parser.getName();
+
+
+ if (name.equals("linearInterpolator")) {
+ interpolator = new LinearInterpolator(c, attrs);
+ } else if (name.equals("accelerateInterpolator")) {
+ interpolator = new AccelerateInterpolator(c, attrs);
+ } else if (name.equals("decelerateInterpolator")) {
+ interpolator = new DecelerateInterpolator(c, attrs);
+ } else if (name.equals("accelerateDecelerateInterpolator")) {
+ interpolator = new AccelerateDecelerateInterpolator(c, attrs);
+ } else if (name.equals("cycleInterpolator")) {
+ interpolator = new CycleInterpolator(c, attrs);
+ } else {
+ throw new RuntimeException("Unknown interpolator name: " + parser.getName());
+ }
+
+ }
+
+ return interpolator;
+
+ }
+}
diff --git a/core/java/android/view/animation/CycleInterpolator.java b/core/java/android/view/animation/CycleInterpolator.java
new file mode 100644
index 0000000..d355c23
--- /dev/null
+++ b/core/java/android/view/animation/CycleInterpolator.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.animation;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+
+/**
+ * Repeats the animation for a specified number of cycles. The
+ * rate of change follows a sinusoidal pattern.
+ *
+ */
+public class CycleInterpolator implements Interpolator {
+ public CycleInterpolator(float cycles) {
+ mCycles = cycles;
+ }
+
+ public CycleInterpolator(Context context, AttributeSet attrs) {
+ TypedArray a =
+ context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.CycleInterpolator);
+
+ mCycles = a.getFloat(com.android.internal.R.styleable.CycleInterpolator_cycles, 1.0f);
+
+ a.recycle();
+ }
+
+ public float getInterpolation(float input) {
+ return (float)(Math.sin(2 * mCycles * Math.PI * input));
+ }
+
+ private float mCycles;
+}
diff --git a/core/java/android/view/animation/DecelerateInterpolator.java b/core/java/android/view/animation/DecelerateInterpolator.java
new file mode 100644
index 0000000..176169e
--- /dev/null
+++ b/core/java/android/view/animation/DecelerateInterpolator.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.animation;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+
+/**
+ * An interpolator where the rate of change starts out quickly and
+ * and then decelerates.
+ *
+ */
+public class DecelerateInterpolator implements Interpolator {
+ public DecelerateInterpolator() {
+ }
+
+ /**
+ * Constructor
+ *
+ * @param factor Degree to which the animation should be eased. Seting factor to 1.0f produces
+ * an upside-down y=x^2 parabola. Increasing factor above 1.0f makes exaggerates the
+ * ease-out effect (i.e., it starts even faster and ends evens slower)
+ */
+ public DecelerateInterpolator(float factor) {
+ mFactor = factor;
+ }
+
+ public DecelerateInterpolator(Context context, AttributeSet attrs) {
+ TypedArray a =
+ context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.DecelerateInterpolator);
+
+ mFactor = a.getFloat(com.android.internal.R.styleable.DecelerateInterpolator_factor, 1.0f);
+
+ a.recycle();
+ }
+
+ public float getInterpolation(float input) {
+ if (mFactor == 1.0f) {
+ return (float)(1.0f - (1.0f - input) * (1.0f - input));
+ } else {
+ return (float)(1.0f - Math.pow((1.0f - input), 2 * mFactor));
+ }
+ }
+
+ private float mFactor = 1.0f;
+}
diff --git a/core/java/android/view/animation/GridLayoutAnimationController.java b/core/java/android/view/animation/GridLayoutAnimationController.java
new file mode 100644
index 0000000..9161d8b
--- /dev/null
+++ b/core/java/android/view/animation/GridLayoutAnimationController.java
@@ -0,0 +1,424 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.animation;
+
+import android.view.View;
+import android.view.ViewGroup;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+
+import java.util.Random;
+
+/**
+ * A layout animation controller is used to animated a grid layout's children.
+ *
+ * While {@link LayoutAnimationController} relies only on the index of the child
+ * in the view group to compute the animation delay, this class uses both the
+ * X and Y coordinates of the child within a grid.
+ *
+ * In addition, the animation direction can be controlled. The default direction
+ * is <code>DIRECTION_LEFT_TO_RIGHT | DIRECTION_TOP_TO_BOTTOM</code>. You can
+ * also set the animation priority to columns or rows. The default priority is
+ * none.
+ *
+ * Information used to compute the animation delay of each child are stored
+ * in an instance of
+ * {@link android.view.animation.GridLayoutAnimationController.AnimationParameters},
+ * itself stored in the {@link android.view.ViewGroup.LayoutParams} of the view.
+ *
+ * @see LayoutAnimationController
+ * @see android.widget.GridView
+ *
+ * @attr ref android.R.styleable#GridLayoutAnimation_columnDelay
+ * @attr ref android.R.styleable#GridLayoutAnimation_rowDelay
+ * @attr ref android.R.styleable#GridLayoutAnimation_direction
+ * @attr ref android.R.styleable#GridLayoutAnimation_directionPriority
+ */
+public class GridLayoutAnimationController extends LayoutAnimationController {
+ /**
+ * Animates the children starting from the left of the grid to the right.
+ */
+ public static final int DIRECTION_LEFT_TO_RIGHT = 0x0;
+
+ /**
+ * Animates the children starting from the right of the grid to the left.
+ */
+ public static final int DIRECTION_RIGHT_TO_LEFT = 0x1;
+
+ /**
+ * Animates the children starting from the top of the grid to the bottom.
+ */
+ public static final int DIRECTION_TOP_TO_BOTTOM = 0x0;
+
+ /**
+ * Animates the children starting from the bottom of the grid to the top.
+ */
+ public static final int DIRECTION_BOTTOM_TO_TOP = 0x2;
+
+ /**
+ * Bitmask used to retrieve the horizontal component of the direction.
+ */
+ public static final int DIRECTION_HORIZONTAL_MASK = 0x1;
+
+ /**
+ * Bitmask used to retrieve the vertical component of the direction.
+ */
+ public static final int DIRECTION_VERTICAL_MASK = 0x2;
+
+ /**
+ * Rows and columns are animated at the same time.
+ */
+ public static final int PRIORITY_NONE = 0;
+
+ /**
+ * Columns are animated first.
+ */
+ public static final int PRIORITY_COLUMN = 1;
+
+ /**
+ * Rows are animated first.
+ */
+ public static final int PRIORITY_ROW = 2;
+
+ private float mColumnDelay;
+ private float mRowDelay;
+
+ private int mDirection;
+ private int mDirectionPriority;
+
+ /**
+ * Creates a new grid layout animation controller from external resources.
+ *
+ * @param context the Context the view group is running in, through which
+ * it can access the resources
+ * @param attrs the attributes of the XML tag that is inflating the
+ * layout animation controller
+ */
+ public GridLayoutAnimationController(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ TypedArray a = context.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.GridLayoutAnimation);
+
+ Animation.Description d = Animation.Description.parseValue(
+ a.peekValue(com.android.internal.R.styleable.GridLayoutAnimation_columnDelay));
+ mColumnDelay = d.value;
+ d = Animation.Description.parseValue(
+ a.peekValue(com.android.internal.R.styleable.GridLayoutAnimation_rowDelay));
+ mRowDelay = d.value;
+ //noinspection PointlessBitwiseExpression
+ mDirection = a.getInt(com.android.internal.R.styleable.GridLayoutAnimation_direction,
+ DIRECTION_LEFT_TO_RIGHT | DIRECTION_TOP_TO_BOTTOM);
+ mDirectionPriority = a.getInt(com.android.internal.R.styleable.GridLayoutAnimation_directionPriority,
+ PRIORITY_NONE);
+
+ a.recycle();
+ }
+
+ /**
+ * Creates a new layout animation controller with a delay of 50%
+ * for both rows and columns and the specified animation.
+ *
+ * @param animation the animation to use on each child of the view group
+ */
+ public GridLayoutAnimationController(Animation animation) {
+ this(animation, 0.5f, 0.5f);
+ }
+
+ /**
+ * Creates a new layout animation controller with the specified delays
+ * and the specified animation.
+ *
+ * @param animation the animation to use on each child of the view group
+ * @param columnDelay the delay by which each column animation must be offset
+ * @param rowDelay the delay by which each row animation must be offset
+ */
+ public GridLayoutAnimationController(Animation animation, float columnDelay, float rowDelay) {
+ super(animation);
+ mColumnDelay = columnDelay;
+ mRowDelay = rowDelay;
+ }
+
+ /**
+ * Returns the delay by which the children's animation are offset from one
+ * column to the other. The delay is expressed as a fraction of the
+ * animation duration.
+ *
+ * @return a fraction of the animation duration
+ *
+ * @see #setColumnDelay(float)
+ * @see #getRowDelay()
+ * @see #setRowDelay(float)
+ */
+ public float getColumnDelay() {
+ return mColumnDelay;
+ }
+
+ /**
+ * Sets the delay, as a fraction of the animation duration, by which the
+ * children's animations are offset from one column to the other.
+ *
+ * @param columnDelay a fraction of the animation duration
+ *
+ * @see #getColumnDelay()
+ * @see #getRowDelay()
+ * @see #setRowDelay(float)
+ */
+ public void setColumnDelay(float columnDelay) {
+ mColumnDelay = columnDelay;
+ }
+
+ /**
+ * Returns the delay by which the children's animation are offset from one
+ * row to the other. The delay is expressed as a fraction of the
+ * animation duration.
+ *
+ * @return a fraction of the animation duration
+ *
+ * @see #setRowDelay(float)
+ * @see #getColumnDelay()
+ * @see #setColumnDelay(float)
+ */
+ public float getRowDelay() {
+ return mRowDelay;
+ }
+
+ /**
+ * Sets the delay, as a fraction of the animation duration, by which the
+ * children's animations are offset from one row to the other.
+ *
+ * @param rowDelay a fraction of the animation duration
+ *
+ * @see #getRowDelay()
+ * @see #getColumnDelay()
+ * @see #setColumnDelay(float)
+ */
+ public void setRowDelay(float rowDelay) {
+ mRowDelay = rowDelay;
+ }
+
+ /**
+ * Returns the direction of the animation. {@link #DIRECTION_HORIZONTAL_MASK}
+ * and {@link #DIRECTION_VERTICAL_MASK} can be used to retrieve the
+ * horizontal and vertical components of the direction.
+ *
+ * @return the direction of the animation
+ *
+ * @see #setDirection(int)
+ * @see #DIRECTION_BOTTOM_TO_TOP
+ * @see #DIRECTION_TOP_TO_BOTTOM
+ * @see #DIRECTION_LEFT_TO_RIGHT
+ * @see #DIRECTION_RIGHT_TO_LEFT
+ * @see #DIRECTION_HORIZONTAL_MASK
+ * @see #DIRECTION_VERTICAL_MASK
+ */
+ public int getDirection() {
+ return mDirection;
+ }
+
+ /**
+ * Sets the direction of the animation. The direction is expressed as an
+ * integer containing a horizontal and vertical component. For instance,
+ * <code>DIRECTION_BOTTOM_TO_TOP | DIRECTION_RIGHT_TO_LEFT</code>.
+ *
+ * @param direction the direction of the animation
+ *
+ * @see #getDirection()
+ * @see #DIRECTION_BOTTOM_TO_TOP
+ * @see #DIRECTION_TOP_TO_BOTTOM
+ * @see #DIRECTION_LEFT_TO_RIGHT
+ * @see #DIRECTION_RIGHT_TO_LEFT
+ * @see #DIRECTION_HORIZONTAL_MASK
+ * @see #DIRECTION_VERTICAL_MASK
+ */
+ public void setDirection(int direction) {
+ mDirection = direction;
+ }
+
+ /**
+ * Returns the direction priority for the animation. The priority can
+ * be either {@link #PRIORITY_NONE}, {@link #PRIORITY_COLUMN} or
+ * {@link #PRIORITY_ROW}.
+ *
+ * @return the priority of the animation direction
+ *
+ * @see #setDirectionPriority(int)
+ * @see #PRIORITY_COLUMN
+ * @see #PRIORITY_NONE
+ * @see #PRIORITY_ROW
+ */
+ public int getDirectionPriority() {
+ return mDirectionPriority;
+ }
+
+ /**
+ * Specifies the direction priority of the animation. For instance,
+ * {@link #PRIORITY_COLUMN} will give priority to columns: the animation
+ * will first play on the column, then on the rows.Z
+ *
+ * @param directionPriority the direction priority of the animation
+ *
+ * @see #getDirectionPriority()
+ * @see #PRIORITY_COLUMN
+ * @see #PRIORITY_NONE
+ * @see #PRIORITY_ROW
+ */
+ public void setDirectionPriority(int directionPriority) {
+ mDirectionPriority = directionPriority;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean willOverlap() {
+ return mColumnDelay < 1.0f || mRowDelay < 1.0f;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected long getDelayForView(View view) {
+ ViewGroup.LayoutParams lp = view.getLayoutParams();
+ AnimationParameters params = (AnimationParameters) lp.layoutAnimationParameters;
+
+ if (params == null) {
+ return 0;
+ }
+
+ final int column = getTransformedColumnIndex(params);
+ final int row = getTransformedRowIndex(params);
+
+ final int rowsCount = params.rowsCount;
+ final int columnsCount = params.columnsCount;
+
+ final long duration = mAnimation.getDuration();
+ final float columnDelay = mColumnDelay * duration;
+ final float rowDelay = mRowDelay * duration;
+
+ float totalDelay;
+ long viewDelay;
+
+ if (mInterpolator == null) {
+ mInterpolator = new LinearInterpolator();
+ }
+
+ switch (mDirectionPriority) {
+ case PRIORITY_COLUMN:
+ viewDelay = (long) (row * rowDelay + column * rowsCount * rowDelay);
+ totalDelay = rowsCount * rowDelay + columnsCount * rowsCount * rowDelay;
+ break;
+ case PRIORITY_ROW:
+ viewDelay = (long) (column * columnDelay + row * columnsCount * columnDelay);
+ totalDelay = columnsCount * columnDelay + rowsCount * columnsCount * columnDelay;
+ break;
+ case PRIORITY_NONE:
+ default:
+ viewDelay = (long) (column * columnDelay + row * rowDelay);
+ totalDelay = columnsCount * columnDelay + rowsCount * rowDelay;
+ break;
+ }
+
+ float normalizedDelay = viewDelay / totalDelay;
+ normalizedDelay = mInterpolator.getInterpolation(normalizedDelay);
+
+ return (long) (normalizedDelay * totalDelay);
+ }
+
+ private int getTransformedColumnIndex(AnimationParameters params) {
+ int index;
+ switch (getOrder()) {
+ case ORDER_REVERSE:
+ index = params.columnsCount - 1 - params.column;
+ break;
+ case ORDER_RANDOM:
+ if (mRandomizer == null) {
+ mRandomizer = new Random();
+ }
+ index = (int) (params.columnsCount * mRandomizer.nextFloat());
+ break;
+ case ORDER_NORMAL:
+ default:
+ index = params.column;
+ break;
+ }
+
+ int direction = mDirection & DIRECTION_HORIZONTAL_MASK;
+ if (direction == DIRECTION_RIGHT_TO_LEFT) {
+ index = params.columnsCount - 1 - index;
+ }
+
+ return index;
+ }
+
+ private int getTransformedRowIndex(AnimationParameters params) {
+ int index;
+ switch (getOrder()) {
+ case ORDER_REVERSE:
+ index = params.rowsCount - 1 - params.row;
+ break;
+ case ORDER_RANDOM:
+ if (mRandomizer == null) {
+ mRandomizer = new Random();
+ }
+ index = (int) (params.rowsCount * mRandomizer.nextFloat());
+ break;
+ case ORDER_NORMAL:
+ default:
+ index = params.row;
+ break;
+ }
+
+ int direction = mDirection & DIRECTION_VERTICAL_MASK;
+ if (direction == DIRECTION_BOTTOM_TO_TOP) {
+ index = params.rowsCount - 1 - index;
+ }
+
+ return index;
+ }
+
+ /**
+ * The set of parameters that has to be attached to each view contained in
+ * the view group animated by the grid layout animation controller. These
+ * parameters are used to compute the start time of each individual view's
+ * animation.
+ */
+ public static class AnimationParameters extends
+ LayoutAnimationController.AnimationParameters {
+ /**
+ * The view group's column to which the view belongs.
+ */
+ public int column;
+
+ /**
+ * The view group's row to which the view belongs.
+ */
+ public int row;
+
+ /**
+ * The number of columns in the view's enclosing grid layout.
+ */
+ public int columnsCount;
+
+ /**
+ * The number of rows in the view's enclosing grid layout.
+ */
+ public int rowsCount;
+ }
+}
diff --git a/core/java/android/view/animation/Interpolator.java b/core/java/android/view/animation/Interpolator.java
new file mode 100644
index 0000000..d14c3e3
--- /dev/null
+++ b/core/java/android/view/animation/Interpolator.java
@@ -0,0 +1,39 @@
+/*
+ * 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.view.animation;
+
+/**
+ * An interpolator defines the rate of change of an animation. This allows
+ * the basic animation effects (alpha, scale, translate, rotate) to be
+ * accelerated, decelerated, repeated, etc.
+ */
+public interface Interpolator {
+
+ /**
+ * Maps a point on the timeline to a multiplier to be applied to the
+ * transformations of an animation.
+ *
+ * @param input A value between 0 and 1.0 indicating our current point
+ * in the animation where 0 represents the start and 1.0 represents
+ * the end
+ * @return The interpolation value. This value can be more than 1.0 for
+ * Interpolators which overshoot their targets, or less than 0 for
+ * Interpolators that undershoot their targets.
+ */
+ float getInterpolation(float input);
+
+}
diff --git a/core/java/android/view/animation/LayoutAnimationController.java b/core/java/android/view/animation/LayoutAnimationController.java
new file mode 100644
index 0000000..9cfa8d7
--- /dev/null
+++ b/core/java/android/view/animation/LayoutAnimationController.java
@@ -0,0 +1,573 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.animation;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.Random;
+
+/**
+ * A layout animation controller is used to animated a layout's, or a view
+ * group's, children. Each child uses the same animation but for every one of
+ * them, the animation starts at a different time. A layout animation controller
+ * is used by {@link android.view.ViewGroup} to compute the delay by which each
+ * child's animation start must be offset. The delay is computed by using
+ * characteristics of each child, like its index in the view group.
+ *
+ * This standard implementation computes the delay by multiplying a fixed
+ * amount of miliseconds by the index of the child in its parent view group.
+ * Subclasses are supposed to override
+ * {@link #getDelayForView(android.view.View)} to implement a different way
+ * of computing the delay. For instance, a
+ * {@link android.view.animation.GridLayoutAnimationController} will compute the
+ * delay based on the column and row indices of the child in its parent view
+ * group.
+ *
+ * Information used to compute the animation delay of each child are stored
+ * in an instance of
+ * {@link android.view.animation.LayoutAnimationController.AnimationParameters},
+ * itself stored in the {@link android.view.ViewGroup.LayoutParams} of the view.
+ *
+ * @attr ref android.R.styleable#LayoutAnimation_delay
+ * @attr ref android.R.styleable#LayoutAnimation_animationOrder
+ * @attr ref android.R.styleable#LayoutAnimation_interpolator
+ * @attr ref android.R.styleable#LayoutAnimation_animation
+ */
+public class LayoutAnimationController {
+ /**
+ * Distributes the animation delays in the order in which view were added
+ * to their view group.
+ */
+ public static final int ORDER_NORMAL = 0;
+
+ /**
+ * Distributes the animation delays in the reverse order in which view were
+ * added to their view group.
+ */
+ public static final int ORDER_REVERSE = 1;
+
+ /**
+ * Randomly distributes the animation delays.
+ */
+ public static final int ORDER_RANDOM = 2;
+
+ /**
+ * The animation applied on each child of the view group on which this
+ * layout animation controller is set.
+ */
+ protected Animation mAnimation;
+
+ /**
+ * The randomizer used when the order is set to random. Subclasses should
+ * use this object to avoid creating their own.
+ */
+ protected Random mRandomizer;
+
+ /**
+ * The interpolator used to interpolate the delays.
+ */
+ protected Interpolator mInterpolator;
+
+ private float mDelay;
+ private int mOrder;
+
+ private long mDuration;
+ private long mMaxDelay;
+
+ /**
+ * Creates a new layout animation controller from external resources.
+ *
+ * @param context the Context the view group is running in, through which
+ * it can access the resources
+ * @param attrs the attributes of the XML tag that is inflating the
+ * layout animation controller
+ */
+ public LayoutAnimationController(Context context, AttributeSet attrs) {
+ TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.LayoutAnimation);
+
+ Animation.Description d = Animation.Description.parseValue(
+ a.peekValue(com.android.internal.R.styleable.LayoutAnimation_delay));
+ mDelay = d.value;
+
+ mOrder = a.getInt(com.android.internal.R.styleable.LayoutAnimation_animationOrder, ORDER_NORMAL);
+
+ int resource = a.getResourceId(com.android.internal.R.styleable.LayoutAnimation_animation, 0);
+ if (resource > 0) {
+ setAnimation(context, resource);
+ }
+
+ resource = a.getResourceId(com.android.internal.R.styleable.LayoutAnimation_interpolator, 0);
+ if (resource > 0) {
+ setInterpolator(context, resource);
+ }
+
+ a.recycle();
+ }
+
+ /**
+ * Creates a new layout animation controller with a delay of 50%
+ * and the specified animation.
+ *
+ * @param animation the animation to use on each child of the view group
+ */
+ public LayoutAnimationController(Animation animation) {
+ this(animation, 0.5f);
+ }
+
+ /**
+ * Creates a new layout animation controller with the specified delay
+ * and the specified animation.
+ *
+ * @param animation the animation to use on each child of the view group
+ * @param delay the delay by which each child's animation must be offset
+ */
+ public LayoutAnimationController(Animation animation, float delay) {
+ mDelay = delay;
+ setAnimation(animation);
+ }
+
+ /**
+ * Returns the order used to compute the delay of each child's animation.
+ *
+ * @return one of {@link #ORDER_NORMAL}, {@link #ORDER_REVERSE} or
+ * {@link #ORDER_RANDOM)
+ *
+ * @attr ref android.R.styleable#LayoutAnimation_animationOrder
+ */
+ public int getOrder() {
+ return mOrder;
+ }
+
+ /**
+ * Sets the order used to compute the delay of each child's animation.
+ *
+ * @param order one of {@link #ORDER_NORMAL}, {@link #ORDER_REVERSE} or
+ * {@link #ORDER_RANDOM}
+ *
+ * @attr ref android.R.styleable#LayoutAnimation_animationOrder
+ */
+ public void setOrder(int order) {
+ mOrder = order;
+ }
+
+ /**
+ * Sets the animation to be run on each child of the view group on which
+ * this layout animation controller is .
+ *
+ * @param context the context from which the animation must be inflated
+ * @param resourceID the resource identifier of the animation
+ *
+ * @see #setAnimation(Animation)
+ * @see #getAnimation()
+ *
+ * @attr ref android.R.styleable#LayoutAnimation_animation
+ */
+ public void setAnimation(Context context, int resourceID) {
+ setAnimation(AnimationUtils.loadAnimation(context, resourceID));
+ }
+
+ /**
+ * Sets the animation to be run on each child of the view group on which
+ * this layout animation controller is .
+ *
+ * @param animation the animation to run on each child of the view group
+
+ * @see #setAnimation(android.content.Context, int)
+ * @see #getAnimation()
+ *
+ * @attr ref android.R.styleable#LayoutAnimation_animation
+ */
+ public void setAnimation(Animation animation) {
+ mAnimation = animation;
+ mAnimation.setFillBefore(true);
+ }
+
+ /**
+ * Returns the animation applied to each child of the view group on which
+ * this controller is set.
+ *
+ * @return an {@link android.view.animation.Animation} instance
+ *
+ * @see #setAnimation(android.content.Context, int)
+ * @see #setAnimation(Animation)
+ */
+ public Animation getAnimation() {
+ return mAnimation;
+ }
+
+ /**
+ * Sets the interpolator used to interpolate the delays between the
+ * children.
+ *
+ * @param context the context from which the interpolator must be inflated
+ * @param resourceID the resource identifier of the interpolator
+ *
+ * @see #getInterpolator()
+ * @see #setInterpolator(Interpolator)
+ *
+ * @attr ref android.R.styleable#LayoutAnimation_interpolator
+ */
+ public void setInterpolator(Context context, int resourceID) {
+ setInterpolator(AnimationUtils.loadInterpolator(context, resourceID));
+ }
+
+ /**
+ * Sets the interpolator used to interpolate the delays between the
+ * children.
+ *
+ * @param interpolator the interpolator
+ *
+ * @see #getInterpolator()
+ * @see #setInterpolator(Interpolator)
+ *
+ * @attr ref android.R.styleable#LayoutAnimation_interpolator
+ */
+ public void setInterpolator(Interpolator interpolator) {
+ mInterpolator = interpolator;
+ }
+
+ /**
+ * Returns the interpolator used to interpolate the delays between the
+ * children.
+ *
+ * @return an {@link android.view.animation.Interpolator}
+ */
+ public Interpolator getInterpolator() {
+ return mInterpolator;
+ }
+
+ /**
+ * Returns the delay by which the children's animation are offset. The
+ * delay is expressed as a fraction of the animation duration.
+ *
+ * @return a fraction of the animation duration
+ *
+ * @see #setDelay(float)
+ */
+ public float getDelay() {
+ return mDelay;
+ }
+
+ /**
+ * Sets the delay, as a fraction of the animation duration, by which the
+ * children's animations are offset. The general formula is:
+ *
+ * <pre>
+ * child animation delay = child index * delay * animation duration
+ * </pre>
+ *
+ * @param delay a fraction of the animation duration
+ *
+ * @see #getDelay()
+ */
+ public void setDelay(float delay) {
+ mDelay = delay;
+ }
+
+ /**
+ * Indicates whether two children's animations will overlap. Animations
+ * overlap when the delay is lower than 100% (or 1.0).
+ *
+ * @return true if animations will overlap, false otherwise
+ */
+ public boolean willOverlap() {
+ return mDelay < 1.0f;
+ }
+
+ /**
+ * Starts the animation.
+ */
+ public void start() {
+ mDuration = mAnimation.getDuration();
+ mMaxDelay = Long.MIN_VALUE;
+ mAnimation.setStartTime(-1);
+ }
+
+ /**
+ * Returns the animation to be applied to the specified view. The returned
+ * animation is delayed by an offset computed according to the information
+ * provided by
+ * {@link android.view.animation.LayoutAnimationController.AnimationParameters}.
+ * This method is called by view groups to obtain the animation to set on
+ * a specific child.
+ *
+ * @param view the view to animate
+ * @return an animation delayed by the number of milliseconds returned by
+ * {@link #getDelayForView(android.view.View)}
+ *
+ * @see #getDelay()
+ * @see #setDelay(float)
+ * @see #getDelayForView(android.view.View)
+ */
+ public final Animation getAnimationForView(View view) {
+ final long delay = getDelayForView(view);
+ mMaxDelay = Math.max(mMaxDelay, delay);
+ return new DelayedAnimation(delay, mAnimation);
+ }
+
+ /**
+ * Indicates whether the layout animation is over or not. A layout animation
+ * is considered done when the animation with the longest delay is done.
+ *
+ * @return true if all of the children's animations are over, false otherwise
+ */
+ public boolean isDone() {
+ return AnimationUtils.currentAnimationTimeMillis() >
+ mAnimation.getStartTime() + mMaxDelay + mDuration;
+ }
+
+ /**
+ * Returns the amount of milliseconds by which the specified view's
+ * animation must be delayed or offset. Subclasses should override this
+ * method to return a suitable value.
+ *
+ * This implementation returns <code>child animation delay</code>
+ * milliseconds where:
+ *
+ * <pre>
+ * child animation delay = child index * delay
+ * </pre>
+ *
+ * The index is retrieved from the
+ * {@link android.view.animation.LayoutAnimationController.AnimationParameters}
+ * found in the view's {@link android.view.ViewGroup.LayoutParams}.
+ *
+ * @param view the view for which to obtain the animation's delay
+ * @return a delay in milliseconds
+ *
+ * @see #getAnimationForView(android.view.View)
+ * @see #getDelay()
+ * @see #getTransformedIndex(android.view.animation.LayoutAnimationController.AnimationParameters)
+ * @see android.view.ViewGroup.LayoutParams
+ */
+ protected long getDelayForView(View view) {
+ ViewGroup.LayoutParams lp = view.getLayoutParams();
+ AnimationParameters params = lp.layoutAnimationParameters;
+
+ if (params == null) {
+ return 0;
+ }
+
+ final float delay = mDelay * mAnimation.getDuration();
+ final long viewDelay = (long) (getTransformedIndex(params) * delay);
+ final float totalDelay = delay * params.count;
+
+ if (mInterpolator == null) {
+ mInterpolator = new LinearInterpolator();
+ }
+
+ float normalizedDelay = viewDelay / totalDelay;
+ normalizedDelay = mInterpolator.getInterpolation(normalizedDelay);
+
+ return (long) (normalizedDelay * totalDelay);
+ }
+
+ /**
+ * Transforms the index stored in
+ * {@link android.view.animation.LayoutAnimationController.AnimationParameters}
+ * by the order returned by {@link #getOrder()}. Subclasses should override
+ * this method to provide additional support for other types of ordering.
+ * This method should be invoked by
+ * {@link #getDelayForView(android.view.View)} prior to any computation.
+ *
+ * @param params the animation parameters containing the index
+ * @return a transformed index
+ */
+ protected int getTransformedIndex(AnimationParameters params) {
+ switch (getOrder()) {
+ case ORDER_REVERSE:
+ return params.count - 1 - params.index;
+ case ORDER_RANDOM:
+ if (mRandomizer == null) {
+ mRandomizer = new Random();
+ }
+ return (int) (params.count * mRandomizer.nextFloat());
+ case ORDER_NORMAL:
+ default:
+ return params.index;
+ }
+ }
+
+ /**
+ * The set of parameters that has to be attached to each view contained in
+ * the view group animated by the layout animation controller. These
+ * parameters are used to compute the start time of each individual view's
+ * animation.
+ */
+ public static class AnimationParameters {
+ /**
+ * The number of children in the view group containing the view to which
+ * these parameters are attached.
+ */
+ public int count;
+
+ /**
+ * The index of the view to which these parameters are attached in its
+ * containing view group.
+ */
+ public int index;
+ }
+
+ /**
+ * Encapsulates an animation and delays its start offset by a specified
+ * amount. This allows to reuse the same base animation for various views
+ * and get the effect of running multiple instances of the animation at
+ * different times.
+ */
+ private static class DelayedAnimation extends Animation {
+ private final long mDelay;
+ private final Animation mAnimation;
+
+ /**
+ * Creates a new delayed animation that will delay the controller's
+ * animation by the specified delay in milliseconds.
+ *
+ * @param delay the delay in milliseconds by which to offset the
+ * @param animation the animation to delay
+ */
+ private DelayedAnimation(long delay, Animation animation) {
+ mDelay = delay;
+ mAnimation = animation;
+ }
+
+ @Override
+ public boolean isInitialized() {
+ return mAnimation.isInitialized();
+ }
+
+ @Override
+ public void initialize(int width, int height, int parentWidth, int parentHeight) {
+ mAnimation.initialize(width, height, parentWidth, parentHeight);
+ }
+
+ @Override
+ public void reset() {
+ mAnimation.reset();
+ }
+
+ @Override
+ public boolean getTransformation(long currentTime, Transformation outTransformation) {
+ final long oldOffset = mAnimation.getStartOffset();
+ final boolean isSet = mAnimation instanceof AnimationSet;
+ if (isSet) {
+ AnimationSet set = ((AnimationSet) mAnimation);
+ set.saveChildrenStartOffset(mDelay);
+ }
+ mAnimation.setStartOffset(oldOffset + mDelay);
+
+ boolean result = mAnimation.getTransformation(currentTime,
+ outTransformation);
+
+ if (isSet) {
+ AnimationSet set = ((AnimationSet) mAnimation);
+ set.restoreChildrenStartOffset();
+ }
+ mAnimation.setStartOffset(oldOffset);
+
+ return result;
+ }
+
+ @Override
+ public void setStartTime(long startTimeMillis) {
+ mAnimation.setStartTime(startTimeMillis);
+ }
+
+ @Override
+ public long getStartTime() {
+ return mAnimation.getStartTime();
+ }
+
+ @Override
+ public void setInterpolator(Interpolator i) {
+ mAnimation.setInterpolator(i);
+ }
+
+ @Override
+ public void setStartOffset(long startOffset) {
+ mAnimation.setStartOffset(startOffset);
+ }
+
+ @Override
+ public void setDuration(long durationMillis) {
+ mAnimation.setDuration(durationMillis);
+ }
+
+ @Override
+ public void scaleCurrentDuration(float scale) {
+ mAnimation.scaleCurrentDuration(scale);
+ }
+
+ @Override
+ public void setRepeatMode(int repeatMode) {
+ mAnimation.setRepeatMode(repeatMode);
+ }
+
+ @Override
+ public void setFillBefore(boolean fillBefore) {
+ mAnimation.setFillBefore(fillBefore);
+ }
+
+ @Override
+ public void setFillAfter(boolean fillAfter) {
+ mAnimation.setFillAfter(fillAfter);
+ }
+
+ @Override
+ public Interpolator getInterpolator() {
+ return mAnimation.getInterpolator();
+ }
+
+ @Override
+ public long getDuration() {
+ return mAnimation.getDuration();
+ }
+
+ @Override
+ public long getStartOffset() {
+ return mAnimation.getStartOffset() + mDelay;
+ }
+
+ @Override
+ public int getRepeatMode() {
+ return mAnimation.getRepeatMode();
+ }
+
+ @Override
+ public boolean getFillBefore() {
+ return mAnimation.getFillBefore();
+ }
+
+ @Override
+ public boolean getFillAfter() {
+ return mAnimation.getFillAfter();
+ }
+
+ @Override
+ public boolean willChangeTransformationMatrix() {
+ return mAnimation.willChangeTransformationMatrix();
+ }
+
+ @Override
+ public boolean willChangeBounds() {
+ return mAnimation.willChangeBounds();
+ }
+ }
+}
diff --git a/core/java/android/view/animation/LinearInterpolator.java b/core/java/android/view/animation/LinearInterpolator.java
new file mode 100644
index 0000000..96a039f
--- /dev/null
+++ b/core/java/android/view/animation/LinearInterpolator.java
@@ -0,0 +1,37 @@
+/*
+ * 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.view.animation;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+/**
+ * An interpolator where the rate of change is constant
+ *
+ */
+public class LinearInterpolator implements Interpolator {
+
+ public LinearInterpolator() {
+ }
+
+ public LinearInterpolator(Context context, AttributeSet attrs) {
+ }
+
+ public float getInterpolation(float input) {
+ return input;
+ }
+}
diff --git a/core/java/android/view/animation/RotateAnimation.java b/core/java/android/view/animation/RotateAnimation.java
new file mode 100644
index 0000000..2f51b91
--- /dev/null
+++ b/core/java/android/view/animation/RotateAnimation.java
@@ -0,0 +1,165 @@
+/*
+ * 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.view.animation;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+
+/**
+ * An animation that controls the rotation of an object. This rotation takes
+ * place int the X-Y plane. You can specify the point to use for the center of
+ * the rotation, where (0,0) is the top left point. If not specified, (0,0) is
+ * the default rotation point.
+ *
+ */
+public class RotateAnimation extends Animation {
+ private float mFromDegrees;
+ private float mToDegrees;
+
+ private int mPivotXType = ABSOLUTE;
+ private int mPivotYType = ABSOLUTE;
+ private float mPivotXValue = 0.0f;
+ private float mPivotYValue = 0.0f;
+
+ private float mPivotX;
+ private float mPivotY;
+
+ /**
+ * Constructor used whan an RotateAnimation is loaded from a resource.
+ *
+ * @param context Application context to use
+ * @param attrs Attribute set from which to read values
+ */
+ public RotateAnimation(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ TypedArray a = context.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.RotateAnimation);
+
+ mFromDegrees = a.getFloat(
+ com.android.internal.R.styleable.RotateAnimation_fromDegrees, 0.0f);
+ mToDegrees = a.getFloat(com.android.internal.R.styleable.RotateAnimation_toDegrees, 0.0f);
+
+ Description d = Description.parseValue(a.peekValue(
+ com.android.internal.R.styleable.RotateAnimation_pivotX));
+ mPivotXType = d.type;
+ mPivotXValue = d.value;
+
+ d = Description.parseValue(a.peekValue(
+ com.android.internal.R.styleable.RotateAnimation_pivotY));
+ mPivotYType = d.type;
+ mPivotYValue = d.value;
+
+ a.recycle();
+ }
+
+ /**
+ * Constructor to use when building a RotateAnimation from code.
+ * Default pivotX/pivotY point is (0,0).
+ *
+ * @param fromDegrees Rotation offset to apply at the start of the
+ * animation.
+ *
+ * @param toDegrees Rotation offset to apply at the end of the animation.
+ */
+ public RotateAnimation(float fromDegrees, float toDegrees) {
+ mFromDegrees = fromDegrees;
+ mToDegrees = toDegrees;
+ mPivotX = 0.0f;
+ mPivotY = 0.0f;
+ }
+
+ /**
+ * Constructor to use when building a RotateAnimation from code
+ *
+ * @param fromDegrees Rotation offset to apply at the start of the
+ * animation.
+ *
+ * @param toDegrees Rotation offset to apply at the end of the animation.
+ *
+ * @param pivotX The X coordinate of the point about which the object is
+ * being rotated, specified as an absolute number where 0 is the left
+ * edge.
+ * @param pivotY The Y coordinate of the point about which the object is
+ * being rotated, specified as an absolute number where 0 is the top
+ * edge.
+ */
+ public RotateAnimation(float fromDegrees, float toDegrees, float pivotX, float pivotY) {
+ mFromDegrees = fromDegrees;
+ mToDegrees = toDegrees;
+
+ mPivotXType = ABSOLUTE;
+ mPivotYType = ABSOLUTE;
+ mPivotXValue = pivotX;
+ mPivotYValue = pivotY;
+ }
+
+ /**
+ * Constructor to use when building a RotateAnimation from code
+ *
+ * @param fromDegrees Rotation offset to apply at the start of the
+ * animation.
+ *
+ * @param toDegrees Rotation offset to apply at the end of the animation.
+ *
+ * @param pivotXType Specifies how pivotXValue should be interpreted. One of
+ * Animation.ABSOLUTE, Animation.RELATIVE_TO_SELF, or
+ * Animation.RELATIVE_TO_PARENT.
+ * @param pivotXValue The X coordinate of the point about which the object
+ * is being rotated, specified as an absolute number where 0 is the
+ * left edge. This value can either be an absolute number if
+ * pivotXType is ABSOLUTE, or a percentage (where 1.0 is 100%)
+ * otherwise.
+ * @param pivotYType Specifies how pivotYValue should be interpreted. One of
+ * Animation.ABSOLUTE, Animation.RELATIVE_TO_SELF, or
+ * Animation.RELATIVE_TO_PARENT.
+ * @param pivotYValue The Y coordinate of the point about which the object
+ * is being rotated, specified as an absolute number where 0 is the
+ * top edge. This value can either be an absolute number if
+ * pivotYType is ABSOLUTE, or a percentage (where 1.0 is 100%)
+ * otherwise.
+ */
+ public RotateAnimation(float fromDegrees, float toDegrees, int pivotXType, float pivotXValue,
+ int pivotYType, float pivotYValue) {
+ mFromDegrees = fromDegrees;
+ mToDegrees = toDegrees;
+
+ mPivotXValue = pivotXValue;
+ mPivotXType = pivotXType;
+ mPivotYValue = pivotYValue;
+ mPivotYType = pivotYType;
+ }
+
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ float degrees = mFromDegrees + ((mToDegrees - mFromDegrees) * interpolatedTime);
+
+ if (mPivotX == 0.0f && mPivotY == 0.0f) {
+ t.getMatrix().setRotate(degrees);
+ } else {
+ t.getMatrix().setRotate(degrees, mPivotX, mPivotY);
+ }
+ }
+
+ @Override
+ public void initialize(int width, int height, int parentWidth, int parentHeight) {
+ super.initialize(width, height, parentWidth, parentHeight);
+ mPivotX = resolveSize(mPivotXType, mPivotXValue, width, parentWidth);
+ mPivotY = resolveSize(mPivotYType, mPivotYValue, height, parentHeight);
+ }
+}
diff --git a/core/java/android/view/animation/ScaleAnimation.java b/core/java/android/view/animation/ScaleAnimation.java
new file mode 100644
index 0000000..122ed6d
--- /dev/null
+++ b/core/java/android/view/animation/ScaleAnimation.java
@@ -0,0 +1,186 @@
+/*
+ * 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.view.animation;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+
+/**
+ * An animation that controls the scale of an object. You can specify the point
+ * to use for the center of scaling.
+ *
+ */
+public class ScaleAnimation extends Animation {
+ private float mFromX;
+ private float mToX;
+ private float mFromY;
+ private float mToY;
+
+ private int mPivotXType = ABSOLUTE;
+ private int mPivotYType = ABSOLUTE;
+ private float mPivotXValue = 0.0f;
+ private float mPivotYValue = 0.0f;
+
+ private float mPivotX;
+ private float mPivotY;
+
+ /**
+ * Constructor used whan an ScaleAnimation is loaded from a resource.
+ *
+ * @param context Application context to use
+ * @param attrs Attribute set from which to read values
+ */
+ public ScaleAnimation(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ TypedArray a = context.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.ScaleAnimation);
+
+ mFromX = a.getFloat(com.android.internal.R.styleable.ScaleAnimation_fromXScale, 0.0f);
+ mToX = a.getFloat(com.android.internal.R.styleable.ScaleAnimation_toXScale, 0.0f);
+
+ mFromY = a.getFloat(com.android.internal.R.styleable.ScaleAnimation_fromYScale, 0.0f);
+ mToY = a.getFloat(com.android.internal.R.styleable.ScaleAnimation_toYScale, 0.0f);
+
+ Description d = Description.parseValue(a.peekValue(
+ com.android.internal.R.styleable.ScaleAnimation_pivotX));
+ mPivotXType = d.type;
+ mPivotXValue = d.value;
+
+ d = Description.parseValue(a.peekValue(
+ com.android.internal.R.styleable.ScaleAnimation_pivotY));
+ mPivotYType = d.type;
+ mPivotYValue = d.value;
+
+ a.recycle();
+ }
+
+ /**
+ * Constructor to use when building a ScaleAnimation from code
+ *
+ * @param fromX Horizontal scaling factor to apply at the start of the
+ * animation
+ * @param toX Horizontal scaling factor to apply at the end of the animation
+ * @param fromY Vertical scaling factor to apply at the start of the
+ * animation
+ * @param toY Vertical scaling factor to apply at the end of the animation
+ */
+ public ScaleAnimation(float fromX, float toX, float fromY, float toY) {
+ mFromX = fromX;
+ mToX = toX;
+ mFromY = fromY;
+ mToY = toY;
+ mPivotX = 0;
+ mPivotY = 0;
+ }
+
+ /**
+ * Constructor to use when building a ScaleAnimation from code
+ *
+ * @param fromX Horizontal scaling factor to apply at the start of the
+ * animation
+ * @param toX Horizontal scaling factor to apply at the end of the animation
+ * @param fromY Vertical scaling factor to apply at the start of the
+ * animation
+ * @param toY Vertical scaling factor to apply at the end of the animation
+ * @param pivotX The X coordinate of the point about which the object is
+ * being scaled, specified as an absolute number where 0 is the left
+ * edge. (This point remains fixed while the object changes size.)
+ * @param pivotY The Y coordinate of the point about which the object is
+ * being scaled, specified as an absolute number where 0 is the top
+ * edge. (This point remains fixed while the object changes size.)
+ */
+ public ScaleAnimation(float fromX, float toX, float fromY, float toY,
+ float pivotX, float pivotY) {
+ mFromX = fromX;
+ mToX = toX;
+ mFromY = fromY;
+ mToY = toY;
+
+ mPivotXType = ABSOLUTE;
+ mPivotYType = ABSOLUTE;
+ mPivotXValue = pivotX;
+ mPivotYValue = pivotY;
+ }
+
+ /**
+ * Constructor to use when building a ScaleAnimation from code
+ *
+ * @param fromX Horizontal scaling factor to apply at the start of the
+ * animation
+ * @param toX Horizontal scaling factor to apply at the end of the animation
+ * @param fromY Vertical scaling factor to apply at the start of the
+ * animation
+ * @param toY Vertical scaling factor to apply at the end of the animation
+ * @param pivotXType Specifies how pivotXValue should be interpreted. One of
+ * Animation.ABSOLUTE, Animation.RELATIVE_TO_SELF, or
+ * Animation.RELATIVE_TO_PARENT.
+ * @param pivotXValue The X coordinate of the point about which the object
+ * is being scaled, specified as an absolute number where 0 is the
+ * left edge. (This point remains fixed while the object changes
+ * size.) This value can either be an absolute number if pivotXType
+ * is ABSOLUTE, or a percentage (where 1.0 is 100%) otherwise.
+ * @param pivotYType Specifies how pivotYValue should be interpreted. One of
+ * Animation.ABSOLUTE, Animation.RELATIVE_TO_SELF, or
+ * Animation.RELATIVE_TO_PARENT.
+ * @param pivotYValue The Y coordinate of the point about which the object
+ * is being scaled, specified as an absolute number where 0 is the
+ * top edge. (This point remains fixed while the object changes
+ * size.) This value can either be an absolute number if pivotYType
+ * is ABSOLUTE, or a percentage (where 1.0 is 100%) otherwise.
+ */
+ public ScaleAnimation(float fromX, float toX, float fromY, float toY,
+ int pivotXType, float pivotXValue, int pivotYType, float pivotYValue) {
+ mFromX = fromX;
+ mToX = toX;
+ mFromY = fromY;
+ mToY = toY;
+
+ mPivotXValue = pivotXValue;
+ mPivotXType = pivotXType;
+ mPivotYValue = pivotYValue;
+ mPivotYType = pivotYType;
+ }
+
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ float sx = 1.0f;
+ float sy = 1.0f;
+
+ if (mFromX != 1.0f || mToX != 1.0f) {
+ sx = mFromX + ((mToX - mFromX) * interpolatedTime);
+ }
+ if (mFromY != 1.0f || mToY != 1.0f) {
+ sy = mFromY + ((mToY - mFromY) * interpolatedTime);
+ }
+
+ if (mPivotX == 0 && mPivotY == 0) {
+ t.getMatrix().setScale(sx, sy);
+ } else {
+ t.getMatrix().setScale(sx, sy, mPivotX, mPivotY);
+ }
+ }
+
+ @Override
+ public void initialize(int width, int height, int parentWidth, int parentHeight) {
+ super.initialize(width, height, parentWidth, parentHeight);
+
+ mPivotX = resolveSize(mPivotXType, mPivotXValue, width, parentWidth);
+ mPivotY = resolveSize(mPivotYType, mPivotYValue, height, parentHeight);
+ }
+}
diff --git a/core/java/android/view/animation/Transformation.java b/core/java/android/view/animation/Transformation.java
new file mode 100644
index 0000000..c7a0cc8
--- /dev/null
+++ b/core/java/android/view/animation/Transformation.java
@@ -0,0 +1,139 @@
+/*
+ * 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.view.animation;
+
+import android.graphics.Matrix;
+
+/**
+ * Defines the transformation to be applied at
+ * one point in time of an Animation.
+ *
+ */
+public class Transformation {
+ /**
+ * Indicates a transformation that has no effect (alpha = 1 and identity matrix.)
+ */
+ public static int TYPE_IDENTITY = 0x0;
+ /**
+ * Indicates a transformation that applies an alpha only (uses an identity matrix.)
+ */
+ public static int TYPE_ALPHA = 0x1;
+ /**
+ * Indicates a transformation that applies a matrix only (alpha = 1.)
+ */
+ public static int TYPE_MATRIX = 0x2;
+ /**
+ * Indicates a transformation that applies an alpha and a matrix.
+ */
+ public static int TYPE_BOTH = TYPE_ALPHA | TYPE_MATRIX;
+
+ protected Matrix mMatrix;
+ protected float mAlpha;
+ protected int mTransformationType;
+
+ /**
+ * Creates a new transformation with alpha = 1 and the identity matrix.
+ */
+ public Transformation() {
+ clear();
+ }
+
+ /**
+ * Reset the transformation to a state that leaves the object
+ * being animated in an unmodified state. The transformation type is
+ * {@link #TYPE_BOTH} by default.
+ */
+ public void clear() {
+ if (mMatrix == null) {
+ mMatrix = new Matrix();
+ } else {
+ mMatrix.reset();
+ }
+ mAlpha = 1.0f;
+ mTransformationType = TYPE_BOTH;
+ }
+
+ /**
+ * Indicates the nature of this transformation.
+ *
+ * @return {@link #TYPE_ALPHA}, {@link #TYPE_MATRIX},
+ * {@link #TYPE_BOTH} or {@link #TYPE_IDENTITY}.
+ */
+ public int getTransformationType() {
+ return mTransformationType;
+ }
+
+ /**
+ * Sets the transformation type.
+ *
+ * @param transformationType One of {@link #TYPE_ALPHA},
+ * {@link #TYPE_MATRIX}, {@link #TYPE_BOTH} or
+ * {@link #TYPE_IDENTITY}.
+ */
+ public void setTransformationType(int transformationType) {
+ mTransformationType = transformationType;
+ }
+
+ /**
+ * Clones the specified transformation.
+ *
+ * @param t The transformation to clone.
+ */
+ public void set(Transformation t) {
+ mAlpha = t.getAlpha();
+ mMatrix.set(t.getMatrix());
+ mTransformationType = t.getTransformationType();
+ }
+
+ /**
+ * Apply this Transformation to an existing Transformation, e.g. apply
+ * a scale effect to something that has already been rotated.
+ * @param t
+ */
+ public void compose(Transformation t) {
+ mAlpha *= t.getAlpha();
+ mMatrix.preConcat(t.getMatrix());
+ }
+
+ /**
+ * @return The 3x3 Matrix representing the trnasformation to apply to the
+ * coordinates of the object being animated
+ */
+ public Matrix getMatrix() {
+ return mMatrix;
+ }
+
+ /**
+ * Sets the degree of transparency
+ * @param alpha 1.0 means fully opaqe and 0.0 means fully transparent
+ */
+ public void setAlpha(float alpha) {
+ mAlpha = alpha;
+ }
+
+ /**
+ * @return The degree of transparency
+ */
+ public float getAlpha() {
+ return mAlpha;
+ }
+
+ @Override
+ public String toString() {
+ return "Transformation{alpha=" + mAlpha + " matrix=" + mMatrix + "}";
+ }
+}
diff --git a/core/java/android/view/animation/TranslateAnimation.java b/core/java/android/view/animation/TranslateAnimation.java
new file mode 100644
index 0000000..ae21768
--- /dev/null
+++ b/core/java/android/view/animation/TranslateAnimation.java
@@ -0,0 +1,171 @@
+/*
+ * 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.view.animation;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+
+/**
+ * An animation that controls the position of an object. See the
+ * {@link android.view.animation full package} description for details and
+ * sample code.
+ *
+ */
+public class TranslateAnimation extends Animation {
+ private int mFromXType = ABSOLUTE;
+ private int mToXType = ABSOLUTE;
+
+ private int mFromYType = ABSOLUTE;
+ private int mToYType = ABSOLUTE;
+
+ private float mFromXValue = 0.0f;
+ private float mToXValue = 0.0f;
+
+ private float mFromYValue = 0.0f;
+ private float mToYValue = 0.0f;
+
+ private float mFromXDelta;
+ private float mToXDelta;
+ private float mFromYDelta;
+ private float mToYDelta;
+
+ /**
+ * Constructor used whan an ScaleAnimation is loaded from a resource.
+ *
+ * @param context Application context to use
+ * @param attrs Attribute set from which to read values
+ */
+ public TranslateAnimation(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ TypedArray a = context.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.TranslateAnimation);
+
+ Description d = Description.parseValue(a.peekValue(
+ com.android.internal.R.styleable.TranslateAnimation_fromXDelta));
+ mFromXType = d.type;
+ mFromXValue = d.value;
+
+ d = Description.parseValue(a.peekValue(
+ com.android.internal.R.styleable.TranslateAnimation_toXDelta));
+ mToXType = d.type;
+ mToXValue = d.value;
+
+ d = Description.parseValue(a.peekValue(
+ com.android.internal.R.styleable.TranslateAnimation_fromYDelta));
+ mFromYType = d.type;
+ mFromYValue = d.value;
+
+ d = Description.parseValue(a.peekValue(
+ com.android.internal.R.styleable.TranslateAnimation_toYDelta));
+ mToYType = d.type;
+ mToYValue = d.value;
+
+ a.recycle();
+ }
+
+ /**
+ * Constructor to use when building a ScaleAnimation from code
+ *
+ * @param fromXDelta Change in X coordinate to apply at the start of the
+ * animation
+ * @param toXDelta Change in X coordinate to apply at the end of the
+ * animation
+ * @param fromYDelta Change in Y coordinate to apply at the start of the
+ * animation
+ * @param toYDelta Change in Y coordinate to apply at the end of the
+ * animation
+ */
+ public TranslateAnimation(float fromXDelta, float toXDelta, float fromYDelta, float toYDelta) {
+ mFromXValue = fromXDelta;
+ mToXValue = toXDelta;
+ mFromYValue = fromYDelta;
+ mToYValue = toYDelta;
+
+ mFromXType = ABSOLUTE;
+ mToXType = ABSOLUTE;
+ mFromYType = ABSOLUTE;
+ mToYType = ABSOLUTE;
+ }
+
+ /**
+ * Constructor to use when building a ScaleAnimation from code
+ *
+ * @param fromXType Specifies how fromXValue should be interpreted. One of
+ * Animation.ABSOLUTE, Animation.RELATIVE_TO_SELF, or
+ * Animation.RELATIVE_TO_PARENT.
+ * @param fromXValue Change in X coordinate to apply at the start of the
+ * animation. This value can either be an absolute number if fromXType
+ * is ABSOLUTE, or a percentage (where 1.0 is 100%) otherwise.
+ * @param toXType Specifies how toXValue should be interpreted. One of
+ * Animation.ABSOLUTE, Animation.RELATIVE_TO_SELF, or
+ * Animation.RELATIVE_TO_PARENT.
+ * @param toXValue Change in X coordinate to apply at the end of the
+ * animation. This value can either be an absolute number if toXType
+ * is ABSOLUTE, or a percentage (where 1.0 is 100%) otherwise.
+ * @param fromYType Specifies how fromYValue should be interpreted. One of
+ * Animation.ABSOLUTE, Animation.RELATIVE_TO_SELF, or
+ * Animation.RELATIVE_TO_PARENT.
+ * @param fromYValue Change in Y coordinate to apply at the start of the
+ * animation. This value can either be an absolute number if fromYType
+ * is ABSOLUTE, or a percentage (where 1.0 is 100%) otherwise.
+ * @param toYType Specifies how toYValue should be interpreted. One of
+ * Animation.ABSOLUTE, Animation.RELATIVE_TO_SELF, or
+ * Animation.RELATIVE_TO_PARENT.
+ * @param toYValue Change in Y coordinate to apply at the end of the
+ * animation. This value can either be an absolute number if toYType
+ * is ABSOLUTE, or a percentage (where 1.0 is 100%) otherwise.
+ */
+ public TranslateAnimation(int fromXType, float fromXValue, int toXType, float toXValue,
+ int fromYType, float fromYValue, int toYType, float toYValue) {
+
+ mFromXValue = fromXValue;
+ mToXValue = toXValue;
+ mFromYValue = fromYValue;
+ mToYValue = toYValue;
+
+ mFromXType = fromXType;
+ mToXType = toXType;
+ mFromYType = fromYType;
+ mToYType = toYType;
+ }
+
+
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ float dx = mFromXDelta;
+ float dy = mFromYDelta;
+ if (mFromXDelta != mToXDelta) {
+ dx = mFromXDelta + ((mToXDelta - mFromXDelta) * interpolatedTime);
+ }
+ if (mFromYDelta != mToYDelta) {
+ dy = mFromYDelta + ((mToYDelta - mFromYDelta) * interpolatedTime);
+ }
+
+ t.getMatrix().setTranslate(dx, dy);
+ }
+
+ @Override
+ public void initialize(int width, int height, int parentWidth, int parentHeight) {
+ super.initialize(width, height, parentWidth, parentHeight);
+ mFromXDelta = resolveSize(mFromXType, mFromXValue, width, parentWidth);
+ mToXDelta = resolveSize(mToXType, mToXValue, width, parentWidth);
+ mFromYDelta = resolveSize(mFromYType, mFromYValue, height, parentHeight);
+ mToYDelta = resolveSize(mToYType, mToYValue, height, parentHeight);
+ }
+}
diff --git a/core/java/android/view/animation/package.html b/core/java/android/view/animation/package.html
new file mode 100755
index 0000000..c358047
--- /dev/null
+++ b/core/java/android/view/animation/package.html
@@ -0,0 +1,244 @@
+<html>
+<body>
+<p>Provides classes that handle tweened animations.</p>
+<p>Android provides two mechanisms
+ that you can use to create simple animations: <strong>tweened
+ animation</strong>, in which you tell Android to perform a series of simple
+ transformations (position, size, rotation, and so on) to the content of a
+ View; and <strong>frame
+ by frame animation</strong>, which loads a series of Drawable resources
+ one after the other. Both animation types can be used in any View object
+ to provide simple rotating timers, activity icons, and other useful UI elements.
+ Tweened animation is handled by this package; frame by frame animation is
+ handled by the {@link android.graphics.drawable.AnimationDrawable} class.
+ Animations do not have a pause method.</p>
+<h2>Tweened Animation<a name="tweened"></a></h2>
+<p> Android can perform simple visual transformations for you, including straight
+ line motion, size change, and transparency change, on the contents of a {@link
+ android.view.View View} object. These transformations are represented by the
+ following classes:</p>
+<ul>
+ <li> {@link android.view.animation.AlphaAnimation AlphaAnimation} (transparency
+ changes) </li>
+ <li>{@link android.view.animation.RotateAnimation RotateAnimation} (rotations) </li>
+ <li> {@link android.view.animation.ScaleAnimation ScaleAnimation} (growing
+ or shrinking) </li>
+ <li>{@link android.view.animation.TranslateAnimation TranslateAnimation}
+ (position changes) </li>
+</ul>
+<p><em>Note: tweened animation does not provide tools to help you draw shapes.</em> Tweened
+ animation is the act of applying one or more of these
+ transformations applied to the contents of a View object. So, if you have a TextView
+ with text, you can move, rotate, grow, or shrink the text. If it has a background
+ image, the background image will also be transformed along with the text. </p>
+<p>Animations are drawn in the area designated for the View at the start of the animation;
+ this area does not change to accommodate size or movement, so if your animation
+ moves or expands outside the original boundaries of your object, it will be clipped
+ to the size of the original canvas, even if the object's LayoutParams are
+ set to WRAP_CONTENT (the object will not resize to accommodate moving or expanding/shrinking
+ animations).</p>
+<h3>Step 1: Define your animation </h3>
+<p>The first step in creating a tweened animation is to define the transformations.
+ This can be done either in XML or in code. You define an animation by defining
+ the transformations that you want to occur, when they will occur, and how long
+ they should take to apply. Transformations
+ can be sequential or simultaneous&mdash;so, for example, you can have the contents
+ of a TextView move from left to right, and then rotate 180 degrees, or you can
+ have the text move and rotate simultaneously. Each transformation takes a set
+ of parameters specific for that transformation (starting size and ending size
+ for size change, starting angle and ending angle for rotation, and so on), and
+ also a set of common parameters (for instance, start time and duration). To make
+ several transformations happen simultaneously, give them the same start time;
+ to make them sequential, calculate the start time plus the duration of the preceding
+ transformation. </p>
+<p>Screen coordinates are (0,0) at the upper left hand corner, and increase as you
+ go down and to the right. </p>
+<p>Some values, such as pivotX, can be specified relative to the object itself or
+ relative to the parent. Be sure to use the proper format for what you want (&quot;50&quot;
+ for 50% relative to the parent, &quot;50%&quot; for 50% relative to itself).</p>
+<p>You can determine how a transformation is applied over time by assigning an Interpolator
+ to it. Android includes several Interpolator subclasses that specify various
+ speed curves: for instance, AccelerateInterpolator tells a transformation
+ to start slow and speed up; DecelerateInterpolator tells it to start fast than slow
+ down, and so on. </p>
+<p>If
+ you want a group of transformations to share a set of parameters (for example,
+ start time and duration), you can bundle them into an AnimationSet, which
+ defines the common parameters for all its children (and overrides any
+ values explicitly set by the children). Add your AnimationSet as
+ a child to the root AnimationSet (which serves to wrap all transformations into
+ the final animation). </p>
+<p> Here is the XML that defines a simple animation. The object will first move
+ to the right, then rotate and double in size, then move up. Note the
+ transformation start times.</p>
+<table width="100%" border="1">
+ <tr>
+ <th scope="col">XML</th>
+ <th scope="col">Equivalent Java </th>
+ </tr>
+ <tr>
+ <td><pre>&lt;set android:shareInterpolator=&quot;true&quot;
+ android:interpolator=&quot;@android:anim/accelerate_interpolator&quot;&gt;
+
+ &lt;translate android:fromXDelta=&quot;0&quot;
+ android:toXDelta=&quot;30&quot;
+ android:duration=&quot;800&quot;
+ android:fillAfter=&quot;true&quot;/&gt;
+
+ &lt;set android:duration="800"
+ android:pivotX=&quot;50%&quot;
+ android:pivotY=&quot;50%&quot; &gt;
+
+ &lt;rotate android:fromDegrees=&quot;0&quot;
+ android:toDegrees=&quot;-90&quot;
+ android:fillAfter=&quot;true&quot;
+ android:startOffset=&quot;800&quot;/&gt;
+
+ &lt;scale android:fromXScale=&quot;1.0&quot;
+ android:toXScale=&quot;2.0&quot;
+ android:fromYScale=&quot;1.0&quot;
+ android:toYScale=&quot;2.0&quot;
+ android:startOffset=&quot;800&quot; /&gt;
+ &lt;/set&gt;
+
+ &lt;translate android:toYDelta=&quot;-100&quot;
+ android:fillAfter=&quot;true&quot;
+ android:duration=&quot;800&quot;
+ android:startOffset=&quot;1600&quot;/&gt;
+&lt;/set&gt;</pre></td>
+ <td><pre>// Create root AnimationSet.
+AnimationSet rootSet = new AnimationSet(true);
+rootSet.setInterpolator(new AccelerateInterpolator());
+rootSet.setRepeatMode(Animation.NO_REPEAT);
+
+// Create and add first child, a motion animation.
+TranslateAnimation trans1 = new TranslateAnimation(0, 30, 0, 0);
+trans1.setStartOffset(0);
+trans1.setDuration(800);
+trans1.setFillAfter(true);
+rootSet.addAnimation(trans1);
+
+// Create a rotate and a size animation.
+RotateAnimation rotate = new RotateAnimation(
+ 0,
+ -90,
+ RotateAnimation.RELATIVE_TO_SELF, 0.5f,
+ RotateAnimation.RELATIVE_TO_SELF, 0.5f);
+ rotate.setFillAfter(true);
+ rotate.setDuration(800);
+
+ScaleAnimation scale = new ScaleAnimation(
+ 1, 2, 1, 2, // From x, to x, from y, to y
+ ScaleAnimation.RELATIVE_TO_SELF, 0.5f,
+ ScaleAnimation.RELATIVE_TO_SELF, 0.5f);
+ scale.setDuration(800);
+ scale.setFillAfter(true);
+
+// Add rotate and size animations to a new set,
+// then add the set to the root set.
+AnimationSet childSet = new AnimationSet(true);
+childSet.setStartOffset(800);
+childSet.addAnimation(rotate);
+childSet.addAnimation(scale);
+rootSet.addAnimation(childSet);
+
+// Add a final motion animation to the root set.
+TranslateAnimation trans2 = new TranslateAnimation(0, 0, 0, -100);
+trans2.setFillAfter(true);
+trans2.setDuration(800);
+trans2.setStartOffset(1600);
+rootSet.addAnimation(trans2);
+
+// Start the animation.
+animWindow.startAnimation(rootSet);</pre></td>
+ </tr>
+</table>
+<p>&nbsp;</p>
+<p>The following diagram shows the animation drawn from this code: </p>
+<p><img src="{@docRoot}images/tweening_example.png" alt="A tweened animation: move right, turn and grow, move up." ></p>
+<p>The previous diagram shows a few important things. One is the animation itself,
+ and the other is that the animation can get cropped if it moves out of its originally
+ defined area. To avoid this, we could have sized the TextView to fill_parent
+ for its height. </p>
+<p>If you define your animation in XML, save it in the res/anim/ folder as described
+ in <a href="{@docRoot}reference/available-resources.html#tweenedanimation">Resources</a>. That topic
+ also describes the XML tags and attributes you can use to specify transformations. </p>
+<p>Animations
+ have the following common parameters (from the Animation interface).
+ If a group of animations share the same values, you can bundle them into an AnimationSet
+ so you don't have to set these values on each one individually.</p>
+<table width="100%" border="1">
+ <tr>
+ <th scope="col">Property</th>
+ <th scope="col">XML Attribute</th>
+ <th scope="col">Java Method / </th>
+ <th scope="col">Description</th>
+ </tr>
+ <tr>
+ <td>Start time </td>
+ <td><code>android:startOffset</code></td>
+ <td><code>Animation.setStartOffset()</code> (or <code>setStartTime()</code> for absolute time)</td>
+ <td>The start time (in milliseconds) of a transformation, where 0 is the
+ start time of the root animation set. </td>
+ </tr>
+ <tr>
+ <td>Duration</td>
+ <td><code>android:duration</code></td>
+ <td><code>Animation.setDuration()</code></td>
+ <td>The duration (in milliseconds) of a transformation. </td>
+ </tr>
+ <tr>
+ <td>Fill before </td>
+ <td><code>android:fillBefore</code></td>
+ <td><code>Animation.setFillBefore()</code></td>
+ <td>True if you want this transformation to be applied at time zero, regardless
+ of your start time value (you will probably never need this). </td>
+ </tr>
+ <tr>
+ <td>Fill after </td>
+ <td><code>android:fillAfter</code></td>
+ <td><code>Animation.SetFillAfter()</code></td>
+ <td>Whether you want the transform you apply to continue after the duration
+ of the transformation has expired. If false, the original value will
+ immediately be applied when the transformation is done. So, for example,
+ if you want to make a dot move down, then right in an &quot;L&quot; shape, if this
+ value is not true, at the end of the down motion the text box will immediately
+ jump back to the top before moving right. </td>
+ </tr>
+ <tr>
+ <td>Interpolator</td>
+ <td><code>android:interpolator</code></td>
+ <td><code>Animation.SetInterpolator()</code></td>
+ <td>Which interpolator to use. </td>
+ </tr>
+ <tr>
+ <td>Repeat mode </td>
+ <td>Cannot be set in XML </td>
+ <td><code>Animation.SetRepeatMode()</code></td>
+ <td>Whether and how the animation should repeat. </td>
+ </tr>
+</table>
+<p>&nbsp; </p>
+<h3>Step 2: Load and start your animation </h3>
+<ol>
+ <li>If you've created your transformation in XML, you'll need to load it in Java
+ by calling {@link android.view.animation.AnimationUtils#loadAnimation(android.content.Context,
+ int) AnimationUtils.loadAnimation()}. </li>
+ <li>Either start the animation immediately by calling {@link android.view.View#startAnimation(android.view.animation.Animation)
+ View.startAnimation()}, or if you have specified a start time in the animation
+ parameters, you can call
+ {@link android.view.View#setAnimation(android.view.animation.Animation)
+ View.setCurrentAnimation()}.</li>
+</ol>
+<p>The following code demonstrates loading and starting an animation. </p>
+<pre>// Hook into the object to be animated.
+TextView animWindow = (TextView)findViewById(R.id.anim);
+
+// Load the animation from XML (XML file is res/anim/move_animation.xml).
+Animation anim = AnimationUtils.loadAnimation(AnimationSample.this, R.anim.move_animation);
+anim.setRepeatMode(Animation.NO_REPEAT);
+
+// Play the animation.
+animWindow.startAnimation(anim);</pre>
+</body>
+</html>
diff --git a/core/java/android/view/package.html b/core/java/android/view/package.html
new file mode 100644
index 0000000..1c58765
--- /dev/null
+++ b/core/java/android/view/package.html
@@ -0,0 +1,6 @@
+<HTML>
+<BODY>
+Provides classes that expose basic user interface classes that handle
+screen layout and interaction with the user.
+</BODY>
+</HTML> \ No newline at end of file
diff --git a/core/java/android/webkit/BrowserFrame.java b/core/java/android/webkit/BrowserFrame.java
new file mode 100644
index 0000000..e99c444
--- /dev/null
+++ b/core/java/android/webkit/BrowserFrame.java
@@ -0,0 +1,781 @@
+/*
+ * 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.webkit;
+
+import android.content.Context;
+import android.content.res.AssetManager;
+import android.graphics.Bitmap;
+import android.net.ParseException;
+import android.net.WebAddress;
+import android.net.http.SslCertificate;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Config;
+import android.util.Log;
+
+import junit.framework.Assert;
+
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.util.HashMap;
+import java.util.Iterator;
+
+class BrowserFrame extends Handler {
+
+ private static final String LOGTAG = "webkit";
+
+ /**
+ * Cap the number of LoadListeners that will be instantiated, so
+ * we don't blow the GREF count. Attempting to queue more than
+ * this many requests will prompt an error() callback on the
+ * request's LoadListener
+ */
+ private final static int MAX_OUTSTANDING_REQUESTS = 300;
+
+ private final CallbackProxy mCallbackProxy;
+ private final WebSettings mSettings;
+ private final Context mContext;
+ private final WebViewDatabase mDatabase;
+ private final WebViewCore mWebViewCore;
+ private boolean mLoadInitFromJava;
+ private String mCurrentUrl;
+ private int mLoadType;
+ private String mCompletedUrl;
+ private boolean mFirstLayoutDone = true;
+ private boolean mCommitted = true;
+
+ // Is this frame the main frame?
+ private boolean mIsMainFrame;
+
+ // Attached Javascript interfaces
+ private HashMap mJSInterfaceMap;
+
+ // message ids
+ // a message posted when a frame loading is completed
+ static final int FRAME_COMPLETED = 1001;
+ // a message posted when the user decides the policy
+ static final int POLICY_FUNCTION = 1003;
+
+ // Note: need to keep these in sync with FrameLoaderTypes.h in native
+ static final int FRAME_LOADTYPE_STANDARD = 0;
+ static final int FRAME_LOADTYPE_BACK = 1;
+ static final int FRAME_LOADTYPE_FORWARD = 2;
+ static final int FRAME_LOADTYPE_INDEXEDBACKFORWARD = 3;
+ static final int FRAME_LOADTYPE_RELOAD = 4;
+ static final int FRAME_LOADTYPE_RELOADALLOWINGSTALEDATA = 5;
+ static final int FRAME_LOADTYPE_SAME = 6;
+ static final int FRAME_LOADTYPE_REDIRECT = 7;
+ static final int FRAME_LOADTYPE_REPLACE = 8;
+
+ // A progress threshold to switch from history Picture to live Picture
+ private static final int TRANSITION_SWITCH_THRESHOLD = 75;
+
+ // This is a field accessed by native code as well as package classes.
+ /*package*/ int mNativeFrame;
+
+ // Static instance of a JWebCoreJavaBridge to handle timer and cookie
+ // requests from WebCore.
+ static JWebCoreJavaBridge sJavaBridge;
+
+ /**
+ * Create a new BrowserFrame to be used in an application.
+ * @param context An application context to use when retrieving assets.
+ * @param w A WebViewCore used as the view for this frame.
+ * @param proxy A CallbackProxy for posting messages to the UI thread and
+ * querying a client for information.
+ * @param settings A WebSettings object that holds all settings.
+ * XXX: Called by WebCore thread.
+ */
+ public BrowserFrame(Context context, WebViewCore w, CallbackProxy proxy,
+ WebSettings settings) {
+ // Create a global JWebCoreJavaBridge to handle timers and
+ // cookies in the WebCore thread.
+ if (sJavaBridge == null) {
+ sJavaBridge = new JWebCoreJavaBridge();
+ // set WebCore native cache size
+ sJavaBridge.setCacheSize(4 * 1024 * 1024);
+ // initialize CacheManager
+ CacheManager.init(context);
+ // create CookieSyncManager with current Context
+ CookieSyncManager.createInstance(context);
+ }
+ AssetManager am = context.getAssets();
+ nativeCreateFrame(am, proxy.getBackForwardList());
+ // Create a native FrameView and attach it to the native frame.
+ nativeCreateView(w);
+
+ mSettings = settings;
+ mContext = context;
+ mCallbackProxy = proxy;
+ mDatabase = WebViewDatabase.getInstance(context);
+ mWebViewCore = w;
+
+ if (Config.LOGV) {
+ Log.v(LOGTAG, "BrowserFrame constructor: this=" + this);
+ }
+ }
+
+ /**
+ * Load a url from the network or the filesystem into the main frame.
+ * Following the same behaviour as Safari, javascript: URLs are not
+ * passed to the main frame, instead they are evaluated immediately.
+ * @param url The url to load.
+ */
+ public void loadUrl(String url) {
+ mLoadInitFromJava = true;
+ if (URLUtil.isJavaScriptUrl(url)) {
+ // strip off the scheme and evaluate the string
+ stringByEvaluatingJavaScriptFromString(
+ url.substring("javascript:".length()));
+ } else {
+ if (!nativeLoadUrl(url)) {
+ reportError(android.net.http.EventHandler.ERROR_BAD_URL,
+ mContext.getString(com.android.internal.R.string.httpErrorBadUrl),
+ url);
+ }
+ }
+ mLoadInitFromJava = false;
+ }
+
+ /**
+ * Load the content as if it was loaded by the provided base URL. The
+ * failUrl is used as the history entry for the load data. If null or
+ * an empty string is passed for the failUrl, then no history entry is
+ * created.
+ *
+ * @param baseUrl Base URL used to resolve relative paths in the content
+ * @param data Content to render in the browser
+ * @param mimeType Mimetype of the data being passed in
+ * @param encoding Character set encoding of the provided data.
+ * @param failUrl URL to use if the content fails to load or null.
+ */
+ public void loadData(String baseUrl, String data, String mimeType,
+ String encoding, String failUrl) {
+ mLoadInitFromJava = true;
+ if (failUrl == null) {
+ failUrl = "";
+ }
+ if (data == null) {
+ data = "";
+ }
+
+ // Setup defaults for missing values. These defaults where taken from
+ // WebKit's WebFrame.mm
+ if (baseUrl == null || baseUrl.length() == 0) {
+ baseUrl = "about:blank";
+ }
+ if (mimeType == null || mimeType.length() == 0) {
+ mimeType = "text/html";
+ }
+ nativeLoadData(baseUrl, data, mimeType, encoding, failUrl);
+ mLoadInitFromJava = false;
+ }
+
+ /**
+ * native callback
+ * Report an error to an activity.
+ * @param errorCode The HTTP error code.
+ * @param description A String description.
+ * TODO: Report all errors including resource errors but include some kind
+ * of domain identifier. Change errorCode to an enum for a cleaner
+ * interface.
+ */
+ private void reportError(final int errorCode, final String description,
+ final String failingUrl) {
+ // As this is called for the main resource and loading will be stopped
+ // after, reset the state variables.
+ mCommitted = true;
+ mWebViewCore.mEndScaleZoom = mFirstLayoutDone == false;
+ mFirstLayoutDone = true;
+ mCallbackProxy.onReceivedError(errorCode, description, failingUrl);
+ }
+
+ /* package */boolean committed() {
+ return mCommitted;
+ }
+
+ /* package */boolean firstLayoutDone() {
+ return mFirstLayoutDone;
+ }
+
+ /* package */int loadType() {
+ return mLoadType;
+ }
+
+ /* package */String currentUrl() {
+ return mCurrentUrl;
+ }
+
+ /* package */void didFirstLayout(String url) {
+ // this is common case
+ if (url.equals(mCurrentUrl)) {
+ if (!mFirstLayoutDone) {
+ mFirstLayoutDone = true;
+ // ensure {@link WebViewCore#webkitDraw} is called as we were
+ // blocking the update in {@link #loadStarted}
+ mWebViewCore.contentInvalidate();
+ }
+ } else if (url.equals(mCompletedUrl)) {
+ /*
+ * FIXME: when loading http://www.google.com/m,
+ * mCurrentUrl will be http://www.google.com/m,
+ * mCompletedUrl will be http://www.google.com/m#search
+ * and url will be http://www.google.com/m#search.
+ * This is probably a bug in WebKit. If url matches mCompletedUrl,
+ * also set mFirstLayoutDone to be true and update.
+ */
+ if (!mFirstLayoutDone) {
+ mFirstLayoutDone = true;
+ // ensure {@link WebViewCore#webkitDraw} is called as we were
+ // blocking the update in {@link #loadStarted}
+ mWebViewCore.contentInvalidate();
+ }
+ }
+ mWebViewCore.mEndScaleZoom = true;
+ }
+
+ /**
+ * native callback
+ * Indicates the beginning of a new load.
+ * This method will be called once for the main frame.
+ */
+ private void loadStarted(String url, Bitmap favicon, int loadType,
+ boolean isMainFrame) {
+ mIsMainFrame = isMainFrame;
+
+ if (isMainFrame || loadType == FRAME_LOADTYPE_STANDARD) {
+ mCurrentUrl = url;
+ mCompletedUrl = null;
+ mLoadType = loadType;
+
+ if (isMainFrame) {
+ // Call onPageStarted for main frames.
+ mCallbackProxy.onPageStarted(url, favicon);
+ // as didFirstLayout() is only called for the main frame, reset
+ // mFirstLayoutDone only for the main frames
+ mFirstLayoutDone = false;
+ mCommitted = false;
+ // remove pending draw to block update until mFirstLayoutDone is
+ // set to true in didFirstLayout()
+ mWebViewCore.removeMessages(WebViewCore.EventHub.WEBKIT_DRAW);
+ }
+
+ // Note: only saves committed form data in standard load
+ if (loadType == FRAME_LOADTYPE_STANDARD
+ && mSettings.getSaveFormData()) {
+ final WebHistoryItem h = mCallbackProxy.getBackForwardList()
+ .getCurrentItem();
+ if (h != null) {
+ String currentUrl = h.getUrl();
+ if (currentUrl != null) {
+ mDatabase.setFormData(currentUrl, getFormTextData());
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * native callback
+ * Indicates the WebKit has committed to the new load
+ */
+ private void transitionToCommitted(int loadType, boolean isMainFrame) {
+ // loadType is not used yet
+ if (isMainFrame) {
+ mCommitted = true;
+ }
+ }
+
+ /**
+ * native callback
+ * <p>
+ * Indicates the end of a new load.
+ * This method will be called once for the main frame.
+ */
+ private void loadFinished(String url, int loadType, boolean isMainFrame) {
+ // mIsMainFrame and isMainFrame are better be equal!!!
+
+ if (isMainFrame || loadType == FRAME_LOADTYPE_STANDARD) {
+ mCompletedUrl = url;
+ if (isMainFrame) {
+ mCallbackProxy.switchOutDrawHistory();
+ mCallbackProxy.onPageFinished(url);
+ }
+ }
+ }
+
+ /**
+ * We have received an SSL certificate for the main top-level page.
+ *
+ * !!!Called from the network thread!!!
+ */
+ void certificate(SslCertificate certificate) {
+ if (mIsMainFrame) {
+ // we want to make this call even if the certificate is null
+ // (ie, the site is not secure)
+ mCallbackProxy.onReceivedCertificate(certificate);
+ }
+ }
+
+ /**
+ * Destroy all native components of the BrowserFrame.
+ */
+ public void destroy() {
+ nativeDestroyFrame();
+ removeCallbacksAndMessages(null);
+ }
+
+ /**
+ * Handle messages posted to us.
+ * @param msg The message to handle.
+ */
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case FRAME_COMPLETED: {
+ if (mSettings.getSavePassword() && hasPasswordField()) {
+ if (Config.DEBUG) {
+ Assert.assertNotNull(mCallbackProxy.getBackForwardList()
+ .getCurrentItem());
+ }
+ WebAddress uri = new WebAddress(
+ mCallbackProxy.getBackForwardList().getCurrentItem()
+ .getUrl());
+ String host = uri.mHost;
+ String[] up = mDatabase.getUsernamePassword(host);
+ if (up != null && up[0] != null) {
+ setUsernamePassword(up[0], up[1]);
+ }
+ }
+ CacheManager.trimCacheIfNeeded();
+ break;
+ }
+
+ case POLICY_FUNCTION: {
+ nativeCallPolicyFunction(msg.arg1, msg.arg2);
+ break;
+ }
+
+ default:
+ break;
+ }
+ }
+
+ /**
+ * Punch-through for WebCore to set the document
+ * title. Inform the Activity of the new title.
+ * @param title The new title of the document.
+ */
+ private void setTitle(String title) {
+ // FIXME: The activity must call getTitle (a native method) to get the
+ // title. We should try and cache the title if we can also keep it in
+ // sync with the document.
+ mCallbackProxy.onReceivedTitle(title);
+ }
+
+ /**
+ * Retrieves the render tree of this frame and puts it as the object for
+ * the message and sends the message.
+ * @param callback the message to use to send the render tree
+ */
+ public void externalRepresentation(Message callback) {
+ callback.obj = externalRepresentation();;
+ callback.sendToTarget();
+ }
+
+ /**
+ * Return the render tree as a string
+ */
+ private native String externalRepresentation();
+
+ /**
+ * Retrieves the visual text of the current frame, puts it as the object for
+ * the message and sends the message.
+ * @param callback the message to use to send the visual text
+ */
+ public void documentAsText(Message callback) {
+ callback.obj = documentAsText();;
+ callback.sendToTarget();
+ }
+
+ /**
+ * Return the text drawn on the screen as a string
+ */
+ private native String documentAsText();
+
+ /*
+ * This method is called by WebCore to inform the frame that
+ * the Javascript window object has been cleared.
+ * We should re-attach any attached js interfaces.
+ */
+ private void windowObjectCleared(int nativeFramePointer) {
+ if (mJSInterfaceMap != null) {
+ Iterator iter = mJSInterfaceMap.keySet().iterator();
+ while (iter.hasNext()) {
+ String interfaceName = (String) iter.next();
+ nativeAddJavascriptInterface(nativeFramePointer,
+ mJSInterfaceMap.get(interfaceName), interfaceName);
+ }
+ }
+ }
+
+ /**
+ * This method is called by WebCore to check whether application
+ * wants to hijack url loading
+ */
+ public boolean handleUrl(String url) {
+ if (mLoadInitFromJava == true) {
+ return false;
+ }
+ return mCallbackProxy.shouldOverrideUrlLoading(url);
+ }
+
+ public void addJavascriptInterface(Object obj, String interfaceName) {
+ if (mJSInterfaceMap == null) {
+ mJSInterfaceMap = new HashMap<String, Object>();
+ }
+ if (mJSInterfaceMap.containsKey(interfaceName)) {
+ mJSInterfaceMap.remove(interfaceName);
+ }
+ mJSInterfaceMap.put(interfaceName, obj);
+ }
+
+ /**
+ * Start loading a resource.
+ * @param loaderHandle The native ResourceLoader that is the target of the
+ * data.
+ * @param url The url to load.
+ * @param method The http method.
+ * @param headers The http headers.
+ * @param postData If the method is "POST" postData is sent as the request
+ * body.
+ * @param cacheMode The cache mode to use when loading this resource.
+ * @param isHighPriority True if this resource needs to be put at the front
+ * of the network queue.
+ * @param synchronous True if the load is synchronous.
+ * @return A newly created LoadListener object.
+ */
+ private LoadListener startLoadingResource(int loaderHandle,
+ String url,
+ String method,
+ HashMap headers,
+ String postData,
+ int cacheMode,
+ boolean isHighPriority,
+ boolean synchronous) {
+ PerfChecker checker = new PerfChecker();
+
+ if (mSettings.getCacheMode() != WebSettings.LOAD_DEFAULT) {
+ cacheMode = mSettings.getCacheMode();
+ }
+
+ if (method.equals("POST")) {
+ // Don't use the cache on POSTs when issuing a normal POST
+ // request.
+ if (cacheMode == WebSettings.LOAD_NORMAL) {
+ cacheMode = WebSettings.LOAD_NO_CACHE;
+ }
+ if (mSettings.getSavePassword() && hasPasswordField()) {
+ try {
+ if (Config.DEBUG) {
+ Assert.assertNotNull(mCallbackProxy.getBackForwardList()
+ .getCurrentItem());
+ }
+ WebAddress uri = new WebAddress(mCallbackProxy
+ .getBackForwardList().getCurrentItem().getUrl());
+ String host = uri.mHost;
+ String[] ret = getUsernamePassword();
+ if (ret != null && postData != null && ret[0].length() > 0
+ && ret[1].length() > 0
+ && postData.contains(URLEncoder.encode(ret[0]))
+ && postData.contains(URLEncoder.encode(ret[1]))) {
+ String[] saved = mDatabase.getUsernamePassword(host);
+ if (saved != null) {
+ // null username implies that user has chosen not to
+ // save password
+ if (saved[0] != null) {
+ // non-null username implies that user has
+ // chosen to save password, so update the
+ // recorded password
+ mDatabase.setUsernamePassword(host, ret[0],
+ ret[1]);
+ }
+ } else {
+ // CallbackProxy will handle creating the resume
+ // message
+ mCallbackProxy.onSavePassword(host, ret[0], ret[1],
+ null);
+ }
+ }
+ } catch (ParseException ex) {
+ // if it is bad uri, don't save its password
+ }
+ }
+ if (postData == null) {
+ postData = "";
+ }
+ }
+
+ // is this resource the main-frame top-level page?
+ boolean isMainFramePage = mIsMainFrame && url.equals(mCurrentUrl);
+
+ if (Config.LOGV) {
+ Log.v(LOGTAG, "startLoadingResource: url=" + url + ", method="
+ + method + ", postData=" + postData + ", isHighPriority="
+ + isHighPriority + ", isMainFramePage=" + isMainFramePage);
+ }
+
+ // Create a LoadListener
+ LoadListener loadListener = LoadListener.getLoadListener(mContext, this, url,
+ loaderHandle, synchronous, isMainFramePage);
+
+ mCallbackProxy.onLoadResource(url);
+
+ if (LoadListener.getNativeLoaderCount() > MAX_OUTSTANDING_REQUESTS) {
+ loadListener.error(
+ android.net.http.EventHandler.ERROR, mContext.getString(
+ com.android.internal.R.string.httpErrorTooManyRequests));
+ loadListener.notifyError();
+ loadListener.tearDown();
+ return null;
+ }
+
+ // during synchronous load, the WebViewCore thread is blocked, so we
+ // need to endCacheTransaction first so that http thread won't be
+ // blocked in setupFile() when createCacheFile.
+ if (synchronous) {
+ CacheManager.endCacheTransaction();
+ }
+
+ FrameLoader loader = new FrameLoader(loadListener,
+ mSettings.getUserAgentString(), method, isHighPriority);
+ loader.setHeaders(headers);
+ loader.setPostData(postData);
+ loader.setCacheMode(cacheMode); // Set the load mode to the mode used
+ // for the current page.
+ // Set referrer to current URL?
+ if (!loader.executeLoad()) {
+ checker.responseAlert("startLoadingResource fail");
+ }
+ checker.responseAlert("startLoadingResource succeed");
+
+ if (synchronous) {
+ CacheManager.startCacheTransaction();
+ }
+
+ return !synchronous ? loadListener : null;
+ }
+
+ /**
+ * Set the progress for the browser activity. Called by native code.
+ * Uses a delay so it does not happen too often.
+ * @param newProgress An int between zero and one hundred representing
+ * the current progress percentage of loading the page.
+ */
+ private void setProgress(int newProgress) {
+ mCallbackProxy.onProgressChanged(newProgress);
+ if (newProgress == 100) {
+ sendMessageDelayed(obtainMessage(FRAME_COMPLETED), 100);
+ }
+ // FIXME: Need to figure out a better way to switch out of the history
+ // drawing mode. Maybe we can somehow compare the history picture with
+ // the current picture, and switch when it contains more content.
+ if (mFirstLayoutDone && newProgress > TRANSITION_SWITCH_THRESHOLD) {
+ mCallbackProxy.switchOutDrawHistory();
+ }
+ }
+
+ /**
+ * Send the icon to the activity for display.
+ * @param icon A Bitmap representing a page's favicon.
+ */
+ private void didReceiveIcon(Bitmap icon) {
+ mCallbackProxy.onReceivedIcon(icon);
+ }
+
+ /**
+ * Request a new window from the client.
+ * @return The BrowserFrame object stored in the new WebView.
+ */
+ private BrowserFrame createWindow(boolean dialog, boolean userGesture) {
+ WebView w = mCallbackProxy.createWindow(dialog, userGesture);
+ if (w != null) {
+ return w.getWebViewCore().getBrowserFrame();
+ }
+ return null;
+ }
+
+ /**
+ * Try to focus this WebView.
+ */
+ private void requestFocus() {
+ mCallbackProxy.onRequestFocus();
+ }
+
+ /**
+ * Close this frame and window.
+ */
+ private void closeWindow(WebViewCore w) {
+ mCallbackProxy.onCloseWindow(w.getWebView());
+ }
+
+ // XXX: Must match PolicyAction in FrameLoaderTypes.h in webcore
+ static final int POLICY_USE = 0;
+ static final int POLICY_IGNORE = 2;
+
+ private void decidePolicyForFormResubmission(int policyFunction) {
+ Message dontResend = obtainMessage(POLICY_FUNCTION, policyFunction,
+ POLICY_IGNORE);
+ Message resend = obtainMessage(POLICY_FUNCTION, policyFunction,
+ POLICY_USE);
+ mCallbackProxy.onFormResubmission(dontResend, resend);
+ }
+
+ /**
+ * Tell the activity to update its global history.
+ */
+ private void updateVisitedHistory(String url, boolean isReload) {
+ mCallbackProxy.doUpdateVisitedHistory(url, isReload);
+ }
+
+ /**
+ * Get the CallbackProxy for sending messages to the UI thread.
+ */
+ /* package */ CallbackProxy getCallbackProxy() {
+ return mCallbackProxy;
+ }
+
+ /**
+ * Returns the User Agent used by this frame
+ */
+ String getUserAgentString() {
+ return mSettings.getUserAgentString();
+ }
+
+ //==========================================================================
+ // native functions
+ //==========================================================================
+
+ /**
+ * Create a new native frame.
+ * @param am AssetManager to use to get assets.
+ * @param list The native side will add and remove items from this list as
+ * the native list changes.
+ */
+ private native void nativeCreateFrame(AssetManager am,
+ WebBackForwardList list);
+
+ /**
+ * Create a native view attached to a WebView.
+ * @param w A WebView that the frame draws into.
+ */
+ private native void nativeCreateView(WebViewCore w);
+
+ private native void nativeCallPolicyFunction(int policyFunction,
+ int decision);
+ /**
+ * Destroy the native frame.
+ */
+ public native void nativeDestroyFrame();
+
+ /**
+ * Detach the view from the frame.
+ */
+ private native void nativeDetachView();
+
+ /**
+ * Reload the current main frame.
+ */
+ public native void reload(boolean allowStale);
+
+ /**
+ * Go back or forward the number of steps given.
+ * @param steps A negative or positive number indicating the direction
+ * and number of steps to move.
+ */
+ public native void goBackOrForward(int steps);
+
+ /**
+ * stringByEvaluatingJavaScriptFromString will execute the
+ * JS passed in in the context of this browser frame.
+ * @param script A javascript string to execute
+ *
+ * @return string result of execution or null
+ */
+ public native String stringByEvaluatingJavaScriptFromString(String script);
+
+ /**
+ * Add a javascript interface to the main frame.
+ */
+ private native void nativeAddJavascriptInterface(int nativeFramePointer,
+ Object obj, String interfaceName);
+
+ /**
+ * Enable or disable the native cache.
+ */
+ /* FIXME: The native cache is always on for now until we have a better
+ * solution for our 2 caches. */
+ private native void setCacheDisabled(boolean disabled);
+
+ public native boolean cacheDisabled();
+
+ public native void clearCache();
+
+ /**
+ * Returns false if the url is bad.
+ */
+ private native boolean nativeLoadUrl(String url);
+
+ private native void nativeLoadData(String baseUrl, String data,
+ String mimeType, String encoding, String failUrl);
+
+ /**
+ * Stop loading the current page.
+ */
+ public native void stopLoading();
+
+ /**
+ * Return true if the document has images.
+ */
+ public native boolean documentHasImages();
+
+ /**
+ * @return TRUE if there is a password field in the current frame
+ */
+ private native boolean hasPasswordField();
+
+ /**
+ * Get username and password in the current frame. If found, String[0] is
+ * username and String[1] is password. Otherwise return NULL.
+ * @return String[]
+ */
+ private native String[] getUsernamePassword();
+
+ /**
+ * Set username and password to the proper fields in the current frame
+ * @param username
+ * @param password
+ */
+ private native void setUsernamePassword(String username, String password);
+
+ /**
+ * Get form's "text" type data associated with the current frame.
+ * @return HashMap If succeed, returns a list of name/value pair. Otherwise
+ * returns null.
+ */
+ private native HashMap getFormTextData();
+}
diff --git a/core/java/android/webkit/ByteArrayBuilder.java b/core/java/android/webkit/ByteArrayBuilder.java
new file mode 100644
index 0000000..16d663c
--- /dev/null
+++ b/core/java/android/webkit/ByteArrayBuilder.java
@@ -0,0 +1,134 @@
+/*
+ * 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.webkit;
+
+import java.util.LinkedList;
+
+/** Utility class optimized for accumulating bytes, and then spitting
+ them back out. It does not optimize for returning the result in a
+ single array, though this is supported in the API. It is fastest
+ if the retrieval can be done via iterating through chunks.
+
+ Things to add:
+ - consider dynamically increasing our min_capacity,
+ as we see mTotalSize increase
+*/
+class ByteArrayBuilder {
+
+ private static final int DEFAULT_CAPACITY = 8192;
+
+ private LinkedList<Chunk> mChunks;
+
+ /** free pool */
+ private LinkedList<Chunk> mPool;
+
+ private int mMinCapacity;
+
+ public ByteArrayBuilder() {
+ init(0);
+ }
+
+ public ByteArrayBuilder(int minCapacity) {
+ init(minCapacity);
+ }
+
+ private void init(int minCapacity) {
+ mChunks = new LinkedList<Chunk>();
+ mPool = new LinkedList<Chunk>();
+
+ if (minCapacity <= 0) {
+ minCapacity = DEFAULT_CAPACITY;
+ }
+ mMinCapacity = minCapacity;
+ }
+
+ public void append(byte[] array) {
+ append(array, 0, array.length);
+ }
+
+ public synchronized void append(byte[] array, int offset, int length) {
+ while (length > 0) {
+ Chunk c = appendChunk(length);
+ int amount = Math.min(length, c.mArray.length - c.mLength);
+ System.arraycopy(array, offset, c.mArray, c.mLength, amount);
+ c.mLength += amount;
+ length -= amount;
+ offset += amount;
+ }
+ }
+
+ /**
+ * The fastest way to retrieve the data is to iterate through the
+ * chunks. This returns the first chunk. Note: this pulls the
+ * chunk out of the queue. The caller must call releaseChunk() to
+ * dispose of it.
+ */
+ public synchronized Chunk getFirstChunk() {
+ if (mChunks.isEmpty()) return null;
+ return mChunks.removeFirst();
+ }
+
+ /**
+ * recycles chunk
+ */
+ public synchronized void releaseChunk(Chunk c) {
+ c.mLength = 0;
+ mPool.addLast(c);
+ }
+
+ public boolean isEmpty() {
+ return mChunks.isEmpty();
+ }
+
+ private Chunk appendChunk(int length) {
+ if (length < mMinCapacity) {
+ length = mMinCapacity;
+ }
+
+ Chunk c;
+ if (mChunks.isEmpty()) {
+ c = obtainChunk(length);
+ } else {
+ c = mChunks.getLast();
+ if (c.mLength == c.mArray.length) {
+ c = obtainChunk(length);
+ }
+ }
+ return c;
+ }
+
+ private Chunk obtainChunk(int length) {
+ Chunk c;
+ if (mPool.isEmpty()) {
+ c = new Chunk(length);
+ } else {
+ c = mPool.removeFirst();
+ }
+ mChunks.addLast(c);
+ return c;
+ }
+
+ public static class Chunk {
+ public byte[] mArray;
+ public int mLength;
+
+ public Chunk(int length) {
+ mArray = new byte[length];
+ mLength = 0;
+ }
+ }
+}
diff --git a/core/java/android/webkit/CacheLoader.java b/core/java/android/webkit/CacheLoader.java
new file mode 100644
index 0000000..3e1b602
--- /dev/null
+++ b/core/java/android/webkit/CacheLoader.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.webkit;
+
+import android.net.http.Headers;
+
+/**
+ * This class is a concrete implementation of StreamLoader that uses a
+ * CacheResult as the source for the stream. The CacheResult stored mimetype
+ * and encoding is added to the HTTP response headers.
+ */
+class CacheLoader extends StreamLoader {
+
+ CacheManager.CacheResult mCacheResult; // Content source
+
+ /**
+ * Constructs a CacheLoader for use when loading content from the cache.
+ *
+ * @param loadListener LoadListener to pass the content to
+ * @param result CacheResult used as the source for the content.
+ */
+ CacheLoader(LoadListener loadListener, CacheManager.CacheResult result) {
+ super(loadListener);
+ mCacheResult = result;
+ }
+
+ @Override
+ protected boolean setupStreamAndSendStatus() {
+ mDataStream = mCacheResult.inStream;
+ mContentLength = mCacheResult.contentLength;
+ mHandler.status(1, 1, mCacheResult.httpStatusCode, "OK");
+ return true;
+ }
+
+ @Override
+ protected void buildHeaders(Headers headers) {
+ StringBuilder sb = new StringBuilder(mCacheResult.mimeType);
+ if (mCacheResult.encoding != null &&
+ mCacheResult.encoding.length() > 0) {
+ sb.append(';');
+ sb.append(mCacheResult.encoding);
+ }
+ headers.setContentType(sb.toString());
+
+ if (mCacheResult.location != null &&
+ mCacheResult.location.length() > 0) {
+ headers.setLocation(mCacheResult.location);
+ }
+ }
+
+}
diff --git a/core/java/android/webkit/CacheManager.java b/core/java/android/webkit/CacheManager.java
new file mode 100644
index 0000000..f5a09b8
--- /dev/null
+++ b/core/java/android/webkit/CacheManager.java
@@ -0,0 +1,643 @@
+/*
+ * 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.webkit;
+
+import android.content.Context;
+import android.net.http.Headers;
+import android.os.FileUtils;
+import android.util.Config;
+import android.util.Log;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Map;
+
+import org.bouncycastle.crypto.Digest;
+import org.bouncycastle.crypto.digests.SHA1Digest;
+
+/**
+ * The class CacheManager provides the persistent cache of content that is
+ * received over the network. The component handles parsing of HTTP headers and
+ * utilizes the relevant cache headers to determine if the content should be
+ * stored and if so, how long it is valid for. Network requests are provided to
+ * this component and if they can not be resolved by the cache, the HTTP headers
+ * are attached, as appropriate, to the request for revalidation of content. The
+ * class also manages the cache size.
+ */
+public final class CacheManager {
+
+ private static final String LOGTAG = "cache";
+
+ static final String HEADER_KEY_IFMODIFIEDSINCE = "if-modified-since";
+ static final String HEADER_KEY_IFNONEMATCH = "if-none-match";
+
+ private static final String NO_STORE = "no-store";
+ private static final String NO_CACHE = "no-cache";
+ private static final String MAX_AGE = "max-age";
+
+ private static long CACHE_THRESHOLD = 6 * 1024 * 1024;
+ private static long CACHE_TRIM_AMOUNT = 2 * 1024 * 1024;
+
+ private static boolean mDisabled;
+
+ // Reference count the enable/disable transaction
+ private static int mRefCount;
+
+ private static WebViewDatabase mDataBase;
+ private static File mBaseDir;
+
+ public static class CacheResult {
+ // these fields are saved to the database
+ int httpStatusCode;
+ long contentLength;
+ long expires;
+ String localPath;
+ String lastModified;
+ String etag;
+ String mimeType;
+ String location;
+ String encoding;
+
+ // these fields are NOT saved to the database
+ InputStream inStream;
+ OutputStream outStream;
+ File outFile;
+
+ public int getHttpStatusCode() {
+ return httpStatusCode;
+ }
+
+ public long getContentLength() {
+ return contentLength;
+ }
+
+ public String getLocalPath() {
+ return localPath;
+ }
+
+ public long getExpires() {
+ return expires;
+ }
+
+ public String getLastModified() {
+ return lastModified;
+ }
+
+ public String getETag() {
+ return etag;
+ }
+
+ public String getMimeType() {
+ return mimeType;
+ }
+
+ public String getLocation() {
+ return location;
+ }
+
+ public String getEncoding() {
+ return encoding;
+ }
+
+ // For out-of-package access to the underlying streams.
+ public InputStream getInputStream() {
+ return inStream;
+ }
+
+ public OutputStream getOutputStream() {
+ return outStream;
+ }
+
+ // These fields can be set manually.
+ public void setInputStream(InputStream stream) {
+ this.inStream = stream;
+ }
+
+ public void setEncoding(String encoding) {
+ this.encoding = encoding;
+ }
+ }
+
+ /**
+ * initialize the CacheManager. WebView should handle this for each process.
+ *
+ * @param context The application context.
+ */
+ static void init(Context context) {
+ mDataBase = WebViewDatabase.getInstance(context);
+ mBaseDir = new File(context.getCacheDir(), "webviewCache");
+ if (!mBaseDir.exists()) {
+ if(!mBaseDir.mkdirs()) {
+ Log.w(LOGTAG, "Unable to create webviewCache directory");
+ return;
+ }
+ FileUtils.setPermissions(
+ mBaseDir.toString(),
+ FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH,
+ -1, -1);
+ }
+ }
+
+ /**
+ * get the base directory of the cache. With localPath of the CacheResult,
+ * it identifies the cache file.
+ *
+ * @return File The base directory of the cache.
+ */
+ public static File getCacheFileBaseDir() {
+ return mBaseDir;
+ }
+
+ /**
+ * set the flag to control whether cache is enabled or disabled
+ *
+ * @param disabled true to disable the cache
+ */
+ // only called from WebCore thread
+ static void setCacheDisabled(boolean disabled) {
+ if (disabled == mDisabled) {
+ return;
+ }
+ mDisabled = disabled;
+ if (mDisabled) {
+ removeAllCacheFiles();
+ }
+ }
+
+ /**
+ * get the state of the current cache, enabled or disabled
+ *
+ * @return return if it is disabled
+ */
+ public static boolean cacheDisabled() {
+ return mDisabled;
+ }
+
+ // only called from WebCore thread
+ // make sure to call enableTransaction/disableTransaction in pair
+ static boolean enableTransaction() {
+ if (++mRefCount == 1) {
+ mDataBase.startCacheTransaction();
+ return true;
+ }
+ return false;
+ }
+
+ // only called from WebCore thread
+ // make sure to call enableTransaction/disableTransaction in pair
+ static boolean disableTransaction() {
+ if (mRefCount == 0) {
+ Log.e(LOGTAG, "disableTransaction is out of sync");
+ }
+ if (--mRefCount == 0) {
+ mDataBase.endCacheTransaction();
+ return true;
+ }
+ return false;
+ }
+
+ // only called from WebCore thread
+ // make sure to call startCacheTransaction/endCacheTransaction in pair
+ public static boolean startCacheTransaction() {
+ return mDataBase.startCacheTransaction();
+ }
+
+ // only called from WebCore thread
+ // make sure to call startCacheTransaction/endCacheTransaction in pair
+ public static boolean endCacheTransaction() {
+ return mDataBase.endCacheTransaction();
+ }
+
+ /**
+ * Given a url, returns the CacheResult if exists. Otherwise returns null.
+ * If headers are provided and a cache needs validation,
+ * HEADER_KEY_IFNONEMATCH or HEADER_KEY_IFMODIFIEDSINCE will be set in the
+ * cached headers.
+ *
+ * @return the CacheResult for a given url
+ */
+ // only called from WebCore thread
+ public static CacheResult getCacheFile(String url,
+ Map<String, String> headers) {
+ if (mDisabled) {
+ return null;
+ }
+
+ CacheResult result = mDataBase.getCache(url);
+ if (result != null) {
+ if (result.contentLength == 0) {
+ if (result.httpStatusCode != 301
+ && result.httpStatusCode != 302
+ && result.httpStatusCode != 307) {
+ // this should not happen. If it does, remove it.
+ mDataBase.removeCache(url);
+ return null;
+ }
+ } else {
+ File src = new File(mBaseDir, result.localPath);
+ try {
+ // open here so that even the file is deleted, the content
+ // is still readable by the caller until close() is called
+ result.inStream = new FileInputStream(src);
+ } catch (FileNotFoundException e) {
+ // the files in the cache directory can be removed by the
+ // system. If it is gone, clean up the database
+ mDataBase.removeCache(url);
+ return null;
+ }
+ }
+ } else {
+ return null;
+ }
+
+ // null headers request coming from CACHE_MODE_CACHE_ONLY
+ // which implies that it needs cache even it is expired.
+ // negative expires means time in the far future.
+ if (headers != null && result.expires >= 0
+ && result.expires <= System.currentTimeMillis()) {
+ if (result.lastModified == null && result.etag == null) {
+ return null;
+ }
+ // return HEADER_KEY_IFNONEMATCH or HEADER_KEY_IFMODIFIEDSINCE
+ // for requesting validation
+ if (result.etag != null) {
+ headers.put(HEADER_KEY_IFNONEMATCH, result.etag);
+ }
+ if (result.lastModified != null) {
+ headers.put(HEADER_KEY_IFMODIFIEDSINCE, result.lastModified);
+ }
+ }
+
+ if (Config.LOGV) {
+ Log.v(LOGTAG, "getCacheFile for url " + url);
+ }
+
+ return result;
+ }
+
+ /**
+ * Given a url and its full headers, returns CacheResult if a local cache
+ * can be stored. Otherwise returns null. The mimetype is passed in so that
+ * the function can use the mimetype that will be passed to WebCore which
+ * could be different from the mimetype defined in the headers.
+ * forceCache is for out-of-package callers to force creation of a
+ * CacheResult, and is used to supply surrogate responses for URL
+ * interception.
+ * @return CacheResult for a given url
+ * @hide - hide createCacheFile since it has a parameter of type headers, which is
+ * in a hidden package.
+ */
+ // can be called from any thread
+ public static CacheResult createCacheFile(String url, int statusCode,
+ Headers headers, String mimeType, boolean forceCache) {
+ if (!forceCache && mDisabled) {
+ return null;
+ }
+
+ CacheResult ret = parseHeaders(statusCode, headers, mimeType);
+ if (ret != null) {
+ setupFiles(url, ret);
+ try {
+ ret.outStream = new FileOutputStream(ret.outFile);
+ } catch (FileNotFoundException e) {
+ return null;
+ }
+ ret.mimeType = mimeType;
+ }
+
+ return ret;
+ }
+
+ /**
+ * Save the info of a cache file for a given url to the CacheMap so that it
+ * can be reused later
+ */
+ // only called from WebCore thread
+ public static void saveCacheFile(String url, CacheResult cacheRet) {
+ try {
+ cacheRet.outStream.close();
+ } catch (IOException e) {
+ return;
+ }
+
+ if (!cacheRet.outFile.exists()) {
+ // the file in the cache directory can be removed by the system
+ return;
+ }
+
+ cacheRet.contentLength = cacheRet.outFile.length();
+ if (cacheRet.httpStatusCode == 301
+ || cacheRet.httpStatusCode == 302
+ || cacheRet.httpStatusCode == 307) {
+ // location is in database, no need to keep the file
+ cacheRet.contentLength = 0;
+ cacheRet.localPath = new String();
+ cacheRet.outFile.delete();
+ } else if (cacheRet.contentLength == 0) {
+ cacheRet.outFile.delete();
+ return;
+ }
+
+ mDataBase.addCache(url, cacheRet);
+
+ if (Config.LOGV) {
+ Log.v(LOGTAG, "saveCacheFile for url " + url);
+ }
+ }
+
+ /**
+ * remove all cache files
+ *
+ * @return true if it succeeds
+ */
+ // only called from WebCore thread
+ static boolean removeAllCacheFiles() {
+ // delete cache in a separate thread to not block UI.
+ final Runnable clearCache = new Runnable() {
+ public void run() {
+ // delete all cache files
+ try {
+ String[] files = mBaseDir.list();
+ for (int i = 0; i < files.length; i++) {
+ new File(mBaseDir, files[i]).delete();
+ }
+ } catch (SecurityException e) {
+ // Ignore SecurityExceptions.
+ }
+ // delete database
+ mDataBase.clearCache();
+ }
+ };
+ new Thread(clearCache).start();
+ return true;
+ }
+
+ /**
+ * Return true if the cache is empty.
+ */
+ // only called from WebCore thread
+ static boolean cacheEmpty() {
+ return mDataBase.hasCache();
+ }
+
+ // only called from WebCore thread
+ static void trimCacheIfNeeded() {
+ if (mDataBase.getCacheTotalSize() > CACHE_THRESHOLD) {
+ ArrayList<String> pathList = mDataBase.trimCache(CACHE_TRIM_AMOUNT);
+ int size = pathList.size();
+ for (int i = 0; i < size; i++) {
+ new File(mBaseDir, pathList.get(i)).delete();
+ }
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ private static void setupFiles(String url, CacheResult cacheRet) {
+ if (true) {
+ // Note: SHA1 is much stronger hash. But the cost of setupFiles() is
+ // 3.2% cpu time for a fresh load of nytimes.com. While a simple
+ // String.hashCode() is only 0.6%. If adding the collision resolving
+ // to String.hashCode(), it makes the cpu time to be 1.6% for a
+ // fresh load, but 5.3% for the worst case where all the files
+ // already exist in the file system, but database is gone. So it
+ // needs to resolve collision for every file at least once.
+ int hashCode = url.hashCode();
+ StringBuffer ret = new StringBuffer(8);
+ appendAsHex(hashCode, ret);
+ String path = ret.toString();
+ File file = new File(mBaseDir, path);
+ if (true) {
+ boolean checkOldPath = true;
+ // Check hash collision. If the hash file doesn't exist, just
+ // continue. There is a chance that the old cache file is not
+ // same as the hash file. As mDataBase.getCache() is more
+ // expansive than "leak" a file until clear cache, don't bother.
+ // If the hash file exists, make sure that it is same as the
+ // cache file. If it is not, resolve the collision.
+ while (file.exists()) {
+ if (checkOldPath) {
+ // as this is called from http thread through
+ // createCacheFile, we need endCacheTransaction before
+ // database access.
+ WebViewCore.endCacheTransaction();
+ CacheResult oldResult = mDataBase.getCache(url);
+ WebViewCore.startCacheTransaction();
+ if (oldResult != null && oldResult.contentLength > 0) {
+ if (path.equals(oldResult.localPath)) {
+ path = oldResult.localPath;
+ } else {
+ path = oldResult.localPath;
+ file = new File(mBaseDir, path);
+ }
+ break;
+ }
+ checkOldPath = false;
+ }
+ ret = new StringBuffer(8);
+ appendAsHex(++hashCode, ret);
+ path = ret.toString();
+ file = new File(mBaseDir, path);
+ }
+ }
+ cacheRet.localPath = path;
+ cacheRet.outFile = file;
+ } else {
+ // get hash in byte[]
+ Digest digest = new SHA1Digest();
+ int digestLen = digest.getDigestSize();
+ byte[] hash = new byte[digestLen];
+ int urlLen = url.length();
+ byte[] data = new byte[urlLen];
+ url.getBytes(0, urlLen, data, 0);
+ digest.update(data, 0, urlLen);
+ digest.doFinal(hash, 0);
+ // convert byte[] to hex String
+ StringBuffer result = new StringBuffer(2 * digestLen);
+ for (int i = 0; i < digestLen; i = i + 4) {
+ int h = (0x00ff & hash[i]) << 24 | (0x00ff & hash[i + 1]) << 16
+ | (0x00ff & hash[i + 2]) << 8 | (0x00ff & hash[i + 3]);
+ appendAsHex(h, result);
+ }
+ cacheRet.localPath = result.toString();
+ cacheRet.outFile = new File(mBaseDir, cacheRet.localPath);
+ }
+ }
+
+ private static void appendAsHex(int i, StringBuffer ret) {
+ String hex = Integer.toHexString(i);
+ switch (hex.length()) {
+ case 1:
+ ret.append("0000000");
+ break;
+ case 2:
+ ret.append("000000");
+ break;
+ case 3:
+ ret.append("00000");
+ break;
+ case 4:
+ ret.append("0000");
+ break;
+ case 5:
+ ret.append("000");
+ break;
+ case 6:
+ ret.append("00");
+ break;
+ case 7:
+ ret.append("0");
+ break;
+ }
+ ret.append(hex);
+ }
+
+ private static CacheResult parseHeaders(int statusCode, Headers headers,
+ String mimeType) {
+ // TODO: if authenticated or secure, return null
+ CacheResult ret = new CacheResult();
+ ret.httpStatusCode = statusCode;
+
+ String location = headers.getLocation();
+ if (location != null) ret.location = location;
+
+ ret.expires = -1;
+ String expires = headers.getExpires();
+ if (expires != null) {
+ try {
+ ret.expires = HttpDateTime.parse(expires);
+ } catch (IllegalArgumentException ex) {
+ // Take care of the special "-1" and "0" cases
+ if ("-1".equals(expires) || "0".equals(expires)) {
+ // make it expired, but can be used for history navigation
+ ret.expires = 0;
+ } else {
+ Log.e(LOGTAG, "illegal expires: " + expires);
+ }
+ }
+ }
+
+ String lastModified = headers.getLastModified();
+ if (lastModified != null) ret.lastModified = lastModified;
+
+ String etag = headers.getEtag();
+ if (etag != null) ret.etag = etag;
+
+ String cacheControl = headers.getCacheControl();
+ if (cacheControl != null) {
+ String[] controls = cacheControl.toLowerCase().split("[ ,;]");
+ for (int i = 0; i < controls.length; i++) {
+ if (NO_STORE.equals(controls[i])) {
+ return null;
+ }
+ // According to the spec, 'no-cache' means that the content
+ // must be re-validated on every load. It does not mean that
+ // the content can not be cached. set to expire 0 means it
+ // can only be used in CACHE_MODE_CACHE_ONLY case
+ if (NO_CACHE.equals(controls[i])) {
+ ret.expires = 0;
+ } else if (controls[i].startsWith(MAX_AGE)) {
+ int separator = controls[i].indexOf('=');
+ if (separator < 0) {
+ separator = controls[i].indexOf(':');
+ }
+ if (separator > 0) {
+ String s = controls[i].substring(separator + 1);
+ try {
+ long sec = Long.parseLong(s);
+ if (sec >= 0) {
+ ret.expires = System.currentTimeMillis() + 1000
+ * sec;
+ }
+ } catch (NumberFormatException ex) {
+ if ("1d".equals(s)) {
+ // Take care of the special "1d" case
+ ret.expires = System.currentTimeMillis() + 86400000; // 24*60*60*1000
+ } else {
+ Log.e(LOGTAG, "exception in parseHeaders for "
+ + "max-age:"
+ + controls[i].substring(separator + 1));
+ ret.expires = 0;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // According to RFC 2616 section 14.32:
+ // HTTP/1.1 caches SHOULD treat "Pragma: no-cache" as if the
+ // client had sent "Cache-Control: no-cache"
+ if (NO_CACHE.equals(headers.getPragma())) {
+ ret.expires = 0;
+ }
+
+ // According to RFC 2616 section 13.2.4, if an expiration has not been
+ // explicitly defined a heuristic to set an expiration may be used.
+ if (ret.expires == -1) {
+ if (ret.httpStatusCode == 301) {
+ // If it is a permanent redirect, and it did not have an
+ // explicit cache directive, then it never expires
+ ret.expires = Long.MAX_VALUE;
+ } else if (ret.httpStatusCode == 302 || ret.httpStatusCode == 307) {
+ // If it is temporary redirect, expires
+ ret.expires = 0;
+ } else if (ret.lastModified == null) {
+ // When we have no last-modified, then expire the content with
+ // in 24hrs as, according to the RFC, longer time requires a
+ // warning 113 to be added to the response.
+
+ // Only add the default expiration for non-html markup. Some
+ // sites like news.google.com have no cache directives.
+ if (!mimeType.startsWith("text/html")) {
+ ret.expires = System.currentTimeMillis() + 86400000; // 24*60*60*1000
+ } else {
+ // Setting a expires as zero will cache the result for
+ // forward/back nav.
+ ret.expires = 0;
+ }
+ } else {
+ // If we have a last-modified value, we could use it to set the
+ // expiration. Suggestion from RFC is 10% of time since
+ // last-modified. As we are on mobile, loads are expensive,
+ // increasing this to 20%.
+
+ // 24 * 60 * 60 * 1000
+ long lastmod = System.currentTimeMillis() + 86400000;
+ try {
+ lastmod = HttpDateTime.parse(ret.lastModified);
+ } catch (IllegalArgumentException ex) {
+ Log.e(LOGTAG, "illegal lastModified: " + ret.lastModified);
+ }
+ long difference = System.currentTimeMillis() - lastmod;
+ if (difference > 0) {
+ ret.expires = System.currentTimeMillis() + difference / 5;
+ } else {
+ // last modified is in the future, expire the content
+ // on the last modified
+ ret.expires = lastmod;
+ }
+ }
+ }
+
+ return ret;
+ }
+}
diff --git a/core/java/android/webkit/CallbackProxy.java b/core/java/android/webkit/CallbackProxy.java
new file mode 100644
index 0000000..7c296cc
--- /dev/null
+++ b/core/java/android/webkit/CallbackProxy.java
@@ -0,0 +1,906 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.webkit;
+
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.net.http.SslCertificate;
+import android.net.http.SslError;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.SystemClock;
+import android.util.Config;
+import android.util.Log;
+import android.view.KeyEvent;
+
+import java.util.HashMap;
+
+/**
+ * This class is a proxy class for handling WebCore -> UI thread messaging. All
+ * the callback functions are called from the WebCore thread and messages are
+ * posted to the UI thread for the actual client callback.
+ */
+/*
+ * This class is created in the UI thread so its handler and any private classes
+ * that extend Handler will operate in the UI thread.
+ */
+class CallbackProxy extends Handler {
+ // Logging tag
+ private static final String LOGTAG = "CallbackProxy";
+ // Instance of WebViewClient that is the client callback.
+ private volatile WebViewClient mWebViewClient;
+ // Instance of WebChromeClient for handling all chrome functions.
+ private volatile WebChromeClient mWebChromeClient;
+ // Instance of WebView for handling UI requests.
+ private final WebView mWebView;
+ // Client registered callback listener for download events
+ private volatile DownloadListener mDownloadListener;
+ // Keep track of multiple progress updates.
+ private boolean mProgressUpdatePending;
+ // Keep track of the last progress amount.
+ private volatile int mLatestProgress;
+ // Back/Forward list
+ private final WebBackForwardList mBackForwardList;
+ // Used to call startActivity during url override.
+ private final Context mContext;
+
+ // Message Ids
+ private static final int PAGE_STARTED = 100;
+ private static final int RECEIVED_ICON = 101;
+ private static final int RECEIVED_TITLE = 102;
+ private static final int OVERRIDE_URL = 103;
+ private static final int AUTH_REQUEST = 104;
+ private static final int SSL_ERROR = 105;
+ private static final int PROGRESS = 106;
+ private static final int UPDATE_VISITED = 107;
+ private static final int LOAD_RESOURCE = 108;
+ private static final int CREATE_WINDOW = 109;
+ private static final int CLOSE_WINDOW = 110;
+ private static final int SAVE_PASSWORD = 111;
+ private static final int JS_ALERT = 112;
+ private static final int JS_CONFIRM = 113;
+ private static final int JS_PROMPT = 114;
+ private static final int JS_UNLOAD = 115;
+ private static final int ASYNC_KEYEVENTS = 116;
+ private static final int TOO_MANY_REDIRECTS = 117;
+ private static final int DOWNLOAD_FILE = 118;
+ private static final int REPORT_ERROR = 119;
+ private static final int RESEND_POST_DATA = 120;
+ private static final int PAGE_FINISHED = 121;
+ private static final int REQUEST_FOCUS = 122;
+ private static final int SCALE_CHANGED = 123;
+ private static final int RECEIVED_CERTIFICATE = 124;
+ private static final int SWITCH_OUT_HISTORY = 125;
+
+ // Message triggered by the client to resume execution
+ private static final int NOTIFY = 200;
+
+ // Result transportation object for returning results across thread
+ // boundaries.
+ private class ResultTransport<E> {
+ // Private result object
+ private E mResult;
+
+ public synchronized void setResult(E result) {
+ mResult = result;
+ }
+
+ public synchronized E getResult() {
+ return mResult;
+ }
+ }
+
+ /**
+ * Construct a new CallbackProxy.
+ */
+ public CallbackProxy(Context context, WebView w) {
+ // Used to start a default activity.
+ mContext = context;
+ mWebView = w;
+ mBackForwardList = new WebBackForwardList();
+ }
+
+ /**
+ * Set the WebViewClient.
+ * @param client An implementation of WebViewClient.
+ */
+ public void setWebViewClient(WebViewClient client) {
+ mWebViewClient = client;
+ }
+
+ /**
+ * Set the WebChromeClient.
+ * @param client An implementation of WebChromeClient.
+ */
+ public void setWebChromeClient(WebChromeClient client) {
+ mWebChromeClient = client;
+ }
+
+ /**
+ * Set the client DownloadListener.
+ * @param client An implementation of DownloadListener.
+ */
+ public void setDownloadListener(DownloadListener client) {
+ mDownloadListener = client;
+ }
+
+ /**
+ * Get the Back/Forward list to return to the user or to update the cached
+ * history list.
+ */
+ public WebBackForwardList getBackForwardList() {
+ return mBackForwardList;
+ }
+
+ /**
+ * Called by the UI side. Calling overrideUrlLoading from the WebCore
+ * side will post a message to call this method.
+ */
+ public boolean uiOverrideUrlLoading(String overrideUrl) {
+ if (overrideUrl == null || overrideUrl.length() == 0) {
+ return false;
+ }
+ boolean override = false;
+ if (mWebViewClient != null) {
+ override = mWebViewClient.shouldOverrideUrlLoading(mWebView,
+ overrideUrl);
+ } else {
+ Intent intent = new Intent(Intent.ACTION_VIEW,
+ Uri.parse(overrideUrl));
+ intent.addCategory(Intent.CATEGORY_BROWSABLE);
+ try {
+ mContext.startActivity(intent);
+ override = true;
+ } catch (ActivityNotFoundException ex) {
+ // If no application can handle the URL, assume that the
+ // browser can handle it.
+ }
+ }
+ return override;
+ }
+
+ /**
+ * Called by UI side.
+ */
+ public boolean uiOverrideKeyEvent(KeyEvent event) {
+ if (mWebViewClient != null) {
+ return mWebViewClient.shouldOverrideKeyEvent(mWebView, event);
+ }
+ return false;
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ // We don't have to do synchronization because this function operates
+ // in the UI thread. The WebViewClient and WebChromeClient functions
+ // that check for a non-null callback are ok because java ensures atomic
+ // 32-bit reads and writes.
+ switch (msg.what) {
+ case PAGE_STARTED:
+ if (mWebViewClient != null) {
+ mWebViewClient.onPageStarted(mWebView,
+ msg.getData().getString("url"),
+ (Bitmap) msg.obj);
+ }
+ break;
+
+ case PAGE_FINISHED:
+ if (mWebViewClient != null) {
+ mWebViewClient.onPageFinished(mWebView, (String) msg.obj);
+ }
+ break;
+
+ case RECEIVED_ICON:
+ if (mWebChromeClient != null) {
+ mWebChromeClient.onReceivedIcon(mWebView, (Bitmap) msg.obj);
+ }
+ break;
+
+ case RECEIVED_TITLE:
+ if (mWebChromeClient != null) {
+ mWebChromeClient.onReceivedTitle(mWebView,
+ (String) msg.obj);
+ }
+ break;
+
+ case TOO_MANY_REDIRECTS:
+ Message cancelMsg =
+ (Message) msg.getData().getParcelable("cancelMsg");
+ Message continueMsg =
+ (Message) msg.getData().getParcelable("continueMsg");
+ if (mWebViewClient != null) {
+ mWebViewClient.onTooManyRedirects(mWebView, cancelMsg,
+ continueMsg);
+ } else {
+ cancelMsg.sendToTarget();
+ }
+ break;
+
+ case REPORT_ERROR:
+ if (mWebViewClient != null) {
+ int reasonCode = msg.arg1;
+ final String description = msg.getData().getString("description");
+ final String failUrl = msg.getData().getString("failingUrl");
+ mWebViewClient.onReceivedError(mWebView, reasonCode,
+ description, failUrl);
+ }
+ break;
+
+ case RESEND_POST_DATA:
+ Message resend =
+ (Message) msg.getData().getParcelable("resend");
+ Message dontResend =
+ (Message) msg.getData().getParcelable("dontResend");
+ if (mWebViewClient != null) {
+ mWebViewClient.onFormResubmission(mWebView, dontResend,
+ resend);
+ } else {
+ dontResend.sendToTarget();
+ }
+ break;
+
+ case OVERRIDE_URL:
+ String overrideUrl = msg.getData().getString("url");
+ boolean override = uiOverrideUrlLoading(overrideUrl);
+ ResultTransport<Boolean> result =
+ (ResultTransport<Boolean>) msg.obj;
+ synchronized (this) {
+ result.setResult(override);
+ notify();
+ }
+ break;
+
+ case AUTH_REQUEST:
+ if (mWebViewClient != null) {
+ HttpAuthHandler handler = (HttpAuthHandler) msg.obj;
+ String host = msg.getData().getString("host");
+ String realm = msg.getData().getString("realm");
+ mWebViewClient.onReceivedHttpAuthRequest(mWebView, handler,
+ host, realm);
+ }
+ break;
+
+ case SSL_ERROR:
+ if (mWebViewClient != null) {
+ HashMap<String, Object> map =
+ (HashMap<String, Object>) msg.obj;
+ mWebViewClient.onReceivedSslError(mWebView,
+ (SslErrorHandler) map.get("handler"),
+ (SslError) map.get("error"));
+ }
+ break;
+
+ case PROGRESS:
+ // Synchronize to ensure mLatestProgress is not modified after
+ // setProgress is called and before mProgressUpdatePending is
+ // changed.
+ synchronized (this) {
+ if (mWebChromeClient != null) {
+ mWebChromeClient.onProgressChanged(mWebView,
+ mLatestProgress);
+ }
+ mProgressUpdatePending = false;
+ }
+ break;
+
+ case UPDATE_VISITED:
+ if (mWebViewClient != null) {
+ mWebViewClient.doUpdateVisitedHistory(mWebView,
+ (String) msg.obj, msg.arg1 != 0);
+ }
+ break;
+
+ case LOAD_RESOURCE:
+ if (mWebViewClient != null) {
+ mWebViewClient.onLoadResource(mWebView, (String) msg.obj);
+ }
+ break;
+
+ case DOWNLOAD_FILE:
+ if (mDownloadListener != null) {
+ String url = msg.getData().getString("url");
+ String userAgent = msg.getData().getString("userAgent");
+ String contentDisposition =
+ msg.getData().getString("contentDisposition");
+ String mimetype = msg.getData().getString("mimetype");
+ Long contentLength = msg.getData().getLong("contentLength");
+
+ mDownloadListener.onDownloadStart(url, userAgent,
+ contentDisposition, mimetype, contentLength);
+ }
+ break;
+
+ case CREATE_WINDOW:
+ if (mWebChromeClient != null) {
+ if (!mWebChromeClient.onCreateWindow(mWebView,
+ msg.arg1 == 1, msg.arg2 == 1,
+ (Message) msg.obj)) {
+ synchronized (this) {
+ notify();
+ }
+ }
+ }
+ break;
+
+ case REQUEST_FOCUS:
+ if (mWebChromeClient != null) {
+ mWebChromeClient.onRequestFocus(mWebView);
+ }
+ break;
+
+ case CLOSE_WINDOW:
+ if (mWebChromeClient != null) {
+ mWebChromeClient.onCloseWindow((WebView) msg.obj);
+ }
+ break;
+
+ case SAVE_PASSWORD:
+ Bundle bundle = msg.getData();
+ String host = bundle.getString("host");
+ String username = bundle.getString("username");
+ String password = bundle.getString("password");
+ // If the client returned false it means that the notify message
+ // will not be sent and we should notify WebCore ourselves.
+ if (!mWebView.onSavePassword(host, username, password,
+ (Message) msg.obj)) {
+ synchronized (this) {
+ notify();
+ }
+ }
+ break;
+
+ case ASYNC_KEYEVENTS:
+ if (mWebViewClient != null) {
+ mWebViewClient.onUnhandledKeyEvent(mWebView,
+ (KeyEvent) msg.obj);
+ }
+ break;
+
+ case JS_ALERT:
+ if (mWebChromeClient != null) {
+ JsResult res = (JsResult) msg.obj;
+ String message = msg.getData().getString("message");
+ String url = msg.getData().getString("url");
+ if (!mWebChromeClient.onJsAlert(mWebView, url, message,
+ res)) {
+ res.handleDefault();
+ }
+ res.setReady();
+ }
+ break;
+
+ case JS_CONFIRM:
+ if (mWebChromeClient != null) {
+ JsResult res = (JsResult) msg.obj;
+ String message = msg.getData().getString("message");
+ String url = msg.getData().getString("url");
+ if (!mWebChromeClient.onJsConfirm(mWebView, url, message,
+ res)) {
+ res.handleDefault();
+ }
+ // Tell the JsResult that it is ready for client
+ // interaction.
+ res.setReady();
+ }
+ break;
+
+ case JS_PROMPT:
+ if (mWebChromeClient != null) {
+ JsPromptResult res = (JsPromptResult) msg.obj;
+ String message = msg.getData().getString("message");
+ String defaultVal = msg.getData().getString("default");
+ String url = msg.getData().getString("url");
+ if (!mWebChromeClient.onJsPrompt(mWebView, url, message,
+ defaultVal, res)) {
+ res.handleDefault();
+ }
+ // Tell the JsResult that it is ready for client
+ // interaction.
+ res.setReady();
+ }
+ break;
+
+ case JS_UNLOAD:
+ if (mWebChromeClient != null) {
+ JsResult res = (JsResult) msg.obj;
+ String message = msg.getData().getString("message");
+ String url = msg.getData().getString("url");
+ if (!mWebChromeClient.onJsBeforeUnload(mWebView, url,
+ message, res)) {
+ res.handleDefault();
+ }
+ res.setReady();
+ }
+ break;
+
+ case RECEIVED_CERTIFICATE:
+ mWebView.setCertificate((SslCertificate) msg.obj);
+ break;
+
+ case NOTIFY:
+ synchronized (this) {
+ notify();
+ }
+ break;
+
+ case SCALE_CHANGED:
+ if (mWebViewClient != null) {
+ mWebViewClient.onScaleChanged(mWebView, msg.getData()
+ .getFloat("old"), msg.getData().getFloat("new"));
+ }
+ break;
+
+ case SWITCH_OUT_HISTORY:
+ mWebView.switchOutDrawHistory();
+ break;
+ }
+ }
+
+ /**
+ * Return the latest progress.
+ */
+ public int getProgress() {
+ return mLatestProgress;
+ }
+
+ /**
+ * Called by WebCore side to switch out of history Picture drawing mode
+ */
+ void switchOutDrawHistory() {
+ sendMessage(obtainMessage(SWITCH_OUT_HISTORY));
+ }
+
+ //--------------------------------------------------------------------------
+ // WebViewClient functions.
+ // NOTE: shouldOverrideKeyEvent is never called from the WebCore thread so
+ // it is not necessary to include it here.
+ //--------------------------------------------------------------------------
+
+ // Performance probe
+ private long mWebCoreThreadTime;
+
+ public void onPageStarted(String url, Bitmap favicon) {
+ // Do an unsynchronized quick check to avoid posting if no callback has
+ // been set.
+ if (mWebViewClient == null) {
+ return;
+ }
+ // Performance probe
+ if (false) {
+ mWebCoreThreadTime = SystemClock.currentThreadTimeMillis();
+ Network.getInstance(mContext).startTiming();
+ }
+ Message msg = obtainMessage(PAGE_STARTED);
+ msg.obj = favicon;
+ msg.getData().putString("url", url);
+ sendMessage(msg);
+ }
+
+ public void onPageFinished(String url) {
+ // Do an unsynchronized quick check to avoid posting if no callback has
+ // been set.
+ if (mWebViewClient == null) {
+ return;
+ }
+ // Performance probe
+ if (false) {
+ Log.d("WebCore", "WebCore thread used " +
+ (SystemClock.currentThreadTimeMillis() - mWebCoreThreadTime)
+ + " ms");
+ Network.getInstance(mContext).stopTiming();
+ }
+ Message msg = obtainMessage(PAGE_FINISHED, url);
+ sendMessage(msg);
+ }
+
+ public void onTooManyRedirects(Message cancelMsg, Message continueMsg) {
+ // Do an unsynchronized quick check to avoid posting if no callback has
+ // been set.
+ if (mWebViewClient == null) {
+ cancelMsg.sendToTarget();
+ return;
+ }
+
+ Message msg = obtainMessage(TOO_MANY_REDIRECTS);
+ Bundle bundle = msg.getData();
+ bundle.putParcelable("cancelMsg", cancelMsg);
+ bundle.putParcelable("continueMsg", continueMsg);
+ sendMessage(msg);
+ }
+
+ public void onReceivedError(int errorCode, String description,
+ String failingUrl) {
+ // Do an unsynchronized quick check to avoid posting if no callback has
+ // been set.
+ if (mWebViewClient == null) {
+ return;
+ }
+
+ Message msg = obtainMessage(REPORT_ERROR);
+ msg.arg1 = errorCode;
+ msg.getData().putString("description", description);
+ msg.getData().putString("failingUrl", failingUrl);
+ sendMessage(msg);
+ }
+
+ public void onFormResubmission(Message dontResend,
+ Message resend) {
+ // Do an unsynchronized quick check to avoid posting if no callback has
+ // been set.
+ if (mWebViewClient == null) {
+ dontResend.sendToTarget();
+ return;
+ }
+
+ Message msg = obtainMessage(RESEND_POST_DATA);
+ Bundle bundle = msg.getData();
+ bundle.putParcelable("resend", resend);
+ bundle.putParcelable("dontResend", dontResend);
+ sendMessage(msg);
+ }
+
+ /**
+ * Called by the WebCore side
+ */
+ public boolean shouldOverrideUrlLoading(String url) {
+ // We have a default behavior if no client exists so always send the
+ // message.
+ ResultTransport<Boolean> res = new ResultTransport<Boolean>();
+ Message msg = obtainMessage(OVERRIDE_URL);
+ msg.getData().putString("url", url);
+ msg.obj = res;
+ synchronized (this) {
+ sendMessage(msg);
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ Log.e(LOGTAG, "Caught exception while waiting for overrideUrl");
+ Log.e(LOGTAG, Log.getStackTraceString(e));
+ }
+ }
+ return res.getResult().booleanValue();
+ }
+
+ public void onReceivedHttpAuthRequest(HttpAuthHandler handler,
+ String hostName, String realmName) {
+ // Do an unsynchronized quick check to avoid posting if no callback has
+ // been set.
+ if (mWebViewClient == null) {
+ handler.cancel();
+ return;
+ }
+ Message msg = obtainMessage(AUTH_REQUEST, handler);
+ msg.getData().putString("host", hostName);
+ msg.getData().putString("realm", realmName);
+ sendMessage(msg);
+ }
+ /**
+ * @hide - hide this because it contains a parameter of type SslError.
+ * SslError is located in a hidden package.
+ */
+ public void onReceivedSslError(SslErrorHandler handler, SslError error) {
+ // Do an unsynchronized quick check to avoid posting if no callback has
+ // been set.
+ if (mWebViewClient == null) {
+ handler.cancel();
+ return;
+ }
+ Message msg = obtainMessage(SSL_ERROR);
+ //, handler);
+ HashMap<String, Object> map = new HashMap();
+ map.put("handler", handler);
+ map.put("error", error);
+ msg.obj = map;
+ sendMessage(msg);
+ }
+ /**
+ * @hide - hide this because it contains a parameter of type SslCertificate,
+ * which is located in a hidden package.
+ */
+
+ public void onReceivedCertificate(SslCertificate certificate) {
+ // Do an unsynchronized quick check to avoid posting if no callback has
+ // been set.
+ if (mWebViewClient == null) {
+ return;
+ }
+ // here, certificate can be null (if the site is not secure)
+ sendMessage(obtainMessage(RECEIVED_CERTIFICATE, certificate));
+ }
+
+ public void doUpdateVisitedHistory(String url, boolean isReload) {
+ // Do an unsynchronized quick check to avoid posting if no callback has
+ // been set.
+ if (mWebViewClient == null) {
+ return;
+ }
+ sendMessage(obtainMessage(UPDATE_VISITED, isReload ? 1 : 0, 0, url));
+ }
+
+ public void onLoadResource(String url) {
+ // Do an unsynchronized quick check to avoid posting if no callback has
+ // been set.
+ if (mWebViewClient == null) {
+ return;
+ }
+ sendMessage(obtainMessage(LOAD_RESOURCE, url));
+ }
+
+ public void onUnhandledKeyEvent(KeyEvent event) {
+ // Do an unsynchronized quick check to avoid posting if no callback has
+ // been set.
+ if (mWebViewClient == null) {
+ return;
+ }
+ sendMessage(obtainMessage(ASYNC_KEYEVENTS, event));
+ }
+
+ public void onScaleChanged(float oldScale, float newScale) {
+ // Do an unsynchronized quick check to avoid posting if no callback has
+ // been set.
+ if (mWebViewClient == null) {
+ return;
+ }
+ Message msg = obtainMessage(SCALE_CHANGED);
+ Bundle bundle = msg.getData();
+ bundle.putFloat("old", oldScale);
+ bundle.putFloat("new", newScale);
+ sendMessage(msg);
+ }
+
+ //--------------------------------------------------------------------------
+ // DownloadListener functions.
+ //--------------------------------------------------------------------------
+
+ /**
+ * Starts a download if a download listener has been registered, otherwise
+ * return false.
+ */
+ public boolean onDownloadStart(String url, String userAgent,
+ String contentDisposition, String mimetype, long contentLength) {
+ // Do an unsynchronized quick check to avoid posting if no callback has
+ // been set.
+ if (mDownloadListener == null) {
+ // Cancel the download if there is no browser client.
+ return false;
+ }
+
+ Message msg = obtainMessage(DOWNLOAD_FILE);
+ Bundle bundle = msg.getData();
+ bundle.putString("url", url);
+ bundle.putString("userAgent", userAgent);
+ bundle.putString("mimetype", mimetype);
+ bundle.putLong("contentLength", contentLength);
+ bundle.putString("contentDisposition", contentDisposition);
+ sendMessage(msg);
+ return true;
+ }
+
+
+ //--------------------------------------------------------------------------
+ // WebView specific functions that do not interact with a client. These
+ // functions just need to operate within the UI thread.
+ //--------------------------------------------------------------------------
+
+ public boolean onSavePassword(String host, String username, String password,
+ Message resumeMsg) {
+ // resumeMsg should be null at this point because we want to create it
+ // within the CallbackProxy.
+ if (Config.DEBUG) {
+ junit.framework.Assert.assertNull(resumeMsg);
+ }
+ resumeMsg = obtainMessage(NOTIFY);
+
+ Message msg = obtainMessage(SAVE_PASSWORD, resumeMsg);
+ Bundle bundle = msg.getData();
+ bundle.putString("host", host);
+ bundle.putString("username", username);
+ bundle.putString("password", password);
+ synchronized (this) {
+ sendMessage(msg);
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ Log.e(LOGTAG,
+ "Caught exception while waiting for onSavePassword");
+ Log.e(LOGTAG, Log.getStackTraceString(e));
+ }
+ }
+ // Doesn't matter here
+ return false;
+ }
+
+ //--------------------------------------------------------------------------
+ // WebChromeClient methods
+ //--------------------------------------------------------------------------
+
+ public void onProgressChanged(int newProgress) {
+ // Synchronize so that mLatestProgress is up-to-date.
+ synchronized (this) {
+ mLatestProgress = newProgress;
+ if (mWebChromeClient == null) {
+ return;
+ }
+ if (!mProgressUpdatePending) {
+ sendEmptyMessage(PROGRESS);
+ mProgressUpdatePending = true;
+ }
+ }
+ }
+
+ public WebView createWindow(boolean dialog, boolean userGesture) {
+ // Do an unsynchronized quick check to avoid posting if no callback has
+ // been set.
+ if (mWebChromeClient == null) {
+ return null;
+ }
+
+ WebView.WebViewTransport transport = mWebView.new WebViewTransport();
+ final Message msg = obtainMessage(NOTIFY);
+ msg.obj = transport;
+ synchronized (this) {
+ sendMessage(obtainMessage(CREATE_WINDOW, dialog ? 1 : 0,
+ userGesture ? 1 : 0, msg));
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ Log.e(LOGTAG,
+ "Caught exception while waiting for createWindow");
+ Log.e(LOGTAG, Log.getStackTraceString(e));
+ }
+ }
+
+ WebView w = transport.getWebView();
+ if (w != null) {
+ w.getWebViewCore().initializeSubwindow();
+ }
+ return w;
+ }
+
+ public void onRequestFocus() {
+ // Do an unsynchronized quick check to avoid posting if no callback has
+ // been set.
+ if (mWebChromeClient == null) {
+ return;
+ }
+
+ sendEmptyMessage(REQUEST_FOCUS);
+ }
+
+ public void onCloseWindow(WebView window) {
+ // Do an unsynchronized quick check to avoid posting if no callback has
+ // been set.
+ if (mWebChromeClient == null) {
+ return;
+ }
+ sendMessage(obtainMessage(CLOSE_WINDOW, window));
+ }
+
+ public void onReceivedIcon(Bitmap icon) {
+ if (Config.DEBUG && mBackForwardList.getCurrentItem() == null) {
+ throw new AssertionError();
+ }
+ mBackForwardList.getCurrentItem().setFavicon(icon);
+ // Do an unsynchronized quick check to avoid posting if no callback has
+ // been set.
+ if (mWebChromeClient == null) {
+ return;
+ }
+ sendMessage(obtainMessage(RECEIVED_ICON, icon));
+ }
+
+ public void onReceivedTitle(String title) {
+ // Do an unsynchronized quick check to avoid posting if no callback has
+ // been set.
+ if (mWebChromeClient == null) {
+ return;
+ }
+ sendMessage(obtainMessage(RECEIVED_TITLE, title));
+ }
+
+ public void onJsAlert(String url, String message) {
+ // Do an unsynchronized quick check to avoid posting if no callback has
+ // been set.
+ if (mWebChromeClient == null) {
+ return;
+ }
+ JsResult result = new JsResult(this, false);
+ Message alert = obtainMessage(JS_ALERT, result);
+ alert.getData().putString("message", message);
+ alert.getData().putString("url", url);
+ synchronized (this) {
+ sendMessage(alert);
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ Log.e(LOGTAG, "Caught exception while waiting for jsAlert");
+ Log.e(LOGTAG, Log.getStackTraceString(e));
+ }
+ }
+ }
+
+ public boolean onJsConfirm(String url, String message) {
+ // Do an unsynchronized quick check to avoid posting if no callback has
+ // been set.
+ if (mWebChromeClient == null) {
+ return false;
+ }
+ JsResult result = new JsResult(this, false);
+ Message confirm = obtainMessage(JS_CONFIRM, result);
+ confirm.getData().putString("message", message);
+ confirm.getData().putString("url", url);
+ synchronized (this) {
+ sendMessage(confirm);
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ Log.e(LOGTAG, "Caught exception while waiting for jsConfirm");
+ Log.e(LOGTAG, Log.getStackTraceString(e));
+ }
+ }
+ return result.getResult();
+ }
+
+ public String onJsPrompt(String url, String message, String defaultValue) {
+ // Do an unsynchronized quick check to avoid posting if no callback has
+ // been set.
+ if (mWebChromeClient == null) {
+ return null;
+ }
+ JsPromptResult result = new JsPromptResult(this);
+ Message prompt = obtainMessage(JS_PROMPT, result);
+ prompt.getData().putString("message", message);
+ prompt.getData().putString("default", defaultValue);
+ prompt.getData().putString("url", url);
+ synchronized (this) {
+ sendMessage(prompt);
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ Log.e(LOGTAG, "Caught exception while waiting for jsPrompt");
+ Log.e(LOGTAG, Log.getStackTraceString(e));
+ }
+ }
+ return result.getStringResult();
+ }
+
+ public boolean onJsBeforeUnload(String url, String message) {
+ // Do an unsynchronized quick check to avoid posting if no callback has
+ // been set.
+ if (mWebChromeClient == null) {
+ return true;
+ }
+ JsResult result = new JsResult(this, true);
+ Message confirm = obtainMessage(JS_UNLOAD, result);
+ confirm.getData().putString("message", message);
+ confirm.getData().putString("url", url);
+ synchronized (this) {
+ sendMessage(confirm);
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ Log.e(LOGTAG, "Caught exception while waiting for jsUnload");
+ Log.e(LOGTAG, Log.getStackTraceString(e));
+ }
+ }
+ return result.getResult();
+ }
+}
diff --git a/core/java/android/webkit/ContentLoader.java b/core/java/android/webkit/ContentLoader.java
new file mode 100644
index 0000000..fb01c8c
--- /dev/null
+++ b/core/java/android/webkit/ContentLoader.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.webkit;
+
+import android.content.Context;
+import android.net.http.EventHandler;
+import android.net.http.Headers;
+import android.net.Uri;
+
+import java.io.File;
+import java.io.FileInputStream;
+
+/**
+ * This class is a concrete implementation of StreamLoader that loads
+ * "content:" URIs
+ */
+class ContentLoader extends StreamLoader {
+
+ private String mUrl;
+ private Context mContext;
+ private String mContentType;
+
+ /**
+ * Construct a ContentLoader with the specified content URI
+ *
+ * @param rawUrl "content:" url pointing to content to be loaded. This url
+ * is the same url passed in to the WebView.
+ * @param loadListener LoadListener to pass the content to
+ * @param context Context to use to access the asset.
+ */
+ ContentLoader(String rawUrl, LoadListener loadListener, Context context) {
+ super(loadListener);
+ mContext = context;
+
+ /* strip off mimetype */
+ int mimeIndex = rawUrl.lastIndexOf('?');
+ if (mimeIndex != -1) {
+ mUrl = rawUrl.substring(0, mimeIndex);
+ mContentType = rawUrl.substring(mimeIndex + 1);
+ } else {
+ mUrl = rawUrl;
+ }
+
+ }
+
+ @Override
+ protected boolean setupStreamAndSendStatus() {
+ Uri uri = Uri.parse(mUrl);
+ if (uri == null) {
+ mHandler.error(
+ EventHandler.FILE_NOT_FOUND_ERROR,
+ mContext.getString(
+ com.android.internal.R.string.httpErrorBadUrl) +
+ " " + mUrl);
+ return false;
+ }
+
+ try {
+ mDataStream = mContext.getContentResolver().openInputStream(uri);
+ mHandler.status(1, 1, 0, "OK");
+ } catch (java.io.FileNotFoundException ex) {
+ mHandler.error(
+ EventHandler.FILE_NOT_FOUND_ERROR,
+ mContext.getString(
+ com.android.internal.R.string.httpErrorFileNotFound) +
+ " " + ex.getMessage());
+ return false;
+
+ } catch (java.io.IOException ex) {
+ mHandler.error(
+ EventHandler.FILE_ERROR,
+ mContext.getString(
+ com.android.internal.R.string.httpErrorFileNotFound) +
+ " " + ex.getMessage());
+ return false;
+ } catch (RuntimeException ex) {
+ // readExceptionWithFileNotFoundExceptionFromParcel in DatabaseUtils
+ // can throw a serial of RuntimeException. Catch them all here.
+ mHandler.error(
+ EventHandler.FILE_ERROR,
+ mContext.getString(
+ com.android.internal.R.string.httpErrorFileNotFound) +
+ " " + ex.getMessage());
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ protected void buildHeaders(Headers headers) {
+ if (mContentType != null) {
+ headers.setContentType("text/html");
+ }
+ }
+
+ /**
+ * Construct a ContentLoader and instruct it to start loading.
+ *
+ * @param url "content:" url pointing to content to be loaded
+ * @param loadListener LoadListener to pass the content to
+ * @param context Context to use to access the asset.
+ */
+ public static void requestUrl(String url, LoadListener loadListener,
+ Context context) {
+ ContentLoader loader = new ContentLoader(url, loadListener, context);
+ loader.load();
+ }
+
+}
diff --git a/core/java/android/webkit/CookieManager.java b/core/java/android/webkit/CookieManager.java
new file mode 100644
index 0000000..176471f
--- /dev/null
+++ b/core/java/android/webkit/CookieManager.java
@@ -0,0 +1,898 @@
+/*
+ * 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.webkit;
+
+import android.net.ParseException;
+import android.net.WebAddress;
+import android.util.Config;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * CookieManager manages cookies according to RFC2109 spec.
+ */
+public final class CookieManager {
+
+ private static CookieManager sRef;
+
+ private static final String LOGTAG = "webkit";
+
+ private static final String DOMAIN = "domain";
+
+ private static final String PATH = "path";
+
+ private static final String EXPIRES = "expires";
+
+ private static final String SECURE = "secure";
+
+ private static final String MAX_AGE = "max-age";
+
+ private static final String HTTP_ONLY = "httponly";
+
+ private static final String HTTPS = "https";
+
+ private static final char PERIOD = '.';
+
+ private static final char COMMA = ',';
+
+ private static final char SEMICOLON = ';';
+
+ private static final char EQUAL = '=';
+
+ private static final char PATH_DELIM = '/';
+
+ private static final char QUESTION_MARK = '?';
+
+ private static final char WHITE_SPACE = ' ';
+
+ private static final char QUOTATION = '\"';
+
+ private static final int SECURE_LENGTH = SECURE.length();
+
+ private static final int HTTP_ONLY_LENGTH = HTTP_ONLY.length();
+
+ // RFC2109 defines 4k as maximum size of a cookie
+ private static final int MAX_COOKIE_LENGTH = 4 * 1024;
+
+ // RFC2109 defines 20 as max cookie count per domain. As we track with base
+ // domain, we allow 50 per base domain
+ private static final int MAX_COOKIE_COUNT_PER_BASE_DOMAIN = 50;
+
+ // RFC2109 defines 300 as max count of domains. As we track with base
+ // domain, we set 200 as max base domain count
+ private static final int MAX_DOMAIN_COUNT = 200;
+
+ // max cookie count to limit RAM cookie takes less than 100k, it is based on
+ // average cookie entry size is less than 100 bytes
+ private static final int MAX_RAM_COOKIES_COUNT = 1000;
+
+ // max domain count to limit RAM cookie takes less than 100k,
+ private static final int MAX_RAM_DOMAIN_COUNT = 15;
+
+ private Map<String, ArrayList<Cookie>> mCookieMap = new LinkedHashMap
+ <String, ArrayList<Cookie>>(MAX_DOMAIN_COUNT, 0.75f, true);
+
+ private boolean mAcceptCookie = true;
+
+ /**
+ * This contains a list of 2nd-level domains that aren't allowed to have
+ * wildcards when combined with country-codes. For example: [.co.uk].
+ */
+ private final static String[] BAD_COUNTRY_2LDS =
+ { "ac", "co", "com", "ed", "edu", "go", "gouv", "gov", "info",
+ "lg", "ne", "net", "or", "org" };
+
+ static {
+ Arrays.sort(BAD_COUNTRY_2LDS);
+ }
+
+ /**
+ * Package level class to be accessed by cookie sync manager
+ */
+ static class Cookie {
+ static final byte MODE_NEW = 0;
+
+ static final byte MODE_NORMAL = 1;
+
+ static final byte MODE_DELETED = 2;
+
+ static final byte MODE_REPLACED = 3;
+
+ String domain;
+
+ String path;
+
+ String name;
+
+ String value;
+
+ long expires;
+
+ long lastAcessTime;
+
+ long lastUpdateTime;
+
+ boolean secure;
+
+ byte mode;
+
+ Cookie() {
+ }
+
+ Cookie(String defaultDomain, String defaultPath) {
+ domain = defaultDomain;
+ path = defaultPath;
+ expires = -1;
+ }
+
+ boolean exactMatch(Cookie in) {
+ return domain.equals(in.domain) && path.equals(in.path) &&
+ name.equals(in.name);
+ }
+
+ boolean domainMatch(String urlHost) {
+ return urlHost.equals(domain) ||
+ (domain.startsWith(".") &&
+ urlHost.endsWith(domain.substring(1)));
+ }
+
+ boolean pathMatch(String urlPath) {
+ return urlPath.startsWith (path);
+ }
+
+ public String toString() {
+ return "domain: " + domain + "; path: " + path + "; name: " + name
+ + "; value: " + value;
+ }
+ }
+
+ private CookieManager() {
+ }
+
+ protected Object clone() throws CloneNotSupportedException {
+ throw new CloneNotSupportedException("doesn't implement Cloneable");
+ }
+
+ /**
+ * Get a singleton CookieManager. If this is called before any
+ * {@link WebView} is created or outside of {@link WebView} context, the
+ * caller needs to call {@link CookieSyncManager#createInstance(Context)}
+ * first.
+ *
+ * @return CookieManager
+= */
+ public static synchronized CookieManager getInstance() {
+ if (sRef == null) {
+ sRef = new CookieManager();
+ }
+ return sRef;
+ }
+
+ /**
+ * Control whether cookie is enabled or disabled
+ * @param accept TRUE if accept cookie
+ */
+ public synchronized void setAcceptCookie(boolean accept) {
+ mAcceptCookie = accept;
+ }
+
+ /**
+ * Return whether cookie is enabled
+ * @return TRUE if accept cookie
+ */
+ public synchronized boolean acceptCookie() {
+ return mAcceptCookie;
+ }
+
+ /**
+ * Set cookie for a given url. The old cookie with same host/path/name will
+ * be removed. The new cookie will be added if it is not expired or it does
+ * not have expiration which implies it is session cookie.
+ * @param url The url which cookie is set for
+ * @param value The value for set-cookie: in http response header
+ */
+ public void setCookie(String url, String value) {
+ WebAddress uri;
+ try {
+ uri = new WebAddress(url);
+ } catch (ParseException ex) {
+ Log.e(LOGTAG, "Bad address: " + url);
+ return;
+ }
+ setCookie(uri, value);
+ }
+
+ /**
+ * Set cookie for a given uri. The old cookie with same host/path/name will
+ * be removed. The new cookie will be added if it is not expired or it does
+ * not have expiration which implies it is session cookie.
+ * @param uri The uri which cookie is set for
+ * @param value The value for set-cookie: in http response header
+ * @hide - hide this because it takes in a parameter of type WebAddress,
+ * a system private class.
+ */
+ public synchronized void setCookie(WebAddress uri, String value) {
+ if (value != null && value.length() > 4096) {
+ return;
+ }
+ if (!mAcceptCookie || uri == null) {
+ return;
+ }
+ if (Config.LOGV) {
+ Log.v(LOGTAG, "setCookie: uri: " + uri + " value: " + value);
+ }
+
+ String[] hostAndPath = getHostAndPath(uri);
+ if (hostAndPath == null) {
+ return;
+ }
+
+ ArrayList<Cookie> cookies = null;
+ try {
+ /* Google is setting cookies like the following to detect whether
+ * a browser supports cookie. We need to skip the leading "www" for
+ * the default host. Otherwise the second cookie will make the first
+ * cookie expired.
+ *
+ * url: https://www.google.com/accounts/ServiceLoginAuth
+ * value: LSID=xxxxxxxxxxxxx;Path=/accounts;
+ * Expires=Tue, 13-Mar-2018 01:41:39 GMT
+ *
+ * url: https://www.google.com/accounts/ServiceLoginAuth
+ * value:LSID=EXPIRED;Domain=www.google.com;Path=/accounts;
+ * Expires=Mon, 01-Jan-1990 00:00:00 GMT
+ */
+ if (hostAndPath[0].startsWith("www.")) {
+ hostAndPath[0] = hostAndPath[0].substring(3);
+ }
+ cookies = parseCookie(hostAndPath[0], hostAndPath[1], value);
+ } catch (RuntimeException ex) {
+ Log.e(LOGTAG, "parse cookie failed for: " + value);
+ }
+
+ if (cookies == null || cookies.size() == 0) {
+ return;
+ }
+
+ String baseDomain = getBaseDomain(hostAndPath[0]);
+ ArrayList<Cookie> cookieList = mCookieMap.get(baseDomain);
+ if (cookieList == null) {
+ cookieList = CookieSyncManager.getInstance()
+ .getCookiesForDomain(baseDomain);
+ mCookieMap.put(baseDomain, cookieList);
+ }
+
+ long now = System.currentTimeMillis();
+ int size = cookies.size();
+ for (int i = 0; i < size; i++) {
+ Cookie cookie = cookies.get(i);
+
+ boolean done = false;
+ Iterator<Cookie> iter = cookieList.iterator();
+ while (iter.hasNext()) {
+ Cookie cookieEntry = iter.next();
+ if (cookie.exactMatch(cookieEntry)) {
+ // expires == -1 means no expires defined. Otherwise
+ // negative means far future
+ if (cookie.expires < 0 || cookie.expires > now) {
+ // secure cookies can't be overwritten by non-HTTPS url
+ if (!cookieEntry.secure || HTTPS.equals(uri.mScheme)) {
+ cookieEntry.value = cookie.value;
+ cookieEntry.expires = cookie.expires;
+ cookieEntry.secure = cookie.secure;
+ cookieEntry.lastAcessTime = now;
+ cookieEntry.lastUpdateTime = now;
+ cookieEntry.mode = Cookie.MODE_REPLACED;
+ }
+ } else {
+ cookieEntry.lastUpdateTime = now;
+ cookieEntry.mode = Cookie.MODE_DELETED;
+ }
+ done = true;
+ break;
+ }
+ }
+
+ // expires == -1 means no expires defined. Otherwise negative means
+ // far future
+ if (!done && (cookie.expires < 0 || cookie.expires > now)) {
+ cookie.lastAcessTime = now;
+ cookie.lastUpdateTime = now;
+ cookie.mode = Cookie.MODE_NEW;
+ if (cookieList.size() > MAX_COOKIE_COUNT_PER_BASE_DOMAIN) {
+ Cookie toDelete = new Cookie();
+ toDelete.lastAcessTime = now;
+ Iterator<Cookie> iter2 = cookieList.iterator();
+ while (iter2.hasNext()) {
+ Cookie cookieEntry2 = iter2.next();
+ if ((cookieEntry2.lastAcessTime < toDelete.lastAcessTime)
+ && cookieEntry2.mode != Cookie.MODE_DELETED) {
+ toDelete = cookieEntry2;
+ }
+ }
+ toDelete.mode = Cookie.MODE_DELETED;
+ }
+ cookieList.add(cookie);
+ }
+ }
+ }
+
+ /**
+ * Get cookie(s) for a given url so that it can be set to "cookie:" in http
+ * request header.
+ * @param url The url needs cookie
+ * @return The cookies in the format of NAME=VALUE [; NAME=VALUE]
+ */
+ public String getCookie(String url) {
+ WebAddress uri;
+ try {
+ uri = new WebAddress(url);
+ } catch (ParseException ex) {
+ Log.e(LOGTAG, "Bad address: " + url);
+ return null;
+ }
+ return getCookie(uri);
+ }
+
+ /**
+ * Get cookie(s) for a given uri so that it can be set to "cookie:" in http
+ * request header.
+ * @param uri The uri needs cookie
+ * @return The cookies in the format of NAME=VALUE [; NAME=VALUE]
+ * @hide - hide this because it has a parameter of type WebAddress, which
+ * is a system private class.
+ */
+ public synchronized String getCookie(WebAddress uri) {
+ if (!mAcceptCookie || uri == null) {
+ return null;
+ }
+
+ String[] hostAndPath = getHostAndPath(uri);
+ if (hostAndPath == null) {
+ return null;
+ }
+
+ String baseDomain = getBaseDomain(hostAndPath[0]);
+ ArrayList<Cookie> cookieList = mCookieMap.get(baseDomain);
+ if (cookieList == null) {
+ cookieList = CookieSyncManager.getInstance()
+ .getCookiesForDomain(baseDomain);
+ mCookieMap.put(baseDomain, cookieList);
+ }
+
+ long now = System.currentTimeMillis();
+ boolean secure = HTTPS.equals(uri.mScheme);
+ Iterator<Cookie> iter = cookieList.iterator();
+ StringBuilder ret = new StringBuilder(256);
+
+ while (iter.hasNext()) {
+ Cookie cookie = iter.next();
+ if (cookie.domainMatch(hostAndPath[0]) &&
+ cookie.pathMatch(hostAndPath[1])
+ // expires == -1 means no expires defined. Otherwise
+ // negative means far future
+ && (cookie.expires < 0 || cookie.expires > now)
+ && (!cookie.secure || secure)
+ && cookie.mode != Cookie.MODE_DELETED) {
+ cookie.lastAcessTime = now;
+
+ if (ret.length() > 0) {
+ ret.append(SEMICOLON);
+ // according to RC2109, SEMICOLON is office separator,
+ // but when log in yahoo.com, it needs WHITE_SPACE too.
+ ret.append(WHITE_SPACE);
+ }
+
+ ret.append(cookie.name);
+ ret.append(EQUAL);
+ ret.append(cookie.value);
+ }
+ }
+ if (ret.length() > 0) {
+ if (Config.LOGV) {
+ Log.v(LOGTAG, "getCookie: uri: " + uri + " value: " + ret);
+ }
+ return ret.toString();
+ } else {
+ if (Config.LOGV) {
+ Log.v(LOGTAG, "getCookie: uri: " + uri
+ + " But can't find cookie.");
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Remove all session cookies, which are cookies without expiration date
+ */
+ public synchronized void removeSessionCookie() {
+ Collection<ArrayList<Cookie>> cookieList = mCookieMap.values();
+ Iterator<ArrayList<Cookie>> listIter = cookieList.iterator();
+ while (listIter.hasNext()) {
+ ArrayList<Cookie> list = listIter.next();
+ Iterator<Cookie> iter = list.iterator();
+ while (iter.hasNext()) {
+ Cookie cookie = iter.next();
+ if (cookie.expires == -1) {
+ iter.remove();
+ }
+ }
+ }
+ CookieSyncManager.getInstance().clearSessionCookies();
+ }
+
+ /**
+ * Remove all cookies
+ */
+ public synchronized void removeAllCookie() {
+ mCookieMap = new LinkedHashMap<String, ArrayList<Cookie>>(
+ MAX_DOMAIN_COUNT, 0.75f, true);
+ CookieSyncManager.getInstance().clearAllCookies();
+ }
+
+ /**
+ * Return true if there are stored cookies.
+ */
+ public synchronized boolean hasCookies() {
+ return CookieSyncManager.getInstance().hasCookies();
+ }
+
+ /**
+ * Remove all expired cookies
+ */
+ public synchronized void removeExpiredCookie() {
+ long now = System.currentTimeMillis();
+ Collection<ArrayList<Cookie>> cookieList = mCookieMap.values();
+ Iterator<ArrayList<Cookie>> listIter = cookieList.iterator();
+ while (listIter.hasNext()) {
+ ArrayList<Cookie> list = listIter.next();
+ Iterator<Cookie> iter = list.iterator();
+ while (iter.hasNext()) {
+ Cookie cookie = iter.next();
+ // expires == -1 means no expires defined. Otherwise negative
+ // means far future
+ if (cookie.expires > 0 && cookie.expires < now) {
+ iter.remove();
+ }
+ }
+ }
+ CookieSyncManager.getInstance().clearExpiredCookies(now);
+ }
+
+ /**
+ * Package level api, called from CookieSyncManager
+ *
+ * Get a list of cookies which are updated since a given time.
+ * @param last The given time in millisec
+ * @return A list of cookies
+ */
+ synchronized ArrayList<Cookie> getUpdatedCookiesSince(long last) {
+ ArrayList<Cookie> cookies = new ArrayList<Cookie>();
+ Collection<ArrayList<Cookie>> cookieList = mCookieMap.values();
+ Iterator<ArrayList<Cookie>> listIter = cookieList.iterator();
+ while (listIter.hasNext()) {
+ ArrayList<Cookie> list = listIter.next();
+ Iterator<Cookie> iter = list.iterator();
+ while (iter.hasNext()) {
+ Cookie cookie = iter.next();
+ if (cookie.lastUpdateTime > last) {
+ cookies.add(cookie);
+ }
+ }
+ }
+ return cookies;
+ }
+
+ /**
+ * Package level api, called from CookieSyncManager
+ *
+ * Delete a Cookie in the RAM
+ * @param cookie Cookie to be deleted
+ */
+ synchronized void deleteACookie(Cookie cookie) {
+ if (cookie.mode == Cookie.MODE_DELETED) {
+ String baseDomain = getBaseDomain(cookie.domain);
+ ArrayList<Cookie> cookieList = mCookieMap.get(baseDomain);
+ if (cookieList != null) {
+ cookieList.remove(cookie);
+ if (cookieList.isEmpty()) {
+ mCookieMap.remove(baseDomain);
+ }
+ }
+ }
+ }
+
+ /**
+ * Package level api, called from CookieSyncManager
+ *
+ * Called after a cookie is synced to FLASH
+ * @param cookie Cookie to be synced
+ */
+ synchronized void syncedACookie(Cookie cookie) {
+ cookie.mode = Cookie.MODE_NORMAL;
+ }
+
+ /**
+ * Package level api, called from CookieSyncManager
+ *
+ * Delete the least recent used domains if the total cookie count in RAM
+ * exceeds the limit
+ * @return A list of cookies which are removed from RAM
+ */
+ synchronized ArrayList<Cookie> deleteLRUDomain() {
+ int count = 0;
+ int byteCount = 0;
+ int mapSize = mCookieMap.size();
+
+ if (mapSize < MAX_RAM_DOMAIN_COUNT) {
+ Collection<ArrayList<Cookie>> cookieLists = mCookieMap.values();
+ Iterator<ArrayList<Cookie>> listIter = cookieLists.iterator();
+ while (listIter.hasNext() && count < MAX_RAM_COOKIES_COUNT) {
+ ArrayList<Cookie> list = listIter.next();
+ if (Config.DEBUG) {
+ Iterator<Cookie> iter = list.iterator();
+ while (iter.hasNext() && count < MAX_RAM_COOKIES_COUNT) {
+ Cookie cookie = iter.next();
+ // 14 is 3 * sizeof(long) + sizeof(boolean)
+ // + sizeof(byte)
+ byteCount += cookie.domain.length()
+ + cookie.path.length()
+ + cookie.name.length()
+ + cookie.value.length() + 14;
+ count++;
+ }
+ } else {
+ count += list.size();
+ }
+ }
+ }
+
+ ArrayList<Cookie> retlist = new ArrayList<Cookie>();
+ if (mapSize >= MAX_RAM_DOMAIN_COUNT || count >= MAX_RAM_COOKIES_COUNT) {
+ if (Config.DEBUG) {
+ Log.v(LOGTAG, count + " cookies used " + byteCount
+ + " bytes with " + mapSize + " domains");
+ }
+ Object[] domains = mCookieMap.keySet().toArray();
+ int toGo = mapSize / 10 + 1;
+ while (toGo-- > 0){
+ String domain = domains[toGo].toString();
+ if (Config.LOGV) {
+ Log.v(LOGTAG, "delete domain: " + domain
+ + " from RAM cache");
+ }
+ retlist.addAll(mCookieMap.get(domain));
+ mCookieMap.remove(domain);
+ }
+ }
+ return retlist;
+ }
+
+ /**
+ * Extract the host and path out of a uri
+ * @param uri The given WebAddress
+ * @return The host and path in the format of String[], String[0] is host
+ * which has at least two periods, String[1] is path which always
+ * ended with "/"
+ */
+ private String[] getHostAndPath(WebAddress uri) {
+ if (uri.mHost != null && uri.mPath != null) {
+ String[] ret = new String[2];
+ ret[0] = uri.mHost;
+ ret[1] = uri.mPath;
+
+ int index = ret[0].indexOf(PERIOD);
+ if (index == -1) {
+ if (uri.mScheme.equalsIgnoreCase("file")) {
+ // There is a potential bug where a local file path matches
+ // another file in the local web server directory. Still
+ // "localhost" is the best pseudo domain name.
+ ret[0] = "localhost";
+ } else if (!ret[0].equals("localhost")) {
+ return null;
+ }
+ } else if (index == ret[0].lastIndexOf(PERIOD)) {
+ // cookie host must have at least two periods
+ ret[0] = PERIOD + ret[0];
+ }
+
+ if (ret[1].charAt(0) != PATH_DELIM) {
+ return null;
+ }
+
+ /*
+ * find cookie path, e.g. for http://www.google.com, the path is "/"
+ * for http://www.google.com/lab/, the path is "/lab/"
+ * for http://www.google.com/lab/foo, the path is "/lab/"
+ * for http://www.google.com/lab?hl=en, the path is "/lab/"
+ * for http://www.google.com/lab.asp?hl=en, the path is "/"
+ * Note: the path from URI has at least one "/"
+ */
+ index = ret[1].indexOf(QUESTION_MARK);
+ if (index != -1) {
+ ret[1] = ret[1].substring(0, index);
+ if (ret[1].charAt(ret[1].length() - 1) != PATH_DELIM) {
+ index = ret[1].lastIndexOf(PATH_DELIM);
+ if (ret[1].lastIndexOf('.') > index) {
+ ret[1] = ret[1].substring(0, index + 1);
+ } else {
+ ret[1] += PATH_DELIM;
+ }
+ }
+ } else if (ret[1].charAt(ret[1].length() - 1) != PATH_DELIM) {
+ ret[1] = ret[1].substring(0,
+ ret[1].lastIndexOf(PATH_DELIM) + 1);
+ }
+ return ret;
+ } else
+ return null;
+ }
+
+ /**
+ * Get the base domain for a give host. E.g. mail.google.com will return
+ * google.com
+ * @param host The give host
+ * @return the base domain
+ */
+ private String getBaseDomain(String host) {
+ int startIndex = 0;
+ int nextIndex = host.indexOf(PERIOD);
+ int lastIndex = host.lastIndexOf(PERIOD);
+ while (nextIndex < lastIndex) {
+ startIndex = nextIndex + 1;
+ nextIndex = host.indexOf(PERIOD, startIndex);
+ }
+ if (startIndex > 0) {
+ return host.substring(startIndex);
+ } else {
+ return host;
+ }
+ }
+
+ /**
+ * parseCookie() parses the cookieString which is a comma-separated list of
+ * one or more cookies in the format of "NAME=VALUE; expires=DATE;
+ * path=PATH; domain=DOMAIN_NAME; secure httponly" to a list of Cookies.
+ * Here is a sample: IGDND=1, IGPC=ET=UB8TSNwtDmQ:AF=0; expires=Sun,
+ * 17-Jan-2038 19:14:07 GMT; path=/ig; domain=.google.com, =,
+ * PREF=ID=408909b1b304593d:TM=1156459854:LM=1156459854:S=V-vCAU6Sh-gobCfO;
+ * expires=Sun, 17-Jan-2038 19:14:07 GMT; path=/; domain=.google.com which
+ * contains 3 cookies IGDND, IGPC, PREF and an empty cookie
+ * @param host The default host
+ * @param path The default path
+ * @param cookieString The string coming from "Set-Cookie:"
+ * @return A list of Cookies
+ */
+ private ArrayList<Cookie> parseCookie(String host, String path,
+ String cookieString) {
+ ArrayList<Cookie> ret = new ArrayList<Cookie>();
+
+ // domain needs at least two PERIOD,
+ if (host.indexOf(PERIOD) == host.lastIndexOf(PERIOD)) {
+ host = PERIOD + host;
+ }
+ int index = 0;
+ int length = cookieString.length();
+ while (true) {
+ Cookie cookie = null;
+
+ // done
+ if (index < 0 || index >= length) {
+ break;
+ }
+
+ // skip white space
+ if (cookieString.charAt(index) == WHITE_SPACE) {
+ index++;
+ continue;
+ }
+
+ /*
+ * get NAME=VALUE; pair. detecting the end of a pair is tricky, it
+ * can be the end of a string, like "foo=bluh", it can be semicolon
+ * like "foo=bluh;path=/"; or it can be enclosed by \", like
+ * "foo=\"bluh bluh\";path=/"
+ *
+ * Note: in the case of "foo=bluh, bar=bluh;path=/", we interpret
+ * it as one cookie instead of two cookies.
+ */
+ int equalIndex = cookieString.indexOf(EQUAL, index);
+ if (equalIndex == -1) {
+ // bad format, force return
+ break;
+ }
+ cookie = new Cookie(host, path);
+ cookie.name = cookieString.substring(index, equalIndex);
+ if (cookieString.charAt(equalIndex + 1) == QUOTATION) {
+ index = cookieString.indexOf(QUOTATION, equalIndex + 2);
+ if (index == -1) {
+ // bad format, force return
+ break;
+ }
+ }
+ int semicolonIndex = cookieString.indexOf(SEMICOLON, index);
+ if (semicolonIndex == -1) {
+ semicolonIndex = length;
+ }
+ if (semicolonIndex - equalIndex > MAX_COOKIE_LENGTH) {
+ // cookie is too big, trim it
+ cookie.value = cookieString.substring(equalIndex + 1,
+ equalIndex + MAX_COOKIE_LENGTH);
+ } else if (equalIndex + 1 == semicolonIndex
+ || semicolonIndex < equalIndex) {
+ // these are unusual case like foo=; and foo; path=/
+ cookie.value = "";
+ } else {
+ cookie.value = cookieString.substring(equalIndex + 1,
+ semicolonIndex);
+ }
+ // get attributes
+ index = semicolonIndex;
+ while (true) {
+ // done
+ if (index < 0 || index >= length) {
+ break;
+ }
+
+ // skip white space and semicolon
+ if (cookieString.charAt(index) == WHITE_SPACE
+ || cookieString.charAt(index) == SEMICOLON) {
+ index++;
+ continue;
+ }
+
+ // comma means next cookie
+ if (cookieString.charAt(index) == COMMA) {
+ index++;
+ break;
+ }
+
+ // "secure" is a known attribute doesn't use "=";
+ // while sites like live.com uses "secure="
+ if (length - index > SECURE_LENGTH
+ && cookieString.substring(index, index + SECURE_LENGTH).
+ equalsIgnoreCase(SECURE)) {
+ index += SECURE_LENGTH;
+ cookie.secure = true;
+ if (cookieString.charAt(index) == EQUAL) index++;
+ continue;
+ }
+
+ // "httponly" is a known attribute doesn't use "=";
+ // while sites like live.com uses "httponly="
+ if (length - index > HTTP_ONLY_LENGTH
+ && cookieString.substring(index,
+ index + HTTP_ONLY_LENGTH).
+ equalsIgnoreCase(HTTP_ONLY)) {
+ index += HTTP_ONLY_LENGTH;
+ if (cookieString.charAt(index) == EQUAL) index++;
+ // FIXME: currently only parse the attribute
+ continue;
+ }
+ equalIndex = cookieString.indexOf(EQUAL, index);
+ if (equalIndex > 0) {
+ String name = cookieString.substring(index, equalIndex)
+ .toLowerCase();
+ if (name.equals(EXPIRES)) {
+ int comaIndex = cookieString.indexOf(COMMA, equalIndex);
+
+ // skip ',' in (Wdy, DD-Mon-YYYY HH:MM:SS GMT) or
+ // (Weekday, DD-Mon-YY HH:MM:SS GMT) if it applies.
+ // "Wednesday" is the longest Weekday which has length 9
+ if ((comaIndex != -1) &&
+ (comaIndex - equalIndex <= 10)) {
+ index = comaIndex + 1;
+ }
+ }
+ semicolonIndex = cookieString.indexOf(SEMICOLON, index);
+ int commaIndex = cookieString.indexOf(COMMA, index);
+ if (semicolonIndex == -1 && commaIndex == -1) {
+ index = length;
+ } else if (semicolonIndex == -1) {
+ index = commaIndex;
+ } else if (commaIndex == -1) {
+ index = semicolonIndex;
+ } else {
+ index = Math.min(semicolonIndex, commaIndex);
+ }
+ String value =
+ cookieString.substring(equalIndex + 1, index);
+
+ // Strip quotes if they exist
+ if (value.length() > 2 && value.charAt(0) == QUOTATION) {
+ int endQuote = value.indexOf(QUOTATION, 1);
+ if (endQuote > 0) {
+ value = value.substring(1, endQuote);
+ }
+ }
+ if (name.equals(EXPIRES)) {
+ try {
+ cookie.expires = HttpDateTime.parse(value);
+ } catch (IllegalArgumentException ex) {
+ Log.e(LOGTAG,
+ "illegal format for expires: " + value);
+ }
+ } else if (name.equals(MAX_AGE)) {
+ try {
+ cookie.expires = System.currentTimeMillis() + 1000
+ * Long.parseLong(value);
+ } catch (NumberFormatException ex) {
+ Log.e(LOGTAG,
+ "illegal format for max-age: " + value);
+ }
+ } else if (name.equals(PATH)) {
+ // make sure path ends with PATH_DELIM
+ if (value.length() > 1 &&
+ value.charAt(value.length() - 1) != PATH_DELIM) {
+ cookie.path = value + PATH_DELIM;
+ } else {
+ cookie.path = value;
+ }
+ } else if (name.equals(DOMAIN)) {
+ int lastPeriod = value.lastIndexOf(PERIOD);
+ try {
+ Integer.parseInt(value.substring(lastPeriod + 1));
+ // no wildcard for ip address match
+ if (!value.equals(host)) {
+ // no cross-site cookie
+ cookie.domain = null;
+ }
+ continue;
+ } catch (NumberFormatException ex) {
+ // ignore the exception, value is a host name
+ }
+ value = value.toLowerCase();
+ if (value.endsWith(host) || host.endsWith(value)) {
+ // domain needs at least two PERIOD
+ if (value.indexOf(PERIOD) == lastPeriod) {
+ value = PERIOD + value;
+ }
+ // disallow cookies set on ccTLDs like [.co.uk]
+ int len = value.length();
+ if ((value.charAt(0) == PERIOD)
+ && (len == lastPeriod + 3)
+ && (len >= 6 && len <= 8)) {
+ String s = value.substring(1, lastPeriod);
+ if (Arrays.binarySearch(BAD_COUNTRY_2LDS, s) >= 0) {
+ cookie.domain = null;
+ continue;
+ }
+ }
+ cookie.domain = value;
+ } else {
+ // no cross-site cookie
+ cookie.domain = null;
+ }
+ }
+ } else {
+ // bad format, force return
+ index = length;
+ }
+ }
+ if (cookie != null && cookie.domain != null) {
+ ret.add(cookie);
+ }
+ }
+ return ret;
+ }
+}
diff --git a/core/java/android/webkit/CookieSyncManager.java b/core/java/android/webkit/CookieSyncManager.java
new file mode 100644
index 0000000..f2511d8
--- /dev/null
+++ b/core/java/android/webkit/CookieSyncManager.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.webkit;
+
+import android.content.Context;
+import android.util.Config;
+import android.util.Log;
+import android.webkit.CookieManager.Cookie;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+
+/**
+ * The class CookieSyncManager is used to synchronize the browser cookies
+ * between RAM and FLASH. To get the best performance, browser cookie is saved
+ * in RAM. We use a separate thread to sync the cookies between RAM and FLASH on
+ * a timer base.
+ * <p>
+ * To use the CookieSyncManager, the host application has to call the following
+ * when the application starts.
+ * <p>
+ * CookieSyncManager.createInstance(context)
+ * <p>
+ * To set up for sync, the host application has to call
+ * <p>
+ * CookieSyncManager.getInstance().startSync()
+ * <p>
+ * in its Activity.onResume(), and call
+ * <p>
+ * CookieSyncManager.getInstance().stopSync()
+ * <p>
+ * in its Activity.onStop().
+ * <p>
+ * To get instant sync instead of waiting for the timer to trigger, the host can
+ * call
+ * <p>
+ * CookieSyncManager.getInstance().sync()
+ */
+public final class CookieSyncManager extends WebSyncManager {
+
+ private static CookieSyncManager sRef;
+
+ // time when last update happened
+ private long mLastUpdate;
+
+ private CookieSyncManager(Context context) {
+ super(context, "CookieSyncManager");
+ }
+
+ /**
+ * Singleton access to a {@link CookieSyncManager}. An
+ * IllegalStateException will be thrown if
+ * {@link CookieSyncManager#createInstance(Context)} is not called before.
+ *
+ * @return CookieSyncManager
+ */
+ public static synchronized CookieSyncManager getInstance() {
+ if (sRef == null) {
+ throw new IllegalStateException(
+ "CookieSyncManager::createInstance() needs to be called "
+ + "before CookieSyncManager::getInstance()");
+ }
+ return sRef;
+ }
+
+ /**
+ * Create a singleton CookieSyncManager within a context
+ * @param context
+ * @return CookieSyncManager
+ */
+ public static synchronized CookieSyncManager createInstance(
+ Context context) {
+ if (sRef == null) {
+ sRef = new CookieSyncManager(context);
+ }
+ return sRef;
+ }
+
+ /**
+ * Package level api, called from CookieManager Get all the cookies which
+ * matches a given base domain.
+ * @param domain
+ * @return A list of Cookie
+ */
+ ArrayList<Cookie> getCookiesForDomain(String domain) {
+ // null mDataBase implies that the host application doesn't support
+ // persistent cookie. No sync needed.
+ if (mDataBase == null) {
+ return new ArrayList<Cookie>();
+ }
+
+ return mDataBase.getCookiesForDomain(domain);
+ }
+
+ /**
+ * Package level api, called from CookieManager Clear all cookies in the
+ * database
+ */
+ void clearAllCookies() {
+ // null mDataBase implies that the host application doesn't support
+ // persistent cookie.
+ if (mDataBase == null) {
+ return;
+ }
+
+ mDataBase.clearCookies();
+ }
+
+ /**
+ * Returns true if there are any saved cookies.
+ */
+ boolean hasCookies() {
+ // null mDataBase implies that the host application doesn't support
+ // persistent cookie.
+ if (mDataBase == null) {
+ return false;
+ }
+
+ return mDataBase.hasCookies();
+ }
+
+ /**
+ * Package level api, called from CookieManager Clear all session cookies in
+ * the database
+ */
+ void clearSessionCookies() {
+ // null mDataBase implies that the host application doesn't support
+ // persistent cookie.
+ if (mDataBase == null) {
+ return;
+ }
+
+ mDataBase.clearSessionCookies();
+ }
+
+ /**
+ * Package level api, called from CookieManager Clear all expired cookies in
+ * the database
+ */
+ void clearExpiredCookies(long now) {
+ // null mDataBase implies that the host application doesn't support
+ // persistent cookie.
+ if (mDataBase == null) {
+ return;
+ }
+
+ mDataBase.clearExpiredCookies(now);
+ }
+
+ protected void syncFromRamToFlash() {
+ if (Config.LOGV) {
+ Log.v(LOGTAG, "CookieSyncManager::syncFromRamToFlash STARTS");
+ }
+
+ if (!CookieManager.getInstance().acceptCookie()) {
+ return;
+ }
+
+ ArrayList<Cookie> cookieList = CookieManager.getInstance()
+ .getUpdatedCookiesSince(mLastUpdate);
+ mLastUpdate = System.currentTimeMillis();
+ syncFromRamToFlash(cookieList);
+
+ ArrayList<Cookie> lruList =
+ CookieManager.getInstance().deleteLRUDomain();
+ syncFromRamToFlash(lruList);
+
+ if (Config.LOGV) {
+ Log.v(LOGTAG, "CookieSyncManager::syncFromRamToFlash DONE");
+ }
+ }
+
+ private void syncFromRamToFlash(ArrayList<Cookie> list) {
+ Iterator<Cookie> iter = list.iterator();
+ while (iter.hasNext()) {
+ Cookie cookie = iter.next();
+ if (cookie.mode != Cookie.MODE_NORMAL) {
+ if (cookie.mode != Cookie.MODE_NEW) {
+ mDataBase.deleteCookies(cookie.domain, cookie.path,
+ cookie.name);
+ }
+ if (cookie.mode != Cookie.MODE_DELETED) {
+ mDataBase.addCookie(cookie);
+ CookieManager.getInstance().syncedACookie(cookie);
+ } else {
+ CookieManager.getInstance().deleteACookie(cookie);
+ }
+ }
+ }
+ }
+}
diff --git a/core/java/android/webkit/DataLoader.java b/core/java/android/webkit/DataLoader.java
new file mode 100644
index 0000000..dcdc949
--- /dev/null
+++ b/core/java/android/webkit/DataLoader.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.webkit;
+
+import org.apache.http.protocol.HTTP;
+
+import android.net.http.Headers;
+
+import java.io.ByteArrayInputStream;
+
+/**
+ * This class is a concrete implementation of StreamLoader that uses the
+ * content supplied as a URL as the source for the stream. The mimetype
+ * optionally provided in the URL is extracted and inserted into the HTTP
+ * response headers.
+ */
+class DataLoader extends StreamLoader {
+
+ private String mContentType; // Content mimetype, if supplied in URL
+
+ /**
+ * Constructor uses the dataURL as the source for an InputStream
+ * @param dataUrl data: URL string optionally containing a mimetype
+ * @param loadListener LoadListener to pass the content to
+ */
+ DataLoader(String dataUrl, LoadListener loadListener) {
+ super(loadListener);
+
+ String url = dataUrl.substring("data:".length());
+ String content;
+ int commaIndex = url.indexOf(',');
+ if (commaIndex != -1) {
+ mContentType = url.substring(0, commaIndex);
+ content = url.substring(commaIndex + 1);
+ } else {
+ content = url;
+ }
+ mDataStream = new ByteArrayInputStream(content.getBytes());
+ mContentLength = content.length();
+ }
+
+ @Override
+ protected boolean setupStreamAndSendStatus() {
+ mHandler.status(1, 1, 0, "OK");
+ return true;
+ }
+
+ @Override
+ protected void buildHeaders(Headers headers) {
+ if (mContentType != null) {
+ headers.setContentType(mContentType);
+ }
+ }
+
+ /**
+ * Construct a DataLoader and instruct it to start loading.
+ *
+ * @param url data: URL string optionally containing a mimetype
+ * @param loadListener LoadListener to pass the content to
+ */
+ public static void requestUrl(String url, LoadListener loadListener) {
+ DataLoader loader = new DataLoader(url, loadListener);
+ loader.load();
+ }
+
+}
diff --git a/core/java/android/webkit/DateSorter.java b/core/java/android/webkit/DateSorter.java
new file mode 100644
index 0000000..3dc15c1
--- /dev/null
+++ b/core/java/android/webkit/DateSorter.java
@@ -0,0 +1,121 @@
+/*
+ * 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.webkit;
+
+import android.content.Context;
+import android.util.Log;
+
+import java.util.Calendar;
+import java.util.Date;
+
+/**
+ * Sorts dates into the following groups:
+ * Today
+ * Yesterday
+ * five days ago
+ * one month ago
+ * older than a month ago
+ */
+
+public class DateSorter {
+
+ private static final String LOGTAG = "webkit";
+
+ /** must be >= 3 */
+ public static final int DAY_COUNT = 5;
+
+ private long [] mBins = new long[DAY_COUNT];
+ private String [] mLabels = new String[DAY_COUNT];
+
+ Date mDate = new Date();
+ Calendar mCal = Calendar.getInstance();
+
+ /**
+ * @param context Application context
+ */
+ public DateSorter(Context context) {
+
+ Calendar c = Calendar.getInstance();
+ beginningOfDay(c);
+
+ // Create the bins
+ mBins[0] = c.getTimeInMillis(); // Today
+ c.roll(Calendar.DAY_OF_YEAR, -1);
+ mBins[1] = c.getTimeInMillis(); // Yesterday
+ c.roll(Calendar.DAY_OF_YEAR, -4);
+ mBins[2] = c.getTimeInMillis(); // Five days ago
+ c.roll(Calendar.DAY_OF_YEAR, 5); // move back to today
+ c.roll(Calendar.MONTH, -1);
+ mBins[3] = c.getTimeInMillis(); // One month ago
+ c.roll(Calendar.MONTH, -1);
+ mBins[4] = c.getTimeInMillis(); // Over one month ago
+
+ // build labels
+ mLabels[0] = context.getText(com.android.internal.R.string.today).toString();
+ mLabels[1] = context.getText(com.android.internal.R.string.yesterday).toString();
+ mLabels[2] = context.getString(com.android.internal.R.string.daysDurationPastPlural, 5);
+ mLabels[3] = context.getText(com.android.internal.R.string.oneMonthDurationPast).toString();
+ StringBuilder sb = new StringBuilder();
+ sb.append(context.getText(com.android.internal.R.string.before)).append(" ");
+ sb.append(context.getText(com.android.internal.R.string.oneMonthDurationPast));
+ mLabels[4] = sb.toString();
+
+
+ }
+
+ /**
+ * @param time time since the Epoch in milliseconds, such as that
+ * returned by Calendar.getTimeInMillis()
+ * @return an index from 0 to (DAY_COUNT - 1) that identifies which
+ * date bin this date belongs to
+ */
+ public int getIndex(long time) {
+ // Lame linear search
+ for (int i = 0; i < DAY_COUNT; i++) {
+ if (time > mBins[i]) return i;
+ }
+ return DAY_COUNT - 1;
+ }
+
+ /**
+ * @param index date bin index as returned by getIndex()
+ * @return string label suitable for display to user
+ */
+ public String getLabel(int index) {
+ return mLabels[index];
+ }
+
+
+ /**
+ * @param index date bin index as returned by getIndex()
+ * @return date boundary at given index
+ */
+ public long getBoundary(int index) {
+ return mBins[index];
+ }
+
+ /**
+ * Calcuate 12:00am by zeroing out hour, minute, second, millisecond
+ */
+ private Calendar beginningOfDay(Calendar c) {
+ c.set(Calendar.HOUR_OF_DAY, 0);
+ c.set(Calendar.MINUTE, 0);
+ c.set(Calendar.SECOND, 0);
+ c.set(Calendar.MILLISECOND, 0);
+ return c;
+ }
+}
diff --git a/core/java/android/webkit/DownloadListener.java b/core/java/android/webkit/DownloadListener.java
new file mode 100644
index 0000000..dfaa1b9
--- /dev/null
+++ b/core/java/android/webkit/DownloadListener.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.webkit;
+
+public interface DownloadListener {
+
+ /**
+ * Notify the host application that a file should be downloaded
+ * @param url The full url to the content that should be downloaded
+ * @param userAgent the user agent to be used for the download.
+ * @param contentDisposition Content-disposition http header, if
+ * present.
+ * @param mimetype The mimetype of the content reported by the server
+ * @param contentLength The file size reported by the server
+ */
+ public void onDownloadStart(String url, String userAgent,
+ String contentDisposition, String mimetype, long contentLength);
+
+}
diff --git a/core/java/android/webkit/FileLoader.java b/core/java/android/webkit/FileLoader.java
new file mode 100644
index 0000000..6696bae
--- /dev/null
+++ b/core/java/android/webkit/FileLoader.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.webkit;
+
+import android.content.Context;
+import android.content.res.AssetManager;
+import android.net.http.EventHandler;
+import android.net.http.Headers;
+import android.os.Environment;
+
+import java.io.File;
+import java.io.FileInputStream;
+
+/**
+ * This class is a concrete implementation of StreamLoader that uses a
+ * file or asset as the source for the stream.
+ *
+ */
+class FileLoader extends StreamLoader {
+
+ private String mPath; // Full path to the file to load
+ private Context mContext; // Application context, used for asset loads
+ private boolean mIsAsset; // Indicates if the load is an asset or not
+
+ /**
+ * Construct a FileLoader with the file URL specified as the content
+ * source.
+ *
+ * @param url Full file url pointing to content to be loaded
+ * @param loadListener LoadListener to pass the content to
+ * @param context Context to use to access the asset.
+ * @param asset true if url points to an asset.
+ */
+ FileLoader(String url, LoadListener loadListener, Context context,
+ boolean asset) {
+ super(loadListener);
+ mIsAsset = asset;
+ mContext = context;
+
+ // clean the Url
+ int index = url.indexOf('?');
+ if (mIsAsset) {
+ mPath = index > 0 ? URLUtil.stripAnchor(
+ url.substring(URLUtil.ASSET_BASE.length(), index)) :
+ URLUtil.stripAnchor(url.substring(
+ URLUtil.ASSET_BASE.length()));
+ } else {
+ mPath = index > 0 ? URLUtil.stripAnchor(
+ url.substring(URLUtil.FILE_BASE.length(), index)) :
+ URLUtil.stripAnchor(url.substring(
+ URLUtil.FILE_BASE.length()));
+ }
+ }
+
+ @Override
+ protected boolean setupStreamAndSendStatus() {
+ try {
+ if (mIsAsset) {
+ mDataStream = mContext.getAssets().open(mPath,
+ AssetManager.ACCESS_STREAMING);
+ } else {
+ mHandler.error(EventHandler.FILE_ERROR,
+ mContext.getString(
+ com.android.internal.R.string.httpErrorFileNotFound));
+ return false;
+/*
+ if (!mPath.startsWith(
+ Environment.getExternalStorageDirectory().getPath())) {
+ mHandler.error(EventHandler.FILE_ERROR,
+ mContext.getString(
+ com.android.internal.R.string.httpErrorFileNotFound));
+ return false;
+ }
+ mDataStream = new FileInputStream(mPath);
+ mContentLength = (new File(mPath)).length();
+*/
+ }
+ mHandler.status(1, 1, 0, "OK");
+
+ } catch (java.io.FileNotFoundException ex) {
+ mHandler.error(
+ EventHandler.FILE_NOT_FOUND_ERROR,
+ mContext.getString(com.android.internal.R.string.httpErrorFileNotFound) +
+ " " + ex.getMessage());
+ return false;
+
+ } catch (java.io.IOException ex) {
+ mHandler.error(EventHandler.FILE_ERROR,
+ mContext.getString(
+ com.android.internal.R.string.httpErrorFileNotFound) +
+ " " + ex.getMessage());
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ protected void buildHeaders(Headers headers) {
+ // do nothing.
+ }
+
+
+ /**
+ * Construct a FileLoader and instruct it to start loading.
+ *
+ * @param url Full file url pointing to content to be loaded
+ * @param loadListener LoadListener to pass the content to
+ * @param context Context to use to access the asset.
+ * @param asset true if url points to an asset.
+ */
+ public static void requestUrl(String url, LoadListener loadListener,
+ Context context, boolean asset) {
+ FileLoader loader = new FileLoader(url, loadListener, context, asset);
+ loader.load();
+ }
+
+}
diff --git a/core/java/android/webkit/FrameLoader.java b/core/java/android/webkit/FrameLoader.java
new file mode 100644
index 0000000..ebfebd0
--- /dev/null
+++ b/core/java/android/webkit/FrameLoader.java
@@ -0,0 +1,402 @@
+/*
+ * 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.webkit;
+
+import android.net.http.EventHandler;
+import android.net.http.RequestHandle;
+import android.util.Config;
+import android.util.Log;
+import android.webkit.CacheManager.CacheResult;
+import android.webkit.UrlInterceptRegistry;
+
+import java.util.HashMap;
+import java.util.Map;
+
+class FrameLoader {
+
+ protected LoadListener mListener;
+ protected Map<String, String> mHeaders;
+ protected String mMethod;
+ protected String mPostData;
+ protected boolean mIsHighPriority;
+ protected Network mNetwork;
+ protected int mCacheMode;
+ protected String mReferrer;
+ protected String mUserAgent;
+ protected String mContentType;
+
+ private static final int URI_PROTOCOL = 0x100;
+
+ private static final String CONTENT_TYPE = "content-type";
+
+ // Contents of an about:blank page
+ private static final String mAboutBlank =
+ "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EB\">" +
+ "<html><head><title>about:blank</title></head><body></body></html>";
+
+ static final String HEADER_STR = "text/xml, text/html, " +
+ "application/xhtml+xml, image/png, text/plain, */*;q=0.8";
+
+ private static final String LOGTAG = "webkit";
+
+ /*
+ * Construct the Accept_Language once. If the user changes language, then
+ * the phone will be rebooted.
+ */
+ private static String ACCEPT_LANGUAGE;
+ static {
+ // Set the accept-language to the current locale plus US if we are in a
+ // different locale than US.
+ java.util.Locale l = java.util.Locale.getDefault();
+ ACCEPT_LANGUAGE = "";
+ if (l.getLanguage() != null) {
+ ACCEPT_LANGUAGE += l.getLanguage();
+ if (l.getCountry() != null) {
+ ACCEPT_LANGUAGE += "-" + l.getCountry();
+ }
+ }
+ if (!l.equals(java.util.Locale.US)) {
+ ACCEPT_LANGUAGE += ", ";
+ java.util.Locale us = java.util.Locale.US;
+ if (us.getLanguage() != null) {
+ ACCEPT_LANGUAGE += us.getLanguage();
+ if (us.getCountry() != null) {
+ ACCEPT_LANGUAGE += "-" + us.getCountry();
+ }
+ }
+ }
+ }
+
+
+ FrameLoader(LoadListener listener, String userAgent,
+ String method, boolean highPriority) {
+ mListener = listener;
+ mHeaders = null;
+ mMethod = method;
+ mIsHighPriority = highPriority;
+ mCacheMode = WebSettings.LOAD_NORMAL;
+ mUserAgent = userAgent;
+ }
+
+ public void setReferrer(String ref) {
+ // only set referrer for http or https
+ if (URLUtil.isNetworkUrl(ref)) mReferrer = ref;
+ }
+
+ public void setPostData(String postData) {
+ mPostData = postData;
+ }
+
+ public void setContentTypeForPost(String postContentType) {
+ mContentType = postContentType;
+ }
+
+ public void setCacheMode(int cacheMode) {
+ mCacheMode = cacheMode;
+ }
+
+ public void setHeaders(HashMap headers) {
+ mHeaders = headers;
+ }
+
+ public LoadListener getLoadListener() {
+ return mListener;
+ }
+
+ /**
+ * Issues the load request.
+ *
+ * Return value does not indicate if the load was successful or not. It
+ * simply indicates that the load request is reasonable.
+ *
+ * @return true if the load is reasonable.
+ */
+ public boolean executeLoad() {
+ String url = mListener.url();
+
+ // Attempt to decode the percent-encoded url.
+ try {
+ url = new String(URLUtil.decode(url.getBytes()));
+ } catch (IllegalArgumentException e) {
+ // Fail with a bad url error if the decode fails.
+ mListener.error(EventHandler.ERROR_BAD_URL,
+ mListener.getContext().getString(
+ com.android.internal.R.string.httpErrorBadUrl));
+ return false;
+ }
+
+ if (URLUtil.isNetworkUrl(url)){
+ mNetwork = Network.getInstance(mListener.getContext());
+ return handleHTTPLoad(false);
+ } else if (URLUtil.isCookielessProxyUrl(url)) {
+ mNetwork = Network.getInstance(mListener.getContext());
+ return handleHTTPLoad(true);
+ } else if (handleLocalFile(url, mListener)) {
+ return true;
+ }
+ if (Config.LOGV) {
+ Log.v(LOGTAG, "FrameLoader.executeLoad: url protocol not supported:"
+ + mListener.url());
+ }
+ mListener.error(EventHandler.ERROR_UNSUPPORTED_SCHEME,
+ mListener.getContext().getText(
+ com.android.internal.R.string.httpErrorUnsupportedScheme).toString());
+ return false;
+
+ }
+
+ /* package */
+ static boolean handleLocalFile(String url, LoadListener loadListener) {
+ if (URLUtil.isAssetUrl(url)) {
+ FileLoader.requestUrl(url, loadListener, loadListener.getContext(),
+ true);
+ return true;
+ } else if (URLUtil.isFileUrl(url)) {
+ FileLoader.requestUrl(url, loadListener, loadListener.getContext(),
+ false);
+ return true;
+ } else if (URLUtil.isContentUrl(url)) {
+ // Send the raw url to the ContentLoader because it will do a
+ // permission check and the url has to match..
+ ContentLoader.requestUrl(loadListener.url(), loadListener,
+ loadListener.getContext());
+ return true;
+ } else if (URLUtil.isDataUrl(url)) {
+ DataLoader.requestUrl(url, loadListener);
+ return true;
+ } else if (URLUtil.isAboutUrl(url)) {
+ loadListener.data(mAboutBlank.getBytes(), mAboutBlank.length());
+ loadListener.endData();
+ return true;
+ }
+ return false;
+ }
+
+ protected boolean handleHTTPLoad(boolean proxyUrl) {
+ if (mHeaders == null) {
+ mHeaders = new HashMap<String, String>();
+ }
+ populateStaticHeaders();
+
+ if (!proxyUrl) {
+ // Don't add private information if this is a proxy load, ie don't
+ // add cookies and authentication
+ populateHeaders();
+ } else {
+ // If this is a proxy URL, fix it to be a network load
+ mListener.setUrl("http://"
+ + mListener.url().substring(URLUtil.PROXY_BASE.length()));
+ }
+
+ // response was handled by UrlIntercept, don't issue HTTP request
+ if (handleUrlIntercept()) return true;
+
+ // response was handled by Cache, don't issue HTTP request
+ if (handleCache()) {
+ // push the request data down to the LoadListener
+ // as response from the cache could be a redirect
+ // and we may need to initiate a network request if the cache
+ // can't satisfy redirect URL
+ mListener.setRequestData(mMethod, mHeaders, mPostData,
+ mIsHighPriority);
+ return true;
+ }
+
+ if (Config.LOGV) {
+ Log.v(LOGTAG, "FrameLoader: http " + mMethod + " load for: "
+ + mListener.url());
+ }
+
+ boolean ret = false;
+ int error = EventHandler.ERROR_UNSUPPORTED_SCHEME;
+
+ try {
+ ret = mNetwork.requestURL(mMethod, mHeaders,
+ mPostData, mListener, mIsHighPriority);
+ } catch (android.net.ParseException ex) {
+ error = EventHandler.ERROR_BAD_URL;
+ } catch (java.lang.RuntimeException ex) {
+ /* probably an empty header set by javascript. We want
+ the same result as bad URL */
+ error = EventHandler.ERROR_BAD_URL;
+ }
+ if (!ret) {
+ mListener.error(error, mListener.getContext().getText(
+ EventHandler.errorStringResources[Math.abs(error)]).toString());
+ return false;
+ }
+ return true;
+ }
+
+ /*
+ * This function is used by handleUrlInterecpt and handleCache to
+ * setup a load from the byte stream in a CacheResult.
+ */
+ protected void startCacheLoad(CacheResult result) {
+ if (Config.LOGV) {
+ Log.v(LOGTAG, "FrameLoader: loading from cache: "
+ + mListener.url());
+ }
+ // Tell the Listener respond with the cache file
+ CacheLoader cacheLoader =
+ new CacheLoader(mListener, result);
+ cacheLoader.load();
+ }
+
+ /*
+ * This function is used by handleHTTPLoad to allow URL
+ * interception. This can be used to provide alternative load
+ * methods such as locally stored versions or for debugging.
+ *
+ * Returns true if the response was handled by UrlIntercept.
+ */
+ protected boolean handleUrlIntercept() {
+ // Check if the URL can be served from UrlIntercept. If
+ // successful, return the data just like a cache hit.
+ CacheResult result = UrlInterceptRegistry.getSurrogate(
+ mListener.url(), mHeaders);
+ if(result != null) {
+ // Intercepted. The data is stored in result.stream. Setup
+ // a load from the CacheResult.
+ startCacheLoad(result);
+ return true;
+ }
+ // Not intercepted. Carry on as normal.
+ return false;
+ }
+
+ /*
+ * This function is used by the handleHTTPLoad to setup the cache headers
+ * correctly.
+ * Returns true if the response was handled from the cache
+ */
+ protected boolean handleCache() {
+ switch (mCacheMode) {
+ // This mode is normally used for a reload, it instructs the http
+ // loader to not use the cached content.
+ case WebSettings.LOAD_NO_CACHE:
+ break;
+
+
+ // This mode is used when the content should only be loaded from
+ // the cache. If it is not there, then fail the load. This is used
+ // to load POST content in a history navigation.
+ case WebSettings.LOAD_CACHE_ONLY: {
+ CacheResult result = CacheManager.getCacheFile(mListener.url(),
+ null);
+ if (result != null) {
+ startCacheLoad(result);
+ } else {
+ // This happens if WebCore was first told that the POST
+ // response was in the cache, then when we try to use it
+ // it has gone.
+ // Generate a file not found error
+ int err = EventHandler.FILE_NOT_FOUND_ERROR;
+ mListener.error(err, mListener.getContext().getText(
+ EventHandler.errorStringResources[Math.abs(err)])
+ .toString());
+ }
+ return true;
+ }
+
+ // This mode is for when the user is doing a history navigation
+ // in the browser and should returned cached content regardless
+ // of it's state. If it is not in the cache, then go to the
+ // network.
+ case WebSettings.LOAD_CACHE_ELSE_NETWORK: {
+ if (Config.LOGV) {
+ Log.v(LOGTAG, "FrameLoader: checking cache: "
+ + mListener.url());
+ }
+ // Get the cache file name for the current URL, passing null for
+ // the validation headers causes no validation to occur
+ CacheResult result = CacheManager.getCacheFile(mListener.url(),
+ null);
+ if (result != null) {
+ startCacheLoad(result);
+ return true;
+ }
+ break;
+ }
+
+ // This is the default case, which is to check to see if the
+ // content in the cache can be used. If it can be used, then
+ // use it. If it needs revalidation then the relevant headers
+ // are added to the request.
+ default:
+ case WebSettings.LOAD_NORMAL:
+ return mListener.checkCache(mHeaders);
+ }// end of switch
+
+ return false;
+ }
+
+ /**
+ * Add the static headers that don't change with each request.
+ */
+ private void populateStaticHeaders() {
+ // Accept header should already be there as they are built by WebCore,
+ // but in the case they are missing, add some.
+ String accept = mHeaders.get("Accept");
+ if (accept == null || accept.length() == 0) {
+ mHeaders.put("Accept", HEADER_STR);
+ }
+ mHeaders.put("Accept-Charset", "utf-8, iso-8859-1, utf-16, *;q=0.7");
+
+ if (ACCEPT_LANGUAGE.length() > 0) {
+ mHeaders.put("Accept-Language", ACCEPT_LANGUAGE);
+ }
+
+ mHeaders.put("User-Agent", mUserAgent);
+ }
+
+ /**
+ * Add the content related headers. These headers contain user private data
+ * and is not used when we are proxying an untrusted request.
+ */
+ private void populateHeaders() {
+
+ if (mReferrer != null) mHeaders.put("Referer", mReferrer);
+ if (mContentType != null) mHeaders.put(CONTENT_TYPE, mContentType);
+
+ // if we have an active proxy and have proxy credentials, do pre-emptive
+ // authentication to avoid an extra round-trip:
+ if (mNetwork.isValidProxySet()) {
+ String username;
+ String password;
+ /* The proxy credentials can be set in the Network thread */
+ synchronized (mNetwork) {
+ username = mNetwork.getProxyUsername();
+ password = mNetwork.getProxyPassword();
+ }
+ if (username != null && password != null) {
+ // we collect credentials ONLY if the proxy scheme is BASIC!!!
+ String proxyHeader = RequestHandle.authorizationHeader(true);
+ mHeaders.put(proxyHeader,
+ "Basic " + RequestHandle.computeBasicAuthResponse(
+ username, password));
+ }
+ }
+
+ // Set cookie header
+ String cookie = CookieManager.getInstance().getCookie(
+ mListener.getWebAddress());
+ if (cookie != null && cookie.length() > 0) {
+ mHeaders.put("cookie", cookie);
+ }
+ }
+}
diff --git a/core/java/android/webkit/HttpAuthHandler.java b/core/java/android/webkit/HttpAuthHandler.java
new file mode 100644
index 0000000..48b9eec
--- /dev/null
+++ b/core/java/android/webkit/HttpAuthHandler.java
@@ -0,0 +1,186 @@
+/*
+ * 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.webkit;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+
+import java.util.ListIterator;
+import java.util.LinkedList;
+
+/**
+ * HTTP authentication handler: local handler that takes care
+ * of HTTP authentication requests. This class is passed as a
+ * parameter to BrowserCallback.displayHttpAuthDialog and is
+ * meant to receive the user's response.
+ */
+public class HttpAuthHandler extends Handler {
+ /* It is important that the handler is in Network, because
+ * we want to share it accross multiple loaders and windows
+ * (like our subwindow and the main window).
+ */
+
+ private static final String LOGTAG = "network";
+
+ /**
+ * Network.
+ */
+ private Network mNetwork;
+
+ /**
+ * Loader queue.
+ */
+ private LinkedList<LoadListener> mLoaderQueue;
+
+
+ // Message id for handling the user response
+ private final int AUTH_PROCEED = 100;
+ private final int AUTH_CANCEL = 200;
+
+ /**
+ * Creates a new HTTP authentication handler with an empty
+ * loader queue
+ *
+ * @param network The parent network object
+ */
+ /* package */ HttpAuthHandler(Network network) {
+ mNetwork = network;
+ mLoaderQueue = new LinkedList<LoadListener>();
+ }
+
+
+ @Override
+ public void handleMessage(Message msg) {
+ LoadListener loader = null;
+ synchronized (mLoaderQueue) {
+ loader = mLoaderQueue.poll();
+ }
+
+ switch (msg.what) {
+ case AUTH_PROCEED:
+ String username = msg.getData().getString("username");
+ String password = msg.getData().getString("password");
+
+ loader.handleAuthResponse(username, password);
+ break;
+
+ case AUTH_CANCEL:
+
+ mNetwork.resetHandlersAndStopLoading(loader.getFrame());
+ break;
+ }
+
+ processNextLoader();
+ }
+
+
+ /**
+ * Proceed with the authorization with the given credentials
+ *
+ * @param username The username to use for authentication
+ * @param password The password to use for authentication
+ */
+ public void proceed(String username, String password) {
+ Message msg = obtainMessage(AUTH_PROCEED);
+ msg.getData().putString("username", username);
+ msg.getData().putString("password", password);
+ sendMessage(msg);
+ }
+
+ /**
+ * Cancel the authorization request
+ */
+ public void cancel() {
+ sendMessage(obtainMessage(AUTH_CANCEL));
+ }
+
+ /**
+ * @return True if we can use user credentials on record
+ * (ie, if we did not fail trying to use them last time)
+ */
+ public boolean useHttpAuthUsernamePassword() {
+ LoadListener loader = null;
+ synchronized (mLoaderQueue) {
+ loader = mLoaderQueue.peek();
+ }
+ if (loader != null) {
+ return !loader.authCredentialsInvalid();
+ }
+
+ return false;
+ }
+
+ /**
+ * Resets the HTTP-authentication request handler, removes
+ * all loaders that share the same BrowserFrame
+ *
+ * @param frame The browser frame
+ */
+ /* package */ void reset(BrowserFrame frame) {
+ synchronized (mLoaderQueue) {
+ ListIterator<LoadListener> i = mLoaderQueue.listIterator(0);
+ while (i.hasNext()) {
+ LoadListener loader = i.next();
+ if (frame == loader.getFrame()) {
+ i.remove();
+ }
+ }
+ }
+ }
+
+ /**
+ * Enqueues the loader, if the loader is the only element
+ * in the queue, starts processing the loader
+ *
+ * @param loader The loader that resulted in this http
+ * authentication request
+ */
+ /* package */ void handleAuthRequest(LoadListener loader) {
+ boolean processNext = false;
+
+ synchronized (mLoaderQueue) {
+ mLoaderQueue.offer(loader);
+ processNext =
+ (mLoaderQueue.size() == 1);
+ }
+
+ if (processNext) {
+ processNextLoader();
+ }
+ }
+
+ /**
+ * Process the next loader in the queue (helper method)
+ */
+ private void processNextLoader() {
+ LoadListener loader = null;
+ synchronized (mLoaderQueue) {
+ loader = mLoaderQueue.peek();
+ }
+ if (loader != null) {
+ CallbackProxy proxy = loader.getFrame().getCallbackProxy();
+
+ String hostname = loader.proxyAuthenticate() ?
+ mNetwork.getProxyHostname() : loader.host();
+
+ String realm = loader.realm();
+
+ proxy.onReceivedHttpAuthRequest(this, hostname, realm);
+ }
+ }
+}
diff --git a/core/java/android/webkit/HttpDateTime.java b/core/java/android/webkit/HttpDateTime.java
new file mode 100644
index 0000000..b22f2ba
--- /dev/null
+++ b/core/java/android/webkit/HttpDateTime.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.webkit;
+
+import android.pim.Time;
+
+import java.util.Calendar;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+
+class HttpDateTime {
+
+ /*
+ * Regular expression for parsing HTTP-date.
+ *
+ * Wdy, DD Mon YYYY HH:MM:SS GMT
+ * RFC 822, updated by RFC 1123
+ *
+ * Weekday, DD-Mon-YY HH:MM:SS GMT
+ * RFC 850, obsoleted by RFC 1036
+ *
+ * Wdy Mon DD HH:MM:SS YYYY
+ * ANSI C's asctime() format
+ *
+ * with following variations
+ *
+ * Wdy, DD-Mon-YYYY HH:MM:SS GMT
+ * Wdy, (SP)D Mon YYYY HH:MM:SS GMT
+ * Wdy,DD Mon YYYY HH:MM:SS GMT
+ * Wdy, DD-Mon-YY HH:MM:SS GMT
+ * Wdy, DD Mon YYYY HH:MM:SS -HHMM
+ * Wdy, DD Mon YYYY HH:MM:SS
+ * Wdy Mon (SP)D HH:MM:SS YYYY
+ * Wdy Mon DD HH:MM:SS YYYY GMT
+ */
+ private static final String HTTP_DATE_RFC_REGEXP =
+ "([0-9]{1,2})[- ]([A-Za-z]{3,3})[- ]([0-9]{2,4})[ ]"
+ + "([0-9][0-9]:[0-9][0-9]:[0-9][0-9])";
+
+ private static final String HTTP_DATE_ANSIC_REGEXP =
+ "[ ]([A-Za-z]{3,3})[ ]+([0-9]{1,2})[ ]"
+ + "([0-9][0-9]:[0-9][0-9]:[0-9][0-9])[ ]([0-9]{2,4})";
+
+ /**
+ * The compiled version of the HTTP-date regular expressions.
+ */
+ private static final Pattern HTTP_DATE_RFC_PATTERN =
+ Pattern.compile(HTTP_DATE_RFC_REGEXP);
+ private static final Pattern HTTP_DATE_ANSIC_PATTERN =
+ Pattern.compile(HTTP_DATE_ANSIC_REGEXP);
+
+ private static class TimeOfDay {
+ int hour;
+ int minute;
+ int second;
+ }
+
+ public static Long parse(String timeString)
+ throws IllegalArgumentException {
+
+ int date = 1;
+ int month = Calendar.JANUARY;
+ int year = 1970;
+ TimeOfDay timeOfDay = new TimeOfDay();
+
+ Matcher rfcMatcher = HTTP_DATE_RFC_PATTERN.matcher(timeString);
+ if (rfcMatcher.find()) {
+ date = getDate(rfcMatcher.group(1));
+ month = getMonth(rfcMatcher.group(2));
+ year = getYear(rfcMatcher.group(3));
+ timeOfDay = getTime(rfcMatcher.group(4));
+ } else {
+ Matcher ansicMatcher = HTTP_DATE_ANSIC_PATTERN.matcher(timeString);
+ if (ansicMatcher.find()) {
+ month = getMonth(ansicMatcher.group(1));
+ date = getDate(ansicMatcher.group(2));
+ timeOfDay = getTime(ansicMatcher.group(3));
+ year = getYear(ansicMatcher.group(4));
+ } else {
+ throw new IllegalArgumentException();
+ }
+ }
+
+ // FIXME: Y2038 BUG!
+ if (year >= 2038) {
+ year = 2038;
+ month = Calendar.JANUARY;
+ date = 1;
+ }
+
+ Time time = new Time(Time.TIMEZONE_UTC);
+ time.set(timeOfDay.second, timeOfDay.minute, timeOfDay.hour, date,
+ month, year);
+ return time.toMillis(false /* use isDst */);
+ }
+
+ private static int getDate(String dateString) {
+ if (dateString.length() == 2) {
+ return (dateString.charAt(0) - '0') * 10
+ + (dateString.charAt(1) - '0');
+ } else {
+ return (dateString.charAt(0) - '0');
+ }
+ }
+
+ /*
+ * jan = 9 + 0 + 13 = 22
+ * feb = 5 + 4 + 1 = 10
+ * mar = 12 + 0 + 17 = 29
+ * apr = 0 + 15 + 17 = 32
+ * may = 12 + 0 + 24 = 36
+ * jun = 9 + 20 + 13 = 42
+ * jul = 9 + 20 + 11 = 40
+ * aug = 0 + 20 + 6 = 26
+ * sep = 18 + 4 + 15 = 37
+ * oct = 14 + 2 + 19 = 35
+ * nov = 13 + 14 + 21 = 48
+ * dec = 3 + 4 + 2 = 9
+ */
+ private static int getMonth(String monthString) {
+ int hash = Character.toLowerCase(monthString.charAt(0)) +
+ Character.toLowerCase(monthString.charAt(1)) +
+ Character.toLowerCase(monthString.charAt(2)) - 3 * 'a';
+ switch (hash) {
+ case 22:
+ return Calendar.JANUARY;
+ case 10:
+ return Calendar.FEBRUARY;
+ case 29:
+ return Calendar.MARCH;
+ case 32:
+ return Calendar.APRIL;
+ case 36:
+ return Calendar.MAY;
+ case 42:
+ return Calendar.JUNE;
+ case 40:
+ return Calendar.JULY;
+ case 26:
+ return Calendar.AUGUST;
+ case 37:
+ return Calendar.SEPTEMBER;
+ case 35:
+ return Calendar.OCTOBER;
+ case 48:
+ return Calendar.NOVEMBER;
+ case 9:
+ return Calendar.DECEMBER;
+ default:
+ throw new IllegalArgumentException();
+ }
+ }
+
+ private static int getYear(String yearString) {
+ if (yearString.length() == 2) {
+ int year = (yearString.charAt(0) - '0') * 10
+ + (yearString.charAt(1) - '0');
+ if (year >= 70) {
+ return year + 1900;
+ } else {
+ return year + 2000;
+ }
+ } else
+ return (yearString.charAt(0) - '0') * 1000
+ + (yearString.charAt(1) - '0') * 100
+ + (yearString.charAt(2) - '0') * 10
+ + (yearString.charAt(3) - '0');
+ }
+
+ private static TimeOfDay getTime(String timeString) {
+ TimeOfDay time = new TimeOfDay();
+ time.hour = (timeString.charAt(0) - '0') * 10
+ + (timeString.charAt(1) - '0');
+ time.minute = (timeString.charAt(3) - '0') * 10
+ + (timeString.charAt(4) - '0');
+ time.second = (timeString.charAt(6) - '0') * 10
+ + (timeString.charAt(7) - '0');
+ return time;
+ }
+}
diff --git a/core/java/android/webkit/JWebCoreJavaBridge.java b/core/java/android/webkit/JWebCoreJavaBridge.java
new file mode 100644
index 0000000..1cfea99
--- /dev/null
+++ b/core/java/android/webkit/JWebCoreJavaBridge.java
@@ -0,0 +1,194 @@
+/*
+ * 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.webkit;
+
+import android.os.Handler;
+import android.os.Message;
+import android.util.Config;
+import android.util.Log;
+
+final class JWebCoreJavaBridge extends Handler {
+ // Identifier for the timer message.
+ private static final int TIMER_MESSAGE = 1;
+ // ID for servicing functionptr queue
+ private static final int FUNCPTR_MESSAGE = 2;
+ // Log system identifier.
+ private static final String LOGTAG = "webkit-timers";
+
+ // Native object pointer for interacting in native code.
+ private int mNativeBridge;
+ // Instant timer is used to implement a timer that needs to fire almost
+ // immediately.
+ private boolean mHasInstantTimer;
+ // Reference count the pause/resume of timers
+ private int mPauseTimerRefCount;
+
+ /**
+ * Construct a new JWebCoreJavaBridge to interface with
+ * WebCore timers and cookies.
+ */
+ public JWebCoreJavaBridge() {
+ nativeConstructor();
+ }
+
+ @Override
+ protected void finalize() {
+ nativeFinalize();
+ }
+
+ /**
+ * handleMessage
+ * @param msg The dispatched message.
+ *
+ * The only accepted message currently is TIMER_MESSAGE
+ */
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case TIMER_MESSAGE: {
+ PerfChecker checker = new PerfChecker();
+ // clear the flag so that sharedTimerFired() can set a new timer
+ mHasInstantTimer = false;
+ sharedTimerFired();
+ checker.responseAlert("sharedTimer");
+ break;
+ }
+ case FUNCPTR_MESSAGE:
+ nativeServiceFuncPtrQueue();
+ break;
+ }
+ }
+
+ // called from JNI side
+ private void signalServiceFuncPtrQueue() {
+ Message msg = obtainMessage(FUNCPTR_MESSAGE);
+ sendMessage(msg);
+ }
+
+ private native void nativeServiceFuncPtrQueue();
+
+ /**
+ * Pause all timers.
+ */
+ public void pause() {
+ if (--mPauseTimerRefCount == 0) {
+ setDeferringTimers(true);
+ }
+ }
+
+ /**
+ * Resume all timers.
+ */
+ public void resume() {
+ if (++mPauseTimerRefCount == 1) {
+ setDeferringTimers(false);
+ }
+ }
+
+ /**
+ * Set WebCore cache size.
+ * @param bytes The cache size in bytes.
+ */
+ public native void setCacheSize(int bytes);
+
+ /**
+ * Store a cookie string associated with a url.
+ * @param url The url to be used as a key for the cookie.
+ * @param docUrl The policy base url used by WebCore.
+ * @param value The cookie string to be stored.
+ */
+ private void setCookies(String url, String docUrl, String value) {
+ if (value.contains("\r") || value.contains("\n")) {
+ // for security reason, filter out '\r' and '\n' from the cookie
+ int size = value.length();
+ StringBuilder buffer = new StringBuilder(size);
+ int i = 0;
+ while (i != -1 && i < size) {
+ int ir = value.indexOf('\r', i);
+ int in = value.indexOf('\n', i);
+ int newi = (ir == -1) ? in : (in == -1 ? ir : (ir < in ? ir
+ : in));
+ if (newi > i) {
+ buffer.append(value.subSequence(i, newi));
+ } else if (newi == -1) {
+ buffer.append(value.subSequence(i, size));
+ break;
+ }
+ i = newi + 1;
+ }
+ value = buffer.toString();
+ }
+ CookieManager.getInstance().setCookie(url, value);
+ }
+
+ /**
+ * Retrieve the cookie string for the given url.
+ * @param url The resource's url.
+ * @return A String representing the cookies for the given resource url.
+ */
+ private String cookies(String url) {
+ return CookieManager.getInstance().getCookie(url);
+ }
+
+ /**
+ * Returns whether cookies are enabled or not.
+ */
+ private boolean cookiesEnabled() {
+ return CookieManager.getInstance().acceptCookie();
+ }
+
+ /**
+ * setSharedTimer
+ * @param timemillis The relative time when the timer should fire
+ */
+ private void setSharedTimer(long timemillis) {
+ if (Config.LOGV) Log.v(LOGTAG, "setSharedTimer " + timemillis);
+
+ if (timemillis <= 0) {
+ // we don't accumulate the sharedTimer unless it is a delayed
+ // request. This way we won't flood the message queue with
+ // WebKit messages. This should improve the browser's
+ // responsiveness to key events.
+ if (mHasInstantTimer) {
+ return;
+ } else {
+ mHasInstantTimer = true;
+ Message msg = obtainMessage(TIMER_MESSAGE);
+ sendMessageDelayed(msg, timemillis);
+ }
+ } else {
+ Message msg = obtainMessage(TIMER_MESSAGE);
+ sendMessageDelayed(msg, timemillis);
+ }
+ }
+
+ /**
+ * Stop the shared timer.
+ */
+ private void stopSharedTimer() {
+ if (Config.LOGV) {
+ Log.v(LOGTAG, "stopSharedTimer removing all timers");
+ }
+ removeMessages(TIMER_MESSAGE);
+ mHasInstantTimer = false;
+ }
+
+ private native void nativeConstructor();
+ private native void nativeFinalize();
+ private native void sharedTimerFired();
+ private native void setDeferringTimers(boolean defer);
+}
diff --git a/core/java/android/webkit/JsPromptResult.java b/core/java/android/webkit/JsPromptResult.java
new file mode 100644
index 0000000..9fcd1bc
--- /dev/null
+++ b/core/java/android/webkit/JsPromptResult.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.webkit;
+
+
+/**
+ * Public class for handling javascript prompt requests. A
+ * JsDialogHandlerInterface implentation will receive a jsPrompt call with a
+ * JsPromptResult parameter. This parameter is used to return a result to
+ * WebView. The client can call cancel() to cancel the dialog or confirm() with
+ * the user's input to confirm the dialog.
+ */
+public class JsPromptResult extends JsResult {
+ // String result of the prompt
+ private String mStringResult;
+
+ /**
+ * Handle a confirmation response from the user.
+ */
+ public void confirm(String result) {
+ mStringResult = result;
+ confirm();
+ }
+
+ /*package*/ JsPromptResult(CallbackProxy proxy) {
+ super(proxy, /* unused */ false);
+ }
+
+ /*package*/ String getStringResult() {
+ return mStringResult;
+ }
+
+ @Override
+ /*package*/ void handleDefault() {
+ mStringResult = null;
+ super.handleDefault();
+ }
+}
diff --git a/core/java/android/webkit/JsResult.java b/core/java/android/webkit/JsResult.java
new file mode 100644
index 0000000..0c86e0a
--- /dev/null
+++ b/core/java/android/webkit/JsResult.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.webkit;
+
+
+public class JsResult {
+ // This prevents a user from interacting with the result before WebCore is
+ // ready to handle it.
+ private boolean mReady;
+ // Tells us if the user tried to confirm or cancel the result before WebCore
+ // is ready.
+ private boolean mTriedToNotifyBeforeReady;
+ // This is a basic result of a confirm or prompt dialog.
+ protected boolean mResult;
+ // This is the caller of the prompt and is the object that is waiting.
+ protected final CallbackProxy mProxy;
+ // This is the default value of the result.
+ private final boolean mDefaultValue;
+
+ /**
+ * Handle the result if the user cancelled the dialog.
+ */
+ public final void cancel() {
+ mResult = false;
+ wakeUp();
+ }
+
+ /**
+ * Handle a confirmation response from the user.
+ */
+ public final void confirm() {
+ mResult = true;
+ wakeUp();
+ }
+
+ /*package*/ JsResult(CallbackProxy proxy, boolean defaultVal) {
+ mProxy = proxy;
+ mDefaultValue = defaultVal;
+ }
+
+ /*package*/ final boolean getResult() {
+ return mResult;
+ }
+
+ /*package*/ final void setReady() {
+ mReady = true;
+ if (mTriedToNotifyBeforeReady) {
+ wakeUp();
+ }
+ }
+
+ /*package*/ void handleDefault() {
+ setReady();
+ mResult = mDefaultValue;
+ wakeUp();
+ }
+
+ /* Wake up the WebCore thread. */
+ protected final void wakeUp() {
+ if (mReady) {
+ synchronized (mProxy) {
+ mProxy.notify();
+ }
+ } else {
+ mTriedToNotifyBeforeReady = true;
+ }
+ }
+}
diff --git a/core/java/android/webkit/LoadListener.java b/core/java/android/webkit/LoadListener.java
new file mode 100644
index 0000000..86947a2
--- /dev/null
+++ b/core/java/android/webkit/LoadListener.java
@@ -0,0 +1,1409 @@
+/*
+ * 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.webkit;
+
+import android.content.Context;
+import android.net.WebAddress;
+import android.net.ParseException;
+import android.net.http.EventHandler;
+import android.net.http.Headers;
+import android.net.http.HttpAuthHeader;
+import android.net.http.RequestHandle;
+import android.net.http.SslCertificate;
+import android.net.http.SslError;
+import android.net.http.SslCertificate;
+
+import android.os.Handler;
+import android.os.Message;
+import android.util.Config;
+import android.util.Log;
+import android.webkit.CacheManager.CacheResult;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Vector;
+import java.util.regex.Pattern;
+import java.util.regex.Matcher;
+
+import org.apache.commons.codec.binary.Base64;
+
+class LoadListener extends Handler implements EventHandler {
+
+ private static final String LOGTAG = "webkit";
+
+ // Messages used internally to communicate state between the
+ // Network thread and the WebCore thread.
+ private static final int MSG_CONTENT_HEADERS = 100;
+ private static final int MSG_CONTENT_DATA = 110;
+ private static final int MSG_CONTENT_FINISHED = 120;
+ private static final int MSG_CONTENT_ERROR = 130;
+ private static final int MSG_LOCATION_CHANGED = 140;
+ private static final int MSG_LOCATION_CHANGED_REQUEST = 150;
+
+ // Standard HTTP status codes in a more representative format
+ private static final int HTTP_OK = 200;
+ private static final int HTTP_MOVED_PERMANENTLY = 301;
+ private static final int HTTP_FOUND = 302;
+ private static final int HTTP_SEE_OTHER = 303;
+ private static final int HTTP_NOT_MODIFIED = 304;
+ private static final int HTTP_TEMPORARY_REDIRECT = 307;
+ private static final int HTTP_AUTH = 401;
+ private static final int HTTP_NOT_FOUND = 404;
+ private static final int HTTP_PROXY_AUTH = 407;
+
+ private static int sNativeLoaderCount;
+
+ private final ByteArrayBuilder mDataBuilder = new ByteArrayBuilder(8192);
+
+ private String mUrl;
+ private WebAddress mUri;
+ private boolean mPermanent;
+ private String mOriginalUrl;
+ private Context mContext;
+ private BrowserFrame mBrowserFrame;
+ private int mNativeLoader;
+ private String mMimeType;
+ private String mEncoding;
+ private String mTransferEncoding;
+ private int mStatusCode;
+ private String mStatusText;
+ public long mContentLength; // Content length of the incoming data
+ private boolean mCancelled; // The request has been cancelled.
+ private boolean mAuthFailed; // indicates that the prev. auth failed
+ private CacheLoader mCacheLoader;
+ private CacheManager.CacheResult mCacheResult;
+ private HttpAuthHeader mAuthHeader;
+ private int mErrorID = OK;
+ private String mErrorDescription;
+ private SslError mSslError;
+ private RequestHandle mRequestHandle;
+
+ // Request data. It is only valid when we are doing a load from the
+ // cache. It is needed if the cache returns a redirect
+ private String mMethod;
+ private Map<String, String> mRequestHeaders;
+ private String mPostData;
+ private boolean mIsHighPriority;
+ // Flag to indicate that this load is synchronous.
+ private boolean mSynchronous;
+ private Vector<Message> mMessageQueue;
+
+ // Does this loader correspond to the main-frame top-level page?
+ private boolean mIsMainPageLoader;
+
+ private Headers mHeaders;
+
+ // =========================================================================
+ // Public functions
+ // =========================================================================
+
+ public static LoadListener getLoadListener(
+ Context context, BrowserFrame frame, String url,
+ int nativeLoader, boolean synchronous, boolean isMainPageLoader) {
+
+ sNativeLoaderCount += 1;
+ return new LoadListener(
+ context, frame, url, nativeLoader, synchronous, isMainPageLoader);
+ }
+
+ public static int getNativeLoaderCount() {
+ return sNativeLoaderCount;
+ }
+
+ LoadListener(Context context, BrowserFrame frame, String url,
+ int nativeLoader, boolean synchronous, boolean isMainPageLoader) {
+ if (Config.LOGV) {
+ Log.v(LOGTAG, "LoadListener constructor url=" + url);
+ }
+ mContext = context;
+ mBrowserFrame = frame;
+ setUrl(url);
+ mNativeLoader = nativeLoader;
+ mMimeType = "";
+ mEncoding = "";
+ mSynchronous = synchronous;
+ if (synchronous) {
+ mMessageQueue = new Vector<Message>();
+ }
+ mIsMainPageLoader = isMainPageLoader;
+ }
+
+ /**
+ * We keep a count of refs to the nativeLoader so we do not create
+ * so many LoadListeners that the GREFs blow up
+ */
+ private void clearNativeLoader() {
+ sNativeLoaderCount -= 1;
+ mNativeLoader = 0;
+ }
+
+ /*
+ * This message handler is to facilitate communication between the network
+ * thread and the browser thread.
+ */
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_CONTENT_HEADERS:
+ /*
+ * This message is sent when the LoadListener has headers
+ * available. The headers are sent onto WebCore to see what we
+ * should do with them.
+ */
+ if (mNativeLoader != 0) {
+ commitHeaders();
+ }
+ break;
+
+ case MSG_CONTENT_DATA:
+ /*
+ * This message is sent when the LoadListener has data available
+ * in it's data buffer. This data buffer could be filled from a
+ * file (this thread) or from http (Network thread).
+ */
+ if (mNativeLoader != 0) {
+ commitLoad();
+ }
+ break;
+
+ case MSG_CONTENT_FINISHED:
+ /*
+ * This message is sent when the LoadListener knows that the
+ * load is finished. This message is not sent in the case of an
+ * error.
+ *
+ */
+ tearDown();
+ break;
+
+ case MSG_CONTENT_ERROR:
+ /*
+ * This message is sent when a load error has occured. The
+ * LoadListener will clean itself up.
+ */
+ notifyError();
+ tearDown();
+ break;
+
+ case MSG_LOCATION_CHANGED:
+ /*
+ * This message is sent from LoadListener.endData to inform the
+ * browser activity that the location of the top level page
+ * changed.
+ */
+ doRedirect();
+ break;
+
+ case MSG_LOCATION_CHANGED_REQUEST:
+ /*
+ * This message is sent from endData on receipt of a 307
+ * Temporary Redirect in response to a POST -- the user must
+ * confirm whether to continue loading. If the user says Yes,
+ * we simply call MSG_LOCATION_CHANGED. If the user says No,
+ * we call MSG_CONTENT_FINISHED.
+ */
+ Message contMsg = obtainMessage(MSG_LOCATION_CHANGED);
+ Message stopMsg = obtainMessage(MSG_CONTENT_FINISHED);
+ //TODO, need to call mCallbackProxy and request UI.
+ break;
+
+ }
+ }
+
+ /**
+ * @return The loader's BrowserFrame.
+ */
+ BrowserFrame getFrame() {
+ return mBrowserFrame;
+ }
+
+ Context getContext() {
+ return mContext;
+ }
+
+ /* package */ boolean isSynchronous() {
+ return mSynchronous;
+ }
+
+ /**
+ * @return True iff the load has been cancelled
+ */
+ public boolean cancelled() {
+ return mCancelled;
+ }
+
+ /**
+ * Parse the headers sent from the server.
+ * @param headers gives up the HeaderGroup
+ * IMPORTANT: as this is called from network thread, can't call native
+ * directly
+ */
+ public void headers(Headers headers) {
+ if (Config.LOGV) Log.v(LOGTAG, "LoadListener.headers");
+ if (mCancelled) return;
+ mHeaders = headers;
+ mMimeType = "";
+ mEncoding = "";
+
+ ArrayList<String> cookies = headers.getSetCookie();
+ for (int i = 0; i < cookies.size(); ++i) {
+ CookieManager.getInstance().setCookie(mUri, cookies.get(i));
+ }
+
+ long contentLength = headers.getContentLength();
+ if (contentLength != Headers.NO_CONTENT_LENGTH) {
+ mContentLength = contentLength;
+ } else {
+ mContentLength = 0;
+ }
+
+ String contentType = headers.getContentType();
+ if (contentType != null) {
+ parseContentTypeHeader(contentType);
+
+ // If we have one of "generic" MIME types, try to deduce
+ // the right MIME type from the file extension (if any):
+ if (mMimeType.equalsIgnoreCase("text/plain") ||
+ mMimeType.equalsIgnoreCase("application/octet-stream")) {
+
+ String newMimeType = guessMimeTypeFromExtension();
+ if (newMimeType != null) {
+ mMimeType = newMimeType;
+ }
+ } else if (mMimeType.equalsIgnoreCase("text/vnd.wap.wml") ||
+ mMimeType.
+ equalsIgnoreCase("application/vnd.wap.xhtml+xml")) {
+ // As we don't support wml, render it as plain text
+ mMimeType = "text/plain";
+ } else {
+ // XXX: Until the servers send us either correct xhtml or
+ // text/html, treat application/xhtml+xml as text/html.
+ if (mMimeType.equalsIgnoreCase("application/xhtml+xml")) {
+ mMimeType = "text/html";
+ }
+ }
+ } else {
+ /* Often when servers respond with 304 Not Modified or a
+ Redirect, then they don't specify a MIMEType. When this
+ occurs, the function below is called. In the case of
+ 304 Not Modified, the cached headers are used rather
+ than the headers that are returned from the server. */
+ guessMimeType();
+ }
+
+ // is it an authentication request?
+ boolean mustAuthenticate = (mStatusCode == HTTP_AUTH ||
+ mStatusCode == HTTP_PROXY_AUTH);
+ // is it a proxy authentication request?
+ boolean isProxyAuthRequest = (mStatusCode == HTTP_PROXY_AUTH);
+ // is this authentication request due to a failed attempt to
+ // authenticate ealier?
+ mAuthFailed = false;
+
+ // if we tried to authenticate ourselves last time
+ if (mAuthHeader != null) {
+ // we failed, if we must to authenticate again now and
+ // we have a proxy-ness match
+ mAuthFailed = (mustAuthenticate &&
+ isProxyAuthRequest == mAuthHeader.isProxy());
+
+ // if we did NOT fail and last authentication request was a
+ // proxy-authentication request
+ if (!mAuthFailed && mAuthHeader.isProxy()) {
+ Network network = Network.getInstance(mContext);
+ // if we have a valid proxy set
+ if (network.isValidProxySet()) {
+ /* The proxy credentials can be read in the WebCore thread
+ */
+ synchronized (network) {
+ // save authentication credentials for pre-emptive proxy
+ // authentication
+ network.setProxyUsername(mAuthHeader.getUsername());
+ network.setProxyPassword(mAuthHeader.getPassword());
+ }
+ }
+ }
+ }
+ // it is only here that we can reset the last mAuthHeader object
+ // (if existed) and start a new one!!!
+ mAuthHeader = null;
+ if (mustAuthenticate) {
+ if (mStatusCode == HTTP_AUTH) {
+ mAuthHeader = parseAuthHeader(
+ headers.getWwwAuthenticate());
+ } else {
+ mAuthHeader = parseAuthHeader(
+ headers.getProxyAuthenticate());
+ // if successfully parsed the header
+ if (mAuthHeader != null) {
+ // mark the auth-header object as a proxy
+ mAuthHeader.setProxy();
+ }
+ }
+ }
+
+ // Only create a cache file if the server has responded positively.
+ if ((mStatusCode == HTTP_OK ||
+ mStatusCode == HTTP_FOUND ||
+ mStatusCode == HTTP_MOVED_PERMANENTLY ||
+ mStatusCode == HTTP_TEMPORARY_REDIRECT) &&
+ mNativeLoader != 0) {
+ // Content arriving from a StreamLoader (eg File, Cache or Data)
+ // will not be cached as they have the header:
+ // cache-control: no-store
+ mCacheResult = CacheManager.createCacheFile(mUrl, mStatusCode,
+ headers, mMimeType, false);
+ if (mCacheResult != null) {
+ mCacheResult.encoding = mEncoding;
+ }
+ }
+ sendMessageInternal(obtainMessage(MSG_CONTENT_HEADERS));
+ }
+
+ /**
+ * @return True iff this loader is in the proxy-authenticate state.
+ */
+ boolean proxyAuthenticate() {
+ if (mAuthHeader != null) {
+ return mAuthHeader.isProxy();
+ }
+
+ return false;
+ }
+
+ /**
+ * Report the status of the response.
+ * TODO: Comments about each parameter.
+ * IMPORTANT: as this is called from network thread, can't call native
+ * directly
+ */
+ public void status(int majorVersion, int minorVersion,
+ int code, /* Status-Code value */ String reasonPhrase) {
+ if (Config.LOGV) {
+ Log.v(LOGTAG, "LoadListener: from: " + mUrl
+ + " major: " + majorVersion
+ + " minor: " + minorVersion
+ + " code: " + code
+ + " reason: " + reasonPhrase);
+ }
+
+ if (mCancelled) return;
+
+ mStatusCode = code;
+ mStatusText = reasonPhrase;
+ mPermanent = false;
+ }
+
+ /**
+ * Implementation of certificate handler for EventHandler.
+ * Called every time a resource is loaded via a secure
+ * connection. In this context, can be called multiple
+ * times if we have redirects
+ * @param certificate The SSL certifcate
+ */
+ public void certificate(SslCertificate certificate) {
+ // if this is the top-most main-frame page loader
+ if (mIsMainPageLoader) {
+ // update the browser frame (ie, the main frame)
+ mBrowserFrame.certificate(certificate);
+ }
+ }
+
+ /**
+ * Implementation of error handler for EventHandler.
+ * Subclasses should call this method to have error fields set.
+ * @param id The error id described by EventHandler.
+ * @param description A string description of the error.
+ * IMPORTANT: as this is called from network thread, can't call native
+ * directly
+ */
+ public void error(int id, String description) {
+ mErrorID = id;
+ mErrorDescription = description;
+ sendMessageInternal(obtainMessage(MSG_CONTENT_ERROR));
+ if (Config.LOGV) {
+ Log.v(LOGTAG, "LoadListener.error url:" +
+ url() + " id:" + id + " description:" + description);
+ }
+ detachRequestHandle();
+ }
+
+ /**
+ * Add data to the internal collection of data. This function is used by
+ * the data: scheme, about: scheme and http/https schemes.
+ * @param data A byte array containing the content.
+ * @param length The length of data.
+ * IMPORTANT: as this is called from network thread, can't call native
+ * directly
+ */
+ public void data(byte[] data, int length) {
+ if (Config.LOGV) {
+ Log.v(LOGTAG, "LoadListener.data(): url: " + url());
+ }
+
+ if (ignoreCallbacks()) {
+ return;
+ }
+
+ // Decode base64 data
+ // Note: It's fine that we only decode base64 here and not in the other
+ // data call because the only caller of the stream version is not
+ // base64 encoded.
+ if ("base64".equalsIgnoreCase(mTransferEncoding)) {
+ if (length < data.length) {
+ byte[] trimmedData = new byte[length];
+ System.arraycopy(data, 0, trimmedData, 0, length);
+ data = trimmedData;
+ }
+ data = Base64.decodeBase64(data);
+ length = data.length;
+ }
+ // Synchronize on mData because commitLoad may write mData to WebCore
+ // and we don't want to replace mData or mDataLength at the same time
+ // as a write.
+ boolean sendMessage = false;
+ synchronized (mDataBuilder) {
+ sendMessage = mDataBuilder.isEmpty();
+ mDataBuilder.append(data, 0, length);
+ }
+ if (sendMessage) {
+ // Send a message whenever data comes in after a write to WebCore
+ sendMessageInternal(obtainMessage(MSG_CONTENT_DATA));
+ }
+ }
+
+ /**
+ * Event handler's endData call. Send a message to the handler notifying
+ * them that the data has finished.
+ * IMPORTANT: as this is called from network thread, can't call native
+ * directly
+ */
+ public void endData() {
+ if (Config.LOGV) {
+ Log.v(LOGTAG, "LoadListener.endData(): url: " + url());
+ }
+
+ if (mCancelled) return;
+
+ switch (mStatusCode) {
+ case HTTP_MOVED_PERMANENTLY:
+ // 301 - permanent redirect
+ mPermanent = true;
+ case HTTP_FOUND:
+ case HTTP_SEE_OTHER:
+ case HTTP_TEMPORARY_REDIRECT:
+ if (mMethod == null && mRequestHandle == null) {
+ Log.e(LOGTAG, "LoadListener.endData(): method is null!");
+ Log.e(LOGTAG, "LoadListener.endData(): url = " + url());
+ }
+ // 301, 302, 303, and 307 - redirect
+ if (mStatusCode == HTTP_TEMPORARY_REDIRECT) {
+ if (mRequestHandle != null &&
+ mRequestHandle.getMethod().equals("POST")) {
+ sendMessageInternal(obtainMessage(
+ MSG_LOCATION_CHANGED_REQUEST));
+ } else if (mMethod != null && mMethod.equals("POST")) {
+ sendMessageInternal(obtainMessage(
+ MSG_LOCATION_CHANGED_REQUEST));
+ } else {
+ sendMessageInternal(obtainMessage(MSG_LOCATION_CHANGED));
+ }
+ } else {
+ sendMessageInternal(obtainMessage(MSG_LOCATION_CHANGED));
+ }
+
+ break;
+
+ case HTTP_AUTH:
+ case HTTP_PROXY_AUTH:
+ if (mAuthHeader != null &&
+ (Network.getInstance(mContext).isValidProxySet() ||
+ !mAuthHeader.isProxy())) {
+ Network.getInstance(mContext).handleAuthRequest(this);
+ } else {
+ final int stringId =
+ com.android.internal.R.string.httpErrorUnsupportedAuthScheme;
+ error(EventHandler.ERROR_UNSUPPORTED_AUTH_SCHEME,
+ getContext().getText(stringId).toString());
+ }
+ break;
+
+ case HTTP_NOT_MODIFIED:
+ // Server could send back NOT_MODIFIED even if we didn't
+ // ask for it, so make sure we have a valid CacheLoader
+ // before calling it.
+ if (mCacheLoader != null) {
+ detachRequestHandle();
+ mCacheLoader.load();
+ if (Config.LOGV) {
+ Log.v(LOGTAG, "LoadListener cache load url=" + url());
+ }
+ break;
+ } // Fall through to default if there is no CacheLoader
+
+ case HTTP_NOT_FOUND:
+ // Not an error, the server can send back content.
+ default:
+ sendMessageInternal(obtainMessage(MSG_CONTENT_FINISHED));
+ detachRequestHandle();
+ break;
+ }
+ }
+
+ /**
+ * Check the cache for the current URL, and load it if it is valid.
+ *
+ * @param headers for the request
+ * @return true if cached response is used.
+ */
+ boolean checkCache(Map<String, String> headers) {
+ // Get the cache file name for the current URL
+ CacheResult result = CacheManager.getCacheFile(url(),
+ headers);
+
+ if (result != null) {
+ CacheLoader cacheLoader =
+ new CacheLoader(this, result);
+
+ // If I got a cachedUrl and the revalidation header was not
+ // added, then the cached content valid, we should use it.
+ if (!headers.containsKey(
+ CacheManager.HEADER_KEY_IFNONEMATCH) &&
+ !headers.containsKey(
+ CacheManager.HEADER_KEY_IFMODIFIEDSINCE)) {
+ if (Config.LOGV) {
+ Log.v(LOGTAG, "FrameLoader: HTTP URL in cache " +
+ "and usable: " + url());
+ }
+ // Load the cached file
+ cacheLoader.load();
+ return true;
+ } else {
+ // The contents of the cache need to be revalidated
+ // so just provide the listener with the cache loader
+ // in the case that the server response positively to
+ // the cached content.
+ setCacheLoader(cacheLoader);
+ }
+ }
+ return false;
+ }
+
+ /**
+ * SSL certificate error callback. Handles SSL error(s) on the way up
+ * to the user.
+ * IMPORTANT: as this is called from network thread, can't call native
+ * directly
+ */
+ public void handleSslErrorRequest(SslError error) {
+ if (Config.LOGV) {
+ Log.v(LOGTAG,
+ "LoadListener.handleSslErrorRequest(): url:" + url() +
+ " primary error: " + error.getPrimaryError() +
+ " certificate: " + error.getCertificate());
+ }
+
+ if (!mCancelled) {
+ mSslError = error;
+ Network.getInstance(mContext).handleSslErrorRequest(this);
+ }
+ }
+
+ /**
+ * @return HTTP authentication realm or null if none.
+ */
+ String realm() {
+ if (mAuthHeader == null) {
+ return null;
+ } else {
+ return mAuthHeader.getRealm();
+ }
+ }
+
+ /**
+ * Returns true iff an HTTP authentication problem has
+ * occured (credentials invalid).
+ */
+ boolean authCredentialsInvalid() {
+ // if it is digest and the nonce is stale, we just
+ // resubmit with a new nonce
+ return (mAuthFailed &&
+ !(mAuthHeader.isDigest() && mAuthHeader.getStale()));
+ }
+
+ /**
+ * @return The last SSL error or null if there is none
+ */
+ SslError sslError() {
+ return mSslError;
+ }
+
+ /**
+ * Handles SSL error(s) on the way down from the user
+ * (the user has already provided their feedback).
+ */
+ void handleSslErrorResponse(boolean proceed) {
+ if (mRequestHandle != null) {
+ mRequestHandle.handleSslErrorResponse(proceed);
+ }
+ }
+
+ /**
+ * Uses user-supplied credentials to restar a request.
+ */
+ void handleAuthResponse(String username, String password) {
+ if (Config.LOGV) {
+ Log.v(LOGTAG, "LoadListener.handleAuthResponse: url: " + mUrl
+ + " username: " + username
+ + " password: " + password);
+ }
+
+ // create and queue an authentication-response
+ if (username != null && password != null) {
+ if (mAuthHeader != null && mRequestHandle != null) {
+ mAuthHeader.setUsername(username);
+ mAuthHeader.setPassword(password);
+
+ int scheme = mAuthHeader.getScheme();
+ if (scheme == HttpAuthHeader.BASIC) {
+ // create a basic response
+ boolean isProxy = mAuthHeader.isProxy();
+
+ mRequestHandle.setupBasicAuthResponse(isProxy,
+ username, password);
+ } else {
+ if (scheme == HttpAuthHeader.DIGEST) {
+ // create a digest response
+ boolean isProxy = mAuthHeader.isProxy();
+
+ String realm = mAuthHeader.getRealm();
+ String nonce = mAuthHeader.getNonce();
+ String qop = mAuthHeader.getQop();
+ String algorithm = mAuthHeader.getAlgorithm();
+ String opaque = mAuthHeader.getOpaque();
+
+ mRequestHandle.setupDigestAuthResponse
+ (isProxy, username, password, realm,
+ nonce, qop, algorithm, opaque);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Set the CacheLoader for the case where we might want to load from cache
+ * @param result
+ */
+ void setCacheLoader(CacheLoader result) {
+ mCacheLoader = result;
+ }
+
+ /**
+ * This is called when a request can be satisfied by the cache, however,
+ * the cache result could be a redirect. In this case we need to issue
+ * the network request.
+ * @param method
+ * @param headers
+ * @param postData
+ * @param isHighPriority
+ */
+ void setRequestData(String method, Map<String, String> headers,
+ String postData, boolean isHighPriority) {
+ mMethod = method;
+ mRequestHeaders = headers;
+ mPostData = postData;
+ mIsHighPriority = isHighPriority;
+ }
+
+ /**
+ * @return The current URL associated with this load.
+ */
+ String url() {
+ return mUrl;
+ }
+
+ /**
+ * @return The current WebAddress associated with this load.
+ */
+ WebAddress getWebAddress() {
+ return mUri;
+ }
+
+ /**
+ * @return URL hostname (current URL).
+ */
+ String host() {
+ if (mUri != null) {
+ return mUri.mHost;
+ }
+
+ return null;
+ }
+
+ /**
+ * @return The original URL associated with this load.
+ */
+ String originalUrl() {
+ if (mOriginalUrl != null) {
+ return mOriginalUrl;
+ } else {
+ return mUrl;
+ }
+ }
+
+ void attachRequestHandle(RequestHandle requestHandle) {
+ if (Config.LOGV) {
+ Log.v(LOGTAG, "LoadListener.attachRequestHandle(): " +
+ "requestHandle: " + requestHandle);
+ }
+ mRequestHandle = requestHandle;
+ }
+
+ void detachRequestHandle() {
+ if (Config.LOGV) {
+ Log.v(LOGTAG, "LoadListener.detachRequestHandle(): " +
+ "requestHandle: " + mRequestHandle);
+ }
+ mRequestHandle = null;
+ }
+
+ /*
+ * This function is called from native WebCore code to
+ * notify this LoadListener that the content it is currently
+ * downloading should be saved to a file and not sent to
+ * WebCore.
+ */
+ void downloadFile() {
+ // Setting the Cache Result to null ensures that this
+ // content is not added to the cache
+ mCacheResult = null;
+
+ // Inform the client that they should download a file
+ mBrowserFrame.getCallbackProxy().onDownloadStart(url(),
+ mBrowserFrame.getUserAgentString(),
+ mHeaders.getContentDisposition(),
+ mMimeType, mContentLength);
+
+ // Cancel the download. We need to stop the http load.
+ // The native loader object will get cleared by the call to
+ // cancel() but will also be cleared on the WebCore side
+ // when this function returns.
+ cancel();
+ }
+
+ /*
+ * This function is called from native WebCore code to
+ * find out if the given URL is in the cache, and if it can
+ * be used. This is just for forward/back navigation to a POST
+ * URL.
+ */
+ static boolean willLoadFromCache(String url) {
+ boolean inCache = CacheManager.getCacheFile(url, null) != null;
+ if (Config.LOGV) {
+ Log.v(LOGTAG, "willLoadFromCache: " + url + " in cache: " +
+ inCache);
+ }
+ return inCache;
+ }
+
+ /*
+ * Reset the cancel flag. This is used when we are resuming a stopped
+ * download. To suspend a download, we cancel it. It can also be cancelled
+ * when it has run out of disk space. In this situation, the download
+ * can be resumed.
+ */
+ void resetCancel() {
+ mCancelled = false;
+ }
+
+ String mimeType() {
+ return mMimeType;
+ }
+
+ /*
+ * Return the size of the content being downloaded. This represents the
+ * full content size, even under the situation where the download has been
+ * resumed after interruption.
+ *
+ * @ return full content size
+ */
+ long contentLength() {
+ return mContentLength;
+ }
+
+ private void commitHeaders() {
+ if (mCancelled) return;
+
+ // do not call webcore if it is redirect. According to the code in
+ // InspectorController::willSendRequest(), the response is only updated
+ // when it is not redirect.
+ if ((mStatusCode >= 301 && mStatusCode <= 303) || mStatusCode == 307) {
+ return;
+ }
+
+ // Commit the headers to WebCore
+ int nativeResponse = createNativeResponse();
+ // The native code deletes the native response object.
+ nativeReceivedResponse(nativeResponse);
+ }
+
+ /**
+ * Create a WebCore response object so that it can be used by
+ * nativeReceivedResponse or nativeRedirectedToUrl
+ * @return native response pointer
+ */
+ private int createNativeResponse() {
+ // The reason we change HTTP_NOT_MODIFIED to HTTP_OK is because we know
+ // that WebCore never sends the if-modified-since header. Our
+ // CacheManager does it for us. If the server responds with a 304, then
+ // we treat it like it was a 200 code and proceed with loading the file
+ // from the cache.
+ int statusCode = mStatusCode == HTTP_NOT_MODIFIED
+ ? HTTP_OK : mStatusCode;
+ // pass content-type content-length and content-encoding
+ int nativeResponse = nativeCreateResponse(mUrl, statusCode, mStatusText,
+ mMimeType, mContentLength, mEncoding,
+ mCacheResult == null ? 0 : mCacheResult.expires / 1000);
+ if (mHeaders != null) {
+ // "content-disposition",
+ String value = mHeaders.getContentDisposition();
+ if (value != null) {
+ nativeSetResponseHeader(nativeResponse,
+ Headers.CONTENT_DISPOSITION, value);
+ }
+
+ // location
+ value = mHeaders.getLocation();
+ if (value != null) {
+ nativeSetResponseHeader(nativeResponse,
+ Headers.LOCATION, value);
+ }
+
+ // refresh (paypal.com are using this)
+ value = mHeaders.getRefresh();
+ if (value != null) {
+ nativeSetResponseHeader(nativeResponse,
+ Headers.REFRESH, value);
+ }
+
+ // Content-Type
+ value = mHeaders.getContentType();
+ if (value != null) {
+ nativeSetResponseHeader(nativeResponse,
+ Headers.CONTENT_TYPE, value);
+ }
+ }
+ return nativeResponse;
+ }
+
+ /**
+ * Commit the load. It should be ok to call repeatedly but only before
+ * tearDown is called.
+ */
+ private void commitLoad() {
+ if (mCancelled) return;
+
+ // Give the data to WebKit now
+ PerfChecker checker = new PerfChecker();
+ ByteArrayBuilder.Chunk c;
+ while (true) {
+ c = mDataBuilder.getFirstChunk();
+ if (c == null) break;
+
+ if (c.mLength != 0) {
+ if (mCacheResult != null) {
+ try {
+ mCacheResult.outStream.write(c.mArray, 0, c.mLength);
+ } catch (IOException e) {
+ mCacheResult = null;
+ }
+ }
+ nativeAddData(c.mArray, c.mLength);
+ }
+ mDataBuilder.releaseChunk(c);
+ checker.responseAlert("res nativeAddData");
+ }
+ }
+
+ /**
+ * Tear down the load. Subclasses should clean up any mess because of
+ * cancellation or errors during the load.
+ */
+ void tearDown() {
+ if (mCacheResult != null) {
+ if (getErrorID() == OK) {
+ CacheManager.saveCacheFile(mUrl, mCacheResult);
+ }
+
+ // we need to reset mCacheResult to be null
+ // resource loader's tearDown will call into WebCore's
+ // nativeFinish, which in turn calls loader.cancel().
+ // If we don't reset mCacheFile, the file will be deleted.
+ mCacheResult = null;
+ }
+ if (mNativeLoader != 0) {
+ PerfChecker checker = new PerfChecker();
+ nativeFinished();
+ checker.responseAlert("res nativeFinished");
+ clearNativeLoader();
+ }
+ }
+
+ /**
+ * Helper for getting the error ID.
+ * @return errorID.
+ */
+ private int getErrorID() {
+ return mErrorID;
+ }
+
+ /**
+ * Return the error description.
+ * @return errorDescription.
+ */
+ private String getErrorDescription() {
+ return mErrorDescription;
+ }
+
+ /**
+ * Notify the loader we encountered an error.
+ */
+ void notifyError() {
+ if (mNativeLoader != 0) {
+ String description = getErrorDescription();
+ if (description == null) description = "";
+ nativeError(getErrorID(), description, url());
+ clearNativeLoader();
+ }
+ }
+
+ /**
+ * Cancel a request.
+ * FIXME: This will only work if the request has yet to be handled. This
+ * is in no way guarenteed if requests are served in a separate thread.
+ * It also causes major problems if cancel is called during an
+ * EventHandler's method call.
+ */
+ public void cancel() {
+ if (Config.LOGV) {
+ if (mRequestHandle == null) {
+ Log.v(LOGTAG, "LoadListener.cancel(): no requestHandle");
+ } else {
+ Log.v(LOGTAG, "LoadListener.cancel()");
+ }
+ }
+ if (mRequestHandle != null) {
+ mRequestHandle.cancel();
+ mRequestHandle = null;
+ }
+
+ mCacheResult = null;
+ mCancelled = true;
+
+ clearNativeLoader();
+ }
+
+ /*
+ * Perform the actual redirection. This involves setting up the new URL,
+ * informing WebCore and then telling the Network to start loading again.
+ */
+ private void doRedirect() {
+ // as cancel() can cancel the load before doRedirect() is
+ // called through handleMessage, needs to check to see if we
+ // are canceled before proceed
+ if (mCancelled) {
+ return;
+ }
+
+ String redirectTo = mHeaders.getLocation();
+ if (redirectTo != null) {
+ int nativeResponse = createNativeResponse();
+ redirectTo =
+ nativeRedirectedToUrl(mUrl, redirectTo, nativeResponse);
+ // nativeRedirectedToUrl() may call cancel(), e.g. when redirect
+ // from a https site to a http site, check mCancelled again
+ if (mCancelled) {
+ return;
+ }
+ if (redirectTo == null) {
+ Log.d(LOGTAG, "Redirection failed for "
+ + mHeaders.getLocation());
+ cancel();
+ return;
+ } else if (!URLUtil.isNetworkUrl(redirectTo)) {
+ cancel();
+ final String text = mContext
+ .getString(com.android.internal.R.string.open_permission_deny)
+ + "\n" + redirectTo;
+ nativeAddData(text.getBytes(), text.length());
+ nativeFinished();
+ clearNativeLoader();
+ return;
+ }
+
+ if (mOriginalUrl == null) {
+ mOriginalUrl = mUrl;
+ }
+
+ // Cache the redirect response
+ if (mCacheResult != null) {
+ if (getErrorID() == OK) {
+ CacheManager.saveCacheFile(mUrl, mCacheResult);
+ }
+ mCacheResult = null;
+ }
+
+ setUrl(redirectTo);
+
+ // Redirect may be in the cache
+ if (mRequestHeaders == null) {
+ mRequestHeaders = new HashMap<String, String>();
+ }
+ if (!checkCache(mRequestHeaders)) {
+ // mRequestHandle can be null when the request was satisfied
+ // by the cache, and the cache returned a redirect
+ if (mRequestHandle != null) {
+ mRequestHandle.setupRedirect(redirectTo, mStatusCode,
+ mRequestHeaders);
+ } else {
+ String method = mMethod;
+
+ if (method == null) {
+ return;
+ }
+
+ Network network = Network.getInstance(getContext());
+ network.requestURL(method, mRequestHeaders,
+ mPostData, this, mIsHighPriority);
+ }
+ }
+ } else {
+ cancel();
+ }
+
+ if (Config.LOGV) {
+ Log.v(LOGTAG, "LoadListener.onRedirect(): redirect to: " +
+ redirectTo);
+ }
+ }
+
+ /**
+ * Parses the content-type header.
+ */
+ private static final Pattern CONTENT_TYPE_PATTERN =
+ Pattern.compile("^([a-zA-Z\\*]+/[\\w\\+\\*-]+[\\.[\\w\\+-]+]*)$");
+
+ private void parseContentTypeHeader(String contentType) {
+ if (Config.LOGV) {
+ Log.v(LOGTAG, "LoadListener.parseContentTypeHeader: " +
+ "contentType: " + contentType);
+ }
+
+ if (contentType != null) {
+ int i = contentType.indexOf(';');
+ if (i >= 0) {
+ mMimeType = contentType.substring(0, i);
+
+ int j = contentType.indexOf('=', i);
+ if (j > 0) {
+ i = contentType.indexOf(';', j);
+ if (i < j) {
+ i = contentType.length();
+ }
+ mEncoding = contentType.substring(j + 1, i);
+ } else {
+ mEncoding = contentType.substring(i + 1);
+ }
+ // Trim excess whitespace.
+ mEncoding = mEncoding.trim();
+
+ if (i < contentType.length() - 1) {
+ // for data: uri the mimeType and encoding have
+ // the form image/jpeg;base64 or text/plain;charset=utf-8
+ // or text/html;charset=utf-8;base64
+ mTransferEncoding = contentType.substring(i + 1).trim();
+ }
+ } else {
+ mMimeType = contentType;
+ }
+
+ // Trim leading and trailing whitespace
+ mMimeType = mMimeType.trim();
+
+ try {
+ Matcher m = CONTENT_TYPE_PATTERN.matcher(mMimeType);
+ if (m.find()) {
+ mMimeType = m.group(1);
+ } else {
+ guessMimeType();
+ }
+ } catch (IllegalStateException ex) {
+ guessMimeType();
+ }
+ }
+ }
+
+ /**
+ * @return The HTTP-authentication object or null if there
+ * is no supported scheme in the header.
+ * If there are several valid schemes present, we pick the
+ * strongest one. If there are several schemes of the same
+ * strength, we pick the one that comes first.
+ */
+ private HttpAuthHeader parseAuthHeader(String header) {
+ if (header != null) {
+ int posMax = 256;
+ int posLen = 0;
+ int[] pos = new int [posMax];
+
+ int headerLen = header.length();
+ if (headerLen > 0) {
+ // first, we find all unquoted instances of 'Basic' and 'Digest'
+ boolean quoted = false;
+ for (int i = 0; i < headerLen && posLen < posMax; ++i) {
+ if (header.charAt(i) == '\"') {
+ quoted = !quoted;
+ } else {
+ if (!quoted) {
+ if (header.startsWith(
+ HttpAuthHeader.BASIC_TOKEN, i)) {
+ pos[posLen++] = i;
+ continue;
+ }
+
+ if (header.startsWith(
+ HttpAuthHeader.DIGEST_TOKEN, i)) {
+ pos[posLen++] = i;
+ continue;
+ }
+ }
+ }
+ }
+ }
+
+ if (posLen > 0) {
+ // consider all digest schemes first (if any)
+ for (int i = 0; i < posLen; i++) {
+ if (header.startsWith(HttpAuthHeader.DIGEST_TOKEN,
+ pos[i])) {
+ String sub = header.substring(pos[i],
+ (i + 1 < posLen ? pos[i + 1] : headerLen));
+
+ HttpAuthHeader rval = new HttpAuthHeader(sub);
+ if (rval.isSupportedScheme()) {
+ // take the first match
+ return rval;
+ }
+ }
+ }
+
+ // ...then consider all basic schemes (if any)
+ for (int i = 0; i < posLen; i++) {
+ if (header.startsWith(HttpAuthHeader.BASIC_TOKEN, pos[i])) {
+ String sub = header.substring(pos[i],
+ (i + 1 < posLen ? pos[i + 1] : headerLen));
+
+ HttpAuthHeader rval = new HttpAuthHeader(sub);
+ if (rval.isSupportedScheme()) {
+ // take the first match
+ return rval;
+ }
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * If the content is a redirect or not modified we should not send
+ * any data into WebCore as that will cause it create a document with
+ * the data, then when we try to provide the real content, it will assert.
+ *
+ * @return True iff the callback should be ignored.
+ */
+ private boolean ignoreCallbacks() {
+ return (mCancelled || mAuthHeader != null ||
+ (mStatusCode > 300 && mStatusCode < 400));
+ }
+
+ /**
+ * Sets the current URL associated with this load.
+ */
+ void setUrl(String url) {
+ if (url != null) {
+ mUrl = URLUtil.stripAnchor(url);
+ mUri = null;
+ if (URLUtil.isNetworkUrl(mUrl)) {
+ try {
+ mUri = new WebAddress(mUrl);
+ } catch (ParseException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+ /**
+ * Guesses MIME type if one was not specified. Defaults to 'text/html'. In
+ * addition, tries to guess the MIME type based on the extension.
+ *
+ */
+ private void guessMimeType() {
+ // Data urls must have a valid mime type or a blank string for the mime
+ // type (implying text/plain).
+ if (URLUtil.isDataUrl(mUrl) && mMimeType.length() != 0) {
+ cancel();
+ final String text = mContext.getString(
+ com.android.internal.R.string.httpErrorBadUrl);
+ error(EventHandler.ERROR_BAD_URL, text);
+ } else {
+ // Note: This is ok because this is used only for the main content
+ // of frames. If no content-type was specified, it is fine to
+ // default to text/html.
+ mMimeType = "text/html";
+ String newMimeType = guessMimeTypeFromExtension();
+ if (newMimeType != null) {
+ mMimeType = newMimeType;
+ }
+ }
+ }
+
+ /**
+ * guess MIME type based on the file extension.
+ */
+ private String guessMimeTypeFromExtension() {
+ // PENDING: need to normalize url
+ if (Config.LOGV) {
+ Log.v(LOGTAG, "guessMimeTypeFromExtension: mURL = " + mUrl);
+ }
+
+ String mimeType =
+ MimeTypeMap.getSingleton().getMimeTypeFromExtension(
+ MimeTypeMap.getFileExtensionFromUrl(mUrl));
+
+ if (mimeType != null) {
+ // XXX: Until the servers send us either correct xhtml or
+ // text/html, treat application/xhtml+xml as text/html.
+ if (mimeType.equals("application/xhtml+xml")) {
+ mimeType = "text/html";
+ }
+ }
+
+ return mimeType;
+ }
+
+ /**
+ * Either send a message to ourselves or queue the message if this is a
+ * synchronous load.
+ */
+ private void sendMessageInternal(Message msg) {
+ if (mSynchronous) {
+ mMessageQueue.add(msg);
+ } else {
+ sendMessage(msg);
+ }
+ }
+
+ /**
+ * Cycle through our messages for synchronous loads.
+ */
+ /* package */ void loadSynchronousMessages() {
+ if (Config.DEBUG && !mSynchronous) {
+ throw new AssertionError();
+ }
+ // Note: this can be called twice if it is a synchronous network load,
+ // and there is a cache, but it needs to go to network to validate. If
+ // validation succeed, the CacheLoader is used so this is first called
+ // from http thread. Then it is called again from WebViewCore thread
+ // after the load is completed. So make sure the queue is cleared but
+ // don't set it to null.
+ for (int size = mMessageQueue.size(); size > 0; size--) {
+ handleMessage(mMessageQueue.remove(0));
+ }
+ }
+
+ //=========================================================================
+ // native functions
+ //=========================================================================
+
+ /**
+ * Create a new native response object.
+ * @param url The url of the resource.
+ * @param statusCode The HTTP status code.
+ * @param statusText The HTTP status text.
+ * @param mimeType HTTP content-type.
+ * @param expectedLength An estimate of the content length or the length
+ * given by the server.
+ * @param encoding HTTP encoding.
+ * @param expireTime HTTP expires converted to seconds since the epoch.
+ * @return The native response pointer.
+ */
+ private native int nativeCreateResponse(String url, int statusCode,
+ String statusText, String mimeType, long expectedLength,
+ String encoding, long expireTime);
+
+ /**
+ * Add a response header to the native object.
+ * @param nativeResponse The native pointer.
+ * @param key String key.
+ * @param val String value.
+ */
+ private native void nativeSetResponseHeader(int nativeResponse, String key,
+ String val);
+
+ /**
+ * Dispatch the response.
+ * @param nativeResponse The native pointer.
+ */
+ private native void nativeReceivedResponse(int nativeResponse);
+
+ /**
+ * Add data to the loader.
+ * @param data Byte array of data.
+ * @param length Number of objects in data.
+ */
+ private native void nativeAddData(byte[] data, int length);
+
+ /**
+ * Tell the loader it has finished.
+ */
+ private native void nativeFinished();
+
+ /**
+ * tell the loader to redirect
+ * @param baseUrl The base url.
+ * @param redirectTo The url to redirect to.
+ * @param nativeResponse The native pointer.
+ * @return The new url that the resource redirected to.
+ */
+ private native String nativeRedirectedToUrl(String baseUrl,
+ String redirectTo, int nativeResponse);
+
+ /**
+ * Tell the loader there is error
+ * @param id
+ * @param desc
+ * @param failingUrl The url that failed.
+ */
+ private native void nativeError(int id, String desc, String failingUrl);
+
+}
diff --git a/core/java/android/webkit/MimeTypeMap.java b/core/java/android/webkit/MimeTypeMap.java
new file mode 100644
index 0000000..2700aa5
--- /dev/null
+++ b/core/java/android/webkit/MimeTypeMap.java
@@ -0,0 +1,499 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.webkit;
+
+import java.util.HashMap;
+import java.util.regex.Pattern;
+
+/**
+ * Two-way map that maps MIME-types to file extensions and vice versa.
+ */
+public /* package */ class MimeTypeMap {
+
+ /**
+ * Singleton MIME-type map instance:
+ */
+ private static MimeTypeMap sMimeTypeMap;
+
+ /**
+ * MIME-type to file extension mapping:
+ */
+ private HashMap<String, String> mMimeTypeToExtensionMap;
+
+ /**
+ * File extension to MIME type mapping:
+ */
+ private HashMap<String, String> mExtensionToMimeTypeMap;
+
+
+ /**
+ * Creates a new MIME-type map.
+ */
+ private MimeTypeMap() {
+ mMimeTypeToExtensionMap = new HashMap<String, String>();
+ mExtensionToMimeTypeMap = new HashMap<String, String>();
+ }
+
+ /**
+ * Returns the file extension or an empty string iff there is no
+ * extension.
+ */
+ public static String getFileExtensionFromUrl(String url) {
+ if (url != null && url.length() > 0) {
+ int query = url.lastIndexOf('?');
+ if (query > 0) {
+ url = url.substring(0, query);
+ }
+ int filenamePos = url.lastIndexOf('/');
+ String filename =
+ 0 <= filenamePos ? url.substring(filenamePos + 1) : url;
+
+ // if the filename contains special characters, we don't
+ // consider it valid for our matching purposes:
+ if (filename.length() > 0 &&
+ Pattern.matches("[a-zA-Z_0-9\\.\\-]+", filename)) {
+ int dotPos = filename.lastIndexOf('.');
+ if (0 <= dotPos) {
+ return filename.substring(dotPos + 1);
+ }
+ }
+ }
+
+ return "";
+ }
+
+ /**
+ * Load an entry into the map. This does not check if the item already
+ * exists, it trusts the caller!
+ */
+ private void loadEntry(String mimeType, String extension,
+ boolean textType) {
+ //
+ // if we have an existing x --> y mapping, we do not want to
+ // override it with another mapping x --> ?
+ // this is mostly because of the way the mime-type map below
+ // is constructed (if a mime type maps to several extensions
+ // the first extension is considered the most popular and is
+ // added first; we do not want to overwrite it later).
+ //
+ if (!mMimeTypeToExtensionMap.containsKey(mimeType)) {
+ mMimeTypeToExtensionMap.put(mimeType, extension);
+ }
+
+ //
+ // here, we don't want to map extensions to text MIME types;
+ // otherwise, we will start replacing generic text/plain and
+ // text/html with text MIME types that our platform does not
+ // understand.
+ //
+ if (!textType) {
+ mExtensionToMimeTypeMap.put(extension, mimeType);
+ }
+ }
+
+ /**
+ * @return True iff there is a mimeType entry in the map.
+ */
+ public boolean hasMimeType(String mimeType) {
+ if (mimeType != null && mimeType.length() > 0) {
+ return mMimeTypeToExtensionMap.containsKey(mimeType);
+ }
+
+ return false;
+ }
+
+ /**
+ * @return The extension for the MIME type or null iff there is none.
+ */
+ public String getMimeTypeFromExtension(String extension) {
+ if (extension != null && extension.length() > 0) {
+ return mExtensionToMimeTypeMap.get(extension);
+ }
+
+ return null;
+ }
+
+ /**
+ * @return True iff there is an extension entry in the map.
+ */
+ public boolean hasExtension(String extension) {
+ if (extension != null && extension.length() > 0) {
+ return mExtensionToMimeTypeMap.containsKey(extension);
+ }
+
+ return false;
+ }
+
+ /**
+ * @return The MIME type for the extension or null iff there is none.
+ */
+ public String getExtensionFromMimeType(String mimeType) {
+ if (mimeType != null && mimeType.length() > 0) {
+ return mMimeTypeToExtensionMap.get(mimeType);
+ }
+
+ return null;
+ }
+
+ /**
+ * @return The singleton instance of the MIME-type map.
+ */
+ public static MimeTypeMap getSingleton() {
+ if (sMimeTypeMap == null) {
+ sMimeTypeMap = new MimeTypeMap();
+
+ // The following table is based on /etc/mime.types data minus
+ // chemical/* MIME types and MIME types that don't map to any
+ // file extensions. We also exclude top-level domain names to
+ // deal with cases like:
+ //
+ // mail.google.com/a/google.com
+ //
+ // and "active" MIME types (due to potential security issues).
+ //
+ // Also, notice that not all data from this table is actually
+ // added (see loadEntry method for more details).
+
+ sMimeTypeMap.loadEntry("application/andrew-inset", "ez", false);
+ sMimeTypeMap.loadEntry("application/dsptype", "tsp", false);
+ sMimeTypeMap.loadEntry("application/futuresplash", "spl", false);
+ sMimeTypeMap.loadEntry("application/hta", "hta", false);
+ sMimeTypeMap.loadEntry("application/mac-binhex40", "hqx", false);
+ sMimeTypeMap.loadEntry("application/mac-compactpro", "cpt", false);
+ sMimeTypeMap.loadEntry("application/mathematica", "nb", false);
+ sMimeTypeMap.loadEntry("application/msaccess", "mdb", false);
+ sMimeTypeMap.loadEntry("application/oda", "oda", false);
+ sMimeTypeMap.loadEntry("application/ogg", "ogg", false);
+ sMimeTypeMap.loadEntry("application/pdf", "pdf", false);
+ sMimeTypeMap.loadEntry("application/pgp-keys", "key", false);
+ sMimeTypeMap.loadEntry("application/pgp-signature", "pgp", false);
+ sMimeTypeMap.loadEntry("application/pics-rules", "prf", false);
+ sMimeTypeMap.loadEntry("application/rar", "rar", false);
+ sMimeTypeMap.loadEntry("application/rdf+xml", "rdf", false);
+ sMimeTypeMap.loadEntry("application/rss+xml", "rss", false);
+ sMimeTypeMap.loadEntry("application/zip", "zip", false);
+ sMimeTypeMap.loadEntry("application/vnd.android.package-archive",
+ "apk", false);
+ sMimeTypeMap.loadEntry("application/vnd.cinderella", "cdy", false);
+ sMimeTypeMap.loadEntry("application/vnd.ms-pki.stl", "stl", false);
+ sMimeTypeMap.loadEntry(
+ "application/vnd.oasis.opendocument.database", "odb",
+ false);
+ sMimeTypeMap.loadEntry(
+ "application/vnd.oasis.opendocument.formula", "odf",
+ false);
+ sMimeTypeMap.loadEntry(
+ "application/vnd.oasis.opendocument.graphics", "odg",
+ false);
+ sMimeTypeMap.loadEntry(
+ "application/vnd.oasis.opendocument.graphics-template",
+ "otg", false);
+ sMimeTypeMap.loadEntry(
+ "application/vnd.oasis.opendocument.image", "odi", false);
+ sMimeTypeMap.loadEntry(
+ "application/vnd.oasis.opendocument.spreadsheet", "ods",
+ false);
+ sMimeTypeMap.loadEntry(
+ "application/vnd.oasis.opendocument.spreadsheet-template",
+ "ots", false);
+ sMimeTypeMap.loadEntry(
+ "application/vnd.oasis.opendocument.text", "odt", false);
+ sMimeTypeMap.loadEntry(
+ "application/vnd.oasis.opendocument.text-master", "odm",
+ false);
+ sMimeTypeMap.loadEntry(
+ "application/vnd.oasis.opendocument.text-template", "ott",
+ false);
+ sMimeTypeMap.loadEntry(
+ "application/vnd.oasis.opendocument.text-web", "oth",
+ false);
+ sMimeTypeMap.loadEntry("application/vnd.rim.cod", "cod", false);
+ sMimeTypeMap.loadEntry("application/vnd.smaf", "mmf", false);
+ sMimeTypeMap.loadEntry("application/vnd.stardivision.calc", "sdc",
+ false);
+ sMimeTypeMap.loadEntry("application/vnd.stardivision.draw", "sda",
+ false);
+ sMimeTypeMap.loadEntry(
+ "application/vnd.stardivision.impress", "sdd", false);
+ sMimeTypeMap.loadEntry(
+ "application/vnd.stardivision.impress", "sdp", false);
+ sMimeTypeMap.loadEntry("application/vnd.stardivision.math", "smf",
+ false);
+ sMimeTypeMap.loadEntry("application/vnd.stardivision.writer", "sdw",
+ false);
+ sMimeTypeMap.loadEntry("application/vnd.stardivision.writer", "vor",
+ false);
+ sMimeTypeMap.loadEntry(
+ "application/vnd.stardivision.writer-global", "sgl", false);
+ sMimeTypeMap.loadEntry("application/vnd.sun.xml.calc", "sxc",
+ false);
+ sMimeTypeMap.loadEntry(
+ "application/vnd.sun.xml.calc.template", "stc", false);
+ sMimeTypeMap.loadEntry("application/vnd.sun.xml.draw", "sxd",
+ false);
+ sMimeTypeMap.loadEntry(
+ "application/vnd.sun.xml.draw.template", "std", false);
+ sMimeTypeMap.loadEntry("application/vnd.sun.xml.impress", "sxi",
+ false);
+ sMimeTypeMap.loadEntry(
+ "application/vnd.sun.xml.impress.template", "sti", false);
+ sMimeTypeMap.loadEntry("application/vnd.sun.xml.math", "sxm",
+ false);
+ sMimeTypeMap.loadEntry("application/vnd.sun.xml.writer", "sxw",
+ false);
+ sMimeTypeMap.loadEntry(
+ "application/vnd.sun.xml.writer.global", "sxg", false);
+ sMimeTypeMap.loadEntry(
+ "application/vnd.sun.xml.writer.template", "stw", false);
+ sMimeTypeMap.loadEntry("application/vnd.visio", "vsd", false);
+ sMimeTypeMap.loadEntry("application/x-abiword", "abw", false);
+ sMimeTypeMap.loadEntry("application/x-apple-diskimage", "dmg",
+ false);
+ sMimeTypeMap.loadEntry("application/x-bcpio", "bcpio", false);
+ sMimeTypeMap.loadEntry("application/x-bittorrent", "torrent",
+ false);
+ sMimeTypeMap.loadEntry("application/x-cdf", "cdf", false);
+ sMimeTypeMap.loadEntry("application/x-cdlink", "vcd", false);
+ sMimeTypeMap.loadEntry("application/x-chess-pgn", "pgn", false);
+ sMimeTypeMap.loadEntry("application/x-cpio", "cpio", false);
+ sMimeTypeMap.loadEntry("application/x-debian-package", "deb",
+ false);
+ sMimeTypeMap.loadEntry("application/x-debian-package", "udeb",
+ false);
+ sMimeTypeMap.loadEntry("application/x-director", "dcr", false);
+ sMimeTypeMap.loadEntry("application/x-director", "dir", false);
+ sMimeTypeMap.loadEntry("application/x-director", "dxr", false);
+ sMimeTypeMap.loadEntry("application/x-dms", "dms", false);
+ sMimeTypeMap.loadEntry("application/x-doom", "wad", false);
+ sMimeTypeMap.loadEntry("application/x-dvi", "dvi", false);
+ sMimeTypeMap.loadEntry("application/x-flac", "flac", false);
+ sMimeTypeMap.loadEntry("application/x-font", "pfa", false);
+ sMimeTypeMap.loadEntry("application/x-font", "pfb", false);
+ sMimeTypeMap.loadEntry("application/x-font", "gsf", false);
+ sMimeTypeMap.loadEntry("application/x-font", "pcf", false);
+ sMimeTypeMap.loadEntry("application/x-font", "pcf.Z", false);
+ sMimeTypeMap.loadEntry("application/x-freemind", "mm", false);
+ sMimeTypeMap.loadEntry("application/x-futuresplash", "spl", false);
+ sMimeTypeMap.loadEntry("application/x-gnumeric", "gnumeric", false);
+ sMimeTypeMap.loadEntry("application/x-go-sgf", "sgf", false);
+ sMimeTypeMap.loadEntry("application/x-graphing-calculator", "gcf",
+ false);
+ sMimeTypeMap.loadEntry("application/x-gtar", "gtar", false);
+ sMimeTypeMap.loadEntry("application/x-gtar", "tgz", false);
+ sMimeTypeMap.loadEntry("application/x-gtar", "taz", false);
+ sMimeTypeMap.loadEntry("application/x-hdf", "hdf", false);
+ sMimeTypeMap.loadEntry("application/x-ica", "ica", false);
+ sMimeTypeMap.loadEntry("application/x-internet-signup", "ins",
+ false);
+ sMimeTypeMap.loadEntry("application/x-internet-signup", "isp",
+ false);
+ sMimeTypeMap.loadEntry("application/x-iphone", "iii", false);
+ sMimeTypeMap.loadEntry("application/x-iso9660-image", "iso", false);
+ sMimeTypeMap.loadEntry("application/x-jmol", "jmz", false);
+ sMimeTypeMap.loadEntry("application/x-kchart", "chrt", false);
+ sMimeTypeMap.loadEntry("application/x-killustrator", "kil", false);
+ sMimeTypeMap.loadEntry("application/x-koan", "skp", false);
+ sMimeTypeMap.loadEntry("application/x-koan", "skd", false);
+ sMimeTypeMap.loadEntry("application/x-koan", "skt", false);
+ sMimeTypeMap.loadEntry("application/x-koan", "skm", false);
+ sMimeTypeMap.loadEntry("application/x-kpresenter", "kpr", false);
+ sMimeTypeMap.loadEntry("application/x-kpresenter", "kpt", false);
+ sMimeTypeMap.loadEntry("application/x-kspread", "ksp", false);
+ sMimeTypeMap.loadEntry("application/x-kword", "kwd", false);
+ sMimeTypeMap.loadEntry("application/x-kword", "kwt", false);
+ sMimeTypeMap.loadEntry("application/x-latex", "latex", false);
+ sMimeTypeMap.loadEntry("application/x-lha", "lha", false);
+ sMimeTypeMap.loadEntry("application/x-lzh", "lzh", false);
+ sMimeTypeMap.loadEntry("application/x-lzx", "lzx", false);
+ sMimeTypeMap.loadEntry("application/x-maker", "frm", false);
+ sMimeTypeMap.loadEntry("application/x-maker", "maker", false);
+ sMimeTypeMap.loadEntry("application/x-maker", "frame", false);
+ sMimeTypeMap.loadEntry("application/x-maker", "fb", false);
+ sMimeTypeMap.loadEntry("application/x-maker", "book", false);
+ sMimeTypeMap.loadEntry("application/x-maker", "fbdoc", false);
+ sMimeTypeMap.loadEntry("application/x-mif", "mif", false);
+ sMimeTypeMap.loadEntry("application/x-ms-wmd", "wmd", false);
+ sMimeTypeMap.loadEntry("application/x-ms-wmz", "wmz", false);
+ sMimeTypeMap.loadEntry("application/x-msi", "msi", false);
+ sMimeTypeMap.loadEntry("application/x-ns-proxy-autoconfig", "pac",
+ false);
+ sMimeTypeMap.loadEntry("application/x-nwc", "nwc", false);
+ sMimeTypeMap.loadEntry("application/x-object", "o", false);
+ sMimeTypeMap.loadEntry("application/x-oz-application", "oza",
+ false);
+ sMimeTypeMap.loadEntry("application/x-pkcs7-certreqresp", "p7r",
+ false);
+ sMimeTypeMap.loadEntry("application/x-pkcs7-crl", "crl", false);
+ sMimeTypeMap.loadEntry("application/x-quicktimeplayer", "qtl",
+ false);
+ sMimeTypeMap.loadEntry("application/x-shar", "shar", false);
+ sMimeTypeMap.loadEntry("application/x-stuffit", "sit", false);
+ sMimeTypeMap.loadEntry("application/x-sv4cpio", "sv4cpio", false);
+ sMimeTypeMap.loadEntry("application/x-sv4crc", "sv4crc", false);
+ sMimeTypeMap.loadEntry("application/x-tar", "tar", false);
+ sMimeTypeMap.loadEntry("application/x-texinfo", "texinfo", false);
+ sMimeTypeMap.loadEntry("application/x-texinfo", "texi", false);
+ sMimeTypeMap.loadEntry("application/x-troff", "t", false);
+ sMimeTypeMap.loadEntry("application/x-troff", "roff", false);
+ sMimeTypeMap.loadEntry("application/x-troff-man", "man", false);
+ sMimeTypeMap.loadEntry("application/x-ustar", "ustar", false);
+ sMimeTypeMap.loadEntry("application/x-wais-source", "src", false);
+ sMimeTypeMap.loadEntry("application/x-wingz", "wz", false);
+ sMimeTypeMap.loadEntry(
+ "application/x-webarchive", "webarchive", false); // added
+ sMimeTypeMap.loadEntry("application/x-x509-ca-cert", "crt", false);
+ sMimeTypeMap.loadEntry("application/x-xcf", "xcf", false);
+ sMimeTypeMap.loadEntry("application/x-xfig", "fig", false);
+ sMimeTypeMap.loadEntry("audio/basic", "snd", false);
+ sMimeTypeMap.loadEntry("audio/midi", "mid", false);
+ sMimeTypeMap.loadEntry("audio/midi", "midi", false);
+ sMimeTypeMap.loadEntry("audio/midi", "kar", false);
+ sMimeTypeMap.loadEntry("audio/mpeg", "mpga", false);
+ sMimeTypeMap.loadEntry("audio/mpeg", "mpega", false);
+ sMimeTypeMap.loadEntry("audio/mpeg", "mp2", false);
+ sMimeTypeMap.loadEntry("audio/mpeg", "mp3", false);
+ sMimeTypeMap.loadEntry("audio/mpeg", "m4a", false);
+ sMimeTypeMap.loadEntry("audio/mpegurl", "m3u", false);
+ sMimeTypeMap.loadEntry("audio/prs.sid", "sid", false);
+ sMimeTypeMap.loadEntry("audio/x-aiff", "aif", false);
+ sMimeTypeMap.loadEntry("audio/x-aiff", "aiff", false);
+ sMimeTypeMap.loadEntry("audio/x-aiff", "aifc", false);
+ sMimeTypeMap.loadEntry("audio/x-gsm", "gsm", false);
+ sMimeTypeMap.loadEntry("audio/x-mpegurl", "m3u", false);
+ sMimeTypeMap.loadEntry("audio/x-ms-wma", "wma", false);
+ sMimeTypeMap.loadEntry("audio/x-ms-wax", "wax", false);
+ sMimeTypeMap.loadEntry("audio/x-pn-realaudio", "ra", false);
+ sMimeTypeMap.loadEntry("audio/x-pn-realaudio", "rm", false);
+ sMimeTypeMap.loadEntry("audio/x-pn-realaudio", "ram", false);
+ sMimeTypeMap.loadEntry("audio/x-realaudio", "ra", false);
+ sMimeTypeMap.loadEntry("audio/x-scpls", "pls", false);
+ sMimeTypeMap.loadEntry("audio/x-sd2", "sd2", false);
+ sMimeTypeMap.loadEntry("audio/x-wav", "wav", false);
+ sMimeTypeMap.loadEntry("image/bmp", "bmp", false); // added
+ sMimeTypeMap.loadEntry("image/gif", "gif", false);
+ sMimeTypeMap.loadEntry("image/ico", "cur", false); // added
+ sMimeTypeMap.loadEntry("image/ico", "ico", false); // added
+ sMimeTypeMap.loadEntry("image/ief", "ief", false);
+ sMimeTypeMap.loadEntry("image/jpeg", "jpeg", false);
+ sMimeTypeMap.loadEntry("image/jpeg", "jpg", false);
+ sMimeTypeMap.loadEntry("image/jpeg", "jpe", false);
+ sMimeTypeMap.loadEntry("image/pcx", "pcx", false);
+ sMimeTypeMap.loadEntry("image/png", "png", false);
+ sMimeTypeMap.loadEntry("image/svg+xml", "svg", false);
+ sMimeTypeMap.loadEntry("image/svg+xml", "svgz", false);
+ sMimeTypeMap.loadEntry("image/tiff", "tiff", false);
+ sMimeTypeMap.loadEntry("image/tiff", "tif", false);
+ sMimeTypeMap.loadEntry("image/vnd.djvu", "djvu", false);
+ sMimeTypeMap.loadEntry("image/vnd.djvu", "djv", false);
+ sMimeTypeMap.loadEntry("image/vnd.wap.wbmp", "wbmp", false);
+ sMimeTypeMap.loadEntry("image/x-cmu-raster", "ras", false);
+ sMimeTypeMap.loadEntry("image/x-coreldraw", "cdr", false);
+ sMimeTypeMap.loadEntry("image/x-coreldrawpattern", "pat", false);
+ sMimeTypeMap.loadEntry("image/x-coreldrawtemplate", "cdt", false);
+ sMimeTypeMap.loadEntry("image/x-corelphotopaint", "cpt", false);
+ sMimeTypeMap.loadEntry("image/x-icon", "ico", false);
+ sMimeTypeMap.loadEntry("image/x-jg", "art", false);
+ sMimeTypeMap.loadEntry("image/x-jng", "jng", false);
+ sMimeTypeMap.loadEntry("image/x-ms-bmp", "bmp", false);
+ sMimeTypeMap.loadEntry("image/x-photoshop", "psd", false);
+ sMimeTypeMap.loadEntry("image/x-portable-anymap", "pnm", false);
+ sMimeTypeMap.loadEntry("image/x-portable-bitmap", "pbm", false);
+ sMimeTypeMap.loadEntry("image/x-portable-graymap", "pgm", false);
+ sMimeTypeMap.loadEntry("image/x-portable-pixmap", "ppm", false);
+ sMimeTypeMap.loadEntry("image/x-rgb", "rgb", false);
+ sMimeTypeMap.loadEntry("image/x-xbitmap", "xbm", false);
+ sMimeTypeMap.loadEntry("image/x-xpixmap", "xpm", false);
+ sMimeTypeMap.loadEntry("image/x-xwindowdump", "xwd", false);
+ sMimeTypeMap.loadEntry("model/iges", "igs", false);
+ sMimeTypeMap.loadEntry("model/iges", "iges", false);
+ sMimeTypeMap.loadEntry("model/mesh", "msh", false);
+ sMimeTypeMap.loadEntry("model/mesh", "mesh", false);
+ sMimeTypeMap.loadEntry("model/mesh", "silo", false);
+ sMimeTypeMap.loadEntry("text/calendar", "ics", true);
+ sMimeTypeMap.loadEntry("text/calendar", "icz", true);
+ sMimeTypeMap.loadEntry("text/comma-separated-values", "csv", true);
+ sMimeTypeMap.loadEntry("text/css", "css", true);
+ sMimeTypeMap.loadEntry("text/h323", "323", true);
+ sMimeTypeMap.loadEntry("text/iuls", "uls", true);
+ sMimeTypeMap.loadEntry("text/mathml", "mml", true);
+ sMimeTypeMap.loadEntry("text/plain", "asc", true);
+ sMimeTypeMap.loadEntry("text/plain", "txt", true);
+ sMimeTypeMap.loadEntry("text/plain", "text", true);
+ sMimeTypeMap.loadEntry("text/plain", "diff", true);
+ sMimeTypeMap.loadEntry("text/plain", "pot", true);
+ sMimeTypeMap.loadEntry("text/richtext", "rtx", true);
+ sMimeTypeMap.loadEntry("text/rtf", "rtf", true);
+ sMimeTypeMap.loadEntry("text/texmacs", "ts", true);
+ sMimeTypeMap.loadEntry("text/text", "phps", true);
+ sMimeTypeMap.loadEntry("text/tab-separated-values", "tsv", true);
+ sMimeTypeMap.loadEntry("text/x-bibtex", "bib", true);
+ sMimeTypeMap.loadEntry("text/x-boo", "boo", true);
+ sMimeTypeMap.loadEntry("text/x-c++hdr", "h++", true);
+ sMimeTypeMap.loadEntry("text/x-c++hdr", "hpp", true);
+ sMimeTypeMap.loadEntry("text/x-c++hdr", "hxx", true);
+ sMimeTypeMap.loadEntry("text/x-c++hdr", "hh", true);
+ sMimeTypeMap.loadEntry("text/x-c++src", "c++", true);
+ sMimeTypeMap.loadEntry("text/x-c++src", "cpp", true);
+ sMimeTypeMap.loadEntry("text/x-c++src", "cxx", true);
+ sMimeTypeMap.loadEntry("text/x-chdr", "h", true);
+ sMimeTypeMap.loadEntry("text/x-component", "htc", true);
+ sMimeTypeMap.loadEntry("text/x-csh", "csh", true);
+ sMimeTypeMap.loadEntry("text/x-csrc", "c", true);
+ sMimeTypeMap.loadEntry("text/x-dsrc", "d", true);
+ sMimeTypeMap.loadEntry("text/x-haskell", "hs", true);
+ sMimeTypeMap.loadEntry("text/x-java", "java", true);
+ sMimeTypeMap.loadEntry("text/x-literate-haskell", "lhs", true);
+ sMimeTypeMap.loadEntry("text/x-moc", "moc", true);
+ sMimeTypeMap.loadEntry("text/x-pascal", "p", true);
+ sMimeTypeMap.loadEntry("text/x-pascal", "pas", true);
+ sMimeTypeMap.loadEntry("text/x-pcs-gcd", "gcd", true);
+ sMimeTypeMap.loadEntry("text/x-setext", "etx", true);
+ sMimeTypeMap.loadEntry("text/x-tcl", "tcl", true);
+ sMimeTypeMap.loadEntry("text/x-tex", "tex", true);
+ sMimeTypeMap.loadEntry("text/x-tex", "ltx", true);
+ sMimeTypeMap.loadEntry("text/x-tex", "sty", true);
+ sMimeTypeMap.loadEntry("text/x-tex", "cls", true);
+ sMimeTypeMap.loadEntry("text/x-vcalendar", "vcs", true);
+ sMimeTypeMap.loadEntry("text/x-vcard", "vcf", true);
+ sMimeTypeMap.loadEntry("video/dl", "dl", false);
+ sMimeTypeMap.loadEntry("video/dv", "dif", false);
+ sMimeTypeMap.loadEntry("video/dv", "dv", false);
+ sMimeTypeMap.loadEntry("video/fli", "fli", false);
+ sMimeTypeMap.loadEntry("video/mpeg", "mpeg", false);
+ sMimeTypeMap.loadEntry("video/mpeg", "mpg", false);
+ sMimeTypeMap.loadEntry("video/mpeg", "mpe", false);
+ sMimeTypeMap.loadEntry("video/mp4", "mp4", false);
+ sMimeTypeMap.loadEntry("video/quicktime", "qt", false);
+ sMimeTypeMap.loadEntry("video/quicktime", "mov", false);
+ sMimeTypeMap.loadEntry("video/vnd.mpegurl", "mxu", false);
+ sMimeTypeMap.loadEntry("video/x-la-asf", "lsf", false);
+ sMimeTypeMap.loadEntry("video/x-la-asf", "lsx", false);
+ sMimeTypeMap.loadEntry("video/x-mng", "mng", false);
+ sMimeTypeMap.loadEntry("video/x-ms-asf", "asf", false);
+ sMimeTypeMap.loadEntry("video/x-ms-asf", "asx", false);
+ sMimeTypeMap.loadEntry("video/x-ms-wm", "wm", false);
+ sMimeTypeMap.loadEntry("video/x-ms-wmv", "wmv", false);
+ sMimeTypeMap.loadEntry("video/x-ms-wmx", "wmx", false);
+ sMimeTypeMap.loadEntry("video/x-ms-wvx", "wvx", false);
+ sMimeTypeMap.loadEntry("video/x-msvideo", "avi", false);
+ sMimeTypeMap.loadEntry("video/x-sgi-movie", "movie", false);
+ sMimeTypeMap.loadEntry("x-conference/x-cooltalk", "ice", false);
+ }
+
+ return sMimeTypeMap;
+ }
+}
diff --git a/core/java/android/webkit/Network.java b/core/java/android/webkit/Network.java
new file mode 100644
index 0000000..ea42e58
--- /dev/null
+++ b/core/java/android/webkit/Network.java
@@ -0,0 +1,350 @@
+/*
+ * 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.webkit;
+
+import android.content.Context;
+import android.net.http.*;
+import android.os.*;
+import android.util.Log;
+import android.util.Config;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.util.Map;
+
+import junit.framework.Assert;
+
+class Network {
+
+ private static final String LOGTAG = "network";
+
+ /**
+ * Static instance of a Network object.
+ */
+ private static Network sNetwork;
+
+ /**
+ * Flag to store the state of platform notifications, for the case
+ * when the Network object has not been constructed yet
+ */
+ private static boolean sPlatformNotifications;
+
+ /**
+ * Reference count for platform notifications as the network class is a
+ * static and can exist over multiple activities, thus over multiple
+ * onPause/onResume pairs.
+ */
+ private static int sPlatformNotificationEnableRefCount;
+
+ /**
+ * Proxy username if known (used for pre-emptive proxy authentication).
+ */
+ private String mProxyUsername;
+
+ /**
+ * Proxy password if known (used for pre-emptive proxy authentication).
+ */
+ private String mProxyPassword;
+
+ /**
+ * Network request queue (requests are added from the browser thread).
+ */
+ private RequestQueue mRequestQueue;
+
+ /**
+ * SSL error handler: takes care of synchronization of multiple async
+ * loaders with SSL-related problems.
+ */
+ private SslErrorHandler mSslErrorHandler;
+
+ /**
+ * HTTP authentication handler: takes care of synchronization of HTTP
+ * authentication requests.
+ */
+ private HttpAuthHandler mHttpAuthHandler;
+
+ /**
+ * @return The singleton instance of the network.
+ */
+ public static synchronized Network getInstance(Context context) {
+ if (sNetwork == null) {
+ // Note Context of the Application is used here, rather than
+ // the what is passed in (usually a Context derived from an
+ // Activity) so the intent receivers belong to the application
+ // rather than an activity - this fixes the issue where
+ // Activities are created and destroyed during the lifetime of
+ // an Application
+ sNetwork = new Network(context.getApplicationContext());
+ if (sPlatformNotifications) {
+ // Adjust the ref count before calling enable as it is already
+ // taken into account when the static function was called
+ // directly
+ --sPlatformNotificationEnableRefCount;
+ enablePlatformNotifications();
+ }
+ }
+ return sNetwork;
+ }
+
+
+ /**
+ * Enables data state and proxy tracking
+ */
+ public static void enablePlatformNotifications() {
+ if (++sPlatformNotificationEnableRefCount == 1) {
+ if (sNetwork != null) {
+ sNetwork.mRequestQueue.enablePlatformNotifications();
+ } else {
+ sPlatformNotifications = true;
+ }
+ }
+ }
+
+ /**
+ * If platform notifications are enabled, this should be called
+ * from onPause() or onStop()
+ */
+ public static void disablePlatformNotifications() {
+ if (--sPlatformNotificationEnableRefCount == 0) {
+ if (sNetwork != null) {
+ sNetwork.mRequestQueue.disablePlatformNotifications();
+ } else {
+ sPlatformNotifications = false;
+ }
+ }
+ }
+
+ /**
+ * Creates a new Network object.
+ * XXX: Must be created in the same thread as WebCore!!!!!
+ */
+ private Network(Context context) {
+ if (Config.DEBUG) {
+ Assert.assertTrue(Thread.currentThread().
+ getName().equals(WebViewCore.THREAD_NAME));
+ }
+ mSslErrorHandler = new SslErrorHandler(this);
+ mHttpAuthHandler = new HttpAuthHandler(this);
+
+ mRequestQueue = new RequestQueue(context);
+ }
+
+ /**
+ * Request a url from either the network or the file system.
+ * @param url The url to load.
+ * @param method The http method.
+ * @param headers The http headers.
+ * @param postData The body of the request.
+ * @param loader A LoadListener for receiving the results of the request.
+ * @param isHighPriority True if this is high priority request.
+ * @return True if the request was successfully queued.
+ */
+ public boolean requestURL(String method,
+ Map<String, String> headers,
+ String postData,
+ LoadListener loader,
+ boolean isHighPriority) {
+
+ String url = loader.url();
+
+ // Not a valid url, return false because we won't service the request!
+ if (!URLUtil.isValidUrl(url)) {
+ return false;
+ }
+
+ // asset, file system or data stream are handled in the other code path.
+ // This only handles network request.
+ if (URLUtil.isAssetUrl(url) || URLUtil.isFileUrl(url) ||
+ URLUtil.isDataUrl(url)) {
+ return false;
+ }
+
+ /* FIXME: this is lame. Pass an InputStream in, rather than
+ making this lame one here */
+ InputStream bodyProvider = null;
+ int bodyLength = 0;
+ if (postData != null) {
+ byte[] data = postData.getBytes();
+ bodyLength = data.length;
+ bodyProvider = new ByteArrayInputStream(data);
+ }
+
+ RequestQueue q = mRequestQueue;
+ if (loader.isSynchronous()) {
+ q = new RequestQueue(loader.getContext(), 1);
+ }
+
+ RequestHandle handle = q.queueRequest(
+ url, loader.getWebAddress(), method, headers, loader,
+ bodyProvider, bodyLength, isHighPriority);
+ loader.attachRequestHandle(handle);
+
+ if (loader.isSynchronous()) {
+ handle.waitUntilComplete();
+ loader.loadSynchronousMessages();
+ q.shutdown();
+ }
+ return true;
+ }
+
+ /**
+ * @return True iff there is a valid proxy set.
+ */
+ public boolean isValidProxySet() {
+ // The proxy host and port can be set within a different thread during
+ // an Intent broadcast.
+ synchronized (mRequestQueue) {
+ return mRequestQueue.getProxyHost() != null;
+ }
+ }
+
+ /**
+ * Get the proxy hostname.
+ * @return The proxy hostname obtained from the network queue and proxy
+ * settings.
+ */
+ public String getProxyHostname() {
+ return mRequestQueue.getProxyHost().getHostName();
+ }
+
+ /**
+ * @return The proxy username or null if none.
+ */
+ public synchronized String getProxyUsername() {
+ return mProxyUsername;
+ }
+
+ /**
+ * Sets the proxy username.
+ * @param proxyUsername Username to use when
+ * connecting through the proxy.
+ */
+ public synchronized void setProxyUsername(String proxyUsername) {
+ if (Config.DEBUG) {
+ Assert.assertTrue(isValidProxySet());
+ }
+
+ mProxyUsername = proxyUsername;
+ }
+
+ /**
+ * @return The proxy password or null if none.
+ */
+ public synchronized String getProxyPassword() {
+ return mProxyPassword;
+ }
+
+ /**
+ * Sets the proxy password.
+ * @param proxyPassword Password to use when
+ * connecting through the proxy.
+ */
+ public synchronized void setProxyPassword(String proxyPassword) {
+ if (Config.DEBUG) {
+ Assert.assertTrue(isValidProxySet());
+ }
+
+ mProxyPassword = proxyPassword;
+ }
+
+ /**
+ * If we need to stop loading done in a handler (here, browser frame), we
+ * send a message to the handler to stop loading, and remove all loaders
+ * that share the same CallbackProxy in question from all local
+ * handlers (such as ssl-error and http-authentication handler).
+ * @param proxy The CallbackProxy responsible for cancelling the current
+ * load.
+ */
+ public void resetHandlersAndStopLoading(BrowserFrame frame) {
+ if (Config.LOGV) {
+ Log.v(LOGTAG, "Network.resetHandlersAndStopLoading()");
+ }
+
+ frame.stopLoading();
+ mSslErrorHandler.reset(frame);
+ mHttpAuthHandler.reset(frame);
+ }
+
+ /**
+ * Saves the state of network handlers (user SSL and HTTP-authentication
+ * preferences).
+ * @param outState The out-state to save (write) to.
+ * @return True iff succeeds.
+ */
+ public boolean saveState(Bundle outState) {
+ if (Config.LOGV) {
+ Log.v(LOGTAG, "Network.saveState()");
+ }
+
+ return mSslErrorHandler.saveState(outState);
+ }
+
+ /**
+ * Restores the state of network handlers (user SSL and HTTP-authentication
+ * preferences).
+ * @param inState The in-state to load (read) from.
+ * @return True iff succeeds.
+ */
+ public boolean restoreState(Bundle inState) {
+ if (Config.LOGV) {
+ Log.v(LOGTAG, "Network.restoreState()");
+ }
+
+ return mSslErrorHandler.restoreState(inState);
+ }
+
+ /**
+ * Clears user SSL-error preference table.
+ */
+ public void clearUserSslPrefTable() {
+ mSslErrorHandler.clear();
+ }
+
+ /**
+ * Handles SSL error(s) on the way up to the user: the user must decide
+ * whether errors should be ignored or not.
+ * @param loader The loader that resulted in SSL errors.
+ */
+ public void handleSslErrorRequest(LoadListener loader) {
+ if (Config.DEBUG) Assert.assertNotNull(loader);
+ if (loader != null) {
+ mSslErrorHandler.handleSslErrorRequest(loader);
+ }
+ }
+
+ /**
+ * Handles authentication requests on their way up to the user (the user
+ * must provide credentials).
+ * @param loader The loader that resulted in an HTTP
+ * authentication request.
+ */
+ public void handleAuthRequest(LoadListener loader) {
+ if (Config.DEBUG) Assert.assertNotNull(loader);
+ if (loader != null) {
+ mHttpAuthHandler.handleAuthRequest(loader);
+ }
+ }
+
+ // Performance probe
+ public void startTiming() {
+ mRequestQueue.startTiming();
+ }
+
+ public void stopTiming() {
+ mRequestQueue.stopTiming();
+ }
+}
diff --git a/core/java/android/webkit/PerfChecker.java b/core/java/android/webkit/PerfChecker.java
new file mode 100644
index 0000000..8c5f86e
--- /dev/null
+++ b/core/java/android/webkit/PerfChecker.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.webkit;
+
+import android.os.SystemClock;
+import android.util.Log;
+
+class PerfChecker {
+
+ private long mTime;
+ private static final long mResponseThreshold = 2000; // 2s
+
+ public PerfChecker() {
+ if (false) {
+ mTime = SystemClock.uptimeMillis();
+ }
+ }
+
+ /**
+ * @param what log string
+ * Logs given string if mResponseThreshold time passed between either
+ * instantiation or previous responseAlert call
+ */
+ public void responseAlert(String what) {
+ if (false) {
+ long upTime = SystemClock.uptimeMillis();
+ long time = upTime - mTime;
+ if (time > mResponseThreshold) {
+ Log.w("webkit", what + " used " + time + " ms");
+ }
+ // Reset mTime, to permit reuse
+ mTime = upTime;
+ }
+ }
+}
diff --git a/core/java/android/webkit/Plugin.java b/core/java/android/webkit/Plugin.java
new file mode 100644
index 0000000..f83da99
--- /dev/null
+++ b/core/java/android/webkit/Plugin.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.webkit;
+
+import com.android.internal.R;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.webkit.WebView;
+
+/**
+ * Represents a plugin (Java equivalent of the PluginPackageAndroid
+ * C++ class in libs/WebKitLib/WebKit/WebCore/plugins/android/)
+ */
+public class Plugin {
+ public interface PreferencesClickHandler {
+ public void handleClickEvent(Context context);
+ }
+
+ private String mName;
+ private String mPath;
+ private String mFileName;
+ private String mDescription;
+ private PreferencesClickHandler mHandler;
+
+ public Plugin(String name,
+ String path,
+ String fileName,
+ String description) {
+ mName = name;
+ mPath = path;
+ mFileName = fileName;
+ mDescription = description;
+ mHandler = new DefaultClickHandler();
+ }
+
+ public String toString() {
+ return mName;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public String getPath() {
+ return mPath;
+ }
+
+ public String getFileName() {
+ return mFileName;
+ }
+
+ public String getDescription() {
+ return mDescription;
+ }
+
+ public void setName(String name) {
+ mName = name;
+ }
+
+ public void setPath(String path) {
+ mPath = path;
+ }
+
+ public void setFileName(String fileName) {
+ mFileName = fileName;
+ }
+
+ public void setDescription(String description) {
+ mDescription = description;
+ }
+
+ public void setClickHandler(PreferencesClickHandler handler) {
+ mHandler = handler;
+ }
+
+ /**
+ * Invokes the click handler for this plugin.
+ */
+ public void dispatchClickEvent(Context context) {
+ if (mHandler != null) {
+ mHandler.handleClickEvent(context);
+ }
+ }
+
+ /**
+ * Default click handler. The plugins should implement their own.
+ */
+ private class DefaultClickHandler implements PreferencesClickHandler,
+ DialogInterface.OnClickListener {
+ private AlertDialog mDialog;
+
+ public void handleClickEvent(Context context) {
+ // Show a simple popup dialog containing the description
+ // string of the plugin.
+ if (mDialog == null) {
+ mDialog = new AlertDialog.Builder(context)
+ .setTitle(mName)
+ .setMessage(mDescription)
+ .setPositiveButton(R.string.ok, this)
+ .setCancelable(false)
+ .show();
+ }
+ }
+
+ public void onClick(DialogInterface dialog, int which) {
+ mDialog.dismiss();
+ mDialog = null;
+ }
+ }
+}
diff --git a/core/java/android/webkit/PluginList.java b/core/java/android/webkit/PluginList.java
new file mode 100644
index 0000000..a9d3d8c
--- /dev/null
+++ b/core/java/android/webkit/PluginList.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.webkit;
+
+import android.content.Context;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A simple list of initialized plugins. This list gets
+ * populated when the plugins are initialized (at
+ * browser startup, at the moment).
+ */
+public class PluginList {
+ private ArrayList<Plugin> mPlugins;
+
+ /**
+ * Public constructor. Initializes the list of plugins.
+ */
+ public PluginList() {
+ mPlugins = new ArrayList<Plugin>();
+ }
+
+ /**
+ * Returns the list of plugins as a java.util.List.
+ */
+ public synchronized List getList() {
+ return mPlugins;
+ }
+
+ /**
+ * Adds a plugin to the list.
+ */
+ public synchronized void addPlugin(Plugin plugin) {
+ if (!mPlugins.contains(plugin)) {
+ mPlugins.add(plugin);
+ }
+ }
+
+ /**
+ * Removes a plugin from the list.
+ */
+ public synchronized void removePlugin(Plugin plugin) {
+ int location = mPlugins.indexOf(plugin);
+ if (location != -1) {
+ mPlugins.remove(location);
+ }
+ }
+
+ /**
+ * Clears the plugin list.
+ */
+ public synchronized void clear() {
+ mPlugins.clear();
+ }
+
+ /**
+ * Dispatches the click event to the appropriate plugin.
+ */
+ public synchronized void pluginClicked(Context context, int position) {
+ try {
+ Plugin plugin = mPlugins.get(position);
+ plugin.dispatchClickEvent(context);
+ } catch (IndexOutOfBoundsException e) {
+ // This can happen if the list of plugins
+ // gets changed while the pref menu is up.
+ }
+ }
+}
diff --git a/core/java/android/webkit/SslErrorHandler.java b/core/java/android/webkit/SslErrorHandler.java
new file mode 100644
index 0000000..115434a
--- /dev/null
+++ b/core/java/android/webkit/SslErrorHandler.java
@@ -0,0 +1,255 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.webkit;
+
+import junit.framework.Assert;
+
+import android.net.http.SslError;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Config;
+import android.util.Log;
+
+import java.util.LinkedList;
+import java.util.ListIterator;
+
+/**
+ * SslErrorHandler: class responsible for handling SSL errors. This class is
+ * passed as a parameter to BrowserCallback.displaySslErrorDialog and is meant
+ * to receive the user's response.
+ */
+public class SslErrorHandler extends Handler {
+ /* One problem here is that there may potentially be multiple SSL errors
+ * coming from mutiple loaders. Therefore, we keep a queue of loaders
+ * that have SSL-related problems and process errors one by one in the
+ * order they were received.
+ */
+
+ private static final String LOGTAG = "network";
+
+ /**
+ * Network.
+ */
+ private Network mNetwork;
+
+ /**
+ * Queue of loaders that experience SSL-related problems.
+ */
+ private LinkedList<LoadListener> mLoaderQueue;
+
+ /**
+ * SSL error preference table.
+ */
+ private Bundle mSslPrefTable;
+
+ // Message id for handling the response
+ private final int HANDLE_RESPONSE = 100;
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case HANDLE_RESPONSE:
+ handleSslErrorResponse(msg.arg1 == 1);
+ fastProcessQueuedSslErrors();
+ break;
+ }
+ }
+
+ /**
+ * Creates a new error handler with an empty loader queue.
+ */
+ /* package */ SslErrorHandler(Network network) {
+ mNetwork = network;
+
+ mLoaderQueue = new LinkedList<LoadListener>();
+ mSslPrefTable = new Bundle();
+ }
+
+ /**
+ * Saves this handler's state into a map.
+ * @return True iff succeeds.
+ */
+ /* package */ boolean saveState(Bundle outState) {
+ boolean success = (outState != null);
+ if (success) {
+ // TODO?
+ outState.putBundle("ssl-error-handler", mSslPrefTable);
+ }
+
+ return success;
+ }
+
+ /**
+ * Restores this handler's state from a map.
+ * @return True iff succeeds.
+ */
+ /* package */ boolean restoreState(Bundle inState) {
+ boolean success = (inState != null);
+ if (success) {
+ success = inState.containsKey("ssl-error-handler");
+ if (success) {
+ mSslPrefTable = inState.getBundle("ssl-error-handler");
+ }
+ }
+
+ return success;
+ }
+
+ /**
+ * Clears SSL error preference table.
+ */
+ /* package */ synchronized void clear() {
+ mSslPrefTable.clear();
+ }
+
+ /**
+ * Resets the SSL error handler, removes all loaders that
+ * share the same BrowserFrame.
+ */
+ /* package */ synchronized void reset(BrowserFrame frame) {
+ ListIterator<LoadListener> i = mLoaderQueue.listIterator(0);
+ while (i.hasNext()) {
+ LoadListener loader = i.next();
+ if (frame == loader.getFrame()) {
+ i.remove();
+ }
+ }
+ }
+
+ /**
+ * Handles SSL error(s) on the way up to the user.
+ */
+ /* package */ synchronized void handleSslErrorRequest(LoadListener loader) {
+ if (Config.LOGV) {
+ Log.v(LOGTAG, "SslErrorHandler.handleSslErrorRequest(): " +
+ "url=" + loader.url());
+ }
+
+ if (!loader.cancelled()) {
+ mLoaderQueue.offer(loader);
+ if (loader == mLoaderQueue.peek()) {
+ fastProcessQueuedSslErrors();
+ }
+ }
+ }
+
+ /**
+ * Processes queued SSL-error confirmation requests in
+ * a tight loop while there is no need to ask the user.
+ */
+ /* package */void fastProcessQueuedSslErrors() {
+ while (processNextLoader());
+ }
+
+ /**
+ * Processes the next loader in the queue.
+ * @return True iff should proceed to processing the
+ * following loader in the queue
+ */
+ private synchronized boolean processNextLoader() {
+ LoadListener loader = mLoaderQueue.peek();
+ if (loader != null) {
+ // if this loader has been cancelled
+ if (loader.cancelled()) {
+ // go to the following loader in the queue
+ return true;
+ }
+
+ SslError error = loader.sslError();
+
+ if (Config.DEBUG) {
+ Assert.assertNotNull(error);
+ }
+
+ int primary = error.getPrimaryError();
+ String host = loader.host();
+
+ if (Config.DEBUG) {
+ Assert.assertTrue(host != null && primary != 0);
+ }
+
+ if (mSslPrefTable.containsKey(host)) {
+ if (primary <= mSslPrefTable.getInt(host)) {
+ handleSslErrorResponse(true);
+ return true;
+ }
+ }
+
+ // if we do not have information on record, ask
+ // the user (display a dialog)
+ CallbackProxy proxy = loader.getFrame().getCallbackProxy();
+ proxy.onReceivedSslError(this, error);
+ }
+
+ // the queue must be empty, stop
+ return false;
+ }
+
+ /**
+ * Proceed with the SSL certificate.
+ */
+ public void proceed() {
+ sendMessage(obtainMessage(HANDLE_RESPONSE, 1, 0));
+ }
+
+ /**
+ * Cancel this request and all pending requests for the WebView that had
+ * the error.
+ */
+ public void cancel() {
+ sendMessage(obtainMessage(HANDLE_RESPONSE, 0, 0));
+ }
+
+ /**
+ * Handles SSL error(s) on the way down from the user.
+ */
+ /* package */ synchronized void handleSslErrorResponse(boolean proceed) {
+ LoadListener loader = mLoaderQueue.poll();
+ if (Config.DEBUG) {
+ Assert.assertNotNull(loader);
+ }
+
+ if (Config.LOGV) {
+ Log.v(LOGTAG, "SslErrorHandler.handleSslErrorResponse():"
+ + " proceed: " + proceed
+ + " url:" + loader.url());
+ }
+
+ if (!loader.cancelled()) {
+ if (proceed) {
+ // update the user's SSL error preference table
+ int primary = loader.sslError().getPrimaryError();
+ String host = loader.host();
+
+ if (Config.DEBUG) {
+ Assert.assertTrue(host != null && primary != 0);
+ }
+ boolean hasKey = mSslPrefTable.containsKey(host);
+ if (!hasKey ||
+ primary > mSslPrefTable.getInt(host)) {
+ mSslPrefTable.putInt(host, new Integer(primary));
+ }
+
+ loader.handleSslErrorResponse(proceed);
+ } else {
+ loader.handleSslErrorResponse(proceed);
+ mNetwork.resetHandlersAndStopLoading(loader.getFrame());
+ }
+ }
+ }
+}
diff --git a/core/java/android/webkit/StreamLoader.java b/core/java/android/webkit/StreamLoader.java
new file mode 100644
index 0000000..9098307
--- /dev/null
+++ b/core/java/android/webkit/StreamLoader.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.webkit;
+
+import android.net.http.EventHandler;
+import android.net.http.Headers;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Config;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+
+/**
+ * This abstract class is used for all content loaders that rely on streaming
+ * content into the rendering engine loading framework.
+ *
+ * The class implements a state machine to load the content into the frame in
+ * a similar manor to the way content arrives from the network. The class uses
+ * messages to move from one state to the next, which enables async. loading of
+ * the streamed content.
+ *
+ * Classes that inherit from this class must implement two methods, the first
+ * method is used to setup the InputStream and notify the loading framework if
+ * it can load it's content. The other method allows the derived class to add
+ * additional HTTP headers to the response.
+ *
+ * By default, content loaded with a StreamLoader is marked with a HTTP header
+ * that indicates the content should not be cached.
+ *
+ */
+abstract class StreamLoader extends Handler {
+
+ public static final String NO_STORE = "no-store";
+
+ private static final int MSG_STATUS = 100; // Send status to loader
+ private static final int MSG_HEADERS = 101; // Send headers to loader
+ private static final int MSG_DATA = 102; // Send data to loader
+ private static final int MSG_END = 103; // Send endData to loader
+
+ protected LoadListener mHandler; // loader class
+ protected InputStream mDataStream; // stream to read data from
+ protected long mContentLength; // content length of data
+ private byte [] mData; // buffer to pass data to loader with.
+
+ /**
+ * Constructor. Although this class calls the LoadListener, it only calls
+ * the EventHandler Interface methods. LoadListener concrete class is used
+ * to avoid the penality of calling an interface.
+ *
+ * @param loadlistener The LoadListener to call with the data.
+ */
+ StreamLoader(LoadListener loadlistener) {
+ mHandler = loadlistener;
+ }
+
+ /**
+ * This method is called when the derived class should setup mDataStream,
+ * and call mHandler.status() to indicate that the load can occur. If it
+ * fails to setup, it should still call status() with the error code.
+ *
+ * @return true if stream was successfully setup
+ */
+ protected abstract boolean setupStreamAndSendStatus();
+
+ /**
+ * This method is called when the headers are about to be sent to the
+ * load framework. The derived class has the opportunity to add addition
+ * headers.
+ *
+ * @param headers Map of HTTP headers that will be sent to the loader.
+ */
+ abstract protected void buildHeaders(Headers headers);
+
+
+ /**
+ * Calling this method starts the load of the content for this StreamLoader.
+ * This method simply posts a message to send the status and returns
+ * immediately.
+ */
+ public void load() {
+ if (!mHandler.isSynchronous()) {
+ sendMessage(obtainMessage(MSG_STATUS));
+ } else {
+ // Load the stream synchronously.
+ if (setupStreamAndSendStatus()) {
+ // We were able to open the stream, create the array
+ // to pass data to the loader
+ mData = new byte[8192];
+ sendHeaders();
+ while (!sendData());
+ closeStreamAndSendEndData();
+ mHandler.loadSynchronousMessages();
+ }
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see android.os.Handler#handleMessage(android.os.Message)
+ */
+ public void handleMessage(Message msg) {
+ if (Config.DEBUG && mHandler.isSynchronous()) {
+ throw new AssertionError();
+ }
+ switch(msg.what) {
+ case MSG_STATUS:
+ if (setupStreamAndSendStatus()) {
+ // We were able to open the stream, create the array
+ // to pass data to the loader
+ mData = new byte[8192];
+ sendMessage(obtainMessage(MSG_HEADERS));
+ }
+ break;
+ case MSG_HEADERS:
+ sendHeaders();
+ sendMessage(obtainMessage(MSG_DATA));
+ break;
+ case MSG_DATA:
+ if (sendData()) {
+ sendMessage(obtainMessage(MSG_END));
+ } else {
+ sendMessage(obtainMessage(MSG_DATA));
+ }
+ break;
+ case MSG_END:
+ closeStreamAndSendEndData();
+ break;
+ default:
+ super.handleMessage(msg);
+ break;
+ }
+ }
+
+ /**
+ * Construct the headers and pass them to the EventHandler.
+ */
+ private void sendHeaders() {
+ Headers headers = new Headers();
+ if (mContentLength > 0) {
+ headers.setContentLength(mContentLength);
+ }
+ headers.setCacheControl(NO_STORE);
+ buildHeaders(headers);
+ mHandler.headers(headers);
+ }
+
+ /**
+ * Read data from the stream and pass it to the EventHandler.
+ * If an error occurs reading the stream, then an error is sent to the
+ * EventHandler, and moves onto the next state - end of data.
+ * @return True if all the data has been read. False if sendData should be
+ * called again.
+ */
+ private boolean sendData() {
+ if (mDataStream != null) {
+ try {
+ int amount = mDataStream.read(mData);
+ if (amount > 0) {
+ mHandler.data(mData, amount);
+ return false;
+ }
+ } catch (IOException ex) {
+ mHandler.error(EventHandler.FILE_ERROR,
+ ex.getMessage());
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Close the stream and inform the EventHandler that load is complete.
+ */
+ private void closeStreamAndSendEndData() {
+ if (mDataStream != null) {
+ try {
+ mDataStream.close();
+ } catch (IOException ex) {
+ // ignore.
+ }
+ }
+ mHandler.endData();
+ }
+
+}
diff --git a/core/java/android/webkit/TextDialog.java b/core/java/android/webkit/TextDialog.java
new file mode 100644
index 0000000..95209c7
--- /dev/null
+++ b/core/java/android/webkit/TextDialog.java
@@ -0,0 +1,543 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.webkit;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
+import android.graphics.drawable.ShapeDrawable;
+import android.graphics.drawable.shapes.RectShape;
+import android.os.Handler;
+import android.os.Message;
+import android.text.Editable;
+import android.text.InputFilter;
+import android.text.Selection;
+import android.text.Spannable;
+import android.text.TextPaint;
+import android.text.TextUtils;
+import android.text.method.MetaKeyKeyListener;
+import android.text.method.MovementMethod;
+import android.text.method.PasswordTransformationMethod;
+import android.text.method.TextKeyListener;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View.MeasureSpec;
+import android.view.ViewConfiguration;
+import android.widget.AbsoluteLayout.LayoutParams;
+import android.widget.AutoCompleteTextView;
+
+/**
+ * TextDialog is a specialized version of EditText used by WebView
+ * to overlay html textfields (and textareas) to use our standard
+ * text editing.
+ */
+/* package */ class TextDialog extends AutoCompleteTextView {
+
+ private WebView mWebView;
+ private boolean mSingle;
+ private int mWidthSpec;
+ private int mHeightSpec;
+ private int mNodePointer;
+ // FIXME: This is a hack for blocking unmatched key ups, in particular
+ // on the enter key. The method for blocking unmatched key ups prevents
+ // the shift key from working properly.
+ private boolean mGotEnterDown;
+ // Determines whether we allow calls to requestRectangleOnScreen to
+ // propagate. We only want to scroll if the user is typing. If the
+ // user is simply navigating through a textfield, we do not want to
+ // scroll.
+ private boolean mScrollToAccommodateCursor;
+ private int mMaxLength;
+ // Keep track of the text before the change so we know whether we actually
+ // need to send down the DOM events.
+ private String mPreChange;
+ // Array to store the final character added in onTextChanged, so that its
+ // KeyEvents may be determined.
+ private char[] mCharacter = new char[1];
+ // This is used to reset the length filter when on a textfield
+ // with no max length.
+ // FIXME: This can be replaced with TextView.NO_FILTERS if that
+ // is made public/protected.
+ private static final InputFilter[] NO_FILTERS = new InputFilter[0];
+ // The time of the last enter down, so we know whether to perform a long
+ // press.
+ private long mDownTime;
+
+ private boolean mTrackballDown = false;
+ private static int LONGPRESS = 1;
+ private Handler mHandler = new Handler() {
+ public void handleMessage(Message msg) {
+ if (msg.what == LONGPRESS) {
+ if (mTrackballDown) {
+ performLongClick();
+ mTrackballDown = false;
+ }
+ }
+ }
+ };
+
+ /**
+ * Create a new TextDialog.
+ * @param context The Context for this TextDialog.
+ * @param webView The WebView that created this.
+ */
+ /* package */ TextDialog(Context context, WebView webView) {
+ super(context);
+ mWebView = webView;
+ ShapeDrawable background = new ShapeDrawable(new RectShape());
+ Paint shapePaint = background.getPaint();
+ shapePaint.setStyle(Paint.Style.STROKE);
+ ColorDrawable color = new ColorDrawable(Color.WHITE);
+ Drawable[] array = new Drawable[2];
+ array[0] = color;
+ array[1] = background;
+ LayerDrawable layers = new LayerDrawable(array);
+ // Hide WebCore's text behind this and allow the WebView
+ // to draw its own focusring.
+ setBackgroundDrawable(layers);
+ // Align the text better with the text behind it, so moving
+ // off of the textfield will not appear to move the text.
+ setPadding(3, 2, 0, 0);
+ mMaxLength = -1;
+ // Turn on subpixel text, and turn off kerning, so it better matches
+ // the text in webkit.
+ TextPaint paint = getPaint();
+ int flags = paint.getFlags() | Paint.SUBPIXEL_TEXT_FLAG |
+ Paint.ANTI_ALIAS_FLAG & ~Paint.DEV_KERN_TEXT_FLAG;
+ paint.setFlags(flags);
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ if (event.isSystem()) {
+ return super.dispatchKeyEvent(event);
+ }
+ // Treat ACTION_DOWN and ACTION MULTIPLE the same
+ boolean down = event.getAction() != KeyEvent.ACTION_UP;
+ int keyCode = event.getKeyCode();
+ Spannable text = (Spannable) getText();
+ int oldLength = text.length();
+ // Normally the delete key's dom events are sent via onTextChanged.
+ // However, if the length is zero, the text did not change, so we
+ // go ahead and pass the key down immediately.
+ if (KeyEvent.KEYCODE_DEL == keyCode && 0 == oldLength) {
+ sendDomEvent(event);
+ return true;
+ }
+
+ // For single-line textfields, return key should not be handled
+ // here. Instead, the WebView is passed the key up, so it may fire a
+ // submit/onClick.
+ // Center key should always be passed to a potential onClick
+ if ((mSingle && KeyEvent.KEYCODE_ENTER == keyCode)
+ || KeyEvent.KEYCODE_DPAD_CENTER == keyCode) {
+ if (isPopupShowing()) {
+ super.dispatchKeyEvent(event);
+ return true;
+ }
+ if (down) {
+ if (event.getRepeatCount() == 0) {
+ mGotEnterDown = true;
+ mDownTime = event.getEventTime();
+ // Send the keydown when the up comes, so that we have
+ // a chance to handle a long press.
+ } else if (mGotEnterDown && event.getEventTime() - mDownTime >
+ ViewConfiguration.getLongPressTimeout()) {
+ performLongClick();
+ mGotEnterDown = false;
+ }
+ } else if (mGotEnterDown) {
+ mGotEnterDown = false;
+ if (KeyEvent.KEYCODE_DPAD_CENTER == keyCode) {
+ mWebView.shortPressOnTextField();
+ return true;
+ }
+ sendDomEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
+ sendDomEvent(event);
+ }
+ return true;
+ }
+ // Ensure there is a layout so arrow keys are handled properly.
+ if (getLayout() == null) {
+ measure(mWidthSpec, mHeightSpec);
+ }
+ int oldStart = Selection.getSelectionStart(text);
+ int oldEnd = Selection.getSelectionEnd(text);
+
+ boolean maxedOut = mMaxLength != -1 && oldLength == mMaxLength;
+ // If we are at max length, and there is a selection rather than a
+ // cursor, we need to store the text to compare later, since the key
+ // may have changed the string.
+ String oldText;
+ if (maxedOut && oldEnd != oldStart) {
+ oldText = text.toString();
+ } else {
+ oldText = "";
+ }
+ if (super.dispatchKeyEvent(event)) {
+ // If the TextDialog handled the key it was either an alphanumeric
+ // key, a delete, or a movement within the text. All of those are
+ // ok to pass to javascript.
+
+ // UNLESS there is a max length determined by the html. In that
+ // case, if the string was already at the max length, an
+ // alphanumeric key will be erased by the LengthFilter,
+ // so do not pass down to javascript, and instead
+ // return true. If it is an arrow key or a delete key, we can go
+ // ahead and pass it down.
+ boolean isArrowKey;
+ switch(keyCode) {
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ case KeyEvent.KEYCODE_DPAD_UP:
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ isArrowKey = true;
+ break;
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ case KeyEvent.KEYCODE_ENTER:
+ // For multi-line text boxes, newlines and dpad center will
+ // trigger onTextChanged for key down (which will send both
+ // key up and key down) but not key up.
+ mGotEnterDown = true;
+ default:
+ isArrowKey = false;
+ break;
+ }
+ if (maxedOut && !isArrowKey && keyCode != KeyEvent.KEYCODE_DEL) {
+ if (oldEnd == oldStart) {
+ // Return true so the key gets dropped.
+ mScrollToAccommodateCursor = true;
+ return true;
+ } else if (!oldText.equals(getText().toString())) {
+ // FIXME: This makes the text work properly, but it
+ // does not pass down the key event, so it may not
+ // work for a textfield that has the type of
+ // behavior of GoogleSuggest. That said, it is
+ // unlikely that a site would combine the two in
+ // one textfield.
+ Spannable span = (Spannable) getText();
+ int newStart = Selection.getSelectionStart(span);
+ int newEnd = Selection.getSelectionEnd(span);
+ mWebView.replaceTextfieldText(0, oldLength, span.toString(),
+ newStart, newEnd);
+ mScrollToAccommodateCursor = true;
+ return true;
+ }
+ }
+ if (isArrowKey) {
+ // Arrow key does not change the text, but we still want to send
+ // the DOM events.
+ sendDomEvent(event);
+ }
+ mScrollToAccommodateCursor = true;
+ return true;
+ }
+ // FIXME: TextViews return false for up and down key events even though
+ // they change the selection. Since we don't want the get out of sync
+ // with WebCore's notion of the current selection, reset the selection
+ // to what it was before the key event.
+ Selection.setSelection(text, oldStart, oldEnd);
+ // Ignore the key up event for newlines or dpad center. This prevents
+ // multiple newlines in the native textarea.
+ if (mGotEnterDown && !down) {
+ return true;
+ }
+ // WebView check the trackballtime in onKeyDown to avoid calling native
+ // from both trackball and key handling. As this is called from
+ // TextDialog, we always want WebView to check with native. Reset
+ // trackballtime to ensure it.
+ mWebView.resetTrackballTime();
+ return down ? mWebView.onKeyDown(keyCode, event) :
+ mWebView.onKeyUp(keyCode, event);
+ }
+
+ /**
+ * Determine whether this TextDialog currently represents the node
+ * represented by ptr.
+ * @param ptr Pointer to a node to compare to.
+ * @return boolean Whether this TextDialog already represents the node
+ * pointed to by ptr.
+ */
+ /* package */ boolean isSameTextField(int ptr) {
+ return ptr == mNodePointer;
+ }
+
+ @Override
+ public boolean onPreDraw() {
+ if (getLayout() == null) {
+ measure(mWidthSpec, mHeightSpec);
+ }
+ return super.onPreDraw();
+ }
+
+ @Override
+ protected void onTextChanged(CharSequence s,int start,int before,int count){
+ super.onTextChanged(s, start, before, count);
+ String postChange = s.toString();
+ // Prevent calls to setText from invoking onTextChanged (since this will
+ // mean we are on a different textfield). Also prevent the change when
+ // going from a textfield with a string of text to one with a smaller
+ // limit on text length from registering the onTextChanged event.
+ if (mPreChange == null || mPreChange.equals(postChange) ||
+ (mMaxLength > -1 && mPreChange.length() > mMaxLength &&
+ mPreChange.substring(0, mMaxLength).equals(postChange))) {
+ return;
+ }
+ mPreChange = postChange;
+ // This was simply a delete or a cut, so just delete the
+ // selection.
+ if (before > 0 && 0 == count) {
+ mWebView.deleteSelection(start, start + before);
+ // For this and all changes to the text, update our cache
+ updateCachedTextfield();
+ return;
+ }
+ // In this case, replace before with all but the last character of the
+ // new text.
+ if (count > 1) {
+ String replace = s.subSequence(start, start + count - 1).toString();
+ mWebView.replaceTextfieldText(start, start + before, replace,
+ start + count - 1, start + count - 1);
+ } else {
+ // This corrects the selection which may have been affected by the
+ // trackball or auto-correct.
+ mWebView.setSelection(start, start + before);
+ }
+ // Whether the text to be added is only one character, or we already
+ // added all but the last character, we now figure out the DOM events
+ // for the last character, and pass them down.
+ TextUtils.getChars(s, start + count - 1, start + count, mCharacter, 0);
+ // We only care about the events that translate directly into
+ // characters. Should we be using KeyCharacterMap.BUILT_IN_KEYBOARD?
+ // The comment makes it sound like it may not be directly related to
+ // the keys. However, KeyCharacterMap.ALPHA says it has "maybe some
+ // numbers." Not sure if that will have the numbers we may need.
+ KeyCharacterMap kmap =
+ KeyCharacterMap.load(KeyCharacterMap.BUILT_IN_KEYBOARD);
+ KeyEvent[] events = kmap.getEvents(mCharacter);
+ updateCachedTextfield();
+ if (null == events) {
+ return;
+ }
+ int length = events.length;
+ for (int i = 0; i < length; i++) {
+ // We never send modifier keys to native code so don't send them
+ // here either.
+ if (!KeyEvent.isModifierKey(events[i].getKeyCode())) {
+ sendDomEvent(events[i]);
+ }
+ }
+ }
+
+ @Override
+ public boolean onTrackballEvent(MotionEvent event) {
+ if (isPopupShowing()) {
+ return super.onTrackballEvent(event);
+ }
+ int action = event.getAction();
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ if (!mTrackballDown) {
+ mTrackballDown = true;
+ mHandler.sendEmptyMessageDelayed(LONGPRESS,
+ ViewConfiguration.getLongPressTimeout());
+ }
+ return true;
+ case MotionEvent.ACTION_UP:
+ if (mTrackballDown) {
+ mWebView.shortPressOnTextField();
+ mTrackballDown = false;
+ mHandler.removeMessages(LONGPRESS);
+ }
+ return true;
+ case MotionEvent.ACTION_CANCEL:
+ mTrackballDown = false;
+ return true;
+ case MotionEvent.ACTION_MOVE:
+ // fall through
+ }
+ Spannable text = (Spannable) getText();
+ MovementMethod move = getMovementMethod();
+ if (move != null && getLayout() != null &&
+ move.onTrackballEvent(this, text, event)) {
+ // Need to pass down the selection, which has changed.
+ // FIXME: This should work, but does not, so we set the selection
+ // in onTextChanged.
+ //int start = Selection.getSelectionStart(text);
+ //int end = Selection.getSelectionEnd(text);
+ //mWebView.setSelection(start, end);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Remove this TextDialog from its host WebView, and return
+ * focus to the host.
+ */
+ /* package */ void remove() {
+ mWebView.removeView(this);
+ mWebView.requestFocus();
+ }
+
+ @Override
+ public boolean requestRectangleOnScreen(Rect rectangle) {
+ if (mScrollToAccommodateCursor) {
+ return super.requestRectangleOnScreen(rectangle);
+ }
+ return false;
+ }
+
+ /**
+ * Send the DOM events for the specified event.
+ * @param event KeyEvent to be translated into a DOM event.
+ */
+ private void sendDomEvent(KeyEvent event) {
+ mWebView.passToJavaScript(getText().toString(), event);
+ }
+
+ /**
+ * Determine whether to use the system-wide password disguising method,
+ * or to use none.
+ * @param inPassword True if the textfield is a password field.
+ */
+ /* package */ void setInPassword(boolean inPassword) {
+ PasswordTransformationMethod method;
+ if (inPassword) {
+ method = PasswordTransformationMethod.getInstance();
+ } else {
+ method = null;
+ }
+ setTransformationMethod(method);
+ }
+
+ /* package */ void setMaxLength(int maxLength) {
+ mMaxLength = maxLength;
+ if (-1 == maxLength) {
+ setFilters(NO_FILTERS);
+ } else {
+ setFilters(new InputFilter[] {
+ new InputFilter.LengthFilter(maxLength) });
+ }
+ }
+
+ /**
+ * Set the pointer for this node so it can be determined which node this
+ * TextDialog represents.
+ * @param ptr Integer representing the pointer to the node which this
+ * TextDialog represents.
+ */
+ /* package */ void setNodePointer(int ptr) {
+ mNodePointer = ptr;
+ }
+
+ /**
+ * Determine the position and size of TextDialog, and add it to the
+ * WebView's view heirarchy. All parameters are presumed to be in
+ * view coordinates. Also requests Focus and sets the cursor to not
+ * request to be in view.
+ * @param x x-position of the textfield.
+ * @param y y-position of the textfield.
+ * @param width width of the textfield.
+ * @param height height of the textfield.
+ */
+ /* package */ void setRect(int x, int y, int width, int height) {
+ LayoutParams lp = (LayoutParams) getLayoutParams();
+ if (null == lp) {
+ lp = new LayoutParams(width, height, x, y);
+ } else {
+ lp.x = x;
+ lp.y = y;
+ lp.width = width;
+ lp.height = height;
+ }
+ if (getParent() == null) {
+ mWebView.addView(this, lp);
+ } else {
+ setLayoutParams(lp);
+ }
+ // Set up a measure spec so a layout can always be recreated.
+ mWidthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
+ mHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
+ mScrollToAccommodateCursor = false;
+ requestFocus();
+ }
+
+ /**
+ * Set whether this is a single-line textfield or a multi-line textarea.
+ * Textfields scroll horizontally, and do not handle the enter key.
+ * Textareas behave oppositely.
+ */
+ public void setSingleLine(boolean single) {
+ if (mSingle != single) {
+ TextKeyListener.Capitalize cap;
+ if (single) {
+ cap = TextKeyListener.Capitalize.NONE;
+ } else {
+ cap = TextKeyListener.Capitalize.SENTENCES;
+ }
+ setKeyListener(TextKeyListener.getInstance(!single, cap));
+ mSingle = single;
+ setHorizontallyScrolling(single);
+ }
+ }
+
+ /**
+ * Set the text for this TextDialog, and set the selection to (start, end)
+ * @param text Text to go into this TextDialog.
+ * @param start Beginning of the selection.
+ * @param end End of the selection.
+ */
+ /* package */ void setText(CharSequence text, int start, int end) {
+ mPreChange = text.toString();
+ setText(text);
+ Spannable span = (Spannable) getText();
+ int length = span.length();
+ if (end > length) {
+ end = length;
+ }
+ if (start < 0) {
+ start = 0;
+ } else if (start > length) {
+ start = length;
+ }
+ Selection.setSelection(span, start, end);
+ }
+
+ /**
+ * Set the text to the new string, but use the old selection, making sure
+ * to keep it within the new string.
+ * @param text The new text to place in the textfield.
+ */
+ /* package */ void setTextAndKeepSelection(String text) {
+ mPreChange = text.toString();
+ Editable edit = (Editable) getText();
+ edit.replace(0, edit.length(), text);
+ }
+
+ /**
+ * Update the cache to reflect the current text.
+ */
+ /* package */ void updateCachedTextfield() {
+ mWebView.updateCachedTextfield(getText().toString());
+ }
+}
diff --git a/core/java/android/webkit/URLUtil.java b/core/java/android/webkit/URLUtil.java
new file mode 100644
index 0000000..43666c1
--- /dev/null
+++ b/core/java/android/webkit/URLUtil.java
@@ -0,0 +1,362 @@
+/*
+ * 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.webkit;
+
+import java.io.UnsupportedEncodingException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import android.net.Uri;
+import android.net.ParseException;
+import android.net.WebAddress;
+import android.util.Config;
+import android.util.Log;
+
+public final class URLUtil {
+
+ private static final String LOGTAG = "webkit";
+
+ static final String ASSET_BASE = "file:///android_asset/";
+ static final String FILE_BASE = "file://";
+ static final String PROXY_BASE = "file:///cookieless_proxy/";
+
+ /**
+ * Cleans up (if possible) user-entered web addresses
+ */
+ public static String guessUrl(String inUrl) {
+
+ String retVal = inUrl;
+ WebAddress webAddress;
+
+ Log.v(LOGTAG, "guessURL before queueRequest: " + inUrl);
+
+ if (inUrl.length() == 0) return inUrl;
+ if (inUrl.startsWith("about:")) return inUrl;
+ // Do not try to interpret data scheme URLs
+ if (inUrl.startsWith("data:")) return inUrl;
+ // Do not try to interpret file scheme URLs
+ if (inUrl.startsWith("file:")) return inUrl;
+ // Do not try to interpret javascript scheme URLs
+ if (inUrl.startsWith("javascript:")) return inUrl;
+
+ // bug 762454: strip period off end of url
+ if (inUrl.endsWith(".") == true) {
+ inUrl = inUrl.substring(0, inUrl.length() - 1);
+ }
+
+ try {
+ webAddress = new WebAddress(inUrl);
+ } catch (ParseException ex) {
+
+ if (Config.LOGV) {
+ Log.v(LOGTAG, "smartUrlFilter: failed to parse url = " + inUrl);
+ }
+ return retVal;
+ }
+
+ // Check host
+ if (webAddress.mHost.indexOf('.') == -1) {
+ // no dot: user probably entered a bare domain. try .com
+ webAddress.mHost = "www." + webAddress.mHost + ".com";
+ }
+ return webAddress.toString();
+ }
+
+ public static String composeSearchUrl(String inQuery, String template,
+ String queryPlaceHolder) {
+ int placeHolderIndex = template.indexOf(queryPlaceHolder);
+ if (placeHolderIndex < 0) {
+ return null;
+ }
+
+ String query;
+ StringBuilder buffer = new StringBuilder();
+ buffer.append(template.substring(0, placeHolderIndex));
+
+ try {
+ query = java.net.URLEncoder.encode(inQuery, "utf-8");
+ buffer.append(query);
+ } catch (UnsupportedEncodingException ex) {
+ return null;
+ }
+
+ buffer.append(template.substring(
+ placeHolderIndex + queryPlaceHolder.length()));
+
+ return buffer.toString();
+ }
+
+ public static byte[] decode(byte[] url) throws IllegalArgumentException {
+ if (url.length == 0) {
+ return new byte[0];
+ }
+
+ // Create a new byte array with the same length to ensure capacity
+ byte[] tempData = new byte[url.length];
+
+ int tempCount = 0;
+ for (int i = 0; i < url.length; i++) {
+ byte b = url[i];
+ if (b == '%') {
+ if (url.length - i > 2) {
+ b = (byte) (parseHex(url[i + 1]) * 16
+ + parseHex(url[i + 2]));
+ i += 2;
+ } else {
+ throw new IllegalArgumentException("Invalid format");
+ }
+ }
+ tempData[tempCount++] = b;
+ }
+ byte[] retData = new byte[tempCount];
+ System.arraycopy(tempData, 0, retData, 0, tempCount);
+ return retData;
+ }
+
+ private static int parseHex(byte b) {
+ if (b >= '0' && b <= '9') return (b - '0');
+ if (b >= 'A' && b <= 'F') return (b - 'A' + 10);
+ if (b >= 'a' && b <= 'f') return (b - 'a' + 10);
+
+ throw new IllegalArgumentException("Invalid hex char '" + b + "'");
+ }
+
+ /**
+ * @return True iff the url is an asset file.
+ */
+ public static boolean isAssetUrl(String url) {
+ return (null != url) && url.startsWith(ASSET_BASE);
+ }
+
+ /**
+ * @return True iff the url is an proxy url to allow cookieless network
+ * requests from a file url.
+ */
+ public static boolean isCookielessProxyUrl(String url) {
+ return (null != url) && url.startsWith(PROXY_BASE);
+ }
+
+ /**
+ * @return True iff the url is a local file.
+ */
+ public static boolean isFileUrl(String url) {
+ return (null != url) && (url.startsWith(FILE_BASE) &&
+ !url.startsWith(ASSET_BASE) &&
+ !url.startsWith(PROXY_BASE));
+ }
+
+ /**
+ * @return True iff the url is an about: url.
+ */
+ public static boolean isAboutUrl(String url) {
+ return (null != url) && url.startsWith("about:");
+ }
+
+ /**
+ * @return True iff the url is a data: url.
+ */
+ public static boolean isDataUrl(String url) {
+ return (null != url) && url.startsWith("data:");
+ }
+
+ /**
+ * @return True iff the url is a javascript: url.
+ */
+ public static boolean isJavaScriptUrl(String url) {
+ return (null != url) && url.startsWith("javascript:");
+ }
+
+ /**
+ * @return True iff the url is an http: url.
+ */
+ public static boolean isHttpUrl(String url) {
+ return (null != url) &&
+ (url.length() > 6) &&
+ url.substring(0, 7).equalsIgnoreCase("http://");
+ }
+
+ /**
+ * @return True iff the url is an https: url.
+ */
+ public static boolean isHttpsUrl(String url) {
+ return (null != url) &&
+ (url.length() > 7) &&
+ url.substring(0, 8).equalsIgnoreCase("https://");
+ }
+
+ /**
+ * @return True iff the url is a network url.
+ */
+ public static boolean isNetworkUrl(String url) {
+ if (url == null || url.length() == 0) {
+ return false;
+ }
+ return isHttpUrl(url) || isHttpsUrl(url);
+ }
+
+ /**
+ * @return True iff the url is a content: url.
+ */
+ public static boolean isContentUrl(String url) {
+ return (null != url) && url.startsWith("content:");
+ }
+
+ /**
+ * @return True iff the url is valid.
+ */
+ public static boolean isValidUrl(String url) {
+ if (url == null || url.length() == 0) {
+ return false;
+ }
+
+ return (isAssetUrl(url) ||
+ isFileUrl(url) ||
+ isAboutUrl(url) ||
+ isHttpUrl(url) ||
+ isHttpsUrl(url) ||
+ isJavaScriptUrl(url) ||
+ isContentUrl(url));
+ }
+
+ /**
+ * Strips the url of the anchor.
+ */
+ public static String stripAnchor(String url) {
+ int anchorIndex = url.indexOf('#');
+ if (anchorIndex != -1) {
+ return url.substring(0, anchorIndex);
+ }
+ return url;
+ }
+
+ /**
+ * Guesses canonical filename that a download would have, using
+ * the URL and contentDisposition. File extension, if not defined,
+ * is added based on the mimetype
+ * @param url Url to the content
+ * @param contentDisposition Content-Disposition HTTP header or null
+ * @param mimeType Mime-type of the content or null
+ *
+ * @return suggested filename
+ */
+ public static final String guessFileName(
+ String url,
+ String contentDisposition,
+ String mimeType) {
+ String filename = null;
+ String extension = null;
+
+ // If we couldn't do anything with the hint, move toward the content disposition
+ if (filename == null && contentDisposition != null) {
+ filename = parseContentDisposition(contentDisposition);
+ if (filename != null) {
+ int index = filename.lastIndexOf('/') + 1;
+ if (index > 0) {
+ filename = filename.substring(index);
+ }
+ }
+ }
+
+ // If all the other http-related approaches failed, use the plain uri
+ if (filename == null) {
+ String decodedUrl = Uri.decode(url);
+ if (decodedUrl != null) {
+ int queryIndex = decodedUrl.indexOf('?');
+ // If there is a query string strip it, same as desktop browsers
+ if (queryIndex > 0) {
+ decodedUrl = decodedUrl.substring(0, queryIndex);
+ }
+ if (!decodedUrl.endsWith("/")) {
+ int index = decodedUrl.lastIndexOf('/') + 1;
+ if (index > 0) {
+ filename = decodedUrl.substring(index);
+ }
+ }
+ }
+ }
+
+ // Finally, if couldn't get filename from URI, get a generic filename
+ if (filename == null) {
+ filename = "downloadfile";
+ }
+
+ // Split filename between base and extension
+ // Add an extension if filename does not have one
+ int dotIndex = filename.indexOf('.');
+ if (dotIndex < 0) {
+ if (mimeType != null) {
+ extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
+ if (extension != null) {
+ extension = "." + extension;
+ }
+ }
+ if (extension == null) {
+ if (mimeType != null && mimeType.toLowerCase().startsWith("text/")) {
+ if (mimeType.equalsIgnoreCase("text/html")) {
+ extension = ".html";
+ } else {
+ extension = ".txt";
+ }
+ } else {
+ extension = ".bin";
+ }
+ }
+ } else {
+ if (mimeType != null) {
+ // Compare the last segment of the extension against the mime type.
+ // If there's a mismatch, discard the entire extension.
+ int lastDotIndex = filename.lastIndexOf('.');
+ String typeFromExt = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
+ filename.substring(lastDotIndex + 1));
+ if (typeFromExt != null && !typeFromExt.equalsIgnoreCase(mimeType)) {
+ extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
+ if (extension != null) {
+ extension = "." + extension;
+ }
+ }
+ }
+ if (extension == null) {
+ extension = filename.substring(dotIndex);
+ }
+ filename = filename.substring(0, dotIndex);
+ }
+
+ return filename + extension;
+ }
+
+ /** Regex used to parse content-disposition headers */
+ private static final Pattern CONTENT_DISPOSITION_PATTERN =
+ Pattern.compile("attachment;\\s*filename\\s*=\\s*\"([^\"]*)\"");
+
+ /*
+ * Parse the Content-Disposition HTTP Header. The format of the header
+ * is defined here: http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html
+ * This header provides a filename for content that is going to be
+ * downloaded to the file system. We only support the attachment type.
+ */
+ private static String parseContentDisposition(String contentDisposition) {
+ try {
+ Matcher m = CONTENT_DISPOSITION_PATTERN.matcher(contentDisposition);
+ if (m.find()) {
+ return m.group(1);
+ }
+ } catch (IllegalStateException ex) {
+ // This function is defined as returning null when it can't parse the header
+ }
+ return null;
+ }
+}
diff --git a/core/java/android/webkit/UrlInterceptHandler.java b/core/java/android/webkit/UrlInterceptHandler.java
new file mode 100644
index 0000000..e1c9d61
--- /dev/null
+++ b/core/java/android/webkit/UrlInterceptHandler.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.webkit;
+
+import android.webkit.CacheManager.CacheResult;
+import java.util.Map;
+
+public interface UrlInterceptHandler {
+
+ /**
+ * Given an URL, returns the CacheResult which contains the
+ * surrogate response for the request, or null if the handler is
+ * not interested.
+ *
+ * @param url URL string.
+ * @param headers The headers associated with the request. May be null.
+ * @return The CacheResult containing the surrogate response.
+ */
+ public CacheResult service(String url, Map<String, String> headers);
+}
diff --git a/core/java/android/webkit/UrlInterceptRegistry.java b/core/java/android/webkit/UrlInterceptRegistry.java
new file mode 100644
index 0000000..a218191
--- /dev/null
+++ b/core/java/android/webkit/UrlInterceptRegistry.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.webkit;
+
+import android.webkit.CacheManager.CacheResult;
+import android.webkit.UrlInterceptHandler;
+
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.Map;
+
+public final class UrlInterceptRegistry {
+
+ private final static String LOGTAG = "intercept";
+
+ private static boolean mDisabled = false;
+
+ private static LinkedList mHandlerList;
+
+ private static synchronized LinkedList getHandlers() {
+ if(mHandlerList == null)
+ mHandlerList = new LinkedList<UrlInterceptHandler>();
+ return mHandlerList;
+ }
+
+ /**
+ * set the flag to control whether url intercept is enabled or disabled
+ *
+ * @param disabled true to disable the cache
+ */
+ public static synchronized void setUrlInterceptDisabled(boolean disabled) {
+ mDisabled = disabled;
+ }
+
+ /**
+ * get the state of the url intercept, enabled or disabled
+ *
+ * @return return if it is disabled
+ */
+ public static synchronized boolean urlInterceptDisabled() {
+ return mDisabled;
+ }
+
+ /**
+ * Register a new UrlInterceptHandler. This handler will be called
+ * before any that were previously registered.
+ *
+ * @param handler The new UrlInterceptHandler object
+ * @return true if the handler was not previously registered.
+ */
+ public static synchronized boolean registerHandler(
+ UrlInterceptHandler handler) {
+ if (!getHandlers().contains(handler)) {
+ getHandlers().addFirst(handler);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Unregister a previously registered UrlInterceptHandler.
+ *
+ * @param handler A previously registered UrlInterceptHandler.
+ * @return true if the handler was found and removed from the list.
+ */
+ public static synchronized boolean unregisterHandler(
+ UrlInterceptHandler handler) {
+ return getHandlers().remove(handler);
+ }
+
+ /**
+ * Given an url, returns the CacheResult of the first
+ * UrlInterceptHandler interested, or null if none are.
+ *
+ * @return A CacheResult containing surrogate content.
+ */
+ public static synchronized CacheResult getSurrogate(
+ String url, Map<String, String> headers) {
+ if (urlInterceptDisabled())
+ return null;
+ Iterator iter = getHandlers().listIterator();
+ while (iter.hasNext()) {
+ UrlInterceptHandler handler = (UrlInterceptHandler) iter.next();
+ CacheResult result = handler.service(url, headers);
+ if (result != null) {
+ return result;
+ }
+ }
+ return null;
+ }
+}
diff --git a/core/java/android/webkit/WebBackForwardList.java b/core/java/android/webkit/WebBackForwardList.java
new file mode 100644
index 0000000..c86b21d
--- /dev/null
+++ b/core/java/android/webkit/WebBackForwardList.java
@@ -0,0 +1,188 @@
+/*
+ * 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.webkit;
+
+import android.util.Config;
+import java.io.Serializable;
+import java.util.ArrayList;
+
+/**
+ * This class contains the back/forward list for a WebView.
+ * WebView.copyBackForwardList() will return a copy of this class used to
+ * inspect the entries in the list.
+ */
+public class WebBackForwardList implements Cloneable, Serializable {
+ // Current position in the list.
+ private int mCurrentIndex;
+ // ArrayList of WebHistoryItems for maintaining our copy.
+ private ArrayList<WebHistoryItem> mArray;
+ // Flag to indicate that the list is invalid
+ private boolean mClearPending;
+
+ /**
+ * Construct a back/forward list used by clients of WebView.
+ */
+ /*package*/ WebBackForwardList() {
+ mCurrentIndex = -1;
+ mArray = new ArrayList<WebHistoryItem>();
+ }
+
+ /**
+ * Return the current history item. This method returns null if the list is
+ * empty.
+ * @return The current history item.
+ */
+ public synchronized WebHistoryItem getCurrentItem() {
+ return getItemAtIndex(mCurrentIndex);
+ }
+
+ /**
+ * Get the index of the current history item. This index can be used to
+ * directly index into the array list.
+ * @return The current index from 0...n or -1 if the list is empty.
+ */
+ public synchronized int getCurrentIndex() {
+ return mCurrentIndex;
+ }
+
+ /**
+ * Get the history item at the given index. The index range is from 0...n
+ * where 0 is the first item and n is the last item.
+ * @param index The index to retrieve.
+ */
+ public synchronized WebHistoryItem getItemAtIndex(int index) {
+ if (index < 0 || index >= getSize()) {
+ return null;
+ }
+ return mArray.get(index);
+ }
+
+ /**
+ * Get the total size of the back/forward list.
+ * @return The size of the list.
+ */
+ public synchronized int getSize() {
+ return mArray.size();
+ }
+
+ /**
+ * Mark the back/forward list as having a pending clear. This is used on the
+ * UI side to mark the list as being invalid during the clearHistory method.
+ */
+ /*package*/ synchronized void setClearPending() {
+ mClearPending = true;
+ }
+
+ /**
+ * Return the status of the clear flag. This is used on the UI side to
+ * determine if the list is valid for checking things like canGoBack.
+ */
+ /*package*/ synchronized boolean getClearPending() {
+ return mClearPending;
+ }
+
+ /**
+ * Add a new history item to the list. This will remove all items after the
+ * current item and append the new item to the end of the list. Called from
+ * the WebCore thread only. Synchronized because the UI thread may be
+ * reading the array or the current index.
+ * @param item A new history item.
+ */
+ /*package*/ synchronized void addHistoryItem(WebHistoryItem item) {
+ // Update the current position because we are going to add the new item
+ // in that slot.
+ ++mCurrentIndex;
+ // If the current position is not at the end, remove all history items
+ // after the current item.
+ final int size = mArray.size();
+ final int newPos = mCurrentIndex;
+ if (newPos != size) {
+ for (int i = size - 1; i >= newPos; i--) {
+ final WebHistoryItem h = mArray.remove(i);
+ }
+ }
+ // Add the item to the list.
+ mArray.add(item);
+ }
+
+ /**
+ * Clear the back/forward list. Called from the WebCore thread.
+ */
+ /*package*/ synchronized void close(int nativeFrame) {
+ // Clear the array first because nativeClose will call addHistoryItem
+ // with the current item.
+ mArray.clear();
+ mCurrentIndex = -1;
+ nativeClose(nativeFrame);
+ // Reset the clear flag
+ mClearPending = false;
+ }
+
+ /* Remove the item at the given index. Called by JNI only. */
+ private void removeHistoryItem(int index) {
+ // XXX: This is a special case. Since the callback is only triggered
+ // when removing the first item, we can assert that the index is 0.
+ // This lets us change the current index without having to query the
+ // native BackForwardList.
+ if (Config.DEBUG && (index != 0)) {
+ throw new AssertionError();
+ }
+ final WebHistoryItem h = mArray.remove(index);
+ // XXX: If we ever add another callback for removing history items at
+ // any index, this will no longer be valid.
+ mCurrentIndex--;
+ }
+
+ /**
+ * Clone the entire object to be used in the UI thread by clients of
+ * WebView. This creates a copy that should never be modified by any of the
+ * webkit package classes.
+ */
+ protected synchronized WebBackForwardList clone() {
+ WebBackForwardList l = new WebBackForwardList();
+ if (mClearPending) {
+ // If a clear is pending, return a copy with only the current item.
+ l.addHistoryItem(getCurrentItem());
+ return l;
+ }
+ l.mCurrentIndex = mCurrentIndex;
+ int size = getSize();
+ l.mArray = new ArrayList<WebHistoryItem>(size);
+ for (int i = 0; i < size; i++) {
+ // Add a copy of each WebHistoryItem
+ l.mArray.add(mArray.get(i).clone());
+ }
+ return l;
+ }
+
+ /**
+ * Set the new history index.
+ * @param newIndex The new history index.
+ */
+ /*package*/ synchronized void setCurrentIndex(int newIndex) {
+ mCurrentIndex = newIndex;
+ }
+
+ /**
+ * Restore the history index.
+ */
+ /*package*/ static native synchronized void restoreIndex(int nativeFrame,
+ int index);
+
+ /* Close the native list. */
+ private static native void nativeClose(int nativeFrame);
+}
diff --git a/core/java/android/webkit/WebChromeClient.java b/core/java/android/webkit/WebChromeClient.java
new file mode 100644
index 0000000..f940006
--- /dev/null
+++ b/core/java/android/webkit/WebChromeClient.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.webkit;
+
+import android.graphics.Bitmap;
+import android.os.Message;
+
+public class WebChromeClient {
+
+ /**
+ * Tell the host application the current progress of loading a page.
+ * @param view The WebView that initiated the callback.
+ * @param newProgress Current page loading progress, represented by
+ * an integer between 0 and 100.
+ */
+ public void onProgressChanged(WebView view, int newProgress) {}
+
+ /**
+ * Notify the host application of a change in the document title.
+ * @param view The WebView that initiated the callback.
+ * @param title A String containing the new title of the document.
+ */
+ public void onReceivedTitle(WebView view, String title) {}
+
+ /**
+ * Notify the host application of a new favicon for the current page.
+ * @param view The WebView that initiated the callback.
+ * @param icon A Bitmap containing the favicon for the current page.
+ */
+ public void onReceivedIcon(WebView view, Bitmap icon) {}
+
+ /**
+ * Request the host application to create a new Webview. The host
+ * application should handle placement of the new WebView in the view
+ * system. The default behavior returns null.
+ * @param view The WebView that initiated the callback.
+ * @param dialog True if the new window is meant to be a small dialog
+ * window.
+ * @param userGesture True if the request was initiated by a user gesture
+ * such as clicking a link.
+ * @param resultMsg The message to send when done creating a new WebView.
+ * Set the new WebView through resultMsg.obj which is
+ * WebView.WebViewTransport() and then call
+ * resultMsg.sendToTarget();
+ * @return Similar to javscript dialogs, this method should return true if
+ * the client is going to handle creating a new WebView. Note that
+ * the WebView will halt processing if this method returns true so
+ * make sure to call resultMsg.sendToTarget(). It is undefined
+ * behavior to call resultMsg.sendToTarget() after returning false
+ * from this method.
+ */
+ public boolean onCreateWindow(WebView view, boolean dialog,
+ boolean userGesture, Message resultMsg) {
+ return false;
+ }
+
+ /**
+ * Request display and focus for this WebView. This may happen due to
+ * another WebView opening a link in this WebView and requesting that this
+ * WebView be displayed.
+ * @param view The WebView that needs to be focused.
+ */
+ public void onRequestFocus(WebView view) {}
+
+ /**
+ * Notify the host application to close the given WebView and remove it
+ * from the view system if necessary. At this point, WebCore has stopped
+ * any loading in this window and has removed any cross-scripting ability
+ * in javascript.
+ * @param window The WebView that needs to be closed.
+ */
+ public void onCloseWindow(WebView window) {}
+
+ /**
+ * Tell the client to display a javascript alert dialog. If the client
+ * returns true, WebView will assume that the client will handle the
+ * dialog. If the client returns false, it will continue execution.
+ * @param view The WebView that initiated the callback.
+ * @param url The url of the page requesting the dialog.
+ * @param message Message to be displayed in the window.
+ * @param result A JsResult to confirm that the user hit enter.
+ * @return boolean Whether the client will handle the alert dialog.
+ */
+ public boolean onJsAlert(WebView view, String url, String message,
+ JsResult result) {
+ return false;
+ }
+
+ /**
+ * Tell the client to display a confirm dialog to the user. If the client
+ * returns true, WebView will assume that the client will handle the
+ * confirm dialog and call the appropriate JsResult method. If the
+ * client returns false, a default value of false will be returned to
+ * javascript. The default behavior is to return false.
+ * @param view The WebView that initiated the callback.
+ * @param url The url of the page requesting the dialog.
+ * @param message Message to be displayed in the window.
+ * @param result A JsResult used to send the user's response to
+ * javascript.
+ * @return boolean Whether the client will handle the confirm dialog.
+ */
+ public boolean onJsConfirm(WebView view, String url, String message,
+ JsResult result) {
+ return false;
+ }
+
+ /**
+ * Tell the client to display a prompt dialog to the user. If the client
+ * returns true, WebView will assume that the client will handle the
+ * prompt dialog and call the appropriate JsPromptResult method. If the
+ * client returns false, a default value of false will be returned to to
+ * javascript. The default behavior is to return false.
+ * @param view The WebView that initiated the callback.
+ * @param url The url of the page requesting the dialog.
+ * @param message Message to be displayed in the window.
+ * @param defaultValue The default value displayed in the prompt dialog.
+ * @param result A JsPromptResult used to send the user's reponse to
+ * javascript.
+ * @return boolean Whether the client will handle the prompt dialog.
+ */
+ public boolean onJsPrompt(WebView view, String url, String message,
+ String defaultValue, JsPromptResult result) {
+ return false;
+ }
+
+ /**
+ * Tell the client to display a dialog to confirm navigation away from the
+ * current page. This is the result of the onbeforeunload javascript event.
+ * If the client returns true, WebView will assume that the client will
+ * handle the confirm dialog and call the appropriate JsResult method. If
+ * the client returns false, a default value of true will be returned to
+ * javascript to accept navigation away from the current page. The default
+ * behavior is to return false. Setting the JsResult to true will navigate
+ * away from the current page, false will cancel the navigation.
+ * @param view The WebView that initiated the callback.
+ * @param url The url of the page requesting the dialog.
+ * @param message Message to be displayed in the window.
+ * @param result A JsResult used to send the user's response to
+ * javascript.
+ * @return boolean Whether the client will handle the confirm dialog.
+ */
+ public boolean onJsBeforeUnload(WebView view, String url, String message,
+ JsResult result) {
+ return false;
+ }
+}
diff --git a/core/java/android/webkit/WebHistoryItem.java b/core/java/android/webkit/WebHistoryItem.java
new file mode 100644
index 0000000..5570af8
--- /dev/null
+++ b/core/java/android/webkit/WebHistoryItem.java
@@ -0,0 +1,163 @@
+/*
+ * 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.webkit;
+
+import android.graphics.Bitmap;
+
+/**
+ * A convenience class for accessing fields in an entry in the back/forward list
+ * of a WebView. Each WebHistoryItem is a snapshot of the requested history
+ * item. Each history item may be updated during the load of a page.
+ * @see WebBackForwardList
+ */
+public class WebHistoryItem implements Cloneable {
+ // Global identifier count.
+ private static int sNextId = 0;
+ // Unique identifier.
+ private final int mId;
+ // The title of this item's document.
+ private String mTitle;
+ // The base url of this item.
+ private String mUrl;
+ // The favicon for this item.
+ private Bitmap mFavicon;
+ // The pre-flattened data used for saving the state.
+ private byte[] mFlattenedData;
+
+ /**
+ * Basic constructor that assigns a unique id to the item. Called by JNI
+ * only.
+ */
+ private WebHistoryItem() {
+ synchronized (WebHistoryItem.class) {
+ mId = sNextId++;
+ }
+ }
+
+ /**
+ * Construct a new WebHistoryItem with initial flattened data.
+ * @param data The pre-flattened data coming from restoreState.
+ */
+ /*package*/ WebHistoryItem(byte[] data) {
+ mUrl = null; // This will be updated natively
+ mFlattenedData = data;
+ synchronized (WebHistoryItem.class) {
+ mId = sNextId++;
+ }
+ }
+
+ /**
+ * Construct a clone of a WebHistoryItem from the given item.
+ * @param item The history item to clone.
+ */
+ private WebHistoryItem(WebHistoryItem item) {
+ mUrl = item.mUrl;
+ mTitle = item.mTitle;
+ mFlattenedData = item.mFlattenedData;
+ mFavicon = item.mFavicon;
+ mId = item.mId;
+}
+
+ /**
+ * Return an identifier for this history item. If an item is a copy of
+ * another item, the identifiers will be the same even if they are not the
+ * same object.
+ * @return The id for this item.
+ */
+ public int getId() {
+ return mId;
+ }
+
+ /**
+ * Return the url of this history item. The url is the base url of this
+ * history item. See getTargetUrl() for the url that is the actual target of
+ * this history item.
+ * @return The base url of this history item.
+ * Note: The VM ensures 32-bit atomic read/write operations so we don't have
+ * to synchronize this method.
+ */
+ public String getUrl() {
+ return mUrl;
+ }
+
+ /**
+ * Return the document title of this history item.
+ * @return The document title of this history item.
+ * Note: The VM ensures 32-bit atomic read/write operations so we don't have
+ * to synchronize this method.
+ */
+ public String getTitle() {
+ return mTitle;
+ }
+
+ /**
+ * Return the favicon of this history item or null if no favicon was found.
+ * @return A Bitmap containing the favicon for this history item or null.
+ * Note: The VM ensures 32-bit atomic read/write operations so we don't have
+ * to synchronize this method.
+ */
+ public Bitmap getFavicon() {
+ return mFavicon;
+ }
+
+ /**
+ * Set the favicon.
+ * @param icon A Bitmap containing the favicon for this history item.
+ * Note: The VM ensures 32-bit atomic read/write operations so we don't have
+ * to synchronize this method.
+ */
+ /*package*/ void setFavicon(Bitmap icon) {
+ mFavicon = icon;
+ }
+
+ /**
+ * Get the pre-flattened data.
+ * Note: The VM ensures 32-bit atomic read/write operations so we don't have
+ * to synchronize this method.
+ */
+ /*package*/ byte[] getFlattenedData() {
+ return mFlattenedData;
+ }
+
+ /**
+ * Inflate this item.
+ * Note: The VM ensures 32-bit atomic read/write operations so we don't have
+ * to synchronize this method.
+ */
+ /*package*/ void inflate(int nativeFrame) {
+ inflate(nativeFrame, mFlattenedData);
+ }
+
+ /**
+ * Clone the history item for use by clients of WebView.
+ */
+ protected synchronized WebHistoryItem clone() {
+ return new WebHistoryItem(this);
+ }
+
+ /* Natively inflate this item, this method is called in the WebCore thread.
+ */
+ private native void inflate(int nativeFrame, byte[] data);
+
+ /* Called by jni when the item is updated */
+ private void update(String url, String title, Bitmap favicon, byte[] data) {
+ mUrl = url;
+ mTitle = title;
+ mFavicon = favicon;
+ mFlattenedData = data;
+ }
+}
diff --git a/core/java/android/webkit/WebIconDatabase.java b/core/java/android/webkit/WebIconDatabase.java
new file mode 100644
index 0000000..d284f5e
--- /dev/null
+++ b/core/java/android/webkit/WebIconDatabase.java
@@ -0,0 +1,251 @@
+/*
+ * 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.webkit;
+
+import android.os.Handler;
+import android.os.Message;
+import android.graphics.Bitmap;
+
+import java.util.Vector;
+
+/**
+ * Functions for manipulating the icon database used by WebView.
+ * These functions require that a WebView be constructed before being invoked
+ * and WebView.getIconDatabase() will return a WebIconDatabase object. This
+ * WebIconDatabase object is a single instance and all methods operate on that
+ * single object.
+ */
+public final class WebIconDatabase {
+ // Global instance of a WebIconDatabase
+ private static WebIconDatabase sIconDatabase;
+ // EventHandler for handling messages before and after the WebCore thread is
+ // ready.
+ private final EventHandler mEventHandler = new EventHandler();
+
+ // Class to handle messages before WebCore is ready
+ private class EventHandler extends Handler {
+ // Message ids
+ static final int OPEN = 0;
+ static final int CLOSE = 1;
+ static final int REMOVE_ALL = 2;
+ static final int REQUEST_ICON = 3;
+ static final int RETAIN_ICON = 4;
+ static final int RELEASE_ICON = 5;
+ // Message for dispatching icon request results
+ private static final int ICON_RESULT = 10;
+ // Actual handler that runs in WebCore thread
+ private Handler mHandler;
+ // Vector of messages before the WebCore thread is ready
+ private Vector<Message> mMessages = new Vector<Message>();
+ // Class to handle a result dispatch
+ private class IconResult {
+ private final String mUrl;
+ private final Bitmap mIcon;
+ private final IconListener mListener;
+ IconResult(String url, Bitmap icon, IconListener l) {
+ mUrl = url;
+ mIcon = icon;
+ mListener = l;
+ }
+ void dispatch() {
+ mListener.onReceivedIcon(mUrl, mIcon);
+ }
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ // Note: This is the message handler for the UI thread.
+ switch (msg.what) {
+ case ICON_RESULT:
+ ((IconResult) msg.obj).dispatch();
+ break;
+ }
+ }
+
+ // Called by WebCore thread to create the actual handler
+ private synchronized void createHandler() {
+ if (mHandler == null) {
+ mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ // Note: This is the message handler for the WebCore
+ // thread.
+ switch (msg.what) {
+ case OPEN:
+ nativeOpen((String) msg.obj);
+ break;
+
+ case CLOSE:
+ nativeClose();
+ break;
+
+ case REMOVE_ALL:
+ nativeRemoveAllIcons();
+ break;
+
+ case REQUEST_ICON:
+ IconListener l = (IconListener) msg.obj;
+ String url = msg.getData().getString("url");
+ Bitmap icon = nativeIconForPageUrl(url);
+ if (icon != null) {
+ EventHandler.this.sendMessage(
+ Message.obtain(null, ICON_RESULT,
+ new IconResult(url, icon, l)));
+ }
+ break;
+
+ case RETAIN_ICON:
+ nativeRetainIconForPageUrl((String) msg.obj);
+ break;
+
+ case RELEASE_ICON:
+ nativeReleaseIconForPageUrl((String) msg.obj);
+ break;
+ }
+ }
+ };
+ // Transfer all pending messages
+ for (int size = mMessages.size(); size > 0; size--) {
+ mHandler.sendMessage(mMessages.remove(0));
+ }
+ mMessages = null;
+ }
+ }
+
+ private synchronized void postMessage(Message msg) {
+ if (mMessages != null) {
+ mMessages.add(msg);
+ } else {
+ mHandler.sendMessage(msg);
+ }
+ }
+ }
+
+ /**
+ * Interface for receiving icons from the database.
+ */
+ public interface IconListener {
+ /**
+ * Called when the icon has been retrieved from the database and the
+ * result is non-null.
+ * @param url The url passed in the request.
+ * @param icon The favicon for the given url.
+ */
+ public void onReceivedIcon(String url, Bitmap icon);
+ }
+
+ /**
+ * Open a the icon database and store the icons in the given path.
+ * @param path The directory path where the icon database will be stored.
+ * @return True if the database was successfully opened or created in
+ * the given path.
+ */
+ public void open(String path) {
+ if (path != null) {
+ mEventHandler.postMessage(
+ Message.obtain(null, EventHandler.OPEN, path));
+ }
+ }
+
+ /**
+ * Close the shared instance of the icon database.
+ */
+ public void close() {
+ mEventHandler.postMessage(
+ Message.obtain(null, EventHandler.CLOSE));
+ }
+
+ /**
+ * Removes all the icons in the database.
+ */
+ public void removeAllIcons() {
+ mEventHandler.postMessage(
+ Message.obtain(null, EventHandler.REMOVE_ALL));
+ }
+
+ /**
+ * Request the Bitmap representing the icon for the given page
+ * url. If the icon exists, the listener will be called with the result.
+ * @param url The page's url.
+ * @param listener An implementation on IconListener to receive the result.
+ */
+ public void requestIconForPageUrl(String url, IconListener listener) {
+ if (listener == null || url == null) {
+ return;
+ }
+ Message msg = Message.obtain(null, EventHandler.REQUEST_ICON, listener);
+ msg.getData().putString("url", url);
+ mEventHandler.postMessage(msg);
+ }
+
+ /**
+ * Retain the icon for the given page url.
+ * @param url The page's url.
+ */
+ public void retainIconForPageUrl(String url) {
+ if (url != null) {
+ mEventHandler.postMessage(
+ Message.obtain(null, EventHandler.RETAIN_ICON, url));
+ }
+ }
+
+ /**
+ * Release the icon for the given page url.
+ * @param url The page's url.
+ */
+ public void releaseIconForPageUrl(String url) {
+ if (url != null) {
+ mEventHandler.postMessage(
+ Message.obtain(null, EventHandler.RELEASE_ICON, url));
+ }
+ }
+
+ /**
+ * Get the global instance of WebIconDatabase.
+ * @return A single instance of WebIconDatabase. It will be the same
+ * instance for the current process each time this method is
+ * called.
+ */
+ public static WebIconDatabase getInstance() {
+ // XXX: Must be created in the UI thread.
+ if (sIconDatabase == null) {
+ sIconDatabase = new WebIconDatabase();
+ }
+ return sIconDatabase;
+ }
+
+ /**
+ * Create the internal handler and transfer all pending messages.
+ * XXX: Called by WebCore thread only!
+ */
+ /*package*/ void createHandler() {
+ mEventHandler.createHandler();
+ }
+
+ /**
+ * Private constructor to avoid anyone else creating an instance.
+ */
+ private WebIconDatabase() {}
+
+ // Native functions
+ private static native void nativeOpen(String path);
+ private static native void nativeClose();
+ private static native void nativeRemoveAllIcons();
+ private static native Bitmap nativeIconForPageUrl(String url);
+ private static native void nativeRetainIconForPageUrl(String url);
+ private static native void nativeReleaseIconForPageUrl(String url);
+}
diff --git a/core/java/android/webkit/WebSettings.java b/core/java/android/webkit/WebSettings.java
new file mode 100644
index 0000000..de64b30
--- /dev/null
+++ b/core/java/android/webkit/WebSettings.java
@@ -0,0 +1,903 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.webkit;
+
+import android.content.Context;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Message;
+
+import java.util.Locale;
+
+/**
+ * Manages settings state for a WebView. When a WebView is first created, it
+ * obtains a set of default settings. These default settings will be returned
+ * from any getter call. A WebSettings object obtained from
+ * WebView.getSettings() is tied to the life of the WebView. If a WebView has
+ * been destroyed, any method call on WebSettings will throw an
+ * IllegalStateException.
+ */
+public class WebSettings {
+ /**
+ * Enum for controlling the layout of html.
+ * NORMAL means no rendering changes.
+ * SINGLE_COLUMN moves all content into one column that is the width of the
+ * view.
+ * NARROW_COLUMNS makes all columns no wider than the screen if possible.
+ */
+ // XXX: These must match LayoutAlgorithm in Settings.h in WebCore.
+ public enum LayoutAlgorithm {
+ NORMAL,
+ SINGLE_COLUMN,
+ NARROW_COLUMNS
+ }
+
+ /**
+ * Enum for specifying the text size.
+ * SMALLEST is 50%
+ * SMALLER is 75%
+ * NORMAL is 100%
+ * LARGER is 150%
+ * LARGEST is 200%
+ */
+ public enum TextSize {
+ SMALLEST(50),
+ SMALLER(75),
+ NORMAL(100),
+ LARGER(150),
+ LARGEST(200);
+ TextSize(int size) {
+ value = size;
+ }
+ int value;
+ }
+
+ /**
+ * Default cache usage pattern Use with {@link #setCacheMode}.
+ */
+ public static final int LOAD_DEFAULT = -1;
+
+ /**
+ * Normal cache usage pattern Use with {@link #setCacheMode}.
+ */
+ public static final int LOAD_NORMAL = 0;
+
+ /**
+ * Use cache if content is there, even if expired (eg, history nav)
+ * If it is not in the cache, load from network.
+ * Use with {@link #setCacheMode}.
+ */
+ public static final int LOAD_CACHE_ELSE_NETWORK = 1;
+
+ /**
+ * Don't use the cache, load from network
+ * Use with {@link #setCacheMode}.
+ */
+ public static final int LOAD_NO_CACHE = 2;
+
+ /**
+ * Don't use the network, load from cache only.
+ * Use with {@link #setCacheMode}.
+ */
+ public static final int LOAD_CACHE_ONLY = 3;
+
+ public enum RenderPriority {
+ NORMAL,
+ HIGH,
+ LOW
+ }
+
+ // BrowserFrame used to access the native frame pointer.
+ private BrowserFrame mBrowserFrame;
+ // Flag to prevent multiple SYNC messages at one time.
+ private boolean mSyncPending = false;
+ // Custom handler that queues messages until the WebCore thread is active.
+ private final EventHandler mEventHandler;
+ // Private settings so we don't have to go into native code to
+ // retrieve the values. After setXXX, postSync() needs to be called.
+ // XXX: The default values need to match those in WebSettings.cpp
+ private LayoutAlgorithm mLayoutAlgorithm = LayoutAlgorithm.NARROW_COLUMNS;
+ private TextSize mTextSize = TextSize.NORMAL;
+ private String mStandardFontFamily = "sans-serif";
+ private String mFixedFontFamily = "monospace";
+ private String mSansSerifFontFamily = "sans-serif";
+ private String mSerifFontFamily = "serif";
+ private String mCursiveFontFamily = "cursive";
+ private String mFantasyFontFamily = "fantasy";
+ private String mDefaultTextEncoding = "Latin-1";
+ private String mUserAgent = ANDROID_USERAGENT;
+ private String mPluginsPath = "";
+ private int mMinimumFontSize = 8;
+ private int mMinimumLogicalFontSize = 8;
+ private int mDefaultFontSize = 16;
+ private int mDefaultFixedFontSize = 13;
+ private boolean mLoadsImagesAutomatically = true;
+ private boolean mBlockNetworkImage = false;
+ private boolean mJavaScriptEnabled = false;
+ private boolean mPluginsEnabled = false;
+ private boolean mJavaScriptCanOpenWindowsAutomatically = false;
+ private boolean mUseDoubleTree = false;
+ private boolean mUseWideViewport = false;
+ private boolean mSupportMultipleWindows = false;
+ // Don't need to synchronize the get/set methods as they
+ // are basic types, also none of these values are used in
+ // native WebCore code.
+ private RenderPriority mRenderPriority = RenderPriority.NORMAL;
+ private int mOverrideCacheMode = LOAD_DEFAULT;
+ private boolean mSaveFormData = true;
+ private boolean mSavePassword = true;
+ private boolean mLightTouchEnabled = false;
+ private boolean mNeedInitialFocus = true;
+ private boolean mNavDump = false;
+ private boolean mSupportZoom = true;
+
+ // Class to handle messages before WebCore is ready.
+ private class EventHandler {
+ // Message id for syncing
+ static final int SYNC = 0;
+ // Message id for setting priority
+ static final int PRIORITY = 1;
+ // Actual WebCore thread handler
+ private Handler mHandler;
+
+ private synchronized void createHandler() {
+ // as mRenderPriority can be set before thread is running, sync up
+ setRenderPriority();
+
+ // create a new handler
+ mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case SYNC:
+ synchronized (WebSettings.this) {
+ if (mBrowserFrame.mNativeFrame != 0) {
+ nativeSync(mBrowserFrame.mNativeFrame);
+ }
+ mSyncPending = false;
+ }
+ break;
+
+ case PRIORITY: {
+ setRenderPriority();
+ break;
+ }
+ }
+ }
+ };
+ }
+
+ private void setRenderPriority() {
+ synchronized (WebSettings.this) {
+ if (mRenderPriority == RenderPriority.NORMAL) {
+ android.os.Process.setThreadPriority(
+ android.os.Process.THREAD_PRIORITY_DEFAULT);
+ } else if (mRenderPriority == RenderPriority.HIGH) {
+ android.os.Process.setThreadPriority(
+ android.os.Process.THREAD_PRIORITY_FOREGROUND +
+ android.os.Process.THREAD_PRIORITY_LESS_FAVORABLE);
+ } else if (mRenderPriority == RenderPriority.LOW) {
+ android.os.Process.setThreadPriority(
+ android.os.Process.THREAD_PRIORITY_BACKGROUND);
+ }
+ }
+ }
+
+ /**
+ * Send a message to the private queue or handler.
+ */
+ private synchronized boolean sendMessage(Message msg) {
+ if (mHandler != null) {
+ mHandler.sendMessage(msg);
+ return true;
+ } else {
+ return false;
+ }
+ }
+ }
+
+ // User agent strings.
+ private static final String DESKTOP_USERAGENT =
+ "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en) AppleWebKit/522+ " +
+ "(KHTML, like Gecko) Safari/419.3";
+ private static final String IPHONE_USERAGENT = "Mozilla/5.0 (iPhone; U; " +
+ "CPU like Mac OS X; en) AppleWebKit/420+ (KHTML, like Gecko) " +
+ "Version/3.0 Mobile/1A543 Safari/419.3";
+ private static String ANDROID_USERAGENT;
+
+ /**
+ * Package constructor to prevent clients from creating a new settings
+ * instance.
+ */
+ WebSettings(Context context) {
+ if (ANDROID_USERAGENT == null) {
+ StringBuffer arg = new StringBuffer();
+ // Add version
+ final String version = Build.VERSION.RELEASE;
+ if (version.length() > 0) {
+ arg.append(version);
+ } else {
+ // default to "1.0"
+ arg.append("1.0");
+ }
+ arg.append("; ");
+ // Initialize the mobile user agent with the default locale.
+ final Locale l = Locale.getDefault();
+ final String language = l.getLanguage();
+ if (language != null) {
+ arg.append(language.toLowerCase());
+ final String country = l.getCountry();
+ if (country != null) {
+ arg.append("-");
+ arg.append(country.toLowerCase());
+ }
+ } else {
+ // default to "en"
+ arg.append("en");
+ }
+ // Add device name
+ final String device = Build.DEVICE;
+ if (device.length() > 0) {
+ arg.append("; ");
+ arg.append(device);
+ }
+ final String base = context.getResources().getText(
+ com.android.internal.R.string.web_user_agent).toString();
+ ANDROID_USERAGENT = String.format(base, arg);
+ mUserAgent = ANDROID_USERAGENT;
+ }
+ mEventHandler = new EventHandler();
+ }
+
+ /**
+ * Enables dumping the pages navigation cache to a text file.
+ */
+ public void setNavDump(boolean enabled) {
+ mNavDump = enabled;
+ }
+
+ /**
+ * Returns true if dumping the navigation cache is enabled.
+ */
+ public boolean getNavDump() {
+ return mNavDump;
+ }
+
+ /**
+ * Set whether the WebView supports zoom
+ */
+ public void setSupportZoom(boolean support) {
+ mSupportZoom = support;
+ }
+
+ /**
+ * Returns whether the WebView supports zoom
+ */
+ public boolean supportZoom() {
+ return mSupportZoom;
+ }
+
+ /**
+ * Store whether the WebView is saving form data.
+ */
+ public void setSaveFormData(boolean save) {
+ mSaveFormData = save;
+ }
+
+ /**
+ * Return whether the WebView is saving form data.
+ */
+ public boolean getSaveFormData() {
+ return mSaveFormData;
+ }
+
+ /**
+ * Store whether the WebView is saving password.
+ */
+ public void setSavePassword(boolean save) {
+ mSavePassword = save;
+ }
+
+ /**
+ * Return whether the WebView is saving password.
+ */
+ public boolean getSavePassword() {
+ return mSavePassword;
+ }
+
+ /**
+ * Set the text size of the page.
+ * @param t A TextSize value for increasing or decreasing the text.
+ * @see WebSettings.TextSize
+ */
+ public synchronized void setTextSize(TextSize t) {
+ mTextSize = t;
+ postSync();
+ }
+
+ /**
+ * Get the text size of the page.
+ * @return A TextSize enum value describing the text size.
+ * @see WebSettings.TextSize
+ */
+ public synchronized TextSize getTextSize() {
+ return mTextSize;
+ }
+
+ /**
+ * Enables using light touches to make a selection and activate mouseovers.
+ */
+ public void setLightTouchEnabled(boolean enabled) {
+ mLightTouchEnabled = enabled;
+ }
+
+ /**
+ * Returns true if light touches are enabled.
+ */
+ public boolean getLightTouchEnabled() {
+ return mLightTouchEnabled;
+ }
+
+ /**
+ * Tell the WebView to use the double tree rendering algorithm.
+ * @param use True if the WebView is to use double tree rendering, false
+ * otherwise.
+ */
+ public synchronized void setUseDoubleTree(boolean use) {
+ if (mUseDoubleTree != use) {
+ mUseDoubleTree = use;
+ postSync();
+ }
+ }
+
+ /**
+ * Return true if the WebView is using the double tree rendering algorithm.
+ * @return True if the WebView is using the double tree rendering
+ * algorithm.
+ */
+ public synchronized boolean getUseDoubleTree() {
+ return mUseDoubleTree;
+ }
+
+ /**
+ * Tell the WebView about user-agent string.
+ * @param ua 0 if the WebView should use an Android user-agent string,
+ * 1 if the WebView should use a desktop user-agent string.
+ * 2 if the WebView should use an iPhone user-agent string.
+ */
+ public synchronized void setUserAgent(int ua) {
+ if (ua == 0 && !ANDROID_USERAGENT.equals(mUserAgent)) {
+ mUserAgent = ANDROID_USERAGENT;
+ postSync();
+ } else if (ua == 1 && !DESKTOP_USERAGENT.equals(mUserAgent)) {
+ mUserAgent = DESKTOP_USERAGENT;
+ postSync();
+ } else if (ua == 2 && !IPHONE_USERAGENT.equals(mUserAgent)) {
+ mUserAgent = IPHONE_USERAGENT;
+ postSync();
+ }
+ }
+
+ /**
+ * Return user-agent as int
+ * @return int 0 if the WebView is using an Android user-agent string.
+ * 1 if the WebView is using a desktop user-agent string.
+ * 2 if the WebView is using an iPhone user-agent string.
+ */
+ public synchronized int getUserAgent() {
+ if (DESKTOP_USERAGENT.equals(mUserAgent)) {
+ return 1;
+ } else if (IPHONE_USERAGENT.equals(mUserAgent)) {
+ return 2;
+ }
+ return 0;
+ }
+
+ /**
+ * Tell the WebView to use the wide viewport
+ */
+ public synchronized void setUseWideViewPort(boolean use) {
+ if (mUseWideViewport != use) {
+ mUseWideViewport = use;
+ postSync();
+ }
+ }
+
+ /**
+ * @return True if the WebView is using a wide viewport
+ */
+ public synchronized boolean getUseWideViewPort() {
+ return mUseWideViewport;
+ }
+
+ /**
+ * Tell the WebView whether it supports multiple windows. TRUE means
+ * that {@link WebChromeClient#onCreateWindow(WebView, boolean,
+ * boolean, Message)} is implemented by the host application.
+ */
+ public synchronized void setSupportMultipleWindows(boolean support) {
+ if (mSupportMultipleWindows != support) {
+ mSupportMultipleWindows = support;
+ postSync();
+ }
+ }
+
+ /**
+ * @return True if the WebView is supporting multiple windows. This means
+ * that {@link WebChromeClient#onCreateWindow(WebView, boolean,
+ * boolean, Message)} is implemented by the host application.
+ */
+ public synchronized boolean supportMultipleWindows() {
+ return mSupportMultipleWindows;
+ }
+
+ /**
+ * Set the underlying layout algorithm. This will cause a relayout of the
+ * WebView.
+ * @param l A LayoutAlgorithm enum specifying the algorithm to use.
+ * @see WebSettings.LayoutAlgorithm
+ */
+ public synchronized void setLayoutAlgorithm(LayoutAlgorithm l) {
+ // XXX: This will only be affective if libwebcore was built with
+ // ANDROID_LAYOUT defined.
+ if (mLayoutAlgorithm != l) {
+ mLayoutAlgorithm = l;
+ postSync();
+ }
+ }
+
+ /**
+ * Return the current layout algorithm.
+ * @return LayoutAlgorithm enum value describing the layout algorithm
+ * being used.
+ * @see WebSettings.LayoutAlgorithm
+ */
+ public synchronized LayoutAlgorithm getLayoutAlgorithm() {
+ return mLayoutAlgorithm;
+ }
+
+ /**
+ * Set the standard font family name.
+ * @param font A font family name.
+ */
+ public synchronized void setStandardFontFamily(String font) {
+ if (font != null && !font.equals(mStandardFontFamily)) {
+ mStandardFontFamily = font;
+ postSync();
+ }
+ }
+
+ /**
+ * Get the standard font family name.
+ * @return The standard font family name as a string.
+ */
+ public synchronized String getStandardFontFamily() {
+ return mStandardFontFamily;
+ }
+
+ /**
+ * Set the fixed font family name.
+ * @param font A font family name.
+ */
+ public synchronized void setFixedFontFamily(String font) {
+ if (font != null && !font.equals(mFixedFontFamily)) {
+ mFixedFontFamily = font;
+ postSync();
+ }
+ }
+
+ /**
+ * Get the fixed font family name.
+ * @return The fixed font family name as a string.
+ */
+ public synchronized String getFixedFontFamily() {
+ return mFixedFontFamily;
+ }
+
+ /**
+ * Set the sans-serif font family name.
+ * @param font A font family name.
+ */
+ public synchronized void setSansSerifFontFamily(String font) {
+ if (font != null && !font.equals(mSansSerifFontFamily)) {
+ mSansSerifFontFamily = font;
+ postSync();
+ }
+ }
+
+ /**
+ * Get the sans-serif font family name.
+ * @return The sans-serif font family name as a string.
+ */
+ public synchronized String getSansSerifFontFamily() {
+ return mSansSerifFontFamily;
+ }
+
+ /**
+ * Set the serif font family name.
+ * @param font A font family name.
+ */
+ public synchronized void setSerifFontFamily(String font) {
+ if (font != null && !font.equals(mSerifFontFamily)) {
+ mSerifFontFamily = font;
+ postSync();
+ }
+ }
+
+ /**
+ * Get the serif font family name.
+ * @return The serif font family name as a string.
+ */
+ public synchronized String getSerifFontFamily() {
+ return mSerifFontFamily;
+ }
+
+ /**
+ * Set the cursive font family name.
+ * @param font A font family name.
+ */
+ public synchronized void setCursiveFontFamily(String font) {
+ if (font != null && !font.equals(mCursiveFontFamily)) {
+ mCursiveFontFamily = font;
+ postSync();
+ }
+ }
+
+ /**
+ * Get the cursive font family name.
+ * @return The cursive font family name as a string.
+ */
+ public synchronized String getCursiveFontFamily() {
+ return mCursiveFontFamily;
+ }
+
+ /**
+ * Set the fantasy font family name.
+ * @param font A font family name.
+ */
+ public synchronized void setFantasyFontFamily(String font) {
+ if (font != null && !font.equals(mFantasyFontFamily)) {
+ mFantasyFontFamily = font;
+ postSync();
+ }
+ }
+
+ /**
+ * Get the fantasy font family name.
+ * @return The fantasy font family name as a string.
+ */
+ public synchronized String getFantasyFontFamily() {
+ return mFantasyFontFamily;
+ }
+
+ /**
+ * Set the minimum font size.
+ * @param size A non-negative integer between 1 and 72.
+ * Any number outside the specified range will be pinned.
+ */
+ public synchronized void setMinimumFontSize(int size) {
+ size = pin(size);
+ if (mMinimumFontSize != size) {
+ mMinimumFontSize = size;
+ postSync();
+ }
+ }
+
+ /**
+ * Get the minimum font size.
+ * @return A non-negative integer between 1 and 72.
+ */
+ public synchronized int getMinimumFontSize() {
+ return mMinimumFontSize;
+ }
+
+ /**
+ * Set the minimum logical font size.
+ * @param size A non-negative integer between 1 and 72.
+ * Any number outside the specified range will be pinned.
+ */
+ public synchronized void setMinimumLogicalFontSize(int size) {
+ size = pin(size);
+ if (mMinimumLogicalFontSize != size) {
+ mMinimumLogicalFontSize = size;
+ postSync();
+ }
+ }
+
+ /**
+ * Get the minimum logical font size.
+ * @return A non-negative integer between 1 and 72.
+ */
+ public synchronized int getMinimumLogicalFontSize() {
+ return mMinimumLogicalFontSize;
+ }
+
+ /**
+ * Set the default font size.
+ * @param size A non-negative integer between 1 and 72.
+ * Any number outside the specified range will be pinned.
+ */
+ public synchronized void setDefaultFontSize(int size) {
+ size = pin(size);
+ if (mDefaultFontSize != size) {
+ mDefaultFontSize = size;
+ postSync();
+ }
+ }
+
+ /**
+ * Get the default font size.
+ * @return A non-negative integer between 1 and 72.
+ */
+ public synchronized int getDefaultFontSize() {
+ return mDefaultFontSize;
+ }
+
+ /**
+ * Set the default fixed font size.
+ * @param size A non-negative integer between 1 and 72.
+ * Any number outside the specified range will be pinned.
+ */
+ public synchronized void setDefaultFixedFontSize(int size) {
+ size = pin(size);
+ if (mDefaultFixedFontSize != size) {
+ mDefaultFixedFontSize = size;
+ postSync();
+ }
+ }
+
+ /**
+ * Get the default fixed font size.
+ * @return A non-negative integer between 1 and 72.
+ */
+ public synchronized int getDefaultFixedFontSize() {
+ return mDefaultFixedFontSize;
+ }
+
+ /**
+ * Tell the WebView to load image resources automatically.
+ * @param flag True if the WebView should load images automatically.
+ */
+ public synchronized void setLoadsImagesAutomatically(boolean flag) {
+ if (mLoadsImagesAutomatically != flag) {
+ mLoadsImagesAutomatically = flag;
+ postSync();
+ }
+ }
+
+ /**
+ * Return true if the WebView will load image resources automatically.
+ * @return True if the WebView loads images automatically.
+ */
+ public synchronized boolean getLoadsImagesAutomatically() {
+ return mLoadsImagesAutomatically;
+ }
+
+ /**
+ * Tell the WebView to block network image. This is only checked when
+ * getLoadsImagesAutomatically() is true.
+ * @param flag True if the WebView should block network image
+ */
+ public synchronized void setBlockNetworkImage(boolean flag) {
+ if (mBlockNetworkImage != flag) {
+ mBlockNetworkImage = flag;
+ postSync();
+ }
+ }
+
+ /**
+ * Return true if the WebView will block network image.
+ * @return True if the WebView blocks network image.
+ */
+ public synchronized boolean getBlockNetworkImage() {
+ return mBlockNetworkImage;
+ }
+
+ /**
+ * Tell the WebView to enable javascript execution.
+ * @param flag True if the WebView should execute javascript.
+ */
+ public synchronized void setJavaScriptEnabled(boolean flag) {
+ if (mJavaScriptEnabled != flag) {
+ mJavaScriptEnabled = flag;
+ postSync();
+ }
+ }
+
+ /**
+ * Tell the WebView to enable plugins.
+ * @param flag True if the WebView should load plugins.
+ */
+ public synchronized void setPluginsEnabled(boolean flag) {
+ if (mPluginsEnabled != flag) {
+ mPluginsEnabled = flag;
+ postSync();
+ }
+ }
+
+ /**
+ * Set a custom path to plugins used by the WebView. The client
+ * must ensure it exists before this call.
+ * @param pluginsPath String path to the directory containing plugins.
+ */
+ public synchronized void setPluginsPath(String pluginsPath) {
+ if (pluginsPath != null && !pluginsPath.equals(mPluginsPath)) {
+ mPluginsPath = pluginsPath;
+ postSync();
+ }
+ }
+
+ /**
+ * Return true if javascript is enabled.
+ * @return True if javascript is enabled.
+ */
+ public synchronized boolean getJavaScriptEnabled() {
+ return mJavaScriptEnabled;
+ }
+
+ /**
+ * Return true if plugins are enabled.
+ * @return True if plugins are enabled.
+ */
+ public synchronized boolean getPluginsEnabled() {
+ return mPluginsEnabled;
+ }
+
+ /**
+ * Return the current path used for plugins in the WebView.
+ * @return The string path to the WebView plugins.
+ */
+ public synchronized String getPluginsPath() {
+ return mPluginsPath;
+ }
+
+ /**
+ * Tell javascript to open windows automatically. This applies to the
+ * javascript function window.open().
+ * @param flag True if javascript can open windows automatically.
+ */
+ public synchronized void setJavaScriptCanOpenWindowsAutomatically(
+ boolean flag) {
+ if (mJavaScriptCanOpenWindowsAutomatically != flag) {
+ mJavaScriptCanOpenWindowsAutomatically = flag;
+ postSync();
+ }
+ }
+
+ /**
+ * Return true if javascript can open windows automatically.
+ * @return True if javascript can open windows automatically during
+ * window.open().
+ */
+ public synchronized boolean getJavaScriptCanOpenWindowsAutomatically() {
+ return mJavaScriptCanOpenWindowsAutomatically;
+ }
+
+ /**
+ * Set the default text encoding name to use when decoding html pages.
+ * @param encoding The text encoding name.
+ */
+ public synchronized void setDefaultTextEncodingName(String encoding) {
+ if (encoding != null && !encoding.equals(mDefaultTextEncoding)) {
+ mDefaultTextEncoding = encoding;
+ postSync();
+ }
+ }
+
+ /**
+ * Get the default text encoding name.
+ * @return The default text encoding name as a string.
+ */
+ public synchronized String getDefaultTextEncodingName() {
+ return mDefaultTextEncoding;
+ }
+
+ /* Package api to grab the user agent string. */
+ /*package*/ synchronized String getUserAgentString() {
+ return mUserAgent;
+ }
+
+ /**
+ * Tell the WebView whether it needs to set a node to have focus when
+ * {@link WebView#requestFocus(int, android.graphics.Rect)} is called.
+ *
+ * @param flag
+ */
+ public void setNeedInitialFocus(boolean flag) {
+ if (mNeedInitialFocus != flag) {
+ mNeedInitialFocus = flag;
+ }
+ }
+
+ /* Package api to get the choice whether it needs to set initial focus. */
+ /* package */ boolean getNeedInitialFocus() {
+ return mNeedInitialFocus;
+ }
+
+ /**
+ * Set the priority of the Render thread. Unlike the other settings, this
+ * one only needs to be called once per process.
+ *
+ * @param priority RenderPriority, can be normal, high or low.
+ */
+ public synchronized void setRenderPriority(RenderPriority priority) {
+ if (mRenderPriority != priority) {
+ mRenderPriority = priority;
+ mEventHandler.sendMessage(Message.obtain(null,
+ EventHandler.PRIORITY));
+ }
+ }
+
+ /**
+ * Override the way the cache is used. The way the cache is used is based
+ * on the navigation option. For a normal page load, the cache is checked
+ * and content is re-validated as needed. When navigating back, content is
+ * not revalidated, instead the content is just pulled from the cache.
+ * This function allows the client to override this behavior.
+ * @param mode One of the LOAD_ values.
+ */
+ public void setCacheMode(int mode) {
+ if (mode != mOverrideCacheMode) {
+ mOverrideCacheMode = mode;
+ }
+ }
+
+ /**
+ * Return the current setting for overriding the cache mode. For a full
+ * description, see the {@link #setCacheMode(int)} function.
+ */
+ public int getCacheMode() {
+ return mOverrideCacheMode;
+ }
+
+ /**
+ * Transfer messages from the queue to the new WebCoreThread. Called from
+ * WebCore thread.
+ */
+ /*package*/
+ synchronized void syncSettingsAndCreateHandler(BrowserFrame frame) {
+ mBrowserFrame = frame;
+ if (android.util.Config.DEBUG) {
+ junit.framework.Assert.assertTrue(frame.mNativeFrame != 0);
+ }
+ nativeSync(frame.mNativeFrame);
+ mSyncPending = false;
+ mEventHandler.createHandler();
+ }
+
+ private int pin(int size) {
+ // FIXME: 72 is just an arbitrary max text size value.
+ if (size < 1) {
+ return 1;
+ } else if (size > 72) {
+ return 72;
+ }
+ return size;
+ }
+
+ /* Post a SYNC message to handle syncing the native settings. */
+ private synchronized void postSync() {
+ // Only post if a sync is not pending
+ if (!mSyncPending) {
+ mSyncPending = mEventHandler.sendMessage(
+ Message.obtain(null, EventHandler.SYNC));
+ }
+ }
+
+ // Synchronize the native and java settings.
+ private native void nativeSync(int nativeFrame);
+}
diff --git a/core/java/android/webkit/WebSyncManager.java b/core/java/android/webkit/WebSyncManager.java
new file mode 100644
index 0000000..e6e9994
--- /dev/null
+++ b/core/java/android/webkit/WebSyncManager.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.webkit;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Process;
+import android.util.Config;
+import android.util.Log;
+
+abstract class WebSyncManager implements Runnable {
+ // message code for sync message
+ private static final int SYNC_MESSAGE = 101;
+ // time delay in millisec for a sync (now) message
+ private static int SYNC_NOW_INTERVAL = 100; // 100 millisec
+ // time delay in millisec for a sync (later) message
+ private static int SYNC_LATER_INTERVAL = 5 * 60 * 1000; // 5 minutes
+ // thread for syncing
+ private Thread mSyncThread;
+ // Name of thread
+ private String mThreadName;
+ // handler of the sync thread
+ protected Handler mHandler;
+ // database for the persistent storage
+ protected WebViewDatabase mDataBase;
+ // Ref count for calls to start/stop sync
+ private int mStartSyncRefCount;
+ // log tag
+ protected static final String LOGTAG = "websync";
+
+ private class SyncHandler extends Handler {
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.what == SYNC_MESSAGE) {
+ if (Config.LOGV) {
+ Log.v(LOGTAG, "*** WebSyncManager sync ***");
+ }
+ syncFromRamToFlash();
+
+ // send a delayed message to request sync later
+ Message newmsg = obtainMessage(SYNC_MESSAGE);
+ sendMessageDelayed(newmsg, SYNC_LATER_INTERVAL);
+ }
+ }
+ }
+
+ protected WebSyncManager(Context context, String name) {
+ mThreadName = name;
+ if (context != null) {
+ mDataBase = WebViewDatabase.getInstance(context);
+ mSyncThread = new Thread(this);
+ mSyncThread.setName(mThreadName);
+ mSyncThread.start();
+ } else {
+ throw new IllegalStateException(
+ "WebSyncManager can't be created without context");
+ }
+ }
+
+ protected Object clone() throws CloneNotSupportedException {
+ throw new CloneNotSupportedException("doesn't implement Cloneable");
+ }
+
+ public void run() {
+ // prepare Looper for sync handler
+ Looper.prepare();
+ mHandler = new SyncHandler();
+ onSyncInit();
+ // lower the priority after onSyncInit() is done
+ Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+
+ Message msg = mHandler.obtainMessage(SYNC_MESSAGE);
+ mHandler.sendMessageDelayed(msg, SYNC_LATER_INTERVAL);
+
+ Looper.loop();
+ }
+
+ /**
+ * sync() forces sync manager to sync now
+ */
+ public void sync() {
+ if (Config.LOGV) {
+ Log.v(LOGTAG, "*** WebSyncManager sync ***");
+ }
+ if (mHandler == null) {
+ return;
+ }
+ mHandler.removeMessages(SYNC_MESSAGE);
+ Message msg = mHandler.obtainMessage(SYNC_MESSAGE);
+ mHandler.sendMessageDelayed(msg, SYNC_NOW_INTERVAL);
+ }
+
+ /**
+ * resetSync() resets sync manager's timer
+ */
+ public void resetSync() {
+ if (Config.LOGV) {
+ Log.v(LOGTAG, "*** WebSyncManager resetSync ***");
+ }
+ if (mHandler == null) {
+ return;
+ }
+ mHandler.removeMessages(SYNC_MESSAGE);
+ Message msg = mHandler.obtainMessage(SYNC_MESSAGE);
+ mHandler.sendMessageDelayed(msg, SYNC_LATER_INTERVAL);
+ }
+
+ /**
+ * startSync() requests sync manager to start sync
+ */
+ public void startSync() {
+ if (Config.LOGV) {
+ Log.v(LOGTAG, "*** WebSyncManager startSync ***, Ref count:" +
+ mStartSyncRefCount);
+ }
+ if (mHandler == null) {
+ return;
+ }
+ if (++mStartSyncRefCount == 1) {
+ Message msg = mHandler.obtainMessage(SYNC_MESSAGE);
+ mHandler.sendMessageDelayed(msg, SYNC_LATER_INTERVAL);
+ }
+ }
+
+ /**
+ * stopSync() requests sync manager to stop sync. remove any SYNC_MESSAGE in
+ * the queue to break the sync loop
+ */
+ public void stopSync() {
+ if (Config.LOGV) {
+ Log.v(LOGTAG, "*** WebSyncManager stopSync ***, Ref count:" +
+ mStartSyncRefCount);
+ }
+ if (mHandler == null) {
+ return;
+ }
+ if (--mStartSyncRefCount == 0) {
+ mHandler.removeMessages(SYNC_MESSAGE);
+ }
+ }
+
+ protected void onSyncInit() {
+ }
+
+ abstract void syncFromRamToFlash();
+}
diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java
new file mode 100644
index 0000000..6623257
--- /dev/null
+++ b/core/java/android/webkit/WebView.java
@@ -0,0 +1,4609 @@
+/*
+ * 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.webkit;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.DialogInterface.OnCancelListener;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Picture;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.net.http.SslCertificate;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.ServiceManager;
+import android.os.SystemClock;
+import android.text.IClipboard;
+import android.text.Selection;
+import android.text.Spannable;
+import android.util.AttributeSet;
+import android.util.Config;
+import android.util.Log;
+import android.view.animation.AlphaAnimation;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.SoundEffectConstants;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.ViewTreeObserver;
+import android.webkit.WebViewCore.EventHub;
+import android.widget.AbsoluteLayout;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.RelativeLayout;
+import android.widget.Scroller;
+import android.widget.Toast;
+import android.widget.ZoomControls;
+import android.widget.AdapterView.OnItemClickListener;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.net.URLDecoder;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * <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.
+ * It uses the WebKit rendering engine to display
+ * web pages and includes methods to navigate forward and backward
+ * through a history, zoom in and out, perform text searches and more.</p>
+ * <p>Note that, in order for your Activity to access the Internet and load web pages
+ * in a WebView, you must add the <var>INTERNET</var> permissions to your
+ * Android Manifest file:</p>
+ * <pre>&lt;uses-permission android:name="android.permission.INTERNET" /></pre>
+ * <p>This must be a child of the <code>&lt;manifest></code> element.</p>
+ */
+public class WebView extends AbsoluteLayout
+ implements ViewTreeObserver.OnGlobalFocusChangeListener,
+ ViewGroup.OnHierarchyChangeListener {
+
+ // keep debugging parameters near the top of the file
+ static final String LOGTAG = "webview";
+ static final boolean DEBUG = false;
+ static final boolean LOGV_ENABLED = DEBUG ? Config.LOGD : Config.LOGV;
+
+ private class ExtendedZoomControls extends RelativeLayout {
+ public ExtendedZoomControls(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ LayoutInflater inflater = (LayoutInflater) context
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ inflater.inflate(com.android.internal.R.layout.zoom_magnify, this
+ , true);
+ mZoomControls = (ZoomControls) findViewById
+ (com.android.internal.R.id.zoomControls);
+ mZoomMagnify = (ImageView) findViewById
+ (com.android.internal.R.id.zoomMagnify);
+ }
+
+ public void show(boolean canZoomOut) {
+ mZoomMagnify.setVisibility(canZoomOut ? View.VISIBLE : View.GONE);
+ fade(View.VISIBLE, 0.0f, 1.0f);
+ }
+
+ public void hide() {
+ fade(View.GONE, 1.0f, 0.0f);
+ }
+
+ private void fade(int visibility, float startAlpha, float endAlpha) {
+ AlphaAnimation anim = new AlphaAnimation(startAlpha, endAlpha);
+ anim.setDuration(500);
+ startAnimation(anim);
+ setVisibility(visibility);
+ }
+
+ public void setIsZoomMagnifyEnabled(boolean isEnabled) {
+ mZoomMagnify.setEnabled(isEnabled);
+ }
+
+ public boolean hasFocus() {
+ return mZoomControls.hasFocus() || mZoomMagnify.hasFocus();
+ }
+
+ public void setOnZoomInClickListener(OnClickListener listener) {
+ mZoomControls.setOnZoomInClickListener(listener);
+ }
+
+ public void setOnZoomOutClickListener(OnClickListener listener) {
+ mZoomControls.setOnZoomOutClickListener(listener);
+ }
+
+ public void setOnZoomMagnifyClickListener(OnClickListener listener) {
+ mZoomMagnify.setOnClickListener(listener);
+ }
+
+ ZoomControls mZoomControls;
+ ImageView mZoomMagnify;
+ }
+
+ /**
+ * Transportation object for returning WebView across thread boundaries.
+ */
+ public class WebViewTransport {
+ private WebView mWebview;
+
+ /**
+ * Set the WebView to the transportation object.
+ * @param webview The WebView to transport.
+ */
+ public synchronized void setWebView(WebView webview) {
+ mWebview = webview;
+ }
+
+ /**
+ * Return the WebView object.
+ * @return WebView The transported WebView object.
+ */
+ public synchronized WebView getWebView() {
+ return mWebview;
+ }
+ }
+
+ // A final CallbackProxy shared by WebViewCore and BrowserFrame.
+ private final CallbackProxy mCallbackProxy;
+
+ private final WebViewDatabase mDatabase;
+
+ // SSL certificate for the main top-level page (if secure)
+ private SslCertificate mCertificate;
+
+ // Native WebView pointer that is 0 until the native object has been
+ // created.
+ private int mNativeClass;
+ // This would be final but it needs to be set to null when the WebView is
+ // destroyed.
+ private WebViewCore mWebViewCore;
+ // Handler for dispatching UI messages.
+ /* package */ final Handler mPrivateHandler = new PrivateHandler();
+ private TextDialog mTextEntry;
+ // Used to ignore changes to webkit text that arrives to the UI side after
+ // more key events.
+ private int mTextGeneration;
+
+ // The list of loaded plugins.
+ private static PluginList sPluginList;
+
+ /**
+ * Position of the last touch event.
+ */
+ private float mLastTouchX;
+ private float mLastTouchY;
+
+ /**
+ * Time of the last touch event.
+ */
+ private long mLastTouchTime;
+
+ /**
+ * Helper class to get velocity for fling
+ */
+ VelocityTracker mVelocityTracker;
+
+ /**
+ * Touch mode
+ */
+ private int mTouchMode = TOUCH_DONE_MODE;
+ private static final int TOUCH_INIT_MODE = 1;
+ private static final int TOUCH_DRAG_START_MODE = 2;
+ private static final int TOUCH_DRAG_MODE = 3;
+ private static final int TOUCH_SHORTPRESS_START_MODE = 4;
+ private static final int TOUCH_SHORTPRESS_MODE = 5;
+ private static final int TOUCH_DOUBLECLICK_MODE = 6;
+ private static final int TOUCH_DONE_MODE = 7;
+ // touch mode values specific to scale+scroll
+ private static final int FIRST_SCROLL_ZOOM = 9;
+ private static final int SCROLL_ZOOM_ANIMATION_IN = 9;
+ private static final int SCROLL_ZOOM_ANIMATION_OUT = 10;
+ private static final int SCROLL_ZOOM_OUT = 11;
+ private static final int LAST_SCROLL_ZOOM = 11;
+ // end of touch mode values specific to scale+scroll
+
+ // If updateTextEntry gets called while we are out of focus, use this
+ // variable to remember to do it next time we gain focus.
+ private boolean mNeedsUpdateTextEntry = false;
+
+ // Whether or not to draw the focus ring.
+ private boolean mDrawFocusRing = true;
+
+ /**
+ * Customizable constant
+ */
+ // pre-computed square of ViewConfiguration.getTouchSlop()
+ private static final int TOUCH_SLOP_SQUARE =
+ ViewConfiguration.getTouchSlop() * ViewConfiguration.getTouchSlop();
+ // This should be ViewConfiguration.getTapTimeout()
+ // But system time out is 100ms, which is too short for the browser.
+ // In the browser, if it switches out of tap too soon, jump tap won't work.
+ private static final int TAP_TIMEOUT = 200;
+ // The duration in milliseconds we will wait to see if it is a double tap.
+ // With a limited survey, the time between the first tap up and the second
+ // tap down in the double tap case is around 70ms - 120ms.
+ private static final int DOUBLE_TAP_TIMEOUT = 200;
+ // This should be ViewConfiguration.getLongPressTimeout()
+ // But system time out is 500ms, which is too short for the browser.
+ // With a short timeout, it's difficult to treat trigger a short press.
+ private static final int LONG_PRESS_TIMEOUT = 1000;
+ // needed to avoid flinging after a pause of no movement
+ private static final int MIN_FLING_TIME = 250;
+ // The time that the Zoom Controls are visible before fading away
+ private static final long ZOOM_CONTROLS_TIMEOUT =
+ ViewConfiguration.getZoomControlsTimeout();
+ // Wait a short time before sending kit focus message, in case
+ // the user is still moving around, to avoid rebuilding the display list
+ // prematurely
+ private static final long SET_KIT_FOCUS_DELAY = 250;
+ // The amount of content to overlap between two screens when going through
+ // pages with the space bar, in pixels.
+ private static final int PAGE_SCROLL_OVERLAP = 24;
+
+ /**
+ * These prevent calling requestLayout if either dimension is fixed. This
+ * depends on the layout parameters and the measure specs.
+ */
+ boolean mWidthCanMeasure;
+ boolean mHeightCanMeasure;
+
+ // Remember the last dimensions we sent to the native side so we can avoid
+ // sending the same dimensions more than once.
+ int mLastWidthSent;
+ int mLastHeightSent;
+
+ private int mContentWidth; // cache of value from WebViewCore
+ private int mContentHeight; // cache of value from WebViewCore
+
+ // Need to have the separate control for horizontal and vertical scrollbar
+ // style than the View's single scrollbar style
+ private boolean mOverlayHorizontalScrollbar = true;
+ private boolean mOverlayVerticalScrollbar = false;
+
+ // our standard speed. this way small distances will be traversed in less
+ // time than large distances, but we cap the duration, so that very large
+ // distances won't take too long to get there.
+ private static final int STD_SPEED = 480; // pixels per second
+ // time for the longest scroll animation
+ private static final int MAX_DURATION = 750; // milliseconds
+ private Scroller mScroller;
+
+ private boolean mWrapContent;
+
+ // The View containing the zoom controls
+ private ExtendedZoomControls mZoomControls;
+ private Runnable mZoomControlRunnable;
+
+ // true if we should call webcore to draw the content, false means we have
+ // requested something but it isn't ready to draw yet.
+ private WebViewCore.FocusData mFocusData;
+ /**
+ * Private message ids
+ */
+ private static final int REMEMBER_PASSWORD = 1;
+ private static final int NEVER_REMEMBER_PASSWORD = 2;
+ private static final int SWITCH_TO_SHORTPRESS = 3;
+ private static final int SWITCH_TO_LONGPRESS = 4;
+ private static final int RELEASE_SINGLE_TAP = 5;
+ private static final int UPDATE_TEXT_ENTRY_ADAPTER = 6;
+ private static final int SWITCH_TO_ENTER = 7;
+ private static final int RESUME_WEBCORE_UPDATE = 8;
+
+ //! arg1=x, arg2=y
+ static final int SCROLL_TO_MSG_ID = 10;
+ static final int SCROLL_BY_MSG_ID = 11;
+ //! arg1=x, arg2=y
+ static final int SPAWN_SCROLL_TO_MSG_ID = 12;
+ //! arg1=x, arg2=y
+ static final int SYNC_SCROLL_TO_MSG_ID = 13;
+ static final int NEW_PICTURE_MSG_ID = 14;
+ static final int UPDATE_TEXT_ENTRY_MSG_ID = 15;
+ static final int WEBCORE_INITIALIZED_MSG_ID = 16;
+ static final int UPDATE_TEXTFIELD_TEXT_MSG_ID = 17;
+ static final int DID_FIRST_LAYOUT_MSG_ID = 18;
+ static final int RECOMPUTE_FOCUS_MSG_ID = 19;
+ static final int NOTIFY_FOCUS_SET_MSG_ID = 20;
+ static final int MARK_NODE_INVALID_ID = 21;
+ static final int UPDATE_CLIPBOARD = 22;
+ static final int LONG_PRESS_TRACKBALL = 24;
+
+ // width which view is considered to be fully zoomed out
+ static final int ZOOM_OUT_WIDTH = 1024;
+
+ private static final float DEFAULT_MAX_ZOOM_SCALE = 4;
+ private static final float DEFAULT_MIN_ZOOM_SCALE = 0.25f;
+ // scale limit, which can be set through viewport meta tag in the web page
+ private float mMaxZoomScale = DEFAULT_MAX_ZOOM_SCALE;
+ private float mMinZoomScale = DEFAULT_MIN_ZOOM_SCALE;
+
+ // initial scale in percent. 0 means using default.
+ private int mInitialScale = 0;
+
+ // computed scale and inverse, from mZoomWidth.
+ private float mActualScale = 1;
+ private float mInvActualScale = 1;
+ // if this is non-zero, it is used on drawing rather than mActualScale
+ private float mZoomScale;
+ private float mInvInitialZoomScale;
+ private float mInvFinalZoomScale;
+ private long mZoomStart;
+ private static final int ZOOM_ANIMATION_LENGTH = 500;
+
+ private boolean mUserScroll = false;
+
+ private int mSnapScrollMode = SNAP_NONE;
+ private static final int SNAP_NONE = 1;
+ private static final int SNAP_X = 2;
+ private static final int SNAP_Y = 3;
+ private static final int SNAP_X_LOCK = 4;
+ private static final int SNAP_Y_LOCK = 5;
+ private boolean mSnapPositive;
+
+ // Used to match key downs and key ups
+ private boolean mGotKeyDown;
+
+ /**
+ * URI scheme for telephone number
+ */
+ public static final String SCHEME_TEL = "tel:";
+ /**
+ * URI scheme for email address
+ */
+ public static final String SCHEME_MAILTO = "mailto:";
+ /**
+ * URI scheme for map address
+ */
+ public static final String SCHEME_GEO = "geo:0,0?q=";
+
+ private int mBackgroundColor = Color.WHITE;
+
+ // Used to notify listeners of a new picture.
+ private PictureListener mPictureListener;
+ /**
+ * Interface to listen for new pictures as they change.
+ */
+ public interface PictureListener {
+ /**
+ * Notify the listener that the picture has changed.
+ * @param view The WebView that owns the picture.
+ * @param picture The new picture.
+ */
+ public void onNewPicture(WebView view, Picture picture);
+ }
+
+ public class HitTestResult {
+ /**
+ * Default HitTestResult, where the target is unknown
+ */
+ public static final int UNKNOWN_TYPE = 0;
+ /**
+ * HitTestResult for hitting a HTML::a tag
+ */
+ public static final int ANCHOR_TYPE = 1;
+ /**
+ * HitTestResult for hitting a phone number
+ */
+ public static final int PHONE_TYPE = 2;
+ /**
+ * HitTestResult for hitting a map address
+ */
+ public static final int GEO_TYPE = 3;
+ /**
+ * HitTestResult for hitting an email address
+ */
+ public static final int EMAIL_TYPE = 4;
+ /**
+ * HitTestResult for hitting an HTML::img tag
+ */
+ public static final int IMAGE_TYPE = 5;
+ /**
+ * HitTestResult for hitting a HTML::a tag which contains HTML::img
+ */
+ public static final int IMAGE_ANCHOR_TYPE = 6;
+ /**
+ * HitTestResult for hitting a HTML::a tag with src=http
+ */
+ public static final int SRC_ANCHOR_TYPE = 7;
+ /**
+ * HitTestResult for hitting a HTML::a tag with src=http + HTML::img
+ */
+ public static final int SRC_IMAGE_ANCHOR_TYPE = 8;
+ /**
+ * HitTestResult for hitting an edit text area
+ */
+ public static final int EDIT_TEXT_TYPE = 9;
+
+ private int mType;
+ private String mExtra;
+
+ HitTestResult() {
+ mType = UNKNOWN_TYPE;
+ }
+
+ private void setType(int type) {
+ mType = type;
+ }
+
+ private void setExtra(String extra) {
+ mExtra = extra;
+ }
+
+ public int getType() {
+ return mType;
+ }
+
+ public String getExtra() {
+ return mExtra;
+ }
+ }
+
+ /**
+ * Construct a new WebView with a Context object.
+ * @param context A Context object used to access application assets.
+ */
+ public WebView(Context context) {
+ this(context, null);
+ }
+
+ /**
+ * Construct a new WebView with layout parameters.
+ * @param context A Context object used to access application assets.
+ * @param attrs An AttributeSet passed to our parent.
+ */
+ public WebView(Context context, AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.webViewStyle);
+ }
+
+ /**
+ * Construct a new WebView with layout parameters and a default style.
+ * @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.
+ */
+ public WebView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ init();
+
+ TypedArray a = context.obtainStyledAttributes(
+ com.android.internal.R.styleable.View);
+ initializeScrollbars(a);
+ a.recycle();
+
+ mCallbackProxy = new CallbackProxy(context, this);
+ mWebViewCore = new WebViewCore(context, this, mCallbackProxy);
+ mDatabase = WebViewDatabase.getInstance(context);
+ mFocusData = new WebViewCore.FocusData();
+ mFocusData.mFrame = 0;
+ mFocusData.mNode = 0;
+ mFocusData.mX = 0;
+ mFocusData.mY = 0;
+ mScroller = new Scroller(context);
+ }
+
+ private void init() {
+ setWillNotDraw(false);
+ setFocusable(true);
+ setFocusableInTouchMode(true);
+ setClickable(true);
+ setLongClickable(true);
+
+ // should be conditional on if we're in the browser activity?
+ setHorizontalScrollBarEnabled(true);
+ setVerticalScrollBarEnabled(true);
+ }
+
+ /* package */ boolean onSavePassword(String host, String username,
+ String password, final Message resumeMsg) {
+ boolean rVal = false;
+ if (resumeMsg == null) {
+ // null resumeMsg implies saving password silently
+ mDatabase.setUsernamePassword(host, username, password);
+ } else {
+ final Message remember = mPrivateHandler.obtainMessage(
+ REMEMBER_PASSWORD);
+ remember.getData().putString("host", host);
+ remember.getData().putString("username", username);
+ remember.getData().putString("password", password);
+ remember.obj = resumeMsg;
+
+ final Message neverRemember = mPrivateHandler.obtainMessage(
+ NEVER_REMEMBER_PASSWORD);
+ neverRemember.getData().putString("host", host);
+ neverRemember.getData().putString("username", username);
+ neverRemember.getData().putString("password", password);
+ neverRemember.obj = resumeMsg;
+
+ new AlertDialog.Builder(getContext())
+ .setTitle(com.android.internal.R.string.save_password_label)
+ .setMessage(com.android.internal.R.string.save_password_message)
+ .setPositiveButton(com.android.internal.R.string.save_password_notnow,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ resumeMsg.sendToTarget();
+ }
+ })
+ .setNeutralButton(com.android.internal.R.string.save_password_remember,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ remember.sendToTarget();
+ }
+ })
+ .setNegativeButton(com.android.internal.R.string.save_password_never,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ neverRemember.sendToTarget();
+ }
+ })
+ .setOnCancelListener(new OnCancelListener() {
+ public void onCancel(DialogInterface dialog) {
+ resumeMsg.sendToTarget();
+ }
+ }).show();
+ // Return true so that WebViewCore will pause while the dialog is
+ // up.
+ rVal = true;
+ }
+ return rVal;
+ }
+
+ @Override
+ public void setScrollBarStyle(int style) {
+ if (style == View.SCROLLBARS_INSIDE_INSET
+ || style == View.SCROLLBARS_OUTSIDE_INSET) {
+ mOverlayHorizontalScrollbar = mOverlayVerticalScrollbar = false;
+ } else {
+ mOverlayHorizontalScrollbar = mOverlayVerticalScrollbar = true;
+ }
+ super.setScrollBarStyle(style);
+ }
+
+ /**
+ * Specify whether the horizontal scrollbar has overlay style.
+ * @param overlay TRUE if horizontal scrollbar should have overlay style.
+ */
+ public void setHorizontalScrollbarOverlay(boolean overlay) {
+ mOverlayHorizontalScrollbar = overlay;
+ }
+
+ /**
+ * Specify whether the vertical scrollbar has overlay style.
+ * @param overlay TRUE if vertical scrollbar should have overlay style.
+ */
+ public void setVerticalScrollbarOverlay(boolean overlay) {
+ mOverlayVerticalScrollbar = overlay;
+ }
+
+ /**
+ * Return whether horizontal scrollbar has overlay style
+ * @return TRUE if horizontal scrollbar has overlay style.
+ */
+ public boolean overlayHorizontalScrollbar() {
+ return mOverlayHorizontalScrollbar;
+ }
+
+ /**
+ * Return whether vertical scrollbar has overlay style
+ * @return TRUE if vertical scrollbar has overlay style.
+ */
+ public boolean overlayVerticalScrollbar() {
+ return mOverlayVerticalScrollbar;
+ }
+
+ /*
+ * Return the width of the view where the content of WebView should render
+ * to.
+ */
+ private int getViewWidth() {
+ if (mOverlayVerticalScrollbar) {
+ return getWidth();
+ } else {
+ return getWidth() - getVerticalScrollbarWidth();
+ }
+ }
+
+ /*
+ * Return the height of the view where the content of WebView should render
+ * to.
+ */
+ private int getViewHeight() {
+ if (mOverlayHorizontalScrollbar) {
+ return getHeight();
+ } else {
+ return getHeight() - getHorizontalScrollbarHeight();
+ }
+ }
+
+ /**
+ * @return The SSL certificate for the main top-level page or null if
+ * there is no certificate (the site is not secure).
+ */
+ public SslCertificate getCertificate() {
+ return mCertificate;
+ }
+
+ /**
+ * Sets the SSL certificate for the main top-level page.
+ */
+ public void setCertificate(SslCertificate certificate) {
+ // here, the certificate can be null (if the site is not secure)
+ mCertificate = certificate;
+ }
+
+ //-------------------------------------------------------------------------
+ // Methods called by activity
+ //-------------------------------------------------------------------------
+
+ /**
+ * Save the username and password for a particular host in the WebView's
+ * internal database.
+ * @param host The host that required the credentials.
+ * @param username The username for the given host.
+ * @param password The password for the given host.
+ */
+ public void savePassword(String host, String username, String password) {
+ mDatabase.setUsernamePassword(host, username, password);
+ }
+
+ /**
+ * Set the HTTP authentication credentials for a given host and realm.
+ *
+ * @param host The host for the credentials.
+ * @param realm The realm for the credentials.
+ * @param username The username for the password. If it is null, it means
+ * password can't be saved.
+ * @param password The password
+ */
+ public void setHttpAuthUsernamePassword(String host, String realm,
+ String username, String password) {
+ mDatabase.setHttpAuthUsernamePassword(host, realm, username, password);
+ }
+
+ /**
+ * Retrieve the HTTP authentication username and password for a given
+ * host & realm pair
+ *
+ * @param host The host for which the credentials apply.
+ * @param realm The realm for which the credentials apply.
+ * @return String[] if found, String[0] is username, which can be null and
+ * String[1] is password. Return null if it can't find anything.
+ */
+ public String[] getHttpAuthUsernamePassword(String host, String realm) {
+ return mDatabase.getHttpAuthUsernamePassword(host, realm);
+ }
+
+ /**
+ * Destroy the internal state of the WebView. This method should be called
+ * after the WebView has been removed from the view system. No other
+ * methods may be called on a WebView after destroy.
+ */
+ public void destroy() {
+ clearTextEntry();
+ getViewTreeObserver().removeOnGlobalFocusChangeListener(this);
+ if (mWebViewCore != null) {
+ // Set the handlers to null before destroying WebViewCore so no
+ // more messages will be posted.
+ mCallbackProxy.setWebViewClient(null);
+ mCallbackProxy.setWebChromeClient(null);
+ // Tell WebViewCore to destroy itself
+ WebViewCore webViewCore = mWebViewCore;
+ mWebViewCore = null; // prevent using partial webViewCore
+ webViewCore.destroy();
+ // Remove any pending messages that might not be serviced yet.
+ mPrivateHandler.removeCallbacksAndMessages(null);
+ mCallbackProxy.removeCallbacksAndMessages(null);
+ // Wake up the WebCore thread just in case it is waiting for a
+ // javascript dialog.
+ synchronized (mCallbackProxy) {
+ mCallbackProxy.notify();
+ }
+ }
+ if (mNativeClass != 0) {
+ nativeDestroy();
+ mNativeClass = 0;
+ }
+ }
+
+ /**
+ * Enables platform notifications of data state and proxy changes.
+ */
+ public static void enablePlatformNotifications() {
+ Network.enablePlatformNotifications();
+ }
+
+ /**
+ * If platform notifications are enabled, this should be called
+ * from onPause() or onStop().
+ */
+ public static void disablePlatformNotifications() {
+ Network.disablePlatformNotifications();
+ }
+
+ /**
+ * Save the state of this WebView used in Activity.onSaveInstanceState.
+ * @param outState The Bundle to store the WebView state.
+ * @return The same copy of the back/forward list used to save the state. If
+ * saveState fails, the returned list will be null.
+ */
+ public WebBackForwardList saveState(Bundle outState) {
+ // We grab a copy of the back/forward list because a client of WebView
+ // may have invalidated the history list by calling clearHistory.
+ WebBackForwardList list = copyBackForwardList();
+ final int currentIndex = list.getCurrentIndex();
+ final int size = list.getSize();
+ // We should fail saving the state if the list is empty or the index is
+ // not in a valid range.
+ if (currentIndex < 0 || currentIndex >= size || size == 0) {
+ return null;
+ }
+ outState.putInt("index", currentIndex);
+ // FIXME: This should just be a byte[][] instead of ArrayList but
+ // Parcel.java does not have the code to handle multi-dimensional
+ // arrays.
+ ArrayList<byte[]> history = new ArrayList<byte[]>(size);
+ for (int i = 0; i < size; i++) {
+ WebHistoryItem item = list.getItemAtIndex(i);
+ byte[] data = item.getFlattenedData();
+ if (data == null) {
+ // It would be very odd to not have any data for a given history
+ // item. And we will fail to rebuild the history list without
+ // flattened data.
+ return null;
+ }
+ history.add(data);
+ if (i == currentIndex) {
+ Picture p = capturePicture();
+ String path = mContext.getDir("thumbnails", 0).getPath()
+ + File.separatorChar + hashCode() + "_pic.save";
+ File f = new File(path);
+ try {
+ final FileOutputStream out = new FileOutputStream(f);
+ p.writeToStream(out);
+ out.close();
+ } catch (FileNotFoundException e){
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ } catch (RuntimeException e) {
+ e.printStackTrace();
+ }
+ if (f.length() > 0) {
+ outState.putString("picture", path);
+ outState.putInt("scrollX", mScrollX);
+ outState.putInt("scrollY", mScrollY);
+ outState.putFloat("scale", mActualScale);
+ }
+ }
+ }
+ outState.putSerializable("history", history);
+ if (mCertificate != null) {
+ outState.putBundle("certificate",
+ SslCertificate.saveState(mCertificate));
+ }
+ return list;
+ }
+
+ /**
+ * Restore the state of this WebView from the given map used in
+ * Activity.onThaw. This method should be called to restore the state of
+ * the WebView before using the object. If it is called after the WebView
+ * has had a chance to build state (load pages, create a back/forward list,
+ * etc.) there may be undesirable side-effects.
+ * @param inState The incoming Bundle of state.
+ * @return The restored back/forward list or null if restoreState failed.
+ */
+ public WebBackForwardList restoreState(Bundle inState) {
+ WebBackForwardList returnList = null;
+ if (inState.containsKey("index") && inState.containsKey("history")) {
+ mCertificate = SslCertificate.restoreState(
+ inState.getBundle("certificate"));
+
+ final WebBackForwardList list = mCallbackProxy.getBackForwardList();
+ final int index = inState.getInt("index");
+ // We can't use a clone of the list because we need to modify the
+ // shared copy, so synchronize instead to prevent concurrent
+ // modifications.
+ synchronized (list) {
+ final List<byte[]> history =
+ (List<byte[]>) inState.getSerializable("history");
+ final int size = history.size();
+ // Check the index bounds so we don't crash in native code while
+ // restoring the history index.
+ if (index < 0 || index >= size) {
+ return null;
+ }
+ for (int i = 0; i < size; i++) {
+ byte[] data = history.remove(0);
+ if (data == null) {
+ // If we somehow have null data, we cannot reconstruct
+ // the item and thus our history list cannot be rebuilt.
+ return null;
+ }
+ WebHistoryItem item = new WebHistoryItem(data);
+ list.addHistoryItem(item);
+ }
+ if (inState.containsKey("picture")) {
+ String path = inState.getString("picture");
+ File f = new File(path);
+ if (f.exists()) {
+ Picture p = null;
+ try {
+ final FileInputStream in = new FileInputStream(f);
+ p = Picture.createFromStream(in);
+ in.close();
+ f.delete();
+ } catch (FileNotFoundException e){
+ e.printStackTrace();
+ } catch (RuntimeException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ if (p != null) {
+ int sx = inState.getInt("scrollX", 0);
+ int sy = inState.getInt("scrollY", 0);
+ float scale = inState.getFloat("scale", 1.0f);
+ mDrawHistory = true;
+ mHistoryPicture = p;
+ mScrollX = sx;
+ mScrollY = sy;
+ mHistoryWidth = Math.round(p.getWidth() * scale);
+ mHistoryHeight = Math.round(p.getHeight() * scale);
+ // as getWidth() / getHeight() of the view are not
+ // available yet, set up mActualScale, so that when
+ // onSizeChanged() is called, the rest will be set
+ // correctly
+ mActualScale = scale;
+ invalidate();
+ }
+ }
+ }
+ // Grab the most recent copy to return to the caller.
+ returnList = copyBackForwardList();
+ // Update the copy to have the correct index.
+ returnList.setCurrentIndex(index);
+ }
+ // Remove all pending messages because we are restoring previous
+ // state.
+ mWebViewCore.removeMessages();
+ // Send a restore state message.
+ mWebViewCore.sendMessage(EventHub.RESTORE_STATE, index);
+ }
+ return returnList;
+ }
+
+ /**
+ * Load the given url.
+ * @param url The url of the resource to load.
+ */
+ public void loadUrl(String url) {
+ switchOutDrawHistory();
+ mWebViewCore.sendMessage(EventHub.LOAD_URL, url);
+ clearTextEntry();
+ }
+
+ /**
+ * Load the given data into the WebView. This will load the data into
+ * WebView using the data: scheme. Content loaded through this mechanism
+ * does not have the ability to load content from the network.
+ * @param data A String of data in the given encoding.
+ * @param mimeType The MIMEType of the data. i.e. text/html, image/jpeg
+ * @param encoding The encoding of the data. i.e. utf-8, base64
+ */
+ public void loadData(String data, String mimeType, String encoding) {
+ loadUrl("data:" + mimeType + ";" + encoding + "," + data);
+ }
+
+ /**
+ * Load the given data into the WebView, use the provided URL as the base
+ * URL for the content. The base URL is the URL that represents the page
+ * that is loaded through this interface. As such, it is used for the
+ * history entry and to resolve any relative URLs.
+ * The failUrl is used if browser fails to load the data provided. If it
+ * is empty or null, and the load fails, then no history entry is created.
+ * @param baseUrl Url to resolve relative paths with, if null defaults to
+ * "about:blank"
+ * @param data A String of data in the given encoding.
+ * @param mimeType The MIMEType of the data. i.e. text/html. If null,
+ * defaults to "text/html"
+ * @param encoding The encoding of the data. i.e. utf-8, us-ascii
+ * @param failUrl URL to use if the content fails to load or null.
+ */
+ public void loadDataWithBaseURL(String baseUrl, String data,
+ String mimeType, String encoding, String failUrl) {
+
+ if (baseUrl != null && baseUrl.toLowerCase().startsWith("data:")) {
+ loadData(data, mimeType, encoding);
+ return;
+ }
+ switchOutDrawHistory();
+ HashMap arg = new HashMap();
+ arg.put("baseUrl", baseUrl);
+ arg.put("data", data);
+ arg.put("mimeType", mimeType);
+ arg.put("encoding", encoding);
+ arg.put("failUrl", failUrl);
+ mWebViewCore.sendMessage(EventHub.LOAD_DATA, arg);
+ clearTextEntry();
+ }
+
+ /**
+ * Stop the current load.
+ */
+ public void stopLoading() {
+ // TODO: should we clear all the messages in the queue before sending
+ // STOP_LOADING?
+ switchOutDrawHistory();
+ mWebViewCore.sendMessage(EventHub.STOP_LOADING);
+ }
+
+ /**
+ * Reload the current url.
+ */
+ public void reload() {
+ switchOutDrawHistory();
+ mWebViewCore.sendMessage(EventHub.RELOAD);
+ }
+
+ /**
+ * Return true if this WebView has a back history item.
+ * @return True iff this WebView has a back history item.
+ */
+ public boolean canGoBack() {
+ WebBackForwardList l = mCallbackProxy.getBackForwardList();
+ synchronized (l) {
+ if (l.getClearPending()) {
+ return false;
+ } else {
+ return l.getCurrentIndex() > 0;
+ }
+ }
+ }
+
+ /**
+ * Go back in the history of this WebView.
+ */
+ public void goBack() {
+ goBackOrForward(-1);
+ }
+
+ /**
+ * Return true if this WebView has a forward history item.
+ * @return True iff this Webview has a forward history item.
+ */
+ public boolean canGoForward() {
+ WebBackForwardList l = mCallbackProxy.getBackForwardList();
+ synchronized (l) {
+ if (l.getClearPending()) {
+ return false;
+ } else {
+ return l.getCurrentIndex() < l.getSize() - 1;
+ }
+ }
+ }
+
+ /**
+ * Go forward in the history of this WebView.
+ */
+ public void goForward() {
+ goBackOrForward(1);
+ }
+
+ /**
+ * Return true if the page can go back or forward the given
+ * number of steps.
+ * @param steps The negative or positive number of steps to move the
+ * history.
+ */
+ public boolean canGoBackOrForward(int steps) {
+ WebBackForwardList l = mCallbackProxy.getBackForwardList();
+ synchronized (l) {
+ if (l.getClearPending()) {
+ return false;
+ } else {
+ int newIndex = l.getCurrentIndex() + steps;
+ return newIndex >= 0 && newIndex < l.getSize();
+ }
+ }
+ }
+
+ /**
+ * Go to the history item that is the number of steps away from
+ * the current item. Steps is negative if backward and positive
+ * if forward.
+ * @param steps The number of steps to take back or forward in the back
+ * forward list.
+ */
+ public void goBackOrForward(int steps) {
+ goBackOrForward(steps, false);
+ }
+
+ private void goBackOrForward(int steps, boolean ignoreSnapshot) {
+ // every time we go back or forward, we want to reset the
+ // WebView certificate:
+ // if the new site is secure, we will reload it and get a
+ // new certificate set;
+ // if the new site is not secure, the certificate must be
+ // null, and that will be the case
+ mCertificate = null;
+ if (steps != 0) {
+ clearTextEntry();
+ mWebViewCore.sendMessage(EventHub.GO_BACK_FORWARD, steps,
+ ignoreSnapshot ? 1 : 0);
+ }
+ }
+
+ private boolean extendScroll(int y) {
+ int finalY = mScroller.getFinalY();
+ int newY = pinLocY(finalY + y);
+ if (newY == finalY) return false;
+ mScroller.setFinalY(newY);
+ mScroller.extendDuration(computeDuration(0, y));
+ return true;
+ }
+
+ /**
+ * Scroll the contents of the view up by half the view size
+ * @param top true to jump to the top of the page
+ * @return true if the page was scrolled
+ */
+ public boolean pageUp(boolean top) {
+ if (mNativeClass == 0) {
+ return false;
+ }
+ nativeClearFocus(-1, -1);
+ if (top) {
+ // go to the top of the document
+ return pinScrollTo(mScrollX, 0, true);
+ }
+ // Page up
+ int h = getHeight();
+ int y;
+ if (h > 2 * PAGE_SCROLL_OVERLAP) {
+ y = -h + PAGE_SCROLL_OVERLAP;
+ } else {
+ y = -h / 2;
+ }
+ mUserScroll = true;
+ return mScroller.isFinished() ? pinScrollBy(0, y, true)
+ : extendScroll(y);
+ }
+
+ /**
+ * Scroll the contents of the view down by half the page size
+ * @param bottom true to jump to bottom of page
+ * @return true if the page was scrolled
+ */
+ public boolean pageDown(boolean bottom) {
+ if (mNativeClass == 0) {
+ return false;
+ }
+ nativeClearFocus(-1, -1);
+ if (bottom) {
+ return pinScrollTo(mScrollX, mContentHeight, true);
+ }
+ // Page down.
+ int h = getHeight();
+ int y;
+ if (h > 2 * PAGE_SCROLL_OVERLAP) {
+ y = h - PAGE_SCROLL_OVERLAP;
+ } else {
+ y = h / 2;
+ }
+ mUserScroll = true;
+ return mScroller.isFinished() ? pinScrollBy(0, y, true)
+ : extendScroll(y);
+ }
+
+ /**
+ * Clear the view so that onDraw() will draw nothing but white background,
+ * and onMeasure() will return 0 if MeasureSpec is not MeasureSpec.EXACTLY
+ */
+ public void clearView() {
+ mContentWidth = 0;
+ mContentHeight = 0;
+ mWebViewCore.clearContentPicture();
+ }
+
+ /**
+ * Return a new picture that captures the current display of the webview.
+ * This is a copy of the display, and will be unaffected if the webview
+ * later loads a different URL.
+ *
+ * @return a picture containing the current contents of the view. Note this
+ * picture is of the entire document, and is not restricted to the
+ * bounds of the view.
+ */
+ public Picture capturePicture() {
+ if (null == mWebViewCore) return null; // check for out of memory tab
+ return mWebViewCore.copyContentPicture();
+ }
+
+ /**
+ * Return true if the browser is displaying a TextView for text input.
+ */
+ private boolean inEditingMode() {
+ return mTextEntry != null && mTextEntry.getParent() != null
+ && mTextEntry.hasFocus();
+ }
+
+ private void clearTextEntry() {
+ if (inEditingMode()) {
+ mTextEntry.remove();
+ }
+ }
+
+ /**
+ * Return the current scale of the WebView
+ * @return The current scale.
+ */
+ public float getScale() {
+ return mActualScale;
+ }
+
+ /**
+ * Set the initial scale for the WebView. 0 means default. If
+ * {@link WebSettings#getUseWideViewPort()} is true, it zooms out all the
+ * way. Otherwise it starts with 100%. If initial scale is greater than 0,
+ * WebView starts will this value as initial scale.
+ *
+ * @param scaleInPercent The initial scale in percent.
+ */
+ public void setInitialScale(int scaleInPercent) {
+ mInitialScale = scaleInPercent;
+ }
+
+ /**
+ * Invoke the graphical zoom picker widget for this WebView. This will
+ * result in the zoom widget appearing on the screen to control the zoom
+ * level of this WebView.
+ */
+ public void invokeZoomPicker() {
+ if (!getSettings().supportZoom()) {
+ Log.w(LOGTAG, "This WebView doesn't support zoom.");
+ return;
+ }
+ clearTextEntry();
+ ExtendedZoomControls zoomControls = (ExtendedZoomControls)
+ getZoomControls();
+ zoomControls.show(canZoomScrollOut());
+ zoomControls.requestFocus();
+ mPrivateHandler.removeCallbacks(mZoomControlRunnable);
+ mPrivateHandler.postDelayed(mZoomControlRunnable,
+ ZOOM_CONTROLS_TIMEOUT);
+ }
+
+ /**
+ * Return a HitTestResult based on the current focus node. If a HTML::a tag
+ * is found, the HitTestResult type is set to ANCHOR_TYPE and the url has to
+ * be retrieved through {@link #requestFocusNodeHref} asynchronously. If a
+ * HTML::img tag is found, the HitTestResult type is set to IMAGE_TYPE and
+ * the url has to be retrieved through {@link #requestFocusNodeHref}
+ * asynchronously. If a phone number is found, the HitTestResult type is set
+ * to PHONE_TYPE and the phone number is set in the "extra" field of
+ * HitTestResult. If a map address is found, the HitTestResult type is set
+ * to GEO_TYPE and the address is set in the "extra" field of HitTestResult.
+ * If an email address is found, the HitTestResult type is set to EMAIL_TYPE
+ * and the email is set in the "extra" field of HitTestResult. Otherwise,
+ * HitTestResult type is set to UNKNOWN_TYPE.
+ */
+ public HitTestResult getHitTestResult() {
+ if (mNativeClass == 0) {
+ return null;
+ }
+
+ HitTestResult result = new HitTestResult();
+
+ if (nativeUpdateFocusNode()) {
+ FocusNode node = mFocusNode;
+ if (node.mIsAnchor && node.mText == null) {
+ result.setType(HitTestResult.ANCHOR_TYPE);
+ } else if (node.mIsTextField || node.mIsTextArea) {
+ result.setType(HitTestResult.EDIT_TEXT_TYPE);
+ } else {
+ String text = node.mText;
+ if (text != null) {
+ if (text.startsWith(SCHEME_TEL)) {
+ result.setType(HitTestResult.PHONE_TYPE);
+ result.setExtra(text.substring(SCHEME_TEL.length()));
+ } else if (text.startsWith(SCHEME_MAILTO)) {
+ result.setType(HitTestResult.EMAIL_TYPE);
+ result.setExtra(text.substring(SCHEME_MAILTO.length()));
+ } else if (text.startsWith(SCHEME_GEO)) {
+ result.setType(HitTestResult.GEO_TYPE);
+ result.setExtra(URLDecoder.decode(text
+ .substring(SCHEME_GEO.length())));
+ }
+ }
+ }
+ }
+ int type = result.getType();
+ if (type == HitTestResult.UNKNOWN_TYPE
+ || type == HitTestResult.ANCHOR_TYPE) {
+ // Now check to see if it is an image.
+ int contentX = viewToContent((int) mLastTouchX + mScrollX);
+ int contentY = viewToContent((int) mLastTouchY + mScrollY);
+ if (nativeIsImage(contentX, contentY)) {
+ result.setType(type == HitTestResult.UNKNOWN_TYPE ?
+ HitTestResult.IMAGE_TYPE :
+ HitTestResult.IMAGE_ANCHOR_TYPE);
+ }
+ if (nativeHasSrcUrl()) {
+ result.setType(result.getType() == HitTestResult.ANCHOR_TYPE ?
+ HitTestResult.SRC_ANCHOR_TYPE :
+ HitTestResult.SRC_IMAGE_ANCHOR_TYPE);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Request the href of an anchor element due to getFocusNodePath returning
+ * "href." If hrefMsg is null, this method returns immediately and does not
+ * dispatch hrefMsg to its target.
+ *
+ * @param hrefMsg This message will be dispatched with the result of the
+ * request as the data member with "url" as key. The result can
+ * be null.
+ */
+ public void requestFocusNodeHref(Message hrefMsg) {
+ if (hrefMsg == null || mNativeClass == 0) {
+ return;
+ }
+ if (nativeUpdateFocusNode()) {
+ FocusNode node = mFocusNode;
+ if (node.mIsAnchor) {
+ mWebViewCore.sendMessage(EventHub.REQUEST_FOCUS_HREF,
+ node.mFramePointer, node.mNodePointer, hrefMsg);
+ }
+ }
+ }
+
+ /**
+ * Request the url of the image last touched by the user. msg will be sent
+ * to its target with a String representing the url as its object.
+ *
+ * @param msg This message will be dispatched with the result of the request
+ * as the data member with "url" as key. The result can be null.
+ */
+ public void requestImageRef(Message msg) {
+ if (msg == null || mNativeClass == 0) {
+ return;
+ }
+ int contentX = viewToContent((int) mLastTouchX + mScrollX);
+ int contentY = viewToContent((int) mLastTouchY + mScrollY);
+ mWebViewCore.sendMessage(EventHub.REQUEST_IMAGE_HREF, contentX,
+ contentY, msg);
+ }
+
+ private static int pinLoc(int x, int viewMax, int docMax) {
+// Log.d(LOGTAG, "-- pinLoc " + x + " " + viewMax + " " + docMax);
+ if (docMax < viewMax) { // the doc has room on the sides for "blank"
+ x = -(viewMax - docMax) >> 1;
+// Log.d(LOGTAG, "--- center " + x);
+ } else if (x < 0) {
+ x = 0;
+// Log.d(LOGTAG, "--- zero");
+ } else if (x + viewMax > docMax) {
+ x = docMax - viewMax;
+// Log.d(LOGTAG, "--- pin " + x);
+ }
+ return x;
+ }
+
+ // Expects x in view coordinates
+ private int pinLocX(int x) {
+ return pinLoc(x, getViewWidth(), computeHorizontalScrollRange());
+ }
+
+ // Expects y in view coordinates
+ private int pinLocY(int y) {
+ return pinLoc(y, getViewHeight(), computeVerticalScrollRange());
+ }
+
+ /*package*/ int viewToContent(int x) {
+ return Math.round(x * mInvActualScale);
+ }
+
+ private int contentToView(int x) {
+ return Math.round(x * mActualScale);
+ }
+
+ /* call from webcoreview.draw(), so we're still executing in the UI thread
+ */
+ private void recordNewContentSize(int w, int h, boolean updateLayout) {
+
+ // premature data from webkit, ignore
+ if ((w | h) == 0) {
+ return;
+ }
+
+ // don't abort a scroll animation if we didn't change anything
+ if (mContentWidth != w || mContentHeight != h) {
+ // record new dimensions
+ mContentWidth = w;
+ mContentHeight = h;
+ // If history Picture is drawn, don't update scroll. They will be
+ // updated when we get out of that mode.
+ if (!mDrawHistory) {
+ // repin our scroll, taking into account the new content size
+ int oldX = mScrollX;
+ int oldY = mScrollY;
+ mScrollX = pinLocX(mScrollX);
+ mScrollY = pinLocY(mScrollY);
+ // android.util.Log.d("skia", "recordNewContentSize -
+ // abortAnimation");
+ mScroller.abortAnimation(); // just in case
+ if (oldX != mScrollX || oldY != mScrollY) {
+ sendOurVisibleRect();
+ }
+ }
+ }
+ contentSizeChanged(updateLayout);
+ }
+
+ private void setNewZoomScale(float scale, boolean force) {
+ if (scale < mMinZoomScale) {
+ scale = mMinZoomScale;
+ } else if (scale > mMaxZoomScale) {
+ scale = mMaxZoomScale;
+ }
+ if (scale != mActualScale || force) {
+ if (mDrawHistory) {
+ // If history Picture is drawn, don't update scroll. They will
+ // be updated when we get out of that mode.
+ if (scale != mActualScale) {
+ mCallbackProxy.onScaleChanged(mActualScale, scale);
+ }
+ mActualScale = scale;
+ mInvActualScale = 1 / scale;
+ sendViewSizeZoom();
+ } else {
+ // update our scroll so we don't appear to jump
+ // i.e. keep the center of the doc in the center of the view
+
+ int oldX = mScrollX;
+ int oldY = mScrollY;
+ float ratio = scale * mInvActualScale; // old inverse
+ float sx = ratio * oldX + (ratio - 1) * getViewWidth() * 0.5f;
+ float sy = ratio * oldY + (ratio - 1) * getViewHeight() * 0.5f;
+
+ // now update our new scale and inverse
+ if (scale != mActualScale) {
+ mCallbackProxy.onScaleChanged(mActualScale, scale);
+ }
+ mActualScale = scale;
+ mInvActualScale = 1 / scale;
+
+ // as we don't have animation for scaling, don't do animation
+ // for scrolling, as it causes weird intermediate state
+ // pinScrollTo(Math.round(sx), Math.round(sy));
+ mScrollX = pinLocX(Math.round(sx));
+ mScrollY = pinLocY(Math.round(sy));
+
+ sendViewSizeZoom();
+ sendOurVisibleRect();
+ }
+ }
+ }
+
+ // Used to avoid sending many visible rect messages.
+ private Rect mLastVisibleRectSent;
+
+ private Rect sendOurVisibleRect() {
+ Rect rect = new Rect();
+ calcOurContentVisibleRect(rect);
+ // Rect.equals() checks for null input.
+ if (!rect.equals(mLastVisibleRectSent)) {
+ mWebViewCore.sendMessage(EventHub.SET_VISIBLE_RECT, rect);
+ mLastVisibleRectSent = rect;
+ }
+ return rect;
+ }
+
+ // Sets r to be the visible rectangle of our webview in view coordinates
+ private void calcOurVisibleRect(Rect r) {
+ Point p = new Point();
+ getGlobalVisibleRect(r, p);
+ r.offset(-p.x, -p.y);
+ }
+
+ // Sets r to be our visible rectangle in content coordinates
+ private void calcOurContentVisibleRect(Rect r) {
+ calcOurVisibleRect(r);
+ r.left = viewToContent(r.left);
+ r.top = viewToContent(r.top);
+ r.right = viewToContent(r.right);
+ r.bottom = viewToContent(r.bottom);
+ }
+
+ /**
+ * Compute unzoomed width and height, and if they differ from the last
+ * values we sent, send them to webkit (to be used has new viewport)
+ *
+ * @return true if new values were sent
+ */
+ private boolean sendViewSizeZoom() {
+ int newWidth = Math.round(getViewWidth() * mInvActualScale);
+ int newHeight = Math.round(getViewHeight() * mInvActualScale);
+ /*
+ * Because the native side may have already done a layout before the
+ * View system was able to measure us, we have to send a height of 0 to
+ * remove excess whitespace when we grow our width. This will trigger a
+ * layout and a change in content size. This content size change will
+ * mean that contentSizeChanged will either call this method directly or
+ * indirectly from onSizeChanged.
+ */
+ if (newWidth > mLastWidthSent && mWrapContent) {
+ newHeight = 0;
+ }
+ // Avoid sending another message if the dimensions have not changed.
+ if (newWidth != mLastWidthSent || newHeight != mLastHeightSent) {
+ mWebViewCore.sendMessage(EventHub.VIEW_SIZE_CHANGED,
+ newWidth, newHeight, new Float(mActualScale));
+ mLastWidthSent = newWidth;
+ mLastHeightSent = newHeight;
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ protected int computeHorizontalScrollRange() {
+ if (mDrawHistory) {
+ return mHistoryWidth;
+ } else {
+ return contentToView(mContentWidth);
+ }
+ }
+
+ @Override
+ protected int computeVerticalScrollRange() {
+ if (mDrawHistory) {
+ return mHistoryHeight;
+ } else {
+ return contentToView(mContentHeight);
+ }
+ }
+
+ /**
+ * Get the url for the current page. This is not always the same as the url
+ * passed to WebViewClient.onPageStarted because although the load for
+ * that url has begun, the current page may not have changed.
+ * @return The url for the current page.
+ */
+ public String getUrl() {
+ WebHistoryItem h = mCallbackProxy.getBackForwardList().getCurrentItem();
+ return h != null ? h.getUrl() : null;
+ }
+
+ /**
+ * Get the title for the current page. This is the title of the current page
+ * until WebViewClient.onReceivedTitle is called.
+ * @return The title for the current page.
+ */
+ public String getTitle() {
+ WebHistoryItem h = mCallbackProxy.getBackForwardList().getCurrentItem();
+ return h != null ? h.getTitle() : null;
+ }
+
+ /**
+ * Get the favicon for the current page. This is the favicon of the current
+ * page until WebViewClient.onReceivedIcon is called.
+ * @return The favicon for the current page.
+ */
+ public Bitmap getFavicon() {
+ WebHistoryItem h = mCallbackProxy.getBackForwardList().getCurrentItem();
+ return h != null ? h.getFavicon() : null;
+ }
+
+ /**
+ * Get the progress for the current page.
+ * @return The progress for the current page between 0 and 100.
+ */
+ public int getProgress() {
+ return mCallbackProxy.getProgress();
+ }
+
+ /**
+ * @return the height of the HTML content.
+ */
+ public int getContentHeight() {
+ return mContentHeight;
+ }
+
+ /**
+ * Pause all layout, parsing, and javascript timers. This can be useful if
+ * the WebView is not visible or the application has been paused.
+ */
+ public void pauseTimers() {
+ mWebViewCore.sendMessage(EventHub.PAUSE_TIMERS);
+ }
+
+ /**
+ * Resume all layout, parsing, and javascript timers. This will resume
+ * dispatching all timers.
+ */
+ public void resumeTimers() {
+ mWebViewCore.sendMessage(EventHub.RESUME_TIMERS);
+ }
+
+ /**
+ * Clear the resource cache. This will cause resources to be re-downloaded
+ * if accessed again.
+ * <p>
+ * Note: this really needs to be a static method as it clears cache for all
+ * WebView. But we need mWebViewCore to send message to WebCore thread, so
+ * we can't make this static.
+ */
+ public void clearCache(boolean includeDiskFiles) {
+ mWebViewCore.sendMessage(EventHub.CLEAR_CACHE,
+ includeDiskFiles ? 1 : 0, 0);
+ }
+
+ /**
+ * Make sure that clearing the form data removes the adapter from the
+ * currently focused textfield if there is one.
+ */
+ public void clearFormData() {
+ if (inEditingMode()) {
+ ArrayAdapter<String> adapter = null;
+ mTextEntry.setAdapter(adapter);
+ }
+ }
+
+ /**
+ * Tell the WebView to clear its internal back/forward list.
+ */
+ public void clearHistory() {
+ mCallbackProxy.getBackForwardList().setClearPending();
+ mWebViewCore.sendMessage(EventHub.CLEAR_HISTORY);
+ }
+
+ /**
+ * Clear the SSL preferences table stored in response to proceeding with SSL
+ * certificate errors.
+ */
+ public void clearSslPreferences() {
+ mWebViewCore.sendMessage(EventHub.CLEAR_SSL_PREF_TABLE);
+ }
+
+ /**
+ * Return the WebBackForwardList for this WebView. This contains the
+ * back/forward list for use in querying each item in the history stack.
+ * This is a copy of the private WebBackForwardList so it contains only a
+ * snapshot of the current state. Multiple calls to this method may return
+ * different objects. The object returned from this method will not be
+ * updated to reflect any new state.
+ */
+ public WebBackForwardList copyBackForwardList() {
+ return mCallbackProxy.getBackForwardList().clone();
+ }
+
+ /*
+ * Making the find methods private since we are disabling for 1.0
+ *
+ * Find and highlight the next occurance of the String find, beginning with
+ * the current selection. Wraps the page infinitely, and scrolls. When
+ * WebCore determines whether it has found it, the response message is sent
+ * to its target with true(1) or false(0) as arg1 depending on whether the
+ * text was found.
+ * @param find String to find.
+ * @param response A Message object that will be dispatched with the result
+ * as the arg1 member. A result of 1 means the search
+ * succeeded.
+ */
+ private void findNext(String find, Message response) {
+ if (response == null) {
+ return;
+ }
+ Message m = Message.obtain(null, EventHub.FIND, 1, 0, response);
+ m.getData().putString("find", find);
+ mWebViewCore.sendMessage(m);
+ }
+
+ /*
+ * Making the find methods private since we are disabling for 1.0
+ *
+ * Find and highlight the previous occurance of the String find, beginning
+ * with the current selection.
+ * @param find String to find.
+ * @param response A Message object that will be dispatched with the result
+ * as the arg1 member. A result of 1 means the search
+ * succeeded.
+ */
+ private void findPrevious(String find, Message response) {
+ if (response == null) {
+ return;
+ }
+ Message m = Message.obtain(null, EventHub.FIND, -1, 0, response);
+ m.getData().putString("find", find);
+ mWebViewCore.sendMessage(m);
+ }
+
+ /*
+ * Making the find methods private since we are disabling for 1.0
+ *
+ * Find and highlight the first occurance of find, beginning with the start
+ * of the page.
+ * @param find String to find.
+ * @param response A Message object that will be dispatched with the result
+ * as the arg1 member. A result of 1 means the search
+ * succeeded.
+ */
+ private void findFirst(String find, Message response) {
+ if (response == null) {
+ return;
+ }
+ Message m = Message.obtain(null, EventHub.FIND, 0, 0, response);
+ m.getData().putString("find", find);
+ mWebViewCore.sendMessage(m);
+ }
+
+ /*
+ * Making the find methods private since we are disabling for 1.0
+ *
+ * Find all instances of find on the page and highlight them.
+ * @param find String to find.
+ * @param response A Message object that will be dispatched with the result
+ * as the arg1 member. The result will be the number of
+ * matches to the String find.
+ */
+ private void findAll(String find, Message response) {
+ if (response == null) {
+ return;
+ }
+ Message m = Message.obtain(null, EventHub.FIND_ALL, 0, 0, response);
+ m.getData().putString("find", find);
+ mWebViewCore.sendMessage(m);
+ }
+
+ /**
+ * Return the first substring consisting of the address of a physical
+ * location. Currently, only addresses in the United States are detected,
+ * and consist of:
+ * - a house number
+ * - a street name
+ * - a street type (Road, Circle, etc), either spelled out or abbreviated
+ * - a city name
+ * - a state or territory, either spelled out or two-letter abbr.
+ * - an optional 5 digit or 9 digit zip code.
+ *
+ * All names must be correctly capitalized, and the zip code, if present,
+ * must be valid for the state. The street type must be a standard USPS
+ * spelling or abbreviation. The state or territory must also be spelled
+ * or abbreviated using USPS standards. The house number may not exceed
+ * five digits.
+ * @param addr The string to search for addresses.
+ *
+ * @return the address, or if no address is found, return null.
+ */
+ public static String findAddress(String addr) {
+ return WebViewCore.nativeFindAddress(addr);
+ }
+
+ /*
+ * Making the find methods private since we are disabling for 1.0
+ *
+ * Clear the highlighting surrounding text matches created by findAll.
+ */
+ private void clearMatches() {
+ mWebViewCore.sendMessage(EventHub.CLEAR_MATCHES);
+ }
+
+ /**
+ * Query the document to see if it contains any image references. The
+ * message object will be dispatched with arg1 being set to 1 if images
+ * were found and 0 if the document does not reference any images.
+ * @param response The message that will be dispatched with the result.
+ */
+ public void documentHasImages(Message response) {
+ if (response == null) {
+ return;
+ }
+ mWebViewCore.sendMessage(EventHub.DOC_HAS_IMAGES, response);
+ }
+
+ @Override
+ public void computeScroll() {
+ if (mScroller.computeScrollOffset()) {
+ int oldX = mScrollX;
+ int oldY = mScrollY;
+ mScrollX = mScroller.getCurrX();
+ mScrollY = mScroller.getCurrY();
+ postInvalidate(); // So we draw again
+ if (oldX != mScrollX || oldY != mScrollY) {
+ // as onScrollChanged() is not called, sendOurVisibleRect()
+ // needs to be call explicitly
+ sendOurVisibleRect();
+ }
+ } else {
+ super.computeScroll();
+ }
+ }
+
+ private static int computeDuration(int dx, int dy) {
+ int distance = Math.max(Math.abs(dx), Math.abs(dy));
+ int duration = distance * 1000 / STD_SPEED;
+ return Math.min(duration, MAX_DURATION);
+ }
+
+ // helper to pin the scrollBy parameters (already in view coordinates)
+ // returns true if the scroll was changed
+ private boolean pinScrollBy(int dx, int dy, boolean animate) {
+ return pinScrollTo(mScrollX + dx, mScrollY + dy, animate);
+ }
+
+ // helper to pin the scrollTo parameters (already in view coordinates)
+ // returns true if the scroll was changed
+ private boolean pinScrollTo(int x, int y, boolean animate) {
+ x = pinLocX(x);
+ y = pinLocY(y);
+ int dx = x - mScrollX;
+ int dy = y - mScrollY;
+
+ if ((dx | dy) == 0) {
+ return false;
+ }
+
+ if (true && animate) {
+ // Log.d(LOGTAG, "startScroll: " + dx + " " + dy);
+
+ mScroller.startScroll(mScrollX, mScrollY, dx, dy,
+ computeDuration(dx, dy));
+ invalidate();
+ } else {
+ mScroller.abortAnimation(); // just in case
+ scrollTo(x, y);
+ }
+ return true;
+ }
+
+ // Scale from content to view coordinates, and pin.
+ // Also called by jni webview.cpp
+ private void setContentScrollBy(int cx, int cy) {
+ if (mDrawHistory) {
+ // disallow WebView to change the scroll position as History Picture
+ // is used in the view system.
+ // TODO: as we switchOutDrawHistory when trackball or navigation
+ // keys are hit, this should be safe. Right?
+ return;
+ }
+ cx = contentToView(cx);
+ cy = contentToView(cy);
+ if (mHeightCanMeasure) {
+ // move our visible rect according to scroll request
+ if (cy != 0) {
+ Rect tempRect = new Rect();
+ calcOurVisibleRect(tempRect);
+ tempRect.offset(cx, cy);
+ requestRectangleOnScreen(tempRect);
+ }
+ // FIXME: We scroll horizontally no matter what because currently
+ // ScrollView and ListView will not scroll horizontally.
+ // FIXME: Why do we only scroll horizontally if there is no
+ // vertical scroll?
+// Log.d(LOGTAG, "setContentScrollBy cy=" + cy);
+ if (cy == 0 && cx != 0) {
+ pinScrollBy(cx, 0, true);
+ }
+ } else {
+ pinScrollBy(cx, cy, true);
+ }
+ }
+
+ // scale from content to view coordinates, and pin
+ // return true if pin caused the final x/y different than the request cx/cy;
+ // return false if the view scroll to the exact position as it is requested.
+ private boolean setContentScrollTo(int cx, int cy) {
+ if (mDrawHistory) {
+ // disallow WebView to change the scroll position as History Picture
+ // is used in the view system.
+ // One known case where this is called is that WebCore tries to
+ // restore the scroll position. As history Picture already uses the
+ // saved scroll position, it is ok to skip this.
+ return false;
+ }
+ int vx = contentToView(cx);
+ int vy = contentToView(cy);
+// Log.d(LOGTAG, "content scrollTo [" + cx + " " + cy + "] view=[" +
+// vx + " " + vy + "]");
+ pinScrollTo(vx, vy, false);
+ if (mScrollX != vx || mScrollY != vy) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ // scale from content to view coordinates, and pin
+ private void spawnContentScrollTo(int cx, int cy) {
+ if (mDrawHistory) {
+ // disallow WebView to change the scroll position as History Picture
+ // is used in the view system.
+ return;
+ }
+ int vx = contentToView(cx);
+ int vy = contentToView(cy);
+ pinScrollTo(vx, vy, true);
+ }
+
+ /**
+ * These are from webkit, and are in content coordinate system (unzoomed)
+ */
+ private void contentSizeChanged(boolean updateLayout) {
+ // suppress 0,0 since we usually see real dimensions soon after
+ // this avoids drawing the prev content in a funny place. If we find a
+ // way to consolidate these notifications, this check may become
+ // obsolete
+ if ((mContentWidth | mContentHeight) == 0) {
+ return;
+ }
+
+ if (mHeightCanMeasure) {
+ if (getMeasuredHeight() != contentToView(mContentHeight)
+ && updateLayout) {
+ requestLayout();
+ }
+ } else if (mWidthCanMeasure) {
+ if (getMeasuredWidth() != contentToView(mContentWidth)
+ && updateLayout) {
+ requestLayout();
+ }
+ } else {
+ // If we don't request a layout, try to send our view size to the
+ // native side to ensure that WebCore has the correct dimensions.
+ sendViewSizeZoom();
+ }
+ }
+
+ /**
+ * Set the WebViewClient that will receive various notifications and
+ * requests. This will replace the current handler.
+ * @param client An implementation of WebViewClient.
+ */
+ public void setWebViewClient(WebViewClient client) {
+ mCallbackProxy.setWebViewClient(client);
+ }
+
+ /**
+ * Register the interface to be used when content can not be handled by
+ * the rendering engine, and should be downloaded instead. This will replace
+ * the current handler.
+ * @param listener An implementation of DownloadListener.
+ */
+ public void setDownloadListener(DownloadListener listener) {
+ mCallbackProxy.setDownloadListener(listener);
+ }
+
+ /**
+ * Set the chrome handler. This is an implementation of WebChromeClient for
+ * use in handling Javascript dialogs, favicons, titles, and the progress.
+ * This will replace the current handler.
+ * @param client An implementation of WebChromeClient.
+ */
+ public void setWebChromeClient(WebChromeClient client) {
+ mCallbackProxy.setWebChromeClient(client);
+ }
+
+ /**
+ * Set the Picture listener. This is an interface used to receive
+ * notifications of a new Picture.
+ * @param listener An implementation of WebView.PictureListener.
+ */
+ public void setPictureListener(PictureListener listener) {
+ mPictureListener = listener;
+ }
+
+ /**
+ * {@hide}
+ */
+ /* FIXME: Debug only! Remove for SDK! */
+ public void externalRepresentation(Message callback) {
+ mWebViewCore.sendMessage(EventHub.REQUEST_EXT_REPRESENTATION, callback);
+ }
+
+ /**
+ * {@hide}
+ */
+ /* FIXME: Debug only! Remove for SDK! */
+ public void documentAsText(Message callback) {
+ mWebViewCore.sendMessage(EventHub.REQUEST_DOC_AS_TEXT, callback);
+ }
+
+ /**
+ * Use this function to bind an object to Javascript so that the
+ * methods can be accessed from Javascript.
+ * IMPORTANT, the object that is bound runs in another thread and
+ * not in the thread that it was constructed in.
+ * @param obj The class instance to bind to Javascript
+ * @param interfaceName The name to used to expose the class in Javascript
+ */
+ public void addJavascriptInterface(Object obj, String interfaceName) {
+ // Use Hashmap rather than Bundle as Bundles can't cope with Objects
+ HashMap arg = new HashMap();
+ arg.put("object", obj);
+ arg.put("interfaceName", interfaceName);
+ mWebViewCore.sendMessage(EventHub.ADD_JS_INTERFACE, arg);
+ }
+
+ /**
+ * Return the WebSettings object used to control the settings for this
+ * WebView.
+ * @return A WebSettings object that can be used to control this WebView's
+ * settings.
+ */
+ public WebSettings getSettings() {
+ return mWebViewCore.getSettings();
+ }
+
+ /**
+ * Return the list of currently loaded plugins.
+ * @return The list of currently loaded plugins.
+ */
+ public static synchronized PluginList getPluginList() {
+ if (sPluginList == null) {
+ sPluginList = new PluginList();
+ }
+ return sPluginList;
+ }
+
+ /**
+ * Signal the WebCore thread to refresh its list of plugins. Use
+ * this if the directory contents of one of the plugin directories
+ * has been modified and needs its changes reflecting. May cause
+ * plugin load and/or unload.
+ * @param reloadOpenPages Set to true to reload all open pages.
+ */
+ public void refreshPlugins(boolean reloadOpenPages) {
+ if (mWebViewCore != null) {
+ mWebViewCore.sendMessage(EventHub.REFRESH_PLUGINS, reloadOpenPages);
+ }
+ }
+
+ //-------------------------------------------------------------------------
+ // Override View methods
+ //-------------------------------------------------------------------------
+
+ @Override
+ protected void finalize() throws Throwable {
+ destroy();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ // if mNativeClass is 0, the WebView has been destroyed. Do nothing.
+ if (mNativeClass == 0) {
+ return;
+ }
+ if (mWebViewCore.mEndScaleZoom) {
+ mWebViewCore.mEndScaleZoom = false;
+ if (mTouchMode >= FIRST_SCROLL_ZOOM
+ && mTouchMode <= LAST_SCROLL_ZOOM) {
+ setHorizontalScrollBarEnabled(true);
+ setVerticalScrollBarEnabled(true);
+ mTouchMode = TOUCH_DONE_MODE;
+ }
+ }
+ int sc = canvas.save();
+ if (mTouchMode >= FIRST_SCROLL_ZOOM && mTouchMode <= LAST_SCROLL_ZOOM) {
+ scrollZoomDraw(canvas);
+ } else {
+ nativeRecomputeFocus();
+ // Update the buttons in the picture, so when we draw the picture
+ // to the screen, they are in the correct state.
+ nativeRecordButtons();
+ drawCoreAndFocusRing(canvas, mBackgroundColor, mDrawFocusRing);
+ }
+ canvas.restoreToCount(sc);
+ }
+
+ @Override
+ public void setLayoutParams(ViewGroup.LayoutParams params) {
+ if (params.height == LayoutParams.WRAP_CONTENT) {
+ mWrapContent = true;
+ }
+ super.setLayoutParams(params);
+ }
+
+ @Override
+ public boolean performLongClick() {
+ if (inEditingMode()) {
+ return mTextEntry.performLongClick();
+ } else {
+ return super.performLongClick();
+ }
+ }
+
+ private void drawCoreAndFocusRing(Canvas canvas, int color,
+ boolean drawFocus) {
+ if (mDrawHistory) {
+ canvas.scale(mActualScale, mActualScale);
+ canvas.drawPicture(mHistoryPicture);
+ return;
+ }
+
+ boolean animateZoom = mZoomScale != 0;
+ boolean animateScroll = !mScroller.isFinished()
+ || mVelocityTracker != null;
+ if (animateZoom) {
+ float zoomScale;
+ int interval = (int) (SystemClock.uptimeMillis() - mZoomStart);
+ if (interval < ZOOM_ANIMATION_LENGTH) {
+ float ratio = (float) interval / ZOOM_ANIMATION_LENGTH;
+ zoomScale = 1.0f / (mInvInitialZoomScale
+ + (mInvFinalZoomScale - mInvInitialZoomScale) * ratio);
+ invalidate();
+ } else {
+ zoomScale = mZoomScale;
+ }
+ float scale = (mActualScale - zoomScale) * mInvActualScale;
+ float tx = scale * ((getLeft() + getRight()) * 0.5f + mScrollX);
+ float ty = scale * ((getTop() + getBottom()) * 0.5f + mScrollY);
+
+ // this block pins the translate to "legal" bounds. This makes the
+ // animation a bit non-obvious, but it means we won't pop when the
+ // "real" zoom takes effect
+ if (true) {
+ // canvas.translate(mScrollX, mScrollY);
+ tx -= mScrollX;
+ ty -= mScrollY;
+ tx = -pinLoc(-Math.round(tx), getViewWidth(), Math
+ .round(mContentWidth * zoomScale));
+ ty = -pinLoc(-Math.round(ty), getViewHeight(), Math
+ .round(mContentHeight * zoomScale));
+ tx += mScrollX;
+ ty += mScrollY;
+ }
+ canvas.translate(tx, ty);
+ canvas.scale(zoomScale, zoomScale);
+ } else {
+ canvas.scale(mActualScale, mActualScale);
+ }
+
+ mWebViewCore.drawContentPicture(canvas, color, animateZoom,
+ animateScroll);
+
+ if (mNativeClass == 0) return;
+ if (mShiftIsPressed) {
+ nativeDrawSelection(canvas, mSelectX, mSelectY, mExtendSelection);
+ } else if (drawFocus) {
+ if (mTouchMode == TOUCH_SHORTPRESS_START_MODE) {
+ mTouchMode = TOUCH_SHORTPRESS_MODE;
+ HitTestResult hitTest = getHitTestResult();
+ if (hitTest != null &&
+ hitTest.mType != HitTestResult.UNKNOWN_TYPE) {
+ mPrivateHandler.sendMessageDelayed(mPrivateHandler
+ .obtainMessage(SWITCH_TO_LONGPRESS),
+ LONG_PRESS_TIMEOUT);
+ }
+ }
+ nativeDrawFocusRing(canvas);
+ }
+ }
+
+ private float scrollZoomGridScale(float invScale) {
+ float griddedInvScale = (int) (invScale * SCROLL_ZOOM_GRID)
+ / (float) SCROLL_ZOOM_GRID;
+ return 1.0f / griddedInvScale;
+ }
+
+ private float scrollZoomX(float scale) {
+ int width = getViewWidth();
+ float maxScrollZoomX = mContentWidth * scale - width;
+ int maxX = mContentWidth - width;
+ return -(maxScrollZoomX > 0 ? mZoomScrollX * maxScrollZoomX / maxX
+ : maxScrollZoomX / 2);
+ }
+
+ private float scrollZoomY(float scale) {
+ int height = getViewHeight();
+ float maxScrollZoomY = mContentHeight * scale - height;
+ int maxY = mContentHeight - height;
+ return -(maxScrollZoomY > 0 ? mZoomScrollY * maxScrollZoomY / maxY
+ : maxScrollZoomY / 2);
+ }
+
+ private void drawMagnifyFrame(Canvas canvas, Rect frame, Paint paint) {
+ final float ADORNMENT_LEN = 16.0f;
+ float width = frame.width();
+ float height = frame.height();
+ Path path = new Path();
+ path.moveTo(-ADORNMENT_LEN, -ADORNMENT_LEN);
+ path.lineTo(0, 0);
+ path.lineTo(width, 0);
+ path.lineTo(width + ADORNMENT_LEN, -ADORNMENT_LEN);
+ path.moveTo(-ADORNMENT_LEN, height + ADORNMENT_LEN);
+ path.lineTo(0, height);
+ path.lineTo(width, height);
+ path.lineTo(width + ADORNMENT_LEN, height + ADORNMENT_LEN);
+ path.moveTo(0, 0);
+ path.lineTo(0, height);
+ path.moveTo(width, 0);
+ path.lineTo(width, height);
+ path.offset(frame.left, frame.top);
+ canvas.drawPath(path, paint);
+ }
+
+ // Returns frame surrounding magified portion of screen while
+ // scroll-zoom is enabled. The frame is also used to center the
+ // zoom-in zoom-out points at the start and end of the animation.
+ private Rect scrollZoomFrame(int width, int height, float halfScale) {
+ Rect scrollFrame = new Rect();
+ scrollFrame.set(mZoomScrollX, mZoomScrollY,
+ mZoomScrollX + width, mZoomScrollY + height);
+ if (mContentWidth == width) {
+ float offsetX = (width * halfScale - width) / 2;
+ scrollFrame.left -= offsetX;
+ scrollFrame.right += offsetX;
+ }
+ if (mContentHeight == height) {
+ float offsetY = (height * halfScale - height) / 2;
+ scrollFrame.top -= offsetY;
+ scrollFrame.bottom += offsetY;
+ }
+ return scrollFrame;
+ }
+
+ private float scrollZoomMagScale(float invScale) {
+ return (invScale * 2 + mInvActualScale) / 3;
+ }
+
+ private void scrollZoomDraw(Canvas canvas) {
+ float invScale = mZoomScrollInvLimit;
+ int elapsed = 0;
+ if (mTouchMode != SCROLL_ZOOM_OUT) {
+ elapsed = (int) Math.min(System.currentTimeMillis()
+ - mZoomScrollStart, SCROLL_ZOOM_DURATION);
+ float transitionScale = (mZoomScrollInvLimit - mInvActualScale)
+ * elapsed / SCROLL_ZOOM_DURATION;
+ if (mTouchMode == SCROLL_ZOOM_ANIMATION_OUT) {
+ invScale = mInvActualScale + transitionScale;
+ } else { /* if (mTouchMode == SCROLL_ZOOM_ANIMATION_IN) */
+ invScale = mZoomScrollInvLimit - transitionScale;
+ }
+ }
+ float scale = scrollZoomGridScale(invScale);
+ invScale = 1.0f / scale;
+ int width = getViewWidth();
+ int height = getViewHeight();
+ float halfScale = scrollZoomMagScale(invScale);
+ Rect scrollFrame = scrollZoomFrame(width, height, halfScale);
+ if (elapsed == SCROLL_ZOOM_DURATION) {
+ if (mTouchMode == SCROLL_ZOOM_ANIMATION_IN) {
+ setHorizontalScrollBarEnabled(true);
+ setVerticalScrollBarEnabled(true);
+ updateTextEntry();
+ scrollTo((int) (scrollFrame.centerX() * mActualScale)
+ - (width >> 1), (int) (scrollFrame.centerY()
+ * mActualScale) - (height >> 1));
+ mTouchMode = TOUCH_DONE_MODE;
+ } else {
+ mTouchMode = SCROLL_ZOOM_OUT;
+ }
+ }
+ float newX = scrollZoomX(scale);
+ float newY = scrollZoomY(scale);
+ if (LOGV_ENABLED) {
+ Log.v(LOGTAG, "scrollZoomDraw scale=" + scale + " + (" + newX
+ + ", " + newY + ") mZoomScroll=(" + mZoomScrollX + ", "
+ + mZoomScrollY + ")" + " invScale=" + invScale + " scale="
+ + scale);
+ }
+ canvas.translate(newX, newY);
+ canvas.scale(scale, scale);
+ boolean animating = mTouchMode != SCROLL_ZOOM_OUT;
+ if (mDrawHistory) {
+ int sc = canvas.save(Canvas.CLIP_SAVE_FLAG);
+ Rect clip = new Rect(0, 0, mHistoryPicture.getWidth(),
+ mHistoryPicture.getHeight());
+ canvas.clipRect(clip, Region.Op.DIFFERENCE);
+ canvas.drawColor(mBackgroundColor);
+ canvas.restoreToCount(sc);
+ canvas.drawPicture(mHistoryPicture);
+ } else {
+ mWebViewCore.drawContentPicture(canvas, mBackgroundColor,
+ animating, true);
+ }
+ if (mTouchMode == TOUCH_DONE_MODE) {
+ return;
+ }
+ Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ paint.setStyle(Paint.Style.STROKE);
+ paint.setStrokeWidth(30.0f);
+ paint.setARGB(0x50, 0, 0, 0);
+ int maxX = mContentWidth - width;
+ int maxY = mContentHeight - height;
+ if (true) { // experiment: draw hint to place finger off magnify area
+ drawMagnifyFrame(canvas, scrollFrame, paint);
+ } else {
+ canvas.drawRect(scrollFrame, paint);
+ }
+ int sc = canvas.save();
+ canvas.clipRect(scrollFrame);
+ float halfX = maxX > 0 ? (float) mZoomScrollX / maxX : 0.5f;
+ float halfY = maxY > 0 ? (float) mZoomScrollY / maxY : 0.5f;
+ canvas.scale(halfScale, halfScale, mZoomScrollX + width * halfX
+ , mZoomScrollY + height * halfY);
+ if (LOGV_ENABLED) {
+ Log.v(LOGTAG, "scrollZoomDraw halfScale=" + halfScale + " w/h=("
+ + width + ", " + height + ") half=(" + halfX + ", "
+ + halfY + ")");
+ }
+ if (mDrawHistory) {
+ canvas.drawPicture(mHistoryPicture);
+ } else {
+ mWebViewCore.drawContentPicture(canvas, mBackgroundColor,
+ animating, false);
+ }
+ canvas.restoreToCount(sc);
+ if (mTouchMode != SCROLL_ZOOM_OUT) {
+ invalidate();
+ }
+ }
+
+ private void zoomScrollTap(float x, float y) {
+ float scale = scrollZoomGridScale(mZoomScrollInvLimit);
+ float left = scrollZoomX(scale);
+ float top = scrollZoomY(scale);
+ int width = getViewWidth();
+ int height = getViewHeight();
+ x -= width * scale / 2;
+ y -= height * scale / 2;
+ mZoomScrollX = Math.min(mContentWidth - width
+ , Math.max(0, (int) ((x - left) / scale)));
+ mZoomScrollY = Math.min(mContentHeight - height
+ , Math.max(0, (int) ((y - top) / scale)));
+ if (LOGV_ENABLED) {
+ Log.v(LOGTAG, "zoomScrollTap scale=" + scale + " + (" + left
+ + ", " + top + ") mZoomScroll=(" + mZoomScrollX + ", "
+ + mZoomScrollY + ")" + " x=" + x + " y=" + y);
+ }
+ }
+
+ private boolean canZoomScrollOut() {
+ if (mContentWidth == 0 || mContentHeight == 0) {
+ return false;
+ }
+ int width = getViewWidth();
+ int height = getViewHeight();
+ float x = (float) width / (float) mContentWidth;
+ float y = (float) height / (float) mContentHeight;
+ mZoomScrollLimit = Math.max(DEFAULT_MIN_ZOOM_SCALE, Math.min(x, y));
+ mZoomScrollInvLimit = 1.0f / mZoomScrollLimit;
+ if (LOGV_ENABLED) {
+ Log.v(LOGTAG, "canZoomScrollOut"
+ + " mInvActualScale=" + mInvActualScale
+ + " mZoomScrollLimit=" + mZoomScrollLimit
+ + " mZoomScrollInvLimit=" + mZoomScrollInvLimit
+ + " mContentWidth=" + mContentWidth
+ + " mContentHeight=" + mContentHeight
+ );
+ }
+ // don't zoom out unless magnify area is at least half as wide
+ // or tall as content
+ float limit = mZoomScrollLimit * 2;
+ return mContentWidth >= width * limit
+ || mContentHeight >= height * limit;
+ }
+
+ private void startZoomScrollOut() {
+ setHorizontalScrollBarEnabled(false);
+ setVerticalScrollBarEnabled(false);
+ if (mZoomControlRunnable != null) {
+ mPrivateHandler.removeCallbacks(mZoomControlRunnable);
+ }
+ if (mZoomControls != null) {
+ mZoomControls.hide();
+ }
+ int width = getViewWidth();
+ int height = getViewHeight();
+ int halfW = width >> 1;
+ mLastTouchX = halfW;
+ int halfH = height >> 1;
+ mLastTouchY = halfH;
+ mScroller.abortAnimation();
+ mZoomScrollStart = System.currentTimeMillis();
+ Rect zoomFrame = scrollZoomFrame(width, height
+ , scrollZoomMagScale(mZoomScrollInvLimit));
+ mZoomScrollX = Math.max(0, (int) ((mScrollX + halfW) * mInvActualScale)
+ - (zoomFrame.width() >> 1));
+ mZoomScrollY = Math.max(0, (int) ((mScrollY + halfH) * mInvActualScale)
+ - (zoomFrame.height() >> 1));
+ scrollTo(0, 0); // triggers inval, starts animation
+ clearTextEntry();
+ if (LOGV_ENABLED) {
+ Log.v(LOGTAG, "startZoomScrollOut mZoomScroll=("
+ + mZoomScrollX + ", " + mZoomScrollY +")");
+ }
+ }
+
+ private void zoomScrollOut() {
+ if (canZoomScrollOut() == false) return;
+ startZoomScrollOut();
+ mTouchMode = SCROLL_ZOOM_ANIMATION_OUT;
+ invalidate();
+ }
+
+ private void moveZoomScrollWindow(float x, float y) {
+ if (Math.abs(x - mLastZoomScrollRawX) < 1.5f
+ && Math.abs(y - mLastZoomScrollRawY) < 1.5f) {
+ return;
+ }
+ mLastZoomScrollRawX = x;
+ mLastZoomScrollRawY = y;
+ int oldX = mZoomScrollX;
+ int oldY = mZoomScrollY;
+ int width = getViewWidth();
+ int height = getViewHeight();
+ int maxZoomX = mContentWidth - width;
+ if (maxZoomX > 0) {
+ int maxScreenX = width - (int) Math.ceil(width
+ * mZoomScrollLimit) - SCROLL_ZOOM_FINGER_BUFFER;
+ if (LOGV_ENABLED) {
+ Log.v(LOGTAG, "moveZoomScrollWindow-X"
+ + " maxScreenX=" + maxScreenX + " width=" + width
+ + " mZoomScrollLimit=" + mZoomScrollLimit + " x=" + x);
+ }
+ x += maxScreenX * mLastScrollX / maxZoomX - mLastTouchX;
+ x = Math.max(0, Math.min(maxScreenX, x));
+ mZoomScrollX = (int) (x * maxZoomX / maxScreenX);
+ }
+ int maxZoomY = mContentHeight - height;
+ if (maxZoomY > 0) {
+ int maxScreenY = height - (int) Math.ceil(height
+ * mZoomScrollLimit) - SCROLL_ZOOM_FINGER_BUFFER;
+ if (LOGV_ENABLED) {
+ Log.v(LOGTAG, "moveZoomScrollWindow-Y"
+ + " maxScreenY=" + maxScreenY + " height=" + height
+ + " mZoomScrollLimit=" + mZoomScrollLimit + " y=" + y);
+ }
+ y += maxScreenY * mLastScrollY / maxZoomY - mLastTouchY;
+ y = Math.max(0, Math.min(maxScreenY, y));
+ mZoomScrollY = (int) (y * maxZoomY / maxScreenY);
+ }
+ if (oldX != mZoomScrollX || oldY != mZoomScrollY) {
+ invalidate();
+ }
+ if (LOGV_ENABLED) {
+ Log.v(LOGTAG, "moveZoomScrollWindow"
+ + " scrollTo=(" + mZoomScrollX + ", " + mZoomScrollY + ")"
+ + " mLastTouch=(" + mLastTouchX + ", " + mLastTouchY + ")"
+ + " maxZoom=(" + maxZoomX + ", " + maxZoomY + ")"
+ + " last=("+mLastScrollX+", "+mLastScrollY+")"
+ + " x=" + x + " y=" + y);
+ }
+ }
+
+ private void setZoomScrollIn() {
+ mZoomScrollStart = System.currentTimeMillis();
+ }
+
+ private float mZoomScrollLimit;
+ private float mZoomScrollInvLimit;
+ private int mLastScrollX;
+ private int mLastScrollY;
+ private long mZoomScrollStart;
+ private int mZoomScrollX;
+ private int mZoomScrollY;
+ private float mLastZoomScrollRawX = -1000.0f;
+ private float mLastZoomScrollRawY = -1000.0f;
+ // The zoomed scale varies from 1.0 to DEFAULT_MIN_ZOOM_SCALE == 0.25.
+ // The zoom animation duration SCROLL_ZOOM_DURATION == 0.5.
+ // Two pressures compete for gridding; a high frame rate (e.g. 20 fps)
+ // and minimizing font cache allocations (fewer frames is better).
+ // A SCROLL_ZOOM_GRID of 6 permits about 20 zoom levels over 0.5 seconds:
+ // the inverse of: 1.0, 1.16, 1.33, 1.5, 1.67, 1.84, 2.0, etc. to 4.0
+ private static final int SCROLL_ZOOM_GRID = 6;
+ private static final int SCROLL_ZOOM_DURATION = 500;
+ // Make it easier to get to the bottom of a document by reserving a 32
+ // pixel buffer, for when the starting drag is a bit below the bottom of
+ // the magnify frame.
+ private static final int SCROLL_ZOOM_FINGER_BUFFER = 32;
+
+ // draw history
+ private boolean mDrawHistory = false;
+ private Picture mHistoryPicture = null;
+ private int mHistoryWidth = 0;
+ private int mHistoryHeight = 0;
+
+ // Only check the flag, can be called from WebCore thread
+ boolean drawHistory() {
+ return mDrawHistory;
+ }
+
+ // Should only be called in UI thread
+ void switchOutDrawHistory() {
+ if (null == mWebViewCore) return; // CallbackProxy may trigger this
+ if (mDrawHistory) {
+ mDrawHistory = false;
+ invalidate();
+ int oldScrollX = mScrollX;
+ int oldScrollY = mScrollY;
+ mScrollX = pinLocX(mScrollX);
+ mScrollY = pinLocY(mScrollY);
+ if (oldScrollX != mScrollX || oldScrollY != mScrollY) {
+ mUserScroll = false;
+ mWebViewCore.sendMessage(EventHub.SYNC_SCROLL, oldScrollX,
+ oldScrollY);
+ }
+ sendOurVisibleRect();
+ }
+ }
+
+ /**
+ * Class representing the node which is focused.
+ */
+ private class FocusNode {
+ public FocusNode() {
+ mBounds = new Rect();
+ }
+ // Only to be called by JNI
+ private void setAll(boolean isTextField, boolean isTextArea, boolean
+ isPassword, boolean isAnchor, boolean isRtlText, int maxLength,
+ int textSize, int boundsX, int boundsY, int boundsRight, int
+ boundsBottom, int nodePointer, int framePointer, String text,
+ String name, int rootTextGeneration) {
+ mIsTextField = isTextField;
+ mIsTextArea = isTextArea;
+ mIsPassword = isPassword;
+ mIsAnchor = isAnchor;
+ mIsRtlText = isRtlText;
+
+ mMaxLength = maxLength;
+ mTextSize = textSize;
+
+ mBounds.set(boundsX, boundsY, boundsRight, boundsBottom);
+
+
+ mNodePointer = nodePointer;
+ mFramePointer = framePointer;
+ mText = text;
+ mName = name;
+ mRootTextGeneration = rootTextGeneration;
+ }
+ public boolean mIsTextField;
+ public boolean mIsTextArea;
+ public boolean mIsPassword;
+ public boolean mIsAnchor;
+ public boolean mIsRtlText;
+
+ public int mSelectionStart;
+ public int mSelectionEnd;
+ public int mMaxLength;
+ public int mTextSize;
+
+ public Rect mBounds;
+
+ public int mNodePointer;
+ public int mFramePointer;
+ public String mText;
+ public String mName;
+ public int mRootTextGeneration;
+ }
+
+ // Warning: ONLY use mFocusNode AFTER calling nativeUpdateFocusNode(),
+ // and ONLY if it returns true;
+ private FocusNode mFocusNode = new FocusNode();
+
+ /**
+ * 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.
+ */
+ /* package */ void deleteSelection(int start, int end) {
+ mWebViewCore.sendMessage(EventHub.DELETE_SELECTION, start, end,
+ new WebViewCore.FocusData(mFocusData));
+ }
+
+ /**
+ * 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.
+ */
+ /* package */ void setSelection(int start, int end) {
+ mWebViewCore.sendMessage(EventHub.SET_SELECTION, start, end,
+ new WebViewCore.FocusData(mFocusData));
+ }
+
+ // Used to register the global focus change listener one time to avoid
+ // multiple references to WebView
+ private boolean mGlobalFocusChangeListenerAdded;
+
+ private void updateTextEntry() {
+ if (mTextEntry == null) {
+ mTextEntry = new TextDialog(mContext, WebView.this);
+ // Initialize our generation number.
+ mTextGeneration = 0;
+ }
+ // If we do not have focus, do nothing until we gain focus.
+ if (!hasFocus() && !mTextEntry.hasFocus()
+ || (mTouchMode >= FIRST_SCROLL_ZOOM
+ && mTouchMode <= LAST_SCROLL_ZOOM)) {
+ mNeedsUpdateTextEntry = true;
+ return;
+ }
+ boolean alreadyThere = inEditingMode();
+ if (0 == mNativeClass || !nativeUpdateFocusNode()) {
+ if (alreadyThere) {
+ mTextEntry.remove();
+ }
+ return;
+ }
+ FocusNode node = mFocusNode;
+ if (!node.mIsTextField && !node.mIsTextArea) {
+ if (alreadyThere) {
+ mTextEntry.remove();
+ }
+ return;
+ }
+ mTextEntry.setTextSize(contentToView(node.mTextSize));
+ Rect visibleRect = sendOurVisibleRect();
+ // Note that sendOurVisibleRect calls viewToContent, so the coordinates
+ // should be in content coordinates.
+ if (!Rect.intersects(node.mBounds, visibleRect)) {
+ if (alreadyThere) {
+ mTextEntry.remove();
+ }
+ // Node is not on screen, so do not bother.
+ return;
+ }
+ int x = node.mBounds.left;
+ int y = node.mBounds.top;
+ int width = node.mBounds.width();
+ int height = node.mBounds.height();
+ if (alreadyThere && mTextEntry.isSameTextField(node.mNodePointer)) {
+ // It is possible that we have the same textfield, but it has moved,
+ // i.e. In the case of opening/closing the screen.
+ // In that case, we need to set the dimensions, but not the other
+ // aspects.
+ // We also need to restore the selection, which gets wrecked by
+ // calling setTextEntryRect.
+ Spannable spannable = (Spannable) mTextEntry.getText();
+ int start = Selection.getSelectionStart(spannable);
+ int end = Selection.getSelectionEnd(spannable);
+ setTextEntryRect(x, y, width, height);
+ // If the text has been changed by webkit, update it. However, if
+ // there has been more UI text input, ignore it. We will receive
+ // another update when that text is recognized.
+ if (node.mText != null && !node.mText.equals(spannable.toString())
+ && node.mRootTextGeneration == mTextGeneration) {
+ mTextEntry.setTextAndKeepSelection(node.mText);
+ } else {
+ Selection.setSelection(spannable, start, end);
+ }
+ } else {
+ String text = node.mText;
+ setTextEntryRect(x, y, width, height);
+ mTextEntry.setGravity(node.mIsRtlText ? Gravity.RIGHT :
+ Gravity.NO_GRAVITY);
+ // this needs to be called before update adapter thread starts to
+ // ensure the mTextEntry has the same node pointer
+ mTextEntry.setNodePointer(node.mNodePointer);
+ int maxLength = -1;
+ if (node.mIsTextField) {
+ maxLength = node.mMaxLength;
+ if (mWebViewCore.getSettings().getSaveFormData()
+ && node.mName != null) {
+ HashMap data = new HashMap();
+ data.put("text", node.mText);
+ Message update = mPrivateHandler.obtainMessage(
+ UPDATE_TEXT_ENTRY_ADAPTER, node.mNodePointer, 0,
+ data);
+ UpdateTextEntryAdapter updater = new UpdateTextEntryAdapter(
+ node.mName, getUrl(), update);
+ Thread t = new Thread(updater);
+ t.start();
+ }
+ }
+ mTextEntry.setMaxLength(maxLength);
+ ArrayAdapter<String> adapter = null;
+ mTextEntry.setAdapter(adapter);
+ mTextEntry.setInPassword(node.mIsPassword);
+ mTextEntry.setSingleLine(node.mIsTextField);
+ if (null == text) {
+ mTextEntry.setText("", 0, 0);
+ } else {
+ mTextEntry.setText(text, 0, text.length());
+ }
+ mTextEntry.requestFocus();
+ }
+ if (!mGlobalFocusChangeListenerAdded) {
+ getViewTreeObserver().addOnGlobalFocusChangeListener(this);
+ mGlobalFocusChangeListenerAdded = true;
+ }
+ }
+
+ private class UpdateTextEntryAdapter implements Runnable {
+ private String mName;
+ private String mUrl;
+ private Message mUpdateMessage;
+
+ public UpdateTextEntryAdapter(String name, String url, Message msg) {
+ mName = name;
+ mUrl = url;
+ mUpdateMessage = msg;
+ }
+
+ public void run() {
+ ArrayList<String> pastEntries = mDatabase.getFormData(mUrl, mName);
+ if (pastEntries.size() > 0) {
+ ArrayAdapter<String> adapter = new ArrayAdapter<String>(
+ mContext, com.android.internal.R.layout.simple_list_item_1,
+ pastEntries);
+ ((HashMap) mUpdateMessage.obj).put("adapter", adapter);
+ mUpdateMessage.sendToTarget();
+ }
+ }
+ }
+
+ private void setTextEntryRect(int x, int y, int width, int height) {
+ x = contentToView(x);
+ y = contentToView(y);
+ width = contentToView(width);
+ height = contentToView(height);
+ mTextEntry.setRect(x, y, width, height);
+ }
+
+ // These variables are used to determine long press with the enter key, or
+ // a center key. Does not affect long press with the trackball/touch.
+ private long mDownTime = 0;
+ private boolean mGotDown = false;
+
+ // Enable copy/paste with trackball here.
+ // This should be left disabled until the framework can guarantee
+ // delivering matching key-up and key-down events for the shift key
+ private static final boolean ENABLE_COPY_PASTE = false;
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (mTouchMode >= FIRST_SCROLL_ZOOM && mTouchMode <= LAST_SCROLL_ZOOM) {
+ return false;
+ }
+ if (keyCode != KeyEvent.KEYCODE_DPAD_CENTER &&
+ keyCode != KeyEvent.KEYCODE_ENTER) {
+ mGotDown = false;
+ }
+ if (mAltIsPressed == false && (keyCode == KeyEvent.KEYCODE_ALT_LEFT
+ || keyCode == KeyEvent.KEYCODE_ALT_RIGHT)) {
+ mAltIsPressed = true;
+ }
+ if (ENABLE_COPY_PASTE && mShiftIsPressed == false
+ && (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT
+ || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT)) {
+ mExtendSelection = false;
+ mShiftIsPressed = true;
+ if (mNativeClass != 0 && nativeUpdateFocusNode()) {
+ FocusNode node = mFocusNode;
+ mSelectX = node.mBounds.left;
+ mSelectY = node.mBounds.top;
+ } else {
+ mSelectX = mScrollX + SELECT_CURSOR_OFFSET;
+ mSelectY = mScrollY + SELECT_CURSOR_OFFSET;
+ }
+ }
+ if (keyCode == KeyEvent.KEYCODE_CALL) {
+ if (mNativeClass != 0 && nativeUpdateFocusNode()) {
+ FocusNode node = mFocusNode;
+ String text = node.mText;
+ if (!node.mIsTextField && !node.mIsTextArea && text != null &&
+ text.startsWith(SCHEME_TEL)) {
+ Intent intent = new Intent(Intent.ACTION_DIAL,
+ Uri.parse(text));
+ getContext().startActivity(intent);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ if (event.isSystem() || mCallbackProxy.uiOverrideKeyEvent(event)) {
+ return false;
+ }
+ if (LOGV_ENABLED) {
+ Log.v(LOGTAG, "keyDown at " + System.currentTimeMillis()
+ + ", " + event);
+ }
+ boolean isArrowKey = keyCode == KeyEvent.KEYCODE_DPAD_UP ||
+ keyCode == KeyEvent.KEYCODE_DPAD_DOWN ||
+ keyCode == KeyEvent.KEYCODE_DPAD_LEFT ||
+ keyCode == KeyEvent.KEYCODE_DPAD_RIGHT;
+ if (isArrowKey && event.getEventTime() - mTrackballLastTime
+ <= TRACKBALL_KEY_TIMEOUT) {
+ if (LOGV_ENABLED) Log.v(LOGTAG, "ignore arrow");
+ return false;
+ }
+ boolean weHandledTheKey = false;
+
+ if (event.getMetaState() == 0) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_UP:
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ // TODO: alternatively we can do panning as touch does
+ switchOutDrawHistory();
+ weHandledTheKey = navHandledKey(keyCode, 1, false
+ , event.getEventTime());
+ if (weHandledTheKey) {
+ playSoundEffect(keyCodeToSoundsEffect(keyCode));
+ }
+ break;
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ case KeyEvent.KEYCODE_ENTER:
+ if (event.getRepeatCount() == 0) {
+ switchOutDrawHistory();
+ mDownTime = event.getEventTime();
+ mGotDown = true;
+ return true;
+ }
+ if (mGotDown && event.getEventTime() - mDownTime >
+ ViewConfiguration.getLongPressTimeout()) {
+ performLongClick();
+ mGotDown = false;
+ return true;
+ }
+ break;
+ case KeyEvent.KEYCODE_9:
+ if (mNativeClass != 0 && getSettings().getNavDump()) {
+ debugDump();
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ // suppress sending arrow keys to webkit
+ if (!weHandledTheKey && !isArrowKey) {
+ mWebViewCore.sendMessage(EventHub.KEY_DOWN, keyCode, 0, event);
+ }
+
+ return weHandledTheKey;
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT
+ || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT) {
+ if (mExtendSelection) {
+ // copy region so core operates on copy without touching orig.
+ Region selection = new Region(nativeGetSelection());
+ if (selection.isEmpty() == false) {
+ Toast.makeText(mContext
+ , com.android.internal.R.string.text_copied
+ , Toast.LENGTH_SHORT).show();
+ mWebViewCore.sendMessage(EventHub.GET_SELECTION, selection);
+ }
+ }
+ mShiftIsPressed = false;
+ return true;
+ }
+ if (LOGV_ENABLED) {
+ Log.v(LOGTAG, "MT keyUp at" + System.currentTimeMillis()
+ + ", " + event);
+ }
+ if (keyCode == KeyEvent.KEYCODE_ALT_LEFT
+ || keyCode == KeyEvent.KEYCODE_ALT_RIGHT) {
+ mAltIsPressed = false;
+ }
+ if (event.isSystem() || mCallbackProxy.uiOverrideKeyEvent(event)) {
+ return false;
+ }
+
+ if (mTouchMode >= FIRST_SCROLL_ZOOM && mTouchMode <= LAST_SCROLL_ZOOM) {
+ if (KeyEvent.KEYCODE_DPAD_CENTER == keyCode
+ && mTouchMode != SCROLL_ZOOM_ANIMATION_IN) {
+ setZoomScrollIn();
+ mTouchMode = SCROLL_ZOOM_ANIMATION_IN;
+ invalidate();
+ return true;
+ }
+ return false;
+ }
+ Rect visibleRect = sendOurVisibleRect();
+ // Note that sendOurVisibleRect calls viewToContent, so the coordinates
+ // should be in content coordinates.
+ boolean nodeOnScreen = false;
+ boolean isTextField = false;
+ boolean isTextArea = false;
+ FocusNode node = null;
+ if (mNativeClass != 0 && nativeUpdateFocusNode()) {
+ node = mFocusNode;
+ isTextField = node.mIsTextField;
+ isTextArea = node.mIsTextArea;
+ nodeOnScreen = Rect.intersects(node.mBounds, visibleRect);
+ }
+
+ if (KeyEvent.KEYCODE_DPAD_CENTER == keyCode) {
+ if (mShiftIsPressed) {
+ return false;
+ }
+ if (getSettings().supportZoom()) {
+ if (mTouchMode == TOUCH_DOUBLECLICK_MODE) {
+ zoomScrollOut();
+ } else {
+ if (LOGV_ENABLED) Log.v(LOGTAG, "TOUCH_DOUBLECLICK_MODE");
+ mPrivateHandler.sendMessageDelayed(mPrivateHandler
+ .obtainMessage(SWITCH_TO_ENTER), TAP_TIMEOUT);
+ mTouchMode = TOUCH_DOUBLECLICK_MODE;
+ }
+ return true;
+ } else {
+ keyCode = KeyEvent.KEYCODE_ENTER;
+ }
+ }
+ if (KeyEvent.KEYCODE_ENTER == keyCode) {
+ if (LOGV_ENABLED) Log.v(LOGTAG, "KEYCODE_ENTER == keyCode");
+ if (!nodeOnScreen) {
+ return false;
+ }
+ if (node != null && !isTextField && !isTextArea) {
+ nativeSetFollowedLink(true);
+ mWebViewCore.sendMessage(EventHub.SET_FINAL_FOCUS,
+ EventHub.BLOCK_FOCUS_CHANGE_UNTIL_KEY_UP, 0,
+ new WebViewCore.FocusData(mFocusData));
+ if (mCallbackProxy.uiOverrideUrlLoading(node.mText)) {
+ return true;
+ }
+ playSoundEffect(SoundEffectConstants.CLICK);
+ }
+ }
+ if (!nodeOnScreen) {
+ // FIXME: Want to give Callback a chance to handle it, and
+ // possibly pass down to javascript.
+ return false;
+ }
+ if (LOGV_ENABLED) Log.v(LOGTAG, "onKeyUp send EventHub.KEY_UP");
+ mWebViewCore.sendMessage(EventHub.KEY_UP, keyCode, 0, event);
+ return true;
+ }
+
+ // Set this as a hierarchy change listener so we can know when this view
+ // is removed and still have access to our parent.
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ ViewParent parent = getParent();
+ if (parent instanceof ViewGroup) {
+ ViewGroup p = (ViewGroup) parent;
+ p.setOnHierarchyChangeListener(this);
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ ViewParent parent = getParent();
+ if (parent instanceof ViewGroup) {
+ ViewGroup p = (ViewGroup) parent;
+ p.setOnHierarchyChangeListener(null);
+ }
+ }
+
+ // Implementation for OnHierarchyChangeListener
+ public void onChildViewAdded(View parent, View child) {}
+
+ // When we are removed, remove this as a global focus change listener.
+ public void onChildViewRemoved(View p, View child) {
+ if (child == this) {
+ p.getViewTreeObserver().removeOnGlobalFocusChangeListener(this);
+ mGlobalFocusChangeListenerAdded = false;
+ }
+ }
+
+ // Use this to know when the textview has lost focus, and something other
+ // than the webview has gained focus. Stop drawing the focus ring, remove
+ // the TextView, and set a flag to put it back when we regain focus.
+ public void onGlobalFocusChanged(View oldFocus, View newFocus) {
+ if (oldFocus == mTextEntry && newFocus != this) {
+ mDrawFocusRing = false;
+ mTextEntry.updateCachedTextfield();
+ removeView(mTextEntry);
+ mNeedsUpdateTextEntry = true;
+ }
+ }
+
+ // To avoid drawing the focus ring, and remove the TextView when our window
+ // loses focus.
+ @Override
+ public void onWindowFocusChanged(boolean hasWindowFocus) {
+ if (hasWindowFocus) {
+ if (hasFocus()) {
+ // If our window regained focus, and we have focus, then begin
+ // drawing the focus ring, and restore the TextView if
+ // necessary.
+ mDrawFocusRing = true;
+ if (mNeedsUpdateTextEntry) {
+ updateTextEntry();
+ }
+ } else {
+ // If our window gained focus, but we do not have it, do not
+ // draw the focus ring.
+ mDrawFocusRing = false;
+ }
+ } else {
+ // If our window has lost focus, stop drawing the focus ring, and
+ // remove the TextView if displayed, and flag it to be added when
+ // we regain focus.
+ mDrawFocusRing = false;
+ mGotKeyDown = false;
+ if (inEditingMode()) {
+ clearTextEntry();
+ mNeedsUpdateTextEntry = true;
+ }
+ }
+ invalidate();
+ super.onWindowFocusChanged(hasWindowFocus);
+ }
+
+ @Override
+ protected void onFocusChanged(boolean focused, int direction,
+ Rect previouslyFocusedRect) {
+ if (LOGV_ENABLED) {
+ Log.v(LOGTAG, "MT focusChanged " + focused + ", " + direction);
+ }
+ if (focused) {
+ // When we regain focus, if we have window focus, resume drawing
+ // the focus ring, and add the TextView if necessary.
+ if (hasWindowFocus()) {
+ mDrawFocusRing = true;
+ if (mNeedsUpdateTextEntry) {
+ updateTextEntry();
+ mNeedsUpdateTextEntry = false;
+ }
+ }
+ } else {
+ // When we lost focus, unless focus went to the TextView (which is
+ // true if we are in editing mode), stop drawing the focus ring.
+ if (!inEditingMode()) {
+ mDrawFocusRing = false;
+ }
+ mGotKeyDown = false;
+ }
+
+ super.onFocusChanged(focused, direction, previouslyFocusedRect);
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int ow, int oh) {
+ super.onSizeChanged(w, h, ow, oh);
+
+ // we always force, in case our height changed, in which case we still
+ // want to send the notification over to webkit
+ setNewZoomScale(mActualScale, true);
+ }
+
+ @Override
+ protected void onScrollChanged(int l, int t, int oldl, int oldt) {
+ super.onScrollChanged(l, t, oldl, oldt);
+ sendOurVisibleRect();
+ }
+
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ boolean dispatch = true;
+
+ if (!inEditingMode()) {
+ if (event.getAction() == KeyEvent.ACTION_DOWN) {
+ mGotKeyDown = true;
+ } else {
+ if (!mGotKeyDown) {
+ /*
+ * We got a key up for which we were not the recipient of
+ * the original key down. Don't give it to the view.
+ */
+ dispatch = false;
+ }
+ mGotKeyDown = false;
+ }
+ }
+
+ if (dispatch) {
+ return super.dispatchKeyEvent(event);
+ } else {
+ // We didn't dispatch, so let something else handle the key
+ return false;
+ }
+ }
+
+ // Here are the snap align logic:
+ // 1. If it starts nearly horizontally or vertically, snap align;
+ // 2. If there is a dramitic direction change, let it go;
+ // 3. If there is a same direction back and forth, lock it.
+
+ // adjustable parameters
+ private static final float MAX_SLOPE_FOR_DIAG = 1.5f;
+ private static final int MIN_LOCK_SNAP_REVERSE_DISTANCE =
+ ViewConfiguration.getTouchSlop();
+ private static final int MIN_BREAK_SNAP_CROSS_DISTANCE = 80;
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ if (mNativeClass == 0 || !isClickable() || !isLongClickable() ||
+ !hasFocus()) {
+ return false;
+ }
+
+ if (LOGV_ENABLED) {
+ Log.v(LOGTAG, ev + " at " + ev.getEventTime() + " mTouchMode="
+ + mTouchMode);
+ }
+
+ int action = ev.getAction();
+ float x = ev.getX();
+ float y = ev.getY();
+ long eventTime = ev.getEventTime();
+ switch (action) {
+ case MotionEvent.ACTION_DOWN: {
+ if (mTouchMode == SCROLL_ZOOM_ANIMATION_IN
+ || mTouchMode == SCROLL_ZOOM_ANIMATION_OUT) {
+ // no interaction while animation is in progress
+ break;
+ } else if (mTouchMode == SCROLL_ZOOM_OUT) {
+ mLastScrollX = mZoomScrollX;
+ mLastScrollY = mZoomScrollY;
+ // If two taps are close, ignore the first tap
+ } else if (!mScroller.isFinished()) {
+ mScroller.abortAnimation();
+ mTouchMode = TOUCH_DRAG_START_MODE;
+ mPrivateHandler.removeMessages(RESUME_WEBCORE_UPDATE);
+ } else {
+ mTouchMode = TOUCH_INIT_MODE;
+ }
+ if (mTouchMode == TOUCH_INIT_MODE) {
+ mPrivateHandler.sendMessageDelayed(mPrivateHandler
+ .obtainMessage(SWITCH_TO_SHORTPRESS), TAP_TIMEOUT);
+ }
+ // Remember where the motion event started
+ mLastTouchX = x;
+ mLastTouchY = y;
+ mLastTouchTime = eventTime;
+ mVelocityTracker = VelocityTracker.obtain();
+ mSnapScrollMode = SNAP_NONE;
+ break;
+ }
+ case MotionEvent.ACTION_MOVE: {
+ if (mTouchMode == TOUCH_DONE_MODE
+ || mTouchMode == SCROLL_ZOOM_ANIMATION_IN
+ || mTouchMode == SCROLL_ZOOM_ANIMATION_OUT) {
+ // no dragging during scroll zoom animation
+ break;
+ }
+ if (mTouchMode == SCROLL_ZOOM_OUT) {
+ // while fully zoomed out, move the virtual window
+ moveZoomScrollWindow(x, y);
+ break;
+ }
+ mVelocityTracker.addMovement(ev);
+
+ int deltaX = (int) (mLastTouchX - x);
+ int deltaY = (int) (mLastTouchY - y);
+
+ if (mTouchMode != TOUCH_DRAG_MODE) {
+ if ((deltaX * deltaX + deltaY * deltaY)
+ < TOUCH_SLOP_SQUARE) {
+ break;
+ }
+
+ if (mTouchMode == TOUCH_SHORTPRESS_MODE
+ || mTouchMode == TOUCH_SHORTPRESS_START_MODE) {
+ mPrivateHandler.removeMessages(SWITCH_TO_LONGPRESS);
+ } else if (mTouchMode == TOUCH_INIT_MODE) {
+ mPrivateHandler.removeMessages(SWITCH_TO_SHORTPRESS);
+ }
+
+ // if it starts nearly horizontal or vertical, enforce it
+ int ax = Math.abs(deltaX);
+ int ay = Math.abs(deltaY);
+ if (ax > MAX_SLOPE_FOR_DIAG * ay) {
+ mSnapScrollMode = SNAP_X;
+ mSnapPositive = deltaX > 0;
+ } else if (ay > MAX_SLOPE_FOR_DIAG * ax) {
+ mSnapScrollMode = SNAP_Y;
+ mSnapPositive = deltaY > 0;
+ }
+
+ mTouchMode = TOUCH_DRAG_MODE;
+ WebViewCore.pauseUpdate(mWebViewCore);
+ int contentX = viewToContent((int) x + mScrollX);
+ int contentY = viewToContent((int) y + mScrollY);
+ if (inEditingMode()) {
+ mTextEntry.updateCachedTextfield();
+ }
+ nativeClearFocus(contentX, contentY);
+ // remove the zoom anchor if there is any
+ if (mZoomScale != 0) {
+ mWebViewCore
+ .sendMessage(EventHub.SET_SNAP_ANCHOR, 0, 0);
+ }
+ }
+
+ // do pan
+ int newScrollX = pinLocX(mScrollX + deltaX);
+ deltaX = newScrollX - mScrollX;
+ int newScrollY = pinLocY(mScrollY + deltaY);
+ deltaY = newScrollY - mScrollY;
+ boolean done = false;
+ if (deltaX == 0 && deltaY == 0) {
+ done = true;
+ } else {
+ if (mSnapScrollMode == SNAP_X || mSnapScrollMode == SNAP_Y) {
+ int ax = Math.abs(deltaX);
+ int ay = Math.abs(deltaY);
+ if (mSnapScrollMode == SNAP_X) {
+ // radical change means getting out of snap mode
+ if (ay > MAX_SLOPE_FOR_DIAG * ax
+ && ay > MIN_BREAK_SNAP_CROSS_DISTANCE) {
+ mSnapScrollMode = SNAP_NONE;
+ }
+ // reverse direction means lock in the snap mode
+ if ((ax > MAX_SLOPE_FOR_DIAG * ay) &&
+ ((mSnapPositive &&
+ deltaX < -MIN_LOCK_SNAP_REVERSE_DISTANCE)
+ || (!mSnapPositive &&
+ deltaX > MIN_LOCK_SNAP_REVERSE_DISTANCE))) {
+ mSnapScrollMode = SNAP_X_LOCK;
+ }
+ } else {
+ // radical change means getting out of snap mode
+ if ((ax > MAX_SLOPE_FOR_DIAG * ay)
+ && ax > MIN_BREAK_SNAP_CROSS_DISTANCE) {
+ mSnapScrollMode = SNAP_NONE;
+ }
+ // reverse direction means lock in the snap mode
+ if ((ay > MAX_SLOPE_FOR_DIAG * ax) &&
+ ((mSnapPositive &&
+ deltaY < -MIN_LOCK_SNAP_REVERSE_DISTANCE)
+ || (!mSnapPositive &&
+ deltaY > MIN_LOCK_SNAP_REVERSE_DISTANCE))) {
+ mSnapScrollMode = SNAP_Y_LOCK;
+ }
+ }
+ }
+
+ if (mSnapScrollMode == SNAP_X
+ || mSnapScrollMode == SNAP_X_LOCK) {
+ scrollBy(deltaX, 0);
+ mLastTouchX = x;
+ } else if (mSnapScrollMode == SNAP_Y
+ || mSnapScrollMode == SNAP_Y_LOCK) {
+ scrollBy(0, deltaY);
+ mLastTouchY = y;
+ } else {
+ scrollBy(deltaX, deltaY);
+ mLastTouchX = x;
+ mLastTouchY = y;
+ }
+ mLastTouchTime = eventTime;
+ mUserScroll = true;
+ }
+
+ if (mZoomControls != null && mMinZoomScale < mMaxZoomScale) {
+ if (mZoomControls.getVisibility() == View.VISIBLE) {
+ mPrivateHandler.removeCallbacks(mZoomControlRunnable);
+ } else {
+ mZoomControls.show(canZoomScrollOut());
+ }
+ mPrivateHandler.postDelayed(mZoomControlRunnable,
+ ZOOM_CONTROLS_TIMEOUT);
+ }
+ if (done) {
+ // return false to indicate that we can't pan out of the
+ // view space
+ return false;
+ }
+ break;
+ }
+ case MotionEvent.ACTION_UP: {
+ switch (mTouchMode) {
+ case TOUCH_INIT_MODE: // tap
+ mPrivateHandler.removeMessages(SWITCH_TO_SHORTPRESS);
+ if (getSettings().supportZoom()) {
+ mPrivateHandler.sendMessageDelayed(mPrivateHandler
+ .obtainMessage(RELEASE_SINGLE_TAP),
+ DOUBLE_TAP_TIMEOUT);
+ } else {
+ // do short press now
+ mTouchMode = TOUCH_DONE_MODE;
+ doShortPress();
+ }
+ break;
+ case SCROLL_ZOOM_ANIMATION_IN:
+ case SCROLL_ZOOM_ANIMATION_OUT:
+ // no action during scroll animation
+ break;
+ case SCROLL_ZOOM_OUT:
+ if (LOGV_ENABLED) {
+ Log.v(LOGTAG, "ACTION_UP SCROLL_ZOOM_OUT"
+ + " eventTime - mLastTouchTime="
+ + (eventTime - mLastTouchTime));
+ }
+ // for now, always zoom back when the drag completes
+ if (true || eventTime - mLastTouchTime < TAP_TIMEOUT) {
+ // but if we tap, zoom in where we tap
+ if (eventTime - mLastTouchTime < TAP_TIMEOUT) {
+ zoomScrollTap(x, y);
+ }
+ // start zooming in back to the original view
+ setZoomScrollIn();
+ mTouchMode = SCROLL_ZOOM_ANIMATION_IN;
+ invalidate();
+ }
+ break;
+ case TOUCH_SHORTPRESS_START_MODE:
+ case TOUCH_SHORTPRESS_MODE: {
+ mPrivateHandler.removeMessages(SWITCH_TO_LONGPRESS);
+ if (eventTime - mLastTouchTime < TAP_TIMEOUT
+ && getSettings().supportZoom()) {
+ // Note: window manager will not release ACTION_UP
+ // until all the previous action events are
+ // returned. If GC happens, it can cause
+ // SWITCH_TO_SHORTPRESS message fired before
+ // ACTION_UP sent even time stamp of ACTION_UP is
+ // less than the tap time out. We need to treat this
+ // as tap instead of short press.
+ mTouchMode = TOUCH_INIT_MODE;
+ mPrivateHandler.sendMessageDelayed(mPrivateHandler
+ .obtainMessage(RELEASE_SINGLE_TAP),
+ DOUBLE_TAP_TIMEOUT);
+ } else {
+ mTouchMode = TOUCH_DONE_MODE;
+ doShortPress();
+ }
+ break;
+ }
+ case TOUCH_DRAG_MODE:
+ // if the user waits a while w/o moving before the
+ // up, we don't want to do a fling
+ if (eventTime - mLastTouchTime <= MIN_FLING_TIME) {
+ mVelocityTracker.addMovement(ev);
+ doFling();
+ break;
+ }
+ WebViewCore.resumeUpdate(mWebViewCore);
+ break;
+ case TOUCH_DRAG_START_MODE:
+ case TOUCH_DONE_MODE:
+ // do nothing
+ break;
+ }
+ // we also use mVelocityTracker == null to tell us that we are
+ // not "moving around", so we can take the slower/prettier
+ // mode in the drawing code
+ if (mVelocityTracker != null) {
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+ break;
+ }
+ case MotionEvent.ACTION_CANCEL: {
+ // we also use mVelocityTracker == null to tell us that we are
+ // not "moving around", so we can take the slower/prettier
+ // mode in the drawing code
+ if (mVelocityTracker != null) {
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+ if (mTouchMode == SCROLL_ZOOM_OUT ||
+ mTouchMode == SCROLL_ZOOM_ANIMATION_IN) {
+ scrollTo(mZoomScrollX, mZoomScrollY);
+ } else if (mTouchMode == TOUCH_DRAG_MODE) {
+ WebViewCore.resumeUpdate(mWebViewCore);
+ }
+ mPrivateHandler.removeMessages(SWITCH_TO_SHORTPRESS);
+ mPrivateHandler.removeMessages(SWITCH_TO_LONGPRESS);
+ mPrivateHandler.removeMessages(RELEASE_SINGLE_TAP);
+ mTouchMode = TOUCH_DONE_MODE;
+ int contentX = viewToContent((int) mLastTouchX + mScrollX);
+ int contentY = viewToContent((int) mLastTouchY + mScrollY);
+ if (inEditingMode()) {
+ mTextEntry.updateCachedTextfield();
+ }
+ nativeClearFocus(contentX, contentY);
+ break;
+ }
+ }
+ return true;
+ }
+
+ private long mTrackballFirstTime = 0;
+ private long mTrackballLastTime = 0;
+ private float mTrackballRemainsX = 0.0f;
+ private float mTrackballRemainsY = 0.0f;
+ private int mTrackballXMove = 0;
+ private int mTrackballYMove = 0;
+ private boolean mExtendSelection = false;
+ private static final int TRACKBALL_KEY_TIMEOUT = 1000;
+ private static final int TRACKBALL_TIMEOUT = 200;
+ private static final int TRACKBALL_WAIT = 100;
+ private static final int TRACKBALL_SCALE = 400;
+ private static final int TRACKBALL_SCROLL_COUNT = 5;
+ private static final int TRACKBALL_MULTIPLIER = 3;
+ private static final int SELECT_CURSOR_OFFSET = 16;
+ private int mSelectX = 0;
+ private int mSelectY = 0;
+ private boolean mShiftIsPressed = false;
+ private boolean mAltIsPressed = false;
+ private boolean mTrackballDown = false;
+ private long mTrackballUpTime = 0;
+ private long mLastFocusTime = 0;
+ private Rect mLastFocusBounds;
+
+ // Used to determine that the trackball is down AND that it has not
+ // been moved while down, whereas mTrackballDown is true until we
+ // receive an ACTION_UP
+ private boolean mTrackTrackball = false;
+
+ // Set by default; BrowserActivity clears to interpret trackball data
+ // directly for movement. Currently, the framework only passes
+ // arrow key events, not trackball events, from one child to the next
+ private boolean mMapTrackballToArrowKeys = true;
+
+ public void setMapTrackballToArrowKeys(boolean setMap) {
+ mMapTrackballToArrowKeys = setMap;
+ }
+
+ void resetTrackballTime() {
+ mTrackballLastTime = 0;
+ }
+
+ @Override
+ public boolean onTrackballEvent(MotionEvent ev) {
+ long time = ev.getEventTime();
+ if (mAltIsPressed) {
+ if (ev.getY() > 0) pageDown(true);
+ if (ev.getY() < 0) pageUp(true);
+ return true;
+ }
+ if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+ mPrivateHandler.removeMessages(SWITCH_TO_ENTER);
+ mPrivateHandler.sendMessageDelayed(
+ mPrivateHandler.obtainMessage(LONG_PRESS_TRACKBALL), 1000);
+ mTrackTrackball = true;
+ mTrackballDown = true;
+ if (time - mLastFocusTime <= TRACKBALL_TIMEOUT
+ && !mLastFocusBounds.equals(nativeGetFocusRingBounds())) {
+ nativeSelectBestAt(mLastFocusBounds);
+ }
+ if (LOGV_ENABLED) {
+ Log.v(LOGTAG, "onTrackballEvent down ev=" + ev
+ + " time=" + time
+ + " mLastFocusTime=" + mLastFocusTime);
+ }
+ return false; // let common code in onKeyDown at it
+ } else if (mTrackTrackball) {
+ mPrivateHandler.removeMessages(LONG_PRESS_TRACKBALL);
+ mTrackTrackball = false;
+ }
+ if (ev.getAction() == MotionEvent.ACTION_UP) {
+ mTrackballDown = false;
+ mTrackballUpTime = time;
+ if (mShiftIsPressed) {
+ mExtendSelection = true;
+ }
+ if (LOGV_ENABLED) {
+ Log.v(LOGTAG, "onTrackballEvent up ev=" + ev
+ + " time=" + time
+ );
+ }
+ return false; // let common code in onKeyUp at it
+ }
+ if (mMapTrackballToArrowKeys && mShiftIsPressed == false) {
+ if (LOGV_ENABLED) Log.v(LOGTAG, "onTrackballEvent gmail quit");
+ return false;
+ }
+ // no move if we're still waiting on SWITCH_TO_ENTER timeout
+ if (mTouchMode == TOUCH_DOUBLECLICK_MODE) {
+ if (LOGV_ENABLED) Log.v(LOGTAG, "onTrackballEvent 2 click quit");
+ return true;
+ }
+ if (mTrackballDown) {
+ if (LOGV_ENABLED) Log.v(LOGTAG, "onTrackballEvent down quit");
+ return true; // discard move if trackball is down
+ }
+ if (time - mTrackballUpTime < TRACKBALL_TIMEOUT) {
+ if (LOGV_ENABLED) Log.v(LOGTAG, "onTrackballEvent up timeout quit");
+ return true;
+ }
+ // TODO: alternatively we can do panning as touch does
+ switchOutDrawHistory();
+ if (time - mTrackballLastTime > TRACKBALL_TIMEOUT) {
+ if (LOGV_ENABLED) {
+ Log.v(LOGTAG, "onTrackballEvent time="
+ + time + " last=" + mTrackballLastTime);
+ }
+ mTrackballFirstTime = time;
+ mTrackballXMove = mTrackballYMove = 0;
+ }
+ mTrackballLastTime = time;
+ if (LOGV_ENABLED) {
+ Log.v(LOGTAG, "onTrackballEvent ev=" + ev + " time=" + time);
+ }
+ mTrackballRemainsX += ev.getX();
+ mTrackballRemainsY += ev.getY();
+ doTrackball(time);
+ return true;
+ }
+
+ void moveSelection(float xRate, float yRate) {
+ if (mNativeClass == 0)
+ return;
+ int width = getViewWidth();
+ int height = getViewHeight();
+ mSelectX += scaleTrackballX(xRate, width);
+ mSelectY += scaleTrackballY(yRate, height);
+ int maxX = width + mScrollX;
+ int maxY = height + mScrollY;
+ mSelectX = Math.min(maxX, Math.max(mScrollX - SELECT_CURSOR_OFFSET
+ , mSelectX));
+ mSelectY = Math.min(maxY, Math.max(mScrollY - SELECT_CURSOR_OFFSET
+ , mSelectY));
+ if (LOGV_ENABLED) {
+ Log.v(LOGTAG, "moveSelection"
+ + " mSelectX=" + mSelectX
+ + " mSelectY=" + mSelectY
+ + " mScrollX=" + mScrollX
+ + " mScrollY=" + mScrollY
+ + " xRate=" + xRate
+ + " yRate=" + yRate
+ );
+ }
+ nativeMoveSelection(viewToContent(mSelectX)
+ , viewToContent(mSelectY), mExtendSelection);
+ int scrollX = mSelectX < mScrollX ? -SELECT_CURSOR_OFFSET
+ : mSelectX > maxX - SELECT_CURSOR_OFFSET ? SELECT_CURSOR_OFFSET
+ : 0;
+ int scrollY = mSelectY < mScrollY ? -SELECT_CURSOR_OFFSET
+ : mSelectY > maxY - SELECT_CURSOR_OFFSET ? SELECT_CURSOR_OFFSET
+ : 0;
+ pinScrollBy(scrollX, scrollY, true);
+ Rect select = new Rect(mSelectX, mSelectY, mSelectX + 1, mSelectY + 1);
+ requestRectangleOnScreen(select);
+ invalidate();
+ }
+
+ private int scaleTrackballX(float xRate, int width) {
+ int xMove = (int) (xRate / TRACKBALL_SCALE * width);
+ int nextXMove = xMove;
+ if (xMove > 0) {
+ if (xMove > mTrackballXMove) {
+ xMove -= mTrackballXMove;
+ }
+ } else if (xMove < mTrackballXMove) {
+ xMove -= mTrackballXMove;
+ }
+ mTrackballXMove = nextXMove;
+ return xMove;
+ }
+
+ private int scaleTrackballY(float yRate, int height) {
+ int yMove = (int) (yRate / TRACKBALL_SCALE * height);
+ int nextYMove = yMove;
+ if (yMove > 0) {
+ if (yMove > mTrackballYMove) {
+ yMove -= mTrackballYMove;
+ }
+ } else if (yMove < mTrackballYMove) {
+ yMove -= mTrackballYMove;
+ }
+ mTrackballYMove = nextYMove;
+ return yMove;
+ }
+
+ private int keyCodeToSoundsEffect(int keyCode) {
+ switch(keyCode) {
+ case KeyEvent.KEYCODE_DPAD_UP:
+ return SoundEffectConstants.NAVIGATION_UP;
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ return SoundEffectConstants.NAVIGATION_RIGHT;
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ return SoundEffectConstants.NAVIGATION_DOWN;
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ return SoundEffectConstants.NAVIGATION_LEFT;
+ }
+ throw new IllegalArgumentException("keyCode must be one of " +
+ "{KEYCODE_DPAD_UP, KEYCODE_DPAD_RIGHT, KEYCODE_DPAD_DOWN, " +
+ "KEYCODE_DPAD_LEFT}.");
+ }
+
+ private void doTrackball(long time) {
+ int elapsed = (int) (mTrackballLastTime - mTrackballFirstTime);
+ if (elapsed == 0) {
+ elapsed = TRACKBALL_TIMEOUT;
+ }
+ float xRate = mTrackballRemainsX * 1000 / elapsed;
+ float yRate = mTrackballRemainsY * 1000 / elapsed;
+ if (mShiftIsPressed) {
+ moveSelection(xRate, yRate);
+ mTrackballRemainsX = mTrackballRemainsY = 0;
+ return;
+ }
+ float ax = Math.abs(xRate);
+ float ay = Math.abs(yRate);
+ float maxA = Math.max(ax, ay);
+ if (LOGV_ENABLED) {
+ Log.v(LOGTAG, "doTrackball elapsed=" + elapsed
+ + " xRate=" + xRate
+ + " yRate=" + yRate
+ + " mTrackballRemainsX=" + mTrackballRemainsX
+ + " mTrackballRemainsY=" + mTrackballRemainsY);
+ }
+ int width = mContentWidth - getViewWidth();
+ int height = mContentHeight - getViewHeight();
+ if (width < 0) width = 0;
+ if (height < 0) height = 0;
+ if (mTouchMode == SCROLL_ZOOM_OUT) {
+ int oldX = mZoomScrollX;
+ int oldY = mZoomScrollY;
+ int maxWH = Math.max(width, height);
+ mZoomScrollX += scaleTrackballX(xRate, maxWH);
+ mZoomScrollY += scaleTrackballY(yRate, maxWH);
+ if (LOGV_ENABLED) {
+ Log.v(LOGTAG, "doTrackball SCROLL_ZOOM_OUT"
+ + " mZoomScrollX=" + mZoomScrollX
+ + " mZoomScrollY=" + mZoomScrollY);
+ }
+ mZoomScrollX = Math.min(width, Math.max(0, mZoomScrollX));
+ mZoomScrollY = Math.min(height, Math.max(0, mZoomScrollY));
+ if (oldX != mZoomScrollX || oldY != mZoomScrollY) {
+ invalidate();
+ }
+ mTrackballRemainsX = mTrackballRemainsY = 0;
+ return;
+ }
+ ax = Math.abs(mTrackballRemainsX * TRACKBALL_MULTIPLIER);
+ ay = Math.abs(mTrackballRemainsY * TRACKBALL_MULTIPLIER);
+ maxA = Math.max(ax, ay);
+ int count = Math.max(0, (int) maxA);
+ int oldScrollX = mScrollX;
+ int oldScrollY = mScrollY;
+ if (count > 0) {
+ int selectKeyCode = ax < ay ? mTrackballRemainsY < 0 ?
+ KeyEvent.KEYCODE_DPAD_UP : KeyEvent.KEYCODE_DPAD_DOWN :
+ mTrackballRemainsX < 0 ? KeyEvent.KEYCODE_DPAD_LEFT :
+ KeyEvent.KEYCODE_DPAD_RIGHT;
+ if (LOGV_ENABLED) {
+ Log.v(LOGTAG, "doTrackball keyCode=" + selectKeyCode
+ + " count=" + count
+ + " mTrackballRemainsX=" + mTrackballRemainsX
+ + " mTrackballRemainsY=" + mTrackballRemainsY);
+ }
+ if (navHandledKey(selectKeyCode, count, false, time)) {
+ playSoundEffect(keyCodeToSoundsEffect(selectKeyCode));
+ }
+ mTrackballRemainsX = mTrackballRemainsY = 0;
+ }
+ if (count >= TRACKBALL_SCROLL_COUNT) {
+ int xMove = scaleTrackballX(xRate, width);
+ int yMove = scaleTrackballY(yRate, height);
+ if (LOGV_ENABLED) {
+ Log.v(LOGTAG, "doTrackball pinScrollBy"
+ + " count=" + count
+ + " xMove=" + xMove + " yMove=" + yMove
+ + " mScrollX-oldScrollX=" + (mScrollX-oldScrollX)
+ + " mScrollY-oldScrollY=" + (mScrollY-oldScrollY)
+ );
+ }
+ if (Math.abs(mScrollX - oldScrollX) > Math.abs(xMove)) {
+ xMove = 0;
+ }
+ if (Math.abs(mScrollY - oldScrollY) > Math.abs(yMove)) {
+ yMove = 0;
+ }
+ if (xMove != 0 || yMove != 0) {
+ pinScrollBy(xMove, yMove, true);
+ }
+ mUserScroll = true;
+ }
+ mWebViewCore.sendMessage(EventHub.UNBLOCK_FOCUS);
+ }
+
+ public void flingScroll(int vx, int vy) {
+ int maxX = Math.max(computeHorizontalScrollRange() - getViewWidth(), 0);
+ int maxY = Math.max(computeVerticalScrollRange() - getViewHeight(), 0);
+
+ mScroller.fling(mScrollX, mScrollY, vx, vy, 0, maxX, 0, maxY);
+ invalidate();
+ }
+
+ private void doFling() {
+ if (mVelocityTracker == null) {
+ return;
+ }
+ int maxX = Math.max(computeHorizontalScrollRange() - getViewWidth(), 0);
+ int maxY = Math.max(computeVerticalScrollRange() - getViewHeight(), 0);
+
+ mVelocityTracker.computeCurrentVelocity(1000);
+ int vx = (int) mVelocityTracker.getXVelocity();
+ int vy = (int) mVelocityTracker.getYVelocity();
+
+ if (mSnapScrollMode != SNAP_NONE) {
+ if (mSnapScrollMode == SNAP_X || mSnapScrollMode == SNAP_X_LOCK) {
+ vy = 0;
+ } else {
+ vx = 0;
+ }
+ }
+
+ if (true /* EMG release: make our fling more like Maps' */) {
+ // maps cuts their velocity in half
+ vx = vx * 3 / 4;
+ vy = vy * 3 / 4;
+ }
+
+ mScroller.fling(mScrollX, mScrollY, -vx, -vy, 0, maxX, 0, maxY);
+ // TODO: duration is calculated based on velocity, if the range is
+ // small, the animation will stop before duration is up. We may
+ // want to calculate how long the animation is going to run to precisely
+ // resume the webcore update.
+ final int time = mScroller.getDuration();
+ mPrivateHandler.sendEmptyMessageDelayed(RESUME_WEBCORE_UPDATE, time);
+ invalidate();
+ }
+
+ private boolean zoomWithPreview(float scale) {
+ float oldScale = mActualScale;
+
+ // snap to 100% if it is close
+ if (scale > 0.95f && scale < 1.05f) {
+ scale = 1.0f;
+ }
+
+ setNewZoomScale(scale, false);
+
+ if (oldScale != mActualScale) {
+ // use mZoomPickerScale to see zoom preview first
+ mZoomStart = SystemClock.uptimeMillis();
+ mInvInitialZoomScale = 1.0f / oldScale;
+ mInvFinalZoomScale = 1.0f / mActualScale;
+ mZoomScale = mActualScale;
+ invalidate();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Returns a view containing zoom controls i.e. +/- buttons. The caller is
+ * in charge of installing this view to the view hierarchy. This view will
+ * become visible when the user starts scrolling via touch and fade away if
+ * the user does not interact with it.
+ */
+ public View getZoomControls() {
+ if (!getSettings().supportZoom()) {
+ Log.w(LOGTAG, "This WebView doesn't support zoom.");
+ return null;
+ }
+ if (mZoomControls == null) {
+ mZoomControls = createZoomControls();
+
+ /*
+ * need to be set to VISIBLE first so that getMeasuredHeight() in
+ * {@link #onSizeChanged()} can return the measured value for proper
+ * layout.
+ */
+ mZoomControls.setVisibility(View.VISIBLE);
+ mZoomControlRunnable = new Runnable() {
+ public void run() {
+
+ /* Don't dismiss the controls if the user has
+ * focus on them. Wait and check again later.
+ */
+ if (!mZoomControls.hasFocus()) {
+ mZoomControls.hide();
+ } else {
+ mPrivateHandler.removeCallbacks(mZoomControlRunnable);
+ mPrivateHandler.postDelayed(mZoomControlRunnable,
+ ZOOM_CONTROLS_TIMEOUT);
+ }
+ }
+ };
+ }
+ return mZoomControls;
+ }
+
+ /**
+ * Perform zoom in in the webview
+ * @return TRUE if zoom in succeeds. FALSE if no zoom changes.
+ */
+ public boolean zoomIn() {
+ // TODO: alternatively we can disallow this during draw history mode
+ switchOutDrawHistory();
+ return zoomWithPreview(mActualScale * 1.25f);
+ }
+
+ /**
+ * Perform zoom out in the webview
+ * @return TRUE if zoom out succeeds. FALSE if no zoom changes.
+ */
+ public boolean zoomOut() {
+ // TODO: alternatively we can disallow this during draw history mode
+ switchOutDrawHistory();
+ return zoomWithPreview(mActualScale * 0.8f);
+ }
+
+ private ExtendedZoomControls createZoomControls() {
+ ExtendedZoomControls zoomControls = new ExtendedZoomControls(mContext
+ , null);
+ zoomControls.setOnZoomInClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ // reset time out
+ mPrivateHandler.removeCallbacks(mZoomControlRunnable);
+ mPrivateHandler.postDelayed(mZoomControlRunnable,
+ ZOOM_CONTROLS_TIMEOUT);
+ zoomIn();
+ }
+ });
+ zoomControls.setOnZoomOutClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ // reset time out
+ mPrivateHandler.removeCallbacks(mZoomControlRunnable);
+ mPrivateHandler.postDelayed(mZoomControlRunnable,
+ ZOOM_CONTROLS_TIMEOUT);
+ zoomOut();
+ }
+ });
+ zoomControls.setOnZoomMagnifyClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ mPrivateHandler.removeCallbacks(mZoomControlRunnable);
+ mPrivateHandler.postDelayed(mZoomControlRunnable,
+ ZOOM_CONTROLS_TIMEOUT);
+ zoomScrollOut();
+ }
+ });
+ return zoomControls;
+ }
+
+ private void updateSelection() {
+ if (mNativeClass == 0) {
+ return;
+ }
+ // mLastTouchX and mLastTouchY are the point in the current viewport
+ int contentX = viewToContent((int) mLastTouchX + mScrollX);
+ int contentY = viewToContent((int) mLastTouchY + mScrollY);
+ int contentSize = ViewConfiguration.getTouchSlop();
+ Rect rect = new Rect(contentX - contentSize, contentY - contentSize,
+ contentX + contentSize, contentY + contentSize);
+ // If we were already focused on a textfield, update its cache.
+ if (inEditingMode()) {
+ mTextEntry.updateCachedTextfield();
+ }
+ nativeSelectBestAt(rect);
+ }
+
+ /*package*/ void shortPressOnTextField() {
+ if (inEditingMode()) {
+ View v = mTextEntry;
+ int x = viewToContent((v.getLeft() + v.getRight()) >> 1);
+ int y = viewToContent((v.getTop() + v.getBottom()) >> 1);
+ int contentSize = ViewConfiguration.getTouchSlop();
+ nativeMotionUp(x, y, contentSize, true);
+ }
+ }
+
+ private void doShortPress() {
+ if (mNativeClass == 0) {
+ return;
+ }
+ switchOutDrawHistory();
+ // mLastTouchX and mLastTouchY are the point in the current viewport
+ int contentX = viewToContent((int) mLastTouchX + mScrollX);
+ int contentY = viewToContent((int) mLastTouchY + mScrollY);
+ int contentSize = ViewConfiguration.getTouchSlop();
+ nativeMotionUp(contentX, contentY, contentSize, true);
+
+ // call uiOverride next to check whether it is a special node,
+ // phone/email/address, which are not handled by WebKit
+ if (nativeUpdateFocusNode()) {
+ FocusNode node = mFocusNode;
+ if (!node.mIsTextField && !node.mIsTextArea) {
+ if (mCallbackProxy.uiOverrideUrlLoading(node.mText)) {
+ return;
+ }
+ }
+ playSoundEffect(SoundEffectConstants.CLICK);
+ }
+ }
+
+ @Override
+ public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
+ boolean result = false;
+ if (inEditingMode()) {
+ result = mTextEntry.requestFocus(direction, previouslyFocusedRect);
+ } else {
+ result = super.requestFocus(direction, previouslyFocusedRect);
+ if (mWebViewCore.getSettings().getNeedInitialFocus()) {
+ // For cases such as GMail, where we gain focus from a direction,
+ // we want to move to the first available link.
+ // FIXME: If there are no visible links, we may not want to
+ int fakeKeyDirection = 0;
+ switch(direction) {
+ case View.FOCUS_UP:
+ fakeKeyDirection = KeyEvent.KEYCODE_DPAD_UP;
+ break;
+ case View.FOCUS_DOWN:
+ fakeKeyDirection = KeyEvent.KEYCODE_DPAD_DOWN;
+ break;
+ case View.FOCUS_LEFT:
+ fakeKeyDirection = KeyEvent.KEYCODE_DPAD_LEFT;
+ break;
+ case View.FOCUS_RIGHT:
+ fakeKeyDirection = KeyEvent.KEYCODE_DPAD_RIGHT;
+ break;
+ default:
+ return result;
+ }
+ if (mNativeClass != 0 && !nativeUpdateFocusNode()) {
+ navHandledKey(fakeKeyDirection, 1, true, 0);
+ }
+ }
+ }
+ return result;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+ int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+
+ int measuredHeight = heightSize;
+ int measuredWidth = widthSize;
+
+ // Grab the content size from WebViewCore.
+ int contentHeight = mContentHeight;
+ int contentWidth = mContentWidth;
+
+// Log.d(LOGTAG, "------- measure " + heightMode);
+
+ if (heightMode != MeasureSpec.EXACTLY) {
+ mHeightCanMeasure = true;
+ measuredHeight = contentHeight;
+ if (heightMode == MeasureSpec.AT_MOST) {
+ // If we are larger than the AT_MOST height, then our height can
+ // no longer be measured and we should scroll internally.
+ if (measuredHeight > heightSize) {
+ measuredHeight = heightSize;
+ mHeightCanMeasure = false;
+ }
+ }
+ }
+ if (mNativeClass != 0) {
+ nativeSetHeightCanMeasure(mHeightCanMeasure);
+ }
+ // For the width, always use the given size unless unspecified.
+ if (widthMode == MeasureSpec.UNSPECIFIED) {
+ mWidthCanMeasure = true;
+ measuredWidth = contentWidth;
+ }
+
+ synchronized (this) {
+ setMeasuredDimension(measuredWidth, measuredHeight);
+ }
+ }
+
+ @Override
+ public boolean requestChildRectangleOnScreen(View child,
+ Rect rect,
+ boolean immediate) {
+ rect.offset(child.getLeft() - child.getScrollX(),
+ child.getTop() - child.getScrollY());
+
+ int height = getHeight() - getHorizontalScrollbarHeight();
+ int screenTop = mScrollY;
+ int screenBottom = screenTop + height;
+
+ int scrollYDelta = 0;
+
+ if (rect.bottom > screenBottom && rect.top > screenTop) {
+ if (rect.height() > height) {
+ scrollYDelta += (rect.top - screenTop);
+ } else {
+ scrollYDelta += (rect.bottom - screenBottom);
+ }
+ } else if (rect.top < screenTop) {
+ scrollYDelta -= (screenTop - rect.top);
+ }
+
+ int width = getWidth() - getVerticalScrollbarWidth();
+ int screenLeft = mScrollX;
+ int screenRight = screenLeft + width;
+
+ int scrollXDelta = 0;
+
+ if (rect.right > screenRight && rect.left > screenLeft) {
+ if (rect.width() > width) {
+ scrollXDelta += (rect.left - screenLeft);
+ } else {
+ scrollXDelta += (rect.right - screenRight);
+ }
+ } else if (rect.left < screenLeft) {
+ scrollXDelta -= (screenLeft - rect.left);
+ }
+
+ if ((scrollYDelta | scrollXDelta) != 0) {
+ return pinScrollBy(scrollXDelta, scrollYDelta, !immediate);
+ }
+
+ return false;
+ }
+
+ /* package */ void replaceTextfieldText(int oldStart, int oldEnd,
+ String replace, int newStart, int newEnd) {
+ HashMap arg = new HashMap();
+ arg.put("focusData", new WebViewCore.FocusData(mFocusData));
+ arg.put("replace", replace);
+ arg.put("start", new Integer(newStart));
+ arg.put("end", new Integer(newEnd));
+ mWebViewCore.sendMessage(EventHub.REPLACE_TEXT, oldStart, oldEnd, arg);
+ }
+
+ /* package */ void passToJavaScript(String currentText, KeyEvent event) {
+ HashMap arg = new HashMap();
+ arg.put("focusData", new WebViewCore.FocusData(mFocusData));
+ arg.put("event", event);
+ arg.put("currentText", currentText);
+ // Increase our text generation number, and pass it to webcore thread
+ mTextGeneration++;
+ mWebViewCore.sendMessage(EventHub.PASS_TO_JS, mTextGeneration, 0, arg);
+ // WebKit's document state is not saved until about to leave the page.
+ // To make sure the host application, like Browser, has the up to date
+ // document state when it goes to background, we force to save the
+ // document state.
+ mWebViewCore.removeMessages(EventHub.SAVE_DOCUMENT_STATE);
+ mWebViewCore.sendMessageDelayed(EventHub.SAVE_DOCUMENT_STATE,
+ new WebViewCore.FocusData(mFocusData), 1000);
+ }
+
+ /* package */ WebViewCore getWebViewCore() {
+ return mWebViewCore;
+ }
+
+ //-------------------------------------------------------------------------
+ // Methods can be called from a separate thread, like WebViewCore
+ // If it needs to call the View system, it has to send message.
+ //-------------------------------------------------------------------------
+
+ /**
+ * General handler to receive message coming from webkit thread
+ */
+ class PrivateHandler extends Handler {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case REMEMBER_PASSWORD: {
+ mDatabase.setUsernamePassword(
+ msg.getData().getString("host"),
+ msg.getData().getString("username"),
+ msg.getData().getString("password"));
+ ((Message) msg.obj).sendToTarget();
+ break;
+ }
+ case NEVER_REMEMBER_PASSWORD: {
+ mDatabase.setUsernamePassword(
+ msg.getData().getString("host"), null, null);
+ ((Message) msg.obj).sendToTarget();
+ break;
+ }
+ case SWITCH_TO_SHORTPRESS: {
+ if (mTouchMode == TOUCH_INIT_MODE) {
+ mTouchMode = TOUCH_SHORTPRESS_START_MODE;
+ updateSelection();
+ }
+ break;
+ }
+ case SWITCH_TO_LONGPRESS: {
+ mTouchMode = TOUCH_DONE_MODE;
+ performLongClick();
+ updateTextEntry();
+ break;
+ }
+ case RELEASE_SINGLE_TAP: {
+ mTouchMode = TOUCH_DONE_MODE;
+ doShortPress();
+ break;
+ }
+ case SWITCH_TO_ENTER:
+ if (LOGV_ENABLED) Log.v(LOGTAG, "SWITCH_TO_ENTER");
+ mTouchMode = TOUCH_DONE_MODE;
+ onKeyUp(KeyEvent.KEYCODE_ENTER
+ , new KeyEvent(KeyEvent.ACTION_UP
+ , KeyEvent.KEYCODE_ENTER));
+ break;
+ case SCROLL_BY_MSG_ID:
+ setContentScrollBy(msg.arg1, msg.arg2);
+ break;
+ case SYNC_SCROLL_TO_MSG_ID:
+ if (mUserScroll) {
+ // if user has scrolled explicitly, don't sync the
+ // scroll position any more
+ mUserScroll = false;
+ break;
+ }
+ // fall through
+ case SCROLL_TO_MSG_ID:
+ if (setContentScrollTo(msg.arg1, msg.arg2)) {
+ // if we can't scroll to the exact position due to pin,
+ // send a message to WebCore to re-scroll when we get a
+ // new picture
+ mUserScroll = false;
+ mWebViewCore.sendMessage(EventHub.SYNC_SCROLL,
+ msg.arg1, msg.arg2);
+ }
+ break;
+ case SPAWN_SCROLL_TO_MSG_ID:
+ spawnContentScrollTo(msg.arg1, msg.arg2);
+ break;
+ case NEW_PICTURE_MSG_ID:
+ // called for new content
+ final Point viewSize = (Point) msg.obj;
+ if (mZoomScale > 0) {
+ if (Math.abs(mZoomScale * viewSize.x -
+ getViewWidth()) < 1) {
+ mZoomScale = 0;
+ mWebViewCore.sendMessage(EventHub.SET_SNAP_ANCHOR,
+ 0, 0);
+ }
+ }
+ // We update the layout (i.e. request a layout from the
+ // view system) if the last view size that we sent to
+ // WebCore matches the view size of the picture we just
+ // received in the fixed dimension.
+ final boolean updateLayout = viewSize.x == mLastWidthSent
+ && viewSize.y == mLastHeightSent;
+ recordNewContentSize(msg.arg1, msg.arg2, updateLayout);
+ invalidate();
+ if (mPictureListener != null) {
+ mPictureListener.onNewPicture(WebView.this, capturePicture());
+ }
+ break;
+ case WEBCORE_INITIALIZED_MSG_ID:
+ // nativeCreate sets mNativeClass to a non-zero value
+ nativeCreate(msg.arg1);
+ break;
+ case UPDATE_TEXTFIELD_TEXT_MSG_ID:
+ // Make sure that the textfield is currently focused
+ // and representing the same node as the pointer.
+ if (inEditingMode() &&
+ mTextEntry.isSameTextField(msg.arg1)) {
+ if (msg.getData().getBoolean("password")) {
+ Spannable text = (Spannable) mTextEntry.getText();
+ int start = Selection.getSelectionStart(text);
+ int end = Selection.getSelectionEnd(text);
+ mTextEntry.setInPassword(true);
+ // Restore the selection, which may have been
+ // ruined by setInPassword.
+ Spannable pword = (Spannable) mTextEntry.getText();
+ Selection.setSelection(pword, start, end);
+ // If the text entry has created more events, ignore
+ // this one.
+ } else if (msg.arg2 == mTextGeneration) {
+ mTextEntry.setTextAndKeepSelection(
+ (String) msg.obj);
+ }
+ }
+ break;
+ case DID_FIRST_LAYOUT_MSG_ID:
+ if (mNativeClass == 0) {
+ break;
+ }
+// Do not reset the focus or clear the text; the user may have already
+// navigated or entered text at this point. The focus should have gotten
+// reset, if need be, when the focus cache was built. Similarly, the text
+// view should already be torn down and rebuilt if needed.
+// nativeResetFocus();
+// clearTextEntry();
+ HashMap scaleLimit = (HashMap) msg.obj;
+ int minScale = (Integer) scaleLimit.get("minScale");
+ if (minScale == 0) {
+ mMinZoomScale = DEFAULT_MIN_ZOOM_SCALE;
+ } else {
+ mMinZoomScale = (float) (minScale / 100.0);
+ }
+ int maxScale = (Integer) scaleLimit.get("maxScale");
+ if (maxScale == 0) {
+ mMaxZoomScale = DEFAULT_MAX_ZOOM_SCALE;
+ } else {
+ mMaxZoomScale = (float) (maxScale / 100.0);
+ }
+ // If history Picture is drawn, don't update zoomWidth
+ if (mDrawHistory) {
+ break;
+ }
+ int width = getViewWidth();
+ if (width == 0) {
+ break;
+ }
+ int initialScale = msg.arg1;
+ int viewportWidth = msg.arg2;
+ float scale = 1.0f;
+ if (mInitialScale > 0) {
+ scale = mInitialScale / 100.0f;
+ } else {
+ if (mWebViewCore.getSettings().getUseWideViewPort()) {
+ // force viewSizeChanged by setting mLastWidthSent
+ // to 0
+ mLastWidthSent = 0;
+ }
+ // by default starting a new page with 100% zoom scale.
+ scale = initialScale == 0 ? (viewportWidth > 0 ?
+ ((float) width / viewportWidth) : 1.0f)
+ : initialScale / 100.0f;
+ }
+ setNewZoomScale(scale, false);
+ break;
+ case MARK_NODE_INVALID_ID:
+ nativeMarkNodeInvalid(msg.arg1);
+ break;
+ case NOTIFY_FOCUS_SET_MSG_ID:
+ if (mNativeClass != 0) {
+ nativeNotifyFocusSet(inEditingMode());
+ }
+ break;
+ case UPDATE_TEXT_ENTRY_MSG_ID:
+ updateTextEntry();
+ break;
+ case RECOMPUTE_FOCUS_MSG_ID:
+ if (mNativeClass != 0) {
+ nativeRecomputeFocus();
+ }
+ break;
+ case UPDATE_TEXT_ENTRY_ADAPTER:
+ HashMap data = (HashMap) msg.obj;
+ if (mTextEntry.isSameTextField(msg.arg1)) {
+ ArrayAdapter<String> adapter =
+ (ArrayAdapter<String>) data.get("adapter");
+ mTextEntry.setAdapter(adapter);
+ }
+ break;
+ case UPDATE_CLIPBOARD:
+ String str = (String) msg.obj;
+ if (LOGV_ENABLED) {
+ Log.v(LOGTAG, "UPDATE_CLIPBOARD " + str);
+ }
+ try {
+ IClipboard clip = IClipboard.Stub.asInterface(
+ ServiceManager.getService("clipboard"));
+ clip.setClipboardText(str);
+ } catch (android.os.RemoteException e) {
+ Log.e(LOGTAG, "Clipboard failed", e);
+ }
+ break;
+ case RESUME_WEBCORE_UPDATE:
+ WebViewCore.resumeUpdate(mWebViewCore);
+ break;
+
+ case LONG_PRESS_TRACKBALL:
+ mTrackTrackball = false;
+ mTrackballDown = false;
+ performLongClick();
+ break;
+
+ default:
+ super.handleMessage(msg);
+ break;
+ }
+ }
+ }
+
+ // Class used to use a dropdown for a <select> element
+ private class InvokeListBox implements Runnable {
+ // Strings for the labels in the listbox.
+ private String[] mArray;
+ // Array representing whether each item is enabled.
+ private boolean[] mEnableArray;
+ // Whether the listbox allows multiple selection.
+ private boolean mMultiple;
+ // Passed in to a list with multiple selection to tell
+ // which items are selected.
+ private int[] mSelectedArray;
+ // Passed in to a list with single selection to tell
+ // where the initial selection is.
+ private int mSelection;
+
+ private Container[] mContainers;
+
+ // Need these to provide stable ids to my ArrayAdapter,
+ // which normally does not have stable ids. (Bug 1250098)
+ private class Container extends Object {
+ String mString;
+ boolean mEnabled;
+ int mId;
+
+ public String toString() {
+ return mString;
+ }
+ }
+
+ /**
+ * Subclass ArrayAdapter so we can disable OptionGroupLabels,
+ * and allow filtering.
+ */
+ private class MyArrayListAdapter extends ArrayAdapter<Container> {
+ public MyArrayListAdapter(Context context, Container[] objects, boolean multiple) {
+ super(context,
+ multiple ? com.android.internal.R.layout.select_dialog_multichoice :
+ com.android.internal.R.layout.select_dialog_singlechoice,
+ objects);
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ private Container item(int position) {
+ if (position < 0 || position >= getCount()) {
+ return null;
+ }
+ return (Container) getItem(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ Container item = item(position);
+ if (item == null) {
+ return -1;
+ }
+ return item.mId;
+ }
+
+ @Override
+ public boolean areAllItemsEnabled() {
+ return false;
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ Container item = item(position);
+ if (item == null) {
+ return false;
+ }
+ return item.mEnabled;
+ }
+ }
+
+ private InvokeListBox(String[] array,
+ boolean[] enabled, int[] selected) {
+ mMultiple = true;
+ mSelectedArray = selected;
+
+ int length = array.length;
+ mContainers = new Container[length];
+ for (int i = 0; i < length; i++) {
+ mContainers[i] = new Container();
+ mContainers[i].mString = array[i];
+ mContainers[i].mEnabled = enabled[i];
+ mContainers[i].mId = i;
+ }
+ }
+
+ private InvokeListBox(String[] array, boolean[] enabled, int
+ selection) {
+ mSelection = selection;
+ mMultiple = false;
+
+ int length = array.length;
+ mContainers = new Container[length];
+ for (int i = 0; i < length; i++) {
+ mContainers[i] = new Container();
+ mContainers[i].mString = array[i];
+ mContainers[i].mEnabled = enabled[i];
+ mContainers[i].mId = i;
+ }
+ }
+
+ public void run() {
+ final ListView listView = (ListView) LayoutInflater.from(mContext)
+ .inflate(com.android.internal.R.layout.select_dialog, null);
+ final MyArrayListAdapter adapter = new
+ MyArrayListAdapter(mContext, mContainers, mMultiple);
+ AlertDialog.Builder b = new AlertDialog.Builder(mContext)
+ .setView(listView).setCancelable(true)
+ .setInverseBackgroundForced(true);
+
+ if (mMultiple) {
+ b.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ mWebViewCore.sendMessage(
+ EventHub.LISTBOX_CHOICES,
+ adapter.getCount(), 0,
+ listView.getCheckedItemPositions());
+ }});
+ b.setNegativeButton(android.R.string.cancel, null);
+ }
+ final AlertDialog dialog = b.create();
+ listView.setAdapter(adapter);
+ listView.setFocusableInTouchMode(true);
+ // There is a bug (1250103) where the checks in a ListView with
+ // multiple items selected are associated with the positions, not
+ // the ids, so the items do not properly retain their checks when
+ // filtered. Do not allow filtering on multiple lists until
+ // that bug is fixed.
+
+ // Disable filter altogether
+ // listView.setTextFilterEnabled(!mMultiple);
+ if (mMultiple) {
+ listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
+ int length = mSelectedArray.length;
+ for (int i = 0; i < length; i++) {
+ listView.setItemChecked(mSelectedArray[i], true);
+ }
+ } else {
+ listView.setOnItemClickListener(new OnItemClickListener() {
+ public void onItemClick(AdapterView parent, View v,
+ int position, long id) {
+ mWebViewCore.sendMessage(
+ EventHub.SINGLE_LISTBOX_CHOICE, (int)id, 0);
+ dialog.dismiss();
+ }
+ });
+ if (mSelection != -1) {
+ listView.setSelection(mSelection);
+ }
+ }
+ dialog.show();
+ }
+ }
+
+ /*
+ * Request a dropdown menu for a listbox with multiple selection.
+ *
+ * @param array Labels for the listbox.
+ * @param enabledArray Which positions are enabled.
+ * @param selectedArray Which positions are initally selected.
+ */
+ void requestListBox(String[] array, boolean[]enabledArray, int[]
+ selectedArray) {
+ mPrivateHandler.post(
+ new InvokeListBox(array, enabledArray, selectedArray));
+ }
+
+ /*
+ * Request a dropdown menu for a listbox with single selection or a single
+ * <select> element.
+ *
+ * @param array Labels for the listbox.
+ * @param enabledArray Which positions are enabled.
+ * @param selection Which position is initally selected.
+ */
+ void requestListBox(String[] array, boolean[]enabledArray, int selection) {
+ mPrivateHandler.post(
+ new InvokeListBox(array, enabledArray, selection));
+ }
+
+ // called by JNI
+ private void sendFinalFocus(int frame, int node, int x, int y) {
+ WebViewCore.FocusData focusData = new WebViewCore.FocusData();
+ focusData.mFrame = frame;
+ focusData.mNode = node;
+ focusData.mX = x;
+ focusData.mY = y;
+ mWebViewCore.sendMessage(EventHub.SET_FINAL_FOCUS,
+ EventHub.NO_FOCUS_CHANGE_BLOCK, 0, focusData);
+ }
+
+ // called by JNI
+ private void setFocusData(int moveGeneration, int buildGeneration,
+ int frame, int node, int x, int y, boolean ignoreNullFocus) {
+ mFocusData.mMoveGeneration = moveGeneration;
+ mFocusData.mBuildGeneration = buildGeneration;
+ mFocusData.mFrame = frame;
+ mFocusData.mNode = node;
+ mFocusData.mX = x;
+ mFocusData.mY = y;
+ mFocusData.mIgnoreNullFocus = ignoreNullFocus;
+ }
+
+ // called by JNI
+ private void sendKitFocus() {
+ WebViewCore.FocusData focusData = new WebViewCore.FocusData(mFocusData);
+ mWebViewCore.sendMessageDelayed(EventHub.SET_KIT_FOCUS, focusData,
+ SET_KIT_FOCUS_DELAY);
+ }
+
+ // called by JNI
+ private void sendMotionUp(int touchGeneration, int buildGeneration,
+ int frame, int node, int x, int y, int size, boolean isClick,
+ boolean retry) {
+ WebViewCore.TouchUpData touchUpData = new WebViewCore.TouchUpData();
+ touchUpData.mMoveGeneration = touchGeneration;
+ touchUpData.mBuildGeneration = buildGeneration;
+ touchUpData.mSize = size;
+ touchUpData.mIsClick = isClick;
+ touchUpData.mRetry = retry;
+ mFocusData.mFrame = touchUpData.mFrame = frame;
+ mFocusData.mNode = touchUpData.mNode = node;
+ mFocusData.mX = touchUpData.mX = x;
+ mFocusData.mY = touchUpData.mY = y;
+ mWebViewCore.sendMessage(EventHub.TOUCH_UP, touchUpData);
+ }
+
+
+ private int getScaledMaxXScroll() {
+ int width;
+ if (mHeightCanMeasure == false) {
+ width = getViewWidth() / 4;
+ } else {
+ Rect visRect = new Rect();
+ calcOurVisibleRect(visRect);
+ width = visRect.width() / 2;
+ }
+ // FIXME the divisor should be retrieved from somewhere
+ return viewToContent(width);
+ }
+
+ private int getScaledMaxYScroll() {
+ int height;
+ if (mHeightCanMeasure == false) {
+ height = getViewHeight() / 4;
+ } else {
+ Rect visRect = new Rect();
+ calcOurVisibleRect(visRect);
+ height = visRect.height() / 2;
+ }
+ // FIXME the divisor should be retrieved from somewhere
+ // the closest thing today is hard-coded into ScrollView.java
+ // (from ScrollView.java, line 363) int maxJump = height/2;
+ return viewToContent(height);
+ }
+
+ /**
+ * Called by JNI to invalidate view
+ */
+ private void viewInvalidate() {
+ invalidate();
+ }
+
+ // return true if the key was handled
+ private boolean navHandledKey(int keyCode, int count, boolean noScroll
+ , long time) {
+ if (mNativeClass == 0) {
+ return false;
+ }
+ mLastFocusTime = time;
+ mLastFocusBounds = nativeGetFocusRingBounds();
+ boolean keyHandled = nativeMoveFocus(keyCode, count, noScroll) == false;
+ if (LOGV_ENABLED) {
+ Log.v(LOGTAG, "navHandledKey mLastFocusBounds=" + mLastFocusBounds
+ + " mLastFocusTime=" + mLastFocusTime
+ + " handled=" + keyHandled);
+ }
+ if (keyHandled == false || mHeightCanMeasure == false) {
+ return keyHandled;
+ }
+ Rect contentFocus = nativeGetFocusRingBounds();
+ if (contentFocus.isEmpty()) return keyHandled;
+ Rect viewFocus = new Rect(contentToView(contentFocus.left)
+ , contentToView(contentFocus.top)
+ , contentToView(contentFocus.right)
+ , contentToView(contentFocus.bottom));
+ Rect visRect = new Rect();
+ calcOurVisibleRect(visRect);
+ Rect outset = new Rect(visRect);
+ int maxXScroll = visRect.width() / 2;
+ int maxYScroll = visRect.height() / 2;
+ outset.inset(-maxXScroll, -maxYScroll);
+ if (Rect.intersects(outset, viewFocus) == false) {
+ return keyHandled;
+ }
+ // FIXME: Necessary because ScrollView/ListView do not scroll left/right
+ int maxH = Math.min(viewFocus.right - visRect.right, maxXScroll);
+ if (maxH > 0) {
+ pinScrollBy(maxH, 0, true);
+ } else {
+ maxH = Math.max(viewFocus.left - visRect.left, -maxXScroll);
+ if (maxH < 0) {
+ pinScrollBy(maxH, 0, true);
+ }
+ }
+ if (mLastFocusBounds.isEmpty()) return keyHandled;
+ if (mLastFocusBounds.equals(contentFocus)) return keyHandled;
+ if (LOGV_ENABLED) {
+ Log.v(LOGTAG, "navHandledKey contentFocus=" + contentFocus);
+ }
+ requestRectangleOnScreen(viewFocus);
+ mUserScroll = true;
+ return keyHandled;
+ }
+
+ /**
+ * Set the background color. It's white by default. Pass
+ * zero to make the view transparent.
+ * @param color the ARGB color described by Color.java
+ */
+ public void setBackgroundColor(int color) {
+ mBackgroundColor = color;
+ mWebViewCore.sendMessage(EventHub.SET_BACKGROUND_COLOR, color);
+ }
+
+ public void debugDump() {
+ nativeDebugDump();
+ mWebViewCore.sendMessage(EventHub.DUMP_WEBKIT);
+ }
+
+ /**
+ * Update our cache with updatedText.
+ * @param updatedText The new text to put in our cache.
+ */
+ /* package */ void updateCachedTextfield(String updatedText) {
+ // Also place our generation number so that when we look at the cache
+ // we recognize that it is up to date.
+ nativeUpdateCachedTextfield(updatedText, mTextGeneration);
+ }
+
+ // Never call this version except by updateCachedTextfield(String) -
+ // we always want to pass in our generation number.
+ private native void nativeUpdateCachedTextfield(String updatedText,
+ int generation);
+ private native void nativeClearFocus(int x, int y);
+ private native void nativeCreate(int ptr);
+ private native void nativeDebugDump();
+ private native void nativeDestroy();
+ private native void nativeDrawFocusRing(Canvas content);
+ private native void nativeDrawSelection(Canvas content
+ , int x, int y, boolean extendSelection);
+ private native boolean nativeUpdateFocusNode();
+ private native Rect nativeGetFocusRingBounds();
+ private native Rect nativeGetNavBounds();
+ private native void nativeMarkNodeInvalid(int node);
+ private native void nativeMotionUp(int x, int y, int slop, boolean isClick);
+ // returns false if it handled the key
+ private native boolean nativeMoveFocus(int keyCode, int count,
+ boolean noScroll);
+ private native void nativeNotifyFocusSet(boolean inEditingMode);
+ private native void nativeRecomputeFocus();
+ private native void nativeRecordButtons();
+ private native void nativeResetFocus();
+ private native void nativeResetNavClipBounds();
+ private native void nativeSelectBestAt(Rect rect);
+ private native void nativeSetFollowedLink(boolean followed);
+ private native void nativeSetHeightCanMeasure(boolean measure);
+ private native void nativeSetNavBounds(Rect rect);
+ private native void nativeSetNavClipBounds(Rect rect);
+ private native boolean nativeHasSrcUrl();
+ private native boolean nativeIsImage(int x, int y);
+ private native void nativeMoveSelection(int x, int y
+ , boolean extendSelection);
+ private native Region nativeGetSelection();
+}
diff --git a/core/java/android/webkit/WebViewClient.java b/core/java/android/webkit/WebViewClient.java
new file mode 100644
index 0000000..a185779
--- /dev/null
+++ b/core/java/android/webkit/WebViewClient.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.webkit;
+
+import android.graphics.Bitmap;
+import android.net.http.SslError;
+import android.os.Message;
+import android.view.KeyEvent;
+
+public class WebViewClient {
+
+ /**
+ * Give the host application a chance to take over the control when a new
+ * url is about to be loaded in the current WebView. If WebViewClient is not
+ * provided, by default WebView will ask Activity Manager to choose the
+ * proper handler for the url. If WebViewClient is provided, return true
+ * means the host application handles the url, while return false means the
+ * current WebView handles the url.
+ *
+ * @param view The WebView that is initiating the callback.
+ * @param url The url to be loaded.
+ * @return True if the host application wants to leave the current WebView
+ * and handle the url itself, otherwise return false.
+ */
+ public boolean shouldOverrideUrlLoading(WebView view, String url) {
+ return false;
+ }
+
+ /**
+ * Notify the host application that a page has started loading. This method
+ * is called once for each main frame load so a page with iframes or
+ * framesets will call onPageStarted one time for the main frame. This also
+ * means that onPageStarted will not be called when the contents of an
+ * embedded frame changes, i.e. clicking a link whose target is an iframe.
+ *
+ * @param view The WebView that is initiating the callback.
+ * @param url The url to be loaded.
+ * @param favicon The favicon for this page if it already exists in the
+ * database.
+ */
+ public void onPageStarted(WebView view, String url, Bitmap favicon) {
+ }
+
+ /**
+ * Notify the host application that a page has finished loading. This method
+ * is called only for main frame. When onPageFinished() is called, the
+ * rendering picture may not be updated yet. To get the notification for the
+ * new Picture, use {@link WebView.PictureListener#onNewPicture}.
+ *
+ * @param view The WebView that is initiating the callback.
+ * @param url The url of the page.
+ */
+ public void onPageFinished(WebView view, String url) {
+ }
+
+ /**
+ * Notify the host application that the WebView will load the resource
+ * specified by the given url.
+ *
+ * @param view The WebView that is initiating the callback.
+ * @param url The url of the resource the WebView will load.
+ */
+ public void onLoadResource(WebView view, String url) {
+ }
+
+ /**
+ * Notify the host application that there have been an excessive number of
+ * HTTP redirects. As the host application if it would like to continue
+ * trying to load the resource. The default behavior is to send the cancel
+ * message.
+ *
+ * @param view The WebView that is initiating the callback.
+ * @param cancelMsg The message to send if the host wants to cancel
+ * @param continueMsg The message to send if the host wants to continue
+ */
+ public void onTooManyRedirects(WebView view, Message cancelMsg,
+ Message continueMsg) {
+ cancelMsg.sendToTarget();
+ }
+
+ /**
+ * Report an error to an activity. These errors come up from WebCore, and
+ * are network errors.
+ *
+ * @param view The WebView that is initiating the callback.
+ * @param errorCode The HTTP error code.
+ * @param description A String description.
+ * @param failingUrl The url that failed.
+ */
+ public void onReceivedError(WebView view, int errorCode,
+ String description, String failingUrl) {
+ }
+
+ /**
+ * As the host application if the browser should resend data as the
+ * requested page was a result of a POST. The default is to not resend the
+ * data.
+ *
+ * @param view The WebView that is initiating the callback.
+ * @param dontResend The message to send if the browser should not resend
+ * @param resend The message to send if the browser should resend data
+ */
+ public void onFormResubmission(WebView view, Message dontResend,
+ Message resend) {
+ dontResend.sendToTarget();
+ }
+
+ /**
+ * Notify the host application to update its visited links database.
+ *
+ * @param view The WebView that is initiating the callback.
+ * @param url The url being visited.
+ * @param isReload True if this url is being reloaded.
+ */
+ public void doUpdateVisitedHistory(WebView view, String url,
+ boolean isReload) {
+ }
+
+ /**
+ * Notify the host application to handle a ssl certificate error request
+ * (display the error to the user and ask whether to proceed or not). The
+ * host application has to call either handler.cancel() or handler.proceed()
+ * as the connection is suspended and waiting for the response. The default
+ * behavior is to cancel the load.
+ *
+ * @param view The WebView that is initiating the callback.
+ * @param handler An SslErrorHandler object that will handle the user's
+ * response.
+ * @param error The SSL error object.
+ * @hide - hide this because it contains a parameter of type SslError,
+ * which is located in a hidden package.
+ */
+ public void onReceivedSslError(WebView view, SslErrorHandler handler,
+ SslError error) {
+ handler.cancel();
+ }
+
+ /**
+ * Notify the host application to handle an authentication request. The
+ * default behavior is to cancel the request.
+ *
+ * @param view The WebView that is initiating the callback.
+ * @param handler The HttpAuthHandler that will handle the user's response.
+ * @param host The host requiring authentication.
+ * @param realm A description to help store user credentials for future
+ * visits.
+ */
+ public void onReceivedHttpAuthRequest(WebView view,
+ HttpAuthHandler handler, String host, String realm) {
+ handler.cancel();
+ }
+
+ /**
+ * Give the host application a chance to handle the key event synchronously.
+ * e.g. menu shortcut key events need to be filtered this way. If return
+ * true, WebView will not handle the key event. If return false, WebView
+ * will always handle the key event, so none of the super in the view chain
+ * will see the key event. The default behavior returns false.
+ *
+ * @param view The WebView that is initiating the callback.
+ * @param event The key event.
+ * @return True if the host application wants to handle the key event
+ * itself, otherwise return false
+ */
+ public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event) {
+ return false;
+ }
+
+ /**
+ * Notify the host application that a key was not handled by the WebView.
+ * Except system keys, WebView always consumes the keys in the normal flow
+ * or if shouldOverrideKeyEvent returns true. This is called asynchronously
+ * from where the key is dispatched. It gives the host application an chance
+ * to handle the unhandled key events.
+ *
+ * @param view The WebView that is initiating the callback.
+ * @param event The key event.
+ */
+ public void onUnhandledKeyEvent(WebView view, KeyEvent event) {
+ }
+
+ /**
+ * Notify the host application that the scale applied to the WebView has
+ * changed.
+ *
+ * @param view he WebView that is initiating the callback.
+ * @param oldScale The old scale factor
+ * @param newScale The new scale factor
+ */
+ public void onScaleChanged(WebView view, float oldScale, float newScale) {
+ }
+}
diff --git a/core/java/android/webkit/WebViewCore.java b/core/java/android/webkit/WebViewCore.java
new file mode 100644
index 0000000..263346e
--- /dev/null
+++ b/core/java/android/webkit/WebViewCore.java
@@ -0,0 +1,1553 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.webkit;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.DrawFilter;
+import android.graphics.Paint;
+import android.graphics.PaintFlagsDrawFilter;
+import android.graphics.Picture;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Process;
+import android.util.Config;
+import android.util.Log;
+import android.util.SparseBooleanArray;
+import android.view.KeyEvent;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+import junit.framework.Assert;
+
+final class WebViewCore {
+
+ private static final String LOGTAG = "webcore";
+ static final boolean DEBUG = false;
+ static final boolean LOGV_ENABLED = DEBUG ? Config.LOGD : Config.LOGV;
+
+ static {
+ // Load libwebcore during static initialization. This happens in the
+ // zygote process so it will be shared read-only across all app
+ // processes.
+ System.loadLibrary("webcore");
+ }
+
+ /*
+ * WebViewCore always executes in the same thread as the native webkit.
+ */
+
+ // The WebView that corresponds to this WebViewCore.
+ private WebView mWebView;
+ // Proxy for handling callbacks from native code
+ private final CallbackProxy mCallbackProxy;
+ // Settings object for maintaining all settings
+ private final WebSettings mSettings;
+ // Context for initializing the BrowserFrame with the proper assets.
+ private final Context mContext;
+ // The pointer to a native view object.
+ private int mNativeClass;
+ // The BrowserFrame is an interface to the native Frame component.
+ private BrowserFrame mBrowserFrame;
+
+
+ /* This is a ring of pictures for content. After B is built, it is swapped
+ with A.
+ */
+ private Picture mContentPictureA = new Picture(); // draw()
+ private Picture mContentPictureB = new Picture(); // nativeDraw()
+
+ /*
+ * range is from 200 to 10,000. 0 is a special value means device-width. -1
+ * means undefined.
+ */
+ private int mViewportWidth = -1;
+
+ /*
+ * range is from 200 to 10,000. 0 is a special value means device-height. -1
+ * means undefined.
+ */
+ private int mViewportHeight = -1;
+
+ /*
+ * scale in percent, range is from 1 to 1000. 0 means undefined.
+ */
+ private int mViewportInitialScale = 0;
+
+ /*
+ * scale in percent, range is from 1 to 1000. 0 means undefined.
+ */
+ private int mViewportMinimumScale = 0;
+
+ /*
+ * scale in percent, range is from 1 to 1000. 0 means undefined.
+ */
+ private int mViewportMaximumScale = 0;
+
+ private boolean mViewportUserScalable = true;
+
+ private int mRestoredScale = 100;
+ private int mRestoredX = 0;
+ private int mRestoredY = 0;
+
+ private int mWebkitScrollX = 0;
+ private int mWebkitScrollY = 0;
+
+ // The thread name used to identify the WebCore thread and for use in
+ // debugging other classes that require operation within the WebCore thread.
+ /* package */ static final String THREAD_NAME = "WebViewCoreThread";
+
+ public WebViewCore(Context context, WebView w, CallbackProxy proxy) {
+ // No need to assign this in the WebCore thread.
+ mCallbackProxy = proxy;
+ mWebView = w;
+ // This context object is used to initialize the WebViewCore during
+ // subwindow creation.
+ mContext = context;
+
+ // We need to wait for the initial thread creation before sending
+ // a message to the WebCore thread.
+ // XXX: This is the only time the UI thread will wait for the WebCore
+ // thread!
+ synchronized (WebViewCore.class) {
+ if (sWebCoreHandler == null) {
+ // Create a global thread and start it.
+ Thread t = new Thread(new WebCoreThread());
+ t.setName(THREAD_NAME);
+ t.start();
+ try {
+ WebViewCore.class.wait();
+ } catch (InterruptedException e) {
+ Log.e(LOGTAG, "Caught exception while waiting for thread " +
+ "creation.");
+ Log.e(LOGTAG, Log.getStackTraceString(e));
+ }
+ }
+ }
+ // Create an EventHub to handle messages before and after the thread is
+ // ready.
+ mEventHub = new EventHub();
+ // Create a WebSettings object for maintaining all settings
+ mSettings = new WebSettings(mContext);
+ // The WebIconDatabase needs to be initialized within the UI thread so
+ // just request the instance here.
+ WebIconDatabase.getInstance();
+ // Send a message to initialize the WebViewCore.
+ Message init = sWebCoreHandler.obtainMessage(
+ WebCoreThread.INITIALIZE, this);
+ sWebCoreHandler.sendMessage(init);
+ }
+
+ /* Initialize private data within the WebCore thread.
+ */
+ private void initialize() {
+ /* Initialize our private BrowserFrame class to handle all
+ * frame-related functions. We need to create a new view which
+ * in turn creates a C level FrameView and attaches it to the frame.
+ */
+ mBrowserFrame = new BrowserFrame(mContext, this, mCallbackProxy,
+ mSettings);
+ // Sync the native settings and also create the WebCore thread handler.
+ mSettings.syncSettingsAndCreateHandler(mBrowserFrame);
+ // Create the handler and transfer messages for the IconDatabase
+ WebIconDatabase.getInstance().createHandler();
+ // The transferMessages call will transfer all pending messages to the
+ // WebCore thread handler.
+ mEventHub.transferMessages();
+
+ // Send a message back to WebView to tell it that we have set up the
+ // WebCore thread.
+ if (mWebView != null) {
+ Message.obtain(mWebView.mPrivateHandler,
+ WebView.WEBCORE_INITIALIZED_MSG_ID,
+ mNativeClass, 0).sendToTarget();
+ }
+
+ }
+
+ /* Handle the initialization of WebViewCore during subwindow creation. This
+ * method is called from the WebCore thread but it is called before the
+ * INITIALIZE message can be handled.
+ */
+ /* package */ void initializeSubwindow() {
+ // Go ahead and initialize the core components.
+ initialize();
+ // Remove the INITIALIZE method so we don't try to initialize twice.
+ sWebCoreHandler.removeMessages(WebCoreThread.INITIALIZE, this);
+ }
+
+ /* Get the BrowserFrame component. This is used for subwindow creation. */
+ /* package */ BrowserFrame getBrowserFrame() {
+ return mBrowserFrame;
+ }
+
+ //-------------------------------------------------------------------------
+ // Common methods
+ //-------------------------------------------------------------------------
+
+ /**
+ * Causes all timers to pause. This applies to all WebViews in the current
+ * app process.
+ */
+ public static void pauseTimers() {
+ if (BrowserFrame.sJavaBridge == null) {
+ throw new IllegalStateException(
+ "No WebView has been created in this process!");
+ }
+ BrowserFrame.sJavaBridge.pause();
+ }
+
+ /**
+ * Resume all timers. This applies to all WebViews in the current process.
+ */
+ public static void resumeTimers() {
+ if (BrowserFrame.sJavaBridge == null) {
+ throw new IllegalStateException(
+ "No WebView has been created in this process!");
+ }
+ BrowserFrame.sJavaBridge.resume();
+ }
+
+ public WebSettings getSettings() {
+ return mSettings;
+ }
+
+ /**
+ * Invoke a javascript alert.
+ * @param message The message displayed in the alert.
+ */
+ protected void jsAlert(String url, String message) {
+ mCallbackProxy.onJsAlert(url, message);
+ }
+
+ /**
+ * Invoke a javascript confirm dialog.
+ * @param message The message displayed in the dialog.
+ * @return True if the user confirmed or false if the user cancelled.
+ */
+ protected boolean jsConfirm(String url, String message) {
+ return mCallbackProxy.onJsConfirm(url, message);
+ }
+
+ /**
+ * Invoke a javascript prompt dialog.
+ * @param message The message to be displayed in the dialog.
+ * @param defaultValue The default value in the prompt input.
+ * @return The input from the user or null to indicate the user cancelled
+ * the dialog.
+ */
+ protected String jsPrompt(String url, String message, String defaultValue) {
+ return mCallbackProxy.onJsPrompt(url, message, defaultValue);
+ }
+
+ /**
+ * Invoke a javascript before unload dialog.
+ * @param url The url that is requesting the dialog.
+ * @param message The message displayed in the dialog.
+ * @return True if the user confirmed or false if the user cancelled. False
+ * will cancel the navigation.
+ */
+ protected boolean jsUnload(String url, String message) {
+ return mCallbackProxy.onJsBeforeUnload(url, message);
+ }
+
+ //-------------------------------------------------------------------------
+ // JNI methods
+ //-------------------------------------------------------------------------
+
+ static native String nativeFindAddress(String addr);
+
+ /**
+ * Find and highlight an occurance of text matching find
+ * @param find The text to find.
+ * @param forward If true, search forward. Else, search backwards.
+ * @param fromSelection Whether to start from the current selection or from
+ * the beginning of the viewable page.
+ * @return boolean Whether the text was found.
+ */
+ private native boolean nativeFind(String find,
+ boolean forward,
+ boolean fromSelection);
+
+ /**
+ * Find all occurances of text matching find and highlight them.
+ * @param find The text to find.
+ * @return int The number of occurances of find found.
+ */
+ private native int nativeFindAll(String find);
+
+ /**
+ * Clear highlights on text created by nativeFindAll.
+ */
+ private native void nativeClearMatches();
+
+ private native void nativeDraw(Picture content);
+
+ private native boolean nativeKeyUp(int keycode, int keyvalue);
+
+ private native void nativeSendListBoxChoices(boolean[] choices, int size);
+
+ private native void nativeSendListBoxChoice(int choice);
+
+ /* Tell webkit what its width and height are, for the purposes
+ of layout/line-breaking. These coordinates are in document space,
+ which is the same as View coords unless we have zoomed the document
+ (see nativeSetZoom).
+ screenWidth is used by layout to wrap column around. If viewport uses
+ fixed size, screenWidth can be different from width with zooming.
+ should this be called nativeSetViewPortSize?
+ */
+ private native void nativeSetSize(int width, int height, int screenWidth,
+ float scale);
+
+ private native int nativeGetContentMinPrefWidth();
+
+ // Start: functions that deal with text editing
+ private native void nativeReplaceTextfieldText(int frame, int node, int x,
+ int y, int oldStart, int oldEnd, String replace, int newStart,
+ int newEnd);
+
+ private native void passToJs(int frame, int node, int x, int y, int gen,
+ String currentText, int keyCode, int keyValue, boolean down,
+ boolean cap, boolean fn, boolean sym);
+
+ private native void nativeSaveDocumentState(int frame, int node, int x,
+ int y);
+
+ private native void nativeSetFinalFocus(int framePtr, int nodePtr, int x,
+ int y, boolean block);
+
+ private native void nativeSetKitFocus(int moveGeneration,
+ int buildGeneration, int framePtr, int nodePtr, int x, int y,
+ boolean ignoreNullFocus);
+
+ private native String nativeRetrieveHref(int framePtr, int nodePtr);
+
+ /**
+ * Return the url of the image located at (x,y) in content coordinates, or
+ * null if there is no image at that point.
+ *
+ * @param x x content ordinate
+ * @param y y content ordinate
+ * @return String url of the image located at (x,y), or null if there is
+ * no image there.
+ */
+ private native String nativeRetrieveImageRef(int x, int y);
+
+ private native void nativeTouchUp(int touchGeneration,
+ int buildGeneration, int framePtr, int nodePtr, int x, int y,
+ int size, boolean isClick, boolean retry);
+
+ private native void nativeUnblockFocus();
+
+ private native void nativeUpdateFrameCache();
+
+ private native void nativeSetSnapAnchor(int x, int y);
+
+ private native void nativeSnapToAnchor();
+
+ private native void nativeSetBackgroundColor(int color);
+
+ private native void nativeDump();
+
+ private native void nativeRefreshPlugins(boolean reloadOpenPages);
+
+ /**
+ * 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.
+ */
+ private native void nativeDeleteSelection(int frame, int node, int x, int y,
+ int start, int end);
+
+ /**
+ * 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.
+ */
+ private native void nativeSetSelection(int frame, int node, int x, int y,
+ int start, int end);
+
+ private native String nativeGetSelection(Region sel);
+
+ // EventHub for processing messages
+ private final EventHub mEventHub;
+ // WebCore thread handler
+ private static Handler sWebCoreHandler;
+ // Class for providing Handler creation inside the WebCore thread.
+ private static class WebCoreThread implements Runnable {
+ // Message id for initializing a new WebViewCore.
+ private static final int INITIALIZE = 0;
+ private static final int REDUCE_PRIORITY = 1;
+ private static final int RESUME_PRIORITY = 2;
+ private static final int CACHE_TICKER = 3;
+ private static final int BLOCK_CACHE_TICKER = 4;
+ private static final int RESUME_CACHE_TICKER = 5;
+
+ private static final int CACHE_TICKER_INTERVAL = 60 * 1000; // 1 minute
+
+ private static boolean mCacheTickersBlocked = true;
+
+ public void run() {
+ Looper.prepare();
+ Assert.assertNull(sWebCoreHandler);
+ synchronized (WebViewCore.class) {
+ sWebCoreHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case INITIALIZE:
+ WebViewCore core = (WebViewCore) msg.obj;
+ synchronized (core) {
+ core.initialize();
+ core.notify();
+ }
+ break;
+
+ case REDUCE_PRIORITY:
+ // 3 is an adjustable number.
+ Process.setThreadPriority(
+ Process.THREAD_PRIORITY_DEFAULT + 3 *
+ Process.THREAD_PRIORITY_LESS_FAVORABLE);
+ break;
+
+ case RESUME_PRIORITY:
+ Process.setThreadPriority(
+ Process.THREAD_PRIORITY_DEFAULT);
+ break;
+
+ case CACHE_TICKER:
+ if (!mCacheTickersBlocked) {
+ CacheManager.endCacheTransaction();
+ CacheManager.startCacheTransaction();
+ sendMessageDelayed(
+ obtainMessage(CACHE_TICKER),
+ CACHE_TICKER_INTERVAL);
+ }
+ break;
+
+ case BLOCK_CACHE_TICKER:
+ if (CacheManager.endCacheTransaction()) {
+ mCacheTickersBlocked = true;
+ }
+ break;
+
+ case RESUME_CACHE_TICKER:
+ if (CacheManager.startCacheTransaction()) {
+ mCacheTickersBlocked = false;
+ }
+ break;
+ }
+ }
+ };
+ WebViewCore.class.notify();
+ }
+ Looper.loop();
+ }
+ }
+
+ static class FocusData {
+ FocusData() {}
+ FocusData(FocusData d) {
+ mMoveGeneration = d.mMoveGeneration;
+ mBuildGeneration = d.mBuildGeneration;
+ mFrame = d.mFrame;
+ mNode = d.mNode;
+ mX = d.mX;
+ mY = d.mY;
+ mIgnoreNullFocus = d.mIgnoreNullFocus;
+ }
+ int mMoveGeneration;
+ int mBuildGeneration;
+ int mFrame;
+ int mNode;
+ int mX;
+ int mY;
+ boolean mIgnoreNullFocus;
+ }
+
+ static class TouchUpData {
+ int mMoveGeneration;
+ int mBuildGeneration;
+ int mFrame;
+ int mNode;
+ int mX;
+ int mY;
+ int mSize;
+ boolean mIsClick;
+ boolean mRetry;
+ }
+
+ class EventHub {
+ // Message Ids
+ static final int LOAD_URL = 100;
+ static final int STOP_LOADING = 101;
+ static final int RELOAD = 102;
+ static final int KEY_DOWN = 103;
+ static final int KEY_UP = 104;
+ static final int VIEW_SIZE_CHANGED = 105;
+ static final int GO_BACK_FORWARD = 106;
+ static final int SET_VISIBLE_RECT = 107;
+ static final int RESTORE_STATE = 108;
+ static final int PAUSE_TIMERS = 109;
+ static final int RESUME_TIMERS = 110;
+ static final int CLEAR_CACHE = 111;
+ static final int CLEAR_HISTORY = 112;
+ static final int SET_SELECTION = 113;
+ static final int REPLACE_TEXT = 114;
+ static final int PASS_TO_JS = 115;
+ static final int FIND = 116;
+ static final int UPDATE_CACHE_AND_TEXT_ENTRY = 117;
+ static final int FIND_ALL = 118;
+ static final int CLEAR_MATCHES = 119;
+ static final int DOC_HAS_IMAGES = 120;
+ static final int SET_SNAP_ANCHOR = 121;
+ static final int DELETE_SELECTION = 122;
+ static final int LISTBOX_CHOICES = 123;
+ static final int SINGLE_LISTBOX_CHOICE = 124;
+ static final int DUMP_WEBKIT = 125;
+ static final int SET_BACKGROUND_COLOR = 126;
+ static final int UNBLOCK_FOCUS = 127;
+ static final int SAVE_DOCUMENT_STATE = 128;
+ static final int GET_SELECTION = 129;
+ static final int WEBKIT_DRAW = 130;
+ static final int SYNC_SCROLL = 131;
+ static final int REFRESH_PLUGINS = 132;
+
+ // UI nav messages
+ static final int REQUEST_IMAGE_HREF = 134;
+ static final int SET_FINAL_FOCUS = 135;
+ static final int SET_KIT_FOCUS = 136;
+ static final int REQUEST_FOCUS_HREF = 137;
+ static final int ADD_JS_INTERFACE = 138;
+ static final int LOAD_DATA = 139;
+
+ // motion
+ static final int TOUCH_UP = 140;
+
+ // Network-based messaging
+ static final int CLEAR_SSL_PREF_TABLE = 150;
+
+ // Test harness messages
+ static final int REQUEST_EXT_REPRESENTATION = 160;
+ static final int REQUEST_DOC_AS_TEXT = 161;
+ // private message ids
+ private static final int DESTROY = 200;
+
+ // flag values passed to message SET_FINAL_FOCUS
+ static final int NO_FOCUS_CHANGE_BLOCK = 0;
+ static final int BLOCK_FOCUS_CHANGE_UNTIL_KEY_UP = 1;
+
+ // Private handler for WebCore messages.
+ private Handler mHandler;
+ // Message queue for containing messages before the WebCore thread is
+ // ready.
+ private ArrayList<Message> mMessages = new ArrayList<Message>();
+ // Flag for blocking messages. This is used during DESTROY to avoid
+ // posting more messages to the EventHub or to WebView's event handler.
+ private boolean mBlockMessages;
+
+ private int mTid;
+ private int mSavedPriority;
+
+ /**
+ * Prevent other classes from creating an EventHub.
+ */
+ private EventHub() {}
+
+ /**
+ * Transfer all messages to the newly created webcore thread handler.
+ */
+ private void transferMessages() {
+ mTid = Process.myTid();
+ mSavedPriority = Process.getThreadPriority(mTid);
+
+ mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case WEBKIT_DRAW:
+ webkitDraw();
+ break;
+
+ case DESTROY:
+ // Time to take down the world. Cancel all pending
+ // loads and destroy the native view and frame.
+ mBrowserFrame.destroy();
+ mBrowserFrame = null;
+ mNativeClass = 0;
+ break;
+
+ case LOAD_URL:
+ loadUrl((String) msg.obj);
+ break;
+
+ case LOAD_DATA:
+ HashMap loadParams = (HashMap) msg.obj;
+ mBrowserFrame.loadData(
+ (String) loadParams.get("baseUrl"),
+ (String) loadParams.get("data"),
+ (String) loadParams.get("mimeType"),
+ (String) loadParams.get("encoding"),
+ (String) loadParams.get("failUrl"));
+ break;
+
+ case STOP_LOADING:
+ // If the WebCore has committed the load, but not
+ // finished the first layout yet, we need to set
+ // first layout done to trigger the interpreted side sync
+ // up with native side
+ if (mBrowserFrame.committed()
+ && !mBrowserFrame.firstLayoutDone()) {
+ mBrowserFrame.didFirstLayout(mBrowserFrame
+ .currentUrl());
+ }
+ // Do this after syncing up the layout state.
+ stopLoading();
+ break;
+
+ case RELOAD:
+ mBrowserFrame.reload(false);
+ break;
+
+ case KEY_DOWN:
+ keyDown(msg.arg1, (KeyEvent) msg.obj);
+ break;
+
+ case KEY_UP:
+ keyUp(msg.arg1, (KeyEvent) msg.obj);
+ break;
+
+ case VIEW_SIZE_CHANGED:
+ viewSizeChanged(msg.arg1, msg.arg2,
+ ((Float) msg.obj).floatValue());
+ break;
+
+ case SET_VISIBLE_RECT:
+ Rect r = (Rect) msg.obj;
+ // note: these are in document coordinates
+ // (inv-zoom)
+ nativeSetVisibleRect(r.left, r.top, r.width(),
+ r.height());
+ break;
+
+ case GO_BACK_FORWARD:
+ // If it is a standard load and the load is not
+ // committed yet, we interpret BACK as RELOAD
+ if (!mBrowserFrame.committed() && msg.arg1 == -1 &&
+ (mBrowserFrame.loadType() ==
+ BrowserFrame.FRAME_LOADTYPE_STANDARD)) {
+ mBrowserFrame.reload(true);
+ } else {
+ mBrowserFrame.goBackOrForward(msg.arg1);
+ }
+ break;
+
+ case RESTORE_STATE:
+ stopLoading();
+ restoreState(msg.arg1);
+ break;
+
+ case PAUSE_TIMERS:
+ mSavedPriority = Process.getThreadPriority(mTid);
+ Process.setThreadPriority(mTid,
+ Process.THREAD_PRIORITY_BACKGROUND);
+ pauseTimers();
+ if (CacheManager.disableTransaction()) {
+ WebCoreThread.mCacheTickersBlocked = true;
+ sWebCoreHandler.removeMessages(
+ WebCoreThread.CACHE_TICKER);
+ }
+ break;
+
+ case RESUME_TIMERS:
+ Process.setThreadPriority(mTid, mSavedPriority);
+ resumeTimers();
+ if (CacheManager.enableTransaction()) {
+ WebCoreThread.mCacheTickersBlocked = false;
+ sWebCoreHandler.sendMessageDelayed(
+ sWebCoreHandler.obtainMessage(
+ WebCoreThread.CACHE_TICKER),
+ WebCoreThread.CACHE_TICKER_INTERVAL);
+ }
+ break;
+
+ case CLEAR_CACHE:
+ mBrowserFrame.clearCache();
+ if (msg.arg1 == 1) {
+ CacheManager.removeAllCacheFiles();
+ }
+ break;
+
+ case CLEAR_HISTORY:
+ mCallbackProxy.getBackForwardList().
+ close(mBrowserFrame.mNativeFrame);
+ break;
+
+ case REPLACE_TEXT:
+ HashMap jMap = (HashMap) msg.obj;
+ FocusData fData = (FocusData) jMap.get("focusData");
+ String replace = (String) jMap.get("replace");
+ int newStart =
+ ((Integer) jMap.get("start")).intValue();
+ int newEnd =
+ ((Integer) jMap.get("end")).intValue();
+ nativeReplaceTextfieldText(fData.mFrame,
+ fData.mNode, fData.mX, fData.mY, msg.arg1,
+ msg.arg2, replace, newStart, newEnd);
+ break;
+
+ case PASS_TO_JS: {
+ HashMap jsMap = (HashMap) msg.obj;
+ FocusData fDat = (FocusData) jsMap.get("focusData");
+ KeyEvent evt = (KeyEvent) jsMap.get("event");
+ int keyCode = evt.getKeyCode();
+ int keyValue = evt.getUnicodeChar();
+ int generation = msg.arg1;
+ passToJs(fDat.mFrame, fDat.mNode, fDat.mX, fDat.mY,
+ generation,
+ (String) jsMap.get("currentText"),
+ keyCode,
+ keyValue,
+ evt.isDown(),
+ evt.isShiftPressed(), evt.isAltPressed(),
+ evt.isSymPressed());
+ break;
+ }
+
+ case SAVE_DOCUMENT_STATE: {
+ FocusData fDat = (FocusData) msg.obj;
+ nativeSaveDocumentState(fDat.mFrame, fDat.mNode,
+ fDat.mX, fDat.mY);
+ break;
+ }
+
+ case FIND:
+ /* arg1:
+ * 1 - Find next
+ * -1 - Find previous
+ * 0 - Find first
+ */
+ Message response = (Message) msg.obj;
+ boolean find = nativeFind(msg.getData().getString("find"),
+ msg.arg1 != -1, msg.arg1 != 0);
+ response.arg1 = find ? 1 : 0;
+ response.sendToTarget();
+ break;
+
+ case FIND_ALL:
+ int found = nativeFindAll(msg.getData().getString("find"));
+ Message resAll = (Message) msg.obj;
+ resAll.arg1 = found;
+ resAll.sendToTarget();
+ break;
+
+ case CLEAR_MATCHES:
+ nativeClearMatches();
+ break;
+
+ case CLEAR_SSL_PREF_TABLE:
+ Network.getInstance(mContext)
+ .clearUserSslPrefTable();
+ break;
+
+ case TOUCH_UP:
+ TouchUpData touchUpData = (TouchUpData) msg.obj;
+ nativeTouchUp(touchUpData.mMoveGeneration,
+ touchUpData.mBuildGeneration,
+ touchUpData.mFrame, touchUpData.mNode,
+ touchUpData.mX, touchUpData.mY,
+ touchUpData.mSize, touchUpData.mIsClick,
+ touchUpData.mRetry);
+ break;
+
+ case ADD_JS_INTERFACE:
+ HashMap map = (HashMap) msg.obj;
+ Object obj = map.get("object");
+ String interfaceName = (String)
+ map.get("interfaceName");
+ mBrowserFrame.addJavascriptInterface(obj,
+ interfaceName);
+ break;
+
+ case REQUEST_EXT_REPRESENTATION:
+ mBrowserFrame.externalRepresentation(
+ (Message) msg.obj);
+ break;
+
+ case REQUEST_DOC_AS_TEXT:
+ mBrowserFrame.documentAsText((Message) msg.obj);
+ break;
+
+ case SET_FINAL_FOCUS:
+ FocusData finalData = (FocusData) msg.obj;
+ nativeSetFinalFocus(finalData.mFrame,
+ finalData.mNode, finalData.mX,
+ finalData.mY, msg.arg1
+ != EventHub.NO_FOCUS_CHANGE_BLOCK);
+ break;
+
+ case UNBLOCK_FOCUS:
+ nativeUnblockFocus();
+ break;
+
+ case SET_KIT_FOCUS:
+ FocusData focusData = (FocusData) msg.obj;
+ nativeSetKitFocus(focusData.mMoveGeneration,
+ focusData.mBuildGeneration,
+ focusData.mFrame, focusData.mNode,
+ focusData.mX, focusData.mY,
+ focusData.mIgnoreNullFocus);
+ break;
+
+ case REQUEST_FOCUS_HREF: {
+ Message hrefMsg = (Message) msg.obj;
+ String res = nativeRetrieveHref(msg.arg1, msg.arg2);
+ Bundle data = hrefMsg.getData();
+ data.putString("url", res);
+ hrefMsg.setData(data);
+ hrefMsg.sendToTarget();
+ break;
+ }
+
+ case REQUEST_IMAGE_HREF: {
+ Message refMsg = (Message) msg.obj;
+ String ref =
+ nativeRetrieveImageRef(msg.arg1, msg.arg2);
+ Bundle data = refMsg.getData();
+ data.putString("url", ref);
+ refMsg.setData(data);
+ refMsg.sendToTarget();
+ break;
+ }
+
+ case UPDATE_CACHE_AND_TEXT_ENTRY:
+ nativeUpdateFrameCache();
+ sendViewInvalidate();
+ sendUpdateTextEntry();
+ break;
+
+ case DOC_HAS_IMAGES:
+ Message imageResult = (Message) msg.obj;
+ imageResult.arg1 =
+ mBrowserFrame.documentHasImages() ? 1 : 0;
+ imageResult.sendToTarget();
+ break;
+
+ case SET_SNAP_ANCHOR:
+ nativeSetSnapAnchor(msg.arg1, msg.arg2);
+ break;
+
+ case DELETE_SELECTION:
+ FocusData delData = (FocusData) msg.obj;
+ nativeDeleteSelection(delData.mFrame,
+ delData.mNode, delData.mX,
+ delData.mY, msg.arg1, msg.arg2);
+ break;
+
+ case SET_SELECTION:
+ FocusData selData = (FocusData) msg.obj;
+ nativeSetSelection(selData.mFrame,
+ selData.mNode, selData.mX,
+ selData.mY, msg.arg1, msg.arg2);
+ break;
+
+ case LISTBOX_CHOICES:
+ SparseBooleanArray choices = (SparseBooleanArray)
+ msg.obj;
+ int choicesSize = msg.arg1;
+ boolean[] choicesArray = new boolean[choicesSize];
+ for (int c = 0; c < choicesSize; c++) {
+ choicesArray[c] = choices.get(c);
+ }
+ nativeSendListBoxChoices(choicesArray,
+ choicesSize);
+ break;
+
+ case SINGLE_LISTBOX_CHOICE:
+ nativeSendListBoxChoice(msg.arg1);
+ break;
+
+ case SET_BACKGROUND_COLOR:
+ nativeSetBackgroundColor(msg.arg1);
+ break;
+
+ case GET_SELECTION:
+ String str = nativeGetSelection((Region) msg.obj);
+ Message.obtain(mWebView.mPrivateHandler
+ , WebView.UPDATE_CLIPBOARD, str)
+ .sendToTarget();
+ break;
+
+ case DUMP_WEBKIT:
+ nativeDump();
+ break;
+
+ case SYNC_SCROLL:
+ mWebkitScrollX = msg.arg1;
+ mWebkitScrollY = msg.arg2;
+ break;
+
+ case REFRESH_PLUGINS:
+ nativeRefreshPlugins(msg.arg1 != 0);
+ break;
+ }
+ }
+ };
+ // Take all queued messages and resend them to the new handler.
+ synchronized (this) {
+ int size = mMessages.size();
+ for (int i = 0; i < size; i++) {
+ mHandler.sendMessage(mMessages.get(i));
+ }
+ mMessages = null;
+ }
+ }
+
+ /**
+ * Send a message internally to the queue or to the handler
+ */
+ private synchronized void sendMessage(Message msg) {
+ if (mBlockMessages) {
+ return;
+ }
+ if (mMessages != null) {
+ mMessages.add(msg);
+ } else {
+ mHandler.sendMessage(msg);
+ }
+ }
+
+ private synchronized void removeMessages(int what) {
+ if (mBlockMessages) {
+ return;
+ }
+ if (what == EventHub.WEBKIT_DRAW) {
+ mDrawIsScheduled = false;
+ }
+ if (mMessages != null) {
+ Log.w(LOGTAG, "Not supported in this case.");
+ } else {
+ mHandler.removeMessages(what);
+ }
+ }
+
+ private synchronized void sendMessageDelayed(Message msg, long delay) {
+ if (mBlockMessages) {
+ return;
+ }
+ mHandler.sendMessageDelayed(msg, delay);
+ }
+
+ /**
+ * Send a message internally to the front of the queue.
+ */
+ private synchronized void sendMessageAtFrontOfQueue(Message msg) {
+ if (mBlockMessages) {
+ return;
+ }
+ if (mMessages != null) {
+ mMessages.add(0, msg);
+ } else {
+ mHandler.sendMessageAtFrontOfQueue(msg);
+ }
+ }
+
+ /**
+ * Remove all the messages.
+ */
+ private synchronized void removeMessages() {
+ // reset mDrawIsScheduled flag as WEBKIT_DRAW may be removed
+ mDrawIsScheduled = false;
+ if (mMessages != null) {
+ mMessages.clear();
+ } else {
+ mHandler.removeCallbacksAndMessages(null);
+ }
+ }
+
+ /**
+ * Block sending messages to the EventHub.
+ */
+ private synchronized void blockMessages() {
+ mBlockMessages = true;
+ }
+ }
+
+ //-------------------------------------------------------------------------
+ // Methods called by host activity (in the same thread)
+ //-------------------------------------------------------------------------
+
+ public void stopLoading() {
+ if (LOGV_ENABLED) Log.v(LOGTAG, "CORE stopLoading");
+ if (mBrowserFrame != null) {
+ mBrowserFrame.stopLoading();
+ }
+ }
+
+ //-------------------------------------------------------------------------
+ // Methods called by WebView
+ // If it refers to local variable, it needs synchronized().
+ // If it needs WebCore, it has to send message.
+ //-------------------------------------------------------------------------
+
+ void sendMessage(Message msg) {
+ mEventHub.sendMessage(msg);
+ }
+
+ void sendMessage(int what) {
+ mEventHub.sendMessage(Message.obtain(null, what));
+ }
+
+ void sendMessage(int what, Object obj) {
+ mEventHub.sendMessage(Message.obtain(null, what, obj));
+ }
+
+ void sendMessage(int what, int arg1) {
+ // just ignore the second argument (make it 0)
+ mEventHub.sendMessage(Message.obtain(null, what, arg1, 0));
+ }
+
+ void sendMessage(int what, int arg1, int arg2) {
+ mEventHub.sendMessage(Message.obtain(null, what, arg1, arg2));
+ }
+
+ void sendMessage(int what, int arg1, Object obj) {
+ // just ignore the second argument (make it 0)
+ mEventHub.sendMessage(Message.obtain(null, what, arg1, 0, obj));
+ }
+
+ void sendMessage(int what, int arg1, int arg2, Object obj) {
+ mEventHub.sendMessage(Message.obtain(null, what, arg1, arg2, obj));
+ }
+
+ void sendMessageDelayed(int what, Object obj, long delay) {
+ mEventHub.sendMessageDelayed(Message.obtain(null, what, obj), delay);
+ }
+
+ void removeMessages(int what) {
+ mEventHub.removeMessages(what);
+ }
+
+ void removeMessages() {
+ mEventHub.removeMessages();
+ }
+
+ /**
+ * Removes pending messages and trigger a DESTROY message to send to
+ * WebCore.
+ * Called from UI thread.
+ */
+ void destroy() {
+ // We don't want anyone to post a message between removing pending
+ // messages and sending the destroy message.
+ synchronized (mEventHub) {
+ mEventHub.removeMessages();
+ mEventHub.sendMessageAtFrontOfQueue(
+ Message.obtain(null, EventHub.DESTROY));
+ mEventHub.blockMessages();
+ mWebView = null;
+ }
+ }
+
+ //-------------------------------------------------------------------------
+ // WebViewCore private methods
+ //-------------------------------------------------------------------------
+
+ private void loadUrl(String url) {
+ if (LOGV_ENABLED) Log.v(LOGTAG, " CORE loadUrl " + url);
+ mBrowserFrame.loadUrl(url);
+ }
+
+ private void keyDown(int code, KeyEvent event) {
+ if (LOGV_ENABLED) {
+ Log.v(LOGTAG, "CORE keyDown at " + System.currentTimeMillis()
+ + ", " + event);
+ }
+ mCallbackProxy.onUnhandledKeyEvent(event);
+ }
+
+ private void keyUp(int code, KeyEvent event) {
+ if (LOGV_ENABLED) {
+ Log.v(LOGTAG, "CORE keyUp at " + System.currentTimeMillis()
+ + ", " + event);
+ }
+ if (!nativeKeyUp(code, event.getUnicodeChar())) {
+ mCallbackProxy.onUnhandledKeyEvent(event);
+ }
+ }
+
+ // These values are used to avoid requesting a layout based on old values
+ private int mCurrentViewWidth = 0;
+ private int mCurrentViewHeight = 0;
+
+ // notify webkit that our virtual view size changed size (after inv-zoom)
+ private void viewSizeChanged(int w, int h, float scale) {
+ if (LOGV_ENABLED) Log.v(LOGTAG, "CORE onSizeChanged");
+ if (mSettings.getUseWideViewPort()
+ && (w < mViewportWidth || mViewportWidth == -1)) {
+ int width = mViewportWidth;
+ if (mViewportWidth == -1) {
+ if (mSettings.getLayoutAlgorithm() ==
+ WebSettings.LayoutAlgorithm.NORMAL) {
+ width = WebView.ZOOM_OUT_WIDTH;
+ } else {
+ /*
+ * if a page's minimum preferred width is wider than the
+ * given "w", use it instead to get better layout result. If
+ * we start a page with MAX_ZOOM_WIDTH, "w" will be always
+ * wider. If we start a page with screen width, due to the
+ * delay between {@link #didFirstLayout} and
+ * {@link #viewSizeChanged},
+ * {@link #nativeGetContentMinPrefWidth} will return a more
+ * accurate value than initial 0 to result a better layout.
+ * In the worse case, the native width will be adjusted when
+ * next zoom or screen orientation change happens.
+ */
+ width = Math.max(w, nativeGetContentMinPrefWidth());
+ }
+ }
+ nativeSetSize(width, Math.round((float) width * h / w), w, scale);
+ } else {
+ nativeSetSize(w, h, w, scale);
+ }
+ // Remember the current width and height
+ boolean needInvalidate = (mCurrentViewWidth == 0);
+ mCurrentViewWidth = w;
+ mCurrentViewHeight = h;
+ if (needInvalidate) {
+ // ensure {@link #webkitDraw} is called as we were blocking in
+ // {@link #contentInvalidate} when mCurrentViewWidth is 0
+ contentInvalidate();
+ }
+ mEventHub.sendMessage(Message.obtain(null,
+ EventHub.UPDATE_CACHE_AND_TEXT_ENTRY));
+ }
+
+ private void sendUpdateTextEntry() {
+ if (mWebView != null) {
+ Message.obtain(mWebView.mPrivateHandler,
+ WebView.UPDATE_TEXT_ENTRY_MSG_ID).sendToTarget();
+ }
+ }
+
+ // Used to avoid posting more than one draw message.
+ private boolean mDrawIsScheduled;
+
+ // Used to end scale+scroll mode, accessed by both threads
+ boolean mEndScaleZoom = false;
+
+ private void webkitDraw() {
+ mDrawIsScheduled = false;
+ nativeDraw(mContentPictureB);
+ int w;
+ int h;
+ synchronized (this) {
+ Picture temp = mContentPictureB;
+ mContentPictureB = mContentPictureA;
+ mContentPictureA = temp;
+ w = mContentPictureA.getWidth();
+ h = mContentPictureA.getHeight();
+ }
+
+ if (mWebView != null) {
+ // Send the native view size that was used during the most recent
+ // layout.
+ Message.obtain(mWebView.mPrivateHandler,
+ WebView.NEW_PICTURE_MSG_ID, w, h,
+ new Point(mCurrentViewWidth, mCurrentViewHeight))
+ .sendToTarget();
+ if (mWebkitScrollX != 0 || mWebkitScrollY != 0) {
+ // as we have the new picture, try to sync the scroll position
+ Message.obtain(mWebView.mPrivateHandler,
+ WebView.SYNC_SCROLL_TO_MSG_ID, mWebkitScrollX,
+ mWebkitScrollY).sendToTarget();
+ mWebkitScrollX = mWebkitScrollY = 0;
+ }
+ // nativeSnapToAnchor() needs to be called after NEW_PICTURE_MSG_ID
+ // is sent, so that scroll will be based on the new content size.
+ nativeSnapToAnchor();
+ }
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // These are called from the UI thread, not our thread
+
+ static final int ZOOM_BITS = Paint.FILTER_BITMAP_FLAG |
+ Paint.DITHER_FLAG |
+ Paint.SUBPIXEL_TEXT_FLAG;
+ static final int SCROLL_BITS = Paint.FILTER_BITMAP_FLAG |
+ Paint.DITHER_FLAG;
+
+ final DrawFilter mZoomFilter =
+ new PaintFlagsDrawFilter(ZOOM_BITS, Paint.LINEAR_TEXT_FLAG);
+ final DrawFilter mScrollFilter =
+ new PaintFlagsDrawFilter(SCROLL_BITS, 0);
+
+ /* package */ void drawContentPicture(Canvas canvas, int color,
+ boolean animatingZoom,
+ boolean animatingScroll) {
+ DrawFilter df = null;
+ if (animatingZoom) {
+ df = mZoomFilter;
+ } else if (animatingScroll) {
+ df = mScrollFilter;
+ }
+ canvas.setDrawFilter(df);
+ synchronized (this) {
+ Picture picture = mContentPictureA;
+ int sc = canvas.save(Canvas.CLIP_SAVE_FLAG);
+ Rect clip = new Rect(0, 0, picture.getWidth(), picture.getHeight());
+ canvas.clipRect(clip, Region.Op.DIFFERENCE);
+ canvas.drawColor(color);
+ canvas.restoreToCount(sc);
+ // experiment commented out
+ // if (TEST_BUCKET) {
+ // nativeDrawContentPicture(canvas);
+ // } else {
+ canvas.drawPicture(picture);
+ // }
+ }
+ canvas.setDrawFilter(null);
+ }
+
+ /* package */ void clearContentPicture() {
+ // experiment commented out
+ // if (TEST_BUCKET) {
+ // nativeClearContentPicture();
+ // }
+ synchronized (this) {
+ mContentPictureA = new Picture();
+ }
+ }
+
+ /*package*/ Picture copyContentPicture() {
+ synchronized (this) {
+ return new Picture(mContentPictureA);
+ }
+ }
+
+ static void pauseUpdate(WebViewCore core) {
+ // remove the pending REDUCE_PRIORITY and RESUME_PRIORITY messages
+ sWebCoreHandler.removeMessages(WebCoreThread.REDUCE_PRIORITY);
+ sWebCoreHandler.removeMessages(WebCoreThread.RESUME_PRIORITY);
+ sWebCoreHandler.sendMessageAtFrontOfQueue(sWebCoreHandler
+ .obtainMessage(WebCoreThread.REDUCE_PRIORITY));
+ // Note: there is one possible failure mode. If pauseUpdate() is called
+ // from UI thread while in webcore thread WEBKIT_DRAW is just pulled out
+ // of the queue and about to be executed. mDrawIsScheduled may be set to
+ // false in webkitDraw(). So update won't be blocked. But at least the
+ // webcore thread priority is still lowered.
+ if (core != null) {
+ synchronized (core) {
+ core.mDrawIsScheduled = true;
+ core.mEventHub.removeMessages(EventHub.WEBKIT_DRAW);
+ }
+ }
+ }
+
+ static void resumeUpdate(WebViewCore core) {
+ // remove the pending REDUCE_PRIORITY and RESUME_PRIORITY messages
+ sWebCoreHandler.removeMessages(WebCoreThread.REDUCE_PRIORITY);
+ sWebCoreHandler.removeMessages(WebCoreThread.RESUME_PRIORITY);
+ sWebCoreHandler.sendMessageAtFrontOfQueue(sWebCoreHandler
+ .obtainMessage(WebCoreThread.RESUME_PRIORITY));
+ if (core != null) {
+ synchronized (core) {
+ core.mDrawIsScheduled = false;
+ core.contentInvalidate();
+ }
+ }
+ }
+
+ static void startCacheTransaction() {
+ sWebCoreHandler.sendMessage(sWebCoreHandler
+ .obtainMessage(WebCoreThread.RESUME_CACHE_TICKER));
+ }
+
+ static void endCacheTransaction() {
+ sWebCoreHandler.sendMessage(sWebCoreHandler
+ .obtainMessage(WebCoreThread.BLOCK_CACHE_TICKER));
+ }
+
+ //////////////////////////////////////////////////////////////////////////
+
+ private void restoreState(int index) {
+ WebBackForwardList list = mCallbackProxy.getBackForwardList();
+ int size = list.getSize();
+ for (int i = 0; i < size; i++) {
+ list.getItemAtIndex(i).inflate(mBrowserFrame.mNativeFrame);
+ }
+ list.restoreIndex(mBrowserFrame.mNativeFrame, index);
+ }
+
+ //-------------------------------------------------------------------------
+ // Implement abstract methods in WebViewCore, native WebKit callback part
+ //-------------------------------------------------------------------------
+
+ // called from JNI or WebView thread
+ /* package */ void contentInvalidate() {
+ // don't update the Picture until we have an initial width and finish
+ // the first layout
+ if (mCurrentViewWidth == 0 || !mBrowserFrame.firstLayoutDone()) {
+ return;
+ }
+ // only fire an event if this is our first request
+ synchronized (this) {
+ if (mDrawIsScheduled) {
+ return;
+ }
+ mDrawIsScheduled = true;
+ mEventHub.sendMessage(Message.obtain(null, EventHub.WEBKIT_DRAW));
+ }
+ }
+
+ // called by JNI
+ private void contentScrollBy(int dx, int dy) {
+ if (!mBrowserFrame.firstLayoutDone()) {
+ // Will this happen? If yes, we need to do something here.
+ return;
+ }
+ if (mWebView != null) {
+ Message.obtain(mWebView.mPrivateHandler,
+ WebView.SCROLL_BY_MSG_ID, dx, dy).sendToTarget();
+ }
+ }
+
+ // called by JNI
+ private void contentScrollTo(int x, int y) {
+ if (!mBrowserFrame.firstLayoutDone()) {
+ /*
+ * WebKit restore state will be called before didFirstLayout(),
+ * remember the position as it has to be applied after restoring
+ * zoom factor which is controlled by screenWidth.
+ */
+ mRestoredX = x;
+ mRestoredY = y;
+ return;
+ }
+ if (mWebView != null) {
+ Message.obtain(mWebView.mPrivateHandler,
+ WebView.SCROLL_TO_MSG_ID, x, y).sendToTarget();
+ }
+ }
+
+ // called by JNI
+ private void contentSpawnScrollTo(int x, int y) {
+ if (!mBrowserFrame.firstLayoutDone()) {
+ /*
+ * WebKit restore state will be called before didFirstLayout(),
+ * remember the position as it has to be applied after restoring
+ * zoom factor which is controlled by screenWidth.
+ */
+ mRestoredX = x;
+ mRestoredY = y;
+ return;
+ }
+ if (mWebView != null) {
+ Message.obtain(mWebView.mPrivateHandler,
+ WebView.SPAWN_SCROLL_TO_MSG_ID, x, y).sendToTarget();
+ }
+ }
+
+ // called by JNI
+ private void sendMarkNodeInvalid(int node) {
+ if (mWebView != null) {
+ Message.obtain(mWebView.mPrivateHandler,
+ WebView.MARK_NODE_INVALID_ID, node, 0).sendToTarget();
+ }
+ }
+
+ // called by JNI
+ private void sendNotifyFocusSet() {
+ if (mWebView != null) {
+ Message.obtain(mWebView.mPrivateHandler,
+ WebView.NOTIFY_FOCUS_SET_MSG_ID).sendToTarget();
+ }
+ }
+
+ // called by JNI
+ private void sendNotifyProgressFinished() {
+ sendUpdateTextEntry();
+ // as CacheManager can behave based on database transaction, we need to
+ // call tick() to trigger endTransaction
+ sWebCoreHandler.removeMessages(WebCoreThread.CACHE_TICKER);
+ sWebCoreHandler.sendMessage(sWebCoreHandler
+ .obtainMessage(WebCoreThread.CACHE_TICKER));
+ }
+
+ // called by JNI
+ private void sendRecomputeFocus() {
+ if (mWebView != null) {
+ Message.obtain(mWebView.mPrivateHandler,
+ WebView.RECOMPUTE_FOCUS_MSG_ID).sendToTarget();
+ }
+ }
+
+ // called by JNI
+ private void sendViewInvalidate() {
+ if (mWebView != null) {
+ mWebView.postInvalidate();
+ }
+ }
+
+ /* package */ WebView getWebView() {
+ return mWebView;
+ }
+
+ private native void setViewportSettingsFromNative();
+
+ // called by JNI
+ private void didFirstLayout(String url) {
+ // Trick to ensure that the Picture has the exact height for the content
+ // by forcing to layout with 0 height after the page is ready, which is
+ // indicated by didFirstLayout. This is essential to get rid of the
+ // white space in the GMail which uses WebView for message view.
+ if (mWebView != null && mWebView.mHeightCanMeasure) {
+ mWebView.mLastHeightSent = 0;
+ // Send a negative scale to indicate that WebCore should reuse the
+ // current scale
+ mEventHub.sendMessage(Message.obtain(null,
+ EventHub.VIEW_SIZE_CHANGED, mWebView.mLastWidthSent,
+ mWebView.mLastHeightSent, -1.0f));
+ }
+
+ mBrowserFrame.didFirstLayout(url);
+
+ // reset the scroll position as it is a new page now
+ mWebkitScrollX = mWebkitScrollY = 0;
+
+ // set the viewport settings from WebKit
+ setViewportSettingsFromNative();
+
+ // infer the values if they are not defined.
+ if (mViewportWidth == 0) {
+ if (mViewportInitialScale == 0) {
+ mViewportInitialScale = 100;
+ }
+ if (mViewportMinimumScale == 0) {
+ mViewportMinimumScale = 100;
+ }
+ }
+ if (mViewportUserScalable == false) {
+ mViewportInitialScale = 100;
+ mViewportMinimumScale = 100;
+ mViewportMaximumScale = 100;
+ }
+ if (mViewportMinimumScale > mViewportInitialScale) {
+ if (mViewportInitialScale == 0) {
+ mViewportInitialScale = mViewportMinimumScale;
+ } else {
+ mViewportMinimumScale = mViewportInitialScale;
+ }
+ }
+ if (mViewportMaximumScale > 0) {
+ if (mViewportMaximumScale < mViewportInitialScale) {
+ mViewportMaximumScale = mViewportInitialScale;
+ } else if (mViewportInitialScale == 0) {
+ mViewportInitialScale = mViewportMaximumScale;
+ }
+ }
+ if (mViewportWidth < 0 && mViewportInitialScale == 100) {
+ mViewportWidth = 0;
+ }
+
+ // now notify webview
+ if (mWebView != null) {
+ HashMap scaleLimit = new HashMap();
+ scaleLimit.put("minScale", mViewportMinimumScale);
+ scaleLimit.put("maxScale", mViewportMaximumScale);
+
+ if (mRestoredScale > 0) {
+ Message.obtain(mWebView.mPrivateHandler,
+ WebView.DID_FIRST_LAYOUT_MSG_ID, mRestoredScale, 0,
+ scaleLimit).sendToTarget();
+ mRestoredScale = 0;
+ } else {
+ Message.obtain(mWebView.mPrivateHandler,
+ WebView.DID_FIRST_LAYOUT_MSG_ID, mViewportInitialScale,
+ mViewportWidth, scaleLimit).sendToTarget();
+ }
+
+ // if no restored offset, move the new page to (0, 0)
+ Message.obtain(mWebView.mPrivateHandler, WebView.SCROLL_TO_MSG_ID,
+ mRestoredX, mRestoredY).sendToTarget();
+ mRestoredX = mRestoredY = 0;
+
+ // force an early draw for quick feedback after the first layout
+ if (mCurrentViewWidth != 0) {
+ synchronized (this) {
+ if (mDrawIsScheduled) {
+ mEventHub.removeMessages(EventHub.WEBKIT_DRAW);
+ }
+ mDrawIsScheduled = true;
+ mEventHub.sendMessageAtFrontOfQueue(Message.obtain(null,
+ EventHub.WEBKIT_DRAW));
+ }
+ }
+ }
+ }
+
+ // called by JNI
+ private void restoreScale(int scale) {
+ if (mBrowserFrame.firstLayoutDone() == false) {
+ mRestoredScale = scale;
+ }
+ }
+
+ // called by JNI
+ private void updateTextfield(int ptr, boolean changeToPassword,
+ String text, int textGeneration) {
+ if (mWebView != null) {
+ Message msg = Message.obtain(mWebView.mPrivateHandler,
+ WebView.UPDATE_TEXTFIELD_TEXT_MSG_ID, ptr,
+ textGeneration, text);
+ msg.getData().putBoolean("password", changeToPassword);
+ msg.sendToTarget();
+ }
+ }
+
+ // these must be in document space (i.e. not scaled/zoomed.
+ private native void nativeSetVisibleRect(int x, int y, int width,
+ int height);
+
+ // called by JNI
+ private void requestListBox(String[] array, boolean[] enabledArray,
+ int[] selectedArray) {
+ if (mWebView != null) {
+ mWebView.requestListBox(array, enabledArray, selectedArray);
+ }
+ }
+
+ // called by JNI
+ private void requestListBox(String[] array, boolean[] enabledArray,
+ int selection) {
+ if (mWebView != null) {
+ mWebView.requestListBox(array, enabledArray, selection);
+ }
+
+ }
+}
diff --git a/core/java/android/webkit/WebViewDatabase.java b/core/java/android/webkit/WebViewDatabase.java
new file mode 100644
index 0000000..b367e27
--- /dev/null
+++ b/core/java/android/webkit/WebViewDatabase.java
@@ -0,0 +1,962 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.webkit;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.Map.Entry;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteStatement;
+import android.util.Log;
+import android.webkit.CookieManager.Cookie;
+import android.webkit.CacheManager.CacheResult;
+
+public class WebViewDatabase {
+ private static final String DATABASE_FILE = "webview.db";
+ private static final String CACHE_DATABASE_FILE = "webviewCache.db";
+
+ // log tag
+ protected static final String LOGTAG = "webviewdatabase";
+
+ private static final int DATABASE_VERSION = 8;
+ // 2 -> 3 Modified Cache table to allow cache of redirects
+ // 3 -> 4 Added Oma-Downloads table
+ // 4 -> 5 Modified Cache table to support persistent contentLength
+ // 5 -> 4 Removed Oma-Downoads table
+ // 5 -> 6 Add INDEX for cache table
+ // 6 -> 7 Change cache localPath from int to String
+ // 7 -> 8 Move cache to its own db
+ private static final int CACHE_DATABASE_VERSION = 1;
+
+ private static WebViewDatabase mInstance = null;
+
+ private static SQLiteDatabase mDatabase = null;
+ private static SQLiteDatabase mCacheDatabase = null;
+
+ // synchronize locks
+ private final Object mCookieLock = new Object();
+ private final Object mPasswordLock = new Object();
+ private final Object mFormLock = new Object();
+ private final Object mHttpAuthLock = new Object();
+
+ private static final String mTableNames[] = {
+ "cookies", "password", "formurl", "formdata", "httpauth"
+ };
+
+ // Table ids (they are index to mTableNames)
+ private static final int TABLE_COOKIES_ID = 0;
+
+ private static final int TABLE_PASSWORD_ID = 1;
+
+ private static final int TABLE_FORMURL_ID = 2;
+
+ private static final int TABLE_FORMDATA_ID = 3;
+
+ private static final int TABLE_HTTPAUTH_ID = 4;
+
+ // column id strings for "_id" which can be used by any table
+ private static final String ID_COL = "_id";
+
+ private static final String[] ID_PROJECTION = new String[] {
+ "_id"
+ };
+
+ // column id strings for "cookies" table
+ private static final String COOKIES_NAME_COL = "name";
+
+ private static final String COOKIES_VALUE_COL = "value";
+
+ private static final String COOKIES_DOMAIN_COL = "domain";
+
+ private static final String COOKIES_PATH_COL = "path";
+
+ private static final String COOKIES_EXPIRES_COL = "expires";
+
+ private static final String COOKIES_SECURE_COL = "secure";
+
+ // column id strings for "cache" table
+ private static final String CACHE_URL_COL = "url";
+
+ private static final String CACHE_FILE_PATH_COL = "filepath";
+
+ private static final String CACHE_LAST_MODIFY_COL = "lastmodify";
+
+ private static final String CACHE_ETAG_COL = "etag";
+
+ private static final String CACHE_EXPIRES_COL = "expires";
+
+ private static final String CACHE_MIMETYPE_COL = "mimetype";
+
+ private static final String CACHE_ENCODING_COL = "encoding";
+
+ private static final String CACHE_HTTP_STATUS_COL = "httpstatus";
+
+ private static final String CACHE_LOCATION_COL = "location";
+
+ private static final String CACHE_CONTENTLENGTH_COL = "contentlength";
+
+ // column id strings for "password" table
+ private static final String PASSWORD_HOST_COL = "host";
+
+ private static final String PASSWORD_USERNAME_COL = "username";
+
+ private static final String PASSWORD_PASSWORD_COL = "password";
+
+ // column id strings for "formurl" table
+ private static final String FORMURL_URL_COL = "url";
+
+ // column id strings for "formdata" table
+ private static final String FORMDATA_URLID_COL = "urlid";
+
+ private static final String FORMDATA_NAME_COL = "name";
+
+ private static final String FORMDATA_VALUE_COL = "value";
+
+ // column id strings for "httpauth" table
+ private static final String HTTPAUTH_HOST_COL = "host";
+
+ private static final String HTTPAUTH_REALM_COL = "realm";
+
+ private static final String HTTPAUTH_USERNAME_COL = "username";
+
+ private static final String HTTPAUTH_PASSWORD_COL = "password";
+
+ // use InsertHelper to improve insert performance by 40%
+ private static DatabaseUtils.InsertHelper mCacheInserter;
+ private static int mCacheUrlColIndex;
+ private static int mCacheFilePathColIndex;
+ private static int mCacheLastModifyColIndex;
+ private static int mCacheETagColIndex;
+ private static int mCacheExpiresColIndex;
+ private static int mCacheMimeTypeColIndex;
+ private static int mCacheEncodingColIndex;
+ private static int mCacheHttpStatusColIndex;
+ private static int mCacheLocationColIndex;
+ private static int mCacheContentLengthColIndex;
+
+ private static int mCacheTransactionRefcount;
+
+ private WebViewDatabase() {
+ // Singleton only, use getInstance()
+ }
+
+ public static synchronized WebViewDatabase getInstance(Context context) {
+ if (mInstance == null) {
+ mInstance = new WebViewDatabase();
+ mDatabase = context.openOrCreateDatabase(DATABASE_FILE, 0, null);
+
+ // mDatabase should not be null,
+ // the only case is RequestAPI test has problem to create db
+ if (mDatabase != null && mDatabase.getVersion() != DATABASE_VERSION) {
+ mDatabase.beginTransaction();
+ try {
+ upgradeDatabase();
+ bootstrapDatabase();
+ mDatabase.setTransactionSuccessful();
+ } finally {
+ mDatabase.endTransaction();
+ }
+ }
+
+ if (mDatabase != null) {
+ // use per table Mutex lock, turn off database lock, this
+ // improves performance as database's ReentrantLock is expansive
+ mDatabase.setLockingEnabled(false);
+ }
+
+ mCacheDatabase = context.openOrCreateDatabase(CACHE_DATABASE_FILE,
+ 0, null);
+
+ // mCacheDatabase should not be null,
+ // the only case is RequestAPI test has problem to create db
+ if (mCacheDatabase != null
+ && mCacheDatabase.getVersion() != CACHE_DATABASE_VERSION) {
+ mCacheDatabase.beginTransaction();
+ try {
+ upgradeCacheDatabase();
+ bootstrapCacheDatabase();
+ mCacheDatabase.setTransactionSuccessful();
+ } finally {
+ mCacheDatabase.endTransaction();
+ }
+ }
+
+ if (mCacheDatabase != null) {
+ // use InsertHelper for faster insertion
+ mCacheInserter = new DatabaseUtils.InsertHelper(mCacheDatabase,
+ "cache");
+ mCacheUrlColIndex = mCacheInserter
+ .getColumnIndex(CACHE_URL_COL);
+ mCacheFilePathColIndex = mCacheInserter
+ .getColumnIndex(CACHE_FILE_PATH_COL);
+ mCacheLastModifyColIndex = mCacheInserter
+ .getColumnIndex(CACHE_LAST_MODIFY_COL);
+ mCacheETagColIndex = mCacheInserter
+ .getColumnIndex(CACHE_ETAG_COL);
+ mCacheExpiresColIndex = mCacheInserter
+ .getColumnIndex(CACHE_EXPIRES_COL);
+ mCacheMimeTypeColIndex = mCacheInserter
+ .getColumnIndex(CACHE_MIMETYPE_COL);
+ mCacheEncodingColIndex = mCacheInserter
+ .getColumnIndex(CACHE_ENCODING_COL);
+ mCacheHttpStatusColIndex = mCacheInserter
+ .getColumnIndex(CACHE_HTTP_STATUS_COL);
+ mCacheLocationColIndex = mCacheInserter
+ .getColumnIndex(CACHE_LOCATION_COL);
+ mCacheContentLengthColIndex = mCacheInserter
+ .getColumnIndex(CACHE_CONTENTLENGTH_COL);
+ }
+ }
+
+ return mInstance;
+ }
+
+ private static void upgradeDatabase() {
+ int oldVersion = mDatabase.getVersion();
+ if (oldVersion != 0) {
+ Log.i(LOGTAG, "Upgrading database from version "
+ + oldVersion + " to "
+ + DATABASE_VERSION + ", which will destroy all old data");
+ }
+ mDatabase.execSQL("DROP TABLE IF EXISTS "
+ + mTableNames[TABLE_COOKIES_ID]);
+ mDatabase.execSQL("DROP TABLE IF EXISTS cache");
+ mDatabase.execSQL("DROP TABLE IF EXISTS "
+ + mTableNames[TABLE_PASSWORD_ID]);
+ mDatabase.execSQL("DROP TABLE IF EXISTS "
+ + mTableNames[TABLE_FORMURL_ID]);
+ mDatabase.execSQL("DROP TABLE IF EXISTS "
+ + mTableNames[TABLE_FORMDATA_ID]);
+ mDatabase.execSQL("DROP TABLE IF EXISTS "
+ + mTableNames[TABLE_HTTPAUTH_ID]);
+ mDatabase.setVersion(DATABASE_VERSION);
+ }
+
+ private static void bootstrapDatabase() {
+ if (mDatabase != null) {
+ // cookies
+ mDatabase.execSQL("CREATE TABLE " + mTableNames[TABLE_COOKIES_ID]
+ + " (" + ID_COL + " INTEGER PRIMARY KEY, "
+ + COOKIES_NAME_COL + " TEXT, " + COOKIES_VALUE_COL
+ + " TEXT, " + COOKIES_DOMAIN_COL + " TEXT, "
+ + COOKIES_PATH_COL + " TEXT, " + COOKIES_EXPIRES_COL
+ + " INTEGER, " + COOKIES_SECURE_COL + " INTEGER" + ");");
+ mDatabase.execSQL("CREATE INDEX cookiesIndex ON "
+ + mTableNames[TABLE_COOKIES_ID] + " (path)");
+
+ // password
+ mDatabase.execSQL("CREATE TABLE " + mTableNames[TABLE_PASSWORD_ID]
+ + " (" + ID_COL + " INTEGER PRIMARY KEY, "
+ + PASSWORD_HOST_COL + " TEXT, " + PASSWORD_USERNAME_COL
+ + " TEXT, " + PASSWORD_PASSWORD_COL + " TEXT," + " UNIQUE ("
+ + PASSWORD_HOST_COL + ", " + PASSWORD_USERNAME_COL
+ + ") ON CONFLICT REPLACE);");
+
+ // formurl
+ mDatabase.execSQL("CREATE TABLE " + mTableNames[TABLE_FORMURL_ID]
+ + " (" + ID_COL + " INTEGER PRIMARY KEY, " + FORMURL_URL_COL
+ + " TEXT" + ");");
+
+ // formdata
+ mDatabase.execSQL("CREATE TABLE " + mTableNames[TABLE_FORMDATA_ID]
+ + " (" + ID_COL + " INTEGER PRIMARY KEY, "
+ + FORMDATA_URLID_COL + " INTEGER, " + FORMDATA_NAME_COL
+ + " TEXT, " + FORMDATA_VALUE_COL + " TEXT," + " UNIQUE ("
+ + FORMDATA_URLID_COL + ", " + FORMDATA_NAME_COL + ", "
+ + FORMDATA_VALUE_COL + ") ON CONFLICT IGNORE);");
+
+ // httpauth
+ mDatabase.execSQL("CREATE TABLE " + mTableNames[TABLE_HTTPAUTH_ID]
+ + " (" + ID_COL + " INTEGER PRIMARY KEY, "
+ + HTTPAUTH_HOST_COL + " TEXT, " + HTTPAUTH_REALM_COL
+ + " TEXT, " + HTTPAUTH_USERNAME_COL + " TEXT, "
+ + HTTPAUTH_PASSWORD_COL + " TEXT," + " UNIQUE ("
+ + HTTPAUTH_HOST_COL + ", " + HTTPAUTH_REALM_COL + ", "
+ + HTTPAUTH_USERNAME_COL + ") ON CONFLICT REPLACE);");
+ }
+ }
+
+ private static void upgradeCacheDatabase() {
+ int oldVersion = mCacheDatabase.getVersion();
+ if (oldVersion != 0) {
+ Log.i(LOGTAG, "Upgrading cache database from version "
+ + oldVersion + " to "
+ + DATABASE_VERSION + ", which will destroy all old data");
+ }
+ mCacheDatabase.execSQL("DROP TABLE IF EXISTS cache");
+ mCacheDatabase.setVersion(CACHE_DATABASE_VERSION);
+ }
+
+ private static void bootstrapCacheDatabase() {
+ if (mCacheDatabase != null) {
+ mCacheDatabase.execSQL("CREATE TABLE cache"
+ + " (" + ID_COL + " INTEGER PRIMARY KEY, " + CACHE_URL_COL
+ + " TEXT, " + CACHE_FILE_PATH_COL + " TEXT, "
+ + CACHE_LAST_MODIFY_COL + " TEXT, " + CACHE_ETAG_COL
+ + " TEXT, " + CACHE_EXPIRES_COL + " INTEGER, "
+ + CACHE_MIMETYPE_COL + " TEXT, " + CACHE_ENCODING_COL
+ + " TEXT," + CACHE_HTTP_STATUS_COL + " INTEGER, "
+ + CACHE_LOCATION_COL + " TEXT, " + CACHE_CONTENTLENGTH_COL
+ + " INTEGER, " + " UNIQUE (" + CACHE_URL_COL
+ + ") ON CONFLICT REPLACE);");
+ mCacheDatabase.execSQL("CREATE INDEX cacheUrlIndex ON cache ("
+ + CACHE_URL_COL + ")");
+ }
+ }
+
+ private boolean hasEntries(int tableId) {
+ if (mDatabase == null) {
+ return false;
+ }
+
+ Cursor cursor = mDatabase.query(mTableNames[tableId], ID_PROJECTION,
+ null, null, null, null, null);
+ boolean ret = cursor.moveToFirst() == true;
+ cursor.close();
+ return ret;
+ }
+
+ //
+ // cookies functions
+ //
+
+ /**
+ * Get cookies in the format of CookieManager.Cookie inside an ArrayList for
+ * a given domain
+ *
+ * @return ArrayList<Cookie> If nothing is found, return an empty list.
+ */
+ ArrayList<Cookie> getCookiesForDomain(String domain) {
+ ArrayList<Cookie> list = new ArrayList<Cookie>();
+ if (domain == null || mDatabase == null) {
+ return list;
+ }
+
+ synchronized (mCookieLock) {
+ final String[] columns = new String[] {
+ ID_COL, COOKIES_DOMAIN_COL, COOKIES_PATH_COL,
+ COOKIES_NAME_COL, COOKIES_VALUE_COL, COOKIES_EXPIRES_COL,
+ COOKIES_SECURE_COL
+ };
+ final String selection = "(" + COOKIES_DOMAIN_COL
+ + " GLOB '*' || ?)";
+ Cursor cursor = mDatabase.query(mTableNames[TABLE_COOKIES_ID],
+ columns, selection, new String[] { domain }, null, null,
+ null);
+ if (cursor.moveToFirst()) {
+ int domainCol = cursor.getColumnIndex(COOKIES_DOMAIN_COL);
+ int pathCol = cursor.getColumnIndex(COOKIES_PATH_COL);
+ int nameCol = cursor.getColumnIndex(COOKIES_NAME_COL);
+ int valueCol = cursor.getColumnIndex(COOKIES_VALUE_COL);
+ int expiresCol = cursor.getColumnIndex(COOKIES_EXPIRES_COL);
+ int secureCol = cursor.getColumnIndex(COOKIES_SECURE_COL);
+ do {
+ Cookie cookie = new Cookie();
+ cookie.domain = cursor.getString(domainCol);
+ cookie.path = cursor.getString(pathCol);
+ cookie.name = cursor.getString(nameCol);
+ cookie.value = cursor.getString(valueCol);
+ if (cursor.isNull(expiresCol)) {
+ cookie.expires = -1;
+ } else {
+ cookie.expires = cursor.getLong(expiresCol);
+ }
+ cookie.secure = cursor.getShort(secureCol) != 0;
+ cookie.mode = Cookie.MODE_NORMAL;
+ list.add(cookie);
+ } while (cursor.moveToNext());
+ }
+ cursor.close();
+ return list;
+ }
+ }
+
+ /**
+ * Delete cookies which matches (domain, path, name).
+ *
+ * @param domain If it is null, nothing happens.
+ * @param path If it is null, all the cookies match (domain) will be
+ * deleted.
+ * @param name If it is null, all the cookies match (domain, path) will be
+ * deleted.
+ */
+ void deleteCookies(String domain, String path, String name) {
+ if (domain == null || mDatabase == null) {
+ return;
+ }
+
+ synchronized (mCookieLock) {
+ final String where = "(" + COOKIES_DOMAIN_COL + " == ?) AND ("
+ + COOKIES_PATH_COL + " == ?) AND (" + COOKIES_NAME_COL
+ + " == ?)";
+ mDatabase.delete(mTableNames[TABLE_COOKIES_ID], where,
+ new String[] { domain, path, name });
+ }
+ }
+
+ /**
+ * Add a cookie to the database
+ *
+ * @param cookie
+ */
+ void addCookie(Cookie cookie) {
+ if (cookie.domain == null || cookie.path == null || cookie.name == null
+ || mDatabase == null) {
+ return;
+ }
+
+ synchronized (mCookieLock) {
+ ContentValues cookieVal = new ContentValues();
+ cookieVal.put(COOKIES_DOMAIN_COL, cookie.domain);
+ cookieVal.put(COOKIES_PATH_COL, cookie.path);
+ cookieVal.put(COOKIES_NAME_COL, cookie.name);
+ cookieVal.put(COOKIES_VALUE_COL, cookie.value);
+ if (cookie.expires != -1) {
+ cookieVal.put(COOKIES_EXPIRES_COL, cookie.expires);
+ }
+ cookieVal.put(COOKIES_SECURE_COL, cookie.secure);
+ mDatabase.insert(mTableNames[TABLE_COOKIES_ID], null, cookieVal);
+ }
+ }
+
+ /**
+ * Whether there is any cookies in the database
+ *
+ * @return TRUE if there is cookie.
+ */
+ boolean hasCookies() {
+ synchronized (mCookieLock) {
+ return hasEntries(TABLE_COOKIES_ID);
+ }
+ }
+
+ /**
+ * Clear cookie database
+ */
+ void clearCookies() {
+ if (mDatabase == null) {
+ return;
+ }
+
+ synchronized (mCookieLock) {
+ mDatabase.delete(mTableNames[TABLE_COOKIES_ID], null, null);
+ }
+ }
+
+ /**
+ * Clear session cookies, which means cookie doesn't have EXPIRES.
+ */
+ void clearSessionCookies() {
+ if (mDatabase == null) {
+ return;
+ }
+
+ final String sessionExpired = COOKIES_EXPIRES_COL + " ISNULL";
+ synchronized (mCookieLock) {
+ mDatabase.delete(mTableNames[TABLE_COOKIES_ID], sessionExpired,
+ null);
+ }
+ }
+
+ /**
+ * Clear expired cookies
+ *
+ * @param now Time for now
+ */
+ void clearExpiredCookies(long now) {
+ if (mDatabase == null) {
+ return;
+ }
+
+ final String expires = COOKIES_EXPIRES_COL + " <= ?";
+ synchronized (mCookieLock) {
+ mDatabase.delete(mTableNames[TABLE_COOKIES_ID], expires,
+ new String[] { Long.toString(now) });
+ }
+ }
+
+ //
+ // cache functions, can only be called from WebCoreThread
+ //
+
+ boolean startCacheTransaction() {
+ if (++mCacheTransactionRefcount == 1) {
+ mCacheDatabase.beginTransaction();
+ return true;
+ }
+ return false;
+ }
+
+ boolean endCacheTransaction() {
+ if (--mCacheTransactionRefcount == 0) {
+ try {
+ mCacheDatabase.setTransactionSuccessful();
+ } finally {
+ mCacheDatabase.endTransaction();
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Get a cache item.
+ *
+ * @param url The url
+ * @return CacheResult The CacheManager.CacheResult
+ */
+ @SuppressWarnings("deprecation")
+ CacheResult getCache(String url) {
+ if (url == null || mCacheDatabase == null) {
+ return null;
+ }
+
+ CacheResult ret = null;
+ final String s = "SELECT filepath, lastmodify, etag, expires, mimetype, encoding, httpstatus, location, contentlength FROM cache WHERE url = ";
+ StringBuilder sb = new StringBuilder(256);
+ sb.append(s);
+ DatabaseUtils.appendEscapedSQLString(sb, url);
+ Cursor cursor = mCacheDatabase.rawQuery(sb.toString(), null);
+
+ if (cursor.moveToFirst()) {
+ ret = new CacheResult();
+ ret.localPath = cursor.getString(0);
+ ret.lastModified = cursor.getString(1);
+ ret.etag = cursor.getString(2);
+ ret.expires = cursor.getLong(3);
+ ret.mimeType = cursor.getString(4);
+ ret.encoding = cursor.getString(5);
+ ret.httpStatusCode = cursor.getInt(6);
+ ret.location = cursor.getString(7);
+ ret.contentLength = cursor.getLong(8);
+ }
+ cursor.close();
+ return ret;
+ }
+
+ /**
+ * Remove a cache item.
+ *
+ * @param url The url
+ */
+ @SuppressWarnings("deprecation")
+ void removeCache(String url) {
+ if (url == null || mCacheDatabase == null) {
+ return;
+ }
+
+ StringBuilder sb = new StringBuilder(256);
+ sb.append("DELETE FROM cache WHERE url = ");
+ DatabaseUtils.appendEscapedSQLString(sb, url);
+ mCacheDatabase.execSQL(sb.toString());
+ }
+
+ /**
+ * Add or update a cache. CACHE_URL_COL is unique in the table.
+ *
+ * @param url The url
+ * @param c The CacheManager.CacheResult
+ */
+ void addCache(String url, CacheResult c) {
+ if (url == null || mCacheDatabase == null) {
+ return;
+ }
+
+ mCacheInserter.prepareForInsert();
+ mCacheInserter.bind(mCacheUrlColIndex, url);
+ mCacheInserter.bind(mCacheFilePathColIndex, c.localPath);
+ mCacheInserter.bind(mCacheLastModifyColIndex, c.lastModified);
+ mCacheInserter.bind(mCacheETagColIndex, c.etag);
+ mCacheInserter.bind(mCacheExpiresColIndex, c.expires);
+ mCacheInserter.bind(mCacheMimeTypeColIndex, c.mimeType);
+ mCacheInserter.bind(mCacheEncodingColIndex, c.encoding);
+ mCacheInserter.bind(mCacheHttpStatusColIndex, c.httpStatusCode);
+ mCacheInserter.bind(mCacheLocationColIndex, c.location);
+ mCacheInserter.bind(mCacheContentLengthColIndex, c.contentLength);
+ mCacheInserter.execute();
+ }
+
+ /**
+ * Clear cache database
+ */
+ void clearCache() {
+ if (mCacheDatabase == null) {
+ return;
+ }
+
+ mCacheDatabase.delete("cache", null, null);
+ }
+
+ boolean hasCache() {
+ if (mCacheDatabase == null) {
+ return false;
+ }
+
+ Cursor cursor = mCacheDatabase.query("cache", ID_PROJECTION,
+ null, null, null, null, null);
+ boolean ret = cursor.moveToFirst() == true;
+ cursor.close();
+ return ret;
+ }
+
+ long getCacheTotalSize() {
+ long size = 0;
+ Cursor cursor = mCacheDatabase.rawQuery(
+ "SELECT SUM(contentlength) as sum FROM cache", null);
+ if (cursor.moveToFirst()) {
+ size = cursor.getLong(0);
+ }
+ cursor.close();
+ return size;
+ }
+
+ ArrayList<String> trimCache(long amount) {
+ ArrayList<String> pathList = new ArrayList<String>(100);
+ Cursor cursor = mCacheDatabase.rawQuery(
+ "SELECT contentlength, filepath FROM cache ORDER BY expires ASC",
+ null);
+ if (cursor.moveToFirst()) {
+ int batchSize = 100;
+ StringBuilder pathStr = new StringBuilder(20 + 16 * batchSize);
+ pathStr.append("DELETE FROM cache WHERE filepath = ?");
+ for (int i = 1; i < batchSize; i++) {
+ pathStr.append(" OR filepath = ?");
+ }
+ SQLiteStatement statement = mCacheDatabase.compileStatement(pathStr
+ .toString());
+ // as bindString() uses 1-based index, initialize index to 1
+ int index = 1;
+ do {
+ long length = cursor.getLong(0);
+ if (length == 0) {
+ continue;
+ }
+ amount -= length;
+ String filePath = cursor.getString(1);
+ statement.bindString(index, filePath);
+ pathList.add(filePath);
+ if (index++ == batchSize) {
+ statement.execute();
+ index = 1;
+ }
+ } while (cursor.moveToNext() && amount > 0);
+ if (index > 1) {
+ // there may be old bindings from the previous statement if
+ // index is less than batchSize, which is Ok.
+ statement.execute();
+ }
+ statement.close();
+ }
+ cursor.close();
+ return pathList;
+ }
+
+ //
+ // password functions
+ //
+
+ /**
+ * Set password. Tuple (PASSWORD_HOST_COL, PASSWORD_USERNAME_COL) is unique.
+ *
+ * @param host The host for the password
+ * @param username The username for the password. If it is null, it means
+ * password can't be saved.
+ * @param password The password
+ */
+ void setUsernamePassword(String host, String username, String password) {
+ if (host == null || mDatabase == null) {
+ return;
+ }
+
+ synchronized (mPasswordLock) {
+ final ContentValues c = new ContentValues();
+ c.put(PASSWORD_HOST_COL, host);
+ c.put(PASSWORD_USERNAME_COL, username);
+ c.put(PASSWORD_PASSWORD_COL, password);
+ mDatabase.insert(mTableNames[TABLE_PASSWORD_ID], PASSWORD_HOST_COL,
+ c);
+ }
+ }
+
+ /**
+ * Retrieve the username and password for a given host
+ *
+ * @param host The host which passwords applies to
+ * @return String[] if found, String[0] is username, which can be null and
+ * String[1] is password. Return null if it can't find anything.
+ */
+ String[] getUsernamePassword(String host) {
+ if (host == null || mDatabase == null) {
+ return null;
+ }
+
+ final String[] columns = new String[] {
+ PASSWORD_USERNAME_COL, PASSWORD_PASSWORD_COL
+ };
+ final String selection = "(" + PASSWORD_HOST_COL + " == ?)";
+ synchronized (mPasswordLock) {
+ String[] ret = null;
+ Cursor cursor = mDatabase.query(mTableNames[TABLE_PASSWORD_ID],
+ columns, selection, new String[] { host }, null, null,
+ null);
+ if (cursor.moveToFirst()) {
+ ret = new String[2];
+ ret[0] = cursor.getString(
+ cursor.getColumnIndex(PASSWORD_USERNAME_COL));
+ ret[1] = cursor.getString(
+ cursor.getColumnIndex(PASSWORD_PASSWORD_COL));
+ }
+ cursor.close();
+ return ret;
+ }
+ }
+
+ /**
+ * Find out if there are any passwords saved.
+ *
+ * @return TRUE if there is passwords saved
+ */
+ public boolean hasUsernamePassword() {
+ synchronized (mPasswordLock) {
+ return hasEntries(TABLE_PASSWORD_ID);
+ }
+ }
+
+ /**
+ * Clear password database
+ */
+ public void clearUsernamePassword() {
+ if (mDatabase == null) {
+ return;
+ }
+
+ synchronized (mPasswordLock) {
+ mDatabase.delete(mTableNames[TABLE_PASSWORD_ID], null, null);
+ }
+ }
+
+ //
+ // http authentication password functions
+ //
+
+ /**
+ * Set HTTP authentication password. Tuple (HTTPAUTH_HOST_COL,
+ * HTTPAUTH_REALM_COL, HTTPAUTH_USERNAME_COL) is unique.
+ *
+ * @param host The host for the password
+ * @param realm The realm for the password
+ * @param username The username for the password. If it is null, it means
+ * password can't be saved.
+ * @param password The password
+ */
+ void setHttpAuthUsernamePassword(String host, String realm, String username,
+ String password) {
+ if (host == null || realm == null || mDatabase == null) {
+ return;
+ }
+
+ synchronized (mHttpAuthLock) {
+ final ContentValues c = new ContentValues();
+ c.put(HTTPAUTH_HOST_COL, host);
+ c.put(HTTPAUTH_REALM_COL, realm);
+ c.put(HTTPAUTH_USERNAME_COL, username);
+ c.put(HTTPAUTH_PASSWORD_COL, password);
+ mDatabase.insert(mTableNames[TABLE_HTTPAUTH_ID], HTTPAUTH_HOST_COL,
+ c);
+ }
+ }
+
+ /**
+ * Retrieve the HTTP authentication username and password for a given
+ * host+realm pair
+ *
+ * @param host The host the password applies to
+ * @param realm The realm the password applies to
+ * @return String[] if found, String[0] is username, which can be null and
+ * String[1] is password. Return null if it can't find anything.
+ */
+ String[] getHttpAuthUsernamePassword(String host, String realm) {
+ if (host == null || realm == null || mDatabase == null){
+ return null;
+ }
+
+ final String[] columns = new String[] {
+ HTTPAUTH_USERNAME_COL, HTTPAUTH_PASSWORD_COL
+ };
+ final String selection = "(" + HTTPAUTH_HOST_COL + " == ?) AND ("
+ + HTTPAUTH_REALM_COL + " == ?)";
+ synchronized (mHttpAuthLock) {
+ String[] ret = null;
+ Cursor cursor = mDatabase.query(mTableNames[TABLE_HTTPAUTH_ID],
+ columns, selection, new String[] { host, realm }, null,
+ null, null);
+ if (cursor.moveToFirst()) {
+ ret = new String[2];
+ ret[0] = cursor.getString(
+ cursor.getColumnIndex(HTTPAUTH_USERNAME_COL));
+ ret[1] = cursor.getString(
+ cursor.getColumnIndex(HTTPAUTH_PASSWORD_COL));
+ }
+ cursor.close();
+ return ret;
+ }
+ }
+
+ /**
+ * Find out if there are any HTTP authentication passwords saved.
+ *
+ * @return TRUE if there are passwords saved
+ */
+ public boolean hasHttpAuthUsernamePassword() {
+ synchronized (mHttpAuthLock) {
+ return hasEntries(TABLE_HTTPAUTH_ID);
+ }
+ }
+
+ /**
+ * Clear HTTP authentication password database
+ */
+ public void clearHttpAuthUsernamePassword() {
+ if (mDatabase == null) {
+ return;
+ }
+
+ synchronized (mHttpAuthLock) {
+ mDatabase.delete(mTableNames[TABLE_HTTPAUTH_ID], null, null);
+ }
+ }
+
+ //
+ // form data functions
+ //
+
+ /**
+ * Set form data for a site. Tuple (FORMDATA_URLID_COL, FORMDATA_NAME_COL,
+ * FORMDATA_VALUE_COL) is unique
+ *
+ * @param url The url of the site
+ * @param formdata The form data in HashMap
+ */
+ void setFormData(String url, HashMap<String, String> formdata) {
+ if (url == null || formdata == null || mDatabase == null) {
+ return;
+ }
+
+ final String selection = "(" + FORMURL_URL_COL + " == ?)";
+ synchronized (mFormLock) {
+ long urlid = -1;
+ Cursor cursor = mDatabase.query(mTableNames[TABLE_FORMURL_ID],
+ ID_PROJECTION, selection, new String[] { url }, null, null,
+ null);
+ if (cursor.moveToFirst()) {
+ urlid = cursor.getLong(cursor.getColumnIndex(ID_COL));
+ } else {
+ ContentValues c = new ContentValues();
+ c.put(FORMURL_URL_COL, url);
+ urlid = mDatabase.insert(
+ mTableNames[TABLE_FORMURL_ID], null, c);
+ }
+ cursor.close();
+ if (urlid >= 0) {
+ Set<Entry<String, String>> set = formdata.entrySet();
+ Iterator<Entry<String, String>> iter = set.iterator();
+ ContentValues map = new ContentValues();
+ map.put(FORMDATA_URLID_COL, urlid);
+ while (iter.hasNext()) {
+ Entry<String, String> entry = iter.next();
+ map.put(FORMDATA_NAME_COL, entry.getKey());
+ map.put(FORMDATA_VALUE_COL, entry.getValue());
+ mDatabase.insert(mTableNames[TABLE_FORMDATA_ID], null, map);
+ }
+ }
+ }
+ }
+
+ /**
+ * Get all the values for a form entry with "name" in a given site
+ *
+ * @param url The url of the site
+ * @param name The name of the form entry
+ * @return A list of values. Return empty list if nothing is found.
+ */
+ ArrayList<String> getFormData(String url, String name) {
+ ArrayList<String> values = new ArrayList<String>();
+ if (url == null || name == null || mDatabase == null) {
+ return values;
+ }
+
+ final String urlSelection = "(" + FORMURL_URL_COL + " == ?)";
+ final String dataSelection = "(" + FORMDATA_URLID_COL + " == ?) AND ("
+ + FORMDATA_NAME_COL + " == ?)";
+ synchronized (mFormLock) {
+ Cursor cursor = mDatabase.query(mTableNames[TABLE_FORMURL_ID],
+ ID_PROJECTION, urlSelection, new String[] { url }, null,
+ null, null);
+ if (cursor.moveToFirst()) {
+ long urlid = cursor.getLong(cursor.getColumnIndex(ID_COL));
+ Cursor dataCursor = mDatabase.query(
+ mTableNames[TABLE_FORMDATA_ID],
+ new String[] { ID_COL, FORMDATA_VALUE_COL },
+ dataSelection,
+ new String[] { Long.toString(urlid), name }, null,
+ null, null);
+ if (dataCursor.moveToFirst()) {
+ int valueCol =
+ dataCursor.getColumnIndex(FORMDATA_VALUE_COL);
+ do {
+ values.add(dataCursor.getString(valueCol));
+ } while (dataCursor.moveToNext());
+ }
+ dataCursor.close();
+ }
+ cursor.close();
+ return values;
+ }
+ }
+
+ /**
+ * Find out if there is form data saved.
+ *
+ * @return TRUE if there is form data in the database
+ */
+ public boolean hasFormData() {
+ synchronized (mFormLock) {
+ return hasEntries(TABLE_FORMURL_ID);
+ }
+ }
+
+ /**
+ * Clear form database
+ */
+ public void clearFormData() {
+ if (mDatabase == null) {
+ return;
+ }
+
+ synchronized (mFormLock) {
+ mDatabase.delete(mTableNames[TABLE_FORMURL_ID], null, null);
+ mDatabase.delete(mTableNames[TABLE_FORMDATA_ID], null, null);
+ }
+ }
+}
diff --git a/core/java/android/webkit/gears/AndroidGpsLocationProvider.java b/core/java/android/webkit/gears/AndroidGpsLocationProvider.java
new file mode 100644
index 0000000..3646042
--- /dev/null
+++ b/core/java/android/webkit/gears/AndroidGpsLocationProvider.java
@@ -0,0 +1,156 @@
+// Copyright 2008, The Android Open Source Project
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice,
+// this list of conditions and the following disclaimer.
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+// this list of conditions and the following disclaimer in the documentation
+// and/or other materials provided with the distribution.
+// 3. Neither the name of Google Inc. nor the names of its contributors may be
+// used to endorse or promote products derived from this software without
+// specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+package android.webkit.gears;
+
+import android.content.Context;
+import android.location.Location;
+import android.location.LocationListener;
+import android.location.LocationManager;
+import android.location.LocationProvider;
+import android.os.Bundle;
+import android.util.Log;
+import android.webkit.WebView;
+
+/**
+ * GPS provider implementation for Android.
+ */
+public final class AndroidGpsLocationProvider implements LocationListener {
+ /**
+ * Logging tag
+ */
+ private static final String TAG = "Gears-J-GpsProvider";
+ /**
+ * Our location manager instance.
+ */
+ private LocationManager locationManager;
+ /**
+ * The native object ID.
+ */
+ private long nativeObject;
+
+ public AndroidGpsLocationProvider(WebView webview, long object) {
+ nativeObject = object;
+ locationManager = (LocationManager) webview.getContext().getSystemService(
+ Context.LOCATION_SERVICE);
+ if (locationManager == null) {
+ Log.e(TAG,
+ "AndroidGpsLocationProvider: could not get location manager.");
+ throw new NullPointerException(
+ "AndroidGpsLocationProvider: locationManager is null.");
+ }
+ // Register for location updates.
+ try {
+ locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0,
+ this);
+ } catch (IllegalArgumentException ex) {
+ Log.e(TAG,
+ "AndroidLocationGpsProvider: could not register for updates: " + ex);
+ throw ex;
+ } catch (SecurityException ex) {
+ Log.e(TAG,
+ "AndroidGpsLocationProvider: not allowed to register for update: "
+ + ex);
+ throw ex;
+ }
+ }
+
+ /**
+ * Called when the provider is no longer needed.
+ */
+ public void shutdown() {
+ locationManager.removeUpdates(this);
+ Log.i(TAG, "GPS provider closed.");
+ }
+
+ /**
+ * Called when the location has changed.
+ * @param location The new location, as a Location object.
+ */
+ public void onLocationChanged(Location location) {
+ Log.i(TAG, "Location changed: " + location);
+ nativeLocationChanged(location, nativeObject);
+ }
+
+ /**
+ * Called when the provider status changes.
+ *
+ * @param provider the name of the location provider associated with this
+ * update.
+ * @param status {@link LocationProvider#OUT_OF_SERVICE} if the
+ * provider is out of service, and this is not expected to change in the
+ * near future; {@link LocationProvider#TEMPORARILY_UNAVAILABLE} if
+ * the provider is temporarily unavailable but is expected to be available
+ * shortly; and {@link LocationProvider#AVAILABLE} if the
+ * provider is currently available.
+ * @param extras an optional Bundle which will contain provider specific
+ * status variables (such as number of satellites).
+ */
+ public void onStatusChanged(String provider, int status, Bundle extras) {
+ Log.i(TAG, "Provider " + provider + " status changed to " + status);
+ if (status == LocationProvider.OUT_OF_SERVICE ||
+ status == LocationProvider.TEMPORARILY_UNAVAILABLE) {
+ nativeProviderError(false, nativeObject);
+ }
+ }
+
+ /**
+ * Called when the provider is enabled.
+ *
+ * @param provider the name of the location provider that is now enabled.
+ */
+ public void onProviderEnabled(String provider) {
+ Log.i(TAG, "Provider " + provider + " enabled.");
+ // No need to notify the native side. It's enough to start sending
+ // valid position fixes again.
+ }
+
+ /**
+ * Called when the provider is disabled.
+ *
+ * @param provider the name of the location provider that is now disabled.
+ */
+ public void onProviderDisabled(String provider) {
+ Log.i(TAG, "Provider " + provider + " disabled.");
+ nativeProviderError(true, nativeObject);
+ }
+
+ /**
+ * The native method called when a new location is available.
+ * @param location is the new Location instance to pass to the native side.
+ * @param nativeObject is a pointer to the corresponding
+ * AndroidGpsLocationProvider C++ instance.
+ */
+ private native void nativeLocationChanged(Location location, long object);
+
+ /**
+ * The native method called when there is a GPS provder error.
+ * @param isDisabled is true when the error signifies the fact that the GPS
+ * HW is disabled. For other errors, this param is always false.
+ * @param nativeObject is a pointer to the corresponding
+ * AndroidGpsLocationProvider C++ instance.
+ */
+ private native void nativeProviderError(boolean isDisabled, long object);
+}
diff --git a/core/java/android/webkit/gears/AndroidRadioDataProvider.java b/core/java/android/webkit/gears/AndroidRadioDataProvider.java
new file mode 100644
index 0000000..c920d45
--- /dev/null
+++ b/core/java/android/webkit/gears/AndroidRadioDataProvider.java
@@ -0,0 +1,244 @@
+// Copyright 2008, The Android Open Source Project
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice,
+// this list of conditions and the following disclaimer.
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+// this list of conditions and the following disclaimer in the documentation
+// and/or other materials provided with the distribution.
+// 3. Neither the name of Google Inc. nor the names of its contributors may be
+// used to endorse or promote products derived from this software without
+// specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+package android.webkit.gears;
+
+import android.content.Context;
+import android.telephony.CellLocation;
+import android.telephony.ServiceState;
+import android.telephony.gsm.GsmCellLocation;
+import android.telephony.PhoneStateListener;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+import android.webkit.WebView;
+
+/**
+ * Radio data provider implementation for Android.
+ */
+public final class AndroidRadioDataProvider extends PhoneStateListener {
+
+ /** Logging tag */
+ private static final String TAG = "Gears-J-RadioProvider";
+
+ /** Network types */
+ private static final int RADIO_TYPE_UNKNOWN = 0;
+ private static final int RADIO_TYPE_GSM = 1;
+ private static final int RADIO_TYPE_WCDMA = 2;
+
+ /** Simple container for radio data */
+ public static final class RadioData {
+ public int cellId = -1;
+ public int locationAreaCode = -1;
+ public int signalStrength = -1;
+ public int mobileCountryCode = -1;
+ public int mobileNetworkCode = -1;
+ public int homeMobileCountryCode = -1;
+ public int homeMobileNetworkCode = -1;
+ public int radioType = RADIO_TYPE_UNKNOWN;
+ public String carrierName;
+
+ /**
+ * Constructs radioData object from the given telephony data.
+ * @param telephonyManager contains the TelephonyManager instance.
+ * @param cellLocation contains information about the current GSM cell.
+ * @param signalStrength is the strength of the network signal.
+ * @param serviceState contains information about the network service.
+ * @return a new RadioData object populated with the currently
+ * available network information or null if there isn't
+ * enough information.
+ */
+ public static RadioData getInstance(TelephonyManager telephonyManager,
+ CellLocation cellLocation, int signalStrength,
+ ServiceState serviceState) {
+
+ if (!(cellLocation instanceof GsmCellLocation)) {
+ // This also covers the case when cellLocation is null.
+ // When that happens, we do not bother creating a
+ // RadioData instance.
+ return null;
+ }
+
+ RadioData radioData = new RadioData();
+ GsmCellLocation gsmCellLocation = (GsmCellLocation) cellLocation;
+
+ // Extract the cell id, LAC, and signal strength.
+ radioData.cellId = gsmCellLocation.getCid();
+ radioData.locationAreaCode = gsmCellLocation.getLac();
+ radioData.signalStrength = signalStrength;
+
+ // Extract the home MCC and home MNC.
+ String operator = telephonyManager.getSimOperator();
+ radioData.setMobileCodes(operator, true);
+
+ if (serviceState != null) {
+ // Extract the carrier name.
+ radioData.carrierName = serviceState.getOperatorAlphaLong();
+
+ // Extract the MCC and MNC.
+ operator = serviceState.getOperatorNumeric();
+ radioData.setMobileCodes(operator, false);
+ }
+
+ // Finally get the radio type.
+ int type = telephonyManager.getNetworkType();
+ if (type == TelephonyManager.NETWORK_TYPE_UMTS) {
+ radioData.radioType = RADIO_TYPE_WCDMA;
+ } else if (type == TelephonyManager.NETWORK_TYPE_GPRS
+ || type == TelephonyManager.NETWORK_TYPE_EDGE) {
+ radioData.radioType = RADIO_TYPE_GSM;
+ }
+
+ // Print out what we got.
+ Log.i(TAG, "Got the following data:");
+ Log.i(TAG, "CellId: " + radioData.cellId);
+ Log.i(TAG, "LAC: " + radioData.locationAreaCode);
+ Log.i(TAG, "MNC: " + radioData.mobileNetworkCode);
+ Log.i(TAG, "MCC: " + radioData.mobileCountryCode);
+ Log.i(TAG, "home MNC: " + radioData.homeMobileNetworkCode);
+ Log.i(TAG, "home MCC: " + radioData.homeMobileCountryCode);
+ Log.i(TAG, "Signal strength: " + radioData.signalStrength);
+ Log.i(TAG, "Carrier: " + radioData.carrierName);
+ Log.i(TAG, "Network type: " + radioData.radioType);
+
+ return radioData;
+ }
+
+ private RadioData() {}
+
+ /**
+ * Parses a string containing a mobile country code and a mobile
+ * network code and sets the corresponding member variables.
+ * @param codes is the string to parse.
+ * @param homeValues flags whether the codes are for the home operator.
+ */
+ private void setMobileCodes(String codes, boolean homeValues) {
+ if (codes != null) {
+ try {
+ // The operator numeric format is 3 digit country code plus 2 or
+ // 3 digit network code.
+ int mcc = Integer.parseInt(codes.substring(0, 3));
+ int mnc = Integer.parseInt(codes.substring(3));
+ if (homeValues) {
+ homeMobileCountryCode = mcc;
+ homeMobileNetworkCode = mnc;
+ } else {
+ mobileCountryCode = mcc;
+ mobileNetworkCode = mnc;
+ }
+ } catch (IndexOutOfBoundsException ex) {
+ Log.e(
+ TAG,
+ "AndroidRadioDataProvider: Invalid operator numeric data: " + ex);
+ } catch (NumberFormatException ex) {
+ Log.e(
+ TAG,
+ "AndroidRadioDataProvider: Operator numeric format error: " + ex);
+ }
+ }
+ }
+ };
+
+ /** The native object ID */
+ private long nativeObject;
+
+ /** The last known cellLocation */
+ private CellLocation cellLocation = null;
+
+ /** The last known signal strength */
+ private int signalStrength = -1;
+
+ /** The last known serviceState */
+ private ServiceState serviceState = null;
+
+ /**
+ * Our TelephonyManager instance.
+ */
+ private TelephonyManager telephonyManager;
+
+ /**
+ * Public constructor. Uses the webview to get the Context object.
+ */
+ public AndroidRadioDataProvider(WebView webview, long object) {
+ super();
+ nativeObject = object;
+ telephonyManager = (TelephonyManager) webview.getContext().getSystemService(
+ Context.TELEPHONY_SERVICE);
+ if (telephonyManager == null) {
+ Log.e(TAG,
+ "AndroidRadioDataProvider: could not get tepephony manager.");
+ throw new NullPointerException(
+ "AndroidRadioDataProvider: telephonyManager is null.");
+ }
+
+ // Register for cell id, signal strength and service state changed
+ // notifications.
+ telephonyManager.listen(this, PhoneStateListener.LISTEN_CELL_LOCATION
+ | PhoneStateListener.LISTEN_SIGNAL_STRENGTH
+ | PhoneStateListener.LISTEN_SERVICE_STATE);
+ }
+
+ /**
+ * Should be called when the provider is no longer needed.
+ */
+ public void shutdown() {
+ telephonyManager.listen(this, PhoneStateListener.LISTEN_NONE);
+ Log.i(TAG, "AndroidRadioDataProvider shutdown.");
+ }
+
+ @Override
+ public void onServiceStateChanged(ServiceState state) {
+ serviceState = state;
+ notifyListeners();
+ }
+
+ @Override
+ public void onSignalStrengthChanged(int asu) {
+ signalStrength = asu;
+ notifyListeners();
+ }
+
+ @Override
+ public void onCellLocationChanged(CellLocation location) {
+ cellLocation = location;
+ notifyListeners();
+ }
+
+ private void notifyListeners() {
+ RadioData radioData = RadioData.getInstance(telephonyManager, cellLocation,
+ signalStrength, serviceState);
+ if (radioData != null) {
+ onUpdateAvailable(radioData, nativeObject);
+ }
+ }
+
+ /**
+ * The native method called when new radio data is available.
+ * @param radioData is the RadioData instance to pass to the native side.
+ * @param nativeObject is a pointer to the corresponding
+ * AndroidRadioDataProvider C++ instance.
+ */
+ private static native void onUpdateAvailable(
+ RadioData radioData, long nativeObject);
+}
diff --git a/core/java/android/webkit/gears/DesktopAndroid.java b/core/java/android/webkit/gears/DesktopAndroid.java
new file mode 100644
index 0000000..00a9a47
--- /dev/null
+++ b/core/java/android/webkit/gears/DesktopAndroid.java
@@ -0,0 +1,113 @@
+// Copyright 2008 The Android Open Source Project
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice,
+// this list of conditions and the following disclaimer.
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+// this list of conditions and the following disclaimer in the documentation
+// and/or other materials provided with the distribution.
+// 3. Neither the name of Google Inc. nor the names of its contributors may be
+// used to endorse or promote products derived from this software without
+// specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+package android.webkit.gears;
+
+import android.content.Context;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.util.Log;
+import android.webkit.WebView;
+
+/**
+ * Utility class to create a shortcut on Android
+ */
+public class DesktopAndroid {
+
+ private static final String TAG = "Gears-J-Desktop";
+ private static final String BROWSER = "com.android.browser";
+ private static final String BROWSER_ACTIVITY = BROWSER + ".BrowserActivity";
+ private static final String EXTRA_SHORTCUT_DUPLICATE = "duplicate";
+ private static final String ACTION_INSTALL_SHORTCUT =
+ "com.android.launcher.action.INSTALL_SHORTCUT";
+
+ // Android now enforces a 64x64 limit for the icon
+ private static int MAX_WIDTH = 64;
+ private static int MAX_HEIGHT = 64;
+
+ /**
+ * Small utility function returning a Bitmap object.
+ *
+ * @param path the icon path
+ */
+ private static Bitmap getBitmap(String path) {
+ return BitmapFactory.decodeFile(path);
+ }
+
+ /**
+ * Create a shortcut for a webpage.
+ *
+ * <p>To set a shortcut on Android, we use the ACTION_INSTALL_SHORTCUT
+ * from the default Home application. We only have to create an Intent
+ * containing extra parameters specifying the shortcut.
+ * <p>Note: the shortcut mechanism is not system wide and depends on the
+ * Home application; if phone carriers decide to rewrite a Home application
+ * that does not accept this Intent, no shortcut will be added.
+ *
+ * @param webview the webview we are called from
+ * @param title the shortcut's title
+ * @param url the shortcut's url
+ * @param imagePath the local path of the shortcut's icon
+ */
+ public static void setShortcut(WebView webview, String title,
+ String url, String imagePath) {
+ Context context = webview.getContext();
+
+ ComponentName browser = new ComponentName(BROWSER, BROWSER_ACTIVITY);
+
+ Intent viewWebPage = new Intent(Intent.ACTION_VIEW);
+ viewWebPage.setComponent(browser);
+ viewWebPage.setData(Uri.parse(url));
+
+ Intent intent = new Intent(ACTION_INSTALL_SHORTCUT);
+ intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, viewWebPage);
+ intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, title);
+
+ // We disallow the creation of duplicate shortcuts (i.e. same
+ // url, same title, but different screen position).
+ intent.putExtra(EXTRA_SHORTCUT_DUPLICATE, false);
+
+ Bitmap bmp = getBitmap(imagePath);
+ if (bmp != null) {
+ if ((bmp.getWidth() > MAX_WIDTH) ||
+ (bmp.getHeight() > MAX_HEIGHT)) {
+ Bitmap scaledBitmap = Bitmap.createScaledBitmap(bmp,
+ MAX_WIDTH, MAX_HEIGHT, true);
+ intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, scaledBitmap);
+ } else {
+ intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, bmp);
+ }
+ } else {
+ // This should not happen as we just downloaded the icon
+ Log.e(TAG, "icon file <" + imagePath + "> not found");
+ }
+
+ context.sendBroadcast(intent);
+ }
+
+}
diff --git a/core/java/android/webkit/gears/GearsPluginSettings.java b/core/java/android/webkit/gears/GearsPluginSettings.java
new file mode 100644
index 0000000..d36d3fb
--- /dev/null
+++ b/core/java/android/webkit/gears/GearsPluginSettings.java
@@ -0,0 +1,95 @@
+// Copyright 2008 The Android Open Source Project
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice,
+// this list of conditions and the following disclaimer.
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+// this list of conditions and the following disclaimer in the documentation
+// and/or other materials provided with the distribution.
+// 3. Neither the name of Google Inc. nor the names of its contributors may be
+// used to endorse or promote products derived from this software without
+// specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+package android.webkit.gears;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+import android.util.Log;
+import android.webkit.Plugin;
+import android.webkit.Plugin.PreferencesClickHandler;
+
+/**
+ * Simple bridge class intercepting the click in the
+ * browser plugin list and calling the Gears settings
+ * dialog.
+ */
+public class GearsPluginSettings {
+
+ private static final String TAG = "Gears-J-GearsPluginSettings";
+ private Context context;
+
+ public GearsPluginSettings(Plugin plugin) {
+ plugin.setClickHandler(new ClickHandler());
+ }
+
+ /**
+ * We do not need to call the dialog synchronously here (doing so
+ * actually cause a lot of problems as the main message loop is also
+ * blocked), which is why we simply call it via a thread.
+ */
+ private class ClickHandler implements PreferencesClickHandler {
+ public void handleClickEvent(Context aContext) {
+ context = aContext;
+ Thread startService = new Thread(new StartService());
+ startService.run();
+ }
+ }
+
+ private static native void runSettingsDialog(Context c);
+
+ /**
+ * StartService is the runnable we use to open the dialog.
+ * We bind the service to serviceConnection; upon
+ * onServiceConnected the dialog will be called from the
+ * native side using the runSettingsDialog method.
+ */
+ private class StartService implements Runnable {
+ public void run() {
+ HtmlDialogAndroid.bindToService(context, serviceConnection);
+ }
+ }
+
+ /**
+ * ServiceConnection instance.
+ * onServiceConnected is called upon connection with the service;
+ * we can then safely open the dialog.
+ */
+ private ServiceConnection serviceConnection = new ServiceConnection() {
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ IGearsDialogService gearsDialogService =
+ IGearsDialogService.Stub.asInterface(service);
+ HtmlDialogAndroid.setGearsDialogService(gearsDialogService);
+ runSettingsDialog(context);
+ context.unbindService(serviceConnection);
+ HtmlDialogAndroid.setGearsDialogService(null);
+ }
+ public void onServiceDisconnected(ComponentName className) {
+ HtmlDialogAndroid.setGearsDialogService(null);
+ }
+ };
+}
diff --git a/core/java/android/webkit/gears/HtmlDialogAndroid.java b/core/java/android/webkit/gears/HtmlDialogAndroid.java
new file mode 100644
index 0000000..6209ab9
--- /dev/null
+++ b/core/java/android/webkit/gears/HtmlDialogAndroid.java
@@ -0,0 +1,174 @@
+// Copyright 2008 The Android Open Source Project
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice,
+// this list of conditions and the following disclaimer.
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+// this list of conditions and the following disclaimer in the documentation
+// and/or other materials provided with the distribution.
+// 3. Neither the name of Google Inc. nor the names of its contributors may be
+// used to endorse or promote products derived from this software without
+// specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+package android.webkit.gears;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+import android.webkit.CacheManager;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+
+/**
+ * Utility class to call a modal HTML dialog on Android
+ */
+public class HtmlDialogAndroid {
+
+ private static final String TAG = "Gears-J-HtmlDialog";
+ private static final String DIALOG_PACKAGE = "com.android.browser";
+ private static final String DIALOG_SERVICE = DIALOG_PACKAGE
+ + ".GearsDialogService";
+ private static final String DIALOG_INTERFACE = DIALOG_PACKAGE
+ + ".IGearsDialogService";
+
+ private static IGearsDialogService gearsDialogService;
+
+ public static void setGearsDialogService(IGearsDialogService service) {
+ gearsDialogService = service;
+ }
+
+ /**
+ * Bind to the GearsDialogService.
+ */
+ public static boolean bindToService(Context context,
+ ServiceConnection serviceConnection) {
+ Intent dialogIntent = new Intent();
+ dialogIntent.setClassName(DIALOG_PACKAGE, DIALOG_SERVICE);
+ dialogIntent.setAction(DIALOG_INTERFACE);
+ return context.bindService(dialogIntent, serviceConnection,
+ Context.BIND_AUTO_CREATE);
+ }
+
+ /**
+ * Bind to the GearsDialogService synchronously.
+ * The service is started using our own defaultServiceConnection
+ * handler, and we wait until the handler notifies us.
+ */
+ public void synchronousBindToService(Context context) {
+ try {
+ if (bindToService(context, defaultServiceConnection)) {
+ if (gearsDialogService == null) {
+ synchronized(defaultServiceConnection) {
+ defaultServiceConnection.wait(3000); // timeout after 3s
+ }
+ }
+ }
+ } catch (InterruptedException e) {
+ Log.e(TAG, "exception: " + e);
+ }
+ }
+
+ /**
+ * Read the HTML content from the disk
+ */
+ public String readHTML(String filePath) {
+ FileInputStream inputStream = null;
+ String content = "";
+ try {
+ inputStream = new FileInputStream(filePath);
+ StringBuffer out = new StringBuffer();
+ byte[] buffer = new byte[4096];
+ for (int n; (n = inputStream.read(buffer)) != -1;) {
+ out.append(new String(buffer, 0, n));
+ }
+ content = out.toString();
+ } catch (IOException e) {
+ Log.e(TAG, "exception: " + e);
+ } finally {
+ if (inputStream != null) {
+ try {
+ inputStream.close();
+ } catch (IOException e) {
+ Log.e(TAG, "exception: " + e);
+ }
+ }
+ }
+ return content;
+ }
+
+ /**
+ * Open an HTML dialog synchronously and waits for its completion.
+ * The dialog is accessed through the GearsDialogService provided by
+ * the Android Browser.
+ * We can be called either directly, and then gearsDialogService will
+ * not be set and we will bind to the service synchronously, and unbind
+ * after calling the service, or called indirectly via GearsPluginSettings.
+ * In the latter case, GearsPluginSettings does the binding/unbinding.
+ */
+ public String showDialog(Context context, String htmlFilePath,
+ String arguments) {
+
+ CacheManager.endCacheTransaction();
+
+ String ret = null;
+ boolean synchronousCall = false;
+ if (gearsDialogService == null) {
+ synchronousCall = true;
+ synchronousBindToService(context);
+ }
+
+ try {
+ if (gearsDialogService != null) {
+ String htmlContent = readHTML(htmlFilePath);
+ if (htmlContent.length() > 0) {
+ ret = gearsDialogService.showDialog(htmlContent, arguments,
+ !synchronousCall);
+ }
+ } else {
+ Log.e(TAG, "Could not connect to the GearsDialogService!");
+ }
+ if (synchronousCall) {
+ context.unbindService(defaultServiceConnection);
+ gearsDialogService = null;
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "remote exception: " + e);
+ gearsDialogService = null;
+ }
+
+ CacheManager.startCacheTransaction();
+
+ return ret;
+ }
+
+ private ServiceConnection defaultServiceConnection =
+ new ServiceConnection() {
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ synchronized (defaultServiceConnection) {
+ gearsDialogService = IGearsDialogService.Stub.asInterface(service);
+ defaultServiceConnection.notify();
+ }
+ }
+ public void onServiceDisconnected(ComponentName className) {
+ gearsDialogService = null;
+ }
+ };
+}
diff --git a/core/java/android/webkit/gears/HttpRequestAndroid.java b/core/java/android/webkit/gears/HttpRequestAndroid.java
new file mode 100644
index 0000000..8668c54
--- /dev/null
+++ b/core/java/android/webkit/gears/HttpRequestAndroid.java
@@ -0,0 +1,730 @@
+// Copyright 2008, The Android Open Source Project
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice,
+// this list of conditions and the following disclaimer.
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+// this list of conditions and the following disclaimer in the documentation
+// and/or other materials provided with the distribution.
+// 3. Neither the name of Google Inc. nor the names of its contributors may be
+// used to endorse or promote products derived from this software without
+// specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+package android.webkit.gears;
+
+import android.net.http.Headers;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Log;
+import android.webkit.CacheManager;
+import android.webkit.CacheManager.CacheResult;
+import android.webkit.CookieManager;
+
+import org.apache.http.conn.ssl.StrictHostnameVerifier;
+import org.apache.http.impl.cookie.DateUtils;
+import org.apache.http.util.CharArrayBuffer;
+
+import java.io.*;
+import java.net.*;
+import java.util.*;
+import javax.net.ssl.*;
+
+/**
+ * Performs the underlying HTTP/HTTPS GET and POST requests.
+ * <p> These are performed synchronously (blocking). The caller should
+ * ensure that it is in a background thread if asynchronous behavior
+ * is required. All data is pushed, so there is no need for JNI native
+ * callbacks.
+ * <p> This uses the java.net.HttpURLConnection class to perform most
+ * of the underlying network activity. The Android brower's cache,
+ * android.webkit.CacheManager, is also used when caching is enabled,
+ * and updated with new data. The android.webkit.CookieManager is also
+ * queried and updated as necessary.
+ * <p> The public interface is designed to be called by native code
+ * through JNI, and to simplify coding none of the public methods will
+ * surface a checked exception. Unchecked exceptions may still be
+ * raised but only if the system is in an ill state, such as out of
+ * memory.
+ * <p> TODO: This isn't plumbed into LocalServer yet. Mutually
+ * dependent on LocalServer - will attach the two together once both
+ * are submitted.
+ */
+public final class HttpRequestAndroid {
+ /** Debug logging tag. */
+ private static final String LOG_TAG = "Gears-J";
+ /** HTTP response header line endings are CR-LF style. */
+ private static final String HTTP_LINE_ENDING = "\r\n";
+ /** Safe MIME type to use whenever it isn't specified. */
+ private static final String DEFAULT_MIME_TYPE = "text/plain";
+ /** Case-sensitive header keys */
+ public static final String KEY_CONTENT_LENGTH = "Content-Length";
+ public static final String KEY_EXPIRES = "Expires";
+ public static final String KEY_LAST_MODIFIED = "Last-Modified";
+ public static final String KEY_ETAG = "ETag";
+ public static final String KEY_LOCATION = "Location";
+ public static final String KEY_CONTENT_TYPE = "Content-Type";
+ /** Number of bytes to send and receive on the HTTP connection in
+ * one go. */
+ private static final int BUFFER_SIZE = 4096;
+ /** The first element of the String[] value in a headers map is the
+ * unmodified (case-sensitive) key. */
+ public static final int HEADERS_MAP_INDEX_KEY = 0;
+ /** The second element of the String[] value in a headers map is the
+ * associated value. */
+ public static final int HEADERS_MAP_INDEX_VALUE = 1;
+
+ /** Enable/disable all logging in this class. */
+ private static boolean logEnabled = false;
+ /** The underlying HTTP or HTTPS network connection. */
+ private HttpURLConnection connection;
+ /** HTTP body stream, setup after connection. */
+ private InputStream inputStream;
+ /** The complete response line e.g "HTTP/1.0 200 OK" */
+ private String responseLine;
+ /** Request headers, as a lowercase key -> [ unmodified key, value ] map. */
+ private Map<String, String[]> requestHeaders =
+ new HashMap<String, String[]>();
+ /** Response headers, as a lowercase key -> [ unmodified key, value ] map. */
+ private Map<String, String[]> responseHeaders;
+ /** True if the child thread is in performing blocking IO. */
+ private boolean inBlockingOperation = false;
+ /** True when the thread acknowledges the abort. */
+ private boolean abortReceived = false;
+ /** The URL used for createCacheResult() */
+ private String cacheResultUrl;
+ /** CacheResult being saved into, if inserting a new cache entry. */
+ private CacheResult cacheResult;
+ /** Initialized by initChildThread(). Used to target abort(). */
+ private Thread childThread;
+
+ /**
+ * Convenience debug function. Calls Android logging mechanism.
+ * @param str String to log to the Android console.
+ */
+ private static void log(String str) {
+ if (logEnabled) {
+ Log.i(LOG_TAG, str);
+ }
+ }
+
+ /**
+ * Turn on/off logging in this class.
+ * @param on Logging enable state.
+ */
+ public static void enableLogging(boolean on) {
+ logEnabled = on;
+ }
+
+ /**
+ * Initialize childThread using the TLS value of
+ * Thread.currentThread(). Called on start up of the native child
+ * thread.
+ */
+ public synchronized void initChildThread() {
+ childThread = Thread.currentThread();
+ }
+
+ /**
+ * Analagous to the native-side HttpRequest::open() function. This
+ * initializes an underlying java.net.HttpURLConnection, but does
+ * not go to the wire. On success, this enables a call to send() to
+ * initiate the transaction.
+ *
+ * @param method The HTTP method, e.g GET or POST.
+ * @param url The URL to open.
+ * @return True on success with a complete HTTP response.
+ * False on failure.
+ */
+ public synchronized boolean open(String method, String url) {
+ if (logEnabled)
+ log("open " + method + " " + url);
+ // Reset the response between calls to open().
+ inputStream = null;
+ responseLine = null;
+ responseHeaders = null;
+ if (!method.equals("GET") && !method.equals("POST")) {
+ log("Method " + method + " not supported");
+ return false;
+ }
+ // Setup the connection. This doesn't go to the wire yet - it
+ // doesn't block.
+ try {
+ connection = (HttpURLConnection) new URL(url).openConnection();
+ connection.setRequestMethod(method);
+ // Manually follow redirects.
+ connection.setInstanceFollowRedirects(false);
+ // Manually cache.
+ connection.setUseCaches(false);
+ // Enable data output in POST method requests.
+ connection.setDoOutput(method.equals("POST"));
+ // Enable data input in non-HEAD method requests.
+ // TODO: HEAD requests not tested.
+ connection.setDoInput(!method.equals("HEAD"));
+ if (connection instanceof javax.net.ssl.HttpsURLConnection) {
+ // Verify the certificate matches the origin.
+ ((HttpsURLConnection) connection).setHostnameVerifier(
+ new StrictHostnameVerifier());
+ }
+ return true;
+ } catch (IOException e) {
+ log("Got IOException in open: " + e.toString());
+ return false;
+ }
+ }
+
+ /**
+ * Interrupt a blocking IO operation. This will cause the child
+ * thread to expediently return from an operation if it was stuck at
+ * the time. Note that this inherently races, and unfortunately
+ * requires the caller to loop.
+ */
+ public synchronized void interrupt() {
+ if (childThread == null) {
+ log("interrupt() called but no child thread");
+ return;
+ }
+ if (inBlockingOperation) {
+ log("Interrupting blocking operation");
+ childThread.interrupt();
+ } else {
+ log("Nothing to interrupt");
+ }
+ }
+
+ /**
+ * Set a header to send with the HTTP request. Will not take effect
+ * on a transaction already in progress. The key is associated
+ * case-insensitive, but stored case-sensitive.
+ * @param name The name of the header, e.g "Set-Cookie".
+ * @param value The value for this header, e.g "text/html".
+ */
+ public synchronized void setRequestHeader(String name, String value) {
+ String[] mapValue = { name, value };
+ requestHeaders.put(name.toLowerCase(), mapValue);
+ }
+
+ /**
+ * Returns the value associated with the given request header.
+ * @param name The name of the request header, non-null, case-insensitive.
+ * @return The value associated with the request header, or null if
+ * not set, or error.
+ */
+ public synchronized String getRequestHeader(String name) {
+ String[] value = requestHeaders.get(name.toLowerCase());
+ if (value != null) {
+ return value[HEADERS_MAP_INDEX_VALUE];
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Returns the value associated with the given response header.
+ * @param name The name of the response header, non-null, case-insensitive.
+ * @return The value associated with the response header, or null if
+ * not set or error.
+ */
+ public synchronized String getResponseHeader(String name) {
+ if (responseHeaders != null) {
+ String[] value = responseHeaders.get(name.toLowerCase());
+ if (value != null) {
+ return value[HEADERS_MAP_INDEX_VALUE];
+ } else {
+ return null;
+ }
+ } else {
+ log("getResponseHeader() called but response not received");
+ return null;
+ }
+ }
+
+ /**
+ * Set a response header and associated value. The key is associated
+ * case-insensitively, but stored case-sensitively.
+ * @param name Case sensitive request header key.
+ * @param value The associated value.
+ */
+ private void setResponseHeader(String name, String value) {
+ if (logEnabled)
+ log("Set response header " + name + ": " + value);
+ String mapValue[] = { name, value };
+ responseHeaders.put(name.toLowerCase(), mapValue);
+ }
+
+ /**
+ * Apply the contents of the Map requestHeaders to the connection
+ * object. Calls to setRequestHeader() after this will not affect
+ * the connection.
+ */
+ private synchronized void applyRequestHeadersToConnection() {
+ Iterator<String[]> it = requestHeaders.values().iterator();
+ while (it.hasNext()) {
+ // Set the key case-sensitive.
+ String[] entry = it.next();
+ connection.setRequestProperty(
+ entry[HEADERS_MAP_INDEX_KEY],
+ entry[HEADERS_MAP_INDEX_VALUE]);
+ }
+ }
+
+ /**
+ * Return all response headers, separated by CR-LF line endings, and
+ * ending with a trailing blank line. This mimics the format of the
+ * raw response header up to but not including the body.
+ * @return A string containing the entire response header.
+ */
+ public synchronized String getAllResponseHeaders() {
+ if (responseHeaders == null) {
+ log("getAllResponseHeaders() called but response not received");
+ return null;
+ }
+ String result = new String();
+ Iterator<String[]> it = responseHeaders.values().iterator();
+ while (it.hasNext()) {
+ String[] entry = it.next();
+ // Output the "key: value" lines.
+ result += entry[HEADERS_MAP_INDEX_KEY] + ": "
+ + entry[HEADERS_MAP_INDEX_VALUE] + HTTP_LINE_ENDING;
+ }
+ result += HTTP_LINE_ENDING;
+ return result;
+ }
+
+ /**
+ * Get the complete response line of the HTTP request. Only valid on
+ * completion of the transaction.
+ * @return The complete HTTP response line, e.g "HTTP/1.0 200 OK".
+ */
+ public synchronized String getResponseLine() {
+ return responseLine;
+ }
+
+ /**
+ * Get the cookie for the given URL.
+ * @param url The fully qualified URL.
+ * @return A string containing the cookie for the URL if it exists,
+ * or null if not.
+ */
+ public static String getCookieForUrl(String url) {
+ // Get the cookie for this URL, set as a header
+ return CookieManager.getInstance().getCookie(url);
+ }
+
+ /**
+ * Set the cookie for the given URL.
+ * @param url The fully qualified URL.
+ * @param cookie The new cookie value.
+ * @return A string containing the cookie for the URL if it exists,
+ * or null if not.
+ */
+ public static void setCookieForUrl(String url, String cookie) {
+ // Get the cookie for this URL, set as a header
+ CookieManager.getInstance().setCookie(url, cookie);
+ }
+
+ /**
+ * Perform a request using LocalServer if possible. Initializes
+ * class members so that receive() will obtain data from the stream
+ * provided by the response.
+ * @param url The fully qualified URL to try in LocalServer.
+ * @return True if the url was found and is now setup to receive.
+ * False if not found, with no side-effect.
+ */
+ public synchronized boolean useLocalServerResult(String url) {
+ UrlInterceptHandlerGears handler = UrlInterceptHandlerGears.getInstance();
+ if (handler == null) {
+ return false;
+ }
+ UrlInterceptHandlerGears.ServiceResponse serviceResponse =
+ handler.getServiceResponse(url, requestHeaders);
+ if (serviceResponse == null) {
+ log("No response in LocalServer");
+ return false;
+ }
+ // LocalServer will handle this URL. Initialize stream and
+ // response.
+ inputStream = serviceResponse.getInputStream();
+ responseLine = serviceResponse.getStatusLine();
+ responseHeaders = serviceResponse.getResponseHeaders();
+ if (logEnabled)
+ log("Got response from LocalServer: " + responseLine);
+ return true;
+ }
+
+ /**
+ * Perform a request using the cache result if present. Initializes
+ * class members so that receive() will obtain data from the cache.
+ * @param url The fully qualified URL to try in the cache.
+ * @return True is the url was found and is now setup to receive
+ * from cache. False if not found, with no side-effect.
+ */
+ public synchronized boolean useCacheResult(String url) {
+ // Try the browser's cache. CacheManager wants a Map<String, String>.
+ Map<String, String> cacheRequestHeaders = new HashMap<String, String>();
+ Iterator<Map.Entry<String, String[]>> it =
+ requestHeaders.entrySet().iterator();
+ while (it.hasNext()) {
+ Map.Entry<String, String[]> entry = it.next();
+ cacheRequestHeaders.put(
+ entry.getKey(),
+ entry.getValue()[HEADERS_MAP_INDEX_VALUE]);
+ }
+ CacheResult cacheResult =
+ CacheManager.getCacheFile(url, cacheRequestHeaders);
+ if (cacheResult == null) {
+ if (logEnabled)
+ log("No CacheResult for " + url);
+ return false;
+ }
+ if (logEnabled)
+ log("Got CacheResult from browser cache");
+ // Check for expiry. -1 is "never", otherwise milliseconds since 1970.
+ // Can be compared to System.currentTimeMillis().
+ long expires = cacheResult.getExpires();
+ if (expires >= 0 && System.currentTimeMillis() >= expires) {
+ log("CacheResult expired "
+ + (System.currentTimeMillis() - expires)
+ + " milliseconds ago");
+ // Cache hit has expired. Do not return it.
+ return false;
+ }
+ // Setup the inputStream to come from the cache.
+ inputStream = cacheResult.getInputStream();
+ if (inputStream == null) {
+ // Cache result may have gone away.
+ log("No inputStream for CacheResult " + url);
+ return false;
+ }
+ // Cache hit. Parse headers.
+ synthesizeHeadersFromCacheResult(cacheResult);
+ return true;
+ }
+
+ /**
+ * Take the limited set of headers in a CacheResult and synthesize
+ * response headers.
+ * @param cacheResult A CacheResult to populate responseHeaders with.
+ */
+ private void synthesizeHeadersFromCacheResult(CacheResult cacheResult) {
+ int statusCode = cacheResult.getHttpStatusCode();
+ // The status message is informal, so we can greatly simplify it.
+ String statusMessage;
+ if (statusCode >= 200 && statusCode < 300) {
+ statusMessage = "OK";
+ } else if (statusCode >= 300 && statusCode < 400) {
+ statusMessage = "MOVED";
+ } else {
+ statusMessage = "UNAVAILABLE";
+ }
+ // Synthesize the response line.
+ responseLine = "HTTP/1.1 " + statusCode + " " + statusMessage;
+ if (logEnabled)
+ log("Synthesized " + responseLine);
+ // Synthesize the returned headers from cache.
+ responseHeaders = new HashMap<String, String[]>();
+ String contentLength = Long.toString(cacheResult.getContentLength());
+ setResponseHeader(KEY_CONTENT_LENGTH, contentLength);
+ long expires = cacheResult.getExpires();
+ if (expires >= 0) {
+ // "Expires" header is valid and finite. Milliseconds since 1970
+ // epoch, formatted as RFC-1123.
+ String expiresString = DateUtils.formatDate(new Date(expires));
+ setResponseHeader(KEY_EXPIRES, expiresString);
+ }
+ String lastModified = cacheResult.getLastModified();
+ if (lastModified != null) {
+ // Last modification time of the page. Passed end-to-end, but
+ // not used by us.
+ setResponseHeader(KEY_LAST_MODIFIED, lastModified);
+ }
+ String eTag = cacheResult.getETag();
+ if (eTag != null) {
+ // Entity tag. A kind of GUID to identify identical resources.
+ setResponseHeader(KEY_ETAG, eTag);
+ }
+ String location = cacheResult.getLocation();
+ if (location != null) {
+ // If valid, refers to the location of a redirect.
+ setResponseHeader(KEY_LOCATION, location);
+ }
+ String mimeType = cacheResult.getMimeType();
+ if (mimeType == null) {
+ // Use a safe default MIME type when none is
+ // specified. "text/plain" is safe to render in the browser
+ // window (even if large) and won't be intepreted as anything
+ // that would cause execution.
+ mimeType = DEFAULT_MIME_TYPE;
+ }
+ String encoding = cacheResult.getEncoding();
+ // Encoding may not be specified. No default.
+ String contentType = mimeType;
+ if (encoding != null) {
+ contentType += "; charset=" + encoding;
+ }
+ setResponseHeader(KEY_CONTENT_TYPE, contentType);
+ }
+
+ /**
+ * Create a CacheResult for this URL. This enables the repsonse body
+ * to be sent in calls to appendCacheResult().
+ * @param url The fully qualified URL to add to the cache.
+ * @param responseCode The response code returned for the request, e.g 200.
+ * @param mimeType The MIME type of the body, e.g "text/plain".
+ * @param encoding The encoding, e.g "utf-8". Use "" for unknown.
+ */
+ public synchronized boolean createCacheResult(
+ String url, int responseCode, String mimeType, String encoding) {
+ if (logEnabled)
+ log("Making cache entry for " + url);
+ // Take the headers and parse them into a format needed by
+ // CacheManager.
+ Headers cacheHeaders = new Headers();
+ Iterator<Map.Entry<String, String[]>> it =
+ responseHeaders.entrySet().iterator();
+ while (it.hasNext()) {
+ Map.Entry<String, String[]> entry = it.next();
+ // Headers.parseHeader() expects lowercase keys.
+ String keyValue = entry.getKey() + ": "
+ + entry.getValue()[HEADERS_MAP_INDEX_VALUE];
+ CharArrayBuffer buffer = new CharArrayBuffer(keyValue.length());
+ buffer.append(keyValue);
+ // Parse it into the header container.
+ cacheHeaders.parseHeader(buffer);
+ }
+ cacheResult = CacheManager.createCacheFile(
+ url, responseCode, cacheHeaders, mimeType, true);
+ if (cacheResult != null) {
+ if (logEnabled)
+ log("Saving into cache");
+ cacheResult.setEncoding(encoding);
+ cacheResultUrl = url;
+ return true;
+ } else {
+ log("Couldn't create cacheResult");
+ return false;
+ }
+ }
+
+ /**
+ * Add data from the response body to the CacheResult created with
+ * createCacheResult().
+ * @param data A byte array of the next sequential bytes in the
+ * response body.
+ * @param bytes The number of bytes to write from the start of
+ * the array.
+ * @return True if all bytes successfully written, false on failure.
+ */
+ public synchronized boolean appendCacheResult(byte[] data, int bytes) {
+ if (cacheResult == null) {
+ log("appendCacheResult() called without a CacheResult initialized");
+ return false;
+ }
+ try {
+ cacheResult.getOutputStream().write(data, 0, bytes);
+ } catch (IOException ex) {
+ log("Got IOException writing cache data: " + ex);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Save the completed CacheResult into the CacheManager. This must
+ * have been created first with createCacheResult().
+ * @return Returns true if the entry has been successfully saved.
+ */
+ public synchronized boolean saveCacheResult() {
+ if (cacheResult == null || cacheResultUrl == null) {
+ log("Tried to save cache result but createCacheResult not called");
+ return false;
+ }
+ if (logEnabled)
+ log("Saving cache result");
+ CacheManager.saveCacheFile(cacheResultUrl, cacheResult);
+ cacheResult = null;
+ cacheResultUrl = null;
+ return true;
+ }
+
+ /**
+ * Perform an HTTP request on the network. The underlying
+ * HttpURLConnection is connected to the remote server and the
+ * response headers are received.
+ * @return True if the connection succeeded and headers have been
+ * received. False on connection failure.
+ */
+ public boolean connectToRemote() {
+ synchronized (this) {
+ // Transfer a snapshot of our internally maintained map of request
+ // headers to the connection object.
+ applyRequestHeadersToConnection();
+ // Note blocking I/O so abort() can interrupt us.
+ inBlockingOperation = true;
+ }
+ boolean success;
+ try {
+ if (logEnabled)
+ log("Connecting to remote");
+ connection.connect();
+ if (logEnabled)
+ log("Connected");
+ success = true;
+ } catch (IOException e) {
+ log("Got IOException in connect(): " + e.toString());
+ success = false;
+ } finally {
+ synchronized (this) {
+ // No longer blocking.
+ inBlockingOperation = false;
+ }
+ }
+ return success;
+ }
+
+ /**
+ * Receive all headers from the server and populate
+ * responseHeaders. This converts from the slightly odd format
+ * returned by java.net.HttpURLConnection to a simpler
+ * java.util.Map.
+ * @return True if headers are successfully received, False on
+ * connection error.
+ */
+ public synchronized boolean parseHeaders() {
+ responseHeaders = new HashMap<String, String[]>();
+ /* HttpURLConnection contains a null terminated list of
+ * key->value response pairs. If the key is null, then the value
+ * contains the complete status line. If both key and value are
+ * null for an index, we've reached the end.
+ */
+ for (int i = 0; ; ++i) {
+ String key = connection.getHeaderFieldKey(i);
+ String value = connection.getHeaderField(i);
+ if (logEnabled)
+ log("header " + key + " -> " + value);
+ if (key == null && value == null) {
+ // End of list.
+ break;
+ } else if (key == null) {
+ // The pair with null key has the complete status line in
+ // the value, e.g "HTTP/1.0 200 OK".
+ responseLine = value;
+ } else if (value != null) {
+ // If key and value are non-null, this is a response pair, e.g
+ // "Content-Length" -> "5". Use setResponseHeader() to
+ // correctly deal with case-insensitivity of the key.
+ setResponseHeader(key, value);
+ } else {
+ // The key is non-null but value is null. Unexpected
+ // condition.
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Receive the next sequential bytes of the response body after
+ * successful connection. This will receive up to the size of the
+ * provided byte array. If there is no body, this will return 0
+ * bytes on the first call after connection.
+ * @param buf A pre-allocated byte array to receive data into.
+ * @return The number of bytes from the start of the array which
+ * have been filled, 0 on EOF, or negative on error.
+ */
+ public int receive(byte[] buf) {
+ if (inputStream == null) {
+ // If this is the first call, setup the InputStream. This may
+ // fail if there were headers, but no body returned by the
+ // server.
+ try {
+ inputStream = connection.getInputStream();
+ } catch (IOException inputException) {
+ log("Failed to connect InputStream: " + inputException);
+ // Not unexpected. For example, 404 response return headers,
+ // and sometimes a body with a detailed error. Try the error
+ // stream.
+ inputStream = connection.getErrorStream();
+ if (inputStream == null) {
+ // No error stream either. Treat as a 0 byte response.
+ log("No InputStream");
+ return 0; // EOF.
+ }
+ }
+ }
+ synchronized (this) {
+ // Note blocking I/O so abort() can interrupt us.
+ inBlockingOperation = true;
+ }
+ int ret;
+ try {
+ int got = inputStream.read(buf);
+ if (got > 0) {
+ // Got some bytes, not EOF.
+ ret = got;
+ } else {
+ // EOF.
+ inputStream.close();
+ ret = 0;
+ }
+ } catch (IOException e) {
+ // An abort() interrupts us by calling close() on our stream.
+ log("Got IOException in inputStream.read(): " + e.toString());
+ ret = -1;
+ } finally {
+ synchronized (this) {
+ // No longer blocking.
+ inBlockingOperation = false;
+ }
+ }
+ return ret;
+ }
+
+ /**
+ * For POST method requests, send a stream of data provided by the
+ * native side in repeated callbacks.
+ * @param data A byte array containing the data to sent, or null
+ * if indicating EOF.
+ * @param bytes The number of bytes from the start of the array to
+ * send, or 0 if indicating EOF.
+ * @return True if all bytes were successfully sent, false on error.
+ */
+ public boolean sendPostData(byte[] data, int bytes) {
+ synchronized (this) {
+ // Note blocking I/O so abort() can interrupt us.
+ inBlockingOperation = true;
+ }
+ boolean success;
+ try {
+ OutputStream outputStream = connection.getOutputStream();
+ if (data == null && bytes == 0) {
+ outputStream.close();
+ } else {
+ outputStream.write(data, 0, bytes);
+ }
+ success = true;
+ } catch (IOException e) {
+ log("Got IOException in post: " + e.toString());
+ success = false;
+ } finally {
+ synchronized (this) {
+ // No longer blocking.
+ inBlockingOperation = false;
+ }
+ }
+ return success;
+ }
+}
diff --git a/core/java/android/webkit/gears/IGearsDialogService.java b/core/java/android/webkit/gears/IGearsDialogService.java
new file mode 100644
index 0000000..82a3bd9
--- /dev/null
+++ b/core/java/android/webkit/gears/IGearsDialogService.java
@@ -0,0 +1,107 @@
+/*
+ * This file is auto-generated. DO NOT MODIFY.
+ * Original file: android.webkit.gears/IGearsDialogService.aidl
+ */
+package android.webkit.gears;
+import java.lang.String;
+import android.os.RemoteException;
+import android.os.IBinder;
+import android.os.IInterface;
+import android.os.Binder;
+import android.os.Parcel;
+public interface IGearsDialogService extends android.os.IInterface
+{
+/** Local-side IPC implementation stub class. */
+public static abstract class Stub extends android.os.Binder implements android.webkit.gears.IGearsDialogService
+{
+private static final java.lang.String DESCRIPTOR = "com.android.browser.IGearsDialogService";
+/** Construct the stub at attach it to the interface. */
+public Stub()
+{
+this.attachInterface(this, DESCRIPTOR);
+}
+/**
+ * Cast an IBinder object into an IGearsDialogService interface,
+ * generating a proxy if needed.
+ */
+public static android.webkit.gears.IGearsDialogService asInterface(android.os.IBinder obj)
+{
+if ((obj==null)) {
+return null;
+}
+android.webkit.gears.IGearsDialogService in = (android.webkit.gears.IGearsDialogService)obj.queryLocalInterface(DESCRIPTOR);
+if ((in!=null)) {
+return in;
+}
+return new android.webkit.gears.IGearsDialogService.Stub.Proxy(obj);
+}
+public android.os.IBinder asBinder()
+{
+return this;
+}
+public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
+{
+switch (code)
+{
+case INTERFACE_TRANSACTION:
+{
+reply.writeString(DESCRIPTOR);
+return true;
+}
+case TRANSACTION_showDialog:
+{
+data.enforceInterface(DESCRIPTOR);
+java.lang.String _arg0;
+_arg0 = data.readString();
+java.lang.String _arg1;
+_arg1 = data.readString();
+boolean _arg2;
+_arg2 = (0!=data.readInt());
+java.lang.String _result = this.showDialog(_arg0, _arg1, _arg2);
+reply.writeNoException();
+reply.writeString(_result);
+return true;
+}
+}
+return super.onTransact(code, data, reply, flags);
+}
+private static class Proxy implements android.webkit.gears.IGearsDialogService
+{
+private android.os.IBinder mRemote;
+Proxy(android.os.IBinder remote)
+{
+mRemote = remote;
+}
+public android.os.IBinder asBinder()
+{
+return mRemote;
+}
+public java.lang.String getInterfaceDescriptor()
+{
+return DESCRIPTOR;
+}
+public java.lang.String showDialog(java.lang.String htmlContent, java.lang.String dialogArguments, boolean inSettings) throws android.os.RemoteException
+{
+android.os.Parcel _data = android.os.Parcel.obtain();
+android.os.Parcel _reply = android.os.Parcel.obtain();
+java.lang.String _result;
+try {
+_data.writeInterfaceToken(DESCRIPTOR);
+_data.writeString(htmlContent);
+_data.writeString(dialogArguments);
+_data.writeInt(((inSettings)?(1):(0)));
+mRemote.transact(Stub.TRANSACTION_showDialog, _data, _reply, 0);
+_reply.readException();
+_result = _reply.readString();
+}
+finally {
+_reply.recycle();
+_data.recycle();
+}
+return _result;
+}
+}
+static final int TRANSACTION_showDialog = (IBinder.FIRST_CALL_TRANSACTION + 0);
+}
+public java.lang.String showDialog(java.lang.String htmlContent, java.lang.String dialogArguments, boolean inSettings) throws android.os.RemoteException;
+}
diff --git a/core/java/android/webkit/gears/UrlInterceptHandlerGears.java b/core/java/android/webkit/gears/UrlInterceptHandlerGears.java
new file mode 100644
index 0000000..95fc30f
--- /dev/null
+++ b/core/java/android/webkit/gears/UrlInterceptHandlerGears.java
@@ -0,0 +1,497 @@
+// Copyright 2008, The Android Open Source Project
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice,
+// this list of conditions and the following disclaimer.
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+// this list of conditions and the following disclaimer in the documentation
+// and/or other materials provided with the distribution.
+// 3. Neither the name of Google Inc. nor the names of its contributors may be
+// used to endorse or promote products derived from this software without
+// specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+package android.webkit.gears;
+
+import android.net.http.Headers;
+import android.util.Log;
+import android.webkit.CacheManager;
+import android.webkit.CacheManager.CacheResult;
+import android.webkit.Plugin;
+import android.webkit.UrlInterceptRegistry;
+import android.webkit.UrlInterceptHandler;
+import android.webkit.WebView;
+
+import org.apache.http.impl.cookie.DateUtils;
+import org.apache.http.util.CharArrayBuffer;
+
+import java.io.*;
+import java.util.*;
+
+/**
+ * Services requests to handle URLs coming from the browser or
+ * HttpRequestAndroid. This registers itself with the
+ * UrlInterceptRegister in Android so we get a chance to service all
+ * URLs passing through the browser before anything else.
+ */
+public class UrlInterceptHandlerGears implements UrlInterceptHandler {
+ /** Singleton instance. */
+ private static UrlInterceptHandlerGears instance;
+ /** Debug logging tag. */
+ private static final String LOG_TAG = "Gears-J";
+ /** Buffer size for reading/writing streams. */
+ private static final int BUFFER_SIZE = 4096;
+ /**
+ * Number of milliseconds to expire LocalServer temporary entries in
+ * the browser's cache. Somewhat arbitrarily chosen as a compromise
+ * between being a) long enough not to expire during page load and
+ * b) short enough to evict entries during a session. */
+ private static final int CACHE_EXPIRY_MS = 60000; // 1 minute.
+ /** Enable/disable all logging in this class. */
+ private static boolean logEnabled = false;
+ /** The unmodified (case-sensitive) key in the headers map is the
+ * same index as used by HttpRequestAndroid. */
+ public static final int HEADERS_MAP_INDEX_KEY =
+ HttpRequestAndroid.HEADERS_MAP_INDEX_KEY;
+ /** The associated value in the headers map is the same index as
+ * used by HttpRequestAndroid. */
+ public static final int HEADERS_MAP_INDEX_VALUE =
+ HttpRequestAndroid.HEADERS_MAP_INDEX_VALUE;
+
+ /**
+ * Object passed to the native side, containing information about
+ * the URL to service.
+ */
+ public static class ServiceRequest {
+ // The URL being requested.
+ private String url;
+ // Request headers. Map of lowercase key to [ unmodified key, value ].
+ private Map<String, String[]> requestHeaders;
+
+ /**
+ * Initialize members on construction.
+ * @param url The URL being requested.
+ * @param requestHeaders Headers associated with the request,
+ * or null if none.
+ * Map of lowercase key to [ unmodified key, value ].
+ */
+ public ServiceRequest(String url, Map<String, String[]> requestHeaders) {
+ this.url = url;
+ this.requestHeaders = requestHeaders;
+ }
+
+ /**
+ * Returns the URL being requested.
+ * @return The URL being requested.
+ */
+ public String getUrl() {
+ return url;
+ }
+
+ /**
+ * Get the value associated with a request header key, if any.
+ * @param header The key to find, case insensitive.
+ * @return The value associated with this header, or null if not found.
+ */
+ public String getRequestHeader(String header) {
+ if (requestHeaders != null) {
+ String[] value = requestHeaders.get(header.toLowerCase());
+ if (value != null) {
+ return value[HEADERS_MAP_INDEX_VALUE];
+ } else {
+ return null;
+ }
+ } else {
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Object returned by the native side, containing information needed
+ * to pass the entire response back to the browser or
+ * HttpRequestAndroid. Works from either an in-memory array or a
+ * file on disk.
+ */
+ public class ServiceResponse {
+ // The response status code, e.g 200.
+ private int statusCode;
+ // The full status line, e.g "HTTP/1.1 200 OK".
+ private String statusLine;
+ // All headers associated with the response. Map of lowercase key
+ // to [ unmodified key, value ].
+ private Map<String, String[]> responseHeaders =
+ new HashMap<String, String[]>();
+ // The MIME type, e.g "text/html".
+ private String mimeType;
+ // The encoding, e.g "utf-8", or null if none.
+ private String encoding;
+ // The stream which contains the body when read().
+ private InputStream inputStream;
+
+ /**
+ * Initialize members using an in-memory array to return the body.
+ * @param statusCode The response status code, e.g 200.
+ * @param statusLine The full status line, e.g "HTTP/1.1 200 OK".
+ * @param mimeType The MIME type, e.g "text/html".
+ * @param encoding Encoding, e.g "utf-8" or null if none.
+ * @param body The response body as a byte array, non-empty.
+ */
+ void setResultArray(
+ int statusCode,
+ String statusLine,
+ String mimeType,
+ String encoding,
+ byte[] body) {
+ this.statusCode = statusCode;
+ this.statusLine = statusLine;
+ this.mimeType = mimeType;
+ this.encoding = encoding;
+ // Setup a stream to read out of the byte array.
+ this.inputStream = new ByteArrayInputStream(body);
+ }
+
+ /**
+ * Initialize members using a file on disk to return the body.
+ * @param statusCode The response status code, e.g 200.
+ * @param statusLine The full status line, e.g "HTTP/1.1 200 OK".
+ * @param mimeType The MIME type, e.g "text/html".
+ * @param encoding Encoding, e.g "utf-8" or null if none.
+ * @param path Full path to the file containing the body.
+ * @return True if the file is successfully setup to stream,
+ * false on error such as file not found.
+ */
+ boolean setResultFile(
+ int statusCode,
+ String statusLine,
+ String mimeType,
+ String encoding,
+ String path) {
+ this.statusCode = statusCode;
+ this.statusLine = statusLine;
+ this.mimeType = mimeType;
+ this.encoding = encoding;
+ try {
+ // Setup a stream to read out of a file on disk.
+ this.inputStream = new FileInputStream(new File(path));
+ return true;
+ } catch (java.io.FileNotFoundException ex) {
+ log("File not found: " + path);
+ return false;
+ }
+ }
+
+ /**
+ * Set a response header, adding its settings to the header members.
+ * @param key The case sensitive key for the response header,
+ * e.g "Set-Cookie".
+ * @param value The value associated with this key, e.g "cookie1234".
+ */
+ public void setResponseHeader(String key, String value) {
+ // The map value contains the unmodified key (not lowercase).
+ String[] mapValue = { key, value };
+ responseHeaders.put(key.toLowerCase(), mapValue);
+ }
+
+ /**
+ * Return the "Content-Type" header possibly supplied by a
+ * previous setResponseHeader().
+ * @return The "Content-Type" value, or null if not present.
+ */
+ public String getContentType() {
+ // The map keys are lowercase.
+ String[] value = responseHeaders.get("content-type");
+ if (value != null) {
+ return value[HEADERS_MAP_INDEX_VALUE];
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Returns the HTTP status code for the response, supplied in
+ * setResultArray() or setResultFile().
+ * @return The HTTP statue code, e.g 200.
+ */
+ public int getStatusCode() {
+ return statusCode;
+ }
+
+ /**
+ * Returns the full HTTP status line for the response, supplied in
+ * setResultArray() or setResultFile().
+ * @return The HTTP statue line, e.g "HTTP/1.1 200 OK".
+ */
+ public String getStatusLine() {
+ return statusLine;
+ }
+
+ /**
+ * Get all response headers supplied in calls in
+ * setResponseHeader().
+ * @return A Map<String, String[]> containing all headers.
+ */
+ public Map<String, String[]> getResponseHeaders() {
+ return responseHeaders;
+ }
+
+ /**
+ * Returns the MIME type for the response, supplied in
+ * setResultArray() or setResultFile().
+ * @return The MIME type, e.g "text/html".
+ */
+ public String getMimeType() {
+ return mimeType;
+ }
+
+ /**
+ * Returns the encoding for the response, supplied in
+ * setResultArray() or setResultFile(), or null if none.
+ * @return The encoding, e.g "utf-8", or null if none.
+ */
+ public String getEncoding() {
+ return encoding;
+ }
+
+ /**
+ * Returns the InputStream setup by setResultArray() or
+ * setResultFile() to allow reading data either from memory or
+ * disk.
+ * @return The InputStream containing the response body.
+ */
+ public InputStream getInputStream() {
+ return inputStream;
+ }
+ }
+
+ /**
+ * Construct and initialize the singleton instance.
+ */
+ public UrlInterceptHandlerGears() {
+ if (instance != null) {
+ Log.e(LOG_TAG, "UrlInterceptHandlerGears singleton already constructed");
+ throw new RuntimeException();
+ }
+ instance = this;
+ }
+
+ /**
+ * Turn on/off logging in this class.
+ * @param on Logging enable state.
+ */
+ public static void enableLogging(boolean on) {
+ logEnabled = on;
+ }
+
+ /**
+ * Get the singleton instance.
+ * @return The singleton instance.
+ */
+ public static UrlInterceptHandlerGears getInstance() {
+ return instance;
+ }
+
+ /**
+ * Register the singleton instance with the browser's interception
+ * mechanism.
+ */
+ public synchronized void register() {
+ UrlInterceptRegistry.registerHandler(this);
+ }
+
+ /**
+ * Unregister the singleton instance from the browser's interception
+ * mechanism.
+ */
+ public synchronized void unregister() {
+ UrlInterceptRegistry.unregisterHandler(this);
+ }
+
+ /**
+ * Copy the entire InputStream to OutputStream.
+ * @param inputStream The stream to read from.
+ * @param outputStream The stream to write to.
+ * @return True if the entire stream copied successfully, false on error.
+ */
+ private boolean copyStream(InputStream inputStream,
+ OutputStream outputStream) {
+ try {
+ // Temporary buffer to copy stream through.
+ byte[] buf = new byte[BUFFER_SIZE];
+ for (;;) {
+ // Read up to BUFFER_SIZE bytes.
+ int bytes = inputStream.read(buf);
+ if (bytes < 0) {
+ break;
+ }
+ // Write the number of bytes we just read.
+ outputStream.write(buf, 0, bytes);
+ }
+ } catch (IOException ex) {
+ log("Got IOException copying stream: " + ex);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Given an URL, returns a CacheResult which contains the response
+ * for the request. This implements the UrlInterceptHandler interface.
+ *
+ * @param url The fully qualified URL being requested.
+ * @param requestHeaders The request headers for this URL.
+ * @return If a response can be crafted, a CacheResult initialized
+ * to return the surrogate response. If this URL cannot
+ * be serviced, returns null.
+ */
+ public CacheResult service(String url, Map<String, String> requestHeaders) {
+ // Thankfully the browser does call us with case-sensitive
+ // headers. We just need to map it case-insensitive.
+ Map<String, String[]> lowercaseRequestHeaders =
+ new HashMap<String, String[]>();
+ Iterator<Map.Entry<String, String>> requestHeadersIt =
+ requestHeaders.entrySet().iterator();
+ while (requestHeadersIt.hasNext()) {
+ Map.Entry<String, String> entry = requestHeadersIt.next();
+ String key = entry.getKey();
+ String mapValue[] = { key, entry.getValue() };
+ lowercaseRequestHeaders.put(key.toLowerCase(), mapValue);
+ }
+ ServiceResponse response = getServiceResponse(url, lowercaseRequestHeaders);
+ if (response == null) {
+ // No result for this URL.
+ return null;
+ }
+ // Translate the ServiceResponse to a CacheResult.
+ // Translate http -> gears, https -> gearss, so we don't overwrite
+ // existing entries.
+ String gearsUrl = "gears" + url.substring("http".length());
+ // Set the result to expire, so that entries don't pollute the
+ // browser's cache for too long.
+ long now_ms = System.currentTimeMillis();
+ String expires = DateUtils.formatDate(new Date(now_ms + CACHE_EXPIRY_MS));
+ response.setResponseHeader(HttpRequestAndroid.KEY_EXPIRES, expires);
+ // The browser is only interested in a small subset of headers,
+ // contained in a Headers object. Iterate the map of all headers
+ // and add them to Headers.
+ Headers headers = new Headers();
+ Iterator<Map.Entry<String, String[]>> responseHeadersIt =
+ response.getResponseHeaders().entrySet().iterator();
+ while (responseHeadersIt.hasNext()) {
+ Map.Entry<String, String[]> entry = responseHeadersIt.next();
+ // Headers.parseHeader() expects lowercase keys.
+ String keyValue = entry.getKey() + ": "
+ + entry.getValue()[HEADERS_MAP_INDEX_VALUE];
+ CharArrayBuffer buffer = new CharArrayBuffer(keyValue.length());
+ buffer.append(keyValue);
+ // Parse it into the header container.
+ headers.parseHeader(buffer);
+ }
+ CacheResult cacheResult = CacheManager.createCacheFile(
+ gearsUrl,
+ response.getStatusCode(),
+ headers,
+ response.getMimeType(),
+ true); // forceCache
+
+ if (cacheResult == null) {
+ return null;
+ }
+
+ // Set encoding if specified.
+ String encoding = response.getEncoding();
+ if (encoding != null) {
+ cacheResult.setEncoding(encoding);
+ }
+ // Copy the response body to the CacheResult. This handles all
+ // combinations of memory vs on-disk on both sides.
+ InputStream inputStream = response.getInputStream();
+ OutputStream outputStream = cacheResult.getOutputStream();
+ boolean copied = copyStream(inputStream, outputStream);
+ // Close the input and output streams to relinquish their
+ // resources earlier.
+ try {
+ inputStream.close();
+ } catch (IOException ex) {
+ log("IOException closing InputStream: " + ex);
+ copied = false;
+ }
+ try {
+ outputStream.close();
+ } catch (IOException ex) {
+ log("IOException closing OutputStream: " + ex);
+ copied = false;
+ }
+ if (!copied) {
+ log("copyStream of local result failed");
+ return null;
+ }
+ // Save the entry into the browser's cache.
+ CacheManager.saveCacheFile(gearsUrl, cacheResult);
+ // Get it back from the cache, this time properly initialized to
+ // be used for input.
+ cacheResult = CacheManager.getCacheFile(gearsUrl, null);
+ if (cacheResult != null) {
+ if (logEnabled)
+ log("Returning surrogate result");
+ return cacheResult;
+ } else {
+ // Not an expected condition, but handle gracefully. Perhaps out
+ // of memory or disk?
+ Log.e(LOG_TAG, "Lost CacheResult between save and get. Can't serve.\n");
+ return null;
+ }
+ }
+
+ /**
+ * Given an URL, returns a CacheResult and headers which contain the
+ * response for the request.
+ *
+ * @param url The fully qualified URL being requested.
+ * @param requestHeaders The request headers for this URL.
+ * @return If a response can be crafted, a ServiceResponse is
+ * created which contains all response headers and an InputStream
+ * attached to the body. If there is no response, null is returned.
+ */
+ public ServiceResponse getServiceResponse(String url,
+ Map<String, String[]> requestHeaders) {
+ if (!url.startsWith("http://") && !url.startsWith("https://")) {
+ // Don't know how to service non-HTTP URLs
+ return null;
+ }
+ // Call the native handler to craft a response for this URL.
+ return nativeService(new ServiceRequest(url, requestHeaders));
+ }
+
+ /**
+ * Convenience debug function. Calls Android logging mechanism.
+ * @param str String to log to the Android console.
+ */
+ private void log(String str) {
+ if (logEnabled) {
+ Log.i(LOG_TAG, str);
+ }
+ }
+
+ /**
+ * Native method which handles the bulk of the request in LocalServer.
+ * @param request A ServiceRequest object containing information about
+ * the request.
+ * @return If serviced, a ServiceResponse object containing all the
+ * information to provide a response for the URL, or null
+ * if no response available for this URL.
+ */
+ private native static ServiceResponse nativeService(ServiceRequest request);
+}
diff --git a/core/java/android/webkit/gears/VersionExtractor.java b/core/java/android/webkit/gears/VersionExtractor.java
new file mode 100644
index 0000000..172dacb
--- /dev/null
+++ b/core/java/android/webkit/gears/VersionExtractor.java
@@ -0,0 +1,147 @@
+// Copyright 2008, The Android Open Source Project
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice,
+// this list of conditions and the following disclaimer.
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+// this list of conditions and the following disclaimer in the documentation
+// and/or other materials provided with the distribution.
+// 3. Neither the name of Google Inc. nor the names of its contributors may be
+// used to endorse or promote products derived from this software without
+// specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+package android.webkit.gears;
+
+import android.util.Log;
+import java.io.IOException;
+import java.io.StringReader;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.FactoryConfigurationError;
+import javax.xml.parsers.ParserConfigurationException;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.w3c.dom.Document;
+import org.w3c.dom.DOMException;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+/**
+ * A class that can extract the Gears version and upgrade URL from an
+ * xml document.
+ */
+public final class VersionExtractor {
+
+ /**
+ * Logging tag
+ */
+ private static final String TAG = "Gears-J-VersionExtractor";
+ /**
+ * XML element names.
+ */
+ private static final String VERSION = "em:version";
+ private static final String URL = "em:updateLink";
+
+ /**
+ * Parses the input xml string and invokes the native
+ * setVersionAndUrl method.
+ * @param xml is the XML string to parse.
+ * @return true if the extraction is successful and false otherwise.
+ */
+ public static boolean extract(String xml, long nativeObject) {
+ try {
+ // Build the builders.
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ factory.setNamespaceAware(false);
+ DocumentBuilder builder = factory.newDocumentBuilder();
+
+ // Create the document.
+ Document doc = builder.parse(new InputSource(new StringReader(xml)));
+
+ // Look for the version and url elements and get their text
+ // contents.
+ String version = extractText(doc, VERSION);
+ String url = extractText(doc, URL);
+
+ // If we have both, let the native side know.
+ if (version != null && url != null) {
+ setVersionAndUrl(version, url, nativeObject);
+ return true;
+ }
+
+ return false;
+
+ } catch (FactoryConfigurationError ex) {
+ Log.e(TAG, "Could not create the DocumentBuilderFactory " + ex);
+ } catch (ParserConfigurationException ex) {
+ Log.e(TAG, "Could not create the DocumentBuilder " + ex);
+ } catch (SAXException ex) {
+ Log.e(TAG, "Could not parse the xml " + ex);
+ } catch (IOException ex) {
+ Log.e(TAG, "Could not read the xml " + ex);
+ }
+
+ return false;
+ }
+
+ /**
+ * Extracts the text content of the first element with the given name.
+ * @param doc is the Document where the element is searched for.
+ * @param elementName is name of the element to searched for.
+ * @return the text content of the element or null if no such
+ * element is found.
+ */
+ private static String extractText(Document doc, String elementName) {
+ String text = null;
+ NodeList node_list = doc.getElementsByTagName(elementName);
+
+ if (node_list.getLength() > 0) {
+ // We are only interested in the first node. Normally there
+ // should not be more than one anyway.
+ Node node = node_list.item(0);
+
+ // Iterate through the text children.
+ NodeList child_list = node.getChildNodes();
+
+ try {
+ for (int i = 0; i < child_list.getLength(); ++i) {
+ Node child = child_list.item(i);
+ if (child.getNodeType() == Node.TEXT_NODE) {
+ if (text == null) {
+ text = new String();
+ }
+ text += child.getNodeValue();
+ }
+ }
+ } catch (DOMException ex) {
+ Log.e(TAG, "getNodeValue() failed " + ex);
+ }
+ }
+
+ if (text != null) {
+ text = text.trim();
+ }
+
+ return text;
+ }
+
+ /**
+ * Native method used to send the version and url back to the C++
+ * side.
+ */
+ private static native void setVersionAndUrl(
+ String version, String url, long nativeObject);
+}
diff --git a/core/java/android/webkit/gears/ZipInflater.java b/core/java/android/webkit/gears/ZipInflater.java
new file mode 100644
index 0000000..f6b6be5
--- /dev/null
+++ b/core/java/android/webkit/gears/ZipInflater.java
@@ -0,0 +1,200 @@
+// Copyright 2008, The Android Open Source Project
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice,
+// this list of conditions and the following disclaimer.
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+// this list of conditions and the following disclaimer in the documentation
+// and/or other materials provided with the distribution.
+// 3. Neither the name of Google Inc. nor the names of its contributors may be
+// used to endorse or promote products derived from this software without
+// specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+package android.webkit.gears;
+
+import android.os.StatFs;
+import android.util.Log;
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Enumeration;
+import java.util.zip.CRC32;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipInputStream;
+
+
+/**
+ * A class that can inflate a zip archive.
+ */
+public final class ZipInflater {
+ /**
+ * Logging tag
+ */
+ private static final String TAG = "Gears-J-ZipInflater";
+
+ /**
+ * The size of the buffer used to read from the archive.
+ */
+ private static final int BUFFER_SIZE_BYTES = 32 * 1024; // 32 KB.
+ /**
+ * The path navigation component (i.e. "../").
+ */
+ private static final String PATH_NAVIGATION_COMPONENT = ".." + File.separator;
+ /**
+ * The root of the data partition.
+ */
+ private static final String DATA_PARTITION_ROOT = "/data";
+
+ /**
+ * We need two be able to store two versions of gears in parallel:
+ * - the zipped version
+ * - the unzipped version, which will be loaded next time the browser is started.
+ * We are conservative and do not attempt to unpack unless there enough free
+ * space on the device to store 4 times the new Gears size.
+ */
+ private static final long SIZE_MULTIPLIER = 4;
+
+ /**
+ * Unzips the archive with the given name.
+ * @param filename is the name of the zip to inflate.
+ * @param path is the path where the zip should be unpacked. It must contain
+ * a trailing separator, or the extraction will fail.
+ * @return true if the extraction is successful and false otherwise.
+ */
+ public static boolean inflate(String filename, String path) {
+ Log.i(TAG, "Extracting " + filename + " to " + path);
+
+ // Check that the path ends with a separator.
+ if (!path.endsWith(File.separator)) {
+ Log.e(TAG, "Path missing trailing separator: " + path);
+ return false;
+ }
+
+ boolean result = false;
+
+ // Use a ZipFile to get an enumeration of the entries and
+ // calculate the overall uncompressed size of the archive. Also
+ // check for existing files or directories that have the same
+ // name as the entries in the archive. Also check for invalid
+ // entry names (e.g names that attempt to navigate to the
+ // parent directory).
+ ZipInputStream zipStream = null;
+ long uncompressedSize = 0;
+ try {
+ ZipFile zipFile = new ZipFile(filename);
+ try {
+ Enumeration entries = zipFile.entries();
+ while (entries.hasMoreElements()) {
+ ZipEntry entry = (ZipEntry) entries.nextElement();
+ uncompressedSize += entry.getSize();
+ // Check against entry names that may attempt to navigate
+ // out of the destination directory.
+ if (entry.getName().indexOf(PATH_NAVIGATION_COMPONENT) >= 0) {
+ throw new IOException("Illegal entry name: " + entry.getName());
+ }
+
+ // Check against entries with the same name as pre-existing files or
+ // directories.
+ File file = new File(path + entry.getName());
+ if (file.exists()) {
+ // A file or directory with the same name already exist.
+ // This must not happen, so we treat this as an error.
+ throw new IOException(
+ "A file or directory with the same name already exists.");
+ }
+ }
+ } finally {
+ zipFile.close();
+ }
+
+ Log.i(TAG, "Determined uncompressed size: " + uncompressedSize);
+ // Check we have enough space to unpack this archive.
+ if (freeSpace() <= uncompressedSize * SIZE_MULTIPLIER) {
+ throw new IOException("Not enough space to unpack this archive.");
+ }
+
+ zipStream = new ZipInputStream(
+ new BufferedInputStream(new FileInputStream(filename)));
+ ZipEntry entry;
+ int counter;
+ byte buffer[] = new byte[BUFFER_SIZE_BYTES];
+
+ // Iterate through the entries and write each of them to a file.
+ while ((entry = zipStream.getNextEntry()) != null) {
+ File file = new File(path + entry.getName());
+ if (entry.isDirectory()) {
+ // If the entry denotes a directory, we need to create a
+ // directory with the same name.
+ file.mkdirs();
+ } else {
+ CRC32 checksum = new CRC32();
+ BufferedOutputStream output = new BufferedOutputStream(
+ new FileOutputStream(file),
+ BUFFER_SIZE_BYTES);
+ try {
+ // Read the entry and write it to the file.
+ while ((counter = zipStream.read(buffer, 0, BUFFER_SIZE_BYTES)) !=
+ -1) {
+ output.write(buffer, 0, counter);
+ checksum.update(buffer, 0, counter);
+ }
+ output.flush();
+ } finally {
+ output.close();
+ }
+
+ if (checksum.getValue() != entry.getCrc()) {
+ throw new IOException(
+ "Integrity check failed for: " + entry.getName());
+ }
+ }
+ zipStream.closeEntry();
+ }
+
+ result = true;
+
+ } catch (FileNotFoundException ex) {
+ Log.e(TAG, "The zip file could not be found. " + ex);
+ } catch (IOException ex) {
+ Log.e(TAG, "Could not read or write an entry. " + ex);
+ } catch(IllegalArgumentException ex) {
+ Log.e(TAG, "Could not create the BufferedOutputStream. " + ex);
+ } finally {
+ if (zipStream != null) {
+ try {
+ zipStream.close();
+ } catch (IOException ex) {
+ // Ignored.
+ }
+ }
+ // Discard any exceptions and return the result to the native side.
+ return result;
+ }
+ }
+
+ private static final long freeSpace() {
+ StatFs data_partition = new StatFs(DATA_PARTITION_ROOT);
+ long freeSpace = data_partition.getAvailableBlocks() *
+ data_partition.getBlockSize();
+ Log.i(TAG, "Free space on the data partition: " + freeSpace);
+ return freeSpace;
+ }
+}
diff --git a/core/java/android/webkit/gears/package.html b/core/java/android/webkit/gears/package.html
new file mode 100644
index 0000000..db6f78b
--- /dev/null
+++ b/core/java/android/webkit/gears/package.html
@@ -0,0 +1,3 @@
+<body>
+{@hide}
+</body> \ No newline at end of file
diff --git a/core/java/android/webkit/package.html b/core/java/android/webkit/package.html
new file mode 100644
index 0000000..4ed08da
--- /dev/null
+++ b/core/java/android/webkit/package.html
@@ -0,0 +1,7 @@
+<html>
+<body>
+Provides tools for browsing the web.
+<p>The only classes or interfaces in this package intended for use by SDK
+developers are WebView, BroswerCallbackAdapter, BrowserCallback, and CookieManager.
+</body>
+</html> \ No newline at end of file
diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java
new file mode 100644
index 0000000..19b1ce0
--- /dev/null
+++ b/core/java/android/widget/AbsListView.java
@@ -0,0 +1,3196 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.TransitionDrawable;
+import android.os.Debug;
+import android.os.Handler;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewDebug;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.WindowManagerImpl;
+import android.view.ContextMenu.ContextMenuInfo;
+
+import com.android.internal.R;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Common code shared between ListView and GridView
+ *
+ * @attr ref android.R.styleable#AbsListView_listSelector
+ * @attr ref android.R.styleable#AbsListView_drawSelectorOnTop
+ * @attr ref android.R.styleable#AbsListView_stackFromBottom
+ * @attr ref android.R.styleable#AbsListView_scrollingCache
+ * @attr ref android.R.styleable#AbsListView_textFilterEnabled
+ * @attr ref android.R.styleable#AbsListView_transcriptMode
+ * @attr ref android.R.styleable#AbsListView_cacheColorHint
+ */
+public abstract class AbsListView extends AdapterView<ListAdapter> implements TextWatcher,
+ ViewTreeObserver.OnGlobalLayoutListener, Filter.FilterListener,
+ ViewTreeObserver.OnTouchModeChangeListener {
+
+ /**
+ * Disables the transcript mode.
+ *
+ * @see #setTranscriptMode(int)
+ */
+ public static final int TRANSCRIPT_MODE_DISABLED = 0;
+ /**
+ * The list will automatically scroll to the bottom when a data set change
+ * notification is received and only if the last item is already visible
+ * on screen.
+ *
+ * @see #setTranscriptMode(int)
+ */
+ public static final int TRANSCRIPT_MODE_NORMAL = 1;
+ /**
+ * The list will automatically scroll to the bottom, no matter what items
+ * are currently visible.
+ *
+ * @see #setTranscriptMode(int)
+ */
+ public static final int TRANSCRIPT_MODE_ALWAYS_SCROLL = 2;
+
+ /**
+ * Indicates that we are not in the middle of a touch gesture
+ */
+ static final int TOUCH_MODE_REST = -1;
+
+ /**
+ * Indicates we just received the touch event and we are waiting to see if the it is a tap or a
+ * scroll gesture.
+ */
+ static final int TOUCH_MODE_DOWN = 0;
+
+ /**
+ * Indicates the touch has been recognized as a tap and we are now waiting to see if the touch
+ * is a longpress
+ */
+ static final int TOUCH_MODE_TAP = 1;
+
+ /**
+ * Indicates we have waited for everything we can wait for, but the user's finger is still down
+ */
+ static final int TOUCH_MODE_DONE_WAITING = 2;
+
+ /**
+ * Indicates the touch gesture is a scroll
+ */
+ static final int TOUCH_MODE_SCROLL = 3;
+
+ /**
+ * Indicates the view is in the process of being flung
+ */
+ static final int TOUCH_MODE_FLING = 4;
+
+ /**
+ * Regular layout - usually an unsolicited layout from the view system
+ */
+ static final int LAYOUT_NORMAL = 0;
+
+ /**
+ * Show the first item
+ */
+ static final int LAYOUT_FORCE_TOP = 1;
+
+ /**
+ * Force the selected item to be on somewhere on the screen
+ */
+ static final int LAYOUT_SET_SELECTION = 2;
+
+ /**
+ * Show the last item
+ */
+ static final int LAYOUT_FORCE_BOTTOM = 3;
+
+ /**
+ * Make a mSelectedItem appear in a specific location and build the rest of
+ * the views from there. The top is specified by mSpecificTop.
+ */
+ static final int LAYOUT_SPECIFIC = 4;
+
+ /**
+ * Layout to sync as a result of a data change. Restore mSyncPosition to have its top
+ * at mSpecificTop
+ */
+ static final int LAYOUT_SYNC = 5;
+
+ /**
+ * Layout as a result of using the navigation keys
+ */
+ static final int LAYOUT_MOVE_SELECTION = 6;
+
+ /**
+ * Controls how the next layout will happen
+ */
+ int mLayoutMode = LAYOUT_NORMAL;
+
+ /**
+ * Should be used by subclasses to listen to changes in the dataset
+ */
+ AdapterDataSetObserver mDataSetObserver;
+
+ /**
+ * The adapter containing the data to be displayed by this view
+ */
+ ListAdapter mAdapter;
+
+ /**
+ * Indicates whether the list selector should be drawn on top of the children or behind
+ */
+ boolean mDrawSelectorOnTop = false;
+
+ /**
+ * The drawable used to draw the selector
+ */
+ Drawable mSelector;
+
+ /**
+ * Defines the selector's location and dimension at drawing time
+ */
+ Rect mSelectorRect = new Rect();
+
+ /**
+ * The data set used to store unused views that should be reused during the next layout
+ * to avoid creating new ones
+ */
+ final RecycleBin mRecycler = new RecycleBin();
+
+ /**
+ * The selection's left padding
+ */
+ int mSelectionLeftPadding = 0;
+
+ /**
+ * The selection's top padding
+ */
+ int mSelectionTopPadding = 0;
+
+ /**
+ * The selection's right padding
+ */
+ int mSelectionRightPadding = 0;
+
+ /**
+ * The selection's bottom padding
+ */
+ int mSelectionBottomPadding = 0;
+
+ /**
+ * This view's padding
+ */
+ Rect mListPadding = new Rect();
+
+ /**
+ * Subclasses must retain their measure spec from onMeasure() into this member
+ */
+ int mWidthMeasureSpec = 0;
+
+ /**
+ * The top scroll indicator
+ */
+ View mScrollUp;
+
+ /**
+ * The down scroll indicator
+ */
+ View mScrollDown;
+
+ /**
+ * When the view is scrolling, this flag is set to true to indicate subclasses that
+ * the drawing cache was enabled on the children
+ */
+ boolean mCachingStarted;
+
+ /**
+ * The position of the view that received the down motion event
+ */
+ int mMotionPosition;
+
+ /**
+ * The offset to the top of the mMotionPosition view when the down motion event was received
+ */
+ int mMotionViewOriginalTop;
+
+ /**
+ * The desired offset to the top of the mMotionPosition view after a scroll
+ */
+ int mMotionViewNewTop;
+
+ /**
+ * The X value associated with the the down motion event
+ */
+ int mMotionX;
+
+ /**
+ * The Y value associated with the the down motion event
+ */
+ int mMotionY;
+
+ /**
+ * One of TOUCH_MODE_REST, TOUCH_MODE_DOWN, TOUCH_MODE_TAP, TOUCH_MODE_SCROLL, or
+ * TOUCH_MODE_DONE_WAITING
+ */
+ int mTouchMode = TOUCH_MODE_REST;
+
+ /**
+ * Y value from on the previous motion event (if any)
+ */
+ int mLastY;
+
+ /**
+ * How far the finger moved before we started scrolling
+ */
+ int mMotionCorrection;
+
+ /**
+ * Determines speed during touch scrolling
+ */
+ private VelocityTracker mVelocityTracker;
+
+ /**
+ * Handles one frame of a fling
+ */
+ private FlingRunnable mFlingRunnable;
+
+ /**
+ * The offset in pixels form the top of the AdapterView to the top
+ * of the currently selected view. Used to save and restore state.
+ */
+ int mSelectedTop = 0;
+
+ /**
+ * Indicates whether the list is stacked from the bottom edge or
+ * the top edge.
+ */
+ boolean mStackFromBottom;
+
+ /**
+ * When set to true, the list automatically discards the children's
+ * bitmap cache after scrolling.
+ */
+ boolean mScrollingCacheEnabled;
+
+ /**
+ * Optional callback to notify client when scroll position has changed
+ */
+ private OnScrollListener mOnScrollListener;
+
+ /**
+ * Keeps track of our accessory window
+ */
+ PopupWindow mPopup;
+
+ /**
+ * Used with type filter window
+ */
+ EditText mTextFilter;
+
+ /**
+ * Indicates that this view supports filtering
+ */
+ private boolean mTextFilterEnabled;
+
+ /**
+ * Indicates that this view is currently displaying a filtered view of the data
+ */
+ private boolean mFiltered;
+
+ /**
+ * Rectangle used for hit testing children
+ */
+ private Rect mTouchFrame;
+
+ /**
+ * The position to resurrect the selected position to.
+ */
+ int mResurrectToPosition = INVALID_POSITION;
+
+ private ContextMenuInfo mContextMenuInfo = null;
+
+ /**
+ * Used to request a layout when we changed touch mode
+ */
+ private static final int TOUCH_MODE_UNKNOWN = -1;
+ private static final int TOUCH_MODE_ON = 0;
+ private static final int TOUCH_MODE_OFF = 1;
+
+ private int mLastTouchMode = TOUCH_MODE_UNKNOWN;
+
+ // TODO: REMOVE WHEN WE'RE DONE WITH PROFILING
+ private static final boolean PROFILE_SCROLLING = false;
+ private boolean mScrollProfilingStarted = false;
+
+ private static final boolean PROFILE_FLINGING = false;
+ private boolean mFlingProfilingStarted = false;
+
+ /**
+ * The last CheckForLongPress runnable we posted, if any
+ */
+ private CheckForLongPress mPendingCheckForLongPress;
+
+ /**
+ * The last CheckForTap runnable we posted, if any
+ */
+ private Runnable mPendingCheckForTap;
+
+ /**
+ * The last CheckForKeyLongPress runnable we posted, if any
+ */
+ private CheckForKeyLongPress mPendingCheckForKeyLongPress;
+
+ /**
+ * Acts upon click
+ */
+ private AbsListView.PerformClick mPerformClick;
+
+ /**
+ * This view is in transcript mode -- it shows the bottom of the list when the data
+ * changes
+ */
+ private int mTranscriptMode;
+
+ /**
+ * Indicates that this list is always drawn on top of a solid, single-color, opaque
+ * background
+ */
+ private int mCacheColorHint;
+
+ /**
+ * The select child's view (from the adapter's getView) is enabled.
+ */
+ private boolean mIsChildViewEnabled;
+
+ /**
+ * The last scroll state reported to clients through {@link OnScrollListener}.
+ */
+ private int mLastScrollState = OnScrollListener.SCROLL_STATE_IDLE;
+
+ /**
+ * Interface definition for a callback to be invoked when the list or grid
+ * has been scrolled.
+ */
+ public interface OnScrollListener {
+
+ /**
+ * The view is not scrolling. Note navigating the list using the trackball counts as
+ * being in the idle state since these transitions are not animated.
+ */
+ public static int SCROLL_STATE_IDLE = 0;
+
+ /**
+ * The user is scrolling using touch, and their finger is still on the screen
+ */
+ public static int SCROLL_STATE_TOUCH_SCROLL = 1;
+
+ /**
+ * The user had previously been scrolling using touch and had performed a fling. The
+ * animation is now coasting to a stop
+ */
+ public static int SCROLL_STATE_FLING = 2;
+
+ /**
+ * Callback method to be invoked while the list view or grid view is being scrolled. If the
+ * view is being scrolled, this method will be called before the next frame of the scroll is
+ * rendered. In particular, it will be called before any calls to
+ * {@link Adapter#getView(int, View, ViewGroup)}.
+ *
+ * @param view The view whose scroll state is being reported
+ *
+ * @param scrollState The current scroll state. One of {@link #SCROLL_STATE_IDLE},
+ * {@link #SCROLL_STATE_TOUCH_SCROLL} or {@link #SCROLL_STATE_IDLE}.
+ */
+ public void onScrollStateChanged(AbsListView view, int scrollState);
+
+ /**
+ * Callback method to be invoked when the list or grid has been scrolled. This will be
+ * called after the scroll has completed
+ * @param view The view whose scroll state is being reported
+ * @param firstVisibleItem the index of the first visible cell (ignore if
+ * visibleItemCount == 0)
+ * @param visibleItemCount the number of visible cells
+ * @param totalItemCount the number of items in the list adaptor
+ */
+ public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
+ int totalItemCount);
+ }
+
+ public AbsListView(Context context) {
+ super(context);
+ initAbsListView();
+
+ setVerticalScrollBarEnabled(true);
+ TypedArray a = context.obtainStyledAttributes(R.styleable.View);
+ initializeScrollbars(a);
+ a.recycle();
+ }
+
+ public AbsListView(Context context, AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.absListViewStyle);
+ }
+
+ public AbsListView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ initAbsListView();
+
+ TypedArray a = context.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.AbsListView, defStyle, 0);
+
+ Drawable d = a.getDrawable(com.android.internal.R.styleable.AbsListView_listSelector);
+ if (d != null) {
+ setSelector(d);
+ }
+
+ mDrawSelectorOnTop = a.getBoolean(
+ com.android.internal.R.styleable.AbsListView_drawSelectorOnTop, false);
+
+ boolean stackFromBottom = a.getBoolean(R.styleable.AbsListView_stackFromBottom, false);
+ setStackFromBottom(stackFromBottom);
+
+ boolean scrollingCacheEnabled = a.getBoolean(R.styleable.AbsListView_scrollingCache, true);
+ setScrollingCacheEnabled(scrollingCacheEnabled);
+
+ boolean useTextFilter = a.getBoolean(R.styleable.AbsListView_textFilterEnabled, false);
+ setTextFilterEnabled(useTextFilter);
+
+ int transcriptMode = a.getInt(R.styleable.AbsListView_transcriptMode,
+ TRANSCRIPT_MODE_DISABLED);
+ setTranscriptMode(transcriptMode);
+
+ int color = a.getColor(R.styleable.AbsListView_cacheColorHint, 0);
+ setCacheColorHint(color);
+
+ a.recycle();
+ }
+
+ /**
+ * Set the listener that will receive notifications every time the list scrolls.
+ *
+ * @param l the scroll listener
+ */
+ public void setOnScrollListener(OnScrollListener l) {
+ mOnScrollListener = l;
+ invokeOnItemScrollListener();
+ }
+
+ /**
+ * Notify our scroll listener (if there is one) of a change in scroll state
+ */
+ void invokeOnItemScrollListener() {
+ if (mOnScrollListener != null) {
+ mOnScrollListener.onScroll(this, mFirstPosition, getChildCount(), mItemCount);
+ }
+ }
+
+ /**
+ * 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.
+ *
+ * @return true if the scrolling cache is enabled, false otherwise
+ *
+ * @see #setScrollingCacheEnabled(boolean)
+ * @see View#setDrawingCacheEnabled(boolean)
+ */
+ public boolean isScrollingCacheEnabled() {
+ return mScrollingCacheEnabled;
+ }
+
+ /**
+ * Enables or disables the children's drawing cache during a scroll.
+ * By default, the drawing cache is enabled but this will use more memory.
+ *
+ * When the scrolling cache is enabled, the caches are kept after the
+ * first scrolling. You can manually clear the cache by calling
+ * {@link android.view.ViewGroup#setChildrenDrawingCacheEnabled(boolean)}.
+ *
+ * @param enabled true to enable the scroll cache, false otherwise
+ *
+ * @see #isScrollingCacheEnabled()
+ * @see View#setDrawingCacheEnabled(boolean)
+ */
+ public void setScrollingCacheEnabled(boolean enabled) {
+ if (mScrollingCacheEnabled && !enabled) {
+ clearScrollingCache();
+ }
+ mScrollingCacheEnabled = enabled;
+ }
+
+ /**
+ * Enables or disables the type filter window. If enabled, typing when
+ * this view has focus will filter the children to match the users input.
+ * Note that the {@link Adapter} used by this view must implement the
+ * {@link Filterable} interface.
+ *
+ * @param textFilterEnabled true to enable type filtering, false otherwise
+ *
+ * @see Filterable
+ */
+ public void setTextFilterEnabled(boolean textFilterEnabled) {
+ mTextFilterEnabled = textFilterEnabled;
+ }
+
+ /**
+ * Indicates whether type filtering is enabled for this view
+ *
+ * @return true if type filtering is enabled, false otherwise
+ *
+ * @see #setTextFilterEnabled(boolean)
+ * @see Filterable
+ */
+ public boolean isTextFilterEnabled() {
+ return mTextFilterEnabled;
+ }
+
+ @Override
+ public void getFocusedRect(Rect r) {
+ View view = getSelectedView();
+ if (view != null) {
+ // the focused rectangle of the selected view offset into the
+ // coordinate space of this view.
+ view.getFocusedRect(r);
+ offsetDescendantRectToMyCoords(view, r);
+ } else {
+ // otherwise, just the norm
+ super.getFocusedRect(r);
+ }
+ }
+
+ private void initAbsListView() {
+ // Setting focusable in touch mode will set the focusable property to true
+ setFocusableInTouchMode(true);
+ setWillNotDraw(false);
+ setAlwaysDrawnWithCacheEnabled(false);
+ setScrollingCacheEnabled(true);
+ }
+
+ private void useDefaultSelector() {
+ setSelector(getResources().getDrawable(com.android.internal.R.drawable.list_selector_background));
+ }
+
+ /**
+ * Indicates whether the content of this view is pinned to, or stacked from,
+ * the bottom edge.
+ *
+ * @return true if the content is stacked from the bottom edge, false otherwise
+ */
+ public boolean isStackFromBottom() {
+ return mStackFromBottom;
+ }
+
+ /**
+ * When stack from bottom is set to true, the list fills its content starting from
+ * the bottom of the view.
+ *
+ * @param stackFromBottom true to pin the view's content to the bottom edge,
+ * false to pin the view's content to the top edge
+ */
+ public void setStackFromBottom(boolean stackFromBottom) {
+ if (mStackFromBottom != stackFromBottom) {
+ mStackFromBottom = stackFromBottom;
+ requestLayoutIfNecessary();
+ }
+ }
+
+ void requestLayoutIfNecessary() {
+ if (getChildCount() > 0) {
+ resetList();
+ requestLayout();
+ invalidate();
+ }
+ }
+
+ static class SavedState extends BaseSavedState {
+ long selectedId;
+ long firstId;
+ int viewTop;
+ int position;
+ int height;
+ String filter;
+
+ /**
+ * Constructor called from {@link AbsListView#onSaveInstanceState()}
+ */
+ SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ /**
+ * Constructor called from {@link #CREATOR}
+ */
+ private SavedState(Parcel in) {
+ super(in);
+ selectedId = in.readLong();
+ firstId = in.readLong();
+ viewTop = in.readInt();
+ position = in.readInt();
+ height = in.readInt();
+ filter = in.readString();
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ super.writeToParcel(out, flags);
+ out.writeLong(selectedId);
+ out.writeLong(firstId);
+ out.writeInt(viewTop);
+ out.writeInt(position);
+ out.writeInt(height);
+ out.writeString(filter);
+ }
+
+ @Override
+ public String toString() {
+ return "AbsListView.SavedState{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " selectedId=" + selectedId
+ + " firstId=" + firstId
+ + " viewTop=" + viewTop
+ + " position=" + position
+ + " height=" + height
+ + " filter=" + filter + "}";
+ }
+
+ public static final Parcelable.Creator<SavedState> CREATOR
+ = new Parcelable.Creator<SavedState>() {
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ /*
+ * This doesn't really make sense as the place to dismiss the
+ * popup, but there don't seem to be any other useful hooks
+ * that happen early enough to keep from getting complaints
+ * about having leaked the window.
+ */
+ dismissPopup();
+
+ Parcelable superState = super.onSaveInstanceState();
+
+ SavedState ss = new SavedState(superState);
+
+ boolean haveChildren = getChildCount() > 0;
+ long selectedId = getSelectedItemId();
+ ss.selectedId = selectedId;
+ ss.height = getHeight();
+
+ if (selectedId >= 0) {
+ // Remember the selection
+ ss.viewTop = mSelectedTop;
+ ss.position = getSelectedItemPosition();
+ ss.firstId = INVALID_POSITION;
+ } else {
+ if (haveChildren) {
+ // Remember the position of the first child
+ View v = getChildAt(0);
+ ss.viewTop = v.getTop();
+ ss.position = mFirstPosition;
+ ss.firstId = mAdapter.getItemId(mFirstPosition);
+ } else {
+ ss.viewTop = 0;
+ ss.firstId = INVALID_POSITION;
+ ss.position = 0;
+ }
+ }
+
+ ss.filter = null;
+ if (mFiltered) {
+ final EditText textFilter = mTextFilter;
+ if (textFilter != null) {
+ Editable filterText = textFilter.getText();
+ if (filterText != null) {
+ ss.filter = filterText.toString();
+ }
+ }
+ }
+
+ return ss;
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ SavedState ss = (SavedState) state;
+
+ super.onRestoreInstanceState(ss.getSuperState());
+ mDataChanged = true;
+
+ mSyncHeight = ss.height;
+
+ if (ss.selectedId >= 0) {
+ mNeedSync = true;
+ mSyncRowId = ss.selectedId;
+ mSyncPosition = ss.position;
+ mSpecificTop = ss.viewTop;
+ mSyncMode = SYNC_SELECTED_POSITION;
+ } else if (ss.firstId >= 0) {
+ setSelectedPositionInt(INVALID_POSITION);
+ // Do this before setting mNeedSync since setNextSelectedPosition looks at mNeedSync
+ setNextSelectedPositionInt(INVALID_POSITION);
+ mNeedSync = true;
+ mSyncRowId = ss.firstId;
+ mSyncPosition = ss.position;
+ mSpecificTop = ss.viewTop;
+ mSyncMode = SYNC_FIRST_POSITION;
+ }
+
+ // Don't restore the type filter window when there is no keyboard
+ int keyboardHidden = getContext().getResources().getConfiguration().keyboardHidden;
+ if (keyboardHidden != Configuration.KEYBOARDHIDDEN_YES) {
+ String filterText = ss.filter;
+ setFilterText(filterText);
+ }
+ requestLayout();
+ }
+
+ /**
+ * Sets the initial value for the text filter.
+ * @param filterText The text to use for the filter.
+ *
+ * @see #setTextFilterEnabled
+ */
+ public void setFilterText(String filterText) {
+ if (mTextFilterEnabled && filterText != null && filterText.length() > 0) {
+ createTextFilter(false);
+ // This is going to call our listener onTextChanged, but we are
+ // not ready to bring up a window yet
+ mTextFilter.setText(filterText);
+ mTextFilter.setSelection(filterText.length());
+ if (mAdapter instanceof Filterable) {
+ Filter f = ((Filterable) mAdapter).getFilter();
+ f.filter(filterText);
+ // Set filtered to true so we will display the filter window when our main
+ // window is ready
+ mFiltered = true;
+ mDataSetObserver.clearSavedState();
+ }
+ }
+ }
+
+ @Override
+ protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
+ super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
+ if (gainFocus && mSelectedPosition < 0 && !isInTouchMode()) {
+ resurrectSelection();
+ }
+ }
+
+ @Override
+ public void requestLayout() {
+ if (!mBlockLayoutRequests && !mInLayout) {
+ super.requestLayout();
+ }
+ }
+
+ /**
+ * The list is empty. Clear everything out.
+ */
+ void resetList() {
+ removeAllViewsInLayout();
+ mFirstPosition = 0;
+ mDataChanged = false;
+ mNeedSync = false;
+ mOldSelectedPosition = INVALID_POSITION;
+ mOldSelectedRowId = INVALID_ROW_ID;
+ setSelectedPositionInt(INVALID_POSITION);
+ setNextSelectedPositionInt(INVALID_POSITION);
+ mSelectedTop = 0;
+ mSelectorRect.setEmpty();
+ invalidate();
+ }
+
+ @Override
+ protected int computeVerticalScrollExtent() {
+ final int count = getChildCount();
+ if (count > 0) {
+ int extent = count * 100;
+
+ View view = getChildAt(0);
+ final int top = view.getTop();
+ int height = view.getHeight();
+ if (height > 0) {
+ extent += (top * 100) / height;
+ }
+
+ view = getChildAt(count - 1);
+ final int bottom = view.getBottom();
+ height = view.getHeight();
+ if (height > 0) {
+ extent -= ((bottom - getHeight()) * 100) / height;
+ }
+
+ return extent;
+ }
+ return 0;
+ }
+
+ @Override
+ protected int computeVerticalScrollOffset() {
+ if (mFirstPosition >= 0 && getChildCount() > 0) {
+ final View view = getChildAt(0);
+ final int top = view.getTop();
+ int height = view.getHeight();
+ if (height > 0) {
+ return Math.max(mFirstPosition * 100 - (top * 100) / height, 0);
+ }
+ }
+ return 0;
+ }
+
+ @Override
+ protected int computeVerticalScrollRange() {
+ return Math.max(mItemCount * 100, 0);
+ }
+
+ @Override
+ protected float getTopFadingEdgeStrength() {
+ final int count = getChildCount();
+ final float fadeEdge = super.getTopFadingEdgeStrength();
+ if (count == 0) {
+ return fadeEdge;
+ } else {
+ if (mFirstPosition > 0) {
+ return 1.0f;
+ }
+
+ final int top = getChildAt(0).getTop();
+ final float fadeLength = (float) getVerticalFadingEdgeLength();
+ return top < mPaddingTop ? (float) -(top - mPaddingTop) / fadeLength : fadeEdge;
+ }
+ }
+
+ @Override
+ protected float getBottomFadingEdgeStrength() {
+ final int count = getChildCount();
+ final float fadeEdge = super.getBottomFadingEdgeStrength();
+ if (count == 0) {
+ return fadeEdge;
+ } else {
+ if (mFirstPosition + count - 1 < mItemCount - 1) {
+ return 1.0f;
+ }
+
+ final int bottom = getChildAt(count - 1).getBottom();
+ final int height = getHeight();
+ final float fadeLength = (float) getVerticalFadingEdgeLength();
+ return bottom > height - mPaddingBottom ?
+ (float) (bottom - height + mPaddingBottom) / fadeLength : fadeEdge;
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ if (mSelector == null) {
+ useDefaultSelector();
+ }
+ final Rect listPadding = mListPadding;
+ listPadding.left = mSelectionLeftPadding + mPaddingLeft;
+ listPadding.top = mSelectionTopPadding + mPaddingTop;
+ listPadding.right = mSelectionRightPadding + mPaddingRight;
+ listPadding.bottom = mSelectionBottomPadding + mPaddingBottom;
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ super.onLayout(changed, l, t, r, b);
+ mInLayout = true;
+ layoutChildren();
+ mInLayout = false;
+ }
+
+ protected void layoutChildren() {
+ }
+
+ void updateScrollIndicators() {
+ if (mScrollUp != null) {
+ boolean canScrollUp;
+ // 0th element is not visible
+ canScrollUp = mFirstPosition > 0;
+
+ // ... Or top of 0th element is not visible
+ if (!canScrollUp) {
+ if (getChildCount() > 0) {
+ View child = getChildAt(0);
+ canScrollUp = child.getTop() < mListPadding.top;
+ }
+ }
+
+ mScrollUp.setVisibility(canScrollUp ? View.VISIBLE : View.INVISIBLE);
+ }
+
+ if (mScrollDown != null) {
+ boolean canScrollDown;
+ int count = getChildCount();
+
+ // Last item is not visible
+ canScrollDown = (mFirstPosition + count) < mItemCount;
+
+ // ... Or bottom of the last element is not visible
+ if (!canScrollDown && count > 0) {
+ View child = getChildAt(count - 1);
+ canScrollDown = child.getBottom() > mBottom - mListPadding.bottom;
+ }
+
+ mScrollDown.setVisibility(canScrollDown ? View.VISIBLE : View.INVISIBLE);
+ }
+ }
+
+ @Override
+ @ViewDebug.ExportedProperty
+ public View getSelectedView() {
+ if (mItemCount > 0 && mSelectedPosition >= 0) {
+ return getChildAt(mSelectedPosition - mFirstPosition);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * List padding is the maximum of the normal view's padding and the padding of the selector.
+ *
+ * @see android.view.View#getPaddingTop()
+ * @see #getSelector()
+ *
+ * @return The top list padding.
+ */
+ public int getListPaddingTop() {
+ return mListPadding.top;
+ }
+
+ /**
+ * List padding is the maximum of the normal view's padding and the padding of the selector.
+ *
+ * @see android.view.View#getPaddingBottom()
+ * @see #getSelector()
+ *
+ * @return The bottom list padding.
+ */
+ public int getListPaddingBottom() {
+ return mListPadding.bottom;
+ }
+
+ /**
+ * List padding is the maximum of the normal view's padding and the padding of the selector.
+ *
+ * @see android.view.View#getPaddingLeft()
+ * @see #getSelector()
+ *
+ * @return The left list padding.
+ */
+ public int getListPaddingLeft() {
+ return mListPadding.left;
+ }
+
+ /**
+ * List padding is the maximum of the normal view's padding and the padding of the selector.
+ *
+ * @see android.view.View#getPaddingRight()
+ * @see #getSelector()
+ *
+ * @return The right list padding.
+ */
+ public int getListPaddingRight() {
+ return mListPadding.right;
+ }
+
+ /**
+ * Get a view and have it show the data associated with the specified
+ * position. This is called when we have already discovered that the view is
+ * not available for reuse in the recycle bin. The only choices left are
+ * converting an old view or making a new one.
+ *
+ * @param position The position to display
+ * @return A view displaying the data associated with the specified position
+ */
+ View obtainView(int position) {
+ View scrapView;
+
+ scrapView = mRecycler.getScrapView(position);
+
+ View child;
+ if (scrapView != null) {
+ if (ViewDebug.TRACE_RECYCLER) {
+ ViewDebug.trace(scrapView, ViewDebug.RecyclerTraceType.RECYCLE_FROM_SCRAP_HEAP,
+ position, -1);
+ }
+
+ child = mAdapter.getView(position, scrapView, this);
+
+ if (ViewDebug.TRACE_RECYCLER) {
+ ViewDebug.trace(child, ViewDebug.RecyclerTraceType.BIND_VIEW,
+ position, getChildCount());
+ }
+
+ if (child != scrapView) {
+ mRecycler.addScrapView(scrapView);
+ if (mCacheColorHint != 0) {
+ child.setDrawingCacheBackgroundColor(mCacheColorHint);
+ }
+ if (ViewDebug.TRACE_RECYCLER) {
+ ViewDebug.trace(scrapView, ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP,
+ position, -1);
+ }
+ }
+ } else {
+ child = mAdapter.getView(position, null, this);
+ if (mCacheColorHint != 0) {
+ child.setDrawingCacheBackgroundColor(mCacheColorHint);
+ }
+ if (ViewDebug.TRACE_RECYCLER) {
+ ViewDebug.trace(child, ViewDebug.RecyclerTraceType.NEW_VIEW,
+ position, getChildCount());
+ }
+ }
+
+ return child;
+ }
+
+ void positionSelector(View sel) {
+ final Rect selectorRect = mSelectorRect;
+ selectorRect.set(sel.getLeft(), sel.getTop(), sel.getRight(), sel.getBottom());
+ positionSelector(selectorRect.left, selectorRect.top, selectorRect.right,
+ selectorRect.bottom);
+
+ final boolean isChildViewEnabled = mIsChildViewEnabled;
+ if (sel.isEnabled() != isChildViewEnabled) {
+ mIsChildViewEnabled = !isChildViewEnabled;
+ refreshDrawableState();
+ }
+ }
+
+ private void positionSelector(int l, int t, int r, int b) {
+ mSelectorRect.set(l - mSelectionLeftPadding, t - mSelectionTopPadding, r
+ + mSelectionRightPadding, b + mSelectionBottomPadding);
+ }
+
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ int saveCount = 0;
+ final boolean clipToPadding = (mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK;
+ if (clipToPadding) {
+ saveCount = canvas.save();
+ final int scrollX = mScrollX;
+ final int scrollY = mScrollY;
+ canvas.clipRect(scrollX + mPaddingLeft, scrollY + mPaddingTop,
+ scrollX + mRight - mLeft - mPaddingRight,
+ scrollY + mBottom - mTop - mPaddingBottom);
+ mGroupFlags &= ~CLIP_TO_PADDING_MASK;
+ }
+
+ final boolean drawSelectorOnTop = mDrawSelectorOnTop;
+ if (!drawSelectorOnTop) {
+ drawSelector(canvas);
+ }
+
+ super.dispatchDraw(canvas);
+
+ if (drawSelectorOnTop) {
+ drawSelector(canvas);
+ }
+
+ if (clipToPadding) {
+ canvas.restoreToCount(saveCount);
+ mGroupFlags |= CLIP_TO_PADDING_MASK;
+ }
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ if (getChildCount() > 0) {
+ mDataChanged = true;
+ rememberSyncState();
+ }
+ }
+
+ /**
+ * @return True if the current touch mode requires that we draw the selector in the pressed
+ * state.
+ */
+ boolean touchModeDrawsInPressedState() {
+ // FIXME use isPressed for this
+ switch (mTouchMode) {
+ case TOUCH_MODE_TAP:
+ case TOUCH_MODE_DONE_WAITING:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Indicates whether this view is in a state where the selector should be drawn. This will
+ * happen if we have focus but are not in touch mode, or we are in the middle of displaying
+ * the pressed state for an item.
+ *
+ * @return True if the selector should be shown
+ */
+ boolean shouldShowSelector() {
+ return (hasFocus() && !isInTouchMode()) || touchModeDrawsInPressedState();
+ }
+
+ private void drawSelector(Canvas canvas) {
+ if (shouldShowSelector() && mSelectorRect != null && !mSelectorRect.isEmpty()) {
+ final Drawable selector = mSelector;
+ selector.setBounds(mSelectorRect);
+ selector.draw(canvas);
+ }
+ }
+
+ /**
+ * Controls whether the selection highlight drawable should be drawn on top of the item or
+ * behind it.
+ *
+ * @param onTop If true, the selector will be drawn on the item it is highlighting. The default
+ * is false.
+ *
+ * @attr ref android.R.styleable#AbsListView_drawSelectorOnTop
+ */
+ public void setDrawSelectorOnTop(boolean onTop) {
+ mDrawSelectorOnTop = onTop;
+ }
+
+ /**
+ * Set a Drawable that should be used to highlight the currently selected item.
+ *
+ * @param resID A Drawable resource to use as the selection highlight.
+ *
+ * @attr ref android.R.styleable#AbsListView_listSelector
+ */
+ public void setSelector(int resID) {
+ setSelector(getResources().getDrawable(resID));
+ }
+
+ public void setSelector(Drawable sel) {
+ if (mSelector != null) {
+ mSelector.setCallback(null);
+ unscheduleDrawable(mSelector);
+ }
+ mSelector = sel;
+ Rect padding = new Rect();
+ sel.getPadding(padding);
+ mSelectionLeftPadding = padding.left;
+ mSelectionTopPadding = padding.top;
+ mSelectionRightPadding = padding.right;
+ mSelectionBottomPadding = padding.bottom;
+ sel.setCallback(this);
+ sel.setState(getDrawableState());
+ }
+
+ /**
+ * Returns the selector {@link android.graphics.drawable.Drawable} that is used to draw the
+ * selection in the list.
+ *
+ * @return the drawable used to display the selector
+ */
+ public Drawable getSelector() {
+ return mSelector;
+ }
+
+ /**
+ * Sets the selector state to "pressed" and posts a CheckForKeyLongPress to see if
+ * this is a long press.
+ */
+ void keyPressed() {
+ Drawable selector = mSelector;
+ Rect selectorRect = mSelectorRect;
+ if (selector != null && (isFocused() || touchModeDrawsInPressedState())
+ && selectorRect != null && !selectorRect.isEmpty()) {
+ setPressed(true);
+ final boolean longClickable = isLongClickable();
+ Drawable d = selector.getCurrent();
+ if (d != null && d instanceof TransitionDrawable) {
+ if (longClickable) {
+ ((TransitionDrawable) d).startTransition(ViewConfiguration
+ .getLongPressTimeout());
+ } else {
+ ((TransitionDrawable) d).resetTransition();
+ }
+ }
+ if (longClickable && !mDataChanged) {
+ if (mPendingCheckForKeyLongPress == null) {
+ mPendingCheckForKeyLongPress = new CheckForKeyLongPress();
+ }
+ mPendingCheckForKeyLongPress.rememberWindowAttachCount();
+ postDelayed(mPendingCheckForKeyLongPress, ViewConfiguration.getLongPressTimeout());
+ }
+ }
+ }
+
+ public void setScrollIndicators(View up, View down) {
+ mScrollUp = up;
+ mScrollDown = down;
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+ if (mSelector != null) {
+ mSelector.setState(getDrawableState());
+ }
+ }
+
+ @Override
+ protected int[] onCreateDrawableState(int extraSpace) {
+ // If the child view is enabled then do the default behavior.
+ if (mIsChildViewEnabled) {
+ // Common case
+ return super.onCreateDrawableState(extraSpace);
+ }
+
+ // The selector uses this View's drawable state. The selected child view
+ // is disabled, so we need to remove the enabled state from the drawable
+ // states.
+ final int enabledState = ENABLED_STATE_SET[0];
+
+ // If we don't have any extra space, it will return one of the static state arrays,
+ // and clearing the enabled state on those arrays is a bad thing! If we specify
+ // we need extra space, it will create+copy into a new array that safely mutable.
+ int[] state = super.onCreateDrawableState(extraSpace + 1);
+ int enabledPos = -1;
+ for (int i = state.length - 1; i >= 0; i--) {
+ if (state[i] == enabledState) {
+ enabledPos = i;
+ break;
+ }
+ }
+
+ // Remove the enabled state
+ if (enabledPos >= 0) {
+ System.arraycopy(state, enabledPos + 1, state, enabledPos,
+ state.length - enabledPos - 1);
+ }
+
+ return state;
+ }
+
+ @Override
+ public boolean verifyDrawable(Drawable dr) {
+ return mSelector == dr || super.verifyDrawable(dr);
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ final ViewTreeObserver treeObserver = getViewTreeObserver();
+ if (treeObserver != null) {
+ treeObserver.addOnTouchModeChangeListener(this);
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+
+ final ViewTreeObserver treeObserver = getViewTreeObserver();
+ if (treeObserver != null) {
+ treeObserver.removeOnTouchModeChangeListener(this);
+ }
+ }
+
+ @Override
+ public void onWindowFocusChanged(boolean hasWindowFocus) {
+ super.onWindowFocusChanged(hasWindowFocus);
+
+ final int touchMode = isInTouchMode() ? TOUCH_MODE_ON : TOUCH_MODE_OFF;
+
+ if (!hasWindowFocus) {
+ setChildrenDrawingCacheEnabled(false);
+ removeCallbacks(mFlingRunnable);
+ // Always hide the type filter
+ dismissPopup();
+
+ if (touchMode == TOUCH_MODE_OFF) {
+ // Remember the last selected element
+ mResurrectToPosition = mSelectedPosition;
+ }
+ } else {
+ if (mFiltered) {
+ // Show the type filter only if a filter is in effect
+ showPopup();
+ }
+
+ // If we changed touch mode since the last time we had focus
+ if (touchMode != mLastTouchMode && mLastTouchMode != TOUCH_MODE_UNKNOWN) {
+ // If we come back in trackball mode, we bring the selection back
+ if (touchMode == TOUCH_MODE_OFF) {
+ // This will trigger a layout
+ resurrectSelection();
+
+ // If we come back in touch mode, then we want to hide the selector
+ } else {
+ hideSelector();
+ mLayoutMode = LAYOUT_NORMAL;
+ layoutChildren();
+ }
+ }
+ }
+
+ mLastTouchMode = touchMode;
+ }
+
+ /**
+ * Creates the ContextMenuInfo returned from {@link #getContextMenuInfo()}. This
+ * methods knows the view, position and ID of the item that received the
+ * long press.
+ *
+ * @param view The view that received the long press.
+ * @param position The position of the item that received the long press.
+ * @param id The ID of the item that received the long press.
+ * @return The extra information that should be returned by
+ * {@link #getContextMenuInfo()}.
+ */
+ ContextMenuInfo createContextMenuInfo(View view, int position, long id) {
+ return new AdapterContextMenuInfo(view, position, id);
+ }
+
+ /**
+ * A base class for Runnables that will check that their view is still attached to
+ * the original window as when the Runnable was created.
+ *
+ */
+ private class WindowRunnnable {
+ private int mOriginalAttachCount;
+
+ public void rememberWindowAttachCount() {
+ mOriginalAttachCount = getWindowAttachCount();
+ }
+
+ public boolean sameWindow() {
+ return hasWindowFocus() && getWindowAttachCount() == mOriginalAttachCount;
+ }
+ }
+
+ private class PerformClick extends WindowRunnnable implements Runnable {
+ View mChild;
+ int mClickMotionPosition;
+
+ public void run() {
+ // The data has changed since we posted this action in the event queue,
+ // bail out before bad things happen
+ if (mDataChanged) return;
+
+ if (mAdapter != null && mItemCount > 0 &&
+ mClickMotionPosition < mAdapter.getCount() && sameWindow()) {
+ performItemClick(mChild, mClickMotionPosition, getAdapter().getItemId(
+ mClickMotionPosition));
+ }
+ }
+ }
+
+ private class CheckForLongPress extends WindowRunnnable implements Runnable {
+ public void run() {
+ final int motionPosition = mMotionPosition;
+ final View child = getChildAt(motionPosition - mFirstPosition);
+ if (child != null) {
+ final int longPressPosition = mMotionPosition;
+ final long longPressId = mAdapter.getItemId(mMotionPosition);
+
+ boolean handled = false;
+ if (sameWindow() && !mDataChanged) {
+ handled = performLongPress(child, longPressPosition, longPressId);
+ }
+ if (handled) {
+ mTouchMode = TOUCH_MODE_REST;
+ setPressed(false);
+ child.setPressed(false);
+ } else {
+ mTouchMode = TOUCH_MODE_DONE_WAITING;
+ }
+
+ }
+ }
+ }
+
+ private class CheckForKeyLongPress extends WindowRunnnable implements Runnable {
+ public void run() {
+ if (isPressed() && mSelectedPosition >= 0) {
+ int index = mSelectedPosition - mFirstPosition;
+ View v = getChildAt(index);
+
+ if (!mDataChanged) {
+ boolean handled = false;
+ if (sameWindow()) {
+ handled = performLongPress(v, mSelectedPosition, mSelectedRowId);
+ }
+ if (handled) {
+ setPressed(false);
+ v.setPressed(false);
+ }
+ } else {
+ setPressed(false);
+ if (v != null) v.setPressed(false);
+ }
+ }
+ }
+ }
+
+ private boolean performLongPress(final View child,
+ final int longPressPosition, final long longPressId) {
+ boolean handled = false;
+
+ if (mOnItemLongClickListener != null) {
+ handled = mOnItemLongClickListener.onItemLongClick(AbsListView.this, child,
+ longPressPosition, longPressId);
+ }
+ if (!handled) {
+ mContextMenuInfo = createContextMenuInfo(child, longPressPosition, longPressId);
+ handled = super.showContextMenuForChild(AbsListView.this);
+ }
+ return handled;
+ }
+
+ @Override
+ protected ContextMenuInfo getContextMenuInfo() {
+ return mContextMenuInfo;
+ }
+
+ @Override
+ public boolean showContextMenuForChild(View originalView) {
+ final int longPressPosition = getPositionForView(originalView);
+ if (longPressPosition >= 0) {
+ final long longPressId = mAdapter.getItemId(longPressPosition);
+ boolean handled = false;
+
+ if (mOnItemLongClickListener != null) {
+ handled = mOnItemLongClickListener.onItemLongClick(AbsListView.this, originalView,
+ longPressPosition, longPressId);
+ }
+ if (!handled) {
+ mContextMenuInfo = createContextMenuInfo(
+ getChildAt(longPressPosition - mFirstPosition),
+ longPressPosition, longPressId);
+ handled = super.showContextMenuForChild(originalView);
+ }
+
+ return handled;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ case KeyEvent.KEYCODE_ENTER:
+ if (isPressed() && mSelectedPosition >= 0 && mAdapter != null &&
+ mSelectedPosition < mAdapter.getCount()) {
+ final int index = mSelectedPosition - mFirstPosition;
+ performItemClick(getChildAt(index), mSelectedPosition, mSelectedRowId);
+ setPressed(false);
+ return true;
+ }
+ }
+ return super.onKeyUp(keyCode, event);
+ }
+
+ @Override
+ protected void dispatchSetPressed(boolean pressed) {
+ // Don't dispatch setPressed to our children. We call setPressed on ourselves to
+ // get the selector in the right state, but we don't want to press each child.
+ }
+
+ /**
+ * Maps a point to a position in the list.
+ *
+ * @param x X in local coordinate
+ * @param y Y in local coordinate
+ * @return The position of the item which contains the specified point, or
+ * {@link #INVALID_POSITION} if the point does not intersect an item.
+ */
+ public int pointToPosition(int x, int y) {
+ Rect frame = mTouchFrame;
+ if (frame == null) {
+ mTouchFrame = new Rect();
+ frame = mTouchFrame;
+ }
+
+ final int count = getChildCount();
+ for (int i = count - 1; i >= 0; i--) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() == View.VISIBLE) {
+ child.getHitRect(frame);
+ if (frame.contains(x, y)) {
+ return mFirstPosition + i;
+ }
+ }
+ }
+ return INVALID_POSITION;
+ }
+
+
+ /**
+ * Maps a point to a the rowId of the item which intersects that point.
+ *
+ * @param x X in local coordinate
+ * @param y Y in local coordinate
+ * @return The rowId of the item which contains the specified point, or {@link #INVALID_ROW_ID}
+ * if the point does not intersect an item.
+ */
+ public long pointToRowId(int x, int y) {
+ int position = pointToPosition(x, y);
+ if (position >= 0) {
+ return mAdapter.getItemId(position);
+ }
+ return INVALID_ROW_ID;
+ }
+
+ final class CheckForTap implements Runnable {
+ public void run() {
+ if (mTouchMode == TOUCH_MODE_DOWN) {
+ mTouchMode = TOUCH_MODE_TAP;
+ final View child = getChildAt(mMotionPosition - mFirstPosition);
+ if (child != null && !child.hasFocusable()) {
+ mLayoutMode = LAYOUT_NORMAL;
+
+ if (!mDataChanged) {
+ layoutChildren();
+ child.setPressed(true);
+ positionSelector(child);
+ setPressed(true);
+
+ final int longPressTimeout = ViewConfiguration.getLongPressTimeout();
+ final boolean longClickable = isLongClickable();
+
+ if (mSelector != null) {
+ Drawable d = mSelector.getCurrent();
+ if (d != null && d instanceof TransitionDrawable) {
+ if (longClickable) {
+ ((TransitionDrawable) d).startTransition(longPressTimeout);
+ } else {
+ ((TransitionDrawable) d).resetTransition();
+ }
+ }
+ }
+
+ if (longClickable) {
+ if (mPendingCheckForLongPress == null) {
+ mPendingCheckForLongPress = new CheckForLongPress();
+ }
+ mPendingCheckForLongPress.rememberWindowAttachCount();
+ postDelayed(mPendingCheckForLongPress, longPressTimeout);
+ } else {
+ mTouchMode = TOUCH_MODE_DONE_WAITING;
+ }
+ } else {
+ mTouchMode = TOUCH_MODE_DONE_WAITING;
+ }
+ }
+ }
+ }
+ }
+
+ private boolean startScrollIfNeeded(int deltaY) {
+ // Check if we have moved far enough that it looks more like a
+ // scroll than a tap
+ final int distance = Math.abs(deltaY);
+ int touchSlop = ViewConfiguration.getTouchSlop();
+ if (distance > touchSlop) {
+ createScrollingCache();
+ mTouchMode = TOUCH_MODE_SCROLL;
+ mMotionCorrection = deltaY;
+ final Handler handler = getHandler();
+ // Handler should not be null unless the AbsListView is not attached to a
+ // window, which would make it very hard to scroll it... but the monkeys
+ // say it's possible.
+ if (handler != null) {
+ handler.removeCallbacks(mPendingCheckForLongPress);
+ }
+ setPressed(false);
+ View motionView = this.getChildAt(mMotionPosition - mFirstPosition);
+ if (motionView != null) {
+ motionView.setPressed(false);
+ }
+ reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
+ // Time to start stealing events! Once we've stolen them, don't let anyone
+ // steal from us
+ requestDisallowInterceptTouchEvent(true);
+ return true;
+ }
+
+ return false;
+ }
+
+ public void onTouchModeChanged(boolean isInTouchMode) {
+ if (isInTouchMode) {
+ // Get rid of the selection when we enter touch mode
+ hideSelector();
+ // Layout, but only if we already have done so previously.
+ // (Otherwise may clobber a LAYOUT_SYNC layout that was requested to restore
+ // state.)
+ if (getHeight() > 0 && getChildCount() > 0) {
+ // We do not lose focus initiating a touch (since AbsListView is focusable in
+ // touch mode). Force an initial layout to get rid of the selection.
+ mLayoutMode = LAYOUT_NORMAL;
+ layoutChildren();
+ }
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ final int action = ev.getAction();
+ final int x = (int) ev.getX();
+ final int y = (int) ev.getY();
+
+ View v;
+ int deltaY;
+
+ if (mVelocityTracker == null) {
+ mVelocityTracker = VelocityTracker.obtain();
+ }
+ mVelocityTracker.addMovement(ev);
+
+ switch (action) {
+ case MotionEvent.ACTION_DOWN: {
+ int motionPosition = pointToPosition(x, y);
+ if ((mTouchMode != TOUCH_MODE_FLING) && (motionPosition >= 0)
+ && (getAdapter().isEnabled(motionPosition))) {
+ // User clicked on an actual view (and was not stopping a fling). It might be a
+ // click or a scroll. Assume it is a click until proven otherwise
+ mTouchMode = TOUCH_MODE_DOWN;
+ // FIXME Debounce
+ if (mPendingCheckForTap == null) {
+ mPendingCheckForTap = new CheckForTap();
+ }
+ postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
+ } else {
+ if (ev.getEdgeFlags() != 0 && motionPosition < 0) {
+ // If we couldn't find a view to click on, but the down event was touching
+ // the edge, we will bail out and try again. This allows the edge correcting
+ // code in ViewRoot to try to find a nearby view to select
+ return false;
+ }
+ // User clicked on whitespace, or stopped a fling. It is a scroll.
+ createScrollingCache();
+ mTouchMode = TOUCH_MODE_SCROLL;
+ motionPosition = findMotionRow(y);
+ reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
+ }
+
+ if (motionPosition >= 0) {
+ // Remember where the motion event started
+ v = getChildAt(motionPosition - mFirstPosition);
+ mMotionViewOriginalTop = v.getTop();
+ mMotionX = x;
+ mMotionY = y;
+ mMotionPosition = motionPosition;
+ }
+ mLastY = Integer.MIN_VALUE;
+ break;
+ }
+
+ case MotionEvent.ACTION_MOVE: {
+ deltaY = y - mMotionY;
+ switch (mTouchMode) {
+ case TOUCH_MODE_DOWN:
+ case TOUCH_MODE_TAP:
+ case TOUCH_MODE_DONE_WAITING:
+ // Check if we have moved far enough that it looks more like a
+ // scroll than a tap
+ startScrollIfNeeded(deltaY);
+ break;
+ case TOUCH_MODE_SCROLL:
+ if (PROFILE_SCROLLING) {
+ if (!mScrollProfilingStarted) {
+ Debug.startMethodTracing("AbsListViewScroll");
+ mScrollProfilingStarted = true;
+ }
+ }
+
+ if (y != mLastY) {
+ deltaY -= mMotionCorrection;
+ int incrementalDeltaY = mLastY != Integer.MIN_VALUE ? y - mLastY : deltaY;
+ trackMotionScroll(deltaY, incrementalDeltaY);
+
+ // Check to see if we have bumped into the scroll limit
+ View motionView = this.getChildAt(mMotionPosition - mFirstPosition);
+ if (motionView != null) {
+ // Check if the top of the motion view is where it is
+ // supposed to be
+ if (motionView.getTop() != mMotionViewNewTop) {
+ // We did not scroll the full amount. Treat this essentially like the
+ // start of a new touch scroll
+ final int motionPosition = findMotionRow(y);
+
+ mMotionCorrection = 0;
+ motionView = getChildAt(motionPosition - mFirstPosition);
+ mMotionViewOriginalTop = motionView.getTop();
+ mMotionY = y;
+ mMotionPosition = motionPosition;
+ }
+ }
+ mLastY = y;
+ }
+ break;
+ }
+
+ break;
+ }
+
+ case MotionEvent.ACTION_UP: {
+ switch (mTouchMode) {
+ case TOUCH_MODE_DOWN:
+ case TOUCH_MODE_TAP:
+ case TOUCH_MODE_DONE_WAITING:
+ final int motionPosition = mMotionPosition;
+ final View child = getChildAt(motionPosition - mFirstPosition);
+ if (child != null && !child.hasFocusable()) {
+ if (mTouchMode != TOUCH_MODE_DOWN) {
+ child.setPressed(false);
+ }
+
+ if (mPerformClick == null) {
+ mPerformClick = new PerformClick();
+ }
+
+ final AbsListView.PerformClick performClick = mPerformClick;
+ performClick.mChild = child;
+ performClick.mClickMotionPosition = motionPosition;
+ performClick.rememberWindowAttachCount();
+
+ mResurrectToPosition = motionPosition;
+
+ if (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP) {
+ final Handler handler = getHandler();
+ if (handler != null) {
+ handler.removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ?
+ mPendingCheckForTap : mPendingCheckForLongPress);
+ }
+ mLayoutMode = LAYOUT_NORMAL;
+ mTouchMode = TOUCH_MODE_TAP;
+ if (!mDataChanged) {
+ setSelectedPositionInt(mMotionPosition);
+ layoutChildren();
+ child.setPressed(true);
+ positionSelector(child);
+ setPressed(true);
+ if (mSelector != null) {
+ Drawable d = mSelector.getCurrent();
+ if (d != null && d instanceof TransitionDrawable) {
+ ((TransitionDrawable)d).resetTransition();
+ }
+ }
+ postDelayed(new Runnable() {
+ public void run() {
+ child.setPressed(false);
+ setPressed(false);
+ if (!mDataChanged) {
+ post(performClick);
+ }
+ mTouchMode = TOUCH_MODE_REST;
+ }
+ }, ViewConfiguration.getPressedStateDuration());
+ }
+ return true;
+ } else {
+ if (!mDataChanged) {
+ post(performClick);
+ }
+ }
+ }
+ mTouchMode = TOUCH_MODE_REST;
+ break;
+ case TOUCH_MODE_SCROLL:
+ final VelocityTracker velocityTracker = mVelocityTracker;
+ velocityTracker.computeCurrentVelocity(1000);
+ int initialVelocity = (int)velocityTracker.getYVelocity();
+
+ if ((Math.abs(initialVelocity) > ViewConfiguration.getMinimumFlingVelocity()) &&
+ (getChildCount() > 0)){
+ if (mFlingRunnable == null) {
+ mFlingRunnable = new FlingRunnable();
+ }
+ reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
+ mFlingRunnable.start(-initialVelocity);
+ } else {
+ mTouchMode = TOUCH_MODE_REST;
+ reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
+ }
+ }
+
+ setPressed(false);
+
+ // Need to redraw since we probably aren't drawing the selector anymore
+ invalidate();
+
+ final Handler handler = getHandler();
+ if (handler != null) {
+ handler.removeCallbacks(mPendingCheckForLongPress);
+ }
+
+ if (mVelocityTracker != null) {
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+
+ if (PROFILE_SCROLLING) {
+ if (mScrollProfilingStarted) {
+ Debug.stopMethodTracing();
+ mScrollProfilingStarted = false;
+ }
+ }
+ break;
+ }
+
+ case MotionEvent.ACTION_CANCEL: {
+ mTouchMode = TOUCH_MODE_REST;
+ setPressed(false);
+ View motionView = this.getChildAt(mMotionPosition - mFirstPosition);
+ if (motionView != null) {
+ motionView.setPressed(false);
+ }
+ clearScrollingCache();
+
+ final Handler handler = getHandler();
+ if (handler != null) {
+ handler.removeCallbacks(mPendingCheckForLongPress);
+ }
+
+ if (mVelocityTracker != null) {
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+ }
+
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ int action = ev.getAction();
+ int x = (int) ev.getX();
+ int y = (int) ev.getY();
+ View v;
+ switch (action) {
+ case MotionEvent.ACTION_DOWN: {
+ int motionPosition = findMotionRow(y);
+ if (mTouchMode != TOUCH_MODE_FLING && motionPosition >= 0) {
+ // User clicked on an actual view (and was not stopping a fling).
+ // Remember where the motion event started
+ v = getChildAt(motionPosition - mFirstPosition);
+ mMotionViewOriginalTop = v.getTop();
+ mMotionX = x;
+ mMotionY = y;
+ mMotionPosition = motionPosition;
+ mTouchMode = TOUCH_MODE_DOWN;
+ clearScrollingCache();
+ }
+ mLastY = Integer.MIN_VALUE;
+ break;
+ }
+
+ case MotionEvent.ACTION_MOVE: {
+ switch (mTouchMode) {
+ case TOUCH_MODE_DOWN:
+ if (startScrollIfNeeded(y - mMotionY)) {
+ return true;
+ }
+ break;
+ }
+ break;
+ }
+
+ case MotionEvent.ACTION_UP: {
+ mTouchMode = TOUCH_MODE_REST;
+ reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
+ break;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void addTouchables(ArrayList<View> views) {
+ final int count = getChildCount();
+ final int firstPosition = mFirstPosition;
+ final ListAdapter adapter = mAdapter;
+
+ if (adapter == null) {
+ return;
+ }
+
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (adapter.isEnabled(firstPosition + i)) {
+ views.add(child);
+ }
+ child.addTouchables(views);
+ }
+ }
+
+ private void reportScrollStateChange(int newState) {
+ if (newState != mLastScrollState) {
+ if (mOnScrollListener != null) {
+ mOnScrollListener.onScrollStateChanged(this, newState);
+ mLastScrollState = newState;
+ }
+ }
+ }
+
+ /**
+ * Responsible for fling behavior. Use {@link #start(int)} to
+ * initiate a fling. Each frame of the fling is handled in {@link #run()}.
+ * A FlingRunnable will keep re-posting itself until the fling is done.
+ *
+ */
+ private class FlingRunnable implements Runnable {
+ /**
+ * Tracks the decay of a fling scroll
+ */
+ private Scroller mScroller;
+
+ /**
+ * Y value reported by mScroller on the previous fling
+ */
+ private int mLastFlingY;
+
+ public FlingRunnable() {
+ mScroller = new Scroller(getContext());
+ }
+
+ public void start(int initialVelocity) {
+ int initialY = initialVelocity < 0 ? Integer.MAX_VALUE : 0;
+ mLastFlingY = initialY;
+ mScroller.fling(0, initialY, 0, initialVelocity,
+ 0, Integer.MAX_VALUE, 0, Integer.MAX_VALUE);
+ mTouchMode = TOUCH_MODE_FLING;
+ post(this);
+
+ if (PROFILE_FLINGING) {
+ if (!mFlingProfilingStarted) {
+ Debug.startMethodTracing("AbsListViewFling");
+ mFlingProfilingStarted = true;
+ }
+ }
+ }
+
+ private void endFling() {
+ mTouchMode = TOUCH_MODE_REST;
+ if (mOnScrollListener != null) {
+ mOnScrollListener.onScrollStateChanged(AbsListView.this,
+ OnScrollListener.SCROLL_STATE_IDLE);
+ }
+ clearScrollingCache();
+ }
+
+ public void run() {
+ if (mTouchMode != TOUCH_MODE_FLING) {
+ return;
+ }
+
+ if (mItemCount == 0 || getChildCount() == 0) {
+ endFling();
+ return;
+ }
+
+ final Scroller scroller = mScroller;
+ boolean more = scroller.computeScrollOffset();
+ final int y = scroller.getCurrY();
+
+ // Flip sign to convert finger direction to list items direction
+ // (e.g. finger moving down means list is moving towards the top)
+ int delta = mLastFlingY - y;
+
+ // Pretend that each frame of a fling scroll is a touch scroll
+ if (delta > 0) {
+ // List is moving towards the top. Use first view as mMotionPosition
+ mMotionPosition = mFirstPosition;
+ final View firstView = getChildAt(0);
+ mMotionViewOriginalTop = firstView.getTop();
+
+ // Don't fling more than 1 screen
+ delta = Math.min(getHeight() - mPaddingBottom - mPaddingTop - 1, delta);
+ } else {
+ // List is moving towards the bottom. Use last view as mMotionPosition
+ int offsetToLast = getChildCount() - 1;
+ mMotionPosition = mFirstPosition + offsetToLast;
+
+ final View lastView = getChildAt(offsetToLast);
+ mMotionViewOriginalTop = lastView.getTop();
+
+ // Don't fling more than 1 screen
+ delta = Math.max(-(getHeight() - mPaddingBottom - mPaddingTop - 1), delta);
+ }
+
+ trackMotionScroll(delta, delta);
+
+ // Check to see if we have bumped into the scroll limit
+ View motionView = getChildAt(mMotionPosition - mFirstPosition);
+ if (motionView != null) {
+ // Check if the top of the motion view is where it is
+ // supposed to be
+ if (motionView.getTop() != mMotionViewNewTop) {
+ more = false;
+ }
+ }
+
+ if (more) {
+ mLastFlingY = y;
+ post(this);
+ } else {
+ endFling();
+ if (PROFILE_FLINGING) {
+ if (mFlingProfilingStarted) {
+ Debug.stopMethodTracing();
+ mFlingProfilingStarted = false;
+ }
+ }
+ }
+ }
+ }
+
+ private void createScrollingCache() {
+ if (mScrollingCacheEnabled && !mCachingStarted) {
+ setChildrenDrawnWithCacheEnabled(true);
+ setChildrenDrawingCacheEnabled(true);
+ mCachingStarted = true;
+ }
+ }
+
+ private void clearScrollingCache() {
+ if (mCachingStarted) {
+ setChildrenDrawnWithCacheEnabled(false);
+ if ((mPersistentDrawingCache & PERSISTENT_SCROLLING_CACHE) == 0) {
+ setChildrenDrawingCacheEnabled(false);
+ }
+ if (!isAlwaysDrawnWithCacheEnabled()) {
+ invalidate();
+ }
+ mCachingStarted = false;
+ }
+ }
+
+ /**
+ * Track a motion scroll
+ *
+ * @param deltaY Amount to offset mMotionView. This is the accumulated delta since the motion
+ * began. Positive numbers mean the user's finger is moving down the screen.
+ * @param incrementalDeltaY Change in deltaY from the previous event.
+ */
+ void trackMotionScroll(int deltaY, int incrementalDeltaY) {
+ final int childCount = getChildCount();
+ if (childCount == 0) {
+ return;
+ }
+
+ final int firstTop = getChildAt(0).getTop();
+ final int lastBottom = getChildAt(childCount - 1).getBottom();
+
+ final Rect listPadding = mListPadding;
+
+ // FIXME account for grid vertical spacing too?
+ final int spaceAbove = listPadding.top - firstTop;
+ final int end = getHeight() - listPadding.bottom;
+ final int spaceBelow = lastBottom - end;
+
+ final int height = getHeight() - mPaddingBottom - mPaddingTop;
+ if (deltaY < 0) {
+ deltaY = Math.max(-(height - 1), deltaY);
+ } else {
+ deltaY = Math.min(height - 1, deltaY);
+ }
+
+ if (incrementalDeltaY < 0) {
+ incrementalDeltaY = Math.max(-(height - 1), incrementalDeltaY);
+ } else {
+ incrementalDeltaY = Math.min(height - 1, incrementalDeltaY);
+ }
+
+ final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
+
+ if (spaceAbove >= absIncrementalDeltaY && spaceBelow >= absIncrementalDeltaY) {
+ hideSelector();
+ offsetChildrenTopAndBottom(incrementalDeltaY);
+ invalidate();
+ mMotionViewNewTop = mMotionViewOriginalTop + deltaY;
+ } else {
+ final int firstPosition = mFirstPosition;
+
+ if (firstPosition == 0 && firstTop >= listPadding.top && deltaY > 0) {
+ // Don't need to move views down if the top of the first position is already visible
+ return;
+ }
+
+ if (firstPosition + childCount == mItemCount && lastBottom <= end && deltaY < 0) {
+ // Don't need to move views up if the bottom of the last position is already visible
+ return;
+ }
+
+ final boolean down = incrementalDeltaY < 0;
+
+ hideSelector();
+
+ final int headerViewsCount = getHeaderViewsCount();
+ final int footerViewsStart = mItemCount - getFooterViewsCount();
+
+ int start = 0;
+ int count = 0;
+
+ if (down) {
+ final int top = listPadding.top - incrementalDeltaY;
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ if (child.getBottom() >= top) {
+ break;
+ } else {
+ count++;
+ int position = firstPosition + i;
+ if (position >= headerViewsCount && position < footerViewsStart) {
+ mRecycler.addScrapView(child);
+
+ if (ViewDebug.TRACE_RECYCLER) {
+ ViewDebug.trace(child,
+ ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP,
+ firstPosition + i, -1);
+ }
+ }
+ }
+ }
+ } else {
+ final int bottom = getHeight() - listPadding.bottom - incrementalDeltaY;
+ for (int i = childCount - 1; i >= 0; i--) {
+ final View child = getChildAt(i);
+ if (child.getTop() <= bottom) {
+ break;
+ } else {
+ start = i;
+ count++;
+ int position = firstPosition + i;
+ if (position >= headerViewsCount && position < footerViewsStart) {
+ mRecycler.addScrapView(child);
+
+ if (ViewDebug.TRACE_RECYCLER) {
+ ViewDebug.trace(child,
+ ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP,
+ firstPosition + i, -1);
+ }
+ }
+ }
+ }
+ }
+
+ mMotionViewNewTop = mMotionViewOriginalTop + deltaY;
+
+ mBlockLayoutRequests = true;
+ detachViewsFromParent(start, count);
+ offsetChildrenTopAndBottom(incrementalDeltaY);
+
+ if (down) {
+ mFirstPosition += count;
+ }
+
+ invalidate();
+ fillGap(down);
+ mBlockLayoutRequests = false;
+
+ invokeOnItemScrollListener();
+ }
+ }
+
+ /**
+ * Returns the number of header views in the list. Header views are special views
+ * at the top of the list that should not be recycled during a layout.
+ *
+ * @return The number of header views, 0 in the default implementation.
+ */
+ int getHeaderViewsCount() {
+ return 0;
+ }
+
+ /**
+ * Returns the number of footer views in the list. Footer views are special views
+ * at the bottom of the list that should not be recycled during a layout.
+ *
+ * @return The number of footer views, 0 in the default implementation.
+ */
+ int getFooterViewsCount() {
+ return 0;
+ }
+
+ /**
+ * Fills the gap left open by a touch-scroll. During a touch scroll, children that
+ * remain on screen are shifted and the other ones are discarded. The role of this
+ * method is to fill the gap thus created by performing a partial layout in the
+ * empty space.
+ *
+ * @param down true if the scroll is going down, false if it is going up
+ */
+ abstract void fillGap(boolean down);
+
+ void hideSelector() {
+ if (mSelectedPosition != INVALID_POSITION) {
+ mResurrectToPosition = mSelectedPosition;
+ if (mNextSelectedPosition >= 0 && mNextSelectedPosition != mSelectedPosition) {
+ mResurrectToPosition = mNextSelectedPosition;
+ }
+ setSelectedPositionInt(INVALID_POSITION);
+ setNextSelectedPositionInt(INVALID_POSITION);
+ mSelectedTop = 0;
+ mSelectorRect.setEmpty();
+ }
+ }
+
+ /**
+ * @return A position to select. First we try mSelectedPosition. If that has been clobbered by
+ * entering touch mode, we then try mResurrectToPosition. Values are pinned to the range
+ * of items available in the adapter
+ */
+ int reconcileSelectedPosition() {
+ int position = mSelectedPosition;
+ if (position < 0) {
+ position = mResurrectToPosition;
+ }
+ position = Math.max(0, position);
+ position = Math.min(position, mItemCount - 1);
+ return position;
+ }
+
+ /**
+ * Find the row closest to y. This row will be used as the motion row when scrolling
+ *
+ * @param y Where the user touched
+ * @return The position of the first (or only) item in the row closest to y
+ */
+ abstract int findMotionRow(int y);
+
+ /**
+ * Causes all the views to be rebuilt and redrawn.
+ */
+ public void invalidateViews() {
+ mDataChanged = true;
+ rememberSyncState();
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Makes the item at the supplied position selected.
+ *
+ * @param position the position of the new selection
+ */
+ abstract void setSelectionInt(int position);
+
+ /**
+ * Attempt to bring the selection back if the user is switching from touch
+ * to trackball mode
+ * @return Whether selection was set to something.
+ */
+ boolean resurrectSelection() {
+ final int childCount = getChildCount();
+
+ if (childCount <= 0) {
+ return false;
+ }
+
+ int selectedTop = 0;
+ int selectedPos;
+ int childrenTop = mListPadding.top;
+ int childrenBottom = mBottom - mTop - mListPadding.bottom;
+ final int firstPosition = mFirstPosition;
+ final int toPosition = mResurrectToPosition;
+ boolean down = true;
+
+ if (toPosition >= firstPosition && toPosition < firstPosition + childCount) {
+ selectedPos = toPosition;
+
+ final View selected = getChildAt(selectedPos - mFirstPosition);
+ selectedTop = selected.getTop();
+ int selectedBottom = selected.getBottom();
+
+ // We are scrolled, don't get in the fade
+ if (selectedTop < childrenTop) {
+ selectedTop = childrenTop + getVerticalFadingEdgeLength();
+ } else if (selectedBottom > childrenBottom) {
+ selectedTop = childrenBottom - selected.getMeasuredHeight()
+ - getVerticalFadingEdgeLength();
+ }
+ } else {
+ if (toPosition < firstPosition) {
+ // Default to selecting whatever is first
+ selectedPos = firstPosition;
+ for (int i = 0; i < childCount; i++) {
+ final View v = getChildAt(i);
+ final int top = v.getTop();
+
+ if (i == 0) {
+ // Remember the position of the first item
+ selectedTop = top;
+ // See if we are scrolled at all
+ if (firstPosition > 0 || top < childrenTop) {
+ // If we are scrolled, don't select anything that is
+ // in the fade region
+ childrenTop += getVerticalFadingEdgeLength();
+ }
+ }
+ if (top >= childrenTop) {
+ // Found a view whose top is fully visisble
+ selectedPos = firstPosition + i;
+ selectedTop = top;
+ break;
+ }
+ }
+ } else {
+ final int itemCount = mItemCount;
+ down = false;
+ selectedPos = firstPosition + childCount - 1;
+
+ for (int i = childCount - 1; i >= 0; i--) {
+ final View v = getChildAt(i);
+ final int top = v.getTop();
+ final int bottom = v.getBottom();
+
+ if (i == childCount - 1) {
+ selectedTop = top;
+ if (firstPosition + childCount < itemCount || bottom > childrenBottom) {
+ childrenBottom -= getVerticalFadingEdgeLength();
+ }
+ }
+
+ if (bottom <= childrenBottom) {
+ selectedPos = firstPosition + i;
+ selectedTop = top;
+ break;
+ }
+ }
+ }
+ }
+
+ mResurrectToPosition = INVALID_POSITION;
+ removeCallbacks(mFlingRunnable);
+ mTouchMode = TOUCH_MODE_REST;
+ clearScrollingCache();
+ mSpecificTop = selectedTop;
+ selectedPos = lookForSelectablePosition(selectedPos, down);
+ if (selectedPos >= 0) {
+ mLayoutMode = LAYOUT_SPECIFIC;
+ setSelectionInt(selectedPos);
+ }
+
+ return selectedPos >= 0;
+ }
+
+ @Override
+ protected void handleDataChanged() {
+ int count = mItemCount;
+ if (count > 0) {
+
+ int newPos;
+
+ int selectablePos;
+
+ // Find the row we are supposed to sync to
+ if (mNeedSync) {
+ // Update this first, since setNextSelectedPositionInt inspects it
+ mNeedSync = false;
+
+ if (mTranscriptMode == TRANSCRIPT_MODE_ALWAYS_SCROLL ||
+ (mTranscriptMode == TRANSCRIPT_MODE_NORMAL &&
+ mFirstPosition + getChildCount() >= mOldItemCount)) {
+ mLayoutMode = LAYOUT_FORCE_BOTTOM;
+ return;
+ }
+
+ switch (mSyncMode) {
+ case SYNC_SELECTED_POSITION:
+ if (isInTouchMode()) {
+ // We saved our state when not in touch mode. (We know this because
+ // mSyncMode is SYNC_SELECTED_POSITION.) Now we are trying to
+ // restore in touch mode. Just leave mSyncPosition as it is (possibly
+ // adjusting if the available range changed) and return.
+ mLayoutMode = LAYOUT_SYNC;
+ mSyncPosition = Math.min(Math.max(0, mSyncPosition), count - 1);
+
+ return;
+ } else {
+ // See if we can find a position in the new data with the same
+ // id as the old selection. This will change mSyncPosition.
+ newPos = findSyncPosition();
+ if (newPos >= 0) {
+ // Found it. Now verify that new selection is still selectable
+ selectablePos = lookForSelectablePosition(newPos, true);
+ if (selectablePos == newPos) {
+ // Same row id is selected
+ mSyncPosition = newPos;
+
+ if (mSyncHeight == getHeight()) {
+ // If we are at the same height as when we saved state, try
+ // to restore the scroll position too.
+ mLayoutMode = LAYOUT_SYNC;
+ } else {
+ // We are not the same height as when the selection was saved, so
+ // don't try to restore the exact position
+ mLayoutMode = LAYOUT_SET_SELECTION;
+ }
+
+ // Restore selection
+ setNextSelectedPositionInt(newPos);
+ return;
+ }
+ }
+ }
+ break;
+ case SYNC_FIRST_POSITION:
+ // Leave mSyncPosition as it is -- just pin to available range
+ mLayoutMode = LAYOUT_SYNC;
+ mSyncPosition = Math.min(Math.max(0, mSyncPosition), count - 1);
+
+ return;
+ }
+ }
+
+ if (!isInTouchMode()) {
+ // We couldn't find matching data -- try to use the same position
+ newPos = getSelectedItemPosition();
+
+ // Pin position to the available range
+ if (newPos >= count) {
+ newPos = count - 1;
+ }
+ if (newPos < 0) {
+ newPos = 0;
+ }
+
+ // Make sure we select something selectable -- first look down
+ selectablePos = lookForSelectablePosition(newPos, true);
+
+ if (selectablePos >= 0) {
+ setNextSelectedPositionInt(selectablePos);
+ return;
+ } else {
+ // Looking down didn't work -- try looking up
+ selectablePos = lookForSelectablePosition(newPos, false);
+ if (selectablePos >= 0) {
+ setNextSelectedPositionInt(selectablePos);
+ return;
+ }
+ }
+ } else {
+
+ // We already know where we want to resurrect the selection
+ if (mResurrectToPosition >= 0) {
+ return;
+ }
+ }
+
+ }
+
+ // Nothing is selected. Give up and reset everything.
+ mLayoutMode = mStackFromBottom ? LAYOUT_FORCE_BOTTOM : LAYOUT_FORCE_TOP;
+ mSelectedPosition = INVALID_POSITION;
+ mSelectedRowId = INVALID_ROW_ID;
+ mNextSelectedPosition = INVALID_POSITION;
+ mNextSelectedRowId = INVALID_ROW_ID;
+ mNeedSync = false;
+ checkSelectionChanged();
+ }
+
+ /**
+ * Removes the filter window
+ */
+ void dismissPopup() {
+ if (mPopup != null) {
+ mPopup.dismiss();
+ }
+ }
+
+ /**
+ * Shows the filter window
+ */
+ private void showPopup() {
+ // Make sure we have a window before showing the popup
+ if (getWindowVisibility() == View.VISIBLE) {
+ int screenHeight = WindowManagerImpl.getDefault().getDefaultDisplay().getHeight();
+ final int[] xy = mLocation;
+ getLocationOnScreen(xy);
+ int bottomGap = screenHeight - xy[1] - getHeight() + 20;
+ mPopup.showAtLocation(this, Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL,
+ xy[0], bottomGap);
+ // Make sure we get focus if we are showing the popup
+ checkFocus();
+ }
+ }
+
+ /**
+ * What is the distance between the source and destination rectangles given the direction of
+ * focus navigation between them? The direction basically helps figure out more quickly what is
+ * self evident by the relationship between the rects...
+ *
+ * @param source the source rectangle
+ * @param dest the destination rectangle
+ * @param direction the direction
+ * @return the distance between the rectangles
+ */
+ static int getDistance(Rect source, Rect dest, int direction) {
+ int sX, sY; // source x, y
+ int dX, dY; // dest x, y
+ switch (direction) {
+ case View.FOCUS_RIGHT:
+ sX = source.right;
+ sY = source.top + source.height() / 2;
+ dX = dest.left;
+ dY = dest.top + dest.height() / 2;
+ break;
+ case View.FOCUS_DOWN:
+ sX = source.left + source.width() / 2;
+ sY = source.bottom;
+ dX = dest.left + dest.width() / 2;
+ dY = dest.top;
+ break;
+ case View.FOCUS_LEFT:
+ sX = source.left;
+ sY = source.top + source.height() / 2;
+ dX = dest.right;
+ dY = dest.top + dest.height() / 2;
+ break;
+ case View.FOCUS_UP:
+ sX = source.left + source.width() / 2;
+ sY = source.top;
+ dX = dest.left + dest.width() / 2;
+ dY = dest.bottom;
+ break;
+ default:
+ throw new IllegalArgumentException("direction must be one of "
+ + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
+ }
+ int deltaX = dX - sX;
+ int deltaY = dY - sY;
+ return deltaY * deltaY + deltaX * deltaX;
+ }
+
+ @Override
+ protected boolean isInFilterMode() {
+ return mFiltered;
+ }
+
+ /**
+ * Sends a key to the text filter window
+ *
+ * @param keyCode The keycode for the event
+ * @param event The actual key event
+ *
+ * @return True if the text filter handled the event, false otherwise.
+ */
+ boolean sendToTextFilter(int keyCode, int count, KeyEvent event) {
+ if (!mTextFilterEnabled || !(getAdapter() instanceof Filterable) ||
+ ((Filterable) getAdapter()).getFilter() == null) {
+ return false;
+ }
+
+ boolean handled = false;
+ boolean okToSend = true;
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_UP:
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ case KeyEvent.KEYCODE_ENTER:
+ okToSend = false;
+ break;
+ case KeyEvent.KEYCODE_BACK:
+ if (mFiltered && mPopup != null && mPopup.isShowing() &&
+ event.getAction() == KeyEvent.ACTION_DOWN) {
+ handled = true;
+ mTextFilter.setText("");
+ }
+ okToSend = false;
+ break;
+ case KeyEvent.KEYCODE_SPACE:
+ // Only send spaces once we are filtered
+ okToSend = mFiltered = true;
+ break;
+ }
+
+ if (okToSend) {
+ createTextFilter(true);
+
+ KeyEvent forwardEvent = event;
+ if (forwardEvent.getRepeatCount() > 0) {
+ forwardEvent = new KeyEvent(event, event.getEventTime(), 0);
+ }
+
+ int action = event.getAction();
+ switch (action) {
+ case KeyEvent.ACTION_DOWN:
+ handled = mTextFilter.onKeyDown(keyCode, forwardEvent);
+ break;
+
+ case KeyEvent.ACTION_UP:
+ handled = mTextFilter.onKeyUp(keyCode, forwardEvent);
+ break;
+
+ case KeyEvent.ACTION_MULTIPLE:
+ handled = mTextFilter.onKeyMultiple(keyCode, count, event);
+ break;
+ }
+ }
+ return handled;
+ }
+
+ /**
+ * Creates the window for the text filter and populates it with an EditText field;
+ *
+ * @param animateEntrance true if the window should appear with an animation
+ */
+ private void createTextFilter(boolean animateEntrance) {
+ if (mPopup == null) {
+ Context c = getContext();
+ PopupWindow p = new PopupWindow(c);
+ LayoutInflater layoutInflater = (LayoutInflater) c
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mTextFilter = (EditText) layoutInflater.inflate(
+ com.android.internal.R.layout.typing_filter, null);
+ mTextFilter.addTextChangedListener(this);
+ p.setFocusable(false);
+ p.setContentView(mTextFilter);
+ p.setWidth(LayoutParams.WRAP_CONTENT);
+ p.setHeight(LayoutParams.WRAP_CONTENT);
+ p.setBackgroundDrawable(null);
+ mPopup = p;
+ getViewTreeObserver().addOnGlobalLayoutListener(this);
+ }
+ if (animateEntrance) {
+ mPopup.setAnimationStyle(com.android.internal.R.style.Animation_TypingFilter);
+ } else {
+ mPopup.setAnimationStyle(com.android.internal.R.style.Animation_TypingFilterRestore);
+ }
+ }
+
+ /**
+ * Clear the text filter.
+ */
+ public void clearTextFilter() {
+ if (mFiltered) {
+ mTextFilter.setText("");
+ mFiltered = false;
+ if (mPopup != null && mPopup.isShowing()) {
+ dismissPopup();
+ }
+ }
+ }
+
+ /**
+ * Returns if the ListView currently has a text filter.
+ */
+ public boolean hasTextFilter() {
+ return mFiltered;
+ }
+
+ public void onGlobalLayout() {
+ if (isShown()) {
+ // Show the popup if we are filtered
+ if (mFiltered && mPopup != null && !mPopup.isShowing()) {
+ showPopup();
+ }
+ } else {
+ // Hide the popup when we are no longer visible
+ if (mPopup.isShowing()) {
+ dismissPopup();
+ }
+ }
+
+ }
+
+ /**
+ * For our text watcher that associated with the text filter
+ */
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ /**
+ * For our text watcher that associated with the text filter. Performs the actual
+ * filtering as the text changes.
+ */
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ if (mPopup != null) {
+ int length = s.length();
+ boolean showing = mPopup.isShowing();
+ if (!showing && length > 0) {
+ // Show the filter popup if necessary
+ showPopup();
+ mFiltered = true;
+ } else if (showing && length == 0) {
+ // Remove the filter popup if the user has cleared all text
+ mPopup.dismiss();
+ mFiltered = false;
+ }
+ if (mAdapter instanceof Filterable) {
+ Filter f = ((Filterable) mAdapter).getFilter();
+ // Filter should not be null when we reach this part
+ if (f != null) {
+ f.filter(s, this);
+ } else {
+ throw new IllegalStateException("You cannot call onTextChanged with a non "
+ + "filterable adapter");
+ }
+ }
+ }
+ }
+
+ /**
+ * For our text watcher that associated with the text filter
+ */
+ public void afterTextChanged(Editable s) {
+ }
+
+ public void onFilterComplete(int count) {
+ if (mSelectedPosition < 0 && count > 0) {
+ mResurrectToPosition = INVALID_POSITION;
+ resurrectSelection();
+ }
+ }
+
+ @Override
+ protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
+ return new LayoutParams(p);
+ }
+
+ @Override
+ public LayoutParams generateLayoutParams(AttributeSet attrs) {
+ return new AbsListView.LayoutParams(getContext(), attrs);
+ }
+
+ @Override
+ protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+ return p instanceof AbsListView.LayoutParams;
+ }
+
+ /**
+ * Puts the list or grid into transcript mode. In this mode the list or grid will always scroll
+ * to the bottom to show new items.
+ *
+ * @param mode the transcript mode to set
+ *
+ * @see #TRANSCRIPT_MODE_DISABLED
+ * @see #TRANSCRIPT_MODE_NORMAL
+ * @see #TRANSCRIPT_MODE_ALWAYS_SCROLL
+ */
+ public void setTranscriptMode(int mode) {
+ mTranscriptMode = mode;
+ }
+
+ /**
+ * Returns the current transcript mode.
+ *
+ * @return {@link #TRANSCRIPT_MODE_DISABLED}, {@link #TRANSCRIPT_MODE_NORMAL} or
+ * {@link #TRANSCRIPT_MODE_ALWAYS_SCROLL}
+ */
+ public int getTranscriptMode() {
+ return mTranscriptMode;
+ }
+
+ @Override
+ public int getSolidColor() {
+ return mCacheColorHint;
+ }
+
+ /**
+ * When set to a non-zero value, the cache color hint indicates that this list is always drawn
+ * on top of a solid, single-color, opaque background
+ *
+ * @param color The background color
+ */
+ public void setCacheColorHint(int color) {
+ mCacheColorHint = color;
+ }
+
+ /**
+ * When set to a non-zero value, the cache color hint indicates that this list is always drawn
+ * on top of a solid, single-color, opaque background
+ *
+ * @return The cache color hint
+ */
+ public int getCacheColorHint() {
+ return mCacheColorHint;
+ }
+
+ /**
+ * Move all views (excluding headers and footers) held by this AbsListView into the supplied
+ * List. This includes views displayed on the screen as well as views stored in AbsListView's
+ * internal view recycler.
+ *
+ * @param views A list into which to put the reclaimed views
+ */
+ public void reclaimViews(List<View> views) {
+ int childCount = getChildCount();
+ RecyclerListener listener = mRecycler.mRecyclerListener;
+
+ // Reclaim views on screen
+ for (int i = 0; i < childCount; i++) {
+ View child = getChildAt(i);
+ AbsListView.LayoutParams lp = (AbsListView.LayoutParams)child.getLayoutParams();
+ // Don't reclaim header or footer views, or views that should be ignored
+ if (lp != null && mRecycler.shouldRecycleViewType(lp.viewType)) {
+ views.add(child);
+ if (listener != null) {
+ // Pretend they went through the scrap heap
+ listener.onMovedToScrapHeap(child);
+ }
+ }
+ }
+ mRecycler.reclaimScrapViews(views);
+ removeAllViewsInLayout();
+ }
+
+ /**
+ * 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.
+ *
+ * @param listener The recycler listener to be notified of views set aside
+ * in the recycler.
+ *
+ * @see android.widget.AbsListView.RecycleBin
+ * @see android.widget.AbsListView.RecyclerListener
+ */
+ public void setRecyclerListener(RecyclerListener listener) {
+ mRecycler.mRecyclerListener = listener;
+ }
+
+ /**
+ * AbsListView extends LayoutParams to provide a place to hold the view type.
+ */
+ public static class LayoutParams extends ViewGroup.LayoutParams {
+ /**
+ * View type for this view, as returned by
+ * {@link android.widget.Adapter#getItemViewType(int) }
+ */
+ int viewType;
+
+ public LayoutParams(Context c, AttributeSet attrs) {
+ super(c, attrs);
+ }
+
+ public LayoutParams(int w, int h) {
+ super(w, h);
+ }
+
+ public LayoutParams(int w, int h, int viewType) {
+ super(w, h);
+ this.viewType = viewType;
+ }
+
+ public LayoutParams(ViewGroup.LayoutParams source) {
+ super(source);
+ }
+ }
+
+ /**
+ * A RecyclerListener is used to receive a notification whenever a View is placed
+ * inside the RecycleBin's scrap heap. This listener is used to free resources
+ * associated to Views placed in the RecycleBin.
+ *
+ * @see android.widget.AbsListView.RecycleBin
+ * @see android.widget.AbsListView#setRecyclerListener(android.widget.AbsListView.RecyclerListener)
+ */
+ public static interface RecyclerListener {
+ /**
+ * Indicates that the specified View was moved into the recycler's scrap heap.
+ * The view is not displayed on screen any more and any expensive resource
+ * associated with the view should be discarded.
+ *
+ * @param view
+ */
+ void onMovedToScrapHeap(View view);
+ }
+
+ /**
+ * The RecycleBin facilitates reuse of views across layouts. The RecycleBin has two levels of
+ * storage: ActiveViews and ScrapViews. ActiveViews are those views which were onscreen at the
+ * start of a layout. By construction, they are displaying current information. At the end of
+ * layout, all views in ActiveViews are demoted to ScrapViews. ScrapViews are old views that
+ * could potentially be used by the adapter to avoid allocating views unnecessarily.
+ *
+ * @see android.widget.AbsListView#setRecyclerListener(android.widget.AbsListView.RecyclerListener)
+ * @see android.widget.AbsListView.RecyclerListener
+ */
+ class RecycleBin {
+ private RecyclerListener mRecyclerListener;
+
+ /**
+ * The position of the first view stored in mActiveViews.
+ */
+ private int mFirstActivePosition;
+
+ /**
+ * Views that were on screen at the start of layout. This array is populated at the start of
+ * layout, and at the end of layout all view in mActiveViews are moved to mScrapViews.
+ * Views in mActiveViews represent a contiguous range of Views, with position of the first
+ * view store in mFirstActivePosition.
+ */
+ private View[] mActiveViews = new View[0];
+
+ /**
+ * Unsorted views that can be used by the adapter as a convert view.
+ */
+ private ArrayList<View>[] mScrapViews;
+
+ private int mViewTypeCount;
+
+ private ArrayList<View> mCurrentScrap;
+
+ public void setViewTypeCount(int viewTypeCount) {
+ if (viewTypeCount < 1) {
+ throw new IllegalArgumentException("Can't have a viewTypeCount < 1");
+ }
+ //noinspection unchecked
+ ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];
+ for (int i = 0; i < viewTypeCount; i++) {
+ scrapViews[i] = new ArrayList<View>();
+ }
+ mViewTypeCount = viewTypeCount;
+ mCurrentScrap = scrapViews[0];
+ mScrapViews = scrapViews;
+ }
+
+ public boolean shouldRecycleViewType(int viewType) {
+ return viewType >= 0;
+ }
+
+ /**
+ * Clears the scrap heap.
+ */
+ void clear() {
+ if (mViewTypeCount == 1) {
+ final ArrayList<View> scrap = mCurrentScrap;
+ final int scrapCount = scrap.size();
+ for (int i = 0; i < scrapCount; i++) {
+ removeDetachedView(scrap.remove(scrapCount - 1 - i), false);
+ }
+ } else {
+ final int typeCount = mViewTypeCount;
+ for (int i = 0; i < typeCount; i++) {
+ final ArrayList<View> scrap = mScrapViews[i];
+ final int scrapCount = scrap.size();
+ for (int j = 0; j < scrapCount; j++) {
+ removeDetachedView(scrap.remove(scrapCount - 1 - j), false);
+ }
+ }
+ }
+ }
+
+ /**
+ * Fill ActiveViews with all of the children of the AbsListView.
+ *
+ * @param childCount The minimum number of views mActiveViews should hold
+ * @param firstActivePosition The position of the first view that will be stored in
+ * mActiveViews
+ */
+ void fillActiveViews(int childCount, int firstActivePosition) {
+ if (mActiveViews.length < childCount) {
+ mActiveViews = new View[childCount];
+ }
+ mFirstActivePosition = firstActivePosition;
+
+ final View[] activeViews = mActiveViews;
+ for (int i = 0; i < childCount; i++) {
+ View child = getChildAt(i);
+ AbsListView.LayoutParams lp = (AbsListView.LayoutParams)child.getLayoutParams();
+ // Don't put header or footer views into the scrap heap
+ if (lp != null && lp.viewType != AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
+ // Note: We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in active views.
+ // However, we will NOT place them into scrap views.
+ activeViews[i] = getChildAt(i);
+ }
+ }
+ }
+
+ /**
+ * Get the view corresponding to the specified position. The view will be removed from
+ * mActiveViews if it is found.
+ *
+ * @param position The position to look up in mActiveViews
+ * @return The view if it is found, null otherwise
+ */
+ View getActiveView(int position) {
+ int index = position - mFirstActivePosition;
+ final View[] activeViews = mActiveViews;
+ if (index >=0 && index < activeViews.length) {
+ final View match = activeViews[index];
+ activeViews[index] = null;
+ return match;
+ }
+ return null;
+ }
+
+ /**
+ * @return A view from the ScrapViews collection. These are unordered.
+ */
+ View getScrapView(int position) {
+ ArrayList<View> scrapViews;
+ if (mViewTypeCount == 1) {
+ scrapViews = mCurrentScrap;
+ int size = scrapViews.size();
+ if (size > 0) {
+ return scrapViews.remove(size - 1);
+ } else {
+ return null;
+ }
+ } else {
+ int whichScrap = mAdapter.getItemViewType(position);
+ if (whichScrap >= 0 && whichScrap < mScrapViews.length) {
+ scrapViews = mScrapViews[whichScrap];
+ int size = scrapViews.size();
+ if (size > 0) {
+ return scrapViews.remove(size - 1);
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Put a view into the ScapViews list. These views are unordered.
+ *
+ * @param scrap The view to add
+ */
+ void addScrapView(View scrap) {
+ AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
+ if (lp == null) {
+ return;
+ }
+
+ // Don't put header or footer views or views that should be ignored
+ // into the scrap heap
+ int viewType = lp.viewType;
+ if (!shouldRecycleViewType(viewType)) {
+ return;
+ }
+
+ if (mViewTypeCount == 1) {
+ mCurrentScrap.add(scrap);
+ } else {
+ mScrapViews[viewType].add(scrap);
+ }
+
+ if (mRecyclerListener != null) {
+ mRecyclerListener.onMovedToScrapHeap(scrap);
+ }
+ }
+
+ /**
+ * Move all views remaining in mActiveViews to mScrapViews.
+ */
+ void scrapActiveViews() {
+ final View[] activeViews = mActiveViews;
+ final boolean hasListener = mRecyclerListener != null;
+ final boolean multipleScraps = mViewTypeCount > 1;
+
+ ArrayList<View> scrapViews = mCurrentScrap;
+ final int count = activeViews.length;
+ for (int i = 0; i < count; ++i) {
+ final View victim = activeViews[i];
+ if (victim != null) {
+ int whichScrap = ((AbsListView.LayoutParams)
+ victim.getLayoutParams()).viewType;
+
+ activeViews[i] = null;
+
+ if (whichScrap == AdapterView.ITEM_VIEW_TYPE_IGNORE) {
+ // Do not move views that should be ignored
+ continue;
+ }
+
+ if (multipleScraps) {
+ scrapViews = mScrapViews[whichScrap];
+ }
+ scrapViews.add(victim);
+
+ if (hasListener) {
+ mRecyclerListener.onMovedToScrapHeap(victim);
+ }
+
+ if (ViewDebug.TRACE_RECYCLER) {
+ ViewDebug.trace(victim,
+ ViewDebug.RecyclerTraceType.MOVE_FROM_ACTIVE_TO_SCRAP_HEAP,
+ mFirstActivePosition + i, -1);
+ }
+ }
+ }
+
+ pruneScrapViews();
+ }
+
+ /**
+ * Makes sure that the size of mScrapViews does not exceed the size of mActiveViews.
+ * (This can happen if an adapter does not recycle its views).
+ */
+ private void pruneScrapViews() {
+ final int maxViews = mActiveViews.length;
+ final int viewTypeCount = mViewTypeCount;
+ final ArrayList<View>[] scrapViews = mScrapViews;
+ for (int i = 0; i < viewTypeCount; ++i) {
+ final ArrayList<View> scrapPile = scrapViews[i];
+ int size = scrapPile.size();
+ final int extras = size - maxViews;
+ size--;
+ for (int j = 0; j < extras; j++) {
+ removeDetachedView(scrapPile.remove(size--), false);
+ }
+ }
+ }
+
+ /**
+ * Puts all views in the scrap heap into the supplied list.
+ */
+ void reclaimScrapViews(List<View> views) {
+ if (mViewTypeCount == 1) {
+ views.addAll(mCurrentScrap);
+ } else {
+ final int viewTypeCount = mViewTypeCount;
+ final ArrayList<View>[] scrapViews = mScrapViews;
+ for (int i = 0; i < viewTypeCount; ++i) {
+ final ArrayList<View> scrapPile = scrapViews[i];
+ views.addAll(scrapPile);
+ }
+ }
+ }
+ }
+}
diff --git a/core/java/android/widget/AbsSeekBar.java b/core/java/android/widget/AbsSeekBar.java
new file mode 100644
index 0000000..1fa7318
--- /dev/null
+++ b/core/java/android/widget/AbsSeekBar.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+public abstract class AbsSeekBar extends ProgressBar {
+
+ private Drawable mThumb;
+ private int mThumbOffset;
+
+ /**
+ * On touch, this offset plus the scaled value from the position of the
+ * touch will form the progress value. Usually 0.
+ */
+ float mTouchProgressOffset;
+
+ /**
+ * Whether this is user seekable.
+ */
+ boolean mIsUserSeekable = true;
+
+ private static final int NO_ALPHA = 0xFF;
+ float mDisabledAlpha;
+
+ public AbsSeekBar(Context context) {
+ super(context);
+ }
+
+ public AbsSeekBar(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public AbsSeekBar(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+
+ TypedArray a = context.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.SeekBar, defStyle, 0);
+ Drawable thumb = a.getDrawable(com.android.internal.R.styleable.SeekBar_thumb);
+ setThumb(thumb);
+ int thumbOffset =
+ a.getDimensionPixelOffset(com.android.internal.R.styleable.SeekBar_thumbOffset, 0);
+ setThumbOffset(thumbOffset);
+ a.recycle();
+
+ a = context.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.Theme, 0, 0);
+ mDisabledAlpha = a.getFloat(com.android.internal.R.styleable.Theme_disabledAlpha, 0.5f);
+ a.recycle();
+ }
+
+ /**
+ * Sets the thumb that will be drawn at the end of the progress meter within the SeekBar
+ *
+ * @param thumb Drawable representing the thumb
+ */
+ public void setThumb(Drawable thumb) {
+ if (thumb != null) {
+ thumb.setCallback(this);
+ }
+ mThumb = thumb;
+ invalidate();
+ }
+
+ /**
+ * @see #setThumbOffset(int)
+ */
+ public int getThumbOffset() {
+ return mThumbOffset;
+ }
+
+ /**
+ * Sets the thumb offset that allows the thumb to extend out of the range of
+ * the track.
+ *
+ * @param thumbOffset The offset amount in pixels.
+ */
+ public void setThumbOffset(int thumbOffset) {
+ mThumbOffset = thumbOffset;
+ invalidate();
+ }
+
+ @Override
+ protected boolean verifyDrawable(Drawable who) {
+ return who == mThumb || super.verifyDrawable(who);
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+
+ Drawable progressDrawable = getProgressDrawable();
+ if (progressDrawable != null) {
+ progressDrawable.setAlpha(isEnabled() ? NO_ALPHA : (int) (NO_ALPHA * mDisabledAlpha));
+ }
+ }
+
+ @Override
+ void onProgressRefresh(float scale, boolean fromTouch) {
+ Drawable thumb = mThumb;
+ if (thumb != null) {
+ setThumbPos(getWidth(), getHeight(), thumb, scale, Integer.MIN_VALUE);
+ /*
+ * Since we draw translated, the drawable's bounds that it signals
+ * for invalidation won't be the actual bounds we want invalidated,
+ * so just invalidate this whole view.
+ */
+ invalidate();
+ }
+ }
+
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ Drawable d = getCurrentDrawable();
+ Drawable thumb = mThumb;
+ int thumbHeight = thumb == null ? 0 : thumb.getIntrinsicHeight();
+ // The max height does not incorporate padding, whereas the height
+ // parameter does
+ int trackHeight = Math.min(mMaxHeight, h - mPaddingTop - mPaddingBottom);
+
+ int max = getMax();
+ float scale = max > 0 ? (float) getProgress() / (float) max : 0;
+
+ if (thumbHeight > trackHeight) {
+ if (thumb != null) {
+ setThumbPos(w, h, thumb, scale, 0);
+ }
+ int gapForCenteringTrack = (thumbHeight - trackHeight) / 2;
+ if (d != null) {
+ // Canvas will be translated by the padding, so 0,0 is where we start drawing
+ d.setBounds(0, gapForCenteringTrack,
+ w - mPaddingRight - mPaddingLeft, h - mPaddingBottom - gapForCenteringTrack
+ - mPaddingTop);
+ }
+ } else {
+ if (d != null) {
+ // Canvas will be translated by the padding, so 0,0 is where we start drawing
+ d.setBounds(0, 0, w - mPaddingRight - mPaddingLeft, h - mPaddingBottom
+ - mPaddingTop);
+ }
+ int gap = (trackHeight - thumbHeight) / 2;
+ if (thumb != null) {
+ setThumbPos(w, h, thumb, scale, gap);
+ }
+ }
+ }
+
+ /**
+ * @param gap If set to {@link Integer#MIN_VALUE}, this will be ignored and
+ * the old vertical bounds will be used.
+ */
+ private void setThumbPos(int w, int h, Drawable thumb, float scale, int gap) {
+ int available = w - mPaddingLeft - mPaddingRight;
+ int thumbWidth = thumb.getIntrinsicWidth();
+ int thumbHeight = thumb.getIntrinsicHeight();
+ available -= thumbWidth;
+
+ // The extra space for the thumb to move on the track
+ available += mThumbOffset * 2;
+
+ int thumbPos = (int) (scale * available);
+
+ int topBound, bottomBound;
+ if (gap == Integer.MIN_VALUE) {
+ Rect oldBounds = thumb.getBounds();
+ topBound = oldBounds.top;
+ bottomBound = oldBounds.bottom;
+ } else {
+ topBound = gap;
+ bottomBound = gap + thumbHeight;
+ }
+
+ // Canvas will be translated, so 0,0 is where we start drawing
+ thumb.setBounds(thumbPos, topBound, thumbPos + thumbWidth, bottomBound);
+ }
+
+ @Override
+ protected synchronized void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ if (mThumb != null) {
+ canvas.save();
+ // Translate the padding. For the x, we need to allow the thumb to
+ // draw in its extra space
+ canvas.translate(mPaddingLeft - mThumbOffset, mPaddingTop);
+ mThumb.draw(canvas);
+ canvas.restore();
+ }
+ }
+
+ @Override
+ protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ Drawable d = getCurrentDrawable();
+
+ int thumbHeight = mThumb == null ? 0 : mThumb.getIntrinsicHeight();
+ int dw = 0;
+ int dh = 0;
+ if (d != null) {
+ dw = Math.max(mMinWidth, Math.min(mMaxWidth, d.getIntrinsicWidth()));
+ dh = Math.max(mMinHeight, Math.min(mMaxHeight, d.getIntrinsicHeight()));
+ dh = Math.max(thumbHeight, dh);
+ }
+ dw += mPaddingLeft + mPaddingRight;
+ dh += mPaddingTop + mPaddingBottom;
+
+ setMeasuredDimension(resolveSize(dw, widthMeasureSpec),
+ resolveSize(dh, heightMeasureSpec));
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (!mIsUserSeekable || !isEnabled()) {
+ return false;
+ }
+
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ onStartTrackingTouch();
+ trackTouchEvent(event);
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ trackTouchEvent(event);
+ break;
+
+ case MotionEvent.ACTION_UP:
+ trackTouchEvent(event);
+ onStopTrackingTouch();
+ break;
+
+ case MotionEvent.ACTION_CANCEL:
+ onStopTrackingTouch();
+ break;
+ }
+ return true;
+ }
+
+ private void trackTouchEvent(MotionEvent event) {
+ final int width = getWidth();
+ final int available = width - mPaddingLeft - mPaddingRight;
+ int x = (int)event.getX();
+ float scale;
+ float progress = 0;
+ if (x < mPaddingLeft) {
+ scale = 0.0f;
+ } else if (x > width - mPaddingRight) {
+ scale = 1.0f;
+ } else {
+ scale = (float)(x - mPaddingLeft) / (float)available;
+ progress = mTouchProgressOffset;
+ }
+
+ final int max = getMax();
+ progress += scale * max;
+ if (progress < 0) {
+ progress = 0;
+ } else if (progress > max) {
+ progress = max;
+ }
+
+ setProgress((int) progress, true);
+ }
+
+ /**
+ * This is called when the user has started touching this widget.
+ */
+ void onStartTrackingTouch() {
+ }
+
+ /**
+ * This is called when the user either releases his touch or the touch is
+ * canceled.
+ */
+ void onStopTrackingTouch() {
+ }
+
+}
diff --git a/core/java/android/widget/AbsSpinner.java b/core/java/android/widget/AbsSpinner.java
new file mode 100644
index 0000000..424a936
--- /dev/null
+++ b/core/java/android/widget/AbsSpinner.java
@@ -0,0 +1,490 @@
+/*
+ * 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.widget;
+
+import com.android.internal.R;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.database.DataSetObserver;
+import android.graphics.Rect;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.SparseArray;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Interpolator;
+
+
+/**
+ * An abstract base class for spinner widgets. SDK users will probably not
+ * need to use this class.
+ *
+ * @attr ref android.R.styleable#AbsSpinner_entries
+ */
+public abstract class AbsSpinner extends AdapterView<SpinnerAdapter> {
+
+ SpinnerAdapter mAdapter;
+
+ int mHeightMeasureSpec;
+ int mWidthMeasureSpec;
+ boolean mBlockLayoutRequests;
+ int mSelectionLeftPadding = 0;
+ int mSelectionTopPadding = 0;
+ int mSelectionRightPadding = 0;
+ int mSelectionBottomPadding = 0;
+ Rect mSpinnerPadding = new Rect();
+ View mSelectedView = null;
+ Interpolator mInterpolator;
+
+ RecycleBin mRecycler = new RecycleBin();
+ private DataSetObserver mDataSetObserver;
+
+
+ /** Temporary frame to hold a child View's frame rectangle */
+ private Rect mTouchFrame;
+
+ public AbsSpinner(Context context) {
+ super(context);
+ initAbsSpinner();
+ }
+
+ public AbsSpinner(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public AbsSpinner(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ initAbsSpinner();
+
+ TypedArray a = context.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.AbsSpinner, defStyle, 0);
+
+ CharSequence[] entries = a.getTextArray(R.styleable.AbsSpinner_entries);
+ if (entries != null) {
+ ArrayAdapter<CharSequence> adapter =
+ new ArrayAdapter<CharSequence>(context,
+ R.layout.simple_spinner_item, entries);
+ adapter.setDropDownViewResource(R.layout.simple_spinner_dropdown_item);
+ setAdapter(adapter);
+ }
+
+ a.recycle();
+ }
+
+ /**
+ * Common code for different constructor flavors
+ */
+ private void initAbsSpinner() {
+ setFocusable(true);
+ setWillNotDraw(false);
+ }
+
+
+ /**
+ * The Adapter is used to provide the data which backs this Spinner.
+ * It also provides methods to transform spinner items based on their position
+ * relative to the selected item.
+ * @param adapter The SpinnerAdapter to use for this Spinner
+ */
+ @Override
+ public void setAdapter(SpinnerAdapter adapter) {
+ if (null != mAdapter) {
+ mAdapter.unregisterDataSetObserver(mDataSetObserver);
+ resetList();
+ }
+
+ mAdapter = adapter;
+
+ mOldSelectedPosition = INVALID_POSITION;
+ mOldSelectedRowId = INVALID_ROW_ID;
+
+ if (mAdapter != null) {
+ mOldItemCount = mItemCount;
+ mItemCount = mAdapter.getCount();
+ checkFocus();
+
+ mDataSetObserver = new AdapterDataSetObserver();
+ mAdapter.registerDataSetObserver(mDataSetObserver);
+
+ int position = mItemCount > 0 ? 0 : INVALID_POSITION;
+
+ setSelectedPositionInt(position);
+ setNextSelectedPositionInt(position);
+
+ if (mItemCount == 0) {
+ // Nothing selected
+ checkSelectionChanged();
+ }
+
+ } else {
+ checkFocus();
+ resetList();
+ // Nothing selected
+ checkSelectionChanged();
+ }
+
+ requestLayout();
+ }
+
+ /**
+ * Clear out all children from the list
+ */
+ void resetList() {
+ mDataChanged = false;
+ mNeedSync = false;
+
+ removeAllViewsInLayout();
+ mOldSelectedPosition = INVALID_POSITION;
+ mOldSelectedRowId = INVALID_ROW_ID;
+
+ setSelectedPositionInt(INVALID_POSITION);
+ setNextSelectedPositionInt(INVALID_POSITION);
+ invalidate();
+ }
+
+ /**
+ * @see android.view.View#measure(int, int)
+ *
+ * Figure out the dimensions of this Spinner. The width comes from
+ * the widthMeasureSpec as Spinnners can't have their width set to
+ * UNSPECIFIED. The height is based on the height of the selected item
+ * plus padding.
+ */
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ int widthSize;
+ int heightSize;
+
+ mSpinnerPadding.left = mPaddingLeft > mSelectionLeftPadding ? mPaddingLeft
+ : mSelectionLeftPadding;
+ mSpinnerPadding.top = mPaddingTop > mSelectionTopPadding ? mPaddingTop
+ : mSelectionTopPadding;
+ mSpinnerPadding.right = mPaddingRight > mSelectionRightPadding ? mPaddingRight
+ : mSelectionRightPadding;
+ mSpinnerPadding.bottom = mPaddingBottom > mSelectionBottomPadding ? mPaddingBottom
+ : mSelectionBottomPadding;
+
+ if (mDataChanged) {
+ handleDataChanged();
+ }
+
+ int preferredHeight = 0;
+ int preferredWidth = 0;
+ boolean needsMeasuring = true;
+
+ int selectedPosition = getSelectedItemPosition();
+ if (selectedPosition >= 0 && mAdapter != null) {
+ // Try looking in the recycler. (Maybe we were measured once already)
+ View view = mRecycler.get(selectedPosition);
+ if (view == null) {
+ // Make a new one
+ view = mAdapter.getView(selectedPosition, null, this);
+ }
+
+ if (view != null) {
+ // Put in recycler for re-measuring and/or layout
+ mRecycler.put(selectedPosition, view);
+ }
+
+ if (view != null) {
+ if (view.getLayoutParams() == null) {
+ mBlockLayoutRequests = true;
+ view.setLayoutParams(generateDefaultLayoutParams());
+ mBlockLayoutRequests = false;
+ }
+ measureChild(view, widthMeasureSpec, heightMeasureSpec);
+
+ preferredHeight = getChildHeight(view) + mSpinnerPadding.top + mSpinnerPadding.bottom;
+ preferredWidth = getChildWidth(view) + mSpinnerPadding.left + mSpinnerPadding.right;
+
+ needsMeasuring = false;
+ }
+ }
+
+ if (needsMeasuring) {
+ // No views -- just use padding
+ preferredHeight = mSpinnerPadding.top + mSpinnerPadding.bottom;
+ if (widthMode == MeasureSpec.UNSPECIFIED) {
+ preferredWidth = mSpinnerPadding.left + mSpinnerPadding.right;
+ }
+ }
+
+ preferredHeight = Math.max(preferredHeight, getSuggestedMinimumHeight());
+ preferredWidth = Math.max(preferredWidth, getSuggestedMinimumWidth());
+
+ heightSize = resolveSize(preferredHeight, heightMeasureSpec);
+ widthSize = resolveSize(preferredWidth, widthMeasureSpec);
+
+ setMeasuredDimension(widthSize, heightSize);
+ mHeightMeasureSpec = heightMeasureSpec;
+ mWidthMeasureSpec = widthMeasureSpec;
+ }
+
+
+ int getChildHeight(View child) {
+ return child.getMeasuredHeight();
+ }
+
+ int getChildWidth(View child) {
+ return child.getMeasuredWidth();
+ }
+
+ @Override
+ protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
+ return new ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.FILL_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+ }
+
+ void recycleAllViews() {
+ int childCount = getChildCount();
+ final AbsSpinner.RecycleBin recycleBin = mRecycler;
+
+ // All views go in recycler
+ for (int i=0; i<childCount; i++) {
+ View v = getChildAt(i);
+ int index = mFirstPosition + i;
+ recycleBin.put(index, v);
+ }
+ }
+
+ @Override
+ void handleDataChanged() {
+ // FIXME -- this is called from both measure and layout.
+ // This is harmless right now, but we don't want to do redundant work if
+ // this gets more complicated
+ super.handleDataChanged();
+ }
+
+
+
+ /**
+ * Jump directly to a specific item in the adapter data.
+ */
+ public void setSelection(int position, boolean animate) {
+ // Animate only if requested position is already on screen somewhere
+ boolean shouldAnimate = animate && mFirstPosition <= position &&
+ position <= mFirstPosition + getChildCount() - 1;
+ setSelectionInt(position, shouldAnimate);
+ }
+
+
+ @Override
+ public void setSelection(int position) {
+ setNextSelectedPositionInt(position);
+ requestLayout();
+ invalidate();
+ }
+
+
+ /**
+ * Makes the item at the supplied position selected.
+ *
+ * @param position Position to select
+ * @param animate Should the transition be animated
+ *
+ */
+ void setSelectionInt(int position, boolean animate) {
+ if (position != mOldSelectedPosition) {
+ mBlockLayoutRequests = true;
+ int delta = position - mSelectedPosition;
+ setNextSelectedPositionInt(position);
+ layout(delta, animate);
+ mBlockLayoutRequests = false;
+ }
+ }
+
+ abstract void layout(int delta, boolean animate);
+
+ @Override
+ public View getSelectedView() {
+ if (mItemCount > 0 && mSelectedPosition >= 0) {
+ return getChildAt(mSelectedPosition - mFirstPosition);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Override to prevent spamming ourselves with layout requests
+ * as we place views
+ *
+ * @see android.view.View#requestLayout()
+ */
+ @Override
+ public void requestLayout() {
+ if (!mBlockLayoutRequests) {
+ super.requestLayout();
+ }
+ }
+
+
+
+ @Override
+ public SpinnerAdapter getAdapter() {
+ return mAdapter;
+ }
+
+ @Override
+ public int getCount() {
+ return mItemCount;
+ }
+
+ /**
+ * Maps a point to a position in the list.
+ *
+ * @param x X in local coordinate
+ * @param y Y in local coordinate
+ * @return The position of the item which contains the specified point, or
+ * {@link #INVALID_POSITION} if the point does not intersect an item.
+ */
+ public int pointToPosition(int x, int y) {
+ Rect frame = mTouchFrame;
+ if (frame == null) {
+ mTouchFrame = new Rect();
+ frame = mTouchFrame;
+ }
+
+ final int count = getChildCount();
+ for (int i = count - 1; i >= 0; i--) {
+ View child = getChildAt(i);
+ if (child.getVisibility() == View.VISIBLE) {
+ child.getHitRect(frame);
+ if (frame.contains(x, y)) {
+ return mFirstPosition + i;
+ }
+ }
+ }
+ return INVALID_POSITION;
+ }
+
+ static class SavedState extends BaseSavedState {
+ long selectedId;
+ int position;
+
+ /**
+ * Constructor called from {@link AbsSpinner#onSaveInstanceState()}
+ */
+ SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ /**
+ * Constructor called from {@link #CREATOR}
+ */
+ private SavedState(Parcel in) {
+ super(in);
+ selectedId = in.readLong();
+ position = in.readInt();
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ super.writeToParcel(out, flags);
+ out.writeLong(selectedId);
+ out.writeInt(position);
+ }
+
+ @Override
+ public String toString() {
+ return "AbsSpinner.SavedState{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " selectedId=" + selectedId
+ + " position=" + position + "}";
+ }
+
+ public static final Parcelable.Creator<SavedState> CREATOR
+ = new Parcelable.Creator<SavedState>() {
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ Parcelable superState = super.onSaveInstanceState();
+ SavedState ss = new SavedState(superState);
+ ss.selectedId = getSelectedItemId();
+ if (ss.selectedId >= 0) {
+ ss.position = getSelectedItemPosition();
+ } else {
+ ss.position = INVALID_POSITION;
+ }
+ return ss;
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ SavedState ss = (SavedState) state;
+
+ super.onRestoreInstanceState(ss.getSuperState());
+
+ if (ss.selectedId >= 0) {
+ mDataChanged = true;
+ mNeedSync = true;
+ mSyncRowId = ss.selectedId;
+ mSyncPosition = ss.position;
+ mSyncMode = SYNC_SELECTED_POSITION;
+ requestLayout();
+ }
+ }
+
+ class RecycleBin {
+ private SparseArray<View> mScrapHeap = new SparseArray<View>();
+
+ public void put(int position, View v) {
+ mScrapHeap.put(position, v);
+ }
+
+ View get(int position) {
+ // System.out.print("Looking for " + position);
+ View result = mScrapHeap.get(position);
+ if (result != null) {
+ // System.out.println(" HIT");
+ mScrapHeap.delete(position);
+ } else {
+ // System.out.println(" MISS");
+ }
+ return result;
+ }
+
+ View peek(int position) {
+ // System.out.print("Looking for " + position);
+ return mScrapHeap.get(position);
+ }
+
+ void clear() {
+ final SparseArray<View> scrapHeap = mScrapHeap;
+ final int count = scrapHeap.size();
+ for (int i = 0; i < count; i++) {
+ final View view = scrapHeap.valueAt(i);
+ if (view != null) {
+ removeDetachedView(view, true);
+ }
+ }
+ scrapHeap.clear();
+ }
+ }
+}
diff --git a/core/java/android/widget/AbsoluteLayout.java b/core/java/android/widget/AbsoluteLayout.java
new file mode 100644
index 0000000..36a3b10
--- /dev/null
+++ b/core/java/android/widget/AbsoluteLayout.java
@@ -0,0 +1,216 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.RemoteViews.RemoteView;
+
+
+/**
+ * A layout that lets you specify exact locations (x/y coordinates) of its
+ * children. Absolute layouts are less flexible and harder to maintain than
+ * other types of layouts without absolute positioning.
+ *
+ * <p><strong>XML attributes</strong></p> <p> See {@link
+ * android.R.styleable#ViewGroup ViewGroup Attributes}, {@link
+ * android.R.styleable#View View Attributes}</p>
+ */
+@RemoteView
+public class AbsoluteLayout extends ViewGroup {
+ public AbsoluteLayout(Context context) {
+ super(context);
+ }
+
+ public AbsoluteLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public AbsoluteLayout(Context context, AttributeSet attrs,
+ int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int count = getChildCount();
+
+ int maxHeight = 0;
+ int maxWidth = 0;
+
+ // Find out how big everyone wants to be
+ measureChildren(widthMeasureSpec, heightMeasureSpec);
+
+ // Find rightmost and bottom-most child
+ for (int i = 0; i < count; i++) {
+ View child = getChildAt(i);
+ if (child.getVisibility() != GONE) {
+ int childRight;
+ int childBottom;
+
+ AbsoluteLayout.LayoutParams lp
+ = (AbsoluteLayout.LayoutParams) child.getLayoutParams();
+
+ childRight = lp.x + child.getMeasuredWidth();
+ childBottom = lp.y + child.getMeasuredHeight();
+
+ maxWidth = Math.max(maxWidth, childRight);
+ maxHeight = Math.max(maxHeight, childBottom);
+ }
+ }
+
+ // Account for padding too
+ maxWidth += mPaddingLeft + mPaddingRight;
+ maxHeight += mPaddingTop + mPaddingBottom;
+
+ // Check against minimum height and width
+ maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
+ maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
+
+ setMeasuredDimension(resolveSize(maxWidth, widthMeasureSpec),
+ resolveSize(maxHeight, heightMeasureSpec));
+ }
+
+ /**
+ * Returns a set of layout parameters with a width of
+ * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT},
+ * a height of {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}
+ * and with the coordinates (0, 0).
+ */
+ @Override
+ protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
+ return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, 0, 0);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t,
+ int r, int b) {
+ int count = getChildCount();
+
+ for (int i = 0; i < count; i++) {
+ View child = getChildAt(i);
+ if (child.getVisibility() != GONE) {
+
+ AbsoluteLayout.LayoutParams lp =
+ (AbsoluteLayout.LayoutParams) child.getLayoutParams();
+
+ int childLeft = mPaddingLeft + lp.x;
+ int childTop = mPaddingTop + lp.y;
+ child.layout(childLeft, childTop,
+ childLeft + child.getMeasuredWidth(),
+ childTop + child.getMeasuredHeight());
+
+ }
+ }
+ }
+
+ @Override
+ public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
+ return new AbsoluteLayout.LayoutParams(getContext(), attrs);
+ }
+
+ // Override to allow type-checking of LayoutParams.
+ @Override
+ protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+ return p instanceof AbsoluteLayout.LayoutParams;
+ }
+
+ @Override
+ protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
+ return new LayoutParams(p);
+ }
+
+ /**
+ * Per-child layout information associated with AbsoluteLayout.
+ * See
+ * {@link android.R.styleable#AbsoluteLayout_Layout Absolute Layout Attributes}
+ * for a list of all child view attributes that this class supports.
+ */
+ public static class LayoutParams extends ViewGroup.LayoutParams {
+ /**
+ * The horizontal, or X, location of the child within the view group.
+ */
+ public int x;
+ /**
+ * The vertical, or Y, location of the child within the view group.
+ */
+ public int y;
+
+ /**
+ * Creates a new set of layout parameters with the specified width,
+ * height and location.
+ *
+ * @param width the width, either {@link #FILL_PARENT},
+ {@link #WRAP_CONTENT} or a fixed size in pixels
+ * @param height the height, either {@link #FILL_PARENT},
+ {@link #WRAP_CONTENT} or a fixed size in pixels
+ * @param x the X location of the child
+ * @param y the Y location of the child
+ */
+ public LayoutParams(int width, int height, int x, int y) {
+ super(width, height);
+ this.x = x;
+ this.y = y;
+ }
+
+ /**
+ * Creates a new set of layout parameters. The values are extracted from
+ * the supplied attributes set and context. The XML attributes mapped
+ * to this set of layout parameters are:
+ *
+ * <ul>
+ * <li><code>layout_x</code>: the X location of the child</li>
+ * <li><code>layout_y</code>: the Y location of the child</li>
+ * <li>All the XML attributes from
+ * {@link android.view.ViewGroup.LayoutParams}</li>
+ * </ul>
+ *
+ * @param c the application environment
+ * @param attrs the set of attributes fom which to extract the layout
+ * parameters values
+ */
+ public LayoutParams(Context c, AttributeSet attrs) {
+ super(c, attrs);
+ TypedArray a = c.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.AbsoluteLayout_Layout);
+ x = a.getDimensionPixelOffset(
+ com.android.internal.R.styleable.AbsoluteLayout_Layout_layout_x, 0);
+ y = a.getDimensionPixelOffset(
+ com.android.internal.R.styleable.AbsoluteLayout_Layout_layout_y, 0);
+ a.recycle();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public LayoutParams(ViewGroup.LayoutParams source) {
+ super(source);
+ }
+
+ @Override
+ public String debug(String output) {
+ return output + "Absolute.LayoutParams={width="
+ + sizeToString(width) + ", height=" + sizeToString(height)
+ + " x=" + x + " y=" + y + "}";
+ }
+ }
+}
+
+
diff --git a/core/java/android/widget/Adapter.java b/core/java/android/widget/Adapter.java
new file mode 100644
index 0000000..e952dd5
--- /dev/null
+++ b/core/java/android/widget/Adapter.java
@@ -0,0 +1,149 @@
+/*
+ * 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.widget;
+
+import android.database.DataSetObserver;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * An Adapter object acts as a bridge between an {@link AdapterView} and the
+ * underlying data for that view. The Adapter provides access to the data items.
+ * The Adapter is also responsible for making a {@link android.view.View} for
+ * each item in the data set.
+ *
+ * @see android.widget.ArrayAdapter
+ * @see android.widget.CursorAdapter
+ * @see android.widget.SimpleCursorAdapter
+ */
+public interface Adapter {
+ /**
+ * Register an observer that is called when changes happen to the data used by this adapter.
+ *
+ * @param observer the object that gets notified when the data set changes.
+ */
+ void registerDataSetObserver(DataSetObserver observer);
+
+ /**
+ * Unregister an observer that has previously been registered with this
+ * adapter via {@link #registerDataSetObserver}.
+ *
+ * @param observer the object to unregister.
+ */
+ void unregisterDataSetObserver(DataSetObserver observer);
+
+ /**
+ * How many items are in the data set represented by this Adapter.
+ *
+ * @return Count of items.
+ */
+ int getCount();
+
+ /**
+ * Get the data item associated with the specified position in the data set.
+ *
+ * @param position Position of the item whose data we want within the adapter's
+ * data set.
+ * @return The data at the specified position.
+ */
+ Object getItem(int position);
+
+ /**
+ * Get the row id associated with the specified position in the list.
+ *
+ * @param position The position of the item within the adapter's data set whose row id we want.
+ * @return The id of the item at the specified position.
+ */
+ long getItemId(int position);
+
+ /**
+ * Indicated whether the item ids are stable across changes to the
+ * underlying data.
+ *
+ * @return True if the same id always refers to the same object.
+ */
+ boolean hasStableIds();
+
+ /**
+ * Get a View that displays the data at the specified position in the data set. You can either
+ * create a View manually or inflate it from an XML layout file. When the View is inflated, the
+ * parent View (GridView, ListView...) will apply default layout parameters unless you use
+ * {@link android.view.LayoutInflater#inflate(int, android.view.ViewGroup, boolean)}
+ * to specify a root view and to prevent attachment to the root.
+ *
+ * @param position The position of the item within the adapter's data set of the item whose view
+ * we want.
+ * @param convertView The old view to reuse, if possible. Note: You should check that this view
+ * is non-null and of an appropriate type before using. If it is not possible to convert
+ * this view to display the correct data, this method can create a new view.
+ * @param parent The parent that this view will eventually be attached to
+ * @return A View corresponding to the data at the specified position.
+ */
+ View getView(int position, View convertView, ViewGroup parent);
+
+ /**
+ * An item view type that causes the {@link AdapterView} to ignore the item
+ * view. For example, this can be used if the client does not want a
+ * particular view to be given for conversion in
+ * {@link #getView(int, View, ViewGroup)}.
+ *
+ * @see #getItemViewType(int)
+ * @see #getViewTypeCount()
+ */
+ static final int IGNORE_ITEM_VIEW_TYPE = AdapterView.ITEM_VIEW_TYPE_IGNORE;
+
+ /**
+ * Get the type of View that will be created by {@link #getView} for the specified item.
+ *
+ * @param position The position of the item within the adapter's data set whose view type we
+ * want.
+ * @return An integer representing the type of View. Two views should share the same type if one
+ * can be converted to the other in {@link #getView}. Note: Integers must be in the
+ * range 0 to {@link #getViewTypeCount} - 1. {@link #IGNORE_ITEM_VIEW_TYPE} can
+ * also be returned.
+ * @see IGNORE_ITEM_VIEW_TYPE
+ */
+ int getItemViewType(int position);
+
+ /**
+ * <p>
+ * Returns the number of types of Views that will be created by
+ * {@link #getView}. Each type represents a set of views that can be
+ * converted in {@link #getView}. If the adapter always returns the same
+ * type of View for all items, this method should return 1.
+ * </p>
+ * <p>
+ * This method will only be called when when the adapter is set on the
+ * the {@link AdapterView}.
+ * </p>
+ *
+ * @return The number of types of Views that will be created by this adapter
+ */
+ int getViewTypeCount();
+
+ static final int NO_SELECTION = Integer.MIN_VALUE;
+
+ /**
+ * @return true if this adapter doesn't contain any data. This is used to determine
+ * whether the empty view should be displayed. A typical implementation will return
+ * getCount() == 0 but since getCount() includes the headers and footers, specialized
+ * adapters might want a different behavior.
+ */
+ boolean isEmpty();
+}
+
diff --git a/core/java/android/widget/AdapterView.java b/core/java/android/widget/AdapterView.java
new file mode 100644
index 0000000..e096612
--- /dev/null
+++ b/core/java/android/widget/AdapterView.java
@@ -0,0 +1,1094 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.database.DataSetObserver;
+import android.os.Handler;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.util.AttributeSet;
+import android.util.SparseArray;
+import android.view.ContextMenu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewDebug;
+import android.view.SoundEffectConstants;
+import android.view.ContextMenu.ContextMenuInfo;
+
+
+/**
+ * An AdapterView is a view whose children are determined by an {@link Adapter}.
+ *
+ * <p>
+ * See {@link ListView}, {@link GridView}, {@link Spinner} and
+ * {@link Gallery} for commonly used subclasses of AdapterView.
+ */
+public abstract class AdapterView<T extends Adapter> extends ViewGroup {
+
+ /**
+ * The item view type returned by {@link Adapter#getItemViewType(int)} when
+ * the adapter does not want the item's view recycled.
+ */
+ public static final int ITEM_VIEW_TYPE_IGNORE = -1;
+
+ /**
+ * The item view type returned by {@link Adapter#getItemViewType(int)} when
+ * the item is a header or footer.
+ */
+ public static final int ITEM_VIEW_TYPE_HEADER_OR_FOOTER = -2;
+
+ /**
+ * The position of the first child displayed
+ */
+ @ViewDebug.ExportedProperty
+ int mFirstPosition = 0;
+
+ /**
+ * The offset in pixels from the top of the AdapterView to the top
+ * of the view to select during the next layout.
+ */
+ int mSpecificTop;
+
+ /**
+ * Position from which to start looking for mSyncRowId
+ */
+ int mSyncPosition;
+
+ /**
+ * Row id to look for when data has changed
+ */
+ long mSyncRowId = INVALID_ROW_ID;
+
+ /**
+ * Height of the view when mSyncPosition and mSyncRowId where set
+ */
+ long mSyncHeight;
+
+ /**
+ * True if we need to sync to mSyncRowId
+ */
+ boolean mNeedSync = false;
+
+ /**
+ * Indicates whether to sync based on the selection or position. Possible
+ * values are {@link #SYNC_SELECTED_POSITION} or
+ * {@link #SYNC_FIRST_POSITION}.
+ */
+ int mSyncMode;
+
+ /**
+ * Our height after the last layout
+ */
+ private int mLayoutHeight;
+
+ /**
+ * Sync based on the selected child
+ */
+ static final int SYNC_SELECTED_POSITION = 0;
+
+ /**
+ * Sync based on the first child displayed
+ */
+ static final int SYNC_FIRST_POSITION = 1;
+
+ /**
+ * Maximum amount of time to spend in {@link #findSyncPosition()}
+ */
+ static final int SYNC_MAX_DURATION_MILLIS = 100;
+
+ /**
+ * Indicates that this view is currently being laid out.
+ */
+ boolean mInLayout = false;
+
+ /**
+ * The listener that receives notifications when an item is selected.
+ */
+ OnItemSelectedListener mOnItemSelectedListener;
+
+ /**
+ * The listener that receives notifications when an item is clicked.
+ */
+ OnItemClickListener mOnItemClickListener;
+
+ /**
+ * The listener that receives notifications when an item is long clicked.
+ */
+ OnItemLongClickListener mOnItemLongClickListener;
+
+ /**
+ * True if the data has changed since the last layout
+ */
+ boolean mDataChanged;
+
+ /**
+ * The position within the adapter's data set of the item to select
+ * during the next layout.
+ */
+ @ViewDebug.ExportedProperty
+ int mNextSelectedPosition = INVALID_POSITION;
+
+ /**
+ * The item id of the item to select during the next layout.
+ */
+ long mNextSelectedRowId = INVALID_ROW_ID;
+
+ /**
+ * The position within the adapter's data set of the currently selected item.
+ */
+ @ViewDebug.ExportedProperty
+ int mSelectedPosition = INVALID_POSITION;
+
+ /**
+ * The item id of the currently selected item.
+ */
+ long mSelectedRowId = INVALID_ROW_ID;
+
+ /**
+ * View to show if there are no items to show.
+ */
+ View mEmptyView;
+
+ /**
+ * The number of items in the current adapter.
+ */
+ @ViewDebug.ExportedProperty
+ int mItemCount;
+
+ /**
+ * The number of items in the adapter before a data changed event occured.
+ */
+ int mOldItemCount;
+
+ /**
+ * Represents an invalid position. All valid positions are in the range 0 to 1 less than the
+ * number of items in the current adapter.
+ */
+ public static final int INVALID_POSITION = -1;
+
+ /**
+ * Represents an empty or invalid row id
+ */
+ public static final long INVALID_ROW_ID = Long.MIN_VALUE;
+
+ /**
+ * The last selected position we used when notifying
+ */
+ int mOldSelectedPosition = INVALID_POSITION;
+
+ /**
+ * The id of the last selected position we used when notifying
+ */
+ long mOldSelectedRowId = INVALID_ROW_ID;
+
+ /**
+ * Indicates what focusable state is requested when calling setFocusable().
+ * In addition to this, this view has other criteria for actually
+ * determining the focusable state (such as whether its empty or the text
+ * filter is shown).
+ *
+ * @see #setFocusable(boolean)
+ * @see #checkFocus()
+ */
+ private boolean mDesiredFocusableState;
+ private boolean mDesiredFocusableInTouchModeState;
+
+ private SelectionNotifier mSelectionNotifier;
+ /**
+ * When set to true, calls to requestLayout() will not propagate up the parent hierarchy.
+ * This is used to layout the children during a layout pass.
+ */
+ boolean mBlockLayoutRequests = false;
+
+ public AdapterView(Context context) {
+ super(context);
+ }
+
+ public AdapterView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public AdapterView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+
+ /**
+ * Interface definition for a callback to be invoked when an item in this
+ * AdapterView has been clicked.
+ */
+ public interface OnItemClickListener {
+
+ /**
+ * Callback method to be invoked when an item in this AdapterView has
+ * been clicked.
+ * <p>
+ * Implementers can call getItemAtPosition(position) if they need
+ * to access the data associated with the selected item.
+ *
+ * @param parent The AdapterView where the click happened.
+ * @param view The view within the AdapterView that was clicked (this
+ * will be a view provided by the adapter)
+ * @param position The position of the view in the adapter.
+ * @param id The row id of the item that was clicked.
+ */
+ void onItemClick(AdapterView<?> parent, View view, int position, long id);
+ }
+
+ /**
+ * Register a callback to be invoked when an item in this AdapterView has
+ * been clicked.
+ *
+ * @param listener The callback that will be invoked.
+ */
+ public void setOnItemClickListener(OnItemClickListener listener) {
+ mOnItemClickListener = listener;
+ }
+
+ /**
+ * @return The callback to be invoked with an item in this AdapterView has
+ * been clicked, or null id no callback has been set.
+ */
+ public final OnItemClickListener getOnItemClickListener() {
+ return mOnItemClickListener;
+ }
+
+ /**
+ * Call the OnItemClickListener, if it is defined.
+ *
+ * @param view The view within the AdapterView that was clicked.
+ * @param position The position of the view in the adapter.
+ * @param id The row id of the item that was clicked.
+ * @return True if there was an assigned OnItemClickListener that was
+ * called, false otherwise is returned.
+ */
+ public boolean performItemClick(View view, int position, long id) {
+ if (mOnItemClickListener != null) {
+ playSoundEffect(SoundEffectConstants.CLICK);
+ mOnItemClickListener.onItemClick(this, view, position, id);
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when an item in this
+ * view has been clicked and held.
+ */
+ public interface OnItemLongClickListener {
+ /**
+ * Callback method to be invoked when an item in this view has been
+ * clicked and held.
+ *
+ * Implementers can call getItemAtPosition(position) if they need to access
+ * the data associated with the selected item.
+ *
+ * @param parent The AbsListView where the click happened
+ * @param view The view within the AbsListView that was clicked
+ * @param position The position of the view in the list
+ * @param id The row id of the item that was clicked
+ *
+ * @return true if the callback consumed the long click, false otherwise
+ */
+ boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id);
+ }
+
+
+ /**
+ * Register a callback to be invoked when an item in this AdapterView has
+ * been clicked and held
+ *
+ * @param listener The callback that will run
+ */
+ public void setOnItemLongClickListener(OnItemLongClickListener listener) {
+ if (!isLongClickable()) {
+ setLongClickable(true);
+ }
+ mOnItemLongClickListener = listener;
+ }
+
+ /**
+ * @return The callback to be invoked with an item in this AdapterView has
+ * been clicked and held, or null id no callback as been set.
+ */
+ public final OnItemLongClickListener getOnItemLongClickListener() {
+ return mOnItemLongClickListener;
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when
+ * an item in this view has been selected.
+ */
+ public interface OnItemSelectedListener {
+ /**
+ * Callback method to be invoked when an item in this view has been
+ * selected.
+ *
+ * Impelmenters can call getItemAtPosition(position) if they need to access the
+ * data associated with the selected item.
+ *
+ * @param parent The AdapterView where the selection happened
+ * @param view The view within the AdapterView that was clicked
+ * @param position The position of the view in the adapter
+ * @param id The row id of the item that is selected
+ */
+ void onItemSelected(AdapterView<?> parent, View view, int position, long id);
+
+ /**
+ * Callback method to be invoked when the selection disappears from this
+ * view. The selection can disappear for instance when touch is activated
+ * or when the adapter becomes empty.
+ *
+ * @param parent The AdapterView that now contains no selected item.
+ */
+ void onNothingSelected(AdapterView<?> parent);
+ }
+
+
+ /**
+ * Register a callback to be invoked when an item in this AdapterView has
+ * been selected.
+ *
+ * @param listener The callback that will run
+ */
+ public void setOnItemSelectedListener(OnItemSelectedListener listener) {
+ mOnItemSelectedListener = listener;
+ }
+
+ public final OnItemSelectedListener getOnItemSelectedListener() {
+ return mOnItemSelectedListener;
+ }
+
+ /**
+ * Extra menu information provided to the
+ * {@link android.view.View.OnCreateContextMenuListener#onCreateContextMenu(ContextMenu, View, ContextMenuInfo) }
+ * callback when a context menu is brought up for this AdapterView.
+ *
+ */
+ public static class AdapterContextMenuInfo implements ContextMenu.ContextMenuInfo {
+
+ public AdapterContextMenuInfo(View targetView, int position, long id) {
+ this.targetView = targetView;
+ this.position = position;
+ this.id = id;
+ }
+
+ /**
+ * The child view for which the context menu is being displayed. This
+ * will be one of the children of this AdapterView.
+ */
+ public View targetView;
+
+ /**
+ * The position in the adapter for which the context menu is being
+ * displayed.
+ */
+ public int position;
+
+ /**
+ * The row id of the item for which the context menu is being displayed.
+ */
+ public long id;
+ }
+
+ /**
+ * Returns the adapter currently associated with this widget.
+ *
+ * @return The adapter used to provide this view's content.
+ */
+ public abstract T getAdapter();
+
+ /**
+ * Sets the adapter that provides the data and the views to represent the data
+ * in this widget.
+ *
+ * @param adapter The adapter to use to create this view's content.
+ */
+ public abstract void setAdapter(T adapter);
+
+ /**
+ * This method is not supported and throws an UnsupportedOperationException when called.
+ *
+ * @param child Ignored.
+ *
+ * @throws UnsupportedOperationException Every time this method is invoked.
+ */
+ @Override
+ public void addView(View child) {
+ throw new UnsupportedOperationException("addView(View) is not supported in AdapterView");
+ }
+
+ /**
+ * This method is not supported and throws an UnsupportedOperationException when called.
+ *
+ * @param child Ignored.
+ * @param index Ignored.
+ *
+ * @throws UnsupportedOperationException Every time this method is invoked.
+ */
+ @Override
+ public void addView(View child, int index) {
+ throw new UnsupportedOperationException("addView(View, int) is not supported in AdapterView");
+ }
+
+ /**
+ * This method is not supported and throws an UnsupportedOperationException when called.
+ *
+ * @param child Ignored.
+ * @param params Ignored.
+ *
+ * @throws UnsupportedOperationException Every time this method is invoked.
+ */
+ @Override
+ public void addView(View child, LayoutParams params) {
+ throw new UnsupportedOperationException("addView(View, LayoutParams) "
+ + "is not supported in AdapterView");
+ }
+
+ /**
+ * This method is not supported and throws an UnsupportedOperationException when called.
+ *
+ * @param child Ignored.
+ * @param index Ignored.
+ * @param params Ignored.
+ *
+ * @throws UnsupportedOperationException Every time this method is invoked.
+ */
+ @Override
+ public void addView(View child, int index, LayoutParams params) {
+ throw new UnsupportedOperationException("addView(View, int, LayoutParams) "
+ + "is not supported in AdapterView");
+ }
+
+ /**
+ * This method is not supported and throws an UnsupportedOperationException when called.
+ *
+ * @param child Ignored.
+ *
+ * @throws UnsupportedOperationException Every time this method is invoked.
+ */
+ @Override
+ public void removeView(View child) {
+ throw new UnsupportedOperationException("removeView(View) is not supported in AdapterView");
+ }
+
+ /**
+ * This method is not supported and throws an UnsupportedOperationException when called.
+ *
+ * @param index Ignored.
+ *
+ * @throws UnsupportedOperationException Every time this method is invoked.
+ */
+ @Override
+ public void removeViewAt(int index) {
+ throw new UnsupportedOperationException("removeViewAt(int) is not supported in AdapterView");
+ }
+
+ /**
+ * This method is not supported and throws an UnsupportedOperationException when called.
+ *
+ * @throws UnsupportedOperationException Every time this method is invoked.
+ */
+ @Override
+ public void removeAllViews() {
+ throw new UnsupportedOperationException("removeAllViews() is not supported in AdapterView");
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ mLayoutHeight = getHeight();
+ }
+
+ /**
+ * Return the position of the currently selected item within the adapter's data set
+ *
+ * @return int Position (starting at 0), or {@link #INVALID_POSITION} if there is nothing selected.
+ */
+ public int getSelectedItemPosition() {
+ return mNextSelectedPosition;
+ }
+
+ /**
+ * @return The id corresponding to the currently selected item, or {@link #INVALID_ROW_ID}
+ * if nothing is selected.
+ */
+ public long getSelectedItemId() {
+ return mNextSelectedRowId;
+ }
+
+ /**
+ * @return The view corresponding to the currently selected item, or null
+ * if nothing is selected
+ */
+ public abstract View getSelectedView();
+
+ /**
+ * @return The data corresponding to the currently selected item, or
+ * null if there is nothing selected.
+ */
+ public Object getSelectedItem() {
+ T adapter = getAdapter();
+ int selection = getSelectedItemPosition();
+ if (adapter != null && adapter.getCount() > 0 && selection >= 0) {
+ return adapter.getItem(selection);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * @return The number of items owned by the Adapter associated with this
+ * AdapterView. (This is the number of data items, which may be
+ * larger than the number of visible view.)
+ */
+ public int getCount() {
+ return mItemCount;
+ }
+
+ /**
+ * Get the position within the adapter's data set for the view, where view is a an adapter item
+ * or a descendant of an adapter item.
+ *
+ * @param view an adapter item, or a descendant of an adapter item. This must be visible in this
+ * AdapterView at the time of the call.
+ * @return the position within the adapter's data set of the view, or {@link #INVALID_POSITION}
+ * if the view does not correspond to a list item (or it is not currently visible).
+ */
+ public int getPositionForView(View view) {
+ View listItem = view;
+ try {
+ View v;
+ while (!(v = (View) listItem.getParent()).equals(this)) {
+ listItem = v;
+ }
+ } catch (ClassCastException e) {
+ // We made it up to the window without find this list view
+ return INVALID_POSITION;
+ }
+
+ // Search the children for the list item
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ if (getChildAt(i).equals(listItem)) {
+ return mFirstPosition + i;
+ }
+ }
+
+ // Child not found!
+ return INVALID_POSITION;
+ }
+
+ /**
+ * Returns the position within the adapter's data set for the first item
+ * displayed on screen.
+ *
+ * @return The position within the adapter's data set
+ */
+ public int getFirstVisiblePosition() {
+ return mFirstPosition;
+ }
+
+ /**
+ * Returns the position within the adapter's data set for the last item
+ * displayed on screen.
+ *
+ * @return The position within the adapter's data set
+ */
+ public int getLastVisiblePosition() {
+ return mFirstPosition + getChildCount() - 1;
+ }
+
+ /**
+ * Sets the currently selected item
+ * @param position Index (starting at 0) of the data item to be selected.
+ */
+ public abstract void setSelection(int position);
+
+ /**
+ * Sets the view to show if the adapter is empty
+ */
+ public void setEmptyView(View emptyView) {
+ mEmptyView = emptyView;
+
+ final T adapter = getAdapter();
+ final boolean empty = ((adapter == null) || adapter.isEmpty());
+ updateEmptyStatus(empty);
+ }
+
+ /**
+ * When the current adapter is empty, the AdapterView can display a special view
+ * call the empty view. The empty view is used to provide feedback to the user
+ * that no data is available in this AdapterView.
+ *
+ * @return The view to show if the adapter is empty.
+ */
+ public View getEmptyView() {
+ return mEmptyView;
+ }
+
+ /**
+ * Indicates whether this view is in filter mode. Filter mode can for instance
+ * be enabled by a user when typing on the keyboard.
+ *
+ * @return True if the view is in filter mode, false otherwise.
+ */
+ boolean isInFilterMode() {
+ return false;
+ }
+
+ @Override
+ public void setFocusable(boolean focusable) {
+ final T adapter = getAdapter();
+ final boolean empty = adapter == null || adapter.getCount() == 0;
+
+ mDesiredFocusableState = focusable;
+ if (!focusable) {
+ mDesiredFocusableInTouchModeState = false;
+ }
+
+ super.setFocusable(focusable && (!empty || isInFilterMode()));
+ }
+
+ @Override
+ public void setFocusableInTouchMode(boolean focusable) {
+ final T adapter = getAdapter();
+ final boolean empty = adapter == null || adapter.getCount() == 0;
+
+ mDesiredFocusableInTouchModeState = focusable;
+ if (focusable) {
+ mDesiredFocusableState = true;
+ }
+
+ super.setFocusableInTouchMode(focusable && (!empty || isInFilterMode()));
+ }
+
+ void checkFocus() {
+ final T adapter = getAdapter();
+ final boolean empty = adapter == null || adapter.getCount() == 0;
+ final boolean focusable = !empty || isInFilterMode();
+ // The order in which we set focusable in touch mode/focusable may matter
+ // for the client, see View.setFocusableInTouchMode() comments for more
+ // details
+ super.setFocusableInTouchMode(focusable && mDesiredFocusableInTouchModeState);
+ super.setFocusable(focusable && mDesiredFocusableState);
+ if (mEmptyView != null) {
+ updateEmptyStatus((adapter == null) || adapter.isEmpty());
+ }
+ }
+
+ /**
+ * Update the status of the list based on the empty parameter. If empty is true and
+ * we have an empty view, display it. In all the other cases, make sure that the listview
+ * is VISIBLE and that the empty view is GONE (if it's not null).
+ */
+ private void updateEmptyStatus(boolean empty) {
+ if (isInFilterMode()) {
+ empty = false;
+ }
+
+ if (empty) {
+ if (mEmptyView != null) {
+ mEmptyView.setVisibility(View.VISIBLE);
+ setVisibility(View.GONE);
+ } else {
+ // If the caller just removed our empty view, make sure the list view is visible
+ setVisibility(View.VISIBLE);
+ }
+
+ // We are now GONE, so pending layouts will not be dispatched.
+ // Force one here to make sure that the state of the list matches
+ // the state of the adapter.
+ if (mDataChanged) {
+ this.onLayout(false, mLeft, mTop, mRight, mBottom);
+ }
+ } else {
+ if (mEmptyView != null) mEmptyView.setVisibility(View.GONE);
+ setVisibility(View.VISIBLE);
+ }
+ }
+
+ /**
+ * Gets the data associated with the specified position in the list.
+ *
+ * @param position Which data to get
+ * @return The data associated with the specified position in the list
+ */
+ public Object getItemAtPosition(int position) {
+ T adapter = getAdapter();
+ return (adapter == null || position < 0) ? null : adapter.getItem(position);
+ }
+
+ public long getItemIdAtPosition(int position) {
+ T adapter = getAdapter();
+ return (adapter == null || position < 0) ? INVALID_ROW_ID : adapter.getItemId(position);
+ }
+
+ @Override
+ public void setOnClickListener(OnClickListener l) {
+ throw new RuntimeException("Don't call setOnClickListener for an AdapterView. "
+ + "You probably want setOnItemClickListener instead");
+ }
+
+ /**
+ * Override to prevent freezing of any views created by the adapter.
+ */
+ @Override
+ protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
+ dispatchFreezeSelfOnly(container);
+ }
+
+ /**
+ * Override to prevent thawing of any views created by the adapter.
+ */
+ @Override
+ protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
+ dispatchThawSelfOnly(container);
+ }
+
+ class AdapterDataSetObserver extends DataSetObserver {
+
+ private Parcelable mInstanceState = null;
+
+ @Override
+ public void onChanged() {
+ mDataChanged = true;
+ mOldItemCount = mItemCount;
+ mItemCount = getAdapter().getCount();
+
+ // Detect the case where a cursor that was previously invalidated has
+ // been repopulated with new data.
+ if (AdapterView.this.getAdapter().hasStableIds() && mInstanceState != null
+ && mOldItemCount == 0 && mItemCount > 0) {
+ AdapterView.this.onRestoreInstanceState(mInstanceState);
+ mInstanceState = null;
+ } else {
+ rememberSyncState();
+ }
+ checkFocus();
+ requestLayout();
+ }
+
+ @Override
+ public void onInvalidated() {
+ mDataChanged = true;
+
+ if (AdapterView.this.getAdapter().hasStableIds()) {
+ // Remember the current state for the case where our hosting activity is being
+ // stopped and later restarted
+ mInstanceState = AdapterView.this.onSaveInstanceState();
+ }
+
+ // Data is invalid so we should reset our state
+ mOldItemCount = mItemCount;
+ mItemCount = 0;
+ mSelectedPosition = INVALID_POSITION;
+ mSelectedRowId = INVALID_ROW_ID;
+ mNextSelectedPosition = INVALID_POSITION;
+ mNextSelectedRowId = INVALID_ROW_ID;
+ mNeedSync = false;
+ checkSelectionChanged();
+
+ checkFocus();
+ requestLayout();
+ }
+
+ public void clearSavedState() {
+ mInstanceState = null;
+ }
+ }
+
+ private class SelectionNotifier extends Handler implements Runnable {
+ public void run() {
+ if (mDataChanged) {
+ // Data has changed between when this SelectionNotifier
+ // was posted and now. We need to wait until the AdapterView
+ // has been synched to the new data.
+ post(this);
+ } else {
+ fireOnSelected();
+ }
+ }
+ }
+
+ void selectionChanged() {
+ if (mOnItemSelectedListener != null) {
+ if (mInLayout || mBlockLayoutRequests) {
+ // If we are in a layout traversal, defer notification
+ // by posting. This ensures that the view tree is
+ // in a consistent state and is able to accomodate
+ // new layout or invalidate requests.
+ if (mSelectionNotifier == null) {
+ mSelectionNotifier = new SelectionNotifier();
+ }
+ mSelectionNotifier.post(mSelectionNotifier);
+ } else {
+ fireOnSelected();
+ }
+ }
+ }
+
+ private void fireOnSelected() {
+ if (mOnItemSelectedListener == null)
+ return;
+
+ int selection = this.getSelectedItemPosition();
+ if (selection >= 0) {
+ View v = getSelectedView();
+ mOnItemSelectedListener.onItemSelected(this, v, selection,
+ getAdapter().getItemId(selection));
+ } else {
+ mOnItemSelectedListener.onNothingSelected(this);
+ }
+ }
+
+ @Override
+ protected boolean canAnimate() {
+ return super.canAnimate() && mItemCount > 0;
+ }
+
+ void handleDataChanged() {
+ final int count = mItemCount;
+ boolean found = false;
+
+ if (count > 0) {
+
+ int newPos;
+
+ // Find the row we are supposed to sync to
+ if (mNeedSync) {
+ // Update this first, since setNextSelectedPositionInt inspects
+ // it
+ mNeedSync = false;
+
+ // See if we can find a position in the new data with the same
+ // id as the old selection
+ newPos = findSyncPosition();
+ if (newPos >= 0) {
+ // Verify that new selection is selectable
+ int selectablePos = lookForSelectablePosition(newPos, true);
+ if (selectablePos == newPos) {
+ // Same row id is selected
+ setNextSelectedPositionInt(newPos);
+ found = true;
+ }
+ }
+ }
+ if (!found) {
+ // Try to use the same position if we can't find matching data
+ newPos = getSelectedItemPosition();
+
+ // Pin position to the available range
+ if (newPos >= count) {
+ newPos = count - 1;
+ }
+ if (newPos < 0) {
+ newPos = 0;
+ }
+
+ // Make sure we select something selectable -- first look down
+ int selectablePos = lookForSelectablePosition(newPos, true);
+ if (selectablePos < 0) {
+ // Looking down didn't work -- try looking up
+ selectablePos = lookForSelectablePosition(newPos, false);
+ }
+ if (selectablePos >= 0) {
+ setNextSelectedPositionInt(selectablePos);
+ checkSelectionChanged();
+ found = true;
+ }
+ }
+ }
+ if (!found) {
+ // Nothing is selected
+ mSelectedPosition = INVALID_POSITION;
+ mSelectedRowId = INVALID_ROW_ID;
+ mNextSelectedPosition = INVALID_POSITION;
+ mNextSelectedRowId = INVALID_ROW_ID;
+ mNeedSync = false;
+ checkSelectionChanged();
+ }
+ }
+
+ void checkSelectionChanged() {
+ if ((mSelectedPosition != mOldSelectedPosition) || (mSelectedRowId != mOldSelectedRowId)) {
+ selectionChanged();
+ mOldSelectedPosition = mSelectedPosition;
+ mOldSelectedRowId = mSelectedRowId;
+ }
+ }
+
+ /**
+ * Searches the adapter for a position matching mSyncRowId. The search starts at mSyncPosition
+ * and then alternates between moving up and moving down until 1) we find the right position, or
+ * 2) we run out of time, or 3) we have looked at every position
+ *
+ * @return Position of the row that matches mSyncRowId, or {@link #INVALID_POSITION} if it can't
+ * be found
+ */
+ int findSyncPosition() {
+ int count = mItemCount;
+
+ if (count == 0) {
+ return INVALID_POSITION;
+ }
+
+ long idToMatch = mSyncRowId;
+ int seed = mSyncPosition;
+
+ // If there isn't a selection don't hunt for it
+ if (idToMatch == INVALID_ROW_ID) {
+ return INVALID_POSITION;
+ }
+
+ // Pin seed to reasonable values
+ seed = Math.max(0, seed);
+ seed = Math.min(count - 1, seed);
+
+ long endTime = SystemClock.uptimeMillis() + SYNC_MAX_DURATION_MILLIS;
+
+ long rowId;
+
+ // first position scanned so far
+ int first = seed;
+
+ // last position scanned so far
+ int last = seed;
+
+ // True if we should move down on the next iteration
+ boolean next = false;
+
+ // True when we have looked at the first item in the data
+ boolean hitFirst;
+
+ // True when we have looked at the last item in the data
+ boolean hitLast;
+
+ // Get the item ID locally (instead of getItemIdAtPosition), so
+ // we need the adapter
+ T adapter = getAdapter();
+ if (adapter == null) {
+ return INVALID_POSITION;
+ }
+
+ while (SystemClock.uptimeMillis() <= endTime) {
+ rowId = adapter.getItemId(seed);
+ if (rowId == idToMatch) {
+ // Found it!
+ return seed;
+ }
+
+ hitLast = last == count - 1;
+ hitFirst = first == 0;
+
+ if (hitLast && hitFirst) {
+ // Looked at everything
+ break;
+ }
+
+ if (hitFirst || (next && !hitLast)) {
+ // Either we hit the top, or we are trying to move down
+ last++;
+ seed = last;
+ // Try going up next time
+ next = false;
+ } else if (hitLast || (!next && !hitFirst)) {
+ // Either we hit the bottom, or we are trying to move up
+ first--;
+ seed = first;
+ // Try going down next time
+ next = true;
+ }
+
+ }
+
+ return INVALID_POSITION;
+ }
+
+ /**
+ * Find a position that can be selected (i.e., is not a separator).
+ *
+ * @param position The starting position to look at.
+ * @param lookDown Whether to look down for other positions.
+ * @return The next selectable position starting at position and then searching either up or
+ * down. Returns {@link #INVALID_POSITION} if nothing can be found.
+ */
+ int lookForSelectablePosition(int position, boolean lookDown) {
+ return position;
+ }
+
+ /**
+ * Utility to keep mSelectedPosition and mSelectedRowId in sync
+ * @param position Our current position
+ */
+ void setSelectedPositionInt(int position) {
+ mSelectedPosition = position;
+ mSelectedRowId = getItemIdAtPosition(position);
+ }
+
+ /**
+ * Utility to keep mNextSelectedPosition and mNextSelectedRowId in sync
+ * @param position Intended value for mSelectedPosition the next time we go
+ * through layout
+ */
+ void setNextSelectedPositionInt(int position) {
+ mNextSelectedPosition = position;
+ mNextSelectedRowId = getItemIdAtPosition(position);
+ // If we are trying to sync to the selection, update that too
+ if (mNeedSync && mSyncMode == SYNC_SELECTED_POSITION && position >= 0) {
+ mSyncPosition = position;
+ mSyncRowId = mNextSelectedRowId;
+ }
+ }
+
+ /**
+ * Remember enough information to restore the screen state when the data has
+ * changed.
+ *
+ */
+ void rememberSyncState() {
+ if (getChildCount() > 0) {
+ mNeedSync = true;
+ mSyncHeight = mLayoutHeight;
+ if (mSelectedPosition >= 0) {
+ // Sync the selection state
+ View v = getChildAt(mSelectedPosition - mFirstPosition);
+ mSyncRowId = mNextSelectedRowId;
+ mSyncPosition = mNextSelectedPosition;
+ if (v != null) {
+ mSpecificTop = v.getTop();
+ }
+ mSyncMode = SYNC_SELECTED_POSITION;
+ } else {
+ // Sync the based on the offset of the first view
+ View v = getChildAt(0);
+ T adapter = getAdapter();
+ if (mFirstPosition >= 0 && mFirstPosition < adapter.getCount()) {
+ mSyncRowId = adapter.getItemId(mFirstPosition);
+ } else {
+ mSyncRowId = NO_ID;
+ }
+ mSyncPosition = mFirstPosition;
+ if (v != null) {
+ mSpecificTop = v.getTop();
+ }
+ mSyncMode = SYNC_FIRST_POSITION;
+ }
+ }
+ }
+}
diff --git a/core/java/android/widget/AnalogClock.java b/core/java/android/widget/AnalogClock.java
new file mode 100644
index 0000000..808104e
--- /dev/null
+++ b/core/java/android/widget/AnalogClock.java
@@ -0,0 +1,241 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.BroadcastReceiver;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.util.AttributeSet;
+import android.view.View;
+import android.pim.Time;
+
+import java.util.TimeZone;
+
+/**
+ * This widget display an analogic clock with two hands for hours and
+ * minutes.
+ */
+public class AnalogClock extends View {
+ private Time mCalendar;
+
+ private Drawable mHourHand;
+ private Drawable mMinuteHand;
+ private Drawable mDial;
+
+ private int mDialWidth;
+ private int mDialHeight;
+
+ private boolean mAttached;
+ private long mLastTime;
+
+ private final Handler mHandler = new Handler();
+ private float mMinutes;
+ private float mHour;
+ private boolean mChanged;
+
+ public AnalogClock(Context context) {
+ this(context, null);
+ }
+
+ public AnalogClock(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public AnalogClock(Context context, AttributeSet attrs,
+ int defStyle) {
+ super(context, attrs, defStyle);
+ Resources r = mContext.getResources();
+ TypedArray a =
+ context.obtainStyledAttributes(
+ attrs, com.android.internal.R.styleable.AnalogClock, defStyle, 0);
+
+ mDial = a.getDrawable(com.android.internal.R.styleable.AnalogClock_dial);
+ if (mDial == null) {
+ mDial = r.getDrawable(com.android.internal.R.drawable.clock_dial);
+ }
+
+ mHourHand = a.getDrawable(com.android.internal.R.styleable.AnalogClock_hand_hour);
+ if (mHourHand == null) {
+ mHourHand = r.getDrawable(com.android.internal.R.drawable.clock_hand_hour);
+ }
+
+ mMinuteHand = a.getDrawable(com.android.internal.R.styleable.AnalogClock_hand_minute);
+ if (mMinuteHand == null) {
+ mMinuteHand = r.getDrawable(com.android.internal.R.drawable.clock_hand_minute);
+ }
+
+ mCalendar = new Time();
+
+ mDialWidth = mDial.getIntrinsicWidth();
+ mDialHeight = mDial.getIntrinsicHeight();
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ onTimeChanged();
+ if (!mAttached) {
+ mAttached = true;
+ IntentFilter filter = new IntentFilter();
+
+ filter.addAction(Intent.ACTION_TIME_TICK);
+ filter.addAction(Intent.ACTION_TIME_CHANGED);
+ filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
+
+ getContext().registerReceiver(mIntentReceiver, filter, null, mHandler);
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ if (mAttached) {
+ getContext().unregisterReceiver(mIntentReceiver);
+ mAttached = false;
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+
+ int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+ int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+
+ float hScale = 1.0f;
+ float vScale = 1.0f;
+
+ if (widthMode != MeasureSpec.UNSPECIFIED && widthSize < mDialWidth) {
+ hScale = (float) widthSize / (float) mDialWidth;
+ }
+
+ if (heightMode != MeasureSpec.UNSPECIFIED && heightSize < mDialHeight) {
+ vScale = (float )heightSize / (float) mDialHeight;
+ }
+
+ float scale = Math.min(hScale, vScale);
+
+ setMeasuredDimension(resolveSize((int) (mDialWidth * scale), widthMeasureSpec),
+ resolveSize((int) (mDialHeight * scale), heightMeasureSpec));
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ mChanged = true;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ boolean changed = mChanged;
+ if (changed) {
+ mChanged = false;
+ }
+
+ int availableWidth = mRight - mLeft;
+ int availableHeight = mBottom - mTop;
+
+ int x = availableWidth / 2;
+ int y = availableHeight / 2;
+
+ final Drawable dial = mDial;
+ int w = dial.getIntrinsicWidth();
+ int h = dial.getIntrinsicHeight();
+
+ boolean scaled = false;
+
+ if (availableWidth < w || availableHeight < h) {
+ scaled = true;
+ float scale = Math.min((float) availableWidth / (float) w,
+ (float) availableHeight / (float) h);
+ canvas.save();
+ canvas.scale(scale, scale, x, y);
+ }
+
+ if (changed) {
+ dial.setBounds(x - (w / 2), y - (h / 2), x + (w / 2), y + (h / 2));
+ }
+ dial.draw(canvas);
+
+ canvas.save();
+ canvas.rotate(mHour / 12.0f * 360.0f, x, y);
+ final Drawable hourHand = mHourHand;
+ if (changed) {
+ w = hourHand.getIntrinsicWidth();
+ h = hourHand.getIntrinsicHeight();
+ hourHand.setBounds(x - (w / 2), y - (h / 2), x + (w / 2), y + (h / 2));
+ }
+ hourHand.draw(canvas);
+ canvas.restore();
+
+ canvas.save();
+ canvas.rotate(mMinutes / 60.0f * 360.0f, x, y);
+
+ final Drawable minuteHand = mMinuteHand;
+ if (changed) {
+ w = minuteHand.getIntrinsicWidth();
+ h = minuteHand.getIntrinsicHeight();
+ minuteHand.setBounds(x - (w / 2), y - (h / 2), x + (w / 2), y + (h / 2));
+ }
+ minuteHand.draw(canvas);
+ canvas.restore();
+
+ if (scaled) {
+ canvas.restore();
+ }
+ }
+
+ private void onTimeChanged() {
+ long time = System.currentTimeMillis();
+ mCalendar.set(time);
+ mLastTime = time;
+
+ int hour = mCalendar.hour;
+ int minute = mCalendar.minute;
+ int second = mCalendar.second;
+
+ mMinutes = minute + second / 60.0f;
+ mHour = hour + mMinutes / 60.0f;
+ mChanged = true;
+ }
+
+ private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction().equals(Intent.ACTION_TIMEZONE_CHANGED)) {
+ String tz = intent.getStringExtra("time-zone");
+ mCalendar = new Time(TimeZone.getTimeZone(tz).getID());
+ } else {
+ mCalendar = new Time();
+ }
+
+ onTimeChanged();
+
+ invalidate();
+ }
+ };
+}
diff --git a/core/java/android/widget/AppSecurityPermissions.java b/core/java/android/widget/AppSecurityPermissions.java
new file mode 100755
index 0000000..582117f
--- /dev/null
+++ b/core/java/android/widget/AppSecurityPermissions.java
@@ -0,0 +1,383 @@
+/*
+**
+** Copyright 2007, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+package android.widget;
+
+import com.android.internal.R;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageParser;
+import android.content.pm.PermissionGroupInfo;
+import android.content.pm.PermissionInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Set;
+
+/**
+ * This class contains the SecurityPermissions view implementation.
+ * Initially the package's advanced or dangerous security permissions
+ * are displayed under categorized
+ * groups. Clicking on the additional permissions presents
+ * extended information consisting of all groups and permissions.
+ * To use this view define a LinearLayout or any ViewGroup and add this
+ * view by instantiating AppSecurityPermissions and invoking getPermissionsView.
+ *
+ * {@hide}
+ */
+public class AppSecurityPermissions implements View.OnClickListener {
+
+ private enum State {
+ NO_PERMS,
+ DANGEROUS_ONLY,
+ NORMAL_ONLY,
+ BOTH
+ }
+
+ private final String TAG = "AppSecurityPermissions";
+ private boolean localLOGV = false;
+ private Context mContext;
+ private LayoutInflater mInflater;
+ private PackageManager mPm;
+ private LinearLayout mPermsView;
+ private HashMap<String, String> mDangerousMap;
+ private HashMap<String, String> mNormalMap;
+ private ArrayList<PermissionInfo> mPermsList;
+ private String mDefaultGrpLabel;
+ private String mDefaultGrpName="DefaultGrp";
+ private String mPermFormat;
+ private Drawable mNormalIcon;
+ private Drawable mDangerousIcon;
+ private boolean mExpanded;
+ private Drawable mShowMaxIcon;
+ private Drawable mShowMinIcon;
+ private View mShowMore;
+ private TextView mShowMoreText;
+ private ImageView mShowMoreIcon;
+ private State mCurrentState;
+ private LinearLayout mNonDangerousList;
+ private LinearLayout mDangerousList;
+ private HashMap<String, String> mGroupLabelCache;
+ private View mNoPermsView;
+
+ public AppSecurityPermissions(Context context) {
+ this(context, null);
+ }
+
+ public AppSecurityPermissions(Context context, ArrayList<PermissionInfo> permList) {
+ mContext = context;
+ mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mPm = context.getPackageManager();
+ mPermsList = permList;
+ mPermsView = (LinearLayout) mInflater.inflate(R.layout.app_perms_summary, null);
+ mShowMore = mPermsView.findViewById(R.id.show_more);
+ mShowMoreIcon = (ImageView) mShowMore.findViewById(R.id.show_more_icon);
+ mShowMoreText = (TextView) mShowMore.findViewById(R.id.show_more_text);
+ mDangerousList = (LinearLayout) mPermsView.findViewById(R.id.dangerous_perms_list);
+ mNonDangerousList = (LinearLayout) mPermsView.findViewById(R.id.non_dangerous_perms_list);
+ mNoPermsView = mPermsView.findViewById(R.id.no_permissions);
+
+ // Set up the LinearLayout that acts like a list item.
+ mShowMore.setClickable(true);
+ mShowMore.setOnClickListener(this);
+ mShowMore.setFocusable(true);
+ mShowMore.setBackgroundResource(android.R.drawable.list_selector_background);
+
+ // Pick up from framework resources instead.
+ mDefaultGrpLabel = mContext.getString(R.string.default_permission_group);
+ mPermFormat = mContext.getString(R.string.permissions_format);
+ mNormalIcon = mContext.getResources().getDrawable(R.drawable.ic_text_dot);
+ mDangerousIcon = mContext.getResources().getDrawable(R.drawable.ic_bullet_key_permission);
+ mShowMaxIcon = mContext.getResources().getDrawable(R.drawable.expander_ic_maximized);
+ mShowMinIcon = mContext.getResources().getDrawable(R.drawable.expander_ic_minimized);
+ }
+
+ public void setSecurityPermissionsView() {
+ setPermissions(mPermsList);
+ }
+
+ public void setSecurityPermissionsView(Uri pkgURI) {
+ final String archiveFilePath = pkgURI.getPath();
+ PackageParser packageParser = new PackageParser(archiveFilePath);
+ File sourceFile = new File(archiveFilePath);
+ DisplayMetrics metrics = new DisplayMetrics();
+ metrics.setToDefaults();
+ PackageParser.Package pkgInfo = packageParser.parsePackage(sourceFile,
+ archiveFilePath, metrics, 0);
+ mPermsList = generatePermissionsInfo(pkgInfo.requestedPermissions);
+ //For packages that havent been installed we need the application info object
+ //to load the labels and other resources.
+ setPermissions(mPermsList, pkgInfo.applicationInfo);
+ }
+
+ public void setSecurityPermissionsView(PackageInfo pInfo) {
+ mPermsList = generatePermissionsInfo(pInfo.requestedPermissions);
+ setPermissions(mPermsList);
+ }
+
+ public View getPermissionsView() {
+ return mPermsView;
+ }
+
+ /**
+ * Canonicalizes the group description before it is displayed to the user.
+ *
+ * TODO check for internationalization issues remove trailing '.' in str1
+ */
+ private String canonicalizeGroupDesc(String groupDesc) {
+ if ((groupDesc == null) || (groupDesc.length() == 0)) {
+ return null;
+ }
+ // Both str1 and str2 are non-null and are non-zero in size.
+ int len = groupDesc.length();
+ if(groupDesc.charAt(len-1) == '.') {
+ groupDesc = groupDesc.substring(0, len-1);
+ }
+ return groupDesc;
+ }
+
+ /**
+ * Utility method that concatenates two strings defined by mPermFormat.
+ * a null value is returned if both str1 and str2 are null, if one of the strings
+ * is null the other non null value is returned without formatting
+ * this is to placate initial error checks
+ */
+ private String formatPermissions(String groupDesc, String permDesc) {
+ if(groupDesc == null) {
+ return permDesc;
+ }
+ groupDesc = canonicalizeGroupDesc(groupDesc);
+ if(permDesc == null) {
+ return groupDesc;
+ }
+ return String.format(mPermFormat, groupDesc, permDesc);
+ }
+
+ /**
+ * Utility method that concatenates two strings defined by mPermFormat.
+ */
+ private String formatPermissions(String groupDesc, CharSequence permDesc) {
+ groupDesc = canonicalizeGroupDesc(groupDesc);
+ if(permDesc == null) {
+ return groupDesc;
+ }
+ // Format only if str1 and str2 are not null.
+ return formatPermissions(groupDesc, permDesc.toString());
+ }
+
+ private ArrayList<PermissionInfo> generatePermissionsInfo(String[] strList) {
+ ArrayList<PermissionInfo> permInfoList = new ArrayList<PermissionInfo>();
+ if(strList == null) {
+ return permInfoList;
+ }
+ PermissionInfo tmpPermInfo = null;
+ for(int i = 0; i < strList.length; i++) {
+ try {
+ tmpPermInfo = mPm.getPermissionInfo(strList[i], 0);
+ permInfoList.add(tmpPermInfo);
+ } catch (NameNotFoundException e) {
+ Log.i(TAG, "Ignoring unknown permisison:"+strList[i]);
+ continue;
+ }
+ }
+ return permInfoList;
+ }
+
+ private ArrayList<PermissionInfo> generatePermissionsInfo(ArrayList<String> strList) {
+ ArrayList<PermissionInfo> permInfoList = new ArrayList<PermissionInfo>();
+ if(strList != null) {
+ PermissionInfo tmpPermInfo = null;
+ for(String permName:strList) {
+ try {
+ tmpPermInfo = mPm.getPermissionInfo(permName, 0);
+ permInfoList.add(tmpPermInfo);
+ } catch (NameNotFoundException e) {
+ Log.i(TAG, "Ignoring unknown permisison:"+permName);
+ continue;
+ }
+ }
+ }
+ return permInfoList;
+ }
+
+ private String getGroupLabel(String grpName) {
+ if (grpName == null) {
+ //return default label
+ return mDefaultGrpLabel;
+ }
+ String cachedLabel = mGroupLabelCache.get(grpName);
+ if (cachedLabel != null) {
+ return cachedLabel;
+ }
+
+ PermissionGroupInfo pgi;
+ try {
+ pgi = mPm.getPermissionGroupInfo(grpName, 0);
+ } catch (NameNotFoundException e) {
+ Log.i(TAG, "Invalid group name:" + grpName);
+ return null;
+ }
+ String label = pgi.loadLabel(mPm).toString();
+ mGroupLabelCache.put(grpName, label);
+ return label;
+ }
+
+ /**
+ * Utility method that displays permissions from a map containing group name and
+ * list of permission descriptions.
+ */
+ private void displayPermissions(boolean dangerous) {
+ HashMap<String, String> permInfoMap = dangerous ? mDangerousMap : mNormalMap;
+ LinearLayout permListView = dangerous ? mDangerousList : mNonDangerousList;
+ permListView.removeAllViews();
+
+ Set<String> permInfoStrSet = permInfoMap.keySet();
+ for (String loopPermGrpInfoStr : permInfoStrSet) {
+ String grpLabel = getGroupLabel(loopPermGrpInfoStr);
+ //guaranteed that grpLabel wont be null since permissions without groups
+ //will belong to the default group
+ if(localLOGV) Log.i(TAG, "Adding view group:" + grpLabel + ", desc:"
+ + permInfoMap.get(loopPermGrpInfoStr));
+ permListView.addView(getPermissionItemView(grpLabel,
+ permInfoMap.get(loopPermGrpInfoStr), dangerous));
+ }
+ }
+
+ private void displayNoPermissions() {
+ mNoPermsView.setVisibility(View.VISIBLE);
+ }
+
+ private View getPermissionItemView(String grpName, String permList,
+ boolean dangerous) {
+ View permView = mInflater.inflate(R.layout.app_permission_item, null);
+ Drawable icon = dangerous ? mDangerousIcon : mNormalIcon;
+ int grpColor = dangerous ? R.color.perms_dangerous_grp_color :
+ R.color.perms_normal_grp_color;
+ int permColor = dangerous ? R.color.perms_dangerous_perm_color :
+ R.color.perms_normal_perm_color;
+
+ TextView permGrpView = (TextView) permView.findViewById(R.id.permission_group);
+ TextView permDescView = (TextView) permView.findViewById(R.id.permission_list);
+ permGrpView.setTextColor(mContext.getResources().getColor(grpColor));
+ permDescView.setTextColor(mContext.getResources().getColor(permColor));
+
+ ImageView imgView = (ImageView)permView.findViewById(R.id.perm_icon);
+ imgView.setImageDrawable(icon);
+ if(grpName != null) {
+ permGrpView.setText(grpName);
+ permDescView.setText(permList);
+ } else {
+ permGrpView.setText(permList);
+ permDescView.setVisibility(View.GONE);
+ }
+ return permView;
+ }
+
+ private void showPermissions() {
+
+ switch(mCurrentState) {
+ case NO_PERMS:
+ displayNoPermissions();
+ break;
+
+ case DANGEROUS_ONLY:
+ displayPermissions(true);
+ break;
+
+ case NORMAL_ONLY:
+ displayPermissions(false);
+ break;
+
+ case BOTH:
+ displayPermissions(true);
+ if (mExpanded) {
+ displayPermissions(false);
+ mShowMoreIcon.setImageDrawable(mShowMaxIcon);
+ mShowMoreText.setText(R.string.perms_hide);
+ mNonDangerousList.setVisibility(View.VISIBLE);
+ } else {
+ mShowMoreIcon.setImageDrawable(mShowMinIcon);
+ mShowMoreText.setText(R.string.perms_show_all);
+ mNonDangerousList.setVisibility(View.GONE);
+ }
+ mShowMore.setVisibility(View.VISIBLE);
+ break;
+ }
+ }
+
+ private boolean isDisplayablePermission(PermissionInfo pInfo) {
+ if(pInfo.protectionLevel == PermissionInfo.PROTECTION_DANGEROUS ||
+ pInfo.protectionLevel == PermissionInfo.PROTECTION_NORMAL) {
+ return true;
+ }
+ return false;
+ }
+
+ private void setPermissions(ArrayList<PermissionInfo> permList) {
+ setPermissions(permList, null);
+ }
+
+ private void setPermissions(ArrayList<PermissionInfo> permList, ApplicationInfo appInfo) {
+ mDangerousMap = new HashMap<String, String>();
+ mNormalMap = new HashMap<String, String>();
+ mGroupLabelCache = new HashMap<String, String>();
+ //add the default label so that uncategorized permissions can go here
+ mGroupLabelCache.put(mDefaultGrpName, mDefaultGrpLabel);
+ if (permList != null) {
+ for (PermissionInfo pInfo : permList) {
+ if(!isDisplayablePermission(pInfo)) {
+ continue;
+ }
+ String grpName = (pInfo.group == null) ? mDefaultGrpName : pInfo.group;
+ HashMap<String, String> permInfoMap =
+ (pInfo.protectionLevel == PermissionInfo.PROTECTION_DANGEROUS) ?
+ mDangerousMap : mNormalMap;
+ // Check to make sure we have a label for the group
+ if (getGroupLabel(grpName) == null) {
+ continue;
+ }
+ CharSequence permDesc = pInfo.loadLabel(mPm);
+ String grpDesc = permInfoMap.get(grpName);
+ permInfoMap.put(grpName, formatPermissions(grpDesc, permDesc));
+ if(localLOGV) Log.i(TAG, pInfo.name + " : " + permDesc+" : " + grpName);
+ }
+ }
+
+ mCurrentState = State.NO_PERMS;
+ if(mDangerousMap.size() > 0) {
+ mCurrentState = (mNormalMap.size() > 0) ? State.BOTH : State.DANGEROUS_ONLY;
+ } else if(mNormalMap.size() > 0) {
+ mCurrentState = State.NORMAL_ONLY;
+ }
+ if(localLOGV) Log.i(TAG, "mCurrentState=" + mCurrentState);
+ showPermissions();
+ }
+
+ public void onClick(View v) {
+ if(localLOGV) Log.i(TAG, "mExpanded="+mExpanded);
+ mExpanded = !mExpanded;
+ showPermissions();
+ }
+}
diff --git a/core/java/android/widget/ArrayAdapter.java b/core/java/android/widget/ArrayAdapter.java
new file mode 100644
index 0000000..fe50a01
--- /dev/null
+++ b/core/java/android/widget/ArrayAdapter.java
@@ -0,0 +1,455 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * A ListAdapter that manages a ListView backed by an array of arbitrary
+ * objects. By default this class expects that the provided resource id referecnes
+ * a single TextView. If you want to use a more complex layout, use the constructors that
+ * also takes a field id. That field id should reference a TextView in the larger layout
+ * resource.
+ *
+ * However the TextView is referenced, it will be filled with the toString() of each object in
+ * the array. You can add lists or arrays of custom objects. Override the toString() method
+ * of your objects to determine what text will be displayed for the item in the list.
+ *
+ * To use something other than TextViews for the array display, for instance, ImageViews,
+ * or to have some of data besides toString() results fill the views,
+ * override {@link #getView(int, View, ViewGroup)} to return the type of view you want.
+ */
+public class ArrayAdapter<T> extends BaseAdapter implements Filterable {
+ /**
+ * Contains the list of objects that represent the data of this ArrayAdapter.
+ * The content of this list is referred to as "the array" in the documentation.
+ */
+ private List<T> mObjects;
+
+ /**
+ * Lock used to modify the content of {@link #mObjects}. Any write operation
+ * performed on the array should be synchronized on this lock. This lock is also
+ * used by the filter (see {@link #getFilter()} to make a synchronized copy of
+ * the original array of data.
+ */
+ private final Object mLock = new Object();
+
+ /**
+ * The resource indicating what views to inflate to display the content of this
+ * array adapter.
+ */
+ private int mResource;
+
+ /**
+ * The resource indicating what views to inflate to display the content of this
+ * array adapter in a drop down widget.
+ */
+ private int mDropDownResource;
+
+ /**
+ * If the inflated resource is not a TextView, {@link #mFieldId} is used to find
+ * a TextView inside the inflated views hierarchy. This field must contain the
+ * identifier that matches the one defined in the resource file.
+ */
+ private int mFieldId = 0;
+
+ /**
+ * Indicates whether or not {@link #notifyDataSetChanged()} must be called whenever
+ * {@link #mObjects} is modified.
+ */
+ private boolean mNotifyOnChange = true;
+
+ private Context mContext;
+
+ private ArrayList<T> mOriginalValues;
+ private ArrayFilter mFilter;
+
+ private LayoutInflater mInflater;
+
+ /**
+ * Constructor
+ *
+ * @param context The current context.
+ * @param textViewResourceId The resource ID for a layout file containing a TextView to use when
+ * instantiating views.
+ */
+ public ArrayAdapter(Context context, int textViewResourceId) {
+ init(context, textViewResourceId, 0, new ArrayList<T>());
+ }
+
+ /**
+ * Constructor
+ *
+ * @param context The current context.
+ * @param resource The resource ID for a layout file containing a layout to use when
+ * instantiating views.
+ * @param textViewResourceId The id of the TextView within the layout resource to be populated
+ */
+ public ArrayAdapter(Context context, int resource, int textViewResourceId) {
+ init(context, resource, textViewResourceId, new ArrayList<T>());
+ }
+
+ /**
+ * Constructor
+ *
+ * @param context The current context.
+ * @param textViewResourceId The resource ID for a layout file containing a TextView to use when
+ * instantiating views.
+ * @param objects The objects to represent in the ListView.
+ */
+ public ArrayAdapter(Context context, int textViewResourceId, T[] objects) {
+ init(context, textViewResourceId, 0, Arrays.asList(objects));
+ }
+
+ /**
+ * Constructor
+ *
+ * @param context The current context.
+ * @param resource The resource ID for a layout file containing a layout to use when
+ * instantiating views.
+ * @param textViewResourceId The id of the TextView within the layout resource to be populated
+ * @param objects The objects to represent in the ListView.
+ */
+ public ArrayAdapter(Context context, int resource, int textViewResourceId, T[] objects) {
+ init(context, resource, textViewResourceId, Arrays.asList(objects));
+ }
+
+ /**
+ * Constructor
+ *
+ * @param context The current context.
+ * @param textViewResourceId The resource ID for a layout file containing a TextView to use when
+ * instantiating views.
+ * @param objects The objects to represent in the ListView.
+ */
+ public ArrayAdapter(Context context, int textViewResourceId, List<T> objects) {
+ init(context, textViewResourceId, 0, objects);
+ }
+
+ /**
+ * Constructor
+ *
+ * @param context The current context.
+ * @param resource The resource ID for a layout file containing a layout to use when
+ * instantiating views.
+ * @param textViewResourceId The id of the TextView within the layout resource to be populated
+ * @param objects The objects to represent in the ListView.
+ */
+ public ArrayAdapter(Context context, int resource, int textViewResourceId, List<T> objects) {
+ init(context, resource, textViewResourceId, objects);
+ }
+
+ /**
+ * Adds the specified object at the end of the array.
+ *
+ * @param object The object to add at the end of the array.
+ */
+ public void add(T object) {
+ if (mOriginalValues != null) {
+ synchronized (mLock) {
+ mOriginalValues.add(object);
+ if (mNotifyOnChange) notifyDataSetChanged();
+ }
+ } else {
+ mObjects.add(object);
+ if (mNotifyOnChange) notifyDataSetChanged();
+ }
+ }
+
+ /**
+ * Inserts the spcified object at the specified index in the array.
+ *
+ * @param object The object to insert into the array.
+ * @param index The index at which the object must be inserted.
+ */
+ public void insert(T object, int index) {
+ if (mOriginalValues != null) {
+ synchronized (mLock) {
+ mOriginalValues.add(index, object);
+ if (mNotifyOnChange) notifyDataSetChanged();
+ }
+ } else {
+ mObjects.add(index, object);
+ if (mNotifyOnChange) notifyDataSetChanged();
+ }
+ }
+
+ /**
+ * Removes the specified object from the array.
+ *
+ * @param object The object to remove.
+ */
+ public void remove(T object) {
+ if (mOriginalValues != null) {
+ synchronized (mLock) {
+ mOriginalValues.remove(object);
+ }
+ } else {
+ mObjects.remove(object);
+ }
+ if (mNotifyOnChange) notifyDataSetChanged();
+ }
+
+ /**
+ * Remove all elements from the list.
+ */
+ public void clear() {
+ if (mOriginalValues != null) {
+ synchronized (mLock) {
+ mOriginalValues.clear();
+ }
+ } else {
+ mObjects.clear();
+ }
+ if (mNotifyOnChange) notifyDataSetChanged();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void notifyDataSetChanged() {
+ super.notifyDataSetChanged();
+ mNotifyOnChange = true;
+ }
+
+ /**
+ * Control whether methods that change the list ({@link #add},
+ * {@link #insert}, {@link #remove}, {@link #clear}) automatically call
+ * {@link #notifyDataSetChanged}. If set to false, caller must
+ * manually call notifyDataSetChanged() to have the changes
+ * reflected in the attached view.
+ *
+ * The default is true, and calling notifyDataSetChanged()
+ * resets the flag to true.
+ *
+ * @param notifyOnChange if true, modifications to the list will
+ * automatically call {@link
+ * #notifyDataSetChanged}
+ */
+ public void setNotifyOnChange(boolean notifyOnChange) {
+ mNotifyOnChange = notifyOnChange;
+ }
+
+ private void init(Context context, int resource, int textViewResourceId, List<T> objects) {
+ mContext = context;
+ mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mResource = mDropDownResource = resource;
+ mObjects = objects;
+ mFieldId = textViewResourceId;
+ }
+
+ /**
+ * Returns the context associated with this array adapter. The context is used
+ * to create views from the resource passed to the constructor.
+ *
+ * @return The Context associated with this adapter.
+ */
+ public Context getContext() {
+ return mContext;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public int getCount() {
+ return mObjects.size();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public T getItem(int position) {
+ return mObjects.get(position);
+ }
+
+ /**
+ * Returns the position of the specified item in the array.
+ *
+ * @param item The item to retrieve the position of.
+ *
+ * @return The position of the specified item.
+ */
+ public int getPosition(T item) {
+ return mObjects.indexOf(item);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public long getItemId(int position) {
+ return position;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public View getView(int position, View convertView, ViewGroup parent) {
+ return createViewFromResource(position, convertView, parent, mResource);
+ }
+
+ private View createViewFromResource(int position, View convertView, ViewGroup parent,
+ int resource) {
+ View view;
+ TextView text;
+
+ if (convertView == null) {
+ view = mInflater.inflate(resource, parent, false);
+ } else {
+ view = convertView;
+ }
+
+ try {
+ if (mFieldId == 0) {
+ // If no custom field is assigned, assume the whole resource is a TextView
+ text = (TextView) view;
+ } else {
+ // Otherwise, find the TextView field within the layout
+ text = (TextView) view.findViewById(mFieldId);
+ }
+ } catch (ClassCastException e) {
+ Log.e("ArrayAdapter", "You must supply a resource ID for a TextView");
+ throw new IllegalStateException(
+ "ArrayAdapter requires the resource ID to be a TextView", e);
+ }
+
+ text.setText(getItem(position).toString());
+
+ return view;
+ }
+
+ /**
+ * <p>Sets the layout resource to create the drop down views.</p>
+ *
+ * @param resource the layout resource defining the drop down views
+ * @see #getDropDownView(int, android.view.View, android.view.ViewGroup)
+ */
+ public void setDropDownViewResource(int resource) {
+ this.mDropDownResource = resource;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View getDropDownView(int position, View convertView, ViewGroup parent) {
+ return createViewFromResource(position, convertView, parent, mDropDownResource);
+ }
+
+ /**
+ * Creates a new ArrayAdapter from external resources. The content of the array is
+ * obtained through {@link android.content.res.Resources#getTextArray(int)}.
+ *
+ * @param context The application's environment.
+ * @param textArrayResId The identifier of the array to use as the data source.
+ * @param textViewResId The identifier of the layout used to create views.
+ *
+ * @return An ArrayAdapter<CharSequence>.
+ */
+ public static ArrayAdapter<CharSequence> createFromResource(Context context,
+ int textArrayResId, int textViewResId) {
+ CharSequence[] strings = context.getResources().getTextArray(textArrayResId);
+ return new ArrayAdapter<CharSequence>(context, textViewResId, strings);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public Filter getFilter() {
+ if (mFilter == null) {
+ mFilter = new ArrayFilter();
+ }
+ return mFilter;
+ }
+
+ /**
+ * <p>An array filters constrains the content of the array adapter with
+ * a prefix. Each item that does not start with the supplied prefix
+ * is removed from the list.</p>
+ */
+ private class ArrayFilter extends Filter {
+ @Override
+ protected FilterResults performFiltering(CharSequence prefix) {
+ FilterResults results = new FilterResults();
+
+ if (mOriginalValues == null) {
+ synchronized (mLock) {
+ mOriginalValues = new ArrayList<T>(mObjects);
+ }
+ }
+
+ if (prefix == null || prefix.length() == 0) {
+ synchronized (mLock) {
+ ArrayList<T> list = new ArrayList<T>(mOriginalValues);
+ results.values = list;
+ results.count = list.size();
+ }
+ } else {
+ String prefixString = prefix.toString().toLowerCase();
+
+ final ArrayList<T> values = mOriginalValues;
+ final int count = values.size();
+
+ final ArrayList<T> newValues = new ArrayList<T>(count);
+
+ for (int i = 0; i < count; i++) {
+ final T value = values.get(i);
+ final String valueText = value.toString().toLowerCase();
+
+ // First match against the whole, non-splitted value
+ if (valueText.startsWith(prefixString)) {
+ newValues.add(value);
+ } else {
+ final String[] words = valueText.split(" ");
+ final int wordCount = words.length;
+
+ for (int k = 0; k < wordCount; k++) {
+ if (words[k].startsWith(prefixString)) {
+ newValues.add(value);
+ break;
+ }
+ }
+ }
+ }
+
+ results.values = newValues;
+ results.count = newValues.size();
+ }
+
+ return results;
+ }
+
+ @Override
+ protected void publishResults(CharSequence constraint, FilterResults results) {
+ //noinspection unchecked
+ mObjects = (List<T>) results.values;
+ if (results.count > 0) {
+ notifyDataSetChanged();
+ } else {
+ notifyDataSetInvalidated();
+ }
+ }
+ }
+}
diff --git a/core/java/android/widget/AutoCompleteTextView.java b/core/java/android/widget/AutoCompleteTextView.java
new file mode 100644
index 0000000..e1f6fa8
--- /dev/null
+++ b/core/java/android/widget/AutoCompleteTextView.java
@@ -0,0 +1,762 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.text.Editable;
+import android.text.Selection;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.internal.R;
+
+
+/**
+ * <p>An editable text view that shows completion suggestions automatically
+ * while the user is typing. The list of suggestions is displayed in a drop
+ * down menu from which the user can choose an item to replace the content
+ * of the edit box with.</p>
+ *
+ * <p>The drop down can be dismissed at any time by pressing the back key or,
+ * if no item is selected in the drop down, by pressing the enter/dpad center
+ * key.</p>
+ *
+ * <p>The list of suggestions is obtained from a data adapter and appears
+ * only after a given number of characters defined by
+ * {@link #getThreshold() the threshold}.</p>
+ *
+ * <p>The following code snippet shows how to create a text view which suggests
+ * various countries names while the user is typing:</p>
+ *
+ * <pre class="prettyprint">
+ * public class CountriesActivity extends Activity {
+ * protected void onCreate(Bundle icicle) {
+ * super.onCreate(icicle);
+ * setContentView(R.layout.countries);
+ *
+ * ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
+ * android.R.layout.simple_dropdown_item_1line, COUNTRIES);
+ * AutoCompleteTextView textView = (AutoCompleteTextView)
+ * findViewById(R.id.countries_list);
+ * textView.setAdapter(adapter);
+ * }
+ *
+ * private static final String[] COUNTRIES = new String[] {
+ * "Belgium", "France", "Italy", "Germany", "Spain"
+ * };
+ * }
+ * </pre>
+ *
+ * @attr ref android.R.styleable#AutoCompleteTextView_completionHint
+ * @attr ref android.R.styleable#AutoCompleteTextView_completionThreshold
+ * @attr ref android.R.styleable#AutoCompleteTextView_completionHintView
+ * @attr ref android.R.styleable#AutoCompleteTextView_dropDownSelector
+ */
+public class AutoCompleteTextView extends EditText implements Filter.FilterListener {
+ private static final int HINT_VIEW_ID = 0x17;
+
+ private CharSequence mHintText;
+ private int mHintResource;
+
+ private ListAdapter mAdapter;
+ private Filter mFilter;
+ private int mThreshold;
+
+ private PopupWindow mPopup;
+ private DropDownListView mDropDownList;
+
+ private Drawable mDropDownListHighlight;
+
+ private AdapterView.OnItemClickListener mItemClickListener;
+ private AdapterView.OnItemSelectedListener mItemSelectedListener;
+
+ private final DropDownItemClickListener mDropDownItemClickListener =
+ new DropDownItemClickListener();
+
+ private boolean mTextChanged;
+
+ public AutoCompleteTextView(Context context) {
+ this(context, null);
+ }
+
+ public AutoCompleteTextView(Context context, AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.autoCompleteTextViewStyle);
+ }
+
+ public AutoCompleteTextView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ mPopup = new PopupWindow(context, attrs, com.android.internal.R.attr.autoCompleteTextViewStyle);
+
+ TypedArray a =
+ context.obtainStyledAttributes(
+ attrs, com.android.internal.R.styleable.AutoCompleteTextView, defStyle, 0);
+
+ mThreshold = a.getInt(
+ R.styleable.AutoCompleteTextView_completionThreshold, 2);
+
+ mHintText = a.getText(R.styleable.AutoCompleteTextView_completionHint);
+
+ mDropDownListHighlight = a.getDrawable(
+ R.styleable.AutoCompleteTextView_dropDownSelector);
+
+ mHintResource = a.getResourceId(R.styleable.AutoCompleteTextView_completionHintView,
+ R.layout.simple_dropdown_hint);
+
+ a.recycle();
+
+ setFocusable(true);
+ }
+
+ /**
+ * Sets this to be single line; a separate method so
+ * MultiAutoCompleteTextView can skip this.
+ */
+ /* package */ void finishInit() {
+ setSingleLine();
+ }
+
+ /**
+ * <p>Sets the optional hint text that is displayed at the bottom of the
+ * the matching list. This can be used as a cue to the user on how to
+ * best use the list, or to provide extra information.</p>
+ *
+ * @param hint the text to be displayed to the user
+ *
+ * @attr ref android.R.styleable#AutoCompleteTextView_completionHint
+ */
+ public void setCompletionHint(CharSequence hint) {
+ mHintText = hint;
+ }
+
+ /**
+ * <p>Returns the number of characters the user must type before the drop
+ * down list is shown.</p>
+ *
+ * @return the minimum number of characters to type to show the drop down
+ *
+ * @see #setThreshold(int)
+ */
+ public int getThreshold() {
+ return mThreshold;
+ }
+
+ /**
+ * <p>Specifies the minimum number of characters the user has to type in the
+ * edit box before the drop down list is shown.</p>
+ *
+ * <p>When <code>threshold</code> is less than or equals 0, a threshold of
+ * 1 is applied.</p>
+ *
+ * @param threshold the number of characters to type before the drop down
+ * is shown
+ *
+ * @see #getThreshold()
+ *
+ * @attr ref android.R.styleable#AutoCompleteTextView_completionThreshold
+ */
+ public void setThreshold(int threshold) {
+ if (threshold <= 0) {
+ threshold = 1;
+ }
+
+ mThreshold = threshold;
+ }
+
+ /**
+ * <p>Sets the listener that will be notified when the user clicks an item
+ * in the drop down list.</p>
+ *
+ * @param l the item click listener
+ */
+ public void setOnItemClickListener(AdapterView.OnItemClickListener l) {
+ mItemClickListener = l;
+ }
+
+ /**
+ * <p>Sets the listener that will be notified when the user selects an item
+ * in the drop down list.</p>
+ *
+ * @param l the item selected listener
+ */
+ public void setOnItemSelectedListener(AdapterView.OnItemSelectedListener l) {
+ mItemSelectedListener = l;
+ }
+
+ /**
+ * <p>Returns the listener that is notified whenever the user clicks an item
+ * in the drop down list.</p>
+ *
+ * @return the item click listener
+ */
+ public AdapterView.OnItemClickListener getItemClickListener() {
+ return mItemClickListener;
+ }
+
+ /**
+ * <p>Returns the listener that is notified whenever the user selects an
+ * item in the drop down list.</p>
+ *
+ * @return the item selected listener
+ */
+ public AdapterView.OnItemSelectedListener getItemSelectedListener() {
+ return mItemSelectedListener;
+ }
+
+ /**
+ * <p>Returns a filterable list adapter used for auto completion.</p>
+ *
+ * @return a data adapter used for auto completion
+ */
+ public ListAdapter getAdapter() {
+ return mAdapter;
+ }
+
+ /**
+ * <p>Changes the list of data used for auto completion. The provided list
+ * must be a filterable list adapter.</p>
+ *
+ * @param adapter the adapter holding the auto completion data
+ *
+ * @see #getAdapter()
+ * @see android.widget.Filterable
+ * @see android.widget.ListAdapter
+ */
+ public <T extends ListAdapter & Filterable> void setAdapter(T adapter) {
+ mAdapter = adapter;
+ if (mAdapter != null) {
+ //noinspection unchecked
+ mFilter = ((Filterable) mAdapter).getFilter();
+ } else {
+ mFilter = null;
+ }
+
+ if (mDropDownList != null) {
+ mDropDownList.setAdapter(mAdapter);
+ }
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ if (isPopupShowing()) {
+ boolean consumed = mDropDownList.onKeyUp(keyCode, event);
+ if (consumed) {
+ switch (keyCode) {
+ // if the list accepts the key events and the key event
+ // was a click, the text view gets the selected item
+ // from the drop down as its content
+ case KeyEvent.KEYCODE_ENTER:
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ performCompletion();
+ return true;
+ }
+ }
+ }
+ return super.onKeyUp(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ // when the drop down is shown, we drive it directly
+ if (isPopupShowing()) {
+ // special case for the back key, we do not even try to send it
+ // to the drop down list but instead, consume it immediately
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ dismissDropDown();
+ return true;
+
+ // the key events are forwarded to the list in the drop down view
+ // note that ListView handles space but we don't want that to happen
+ } else if (keyCode != KeyEvent.KEYCODE_SPACE) {
+ boolean consumed = mDropDownList.onKeyDown(keyCode, event);
+
+ if (consumed) {
+ switch (keyCode) {
+ // avoid passing the focus from the text view to the
+ // next component
+ case KeyEvent.KEYCODE_ENTER:
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ case KeyEvent.KEYCODE_DPAD_UP:
+ return true;
+ }
+ } else{
+ int index = mDropDownList.getSelectedItemPosition();
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_UP:
+ if (index == 0) {
+ return true;
+ }
+ break;
+ // when the selection is at the bottom, we block the
+ // event to avoid going to the next focusable widget
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ Adapter adapter = mDropDownList.getAdapter();
+ if (index == adapter.getCount() - 1) {
+ return true;
+ }
+ break;
+ }
+ }
+ }
+ } else {
+ switch(keyCode) {
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ performValidation();
+ }
+ }
+
+ // when text is changed, inserted or deleted, we attempt to show
+ // the drop down
+ boolean openBefore = isPopupShowing();
+ mTextChanged = false;
+
+ boolean handled = super.onKeyDown(keyCode, event);
+
+ // if the list was open before the keystroke, but closed afterwards,
+ // then something in the keystroke processing (an input filter perhaps)
+ // called performCompletion() and we shouldn't do any more processing.
+ if (openBefore && !isPopupShowing()) {
+ return handled;
+ }
+
+ if (mTextChanged) { // would have been set in onTextChanged()
+ // the drop down is shown only when a minimum number of characters
+ // was typed in the text view
+ if (enoughToFilter()) {
+ if (mFilter != null) {
+ performFiltering(getText(), keyCode);
+ }
+ } else {
+ // drop down is automatically dismissed when enough characters
+ // are deleted from the text view
+ dismissDropDown();
+ if (mFilter != null) {
+ mFilter.filter(null);
+ }
+ }
+ return true;
+ }
+
+ return handled;
+ }
+
+ /**
+ * Returns <code>true</code> if the amount of text in the field meets
+ * or exceeds the {@link #getThreshold} requirement. You can override
+ * this to impose a different standard for when filtering will be
+ * triggered.
+ */
+ public boolean enoughToFilter() {
+ return getText().length() >= mThreshold;
+ }
+
+ @Override
+ protected void onTextChanged(CharSequence text, int start, int before,
+ int after) {
+ super.onTextChanged(text, start, before, after);
+ mTextChanged = true;
+ }
+
+ /**
+ * <p>Indicates whether the popup menu is showing.</p>
+ *
+ * @return true if the popup menu is showing, false otherwise
+ */
+ public boolean isPopupShowing() {
+ return mPopup.isShowing();
+ }
+
+ /**
+ * <p>Converts the selected item from the drop down list into a sequence
+ * of character that can be used in the edit box.</p>
+ *
+ * @param selectedItem the item selected by the user for completion
+ *
+ * @return a sequence of characters representing the selected suggestion
+ */
+ protected CharSequence convertSelectionToString(Object selectedItem) {
+ return mFilter.convertResultToString(selectedItem);
+ }
+
+ /**
+ * <p>Starts filtering the content of the drop down list. The filtering
+ * pattern is the content of the edit box. Subclasses should override this
+ * method to filter with a different pattern, for instance a substring of
+ * <code>text</code>.</p>
+ *
+ * @param text the filtering pattern
+ * @param keyCode the last character inserted in the edit box
+ */
+ @SuppressWarnings({ "UnusedDeclaration" })
+ protected void performFiltering(CharSequence text, int keyCode) {
+ mFilter.filter(text, this);
+ }
+
+ /**
+ * <p>Performs the text completion by converting the selected item from
+ * the drop down list into a string, replacing the text box's content with
+ * this string and finally dismissing the drop down menu.</p>
+ */
+ public void performCompletion() {
+ performCompletion(null, -1, -1);
+ }
+
+ private void performCompletion(View selectedView, int position, long id) {
+ if (isPopupShowing()) {
+ Object selectedItem;
+ if (position == -1) {
+ selectedItem = mDropDownList.getSelectedItem();
+ } else {
+ selectedItem = mAdapter.getItem(position);
+ }
+ replaceText(convertSelectionToString(selectedItem));
+
+ if (mItemClickListener != null) {
+ final DropDownListView list = mDropDownList;
+
+ if (selectedView == null || position == -1) {
+ selectedView = list.getSelectedView();
+ position = list.getSelectedItemPosition();
+ id = list.getSelectedItemId();
+ }
+ mItemClickListener.onItemClick(list, selectedView, position, id);
+ }
+ }
+
+ dismissDropDown();
+ }
+
+ /**
+ * <p>Performs the text completion by replacing the current text by the
+ * selected item. Subclasses should override this method to avoid replacing
+ * the whole content of the edit box.</p>
+ *
+ * @param text the selected suggestion in the drop down list
+ */
+ protected void replaceText(CharSequence text) {
+ setText(text);
+ // make sure we keep the caret at the end of the text view
+ Editable spannable = getText();
+ Selection.setSelection(spannable, spannable.length());
+ }
+
+ public void onFilterComplete(int count) {
+ /*
+ * This checks enoughToFilter() again because filtering requests
+ * are asynchronous, so the result may come back after enough text
+ * has since been deleted to make it no longer appropriate
+ * to filter.
+ */
+
+ if (count > 0 && enoughToFilter()) {
+ if (hasFocus() && hasWindowFocus()) {
+ showDropDown();
+ }
+ } else {
+ dismissDropDown();
+ }
+ }
+
+ @Override
+ public void onWindowFocusChanged(boolean hasWindowFocus) {
+ super.onWindowFocusChanged(hasWindowFocus);
+ performValidation();
+ if (!hasWindowFocus) {
+ dismissDropDown();
+ }
+ }
+
+ @Override
+ protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
+ super.onFocusChanged(focused, direction, previouslyFocusedRect);
+ performValidation();
+ if (!focused) {
+ dismissDropDown();
+ }
+ }
+
+ /**
+ * <p>Closes the drop down if present on screen.</p>
+ */
+ public void dismissDropDown() {
+ mPopup.dismiss();
+ if (mDropDownList != null) {
+ // start next time with no selection
+ mDropDownList.hideSelector();
+ }
+ }
+
+ @Override
+ protected boolean setFrame(int l, int t, int r, int b) {
+ boolean result = super.setFrame(l, t, r, b);
+
+ mPopup.update(this, getMeasuredWidth(), -1);
+
+ return result;
+ }
+
+ /**
+ * <p>Displays the drop down on screen.</p>
+ */
+ public void showDropDown() {
+ int height = buildDropDown();
+ if (mPopup.isShowing()) {
+ mPopup.update(this, getMeasuredWidth() - mPaddingLeft - mPaddingRight, height);
+ } else {
+ mPopup.setHeight(height);
+ mPopup.setWidth(getMeasuredWidth() - mPaddingLeft - mPaddingRight);
+ mPopup.showAsDropDown(this);
+ }
+ }
+
+ /**
+ * <p>Builds the popup window's content and returns the height the popup
+ * should have. Returns -1 when the content already exists.</p>
+ *
+ * @return the content's height or -1 if content already exists
+ */
+ private int buildDropDown() {
+ ViewGroup dropDownView;
+ int otherHeights = 0;
+
+ if (mDropDownList == null) {
+ Context context = getContext();
+
+ mDropDownList = new DropDownListView(context);
+ mDropDownList.setSelector(mDropDownListHighlight);
+ mDropDownList.setAdapter(mAdapter);
+ mDropDownList.setVerticalFadingEdgeEnabled(true);
+ mDropDownList.setOnItemClickListener(mDropDownItemClickListener);
+
+ if (mItemSelectedListener != null) {
+ mDropDownList.setOnItemSelectedListener(mItemSelectedListener);
+ }
+
+ dropDownView = mDropDownList;
+
+ View hintView = getHintView(context);
+ if (hintView != null) {
+ // if an hint has been specified, we accomodate more space for it and
+ // add a text view in the drop down menu, at the bottom of the list
+ LinearLayout hintContainer = new LinearLayout(context);
+ hintContainer.setOrientation(LinearLayout.VERTICAL);
+
+ LinearLayout.LayoutParams hintParams = new LinearLayout.LayoutParams(
+ ViewGroup.LayoutParams.FILL_PARENT, 0, 1.0f
+ );
+ hintContainer.addView(dropDownView, hintParams);
+ hintContainer.addView(hintView);
+
+ // measure the hint's height to find how much more vertical space
+ // we need to add to the drop down's height
+ int widthSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.AT_MOST);
+ int heightSpec = MeasureSpec.UNSPECIFIED;
+ hintView.measure(widthSpec, heightSpec);
+
+ hintParams = (LinearLayout.LayoutParams) hintView.getLayoutParams();
+ otherHeights = hintView.getMeasuredHeight() + hintParams.topMargin
+ + hintParams.bottomMargin;
+
+ dropDownView = hintContainer;
+ }
+
+ mPopup.setContentView(dropDownView);
+ } else {
+ dropDownView = (ViewGroup) mPopup.getContentView();
+ final View view = dropDownView.findViewById(HINT_VIEW_ID);
+ if (view != null) {
+ LinearLayout.LayoutParams hintParams =
+ (LinearLayout.LayoutParams) view.getLayoutParams();
+ otherHeights = view.getMeasuredHeight() + hintParams.topMargin
+ + hintParams.bottomMargin;
+ }
+ }
+
+ // Max height available on the screen for a popup anchored to us
+ final int maxHeight = mPopup.getMaxAvailableHeight(this);
+ otherHeights += dropDownView.getPaddingTop() + dropDownView.getPaddingBottom();
+
+ return mDropDownList.measureHeightOfChildren(MeasureSpec.UNSPECIFIED,
+ 0, ListView.NO_POSITION, maxHeight - otherHeights, 2) + otherHeights;
+ }
+
+ private View getHintView(Context context) {
+ if (mHintText != null && mHintText.length() > 0) {
+ final TextView hintView = (TextView) LayoutInflater.from(context).inflate(
+ mHintResource, null).findViewById(com.android.internal.R.id.text1);
+ hintView.setText(mHintText);
+ hintView.setId(HINT_VIEW_ID);
+ return hintView;
+ } else {
+ return null;
+ }
+ }
+
+ private class DropDownItemClickListener implements AdapterView.OnItemClickListener {
+ public void onItemClick(AdapterView parent, View v, int position, long id) {
+ performCompletion(v, position, id);
+ }
+ }
+
+ /**
+ * <p>Wrapper class for a ListView. This wrapper hijacks the focus to
+ * make sure the list uses the appropriate drawables and states when
+ * displayed on screen within a drop down. The focus is never actually
+ * passed to the drop down; the list only looks focused.</p>
+ */
+ private static class DropDownListView extends ListView {
+ /**
+ * <p>Creates a new list view wrapper.</p>
+ *
+ * @param context this view's context
+ */
+ public DropDownListView(Context context) {
+ super(context, null, com.android.internal.R.attr.dropDownListViewStyle);
+ }
+
+ /**
+ * <p>Avoids jarring scrolling effect by ensuring that list elements
+ * made of a text view fit on a single line.</p>
+ *
+ * @param position the item index in the list to get a view for
+ * @return the view for the specified item
+ */
+ @Override
+ protected View obtainView(int position) {
+ View view = super.obtainView(position);
+
+ if (view instanceof TextView) {
+ ((TextView) view).setHorizontallyScrolling(true);
+ }
+
+ return view;
+ }
+
+ /**
+ * <p>Returns the top padding of the currently selected view.</p>
+ *
+ * @return the height of the top padding for the selection
+ */
+ public int getSelectionPaddingTop() {
+ return mSelectionTopPadding;
+ }
+
+ /**
+ * <p>Returns the bottom padding of the currently selected view.</p>
+ *
+ * @return the height of the bottom padding for the selection
+ */
+ public int getSelectionPaddingBottom() {
+ return mSelectionBottomPadding;
+ }
+
+ /**
+ * <p>Returns the focus state in the drop down.</p>
+ *
+ * @return true always
+ */
+ @Override
+ public boolean hasWindowFocus() {
+ return true;
+ }
+
+ /**
+ * <p>Returns the focus state in the drop down.</p>
+ *
+ * @return true always
+ */
+ @Override
+ public boolean isFocused() {
+ return true;
+ }
+
+ /**
+ * <p>Returns the focus state in the drop down.</p>
+ *
+ * @return true always
+ */
+ @Override
+ public boolean hasFocus() {
+ return true;
+ }
+ }
+
+ /**
+ * This interface is used to make sure that the text entered in this TextView complies to
+ * a certain format. Since there is no foolproof way to prevent the user from leaving
+ * this View with an incorrect value in it, all we can do is try to fix it ourselves
+ * when this happens.
+ */
+ static public interface Validator {
+ /**
+ * @return true if the text currently in the text editor is valid.
+ */
+ boolean isValid(CharSequence text);
+
+ /**
+ * @param invalidText a string that doesn't pass validation:
+ * isValid(invalidText) returns false
+ * @return a string based on invalidText such as invoking isValid() on it returns true.
+ */
+ CharSequence fixText(CharSequence invalidText);
+ }
+
+ private Validator mValidator = null;
+
+ public void setValidator(Validator validator) {
+ mValidator = validator;
+ }
+
+ /**
+ * Returns the Validator set with {@link #setValidator},
+ * or <code>null</code> if it was not set.
+ */
+ public Validator getValidator() {
+ return mValidator;
+ }
+
+ /**
+ * If a validator was set on this view and the current string is not valid,
+ * ask the validator to fix it.
+ */
+ public void performValidation() {
+ if (mValidator == null) return;
+
+ CharSequence text = getText();
+
+ if (!TextUtils.isEmpty(text) && !mValidator.isValid(text)) {
+ setText(mValidator.fixText(text));
+ }
+ }
+
+ /**
+ * Returns the Filter obtained from {@link Filterable#getFilter},
+ * or <code>null</code> if {@link #setAdapter} was not called with
+ * a Filterable.
+ */
+ protected Filter getFilter() {
+ return mFilter;
+ }
+}
diff --git a/core/java/android/widget/BaseAdapter.java b/core/java/android/widget/BaseAdapter.java
new file mode 100644
index 0000000..1921d73
--- /dev/null
+++ b/core/java/android/widget/BaseAdapter.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import android.database.DataSetObservable;
+import android.database.DataSetObserver;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Common base class of common implementation for an {@link Adapter} that can be
+ * used in both {@link ListView} (by implementing the specialized
+ * {@link ListAdapter} interface} and {@link Spinner} (by implementing the
+ * specialized {@link SpinnerAdapter} interface.
+ */
+public abstract class BaseAdapter implements ListAdapter, SpinnerAdapter {
+ private final DataSetObservable mDataSetObservable = new DataSetObservable();
+
+ public boolean hasStableIds() {
+ return false;
+ }
+
+ public void registerDataSetObserver(DataSetObserver observer) {
+ mDataSetObservable.registerObserver(observer);
+ }
+
+ public void unregisterDataSetObserver(DataSetObserver observer) {
+ mDataSetObservable.unregisterObserver(observer);
+ }
+
+ public void notifyDataSetChanged() {
+ mDataSetObservable.notifyChanged();
+ }
+
+ public void notifyDataSetInvalidated() {
+ mDataSetObservable.notifyInvalidated();
+ }
+
+ public boolean areAllItemsEnabled() {
+ return true;
+ }
+
+ public boolean isEnabled(int position) {
+ return true;
+ }
+
+ public View getDropDownView(int position, View convertView, ViewGroup parent) {
+ return getView(position, convertView, parent);
+ }
+
+ public int getItemViewType(int position) {
+ return 0;
+ }
+
+ public int getViewTypeCount() {
+ return 1;
+ }
+
+ public boolean isEmpty() {
+ return getCount() == 0;
+ }
+}
diff --git a/core/java/android/widget/BaseExpandableListAdapter.java b/core/java/android/widget/BaseExpandableListAdapter.java
new file mode 100644
index 0000000..3a8bb2a
--- /dev/null
+++ b/core/java/android/widget/BaseExpandableListAdapter.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import android.database.DataSetObservable;
+import android.database.DataSetObserver;
+import android.view.KeyEvent;
+
+/**
+ * Base class for a {@link ExpandableListAdapter} used to provide data and Views
+ * from some data to an expandable list view.
+ * <p>
+ * Adapters inheriting this class should verify that the base implementations of
+ * {@link #getCombinedChildId(long, long)} and {@link #getCombinedGroupId(long)}
+ * are correct in generating unique IDs from the group/children IDs.
+ * <p>
+ * @see SimpleExpandableListAdapter
+ * @see SimpleCursorTreeAdapter
+ */
+public abstract class BaseExpandableListAdapter implements ExpandableListAdapter {
+ private final DataSetObservable mDataSetObservable = new DataSetObservable();
+
+ public void registerDataSetObserver(DataSetObserver observer) {
+ mDataSetObservable.registerObserver(observer);
+ }
+
+ public void unregisterDataSetObserver(DataSetObserver observer) {
+ mDataSetObservable.unregisterObserver(observer);
+ }
+
+ /**
+ * {@see DataSetObservable#notifyInvalidated()}
+ */
+ public void notifyDataSetInvalidated() {
+ mDataSetObservable.notifyInvalidated();
+ }
+
+ /**
+ * {@see DataSetObservable#notifyChanged()}
+ */
+ public void notifyDataSetChanged() {
+ mDataSetObservable.notifyChanged();
+ }
+
+ public boolean areAllItemsEnabled() {
+ return true;
+ }
+
+ public void onGroupCollapsed(int groupPosition) {
+ }
+
+ public void onGroupExpanded(int groupPosition) {
+ }
+
+ /**
+ * Override this method if you foresee a clash in IDs based on this scheme:
+ * <p>
+ * Base implementation returns a long:
+ * <li> bit 0: Whether this ID points to a child (unset) or group (set), so for this method
+ * this bit will be 0.
+ * <li> bit 1-31: Lower 31 bits of the groupId
+ * <li> bit 32-63: Lower 32 bits of the childId.
+ * <p>
+ * {@inheritDoc}
+ */
+ public long getCombinedChildId(long groupId, long childId) {
+ return 0x8000000000000000L | ((groupId & 0x7FFFFFFF) << 32) | (childId & 0xFFFFFFFF);
+ }
+
+ /**
+ * Override this method if you foresee a clash in IDs based on this scheme:
+ * <p>
+ * Base implementation returns a long:
+ * <li> bit 0: Whether this ID points to a child (unset) or group (set), so for this method
+ * this bit will be 1.
+ * <li> bit 1-31: Lower 31 bits of the groupId
+ * <li> bit 32-63: Lower 32 bits of the childId.
+ * <p>
+ * {@inheritDoc}
+ */
+ public long getCombinedGroupId(long groupId) {
+ return (groupId & 0x7FFFFFFF) << 32;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean isEmpty() {
+ return getGroupCount() == 0;
+ }
+
+}
diff --git a/core/java/android/widget/Button.java b/core/java/android/widget/Button.java
new file mode 100644
index 0000000..f2868af
--- /dev/null
+++ b/core/java/android/widget/Button.java
@@ -0,0 +1,69 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.KeyEvent;
+
+
+/**
+ * <p>
+ * <code>Button</code> represents a push-button widget. Push-buttons can be
+ * pressed, or clicked, by the user to perform an action. A typical use of a
+ * push-button in an activity would be the following:
+ * </p>
+ *
+ * <pre class="prettyprint">
+ * public class MyActivity extends Activity {
+ * protected void onCreate(Bundle icicle) {
+ * super.onCreate(icicle);
+ *
+ * setContentView(R.layout.content_layout_id);
+ *
+ * final Button button = (Button) findViewById(R.id.button_id);
+ * button.setOnClickListener(new View.OnClickListener() {
+ * public void onClick(View v) {
+ * // Perform action on click
+ * }
+ * });
+ * }
+ * }
+ * </pre>
+ *
+ * <p><strong>XML attributes</strong></p>
+ * <p>
+ * See {@link android.R.styleable#Button Button Attributes},
+ * {@link android.R.styleable#TextView TextView Attributes},
+ * {@link android.R.styleable#View View Attributes}
+ * </p>
+ */
+public class Button extends TextView {
+ public Button(Context context) {
+ this(context, null);
+ }
+
+ public Button(Context context, AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.buttonStyle);
+ }
+
+ public Button(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+}
diff --git a/core/java/android/widget/CheckBox.java b/core/java/android/widget/CheckBox.java
new file mode 100644
index 0000000..ff63a24
--- /dev/null
+++ b/core/java/android/widget/CheckBox.java
@@ -0,0 +1,65 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+
+/**
+ * <p>
+ * A checkbox is a specific type of two-states button that can be either
+ * checked or unchecked. A example usage of a checkbox inside your activity
+ * would be the following:
+ * </p>
+ *
+ * <pre class="prettyprint">
+ * public class MyActivity extends Activity {
+ * protected void onCreate(Bundle icicle) {
+ * super.onCreate(icicle);
+ *
+ * setContentView(R.layout.content_layout_id);
+ *
+ * final CheckBox checkBox = (CheckBox) findViewById(R.id.checkbox_id);
+ * if (checkBox.isChecked()) {
+ * checkBox.setChecked(false);
+ * }
+ * }
+ * }
+ * </pre>
+ *
+ * <p><strong>XML attributes</strong></p>
+ * <p>
+ * See {@link android.R.styleable#CompoundButton CompoundButton Attributes},
+ * {@link android.R.styleable#Button Button Attributes},
+ * {@link android.R.styleable#TextView TextView Attributes},
+ * {@link android.R.styleable#View View Attributes}
+ * </p>
+ */
+public class CheckBox extends CompoundButton {
+ public CheckBox(Context context) {
+ this(context, null);
+ }
+
+ public CheckBox(Context context, AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.checkboxStyle);
+ }
+
+ public CheckBox(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+}
diff --git a/core/java/android/widget/Checkable.java b/core/java/android/widget/Checkable.java
new file mode 100644
index 0000000..eb97b4a
--- /dev/null
+++ b/core/java/android/widget/Checkable.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+/**
+ * Defines an extension for views that make them checkable.
+ *
+ */
+public interface Checkable {
+
+ /**
+ * Change the checked state of the view
+ *
+ * @param checked The new checked state
+ */
+ void setChecked(boolean checked);
+
+ /**
+ * @return The current checked state of the view
+ */
+ boolean isChecked();
+
+ /**
+ * Change the checked state of the view to the inverse of its current state
+ *
+ */
+ void toggle();
+}
diff --git a/core/java/android/widget/CheckedTextView.java b/core/java/android/widget/CheckedTextView.java
new file mode 100644
index 0000000..f5a0b1c
--- /dev/null
+++ b/core/java/android/widget/CheckedTextView.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.Gravity;
+
+import com.android.internal.R;
+
+
+/**
+ * An extension to TextView that supports the {@link android.widget.Checkable} interface.
+ * This is useful when used in a {@link android.widget.ListView ListView} where the it's
+ * {@link android.widget.ListView#setChoiceMode(int) setChoiceMode} has been set to
+ * something other than {@link android.widget.ListView#CHOICE_MODE_NONE CHOICE_MODE_NONE}.
+ *
+ */
+public abstract class CheckedTextView extends TextView implements Checkable {
+ private boolean mChecked;
+ private int mCheckMarkResource;
+ private Drawable mCheckMarkDrawable;
+ private int mBasePaddingRight;
+ private int mCheckMarkWidth;
+
+ private static final int[] CHECKED_STATE_SET = {
+ R.attr.state_checked
+ };
+
+ public CheckedTextView(Context context) {
+ this(context, null);
+ }
+
+ public CheckedTextView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public CheckedTextView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ TypedArray a = context.obtainStyledAttributes(attrs,
+ R.styleable.CheckedTextView, defStyle, 0);
+
+ Drawable d = a.getDrawable(R.styleable.CheckedTextView_checkMark);
+ if (d != null) {
+ setCheckMarkDrawable(d);
+ }
+
+ boolean checked = a.getBoolean(R.styleable.CheckedTextView_checked, false);
+ setChecked(checked);
+
+ a.recycle();
+ }
+
+ public void toggle() {
+ setChecked(!mChecked);
+ }
+
+ public boolean isChecked() {
+ return mChecked;
+ }
+
+ /**
+ * <p>Changes the checked state of this text view.</p>
+ *
+ * @param checked true to check the text, false to uncheck it
+ */
+ public void setChecked(boolean checked) {
+ if (mChecked != checked) {
+ mChecked = checked;
+ refreshDrawableState();
+ }
+ }
+
+
+ /**
+ * Set the checkmark to a given Drawable, identified by its resourece id. This will be drawn
+ * when {@link #isChecked()} is true.
+ *
+ * @param resid The Drawable to use for the checkmark.
+ */
+ public void setCheckMarkDrawable(int resid) {
+ if (resid != 0 && resid == mCheckMarkResource) {
+ return;
+ }
+
+ mCheckMarkResource = resid;
+
+ Drawable d = null;
+ if (mCheckMarkResource != 0) {
+ d = getResources().getDrawable(mCheckMarkResource);
+ }
+ setCheckMarkDrawable(d);
+ }
+
+ /**
+ * Set the checkmark to a given Drawable. This will be drawn when {@link #isChecked()} is true.
+ *
+ * @param d The Drawable to use for the checkmark.
+ */
+ public void setCheckMarkDrawable(Drawable d) {
+ if (d != null) {
+ if (mCheckMarkDrawable != null) {
+ mCheckMarkDrawable.setCallback(null);
+ unscheduleDrawable(mCheckMarkDrawable);
+ }
+ d.setCallback(this);
+ d.setVisible(getVisibility() == VISIBLE, false);
+ d.setState(CHECKED_STATE_SET);
+ setMinHeight(d.getIntrinsicHeight());
+
+ mCheckMarkWidth = d.getIntrinsicWidth();
+ mPaddingRight = mCheckMarkWidth + mBasePaddingRight;
+ d.setState(getDrawableState());
+ mCheckMarkDrawable = d;
+ } else {
+ mPaddingRight = mBasePaddingRight;
+ }
+ requestLayout();
+ }
+
+ @Override
+ public void setPadding(int left, int top, int right, int bottom) {
+ super.setPadding(left, top, right, bottom);
+ mBasePaddingRight = mPaddingRight;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ final Drawable checkMarkDrawable = mCheckMarkDrawable;
+ if (checkMarkDrawable != null) {
+ final int verticalGravity = getGravity() & Gravity.VERTICAL_GRAVITY_MASK;
+ final int height = checkMarkDrawable.getIntrinsicHeight();
+
+ int y = 0;
+
+ switch (verticalGravity) {
+ case Gravity.BOTTOM:
+ y = getHeight() - height;
+ break;
+ case Gravity.CENTER_VERTICAL:
+ y = (getHeight() - height) / 2;
+ break;
+ }
+
+ int right = getWidth();
+ checkMarkDrawable.setBounds(
+ right - mCheckMarkWidth - mBasePaddingRight,
+ y,
+ right - mBasePaddingRight,
+ y + height);
+ checkMarkDrawable.draw(canvas);
+ }
+ }
+
+ @Override
+ protected int[] onCreateDrawableState(int extraSpace) {
+ final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
+ if (isChecked()) {
+ mergeDrawableStates(drawableState, CHECKED_STATE_SET);
+ }
+ return drawableState;
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+
+ if (mCheckMarkDrawable != null) {
+ int[] myDrawableState = getDrawableState();
+
+ // Set the state of the Drawable
+ mCheckMarkDrawable.setState(myDrawableState);
+
+ invalidate();
+ }
+ }
+
+}
diff --git a/core/java/android/widget/Chronometer.java b/core/java/android/widget/Chronometer.java
new file mode 100644
index 0000000..31d2063
--- /dev/null
+++ b/core/java/android/widget/Chronometer.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.os.Handler;
+import android.os.Message;
+import android.os.SystemClock;
+import android.pim.DateUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.widget.RemoteViews.RemoteView;
+
+import java.util.Formatter;
+import java.util.IllegalFormatException;
+import java.util.Locale;
+
+/**
+ * Class that implements a simple timer.
+ * <p>
+ * You can give it a start time in the {@link SystemClock#elapsedRealtime} timebase,
+ * and it counts up from that, or if you don't give it a base time, it will use the
+ * time at which you call {@link #start}. By default it will display the current
+ * timer value in the form "MM:SS" or "H:MM:SS", or you can use {@link #setFormat}
+ * to format the timer value into an arbitrary string.
+ *
+ * @attr ref android.R.styleable#Chronometer_format
+ */
+@RemoteView
+public class Chronometer extends TextView {
+ private static final String TAG = "Chronometer";
+
+ private long mBase;
+ private boolean mVisible;
+ private boolean mStarted;
+ private boolean mRunning;
+ private boolean mLogged;
+ private String mFormat;
+ private Formatter mFormatter;
+ private Locale mFormatterLocale;
+ private Object[] mFormatterArgs = new Object[1];
+ private StringBuilder mFormatBuilder;
+
+ /**
+ * Initialize this Chronometer object.
+ * Sets the base to the current time.
+ */
+ public Chronometer(Context context) {
+ this(context, null, 0);
+ }
+
+ /**
+ * Initialize with standard view layout information.
+ * Sets the base to the current time.
+ */
+ public Chronometer(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ /**
+ * Initialize with standard view layout information and style.
+ * Sets the base to the current time.
+ */
+ public Chronometer(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ TypedArray a = context.obtainStyledAttributes(
+ attrs,
+ com.android.internal.R.styleable.Chronometer, defStyle, 0);
+ setFormat(a.getString(com.android.internal.R.styleable.Chronometer_format));
+ a.recycle();
+
+ init();
+ }
+
+ private void init() {
+ mBase = SystemClock.elapsedRealtime();
+ updateText(mBase);
+ }
+
+ /**
+ * Set the time that the count-up timer is in reference to.
+ *
+ * @param base Use the {@link SystemClock#elapsedRealtime} time base.
+ */
+ public void setBase(long base) {
+ mBase = base;
+ updateText(SystemClock.elapsedRealtime());
+ }
+
+ /**
+ * Return the base time as set through {@link #setBase}.
+ */
+ public long getBase() {
+ return mBase;
+ }
+
+ /**
+ * Sets the format string used for display. The Chronometer will display
+ * this string, with the first "%s" replaced by the current timer value in
+ * "MM:SS" or "H:MM:SS" form.
+ *
+ * If the format string is null, or if you never call setFormat(), the
+ * Chronometer will simply display the timer value in "MM:SS" or "H:MM:SS"
+ * form.
+ *
+ * @param format the format string.
+ */
+ public void setFormat(String format) {
+ mFormat = format;
+ if (format != null && mFormatBuilder == null) {
+ mFormatBuilder = new StringBuilder(format.length() * 2);
+ }
+ }
+
+ /**
+ * Returns the current format string as set through {@link #setFormat}.
+ */
+ public String getFormat() {
+ return mFormat;
+ }
+
+ /**
+ * Start counting up. This does not affect the base as set from {@link #setBase}, just
+ * the view display.
+ *
+ * Chronometer works by regularly scheduling messages to the handler, even when the
+ * Widget is not visible. To make sure resource leaks do not occur, the user should
+ * make sure that each start() call has a reciprocal call to {@link #stop}.
+ */
+ public void start() {
+ mStarted = true;
+ updateRunning();
+ }
+
+ /**
+ * Stop counting up. This does not affect the base as set from {@link #setBase}, just
+ * the view display.
+ *
+ * This stops the messages to the handler, effectively releasing resources that would
+ * be held as the chronometer is running, via {@link #start}.
+ */
+ public void stop() {
+ mStarted = false;
+ updateRunning();
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ mVisible = false;
+ updateRunning();
+ }
+
+ @Override
+ protected void onWindowVisibilityChanged(int visibility) {
+ super.onWindowVisibilityChanged(visibility);
+ mVisible = visibility == VISIBLE;
+ updateRunning();
+ }
+
+ private void updateText(long now) {
+ long seconds = now - mBase;
+ seconds /= 1000;
+ String text = DateUtils.formatElapsedTime(seconds);
+
+ if (mFormat != null) {
+ Locale loc = Locale.getDefault();
+ if (mFormatter == null || !loc.equals(mFormatterLocale)) {
+ mFormatterLocale = loc;
+ mFormatter = new Formatter(mFormatBuilder, loc);
+ }
+ mFormatBuilder.setLength(0);
+ mFormatterArgs[0] = text;
+ try {
+ mFormatter.format(mFormat, mFormatterArgs);
+ text = mFormatBuilder.toString();
+ } catch (IllegalFormatException ex) {
+ if (!mLogged) {
+ Log.w(TAG, "Illegal format string: " + mFormat);
+ mLogged = true;
+ }
+ }
+ }
+ setText(text);
+ }
+
+ private void updateRunning() {
+ boolean running = mVisible && mStarted;
+ if (running != mRunning) {
+ if (running) {
+ updateText(SystemClock.elapsedRealtime());
+ mHandler.sendMessageDelayed(Message.obtain(), 1000);
+ }
+ mRunning = running;
+ }
+ }
+
+ private Handler mHandler = new Handler() {
+ public void handleMessage(Message m) {
+ if (mStarted) {
+ updateText(SystemClock.elapsedRealtime());
+ sendMessageDelayed(Message.obtain(), 1000);
+ }
+ }
+ };
+}
diff --git a/core/java/android/widget/CompoundButton.java b/core/java/android/widget/CompoundButton.java
new file mode 100644
index 0000000..e56a741
--- /dev/null
+++ b/core/java/android/widget/CompoundButton.java
@@ -0,0 +1,318 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import com.android.internal.R;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.view.Gravity;
+
+
+/**
+ * <p>
+ * A button with two states, checked and unchecked. When the button is pressed
+ * or clicked, the state changes automatically.
+ * </p>
+ *
+ * <p><strong>XML attributes</strong></p>
+ * <p>
+ * See {@link android.R.styleable#CompoundButton
+ * CompoundButton Attributes}, {@link android.R.styleable#Button Button
+ * Attributes}, {@link android.R.styleable#TextView TextView Attributes}, {@link
+ * android.R.styleable#View View Attributes}
+ * </p>
+ */
+public abstract class CompoundButton extends Button implements Checkable {
+ private boolean mChecked;
+ private int mButtonResource;
+ private boolean mBroadcasting;
+ private Drawable mButtonDrawable;
+ private OnCheckedChangeListener mOnCheckedChangeListener;
+ private OnCheckedChangeListener mOnCheckedChangeWidgetListener;
+
+ private static final int[] CHECKED_STATE_SET = {
+ R.attr.state_checked
+ };
+
+ public CompoundButton(Context context) {
+ this(context, null);
+ }
+
+ public CompoundButton(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public CompoundButton(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ TypedArray a =
+ context.obtainStyledAttributes(
+ attrs, com.android.internal.R.styleable.CompoundButton, defStyle, 0);
+
+ Drawable d = a.getDrawable(com.android.internal.R.styleable.CompoundButton_button);
+ if (d != null) {
+ setButtonDrawable(d);
+ }
+
+ boolean checked = a
+ .getBoolean(com.android.internal.R.styleable.CompoundButton_checked, false);
+ setChecked(checked);
+
+ a.recycle();
+ }
+
+ public void toggle() {
+ setChecked(!mChecked);
+ }
+
+ @Override
+ public boolean performClick() {
+ /*
+ * XXX: These are tiny, need some surrounding 'expanded touch area',
+ * which will need to be implemented in Button if we only override
+ * performClick()
+ */
+
+ /* When clicked, toggle the state */
+ toggle();
+ return super.performClick();
+ }
+
+ public boolean isChecked() {
+ return mChecked;
+ }
+
+ /**
+ * <p>Changes the checked state of this button.</p>
+ *
+ * @param checked true to check the button, false to uncheck it
+ */
+ public void setChecked(boolean checked) {
+ if (mChecked != checked) {
+ mChecked = checked;
+ refreshDrawableState();
+
+ // Avoid infinite recursions if setChecked() is called from a listener
+ if (mBroadcasting) {
+ return;
+ }
+
+ mBroadcasting = true;
+ if (mOnCheckedChangeListener != null) {
+ mOnCheckedChangeListener.onCheckedChanged(this, mChecked);
+ }
+ if (mOnCheckedChangeWidgetListener != null) {
+ mOnCheckedChangeWidgetListener.onCheckedChanged(this, mChecked);
+ }
+ mBroadcasting = false;
+ }
+ }
+
+ /**
+ * Register a callback to be invoked when the checked state of this button
+ * changes.
+ *
+ * @param listener the callback to call on checked state change
+ */
+ public void setOnCheckedChangeListener(OnCheckedChangeListener listener) {
+ mOnCheckedChangeListener = listener;
+ }
+
+ /**
+ * Register a callback to be invoked when the checked state of this button
+ * changes. This callback is used for internal purpose only.
+ *
+ * @param listener the callback to call on checked state change
+ * @hide
+ */
+ void setOnCheckedChangeWidgetListener(OnCheckedChangeListener listener) {
+ mOnCheckedChangeWidgetListener = listener;
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when the checked state
+ * of a compound button changed.
+ */
+ public static interface OnCheckedChangeListener {
+ /**
+ * Called when the checked state of a compound button has changed.
+ *
+ * @param buttonView The compound button view whose state has changed.
+ * @param isChecked The new checked state of buttonView.
+ */
+ void onCheckedChanged(CompoundButton buttonView, boolean isChecked);
+ }
+
+ /**
+ * Set the background to a given Drawable, identified by its resource id.
+ *
+ * @param resid the resource id of the drawable to use as the background
+ */
+ public void setButtonDrawable(int resid) {
+ if (resid != 0 && resid == mButtonResource) {
+ return;
+ }
+
+ mButtonResource = resid;
+
+ Drawable d = null;
+ if (mButtonResource != 0) {
+ d = getResources().getDrawable(mButtonResource);
+ }
+ setButtonDrawable(d);
+ }
+
+ /**
+ * Set the background to a given Drawable
+ *
+ * @param d The Drawable to use as the background
+ */
+ public void setButtonDrawable(Drawable d) {
+ if (d != null) {
+ if (mButtonDrawable != null) {
+ mButtonDrawable.setCallback(null);
+ unscheduleDrawable(mButtonDrawable);
+ }
+ d.setCallback(this);
+ d.setState(getDrawableState());
+ d.setVisible(getVisibility() == VISIBLE, false);
+ mButtonDrawable = d;
+ mButtonDrawable.setState(null);
+ setMinHeight(mButtonDrawable.getIntrinsicHeight());
+ }
+
+ refreshDrawableState();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ final Drawable buttonDrawable = mButtonDrawable;
+ if (buttonDrawable != null) {
+ final int verticalGravity = getGravity() & Gravity.VERTICAL_GRAVITY_MASK;
+ final int height = buttonDrawable.getIntrinsicHeight();
+
+ int y = 0;
+
+ switch (verticalGravity) {
+ case Gravity.BOTTOM:
+ y = getHeight() - height;
+ break;
+ case Gravity.CENTER_VERTICAL:
+ y = (getHeight() - height) / 2;
+ break;
+ }
+
+ buttonDrawable.setBounds(0, y, buttonDrawable.getIntrinsicWidth(), y + height);
+ buttonDrawable.draw(canvas);
+ }
+ }
+
+ @Override
+ protected int[] onCreateDrawableState(int extraSpace) {
+ final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
+ if (isChecked()) {
+ mergeDrawableStates(drawableState, CHECKED_STATE_SET);
+ }
+ return drawableState;
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+
+ if (mButtonDrawable != null) {
+ int[] myDrawableState = getDrawableState();
+
+ // Set the state of the Drawable
+ mButtonDrawable.setState(myDrawableState);
+
+ invalidate();
+ }
+ }
+
+ static class SavedState extends BaseSavedState {
+ boolean checked;
+
+ /**
+ * Constructor called from {@link CompoundButton#onSaveInstanceState()}
+ */
+ SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ /**
+ * Constructor called from {@link #CREATOR}
+ */
+ private SavedState(Parcel in) {
+ super(in);
+ checked = (Boolean)in.readValue(null);
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ super.writeToParcel(out, flags);
+ out.writeValue(checked);
+ }
+
+ @Override
+ public String toString() {
+ return "CompoundButton.SavedState{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " checked=" + checked + "}";
+ }
+
+ public static final Parcelable.Creator<SavedState> CREATOR
+ = new Parcelable.Creator<SavedState>() {
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ // Force our ancestor class to save its state
+ setFreezesText(true);
+ Parcelable superState = super.onSaveInstanceState();
+
+ SavedState ss = new SavedState(superState);
+
+ ss.checked = isChecked();
+ return ss;
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ SavedState ss = (SavedState) state;
+
+ super.onRestoreInstanceState(ss.getSuperState());
+ setChecked(ss.checked);
+ requestLayout();
+ }
+}
diff --git a/core/java/android/widget/CursorAdapter.java b/core/java/android/widget/CursorAdapter.java
new file mode 100644
index 0000000..cacaeab
--- /dev/null
+++ b/core/java/android/widget/CursorAdapter.java
@@ -0,0 +1,382 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.database.DataSetObserver;
+import android.os.Handler;
+import android.util.Config;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Adapter that exposes data from a {@link android.database.Cursor Cursor} to a
+ * {@link android.widget.ListView ListView} widget. The Cursor must include
+ * a column named "_id" or this class will not work.
+ */
+public abstract class CursorAdapter extends BaseAdapter implements Filterable,
+ CursorFilter.CursorFilterClient {
+ /**
+ * This field should be made private, so it is hidden from the SDK.
+ * {@hide}
+ */
+ protected boolean mDataValid;
+ /**
+ * This field should be made private, so it is hidden from the SDK.
+ * {@hide}
+ */
+ protected boolean mAutoRequery;
+ /**
+ * This field should be made private, so it is hidden from the SDK.
+ * {@hide}
+ */
+ protected Cursor mCursor;
+ /**
+ * This field should be made private, so it is hidden from the SDK.
+ * {@hide}
+ */
+ protected Context mContext;
+ /**
+ * This field should be made private, so it is hidden from the SDK.
+ * {@hide}
+ */
+ protected int mRowIDColumn;
+ /**
+ * This field should be made private, so it is hidden from the SDK.
+ * {@hide}
+ */
+ protected ChangeObserver mChangeObserver;
+ /**
+ * This field should be made private, so it is hidden from the SDK.
+ * {@hide}
+ */
+ protected DataSetObserver mDataSetObserver = new MyDataSetObserver();
+ /**
+ * This field should be made private, so it is hidden from the SDK.
+ * {@hide}
+ */
+ protected CursorFilter mCursorFilter;
+ /**
+ * This field should be made private, so it is hidden from the SDK.
+ * {@hide}
+ */
+ protected FilterQueryProvider mFilterQueryProvider;
+
+ /**
+ * Constructor. The adapter will call requery() on the cursor whenever
+ * it changes so that the most recent data is always displayed.
+ *
+ * @param c The cursor from which to get the data.
+ * @param context The context
+ */
+ public CursorAdapter(Context context, Cursor c) {
+ init(context, c, true);
+ }
+
+ /**
+ * Constructor
+ * @param c The cursor from which to get the data.
+ * @param context The context
+ * @param autoRequery If true the adapter will call requery() on the
+ * cursor whenever it changes so the most recent
+ * data is always displayed.
+ */
+ public CursorAdapter(Context context, Cursor c, boolean autoRequery) {
+ init(context, c, autoRequery);
+ }
+
+ protected void init(Context context, Cursor c, boolean autoRequery) {
+ boolean cursorPresent = c != null;
+ mAutoRequery = autoRequery;
+ mCursor = c;
+ mDataValid = cursorPresent;
+ mContext = context;
+ mRowIDColumn = cursorPresent ? c.getColumnIndexOrThrow("_id") : -1;
+ mChangeObserver = new ChangeObserver();
+ if (cursorPresent) {
+ c.registerContentObserver(mChangeObserver);
+ c.registerDataSetObserver(mDataSetObserver);
+ }
+ }
+
+ /**
+ * Returns the cursor.
+ * @return the cursor.
+ */
+ public Cursor getCursor() {
+ return mCursor;
+ }
+
+ /**
+ * @see android.widget.ListAdapter#getCount()
+ */
+ public final int getCount() {
+ if (mDataValid && mCursor != null) {
+ return mCursor.getCount();
+ } else {
+ return 0;
+ }
+ }
+
+ /**
+ * @see android.widget.ListAdapter#getItem(int)
+ */
+ public final Object getItem(int position) {
+ if (mDataValid && mCursor != null) {
+ mCursor.moveToPosition(position);
+ return mCursor;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * @see android.widget.ListAdapter#getItemId(int)
+ */
+ public final long getItemId(int position) {
+ if (mDataValid && mCursor != null) {
+ if (mCursor.moveToPosition(position)) {
+ return mCursor.getLong(mRowIDColumn);
+ } else {
+ return 0;
+ }
+ } else {
+ return 0;
+ }
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ /**
+ * @see android.widget.ListAdapter#getView(int, View, ViewGroup)
+ */
+ public View getView(int position, View convertView, ViewGroup parent) {
+ if (!mDataValid) {
+ throw new IllegalStateException("this should only be called when the cursor is valid");
+ }
+ if (!mCursor.moveToPosition(position)) {
+ throw new IllegalStateException("couldn't move cursor to position " + position);
+ }
+ View v;
+ if (convertView == null) {
+ v = newView(mContext, mCursor, parent);
+ } else {
+ v = convertView;
+ }
+ bindView(v, mContext, mCursor);
+ return v;
+ }
+
+ @Override
+ public View getDropDownView(int position, View convertView, ViewGroup parent) {
+ if (mDataValid) {
+ mCursor.moveToPosition(position);
+ View v;
+ if (convertView == null) {
+ v = newDropDownView(mContext, mCursor, parent);
+ } else {
+ v = convertView;
+ }
+ bindView(v, mContext, mCursor);
+ return v;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Makes a new view to hold the data pointed to by cursor.
+ * @param context Interface to application's global information
+ * @param cursor The cursor from which to get the data. The cursor is already
+ * moved to the correct position.
+ * @param parent The parent to which the new view is attached to
+ * @return the newly created view.
+ */
+ public abstract View newView(Context context, Cursor cursor, ViewGroup parent);
+
+ /**
+ * Makes a new drop down view to hold the data pointed to by cursor.
+ * @param context Interface to application's global information
+ * @param cursor The cursor from which to get the data. The cursor is already
+ * moved to the correct position.
+ * @param parent The parent to which the new view is attached to
+ * @return the newly created view.
+ */
+ public View newDropDownView(Context context, Cursor cursor, ViewGroup parent) {
+ return newView(context, cursor, parent);
+ }
+
+ /**
+ * Bind an existing view to the data pointed to by cursor
+ * @param view Existing view, returned earlier by newView
+ * @param context Interface to application's global information
+ * @param cursor The cursor from which to get the data. The cursor is already
+ * moved to the correct position.
+ */
+ public abstract void bindView(View view, Context context, Cursor cursor);
+
+ /**
+ * Change the underlying cursor to a new cursor. If there is an existing cursor it will be
+ * closed.
+ *
+ * @param cursor the new cursor to be used
+ */
+ public void changeCursor(Cursor cursor) {
+ if (mCursor != null) {
+ mCursor.unregisterContentObserver(mChangeObserver);
+ mCursor.unregisterDataSetObserver(mDataSetObserver);
+ mCursor.close();
+ }
+ mCursor = cursor;
+ if (cursor != null) {
+ cursor.registerContentObserver(mChangeObserver);
+ cursor.registerDataSetObserver(mDataSetObserver);
+ mRowIDColumn = cursor.getColumnIndexOrThrow("_id");
+ mDataValid = true;
+ // notify the observers about the new cursor
+ notifyDataSetChanged();
+ } else {
+ mRowIDColumn = -1;
+ mDataValid = false;
+ // notify the observers about the lack of a data set
+ notifyDataSetInvalidated();
+ }
+ }
+
+ /**
+ * <p>Converts the cursor into a CharSequence. Subclasses should override this
+ * method to convert their results. The default implementation returns an
+ * empty String for null values or the default String representation of
+ * the value.</p>
+ *
+ * @param cursor the cursor to convert to a CharSequence
+ * @return a CharSequence representing the value
+ */
+ public CharSequence convertToString(Cursor cursor) {
+ return cursor == null ? "" : cursor.toString();
+ }
+
+ /**
+ * Runs a query with the specified constraint. This query is requested
+ * by the filter attached to this adapter.
+ *
+ * The query is provided by a
+ * {@link android.widget.FilterQueryProvider}.
+ * If no provider is specified, the current cursor is not filtered and returned.
+ *
+ * After this method returns the resulting cursor is passed to {@link #changeCursor(Cursor)}
+ * and the previous cursor is closed.
+ *
+ * This method is always executed on a background thread, not on the
+ * application's main thread (or UI thread.)
+ *
+ * Contract: when constraint is null or empty, the original results,
+ * prior to any filtering, must be returned.
+ *
+ * @param constraint the constraint with which the query must be filtered
+ *
+ * @return a Cursor representing the results of the new query
+ *
+ * @see #getFilter()
+ * @see #getFilterQueryProvider()
+ * @see #setFilterQueryProvider(android.widget.FilterQueryProvider)
+ */
+ public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
+ if (mFilterQueryProvider != null) {
+ return mFilterQueryProvider.runQuery(constraint);
+ }
+
+ return mCursor;
+ }
+
+ public Filter getFilter() {
+ if (mCursorFilter == null) {
+ mCursorFilter = new CursorFilter(this);
+ }
+ return mCursorFilter;
+ }
+
+ /**
+ * Returns the query filter provider used for filtering. When the
+ * provider is null, no filtering occurs.
+ *
+ * @return the current filter query provider or null if it does not exist
+ *
+ * @see #setFilterQueryProvider(android.widget.FilterQueryProvider)
+ * @see #runQueryOnBackgroundThread(CharSequence)
+ */
+ public FilterQueryProvider getFilterQueryProvider() {
+ return mFilterQueryProvider;
+ }
+
+ /**
+ * Sets the query filter provider used to filter the current Cursor.
+ * The provider's
+ * {@link android.widget.FilterQueryProvider#runQuery(CharSequence)}
+ * method is invoked when filtering is requested by a client of
+ * this adapter.
+ *
+ * @param filterQueryProvider the filter query provider or null to remove it
+ *
+ * @see #getFilterQueryProvider()
+ * @see #runQueryOnBackgroundThread(CharSequence)
+ */
+ public void setFilterQueryProvider(FilterQueryProvider filterQueryProvider) {
+ mFilterQueryProvider = filterQueryProvider;
+ }
+
+ private class ChangeObserver extends ContentObserver {
+ public ChangeObserver() {
+ super(new Handler());
+ }
+
+ @Override
+ public boolean deliverSelfNotifications() {
+ return true;
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ if (mAutoRequery && mCursor != null) {
+ if (Config.LOGV) Log.v("Cursor", "Auto requerying " + mCursor +
+ " due to update");
+ mDataValid = mCursor.requery();
+ }
+ }
+ }
+
+ private class MyDataSetObserver extends DataSetObserver {
+ @Override
+ public void onChanged() {
+ mDataValid = true;
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public void onInvalidated() {
+ mDataValid = false;
+ notifyDataSetInvalidated();
+ }
+ }
+
+}
diff --git a/core/java/android/widget/CursorFilter.java b/core/java/android/widget/CursorFilter.java
new file mode 100644
index 0000000..afd5b10
--- /dev/null
+++ b/core/java/android/widget/CursorFilter.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import android.database.Cursor;
+
+/**
+ * <p>The CursorFilter delegates most of the work to the CursorAdapter.
+ * Subclasses should override these delegate methods to run the queries
+ * and convert the results into String that can be used by auto-completion
+ * widgets.</p>
+ */
+class CursorFilter extends Filter {
+
+ CursorFilterClient mClient;
+
+ interface CursorFilterClient {
+ CharSequence convertToString(Cursor cursor);
+ Cursor runQueryOnBackgroundThread(CharSequence constraint);
+ Cursor getCursor();
+ void changeCursor(Cursor cursor);
+ }
+
+ CursorFilter(CursorFilterClient client) {
+ mClient = client;
+ }
+
+ @Override
+ public CharSequence convertResultToString(Object resultValue) {
+ return mClient.convertToString((Cursor) resultValue);
+ }
+
+ @Override
+ protected FilterResults performFiltering(CharSequence constraint) {
+ Cursor cursor = mClient.runQueryOnBackgroundThread(constraint);
+
+ FilterResults results = new FilterResults();
+ if (cursor != null) {
+ results.count = cursor.getCount();
+ results.values = cursor;
+ } else {
+ results.count = 0;
+ results.values = null;
+ }
+ return results;
+ }
+
+ @Override
+ protected void publishResults(CharSequence constraint,
+ FilterResults results) {
+ Cursor oldCursor = mClient.getCursor();
+
+ if (results.values != oldCursor) {
+ mClient.changeCursor((Cursor) results.values);
+ }
+ }
+}
diff --git a/core/java/android/widget/CursorTreeAdapter.java b/core/java/android/widget/CursorTreeAdapter.java
new file mode 100644
index 0000000..fa8fd4b
--- /dev/null
+++ b/core/java/android/widget/CursorTreeAdapter.java
@@ -0,0 +1,522 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import android.app.Activity;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.database.DataSetObserver;
+import android.os.Handler;
+import android.util.Config;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * An adapter that exposes data from a series of {@link Cursor}s to an
+ * {@link ExpandableListView} widget. The top-level {@link Cursor} (that is
+ * given in the constructor) exposes the groups, while subsequent {@link Cursor}s
+ * returned from {@link #getChildrenCursor(Cursor)} expose children within a
+ * particular group. The Cursors must include a column named "_id" or this class
+ * will not work.
+ */
+public abstract class CursorTreeAdapter extends BaseExpandableListAdapter implements Filterable,
+ CursorFilter.CursorFilterClient {
+ private Context mContext;
+ private Handler mHandler;
+ private boolean mAutoRequery;
+
+ /** The cursor helper that is used to get the groups */
+ MyCursorHelper mGroupCursorHelper;
+
+ /**
+ * The map of a group position to the group's children cursor helper (the
+ * cursor helper that is used to get the children for that group)
+ */
+ SparseArray<MyCursorHelper> mChildrenCursorHelpers;
+
+ // Filter related
+ CursorFilter mCursorFilter;
+ FilterQueryProvider mFilterQueryProvider;
+
+ /**
+ * Constructor. The adapter will call {@link Cursor#requery()} on the cursor whenever
+ * it changes so that the most recent data is always displayed.
+ *
+ * @param cursor The cursor from which to get the data for the groups.
+ */
+ public CursorTreeAdapter(Cursor cursor, Context context) {
+ init(cursor, context, true);
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param cursor The cursor from which to get the data for the groups.
+ * @param context The context
+ * @param autoRequery If true the adapter will call {@link Cursor#requery()}
+ * on the cursor whenever it changes so the most recent data is
+ * always displayed.
+ */
+ public CursorTreeAdapter(Cursor cursor, Context context, boolean autoRequery) {
+ init(cursor, context, autoRequery);
+ }
+
+ private void init(Cursor cursor, Context context, boolean autoRequery) {
+ mContext = context;
+ mHandler = new Handler();
+ mAutoRequery = autoRequery;
+
+ mGroupCursorHelper = new MyCursorHelper(cursor);
+ mChildrenCursorHelpers = new SparseArray<MyCursorHelper>();
+ }
+
+ /**
+ * Gets the cursor helper for the children in the given group.
+ *
+ * @param groupPosition The group whose children will be returned
+ * @param requestCursor Whether to request a Cursor via
+ * {@link #getChildrenCursor(Cursor)} (true), or to assume a call
+ * to {@link #setChildrenCursor(int, Cursor)} will happen shortly
+ * (false).
+ * @return The cursor helper for the children of the given group
+ */
+ synchronized MyCursorHelper getChildrenCursorHelper(int groupPosition, boolean requestCursor) {
+ MyCursorHelper cursorHelper = mChildrenCursorHelpers.get(groupPosition);
+
+ if (cursorHelper == null) {
+ if (mGroupCursorHelper.moveTo(groupPosition) == null) return null;
+
+ final Cursor cursor = getChildrenCursor(mGroupCursorHelper.getCursor());
+ cursorHelper = new MyCursorHelper(cursor);
+ mChildrenCursorHelpers.put(groupPosition, cursorHelper);
+ }
+
+ return cursorHelper;
+ }
+
+ /**
+ * Gets the Cursor for the children at the given group. Subclasses must
+ * implement this method to return the children data for a particular group.
+ * <p>
+ * If you want to asynchronously query a provider to prevent blocking the
+ * UI, it is possible to return null and at a later time call
+ * {@link #setChildrenCursor(int, Cursor)}.
+ * <p>
+ * It is your responsibility to manage this Cursor through the Activity
+ * lifecycle. It is a good idea to use {@link Activity#managedQuery} which
+ * will handle this for you. In some situations, the adapter will deactivate
+ * the Cursor on its own, but this will not always be the case, so please
+ * ensure the Cursor is properly managed.
+ *
+ * @param groupCursor The cursor pointing to the group whose children cursor
+ * should be returned
+ * @return The cursor for the children of a particular group, or null.
+ */
+ abstract protected Cursor getChildrenCursor(Cursor groupCursor);
+
+ /**
+ * Sets the group Cursor.
+ *
+ * @param cursor The Cursor to set for the group.
+ */
+ public void setGroupCursor(Cursor cursor) {
+ mGroupCursorHelper.changeCursor(cursor, false);
+ }
+
+ /**
+ * Sets the children Cursor for a particular group.
+ * <p>
+ * This is useful when asynchronously querying to prevent blocking the UI.
+ *
+ * @param groupPosition The group whose children are being set via this Cursor.
+ * @param childrenCursor The Cursor that contains the children of the group.
+ */
+ public void setChildrenCursor(int groupPosition, Cursor childrenCursor) {
+
+ /*
+ * Don't request a cursor from the subclass, instead we will be setting
+ * the cursor ourselves.
+ */
+ MyCursorHelper childrenCursorHelper = getChildrenCursorHelper(groupPosition, false);
+
+ /*
+ * Don't release any cursor since we know exactly what data is changing
+ * (this cursor, which is still valid).
+ */
+ childrenCursorHelper.changeCursor(childrenCursor, false);
+ }
+
+ public Cursor getChild(int groupPosition, int childPosition) {
+ // Return this group's children Cursor pointing to the particular child
+ return getChildrenCursorHelper(groupPosition, true).moveTo(childPosition);
+ }
+
+ public long getChildId(int groupPosition, int childPosition) {
+ return getChildrenCursorHelper(groupPosition, true).getId(childPosition);
+ }
+
+ public int getChildrenCount(int groupPosition) {
+ MyCursorHelper helper = getChildrenCursorHelper(groupPosition, true);
+ return (mGroupCursorHelper.isValid() && helper != null) ? helper.getCount() : 0;
+ }
+
+ public Cursor getGroup(int groupPosition) {
+ // Return the group Cursor pointing to the given group
+ return mGroupCursorHelper.moveTo(groupPosition);
+ }
+
+ public int getGroupCount() {
+ return mGroupCursorHelper.getCount();
+ }
+
+ public long getGroupId(int groupPosition) {
+ return mGroupCursorHelper.getId(groupPosition);
+ }
+
+ public View getGroupView(int groupPosition, boolean isExpanded, View convertView,
+ ViewGroup parent) {
+ Cursor cursor = mGroupCursorHelper.moveTo(groupPosition);
+ if (cursor == null) {
+ throw new IllegalStateException("this should only be called when the cursor is valid");
+ }
+
+ View v;
+ if (convertView == null) {
+ v = newGroupView(mContext, cursor, isExpanded, parent);
+ } else {
+ v = convertView;
+ }
+ bindGroupView(v, mContext, cursor, isExpanded);
+ return v;
+ }
+
+ /**
+ * Makes a new group view to hold the group data pointed to by cursor.
+ *
+ * @param context Interface to application's global information
+ * @param cursor The group cursor from which to get the data. The cursor is
+ * already moved to the correct position.
+ * @param isExpanded Whether the group is expanded.
+ * @param parent The parent to which the new view is attached to
+ * @return The newly created view.
+ */
+ protected abstract View newGroupView(Context context, Cursor cursor, boolean isExpanded,
+ ViewGroup parent);
+
+ /**
+ * Bind an existing view to the group data pointed to by cursor.
+ *
+ * @param view Existing view, returned earlier by newGroupView.
+ * @param context Interface to application's global information
+ * @param cursor The cursor from which to get the data. The cursor is
+ * already moved to the correct position.
+ * @param isExpanded Whether the group is expanded.
+ */
+ protected abstract void bindGroupView(View view, Context context, Cursor cursor,
+ boolean isExpanded);
+
+ public View getChildView(int groupPosition, int childPosition, boolean isLastChild,
+ View convertView, ViewGroup parent) {
+ MyCursorHelper cursorHelper = getChildrenCursorHelper(groupPosition, true);
+
+ Cursor cursor = cursorHelper.moveTo(childPosition);
+ if (cursor == null) {
+ throw new IllegalStateException("this should only be called when the cursor is valid");
+ }
+
+ View v;
+ if (convertView == null) {
+ v = newChildView(mContext, cursor, isLastChild, parent);
+ } else {
+ v = convertView;
+ }
+ bindChildView(v, mContext, cursor, isLastChild);
+ return v;
+ }
+
+ /**
+ * Makes a new child view to hold the data pointed to by cursor.
+ *
+ * @param context Interface to application's global information
+ * @param cursor The cursor from which to get the data. The cursor is
+ * already moved to the correct position.
+ * @param isLastChild Whether the child is the last child within its group.
+ * @param parent The parent to which the new view is attached to
+ * @return the newly created view.
+ */
+ protected abstract View newChildView(Context context, Cursor cursor, boolean isLastChild,
+ ViewGroup parent);
+
+ /**
+ * Bind an existing view to the child data pointed to by cursor
+ *
+ * @param view Existing view, returned earlier by newChildView
+ * @param context Interface to application's global information
+ * @param cursor The cursor from which to get the data. The cursor is
+ * already moved to the correct position.
+ * @param isLastChild Whether the child is the last child within its group.
+ */
+ protected abstract void bindChildView(View view, Context context, Cursor cursor,
+ boolean isLastChild);
+
+ public boolean isChildSelectable(int groupPosition, int childPosition) {
+ return true;
+ }
+
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ private synchronized void releaseCursorHelpers() {
+ for (int pos = mChildrenCursorHelpers.size() - 1; pos >= 0; pos--) {
+ mChildrenCursorHelpers.valueAt(pos).deactivate();
+ }
+
+ mChildrenCursorHelpers.clear();
+ }
+
+ @Override
+ public void notifyDataSetChanged() {
+ notifyDataSetChanged(true);
+ }
+
+ /**
+ * Notifies a data set change, but with the option of not releasing any
+ * cached cursors.
+ *
+ * @param releaseCursors Whether to release and deactivate any cached
+ * cursors.
+ */
+ public void notifyDataSetChanged(boolean releaseCursors) {
+
+ if (releaseCursors) {
+ releaseCursorHelpers();
+ }
+
+ super.notifyDataSetChanged();
+ }
+
+ @Override
+ public void notifyDataSetInvalidated() {
+ releaseCursorHelpers();
+ super.notifyDataSetInvalidated();
+ }
+
+ @Override
+ public void onGroupCollapsed(int groupPosition) {
+ deactivateChildrenCursorHelper(groupPosition);
+ }
+
+ /**
+ * Deactivates the Cursor and removes the helper from cache.
+ *
+ * @param groupPosition The group whose children Cursor and helper should be
+ * deactivated.
+ */
+ synchronized void deactivateChildrenCursorHelper(int groupPosition) {
+ MyCursorHelper cursorHelper = getChildrenCursorHelper(groupPosition, true);
+ mChildrenCursorHelpers.remove(groupPosition);
+ cursorHelper.deactivate();
+ }
+
+ /**
+ * @see CursorAdapter#convertToString(Cursor)
+ */
+ public String convertToString(Cursor cursor) {
+ return cursor == null ? "" : cursor.toString();
+ }
+
+ /**
+ * @see CursorAdapter#runQueryOnBackgroundThread(CharSequence)
+ */
+ public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
+ if (mFilterQueryProvider != null) {
+ return mFilterQueryProvider.runQuery(constraint);
+ }
+
+ return mGroupCursorHelper.getCursor();
+ }
+
+ public Filter getFilter() {
+ if (mCursorFilter == null) {
+ mCursorFilter = new CursorFilter(this);
+ }
+ return mCursorFilter;
+ }
+
+ /**
+ * @see CursorAdapter#getFilterQueryProvider()
+ */
+ public FilterQueryProvider getFilterQueryProvider() {
+ return mFilterQueryProvider;
+ }
+
+ /**
+ * @see CursorAdapter#setFilterQueryProvider(FilterQueryProvider)
+ */
+ public void setFilterQueryProvider(FilterQueryProvider filterQueryProvider) {
+ mFilterQueryProvider = filterQueryProvider;
+ }
+
+ /**
+ * @see CursorAdapter#changeCursor(Cursor)
+ */
+ public void changeCursor(Cursor cursor) {
+ mGroupCursorHelper.changeCursor(cursor, true);
+ }
+
+ /**
+ * @see CursorAdapter#getCursor()
+ */
+ public Cursor getCursor() {
+ return mGroupCursorHelper.getCursor();
+ }
+
+ /**
+ * Helper class for Cursor management:
+ * <li> Data validity
+ * <li> Funneling the content and data set observers from a Cursor to a
+ * single data set observer for widgets
+ * <li> ID from the Cursor for use in adapter IDs
+ * <li> Swapping cursors but maintaining other metadata
+ */
+ class MyCursorHelper {
+ private Cursor mCursor;
+ private boolean mDataValid;
+ private int mRowIDColumn;
+ private MyContentObserver mContentObserver;
+ private MyDataSetObserver mDataSetObserver;
+
+ MyCursorHelper(Cursor cursor) {
+ final boolean cursorPresent = cursor != null;
+ mCursor = cursor;
+ mDataValid = cursorPresent;
+ mRowIDColumn = cursorPresent ? cursor.getColumnIndex("_id") : -1;
+ mContentObserver = new MyContentObserver();
+ mDataSetObserver = new MyDataSetObserver();
+ if (cursorPresent) {
+ cursor.registerContentObserver(mContentObserver);
+ cursor.registerDataSetObserver(mDataSetObserver);
+ }
+ }
+
+ Cursor getCursor() {
+ return mCursor;
+ }
+
+ int getCount() {
+ if (mDataValid && mCursor != null) {
+ return mCursor.getCount();
+ } else {
+ return 0;
+ }
+ }
+
+ long getId(int position) {
+ if (mDataValid && mCursor != null) {
+ if (mCursor.moveToPosition(position)) {
+ return mCursor.getLong(mRowIDColumn);
+ } else {
+ return 0;
+ }
+ } else {
+ return 0;
+ }
+ }
+
+ Cursor moveTo(int position) {
+ if (mDataValid && (mCursor != null) && mCursor.moveToPosition(position)) {
+ return mCursor;
+ } else {
+ return null;
+ }
+ }
+
+ void changeCursor(Cursor cursor, boolean releaseCursors) {
+ if (mCursor != null) {
+ mCursor.unregisterContentObserver(mContentObserver);
+ mCursor.unregisterDataSetObserver(mDataSetObserver);
+ }
+ mCursor = cursor;
+ if (cursor != null) {
+ cursor.registerContentObserver(mContentObserver);
+ cursor.registerDataSetObserver(mDataSetObserver);
+ mRowIDColumn = cursor.getColumnIndex("_id");
+ mDataValid = true;
+ // notify the observers about the new cursor
+ notifyDataSetChanged(releaseCursors);
+ } else {
+ mRowIDColumn = -1;
+ mDataValid = false;
+ // notify the observers about the lack of a data set
+ notifyDataSetInvalidated();
+ }
+ }
+
+ void deactivate() {
+ if (mCursor == null) {
+ return;
+ }
+
+ mCursor.unregisterContentObserver(mContentObserver);
+ mCursor.unregisterDataSetObserver(mDataSetObserver);
+ mCursor.deactivate();
+ mCursor = null;
+ }
+
+ boolean isValid() {
+ return mDataValid && mCursor != null;
+ }
+
+ private class MyContentObserver extends ContentObserver {
+ public MyContentObserver() {
+ super(mHandler);
+ }
+
+ @Override
+ public boolean deliverSelfNotifications() {
+ return true;
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ if (mAutoRequery && mCursor != null) {
+ if (Config.LOGV) Log.v("Cursor", "Auto requerying " + mCursor +
+ " due to update");
+ mDataValid = mCursor.requery();
+ }
+ }
+ }
+
+ private class MyDataSetObserver extends DataSetObserver {
+ @Override
+ public void onChanged() {
+ mDataValid = true;
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public void onInvalidated() {
+ mDataValid = false;
+ notifyDataSetInvalidated();
+ }
+ }
+ }
+}
diff --git a/core/java/android/widget/DatePicker.java b/core/java/android/widget/DatePicker.java
new file mode 100644
index 0000000..c03bd32
--- /dev/null
+++ b/core/java/android/widget/DatePicker.java
@@ -0,0 +1,330 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import android.annotation.Widget;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.pim.DateFormat;
+import android.util.AttributeSet;
+import android.util.SparseArray;
+import android.view.LayoutInflater;
+
+import com.android.internal.R;
+import com.android.internal.widget.NumberPicker;
+import com.android.internal.widget.NumberPicker.OnChangedListener;
+
+import java.text.DateFormatSymbols;
+import java.util.Calendar;
+
+/**
+ * A view for selecting a month / year / day based on a calendar like layout.
+ *
+ * For a dialog using this view, see {@link android.app.DatePickerDialog}.
+ */
+@Widget
+public class DatePicker extends FrameLayout {
+
+ private static final int DEFAULT_START_YEAR = 1900;
+ private static final int DEFAULT_END_YEAR = 2100;
+
+ /* UI Components */
+ private final NumberPicker mDayPicker;
+ private final NumberPicker mMonthPicker;
+ private final NumberPicker mYearPicker;
+
+ private final int mStartYear;
+ private final int mEndYear;
+
+ /**
+ * How we notify users the date has changed.
+ */
+ private OnDateChangedListener mOnDateChangedListener;
+
+ private int mDay;
+ private int mMonth;
+ private int mYear;
+
+ /**
+ * The callback used to indicate the user changes the date.
+ */
+ public interface OnDateChangedListener {
+
+ /**
+ * @param view The view associated with this listener.
+ * @param year The year that was set.
+ * @param monthOfYear The month that was set (0-11) for compatibility
+ * with {@link java.util.Calendar}.
+ * @param dayOfMonth The day of the month that was set.
+ */
+ void onDateChanged(DatePicker view, int year, int monthOfYear, int dayOfMonth);
+ }
+
+ public DatePicker(Context context) {
+ this(context, null);
+ }
+
+ public DatePicker(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public DatePicker(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ LayoutInflater inflater =
+ (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ inflater.inflate(R.layout.date_picker,
+ this, // we are the parent
+ true);
+
+ mDayPicker = (NumberPicker) findViewById(R.id.day);
+ mDayPicker.setFormatter(NumberPicker.TWO_DIGIT_FORMATTER);
+ mDayPicker.setSpeed(100);
+ mDayPicker.setOnChangeListener(new OnChangedListener() {
+ public void onChanged(NumberPicker picker, int oldVal, int newVal) {
+ mDay = newVal;
+ if (mOnDateChangedListener != null) {
+ mOnDateChangedListener.onDateChanged(DatePicker.this, mYear, mMonth, mDay);
+ }
+ }
+ });
+ mMonthPicker = (NumberPicker) findViewById(R.id.month);
+ mMonthPicker.setFormatter(NumberPicker.TWO_DIGIT_FORMATTER);
+ DateFormatSymbols dfs = new DateFormatSymbols();
+ mMonthPicker.setRange(1, 12, dfs.getShortMonths());
+ mMonthPicker.setSpeed(200);
+ mMonthPicker.setOnChangeListener(new OnChangedListener() {
+ public void onChanged(NumberPicker picker, int oldVal, int newVal) {
+
+ /* We display the month 1-12 but store it 0-11 so always
+ * subtract by one to ensure our internal state is always 0-11
+ */
+ mMonth = newVal - 1;
+ if (mOnDateChangedListener != null) {
+ mOnDateChangedListener.onDateChanged(DatePicker.this, mYear, mMonth, mDay);
+ }
+ updateDaySpinner();
+ }
+ });
+ mYearPicker = (NumberPicker) findViewById(R.id.year);
+ mYearPicker.setSpeed(100);
+ mYearPicker.setOnChangeListener(new OnChangedListener() {
+ public void onChanged(NumberPicker picker, int oldVal, int newVal) {
+ mYear = newVal;
+ if (mOnDateChangedListener != null) {
+ mOnDateChangedListener.onDateChanged(DatePicker.this, mYear, mMonth, mDay);
+ }
+ }
+ });
+
+ // attributes
+ TypedArray a = context
+ .obtainStyledAttributes(attrs, R.styleable.DatePicker);
+
+ mStartYear = a.getInt(R.styleable.DatePicker_startYear, DEFAULT_START_YEAR);
+ mEndYear = a.getInt(R.styleable.DatePicker_endYear, DEFAULT_END_YEAR);
+ mYearPicker.setRange(mStartYear, mEndYear);
+
+ a.recycle();
+
+ // initialize to current date
+ Calendar cal = Calendar.getInstance();
+ init(cal.get(Calendar.YEAR),
+ cal.get(Calendar.MONTH),
+ cal.get(Calendar.DAY_OF_MONTH), null);
+
+ // re-order the number pickers to match the current date format
+ reorderPickers();
+
+ if (!isEnabled()) {
+ setEnabled(false);
+ }
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ super.setEnabled(enabled);
+ mDayPicker.setEnabled(enabled);
+ mMonthPicker.setEnabled(enabled);
+ mYearPicker.setEnabled(enabled);
+ }
+
+ private void reorderPickers() {
+ char[] order = DateFormat.getDateFormatOrder(mContext);
+
+ /* Default order is month, date, year so if that's the order then
+ * do nothing.
+ */
+ if ((order[0] == DateFormat.MONTH) && (order[1] == DateFormat.DATE)) {
+ return;
+ }
+
+ /* Remove the 3 pickers from their parent and then add them back in the
+ * required order.
+ */
+ LinearLayout parent = (LinearLayout) findViewById(R.id.parent);
+ parent.removeAllViews();
+ for (char c : order) {
+ if (c == DateFormat.DATE) {
+ parent.addView(mDayPicker);
+ } else if (c == DateFormat.MONTH) {
+ parent.addView(mMonthPicker);
+ } else {
+ parent.addView (mYearPicker);
+ }
+ }
+ }
+
+ public void updateDate(int year, int monthOfYear, int dayOfMonth) {
+ mYear = year;
+ mMonth = monthOfYear;
+ mDay = dayOfMonth;
+ updateSpinners();
+ }
+
+ private static class SavedState extends BaseSavedState {
+
+ private final int mYear;
+ private final int mMonth;
+ private final int mDay;
+
+ /**
+ * Constructor called from {@link DatePicker#onSaveInstanceState()}
+ */
+ private SavedState(Parcelable superState, int year, int month, int day) {
+ super(superState);
+ mYear = year;
+ mMonth = month;
+ mDay = day;
+ }
+
+ /**
+ * Constructor called from {@link #CREATOR}
+ */
+ private SavedState(Parcel in) {
+ super(in);
+ mYear = in.readInt();
+ mMonth = in.readInt();
+ mDay = in.readInt();
+ }
+
+ public int getYear() {
+ return mYear;
+ }
+
+ public int getMonth() {
+ return mMonth;
+ }
+
+ public int getDay() {
+ return mDay;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeInt(mYear);
+ dest.writeInt(mMonth);
+ dest.writeInt(mDay);
+ }
+
+ public static final Parcelable.Creator<SavedState> CREATOR =
+ new Creator<SavedState>() {
+
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+
+
+ /**
+ * Override so we are in complete control of save / restore for this widget.
+ */
+ @Override
+ protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
+ dispatchThawSelfOnly(container);
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ Parcelable superState = super.onSaveInstanceState();
+
+ return new SavedState(superState, mYear, mMonth, mDay);
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ SavedState ss = (SavedState) state;
+ super.onRestoreInstanceState(ss.getSuperState());
+ mYear = ss.getYear();
+ mMonth = ss.getMonth();
+ mDay = ss.getDay();
+ }
+
+ /**
+ * Initialize the state.
+ * @param year The initial year.
+ * @param monthOfYear The initial month.
+ * @param dayOfMonth The initial day of the month.
+ * @param onDateChangedListener How user is notified date is changed by user, can be null.
+ */
+ public void init(int year, int monthOfYear, int dayOfMonth,
+ OnDateChangedListener onDateChangedListener) {
+ mYear = year;
+ mMonth = monthOfYear;
+ mDay = dayOfMonth;
+ mOnDateChangedListener = onDateChangedListener;
+ updateSpinners();
+ }
+
+ private void updateSpinners() {
+ updateDaySpinner();
+ mYearPicker.setCurrent(mYear);
+
+ /* The month display uses 1-12 but our internal state stores it
+ * 0-11 so add one when setting the display.
+ */
+ mMonthPicker.setCurrent(mMonth + 1);
+ }
+
+ private void updateDaySpinner() {
+ Calendar cal = Calendar.getInstance();
+ cal.set(mYear, mMonth, mDay);
+ int max = cal.getActualMaximum(Calendar.DAY_OF_MONTH);
+ mDayPicker.setRange(1, max);
+ mDayPicker.setCurrent(mDay);
+ }
+
+ public int getYear() {
+ return mYear;
+ }
+
+ public int getMonth() {
+ return mMonth;
+ }
+
+ public int getDayOfMonth() {
+ return mDay;
+ }
+}
diff --git a/core/java/android/widget/DialerFilter.java b/core/java/android/widget/DialerFilter.java
new file mode 100644
index 0000000..a23887f
--- /dev/null
+++ b/core/java/android/widget/DialerFilter.java
@@ -0,0 +1,431 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.view.KeyEvent;
+import android.text.Editable;
+import android.text.InputFilter;
+import android.text.Selection;
+import android.text.Spannable;
+import android.text.Spanned;
+import android.text.TextWatcher;
+import android.text.method.DialerKeyListener;
+import android.text.method.KeyListener;
+import android.text.method.TextKeyListener;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.KeyCharacterMap;
+import android.view.View;
+import android.graphics.Rect;
+
+
+
+public class DialerFilter extends RelativeLayout
+{
+ public DialerFilter(Context context) {
+ super(context);
+ }
+
+ public DialerFilter(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ // Setup the filter view
+ mInputFilters = new InputFilter[] { new InputFilter.AllCaps() };
+
+ mHint = (EditText) findViewById(com.android.internal.R.id.hint);
+ if (mHint == null) {
+ throw new IllegalStateException("DialerFilter must have a child EditText named hint");
+ }
+ mHint.setFilters(mInputFilters);
+
+ mLetters = mHint;
+ mLetters.setKeyListener(TextKeyListener.getInstance());
+ mLetters.setMovementMethod(null);
+ mLetters.setFocusable(false);
+
+ // Setup the digits view
+ mPrimary = (EditText) findViewById(com.android.internal.R.id.primary);
+ if (mPrimary == null) {
+ throw new IllegalStateException("DialerFilter must have a child EditText named primary");
+ }
+ mPrimary.setFilters(mInputFilters);
+
+ mDigits = mPrimary;
+ mDigits.setKeyListener(DialerKeyListener.getInstance());
+ mDigits.setMovementMethod(null);
+ mDigits.setFocusable(false);
+
+ // Look for an icon
+ mIcon = (ImageView) findViewById(com.android.internal.R.id.icon);
+
+ // Setup focus & highlight for this view
+ setFocusable(true);
+
+ // Default the mode based on the keyboard
+ KeyCharacterMap kmap
+ = KeyCharacterMap.load(KeyCharacterMap.BUILT_IN_KEYBOARD);
+ mIsQwerty = kmap.getKeyboardType() != KeyCharacterMap.NUMERIC;
+ if (mIsQwerty) {
+ Log.i("DialerFilter", "This device looks to be QWERTY");
+// setMode(DIGITS_AND_LETTERS);
+ } else {
+ Log.i("DialerFilter", "This device looks to be 12-KEY");
+// setMode(DIGITS_ONLY);
+ }
+
+ // XXX Force the mode to QWERTY for now, since 12-key isn't supported
+ mIsQwerty = true;
+ setMode(DIGITS_AND_LETTERS);
+ }
+
+ /**
+ * Only show the icon view when focused, if there is one.
+ */
+ @Override
+ protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
+ super.onFocusChanged(focused, direction, previouslyFocusedRect);
+
+ if (mIcon != null) {
+ mIcon.setVisibility(focused ? View.VISIBLE : View.GONE);
+ }
+ }
+
+
+ public boolean isQwertyKeyboard() {
+ return mIsQwerty;
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ boolean handled = false;
+
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_UP:
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ case KeyEvent.KEYCODE_ENTER:
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ break;
+
+ case KeyEvent.KEYCODE_DEL:
+ switch (mMode) {
+ case DIGITS_AND_LETTERS:
+ handled = mDigits.onKeyDown(keyCode, event);
+ handled &= mLetters.onKeyDown(keyCode, event);
+ break;
+
+ case DIGITS_AND_LETTERS_NO_DIGITS:
+ handled = mLetters.onKeyDown(keyCode, event);
+ if (mLetters.getText().length() == mDigits.getText().length()) {
+ setMode(DIGITS_AND_LETTERS);
+ }
+ break;
+
+ case DIGITS_AND_LETTERS_NO_LETTERS:
+ if (mDigits.getText().length() == mLetters.getText().length()) {
+ mLetters.onKeyDown(keyCode, event);
+ setMode(DIGITS_AND_LETTERS);
+ }
+ handled = mDigits.onKeyDown(keyCode, event);
+ break;
+
+ case DIGITS_ONLY:
+ handled = mDigits.onKeyDown(keyCode, event);
+ break;
+
+ case LETTERS_ONLY:
+ handled = mLetters.onKeyDown(keyCode, event);
+ break;
+ }
+ break;
+
+ default:
+ //mIsQwerty = msg.getKeyIsQwertyKeyboard();
+
+ switch (mMode) {
+ case DIGITS_AND_LETTERS:
+ handled = mLetters.onKeyDown(keyCode, event);
+
+ // pass this throw so the shift state is correct (for example,
+ // on a standard QWERTY keyboard, * and 8 are on the same key)
+ if (KeyEvent.isModifierKey(keyCode)) {
+ mDigits.onKeyDown(keyCode, event);
+ handled = true;
+ break;
+ }
+
+ // Only check to see if the digit is valid if the key is a printing key
+ // in the TextKeyListener. This prevents us from hiding the digits
+ // line when keys like UP and DOWN are hit.
+ // XXX note that KEYCODE_TAB is special-cased here for
+ // devices that share tab and 0 on a single key.
+ boolean isPrint = event.isPrintingKey();
+ if (isPrint || keyCode == KeyEvent.KEYCODE_SPACE
+ || keyCode == KeyEvent.KEYCODE_TAB) {
+ char c = event.getMatch(DialerKeyListener.CHARACTERS);
+ if (c != 0) {
+ handled &= mDigits.onKeyDown(keyCode, event);
+ } else {
+ setMode(DIGITS_AND_LETTERS_NO_DIGITS);
+ }
+ }
+ break;
+
+ case DIGITS_AND_LETTERS_NO_LETTERS:
+ case DIGITS_ONLY:
+ handled = mDigits.onKeyDown(keyCode, event);
+ break;
+
+ case DIGITS_AND_LETTERS_NO_DIGITS:
+ case LETTERS_ONLY:
+ handled = mLetters.onKeyDown(keyCode, event);
+ break;
+ }
+ }
+
+ if (!handled) {
+ return super.onKeyDown(keyCode, event);
+ } else {
+ return true;
+ }
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ boolean a = mLetters.onKeyUp(keyCode, event);
+ boolean b = mDigits.onKeyUp(keyCode, event);
+ return a || b;
+ }
+
+ public int getMode() {
+ return mMode;
+ }
+
+ /**
+ * Change the mode of the widget.
+ *
+ * @param newMode The mode to switch to.
+ */
+ public void setMode(int newMode) {
+ switch (newMode) {
+ case DIGITS_AND_LETTERS:
+ makeDigitsPrimary();
+ mLetters.setVisibility(View.VISIBLE);
+ mDigits.setVisibility(View.VISIBLE);
+ break;
+
+ case DIGITS_ONLY:
+ makeDigitsPrimary();
+ mLetters.setVisibility(View.GONE);
+ mDigits.setVisibility(View.VISIBLE);
+ break;
+
+ case LETTERS_ONLY:
+ makeLettersPrimary();
+ mLetters.setVisibility(View.VISIBLE);
+ mDigits.setVisibility(View.GONE);
+ break;
+
+ case DIGITS_AND_LETTERS_NO_LETTERS:
+ makeDigitsPrimary();
+ mLetters.setVisibility(View.INVISIBLE);
+ mDigits.setVisibility(View.VISIBLE);
+ break;
+
+ case DIGITS_AND_LETTERS_NO_DIGITS:
+ makeLettersPrimary();
+ mLetters.setVisibility(View.VISIBLE);
+ mDigits.setVisibility(View.INVISIBLE);
+ break;
+
+ }
+ int oldMode = mMode;
+ mMode = newMode;
+ onModeChange(oldMode, newMode);
+ }
+
+ private void makeLettersPrimary() {
+ if (mPrimary == mDigits) {
+ swapPrimaryAndHint(true);
+ }
+ }
+
+ private void makeDigitsPrimary() {
+ if (mPrimary == mLetters) {
+ swapPrimaryAndHint(false);
+ }
+ }
+
+ private void swapPrimaryAndHint(boolean makeLettersPrimary) {
+ Editable lettersText = mLetters.getText();
+ Editable digitsText = mDigits.getText();
+ KeyListener lettersInput = mLetters.getKeyListener();
+ KeyListener digitsInput = mDigits.getKeyListener();
+
+ if (makeLettersPrimary) {
+ mLetters = mPrimary;
+ mDigits = mHint;
+ } else {
+ mLetters = mHint;
+ mDigits = mPrimary;
+ }
+
+ mLetters.setKeyListener(lettersInput);
+ mLetters.setText(lettersText);
+ lettersText = mLetters.getText();
+ Selection.setSelection(lettersText, lettersText.length());
+
+ mDigits.setKeyListener(digitsInput);
+ mDigits.setText(digitsText);
+ digitsText = mDigits.getText();
+ Selection.setSelection(digitsText, digitsText.length());
+
+ // Reset the filters
+ mPrimary.setFilters(mInputFilters);
+ mHint.setFilters(mInputFilters);
+ }
+
+
+ public CharSequence getLetters() {
+ if (mLetters.getVisibility() == View.VISIBLE) {
+ return mLetters.getText();
+ } else {
+ return "";
+ }
+ }
+
+ public CharSequence getDigits() {
+ if (mDigits.getVisibility() == View.VISIBLE) {
+ return mDigits.getText();
+ } else {
+ return "";
+ }
+ }
+
+ public CharSequence getFilterText() {
+ if (mMode != DIGITS_ONLY) {
+ return getLetters();
+ } else {
+ return getDigits();
+ }
+ }
+
+ public void append(String text) {
+ switch (mMode) {
+ case DIGITS_AND_LETTERS:
+ mDigits.getText().append(text);
+ mLetters.getText().append(text);
+ break;
+
+ case DIGITS_AND_LETTERS_NO_LETTERS:
+ case DIGITS_ONLY:
+ mDigits.getText().append(text);
+ break;
+
+ case DIGITS_AND_LETTERS_NO_DIGITS:
+ case LETTERS_ONLY:
+ mLetters.getText().append(text);
+ break;
+ }
+ }
+
+ /**
+ * Clears both the digits and the filter text.
+ */
+ public void clearText() {
+ Editable text;
+
+ text = mLetters.getText();
+ text.clear();
+
+ text = mDigits.getText();
+ text.clear();
+
+ // Reset the mode based on the hardware type
+ if (mIsQwerty) {
+ setMode(DIGITS_AND_LETTERS);
+ } else {
+ setMode(DIGITS_ONLY);
+ }
+ }
+
+ public void setLettersWatcher(TextWatcher watcher) {
+ CharSequence text = mLetters.getText();
+ Spannable span = (Spannable)text;
+ span.setSpan(watcher, 0, text.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+ }
+
+ public void setDigitsWatcher(TextWatcher watcher) {
+ CharSequence text = mDigits.getText();
+ Spannable span = (Spannable)text;
+ span.setSpan(watcher, 0, text.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+ }
+
+ public void setFilterWatcher(TextWatcher watcher) {
+ if (mMode != DIGITS_ONLY) {
+ setLettersWatcher(watcher);
+ } else {
+ setDigitsWatcher(watcher);
+ }
+ }
+
+ public void removeFilterWatcher(TextWatcher watcher) {
+ Spannable text;
+ if (mMode != DIGITS_ONLY) {
+ text = mLetters.getText();
+ } else {
+ text = mDigits.getText();
+ }
+ text.removeSpan(watcher);
+ }
+
+ /**
+ * Called right after the mode changes to give subclasses the option to
+ * restyle, etc.
+ */
+ protected void onModeChange(int oldMode, int newMode) {
+ }
+
+ /** This mode has both lines */
+ public static final int DIGITS_AND_LETTERS = 1;
+ /** This mode is when after starting in {@link #DIGITS_AND_LETTERS} mode the filter
+ * has removed all possibility of the digits matching, leaving only the letters line */
+ public static final int DIGITS_AND_LETTERS_NO_DIGITS = 2;
+ /** This mode is when after starting in {@link #DIGITS_AND_LETTERS} mode the filter
+ * has removed all possibility of the letters matching, leaving only the digits line */
+ public static final int DIGITS_AND_LETTERS_NO_LETTERS = 3;
+ /** This mode has only the digits line */
+ public static final int DIGITS_ONLY = 4;
+ /** This mode has only the letters line */
+ public static final int LETTERS_ONLY = 5;
+
+ EditText mLetters;
+ EditText mDigits;
+ EditText mPrimary;
+ EditText mHint;
+ InputFilter mInputFilters[];
+ ImageView mIcon;
+ int mMode;
+ private boolean mIsQwerty;
+}
diff --git a/core/java/android/widget/DigitalClock.java b/core/java/android/widget/DigitalClock.java
new file mode 100644
index 0000000..3ca2c81
--- /dev/null
+++ b/core/java/android/widget/DigitalClock.java
@@ -0,0 +1,135 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.database.ContentObserver;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.pim.DateFormat;
+import android.provider.Settings;
+import android.util.AttributeSet;
+
+import java.util.Calendar;
+
+/**
+ * Like AnalogClock, but digital. Shows seconds.
+ *
+ * FIXME: implement separate views for hours/minutes/seconds, so
+ * proportional fonts don't shake rendering
+ */
+
+public class DigitalClock extends TextView {
+
+ Calendar mCalendar;
+ private final static String m12 = "h:mm:ss aa";
+ private final static String m24 = "k:mm:ss";
+ private FormatChangeObserver mFormatChangeObserver;
+
+ private Runnable mTicker;
+ private Handler mHandler;
+
+ private boolean mTickerStopped = false;
+
+ String mFormat;
+
+ public DigitalClock(Context context) {
+ super(context);
+ initClock(context);
+ }
+
+ public DigitalClock(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initClock(context);
+ }
+
+ private void initClock(Context context) {
+ Resources r = mContext.getResources();
+
+ if (mCalendar == null) {
+ mCalendar = Calendar.getInstance();
+ }
+
+ mFormatChangeObserver = new FormatChangeObserver();
+ getContext().getContentResolver().registerContentObserver(
+ Settings.System.CONTENT_URI, true, mFormatChangeObserver);
+
+ setFormat();
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ mTickerStopped = false;
+ super.onAttachedToWindow();
+ mHandler = new Handler();
+
+ /**
+ * requests a tick on the next hard-second boundary
+ */
+ mTicker = new Runnable() {
+ public void run() {
+ if (mTickerStopped) return;
+ mCalendar.setTimeInMillis(System.currentTimeMillis());
+ setText(DateFormat.format(mFormat, mCalendar));
+ invalidate();
+ long now = SystemClock.uptimeMillis();
+ long next = now + (1000 - now % 1000);
+ mHandler.postAtTime(mTicker, next);
+ }
+ };
+ mTicker.run();
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ mTickerStopped = true;
+ }
+
+ /**
+ * Pulls 12/24 mode from system settings
+ */
+ private boolean get24HourMode() {
+ String value = Settings.System.getString(
+ getContext().getContentResolver(),
+ Settings.System.TIME_12_24);
+
+ if (value == null || value.equals("12"))
+ return false;
+ return true;
+ }
+
+ private void setFormat() {
+ if (get24HourMode()) {
+ mFormat = m24;
+ } else {
+ mFormat = m12;
+ }
+ }
+
+ private class FormatChangeObserver extends ContentObserver {
+ public FormatChangeObserver() {
+ super(new Handler());
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ setFormat();
+ }
+ }
+}
diff --git a/core/java/android/widget/DoubleDigitManager.java b/core/java/android/widget/DoubleDigitManager.java
new file mode 100644
index 0000000..1eea1fb
--- /dev/null
+++ b/core/java/android/widget/DoubleDigitManager.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import android.os.Handler;
+
+/**
+ * Provides callbacks indicating the steps in two digit pressing within a
+ * timeout.
+ *
+ * Package private: only relevant in helping {@link TimeSpinnerHelper}.
+ */
+class DoubleDigitManager {
+
+ private final long timeoutInMillis;
+ private final CallBack mCallBack;
+
+ private Integer intermediateDigit;
+
+ /**
+ * @param timeoutInMillis How long after the first digit is pressed does
+ * the user have to press the second digit?
+ * @param callBack The callback to indicate what's going on with the user.
+ */
+ public DoubleDigitManager(long timeoutInMillis, CallBack callBack) {
+ this.timeoutInMillis = timeoutInMillis;
+ mCallBack = callBack;
+ }
+
+ /**
+ * Report to this manager that a digit was pressed.
+ * @param digit
+ */
+ public void reportDigit(int digit) {
+ if (intermediateDigit == null) {
+ intermediateDigit = digit;
+
+ new Handler().postDelayed(new Runnable() {
+ public void run() {
+ if (intermediateDigit != null) {
+ mCallBack.singleDigitFinal(intermediateDigit);
+ intermediateDigit = null;
+ }
+ }
+ }, timeoutInMillis);
+
+ if (!mCallBack.singleDigitIntermediate(digit)) {
+
+ // this wasn't a good candidate for the intermediate digit,
+ // make it the final digit (since there is no opportunity to
+ // reject the final digit).
+ intermediateDigit = null;
+ mCallBack.singleDigitFinal(digit);
+ }
+ } else if (mCallBack.twoDigitsFinal(intermediateDigit, digit)) {
+ intermediateDigit = null;
+ }
+ }
+
+ /**
+ * The callback to indicate what is going on with the digits pressed.
+ */
+ static interface CallBack {
+
+ /**
+ * A digit was pressed, and there are no intermediate digits.
+ * @param digit The digit pressed.
+ * @return Whether the digit was accepted; how the user of this manager
+ * tells us that the intermediate digit is acceptable as an
+ * intermediate digit.
+ */
+ boolean singleDigitIntermediate(int digit);
+
+ /**
+ * A single digit was pressed, and it is 'the final answer'.
+ * - a single digit pressed, and the timeout expires.
+ * - a single digit pressed, and {@link #singleDigitIntermediate}
+ * returned false.
+ * @param digit The digit.
+ */
+ void singleDigitFinal(int digit);
+
+ /**
+ * The user pressed digit1, then digit2 within the timeout.
+ * @param digit1
+ * @param digit2
+ */
+ boolean twoDigitsFinal(int digit1, int digit2);
+ }
+
+}
diff --git a/core/java/android/widget/EditText.java b/core/java/android/widget/EditText.java
new file mode 100644
index 0000000..e89a2bd
--- /dev/null
+++ b/core/java/android/widget/EditText.java
@@ -0,0 +1,102 @@
+/*
+ * 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.widget;
+
+import android.text.*;
+import android.text.method.*;
+import android.content.Context;
+import android.content.res.Resources;
+import android.util.AttributeSet;
+
+
+/*
+ * This is supposed to be a *very* thin veneer over TextView.
+ * Do not make any changes here that do anything that a TextView
+ * with a key listener and a movement method wouldn't do!
+ */
+
+/**
+ * EditText is a thin veneer over TextView that configures itself
+ * to be editable.
+ * <p>
+ * <b>XML attributes</b>
+ * <p>
+ * See {@link android.R.styleable#EditText EditText Attributes},
+ * {@link android.R.styleable#TextView TextView Attributes},
+ * {@link android.R.styleable#View View Attributes}
+ */
+public class EditText extends TextView {
+ public EditText(Context context) {
+ this(context, null);
+ }
+
+ public EditText(Context context, AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.editTextStyle);
+ }
+
+ public EditText(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ protected boolean getDefaultEditable() {
+ return true;
+ }
+
+ @Override
+ protected MovementMethod getDefaultMovementMethod() {
+ return ArrowKeyMovementMethod.getInstance();
+ }
+
+ @Override
+ public Editable getText() {
+ return (Editable) super.getText();
+ }
+
+ @Override
+ public void setText(CharSequence text, BufferType type) {
+ super.setText(text, BufferType.EDITABLE);
+ }
+
+ /**
+ * Convenience for {@link Selection#setSelection(Spannable, int, int)}.
+ */
+ public void setSelection(int start, int stop) {
+ Selection.setSelection(getText(), start, stop);
+ }
+
+ /**
+ * Convenience for {@link Selection#setSelection(Spannable, int)}.
+ */
+ public void setSelection(int index) {
+ Selection.setSelection(getText(), index);
+ }
+
+ /**
+ * Convenience for {@link Selection#selectAll}.
+ */
+ public void selectAll() {
+ Selection.selectAll(getText());
+ }
+
+ /**
+ * Convenience for {@link Selection#extendSelection}.
+ */
+ public void extendSelection(int index) {
+ Selection.extendSelection(getText(), index);
+ }
+}
diff --git a/core/java/android/widget/ExpandableListAdapter.java b/core/java/android/widget/ExpandableListAdapter.java
new file mode 100644
index 0000000..b75983c
--- /dev/null
+++ b/core/java/android/widget/ExpandableListAdapter.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import android.database.DataSetObserver;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * An adapter that links a {@link ExpandableListView} with the underlying
+ * data. The implementation of this interface will provide access
+ * to the data of the children (categorized by groups), and also instantiate
+ * {@link View}s for children and groups.
+ */
+public interface ExpandableListAdapter {
+ /**
+ * @see Adapter#registerDataSetObserver(DataSetObserver)
+ */
+ void registerDataSetObserver(DataSetObserver observer);
+
+ /**
+ * @see Adapter#unregisterDataSetObserver(DataSetObserver)
+ */
+ void unregisterDataSetObserver(DataSetObserver observer);
+
+ /**
+ * Gets the number of groups.
+ *
+ * @return the number of groups
+ */
+ int getGroupCount();
+
+ /**
+ * Gets the number of children in a specified group.
+ *
+ * @param groupPosition the position of the group for which the children
+ * count should be returned
+ * @return the children count in the specified group
+ */
+ int getChildrenCount(int groupPosition);
+
+ /**
+ * Gets the data associated with the given group.
+ *
+ * @param groupPosition the position of the group
+ * @return the data child for the specified group
+ */
+ Object getGroup(int groupPosition);
+
+ /**
+ * Gets the data associated with the given child within the given group.
+ *
+ * @param groupPosition the position of the group that the child resides in
+ * @param childPosition the position of the child with respect to other
+ * children in the group
+ * @return the data of the child
+ */
+ Object getChild(int groupPosition, int childPosition);
+
+ /**
+ * Gets the ID for the group at the given position. This group ID must be
+ * unique across groups. The combined ID (see
+ * {@link #getCombinedGroupId(long)}) must be unique across ALL items
+ * (groups and all children).
+ *
+ * @param groupPosition the position of the group for which the ID is wanted
+ * @return the ID associated with the group
+ */
+ long getGroupId(int groupPosition);
+
+ /**
+ * Gets the ID for the given child within the given group. This ID must be
+ * unique across all children within the group. The combined ID (see
+ * {@link #getCombinedChildId(long, long)}) must be unique across ALL items
+ * (groups and all children).
+ *
+ * @param groupPosition the position of the group that contains the child
+ * @param childPosition the position of the child within the group for which
+ * the ID is wanted
+ * @return the ID associated with the child
+ */
+ long getChildId(int groupPosition, int childPosition);
+
+ /**
+ * Indicates whether the child and group IDs are stable across changes to the
+ * underlying data.
+ *
+ * @return whether or not the same ID always refers to the same object
+ * @see Adapter#hasStableIds()
+ */
+ boolean hasStableIds();
+
+ /**
+ * Gets a View that displays the given group. This View is only for the
+ * group--the Views for the group's children will be fetched using
+ * getChildrenView.
+ *
+ * @param groupPosition the position of the group for which the View is
+ * returned
+ * @param isExpanded whether the group is expanded or collapsed
+ * @param convertView the old view to reuse, if possible. You should check
+ * that this view is non-null and of an appropriate type before
+ * using. If it is not possible to convert this view to display
+ * the correct data, this method can create a new view. It is not
+ * guaranteed that the convertView will have been previously
+ * created by
+ * {@link #getGroupView(int, boolean, View, ViewGroup)}.
+ * @param parent the parent that this view will eventually be attached to
+ * @return the View corresponding to the group at the specified position
+ */
+ View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent);
+
+ /**
+ * Gets a View that displays the data for the given child within the given
+ * group.
+ *
+ * @param groupPosition the position of the group that contains the child
+ * @param childPosition the position of the child (for which the View is
+ * returned) within the group
+ * @param isLastChild Whether the child is the last child within the group
+ * @param convertView the old view to reuse, if possible. You should check
+ * that this view is non-null and of an appropriate type before
+ * using. If it is not possible to convert this view to display
+ * the correct data, this method can create a new view. It is not
+ * guaranteed that the convertView will have been previously
+ * created by
+ * {@link #getChildView(int, int, boolean, View, ViewGroup)}.
+ * @param parent the parent that this view will eventually be attached to
+ * @return the View corresponding to the child at the specified position
+ */
+ View getChildView(int groupPosition, int childPosition, boolean isLastChild,
+ View convertView, ViewGroup parent);
+
+ /**
+ * Whether the child at the specified position is selectable.
+ *
+ * @param groupPosition the position of the group that contains the child
+ * @param childPosition the position of the child within the group
+ * @return whether the child is selectable.
+ */
+ boolean isChildSelectable(int groupPosition, int childPosition);
+
+ /**
+ * @see ListAdapter#areAllItemsEnabled()
+ */
+ boolean areAllItemsEnabled();
+
+ /**
+ * @see ListAdapter#isEmpty()
+ */
+ boolean isEmpty();
+
+ /**
+ * Called when a group is expanded.
+ *
+ * @param groupPosition The group being expanded.
+ */
+ void onGroupExpanded(int groupPosition);
+
+ /**
+ * Called when a group is collapsed.
+ *
+ * @param groupPosition The group being collapsed.
+ */
+ void onGroupCollapsed(int groupPosition);
+
+ /**
+ * Gets an ID for a child that is unique across any item (either group or
+ * child) that is in this list. Expandable lists require each item (group or
+ * child) to have a unique ID among all children and groups in the list.
+ * This method is responsible for returning that unique ID given a child's
+ * ID and its group's ID. Furthermore, if {@link #hasStableIds()} is true, the
+ * returned ID must be stable as well.
+ *
+ * @param groupId The ID of the group that contains this child.
+ * @param childId The ID of the child.
+ * @return The unique (and possibly stable) ID of the child across all
+ * groups and children in this list.
+ */
+ long getCombinedChildId(long groupId, long childId);
+
+ /**
+ * Gets an ID for a group that is unique across any item (either group or
+ * child) that is in this list. Expandable lists require each item (group or
+ * child) to have a unique ID among all children and groups in the list.
+ * This method is responsible for returning that unique ID given a group's
+ * ID. Furthermore, if {@link #hasStableIds()} is true, the returned ID must be
+ * stable as well.
+ *
+ * @param groupId The ID of the group
+ * @return The unique (and possibly stable) ID of the group across all
+ * groups and children in this list.
+ */
+ long getCombinedGroupId(long groupId);
+}
diff --git a/core/java/android/widget/ExpandableListConnector.java b/core/java/android/widget/ExpandableListConnector.java
new file mode 100644
index 0000000..ddedea3
--- /dev/null
+++ b/core/java/android/widget/ExpandableListConnector.java
@@ -0,0 +1,797 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import android.database.DataSetObserver;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.ArrayList;
+
+/*
+ * Implementation notes:
+ *
+ * <p>
+ * Terminology:
+ * <li> flPos - Flat list position, the position used by ListView
+ * <li> gPos - Group position, the position of a group among all the groups
+ * <li> cPos - Child position, the position of a child among all the children
+ * in a group
+ */
+
+/**
+ * A {@link BaseAdapter} that provides data/Views in an expandable list (offers
+ * features such as collapsing/expanding groups containing children). By
+ * itself, this adapter has no data and is a connector to a
+ * {@link ExpandableListAdapter} which provides the data.
+ * <p>
+ * Internally, this connector translates the flat list position that the
+ * ListAdapter expects to/from group and child positions that the ExpandableListAdapter
+ * expects.
+ */
+class ExpandableListConnector extends BaseAdapter implements Filterable {
+ /**
+ * The ExpandableListAdapter to fetch the data/Views for this expandable list
+ */
+ private ExpandableListAdapter mExpandableListAdapter;
+
+ /**
+ * List of metadata for the currently expanded groups. The metadata consists
+ * of data essential for efficiently translating between flat list positions
+ * and group/child positions. See {@link GroupMetadata}.
+ */
+ private ArrayList<GroupMetadata> mExpGroupMetadataList;
+
+ /** The number of children from all currently expanded groups */
+ private int mTotalExpChildrenCount;
+
+ /** The maximum number of allowable expanded groups. Defaults to 'no limit' */
+ private int mMaxExpGroupCount = Integer.MAX_VALUE;
+
+ /** Change observer used to have ExpandableListAdapter changes pushed to us */
+ private DataSetObserver mDataSetObserver = new MyDataSetObserver();
+
+ /**
+ * Constructs the connector
+ */
+ public ExpandableListConnector(ExpandableListAdapter expandableListAdapter) {
+ mExpGroupMetadataList = new ArrayList<GroupMetadata>();
+
+ setExpandableListAdapter(expandableListAdapter);
+ }
+
+ /**
+ * Point to the {@link ExpandableListAdapter} that will give us data/Views
+ *
+ * @param expandableListAdapter the adapter that supplies us with data/Views
+ */
+ public void setExpandableListAdapter(ExpandableListAdapter expandableListAdapter) {
+ if (mExpandableListAdapter != null) {
+ mExpandableListAdapter.unregisterDataSetObserver(mDataSetObserver);
+ }
+
+ mExpandableListAdapter = expandableListAdapter;
+ expandableListAdapter.registerDataSetObserver(mDataSetObserver);
+ }
+
+ /**
+ * Translates a flat list position to either a) group pos if the specified
+ * flat list position corresponds to a group, or b) child pos if it
+ * corresponds to a child. Performs a binary search on the expanded
+ * groups list to find the flat list pos if it is an exp group, otherwise
+ * finds where the flat list pos fits in between the exp groups.
+ *
+ * @param flPos the flat list position to be translated
+ * @return the group position or child position of the specified flat list
+ * position encompassed in a {@link PositionMetadata} object
+ * that contains additional useful info for insertion, etc.
+ */
+ PositionMetadata getUnflattenedPos(final int flPos) {
+ /* Keep locally since frequent use */
+ final ArrayList<GroupMetadata> egml = mExpGroupMetadataList;
+ final int numExpGroups = egml.size();
+
+ /* Binary search variables */
+ int leftExpGroupIndex = 0;
+ int rightExpGroupIndex = numExpGroups - 1;
+ int midExpGroupIndex = 0;
+ GroupMetadata midExpGm;
+
+ if (numExpGroups == 0) {
+ /*
+ * There aren't any expanded groups (hence no visible children
+ * either), so flPos must be a group and its group pos will be the
+ * same as its flPos
+ */
+ return new PositionMetadata(flPos, ExpandableListPosition.GROUP, flPos,
+ -1, null, 0);
+ }
+
+ /*
+ * Binary search over the expanded groups to find either the exact
+ * expanded group (if we're looking for a group) or the group that
+ * contains the child we're looking for. If we are looking for a
+ * collapsed group, we will not have a direct match here, but we will
+ * find the expanded group just before the group we're searching for (so
+ * then we can calculate the group position of the group we're searching
+ * for). If there isn't an expanded group prior to the group being
+ * searched for, then the group being searched for's group position is
+ * the same as the flat list position (since there are no children before
+ * it, and all groups before it are collapsed).
+ */
+ while (leftExpGroupIndex <= rightExpGroupIndex) {
+ midExpGroupIndex =
+ (rightExpGroupIndex - leftExpGroupIndex) / 2
+ + leftExpGroupIndex;
+ midExpGm = egml.get(midExpGroupIndex);
+
+ if (flPos > midExpGm.lastChildFlPos) {
+ /*
+ * The flat list position is after the current middle group's
+ * last child's flat list position, so search right
+ */
+ leftExpGroupIndex = midExpGroupIndex + 1;
+ } else if (flPos < midExpGm.flPos) {
+ /*
+ * The flat list position is before the current middle group's
+ * flat list position, so search left
+ */
+ rightExpGroupIndex = midExpGroupIndex - 1;
+ } else if (flPos == midExpGm.flPos) {
+ /*
+ * The flat list position is this middle group's flat list
+ * position, so we've found an exact hit
+ */
+ return new PositionMetadata(flPos, ExpandableListPosition.GROUP,
+ midExpGm.gPos, -1, midExpGm, midExpGroupIndex);
+ } else if (flPos <= midExpGm.lastChildFlPos
+ /* && flPos > midGm.flPos as deduced from previous
+ * conditions */) {
+ /* The flat list position is a child of the middle group */
+
+ /*
+ * Subtract the first child's flat list position from the
+ * specified flat list pos to get the child's position within
+ * the group
+ */
+ final int childPos = flPos - (midExpGm.flPos + 1);
+ return new PositionMetadata(flPos, ExpandableListPosition.CHILD,
+ midExpGm.gPos, childPos, midExpGm, midExpGroupIndex);
+ }
+ }
+
+ /*
+ * If we've reached here, it means the flat list position must be a
+ * group that is not expanded, since otherwise we would have hit it
+ * in the above search.
+ */
+
+
+ /* If we are to expand this group later, where would it go in the
+ * mExpGroupMetadataList ? */
+ int insertPosition = 0;
+
+ /* What is its group position from the list of all groups? */
+ int groupPos = 0;
+
+ /*
+ * To figure out exact insertion and prior group positions, we need to
+ * determine how we broke out of the binary search. We backtrack
+ * to see this.
+ */
+ if (leftExpGroupIndex > midExpGroupIndex) {
+
+ /*
+ * This would occur in the first conditional, so the flat list
+ * insertion position is after the left group. Also, the
+ * leftGroupPos is one more than it should be (since that broke out
+ * of our binary search), so we decrement it.
+ */
+ final GroupMetadata leftExpGm = egml.get(leftExpGroupIndex-1);
+
+ insertPosition = leftExpGroupIndex;
+
+ /*
+ * Sums the number of groups between the prior exp group and this
+ * one, and then adds it to the prior group's group pos
+ */
+ groupPos =
+ (flPos - leftExpGm.lastChildFlPos) + leftExpGm.gPos;
+ } else if (rightExpGroupIndex < midExpGroupIndex) {
+
+ /*
+ * This would occur in the second conditional, so the flat list
+ * insertion position is before the right group. Also, the
+ * rightGroupPos is one less than it should be, so increment it.
+ */
+ final GroupMetadata rightExpGm = egml.get(++rightExpGroupIndex);
+
+ insertPosition = rightExpGroupIndex;
+
+ /*
+ * Subtracts this group's flat list pos from the group after's flat
+ * list position to find out how many groups are in between the two
+ * groups. Then, subtracts that number from the group after's group
+ * pos to get this group's pos.
+ */
+ groupPos = rightExpGm.gPos - (rightExpGm.flPos - flPos);
+ } else {
+ // TODO: clean exit
+ throw new RuntimeException("Unknown state");
+ }
+
+ return new PositionMetadata(flPos, ExpandableListPosition.GROUP, groupPos, -1,
+ null, insertPosition);
+ }
+
+ /**
+ * Translates either a group pos or a child pos (+ group it belongs to) to a
+ * flat list position. If searching for a child and its group is not expanded, this will
+ * return null since the child isn't being shown in the ListView, and hence it has no
+ * position.
+ *
+ * @param pos a {@link ExpandableListPosition} representing either a group position
+ * or child position
+ * @return the flat list position encompassed in a {@link PositionMetadata}
+ * object that contains additional useful info for insertion, etc.
+ */
+ PositionMetadata getFlattenedPos(final ExpandableListPosition pos) {
+ final ArrayList<GroupMetadata> egml = mExpGroupMetadataList;
+ final int numExpGroups = egml.size();
+
+ /* Binary search variables */
+ int leftExpGroupIndex = 0;
+ int rightExpGroupIndex = numExpGroups - 1;
+ int midExpGroupIndex = 0;
+ GroupMetadata midExpGm;
+
+ if (numExpGroups == 0) {
+ /*
+ * There aren't any expanded groups, so flPos must be a group and
+ * its flPos will be the same as its group pos. The
+ * insert position is 0 (since the list is empty).
+ */
+ return new PositionMetadata(pos.groupPos, pos.type,
+ pos.groupPos, pos.childPos, null, 0);
+ }
+
+ /*
+ * Binary search over the expanded groups to find either the exact
+ * expanded group (if we're looking for a group) or the group that
+ * contains the child we're looking for.
+ */
+ while (leftExpGroupIndex <= rightExpGroupIndex) {
+ midExpGroupIndex = (rightExpGroupIndex - leftExpGroupIndex)/2 + leftExpGroupIndex;
+ midExpGm = egml.get(midExpGroupIndex);
+
+ if (pos.groupPos > midExpGm.gPos) {
+ /*
+ * It's after the current middle group, so search right
+ */
+ leftExpGroupIndex = midExpGroupIndex + 1;
+ } else if (pos.groupPos < midExpGm.gPos) {
+ /*
+ * It's before the current middle group, so search left
+ */
+ rightExpGroupIndex = midExpGroupIndex - 1;
+ } else if (pos.groupPos == midExpGm.gPos) {
+ /*
+ * It's this middle group, exact hit
+ */
+
+ if (pos.type == ExpandableListPosition.GROUP) {
+ /* If it's a group, give them this matched group's flPos */
+ return new PositionMetadata(midExpGm.flPos, pos.type,
+ pos.groupPos, pos.childPos, midExpGm, midExpGroupIndex);
+ } else if (pos.type == ExpandableListPosition.CHILD) {
+ /* If it's a child, calculate the flat list pos */
+ return new PositionMetadata(midExpGm.flPos + pos.childPos
+ + 1, pos.type, pos.groupPos, pos.childPos,
+ midExpGm, midExpGroupIndex);
+ } else {
+ return null;
+ }
+ }
+ }
+
+ /*
+ * If we've reached here, it means there was no match in the expanded
+ * groups, so it must be a collapsed group that they're search for
+ */
+ if (pos.type != ExpandableListPosition.GROUP) {
+ /* If it isn't a group, return null */
+ return null;
+ }
+
+ /*
+ * To figure out exact insertion and prior group positions, we need to
+ * determine how we broke out of the binary search. We backtrack to see
+ * this.
+ */
+ if (leftExpGroupIndex > midExpGroupIndex) {
+
+ /*
+ * This would occur in the first conditional, so the flat list
+ * insertion position is after the left group.
+ *
+ * The leftGroupPos is one more than it should be (from the binary
+ * search loop) so we subtract 1 to get the actual left group. Since
+ * the insertion point is AFTER the left group, we keep this +1
+ * value as the insertion point
+ */
+ final GroupMetadata leftExpGm = egml.get(leftExpGroupIndex-1);
+ final int flPos =
+ leftExpGm.lastChildFlPos
+ + (pos.groupPos - leftExpGm.gPos);
+
+ return new PositionMetadata(flPos, pos.type, pos.groupPos,
+ pos.childPos, null, leftExpGroupIndex);
+ } else if (rightExpGroupIndex < midExpGroupIndex) {
+
+ /*
+ * This would occur in the second conditional, so the flat list
+ * insertion position is before the right group. Also, the
+ * rightGroupPos is one less than it should be (from binary search
+ * loop), so we increment to it.
+ */
+ final GroupMetadata rightExpGm = egml.get(++rightExpGroupIndex);
+ final int flPos =
+ rightExpGm.flPos
+ - (rightExpGm.gPos - pos.groupPos);
+ return new PositionMetadata(flPos, pos.type, pos.groupPos,
+ pos.childPos, null, rightExpGroupIndex);
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public boolean areAllItemsEnabled() {
+ return mExpandableListAdapter.areAllItemsEnabled();
+ }
+
+ @Override
+ public boolean isEnabled(int flatListPos) {
+ final ExpandableListPosition pos = getUnflattenedPos(flatListPos).position;
+
+ if (pos.type == ExpandableListPosition.CHILD) {
+ return mExpandableListAdapter.isChildSelectable(pos.groupPos, pos.childPos);
+ } else {
+ // Groups are always selectable
+ return true;
+ }
+ }
+
+ public int getCount() {
+ /*
+ * Total count for the list view is the number groups plus the
+ * number of children from currently expanded groups (a value we keep
+ * cached in this class)
+ */
+ return mExpandableListAdapter.getGroupCount() + mTotalExpChildrenCount;
+ }
+
+ public Object getItem(int flatListPos) {
+ final PositionMetadata posMetadata = getUnflattenedPos(flatListPos);
+
+ if (posMetadata.position.type == ExpandableListPosition.GROUP) {
+ return mExpandableListAdapter
+ .getGroup(posMetadata.position.groupPos);
+ } else if (posMetadata.position.type == ExpandableListPosition.CHILD) {
+ return mExpandableListAdapter.getChild(posMetadata.position.groupPos,
+ posMetadata.position.childPos);
+ } else {
+ // TODO: clean exit
+ throw new RuntimeException("Flat list position is of unknown type");
+ }
+ }
+
+ public long getItemId(int flatListPos) {
+ final PositionMetadata posMetadata = getUnflattenedPos(flatListPos);
+ final long groupId = mExpandableListAdapter.getGroupId(posMetadata.position.groupPos);
+
+ if (posMetadata.position.type == ExpandableListPosition.GROUP) {
+ return mExpandableListAdapter.getCombinedGroupId(groupId);
+ } else if (posMetadata.position.type == ExpandableListPosition.CHILD) {
+ final long childId = mExpandableListAdapter.getChildId(posMetadata.position.groupPos,
+ posMetadata.position.childPos);
+ return mExpandableListAdapter.getCombinedChildId(groupId, childId);
+ } else {
+ // TODO: clean exit
+ throw new RuntimeException("Flat list position is of unknown type");
+ }
+ }
+
+ public View getView(int flatListPos, View convertView, ViewGroup parent) {
+ final PositionMetadata posMetadata = getUnflattenedPos(flatListPos);
+
+ if (posMetadata.position.type == ExpandableListPosition.GROUP) {
+ return mExpandableListAdapter.getGroupView(posMetadata.position.groupPos, posMetadata
+ .isExpanded(), convertView, parent);
+ } else if (posMetadata.position.type == ExpandableListPosition.CHILD) {
+ final boolean isLastChild = posMetadata.groupMetadata.lastChildFlPos == flatListPos;
+
+ final View view = mExpandableListAdapter.getChildView(posMetadata.position.groupPos,
+ posMetadata.position.childPos, isLastChild, convertView, parent);
+
+ return view;
+ } else {
+ // TODO: clean exit
+ throw new RuntimeException("Flat list position is of unknown type");
+ }
+ }
+
+ @Override
+ public int getItemViewType(int flatListPos) {
+ final ExpandableListPosition pos = getUnflattenedPos(flatListPos).position;
+
+ if (pos.type == ExpandableListPosition.GROUP) {
+ return 0;
+ } else {
+ return 1;
+ }
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return 2;
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return mExpandableListAdapter.hasStableIds();
+ }
+
+ /**
+ * Traverses the expanded group metadata list and fills in the flat list
+ * positions.
+ *
+ * @param forceChildrenCountRefresh Forces refreshing of the children count
+ * for all expanded groups.
+ */
+ private void refreshExpGroupMetadataList(boolean forceChildrenCountRefresh) {
+ final ArrayList<GroupMetadata> egml = mExpGroupMetadataList;
+ final int egmlSize = egml.size();
+ int curFlPos = 0;
+
+ /* Update child count as we go through */
+ mTotalExpChildrenCount = 0;
+
+ GroupMetadata curGm;
+ int gChildrenCount;
+ int lastGPos = 0;
+ for (int i = 0; i < egmlSize; i++) {
+ /* Store in local variable since we'll access freq */
+ curGm = egml.get(i);
+
+ /*
+ * Get the number of children, try to refrain from calling
+ * another class's method unless we have to (so do a subtraction)
+ */
+ if ((curGm.lastChildFlPos == GroupMetadata.REFRESH) || forceChildrenCountRefresh) {
+ gChildrenCount = mExpandableListAdapter.getChildrenCount(curGm.gPos);
+ } else {
+ /* Num children for this group is its last child's fl pos minus
+ * the group's fl pos
+ */
+ gChildrenCount = curGm.lastChildFlPos - curGm.flPos;
+ }
+
+ /* Update */
+ mTotalExpChildrenCount += gChildrenCount;
+
+ /*
+ * This skips the collapsed groups and increments the flat list
+ * position (for subsequent exp groups) by accounting for the collapsed
+ * groups
+ */
+ curFlPos += (curGm.gPos - lastGPos);
+ lastGPos = curGm.gPos;
+
+ /* Update the flat list positions, and the current flat list pos */
+ curGm.flPos = curFlPos;
+ curFlPos += gChildrenCount;
+ curGm.lastChildFlPos = curFlPos;
+ }
+ }
+
+ /**
+ * Collapse a group in the grouped list view
+ *
+ * @param groupPos position of the group to collapse
+ */
+ boolean collapseGroup(int groupPos) {
+ return collapseGroup(getFlattenedPos(new ExpandableListPosition(ExpandableListPosition.GROUP,
+ groupPos, -1, -1)));
+ }
+
+ boolean collapseGroup(PositionMetadata posMetadata) {
+ /*
+ * Collapsing requires removal from mExpGroupMetadataList
+ */
+
+ /*
+ * If it is null, it must be already collapsed. This group metadata
+ * object should have been set from the search that returned the
+ * position metadata object.
+ */
+ if (posMetadata.groupMetadata == null) return false;
+
+ // Remove the group from the list of expanded groups
+ mExpGroupMetadataList.remove(posMetadata.groupMetadata);
+
+ // Refresh the metadata
+ refreshExpGroupMetadataList(false);
+
+ // Notify of change
+ notifyDataSetChanged();
+
+ // Give the callback
+ mExpandableListAdapter.onGroupCollapsed(posMetadata.groupMetadata.gPos);
+
+ return true;
+ }
+
+ /**
+ * Expand a group in the grouped list view
+ * @param groupPos the group to be expanded
+ */
+ boolean expandGroup(int groupPos) {
+ return expandGroup(getFlattenedPos(new ExpandableListPosition(ExpandableListPosition.GROUP,
+ groupPos, -1, -1)));
+ }
+
+ boolean expandGroup(PositionMetadata posMetadata) {
+ /*
+ * Expanding requires insertion into the mExpGroupMetadataList
+ */
+
+ if (posMetadata.position.groupPos < 0) {
+ // TODO clean exit
+ throw new RuntimeException("Need group");
+ }
+
+ if (mMaxExpGroupCount == 0) return false;
+
+ // Check to see if it's already expanded
+ if (posMetadata.groupMetadata != null) return false;
+
+ /* Restrict number of exp groups to mMaxExpGroupCount */
+ if (mExpGroupMetadataList.size() >= mMaxExpGroupCount) {
+ /* Collapse a group */
+ // TODO: Collapse something not on the screen instead of the first one?
+ // TODO: Could write overloaded function to take GroupMetadata to collapse
+ GroupMetadata collapsedGm = mExpGroupMetadataList.get(0);
+
+ int collapsedIndex = mExpGroupMetadataList.indexOf(collapsedGm);
+
+ collapseGroup(collapsedGm.gPos);
+
+ /* Decrement index if it is after the group we removed */
+ if (posMetadata.groupInsertIndex > collapsedIndex) {
+ posMetadata.groupInsertIndex--;
+ }
+ }
+
+ GroupMetadata expandedGm = new GroupMetadata();
+
+ expandedGm.gPos = posMetadata.position.groupPos;
+ expandedGm.flPos = GroupMetadata.REFRESH;
+ expandedGm.lastChildFlPos = GroupMetadata.REFRESH;
+
+ mExpGroupMetadataList.add(posMetadata.groupInsertIndex, expandedGm);
+
+ // Refresh the metadata
+ refreshExpGroupMetadataList(false);
+
+ // Notify of change
+ notifyDataSetChanged();
+
+ // Give the callback
+ mExpandableListAdapter.onGroupExpanded(expandedGm.gPos);
+
+ return true;
+ }
+
+ /**
+ * Whether the given group is currently expanded.
+ * @param groupPosition The group to check.
+ * @return Whether the group is currently expanded.
+ */
+ public boolean isGroupExpanded(int groupPosition) {
+ GroupMetadata groupMetadata;
+ for (int i = mExpGroupMetadataList.size() - 1; i >= 0; i--) {
+ groupMetadata = mExpGroupMetadataList.get(i);
+
+ if (groupMetadata.gPos == groupPosition) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Set the maximum number of groups that can be expanded at any given time
+ */
+ public void setMaxExpGroupCount(int maxExpGroupCount) {
+ mMaxExpGroupCount = maxExpGroupCount;
+ }
+
+ ExpandableListAdapter getAdapter() {
+ return mExpandableListAdapter;
+ }
+
+ public Filter getFilter() {
+ ExpandableListAdapter adapter = getAdapter();
+ if (adapter instanceof Filterable) {
+ return ((Filterable) adapter).getFilter();
+ } else {
+ return null;
+ }
+ }
+
+ ArrayList<GroupMetadata> getExpandedGroupMetadataList() {
+ return mExpGroupMetadataList;
+ }
+
+ void setExpandedGroupMetadataList(ArrayList<GroupMetadata> expandedGroupMetadataList) {
+
+ if ((expandedGroupMetadataList == null) || (mExpandableListAdapter == null)) {
+ return;
+ }
+
+ // Make sure our current data set is big enough for the previously
+ // expanded groups, if not, ignore this request
+ int numGroups = mExpandableListAdapter.getGroupCount();
+ for (int i = expandedGroupMetadataList.size() - 1; i >= 0; i--) {
+ if (expandedGroupMetadataList.get(i).gPos >= numGroups) {
+ // Doh, for some reason the client doesn't have some of the groups
+ return;
+ }
+ }
+
+ mExpGroupMetadataList = expandedGroupMetadataList;
+ refreshExpGroupMetadataList(true);
+ }
+
+ @Override
+ public boolean isEmpty() {
+ ExpandableListAdapter adapter = getAdapter();
+ return adapter != null ? adapter.isEmpty() : true;
+ }
+
+ protected class MyDataSetObserver extends DataSetObserver {
+ @Override
+ public void onChanged() {
+ refreshExpGroupMetadataList(true);
+
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public void onInvalidated() {
+ refreshExpGroupMetadataList(true);
+
+ notifyDataSetInvalidated();
+ }
+ }
+
+ /**
+ * Metadata about an expanded group to help convert from a flat list
+ * position to either a) group position for groups, or b) child position for
+ * children
+ */
+ static class GroupMetadata implements Parcelable {
+ final static int REFRESH = -1;
+
+ /** This group's flat list position */
+ int flPos;
+
+ /* firstChildFlPos isn't needed since it's (flPos + 1) */
+
+ /**
+ * This group's last child's flat list position, so basically
+ * the range of this group in the flat list
+ */
+ int lastChildFlPos;
+
+ /**
+ * This group's group position
+ */
+ int gPos;
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(flPos);
+ dest.writeInt(lastChildFlPos);
+ dest.writeInt(gPos);
+ }
+
+ public static final Parcelable.Creator<GroupMetadata> CREATOR =
+ new Parcelable.Creator<GroupMetadata>() {
+
+ public GroupMetadata createFromParcel(Parcel in) {
+ GroupMetadata gm = new GroupMetadata();
+ gm.flPos = in.readInt();
+ gm.lastChildFlPos = in.readInt();
+ gm.gPos = in.readInt();
+ return gm;
+ }
+
+ public GroupMetadata[] newArray(int size) {
+ return new GroupMetadata[size];
+ }
+ };
+
+ }
+
+ /**
+ * Data type that contains an expandable list position (can refer to either a group
+ * or child) and some extra information regarding referred item (such as
+ * where to insert into the flat list, etc.)
+ */
+ static public class PositionMetadata {
+ /** Data type to hold the position and its type (child/group) */
+ public ExpandableListPosition position;
+
+ /**
+ * Link back to the expanded GroupMetadata for this group. Useful for
+ * removing the group from the list of expanded groups inside the
+ * connector when we collapse the group, and also as a check to see if
+ * the group was expanded or collapsed (this will be null if the group
+ * is collapsed since we don't keep that group's metadata)
+ */
+ public GroupMetadata groupMetadata;
+
+ /**
+ * For groups that are collapsed, we use this as the index (in
+ * mExpGroupMetadataList) to insert this group when we are expanding
+ * this group.
+ */
+ public int groupInsertIndex;
+
+ public PositionMetadata(int flatListPos, int type, int groupPos,
+ int childPos) {
+ position = new ExpandableListPosition(type, groupPos, childPos, flatListPos);
+ }
+
+ protected PositionMetadata(int flatListPos, int type, int groupPos,
+ int childPos, GroupMetadata groupMetadata, int groupInsertIndex) {
+ position = new ExpandableListPosition(type, groupPos, childPos, flatListPos);
+
+ this.groupMetadata = groupMetadata;
+ this.groupInsertIndex = groupInsertIndex;
+ }
+
+ /**
+ * Checks whether the group referred to in this object is expanded,
+ * or not (at the time this object was created)
+ *
+ * @return whether the group at groupPos is expanded or not
+ */
+ public boolean isExpanded() {
+ return groupMetadata != null;
+ }
+ }
+}
diff --git a/core/java/android/widget/ExpandableListPosition.java b/core/java/android/widget/ExpandableListPosition.java
new file mode 100644
index 0000000..71e970c
--- /dev/null
+++ b/core/java/android/widget/ExpandableListPosition.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+/**
+ * ExpandableListPosition can refer to either a group's position or a child's
+ * position. Referring to a child's position requires both a group position (the
+ * group containing the child) and a child position (the child's position within
+ * that group). To create objects, use {@link #obtainChildPosition(int, int)} or
+ * {@link #obtainGroupPosition(int)}.
+ */
+class ExpandableListPosition {
+ /**
+ * This data type represents a child position
+ */
+ public final static int CHILD = 1;
+
+ /**
+ * This data type represents a group position
+ */
+ public final static int GROUP = 2;
+
+ /**
+ * The position of either the group being referred to, or the parent
+ * group of the child being referred to
+ */
+ public int groupPos;
+
+ /**
+ * The position of the child within its parent group
+ */
+ public int childPos;
+
+ /**
+ * The position of the item in the flat list (optional, used internally when
+ * the corresponding flat list position for the group or child is known)
+ */
+ int flatListPos;
+
+ /**
+ * What type of position this ExpandableListPosition represents
+ */
+ public int type;
+
+ ExpandableListPosition(int type, int groupPos, int childPos, int flatListPos) {
+ this.type = type;
+ this.flatListPos = flatListPos;
+ this.groupPos = groupPos;
+ this.childPos = childPos;
+ }
+
+ /**
+ * Used internally by the {@link #obtainChildPosition} and
+ * {@link #obtainGroupPosition} methods to construct a new object.
+ */
+ private ExpandableListPosition(int type, int groupPos, int childPos) {
+ this.type = type;
+ this.groupPos = groupPos;
+ this.childPos = childPos;
+ }
+
+ long getPackedPosition() {
+ if (type == CHILD) return ExpandableListView.getPackedPositionForChild(groupPos, childPos);
+ else return ExpandableListView.getPackedPositionForGroup(groupPos);
+ }
+
+ static ExpandableListPosition obtainGroupPosition(int groupPosition) {
+ return new ExpandableListPosition(GROUP, groupPosition, 0);
+ }
+
+ static ExpandableListPosition obtainChildPosition(int groupPosition, int childPosition) {
+ return new ExpandableListPosition(CHILD, groupPosition, childPosition);
+ }
+
+ static ExpandableListPosition obtainPosition(long packedPosition) {
+ if (packedPosition == ExpandableListView.PACKED_POSITION_VALUE_NULL) {
+ return null;
+ }
+
+ final int type = ExpandableListView.getPackedPositionType(packedPosition) ==
+ ExpandableListView.PACKED_POSITION_TYPE_CHILD ? CHILD : GROUP;
+
+ return new ExpandableListPosition(type, ExpandableListView
+ .getPackedPositionGroup(packedPosition), ExpandableListView
+ .getPackedPositionChild(packedPosition));
+ }
+
+}
diff --git a/core/java/android/widget/ExpandableListView.java b/core/java/android/widget/ExpandableListView.java
new file mode 100644
index 0000000..138cace
--- /dev/null
+++ b/core/java/android/widget/ExpandableListView.java
@@ -0,0 +1,1057 @@
+/*
+ * 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.widget;
+
+import com.android.internal.R;
+
+import java.util.ArrayList;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.view.ContextMenu;
+import android.view.SoundEffectConstants;
+import android.view.View;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.widget.ExpandableListConnector.PositionMetadata;
+
+/**
+ * A view that shows items in a vertically scrolling two-level list. This
+ * differs from the {@link ListView} by allowing two levels: groups which can
+ * individually be expanded to show its children. The items come from the
+ * {@link ExpandableListAdapter} associated with this view.
+ * <p>
+ * Expandable lists are able to show an indicator beside each item to display
+ * the item's current state (the states are usually one of expanded group,
+ * collapsed group, child, or last child). Use
+ * {@link #setChildIndicator(Drawable)} or {@link #setGroupIndicator(Drawable)}
+ * (or the corresponding XML attributes) to set these indicators (see the docs
+ * for each method to see additional state that each Drawable can have). The
+ * default style for an {@link ExpandableListView} provides indicators which
+ * will be shown next to Views given to the {@link ExpandableListView}. The
+ * layouts android.R.layout.simple_expandable_list_item_1 and
+ * android.R.layout.simple_expandable_list_item_2 (which should be used with
+ * {@link SimpleCursorTreeAdapter}) contain the preferred position information
+ * for indicators.
+ * <p>
+ * The context menu information set by an {@link ExpandableListView} will be a
+ * {@link ExpandableListContextMenuInfo} object with
+ * {@link ExpandableListContextMenuInfo#packedPosition} being a packed position
+ * that can be used with {@link #getPackedPositionType(long)} and the other
+ * similar methods.
+ * <p>
+ * <em><b>Note:</b></em> You cannot use the value <code>wrap_content</code>
+ * for the <code>android:layout_height</code> attribute of a
+ * ExpandableListView in XML if the parent's size is also not strictly specified
+ * (for example, if the parent were ScrollView you could not specify
+ * wrap_content since it also can be any length. However, you can use
+ * wrap_content if the ExpandableListView parent has a specific size, such as
+ * 100 pixels.
+ *
+ * @attr ref android.R.styleable#ExpandableListView_groupIndicator
+ * @attr ref android.R.styleable#ExpandableListView_indicatorLeft
+ * @attr ref android.R.styleable#ExpandableListView_indicatorRight
+ * @attr ref android.R.styleable#ExpandableListView_childIndicator
+ * @attr ref android.R.styleable#ExpandableListView_childIndicatorLeft
+ * @attr ref android.R.styleable#ExpandableListView_childIndicatorRight
+ * @attr ref android.R.styleable#ExpandableListView_childDivider
+ */
+public class ExpandableListView extends ListView {
+
+ /**
+ * The packed position represents a group.
+ */
+ public static final int PACKED_POSITION_TYPE_GROUP = 0;
+
+ /**
+ * The packed position represents a child.
+ */
+ public static final int PACKED_POSITION_TYPE_CHILD = 1;
+
+ /**
+ * The packed position represents a neither/null/no preference.
+ */
+ public static final int PACKED_POSITION_TYPE_NULL = 2;
+
+ /**
+ * The value for a packed position that represents neither/null/no
+ * preference. This value is not otherwise possible since a group type
+ * (first bit 0) should not have a child position filled.
+ */
+ public static final long PACKED_POSITION_VALUE_NULL = 0x00000000FFFFFFFFL;
+
+ /** The mask (in packed position representation) for the child */
+ private static final long PACKED_POSITION_MASK_CHILD = 0x00000000FFFFFFFFL;
+
+ /** The mask (in packed position representation) for the group */
+ private static final long PACKED_POSITION_MASK_GROUP = 0x7FFFFFFF00000000L;
+
+ /** The mask (in packed position representation) for the type */
+ private static final long PACKED_POSITION_MASK_TYPE = 0x8000000000000000L;
+
+ /** The shift amount (in packed position representation) for the group */
+ private static final long PACKED_POSITION_SHIFT_GROUP = 32;
+
+ /** The shift amount (in packed position representation) for the type */
+ private static final long PACKED_POSITION_SHIFT_TYPE = 63;
+
+ /** The mask (in integer child position representation) for the child */
+ private static final long PACKED_POSITION_INT_MASK_CHILD = 0xFFFFFFFF;
+
+ /** The mask (in integer group position representation) for the group */
+ private static final long PACKED_POSITION_INT_MASK_GROUP = 0x7FFFFFFF;
+
+ /** Serves as the glue/translator between a ListView and an ExpandableListView */
+ private ExpandableListConnector mConnector;
+
+ /** Gives us Views through group+child positions */
+ private ExpandableListAdapter mAdapter;
+
+ /** Left bound for drawing the indicator. */
+ private int mIndicatorLeft;
+
+ /** Right bound for drawing the indicator. */
+ private int mIndicatorRight;
+
+ /**
+ * Left bound for drawing the indicator of a child. Value of
+ * {@link #CHILD_INDICATOR_INHERIT} means use mIndicatorLeft.
+ */
+ private int mChildIndicatorLeft;
+
+ /**
+ * Right bound for drawing the indicator of a child. Value of
+ * {@link #CHILD_INDICATOR_INHERIT} means use mIndicatorRight.
+ */
+ private int mChildIndicatorRight;
+
+ /**
+ * Denotes when a child indicator should inherit this bound from the generic
+ * indicator bounds
+ */
+ public static final int CHILD_INDICATOR_INHERIT = -1;
+
+ /** The indicator drawn next to a group. */
+ private Drawable mGroupIndicator;
+
+ /** The indicator drawn next to a child. */
+ private Drawable mChildIndicator;
+
+ private static final int[] EMPTY_STATE_SET = {};
+
+ /** State indicating the group is expanded. */
+ private static final int[] GROUP_EXPANDED_STATE_SET =
+ {R.attr.state_expanded};
+
+ /** State indicating the group is empty (has no children). */
+ private static final int[] GROUP_EMPTY_STATE_SET =
+ {R.attr.state_empty};
+
+ /** State indicating the group is expanded and empty (has no children). */
+ private static final int[] GROUP_EXPANDED_EMPTY_STATE_SET =
+ {R.attr.state_expanded, R.attr.state_empty};
+
+ /** States for the group where the 0th bit is expanded and 1st bit is empty. */
+ private static final int[][] GROUP_STATE_SETS = {
+ EMPTY_STATE_SET, // 00
+ GROUP_EXPANDED_STATE_SET, // 01
+ GROUP_EMPTY_STATE_SET, // 10
+ GROUP_EXPANDED_EMPTY_STATE_SET // 11
+ };
+
+ /** State indicating the child is the last within its group. */
+ private static final int[] CHILD_LAST_STATE_SET =
+ {R.attr.state_last};
+
+ /** Drawable to be used as a divider when it is adjacent to any children */
+ private Drawable mChildDivider;
+
+ public ExpandableListView(Context context) {
+ this(context, null);
+ }
+
+ public ExpandableListView(Context context, AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.expandableListViewStyle);
+ }
+
+ public ExpandableListView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ TypedArray a =
+ context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.ExpandableListView, defStyle,
+ 0);
+
+ mGroupIndicator = a
+ .getDrawable(com.android.internal.R.styleable.ExpandableListView_groupIndicator);
+ mChildIndicator = a
+ .getDrawable(com.android.internal.R.styleable.ExpandableListView_childIndicator);
+ mIndicatorLeft = a
+ .getDimensionPixelSize(com.android.internal.R.styleable.ExpandableListView_indicatorLeft, 0);
+ mIndicatorRight = a
+ .getDimensionPixelSize(com.android.internal.R.styleable.ExpandableListView_indicatorRight, 0);
+ mChildIndicatorLeft = a.getDimensionPixelSize(
+ com.android.internal.R.styleable.ExpandableListView_childIndicatorLeft, CHILD_INDICATOR_INHERIT);
+ mChildIndicatorRight = a.getDimensionPixelSize(
+ com.android.internal.R.styleable.ExpandableListView_childIndicatorRight, CHILD_INDICATOR_INHERIT);
+ mChildDivider = a.getDrawable(com.android.internal.R.styleable.ExpandableListView_childDivider);
+
+ a.recycle();
+ }
+
+
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ // Draw children, etc.
+ super.dispatchDraw(canvas);
+
+ // If we have any indicators to draw, we do it here
+ if ((mChildIndicator == null) && (mGroupIndicator == null)) {
+ return;
+ }
+
+ int saveCount = 0;
+ final boolean clipToPadding = (mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK;
+ if (clipToPadding) {
+ saveCount = canvas.save();
+ final int scrollX = mScrollX;
+ final int scrollY = mScrollY;
+ canvas.clipRect(scrollX + mPaddingLeft, scrollY + mPaddingTop,
+ scrollX + mRight - mLeft - mPaddingRight,
+ scrollY + mBottom - mTop - mPaddingBottom);
+ }
+
+ final int headerViewsCount = getHeaderViewsCount();
+
+ final int lastChildFlPos = mItemCount - getFooterViewsCount() - headerViewsCount - 1;
+
+ final int myB = mBottom;
+
+ PositionMetadata pos;
+ View item;
+ Drawable indicator;
+ int t, b;
+
+ // Start at a value that is neither child nor group
+ int lastItemType = ~(ExpandableListPosition.CHILD | ExpandableListPosition.GROUP);
+
+ // Bounds of the indicator to be drawn
+ Rect indicatorRect = new Rect();
+
+ // The "child" mentioned in the following two lines is this
+ // View's child, not referring to an expandable list's
+ // notion of a child (as opposed to a group)
+ final int childCount = getChildCount();
+ for (int i = 0, childFlPos = mFirstPosition - headerViewsCount; i < childCount;
+ i++, childFlPos++) {
+
+ if (childFlPos < 0) {
+ // This child is header
+ continue;
+ } else if (childFlPos > lastChildFlPos) {
+ // This child is footer, so are all subsequent children
+ break;
+ }
+
+ item = getChildAt(i);
+ t = item.getTop();
+ b = item.getBottom();
+
+ // This item isn't on the screen
+ if ((b < 0) || (t > myB)) continue;
+
+ // Get more expandable list-related info for this item
+ pos = mConnector.getUnflattenedPos(childFlPos);
+
+ // If this item type and the previous item type are different, then we need to change
+ // the left & right bounds
+ if (pos.position.type != lastItemType) {
+ if (pos.position.type == ExpandableListPosition.CHILD) {
+ indicatorRect.left = (mChildIndicatorLeft == CHILD_INDICATOR_INHERIT) ?
+ mIndicatorLeft : mChildIndicatorLeft;
+ indicatorRect.right = (mChildIndicatorRight == CHILD_INDICATOR_INHERIT) ?
+ mIndicatorRight : mChildIndicatorRight;
+ } else {
+ indicatorRect.left = mIndicatorLeft;
+ indicatorRect.right = mIndicatorRight;
+ }
+
+ lastItemType = pos.position.type;
+ }
+
+ if (indicatorRect.left == indicatorRect.right) {
+ // The left and right bounds are the same, so nothing will be drawn
+ continue;
+ }
+
+ // Use item's full height + the divider height
+ if (mStackFromBottom) {
+ // See ListView#dispatchDraw
+ indicatorRect.top = t - mDividerHeight;
+ indicatorRect.bottom = b;
+ } else {
+ indicatorRect.top = t;
+ indicatorRect.bottom = b + mDividerHeight;
+ }
+
+ // Get the indicator (with its state set to the item's state)
+ indicator = getIndicator(pos);
+ if (indicator == null) continue;
+
+ // Draw the indicator
+ indicator.setBounds(indicatorRect);
+ indicator.draw(canvas);
+ }
+
+ if (clipToPadding) {
+ canvas.restoreToCount(saveCount);
+ }
+ }
+
+ /**
+ * Gets the indicator for the item at the given position. If the indicator
+ * is stateful, the state will be given to the indicator.
+ *
+ * @param pos The flat list position of the item whose indicator
+ * should be returned.
+ * @return The indicator in the proper state.
+ */
+ private Drawable getIndicator(PositionMetadata pos) {
+ Drawable indicator;
+
+ if (pos.position.type == ExpandableListPosition.GROUP) {
+ indicator = mGroupIndicator;
+
+ if (indicator != null && indicator.isStateful()) {
+ // Empty check based on availability of data. If the groupMetadata isn't null,
+ // we do a check on it. Otherwise, the group is collapsed so we consider it
+ // empty for performance reasons.
+ boolean isEmpty = (pos.groupMetadata == null) ||
+ (pos.groupMetadata.lastChildFlPos == pos.groupMetadata.flPos);
+
+ final int stateSetIndex =
+ (pos.isExpanded() ? 1 : 0) | // Expanded?
+ (isEmpty ? 2 : 0); // Empty?
+ indicator.setState(GROUP_STATE_SETS[stateSetIndex]);
+ }
+ } else {
+ indicator = mChildIndicator;
+
+ if (indicator != null && indicator.isStateful()) {
+ // No need for a state sets array for the child since it only has two states
+ final int stateSet[] = pos.position.flatListPos == pos.groupMetadata.lastChildFlPos
+ ? CHILD_LAST_STATE_SET
+ : EMPTY_STATE_SET;
+ indicator.setState(stateSet);
+ }
+ }
+
+ return indicator;
+ }
+
+ /**
+ * Sets the drawable that will be drawn adjacent to every child in the list. This will
+ * be drawn using the same height as the normal divider ({@link #setDivider(Drawable)}) or
+ * if it does not have an intrinsic height, the height set by {@link #setDividerHeight(int)}.
+ *
+ * @param childDivider The drawable to use.
+ */
+ public void setChildDivider(Drawable childDivider) {
+ mChildDivider = childDivider;
+ }
+
+ @Override
+ void drawDivider(Canvas canvas, Rect bounds, int childIndex) {
+ int flatListPosition = childIndex + mFirstPosition;
+
+ // Only proceed as possible child if the divider isn't above all items (if it is above
+ // all items, then the item below it has to be a group)
+ if (flatListPosition >= 0) {
+ PositionMetadata pos = mConnector.getUnflattenedPos(flatListPosition);
+ // If this item is a child, or it is a non-empty group that is expanded
+ if ((pos.position.type == ExpandableListPosition.CHILD)
+ || (pos.isExpanded() &&
+ pos.groupMetadata.lastChildFlPos != pos.groupMetadata.flPos)) {
+ // These are the cases where we draw the child divider
+ mChildDivider.setBounds(bounds);
+ mChildDivider.draw(canvas);
+ return;
+ }
+ }
+
+ // Otherwise draw the default divider
+ super.drawDivider(canvas, bounds, flatListPosition);
+ }
+
+ /**
+ * This overloaded method should not be used, instead use
+ * {@link #setAdapter(ExpandableListAdapter)}.
+ * <p>
+ * {@inheritDoc}
+ */
+ @Override
+ public void setAdapter(ListAdapter adapter) {
+ throw new RuntimeException(
+ "For ExpandableListView, use setAdapter(ExpandableListAdapter) instead of " +
+ "setAdapter(ListAdapter)");
+ }
+
+ /**
+ * This method should not be used, use {@link #getExpandableListAdapter()}.
+ */
+ @Override
+ public ListAdapter getAdapter() {
+ /*
+ * The developer should never really call this method on an
+ * ExpandableListView, so it would be nice to throw a RuntimeException,
+ * but AdapterView calls this
+ */
+ return super.getAdapter();
+ }
+
+ /**
+ * Register a callback to be invoked when an item has been clicked and the
+ * caller prefers to receive a ListView-style position instead of a group
+ * and/or child position. In most cases, the caller should use
+ * {@link #setOnGroupClickListener} and/or {@link #setOnChildClickListener}.
+ * <p />
+ * {@inheritDoc}
+ */
+ @Override
+ public void setOnItemClickListener(OnItemClickListener l) {
+ super.setOnItemClickListener(l);
+ }
+
+ /**
+ * Sets the adapter that provides data to this view.
+ * @param adapter The adapter that provides data to this view.
+ */
+ public void setAdapter(ExpandableListAdapter adapter) {
+ // Set member variable
+ mAdapter = adapter;
+
+ if (adapter != null) {
+ // Create the connector
+ mConnector = new ExpandableListConnector(adapter);
+ } else {
+ mConnector = null;
+ }
+
+ // Link the ListView (superclass) to the expandable list data through the connector
+ super.setAdapter(mConnector);
+ }
+
+ /**
+ * Gets the adapter that provides data to this view.
+ * @return The adapter that provides data to this view.
+ */
+ public ExpandableListAdapter getExpandableListAdapter() {
+ return mAdapter;
+ }
+
+ @Override
+ public boolean performItemClick(View v, int position, long id) {
+ // Ignore clicks in header/footers
+ final int headerViewsCount = getHeaderViewsCount();
+ final int footerViewsStart = mItemCount - getFooterViewsCount();
+
+ if (position < headerViewsCount || position >= footerViewsStart) {
+ // Clicked on a header/footer, so ignore pass it on to super
+ return super.performItemClick(v, position, id);
+ }
+
+ // Internally handle the item click
+ return handleItemClick(v, position - headerViewsCount, id);
+ }
+
+ /**
+ * This will either expand/collapse groups (if a group was clicked) or pass
+ * on the click to the proper child (if a child was clicked)
+ *
+ * @param position The flat list position. This has already been factored to
+ * remove the header/footer.
+ * @param id The ListAdapter ID, not the group or child ID.
+ */
+ boolean handleItemClick(View v, int position, long id) {
+ final PositionMetadata posMetadata = mConnector.getUnflattenedPos(position);
+
+ id = getChildOrGroupId(posMetadata.position);
+
+ if (posMetadata.position.type == ExpandableListPosition.GROUP) {
+ /* It's a group, so handle collapsing/expanding */
+
+ if (posMetadata.isExpanded()) {
+ /* Collapse it */
+ mConnector.collapseGroup(posMetadata);
+
+ playSoundEffect(SoundEffectConstants.CLICK);
+
+ if (mOnGroupCollapseListener != null) {
+ mOnGroupCollapseListener.onGroupCollapse(posMetadata.position.groupPos);
+ }
+
+ } else {
+ /* It's a group click, so pass on event */
+ if (mOnGroupClickListener != null) {
+ if (mOnGroupClickListener.onGroupClick(this, v,
+ posMetadata.position.groupPos, id)) {
+ return true;
+ }
+ }
+
+ /* Expand it */
+ mConnector.expandGroup(posMetadata);
+
+ playSoundEffect(SoundEffectConstants.CLICK);
+
+ if (mOnGroupExpandListener != null) {
+ mOnGroupExpandListener.onGroupExpand(posMetadata.position.groupPos);
+ }
+ }
+
+ return true;
+ } else {
+ /* It's a child, so pass on event */
+ if (mOnChildClickListener != null) {
+ playSoundEffect(SoundEffectConstants.CLICK);
+ return mOnChildClickListener.onChildClick(this, v, posMetadata.position.groupPos,
+ posMetadata.position.childPos, id);
+ }
+
+ return false;
+ }
+ }
+
+ /**
+ * Expand a group in the grouped list view
+ *
+ * @param groupPos the group to be expanded
+ * @return True if the group was expanded, false otherwise (if the group
+ * was already expanded, this will return false)
+ */
+ public boolean expandGroup(int groupPos) {
+ boolean retValue = mConnector.expandGroup(groupPos);
+
+ if (mOnGroupExpandListener != null) {
+ mOnGroupExpandListener.onGroupExpand(groupPos);
+ }
+
+ return retValue;
+ }
+
+ /**
+ * Collapse a group in the grouped list view
+ *
+ * @param groupPos position of the group to collapse
+ * @return True if the group was collapsed, false otherwise (if the group
+ * was already collapsed, this will return false)
+ */
+ public boolean collapseGroup(int groupPos) {
+ boolean retValue = mConnector.collapseGroup(groupPos);
+
+ if (mOnGroupCollapseListener != null) {
+ mOnGroupCollapseListener.onGroupCollapse(groupPos);
+ }
+
+ return retValue;
+ }
+
+ /** Used for being notified when a group is collapsed */
+ public interface OnGroupCollapseListener {
+ /**
+ * Callback method to be invoked when a group in this expandable list has
+ * been collapsed.
+ *
+ * @param groupPosition The group position that was collapsed
+ */
+ void onGroupCollapse(int groupPosition);
+ }
+
+ private OnGroupCollapseListener mOnGroupCollapseListener;
+
+ public void setOnGroupCollapseListener(
+ OnGroupCollapseListener onGroupCollapseListener) {
+ mOnGroupCollapseListener = onGroupCollapseListener;
+ }
+
+ /** Used for being notified when a group is expanded */
+ public interface OnGroupExpandListener {
+ /**
+ * Callback method to be invoked when a group in this expandable list has
+ * been expanded.
+ *
+ * @param groupPosition The group position that was expanded
+ */
+ void onGroupExpand(int groupPosition);
+ }
+
+ private OnGroupExpandListener mOnGroupExpandListener;
+
+ public void setOnGroupExpandListener(
+ OnGroupExpandListener onGroupExpandListener) {
+ mOnGroupExpandListener = onGroupExpandListener;
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when a group in this
+ * expandable list has been clicked.
+ */
+ public interface OnGroupClickListener {
+ /**
+ * Callback method to be invoked when a group in this expandable list has
+ * been clicked.
+ *
+ * @param parent The ExpandableListConnector where the click happened
+ * @param v The view within the expandable list/ListView that was clicked
+ * @param groupPosition The group position that was clicked
+ * @param id The row id of the group that was clicked
+ * @return True if the click was handled
+ */
+ boolean onGroupClick(ExpandableListView parent, View v, int groupPosition,
+ long id);
+ }
+
+ private OnGroupClickListener mOnGroupClickListener;
+
+ public void setOnGroupClickListener(OnGroupClickListener onGroupClickListener) {
+ mOnGroupClickListener = onGroupClickListener;
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when a child in this
+ * expandable list has been clicked.
+ */
+ public interface OnChildClickListener {
+ /**
+ * Callback method to be invoked when a child in this expandable list has
+ * been clicked.
+ *
+ * @param parent The ExpandableListView where the click happened
+ * @param v The view within the expandable list/ListView that was clicked
+ * @param groupPosition The group position that contains the child that
+ * was clicked
+ * @param childPosition The child position within the group
+ * @param id The row id of the child that was clicked
+ * @return True if the click was handled
+ */
+ boolean onChildClick(ExpandableListView parent, View v, int groupPosition,
+ int childPosition, long id);
+ }
+
+ private OnChildClickListener mOnChildClickListener;
+
+ public void setOnChildClickListener(OnChildClickListener onChildClickListener) {
+ mOnChildClickListener = onChildClickListener;
+ }
+
+ /**
+ * Converts a flat list position (the raw position of an item (child or
+ * group) in the list) to an group and/or child position (represented in a
+ * packed position). This is useful in situations where the caller needs to
+ * use the underlying {@link ListView}'s methods. Use
+ * {@link ExpandableListView#getPackedPositionType} ,
+ * {@link ExpandableListView#getPackedPositionChild},
+ * {@link ExpandableListView#getPackedPositionGroup} to unpack.
+ *
+ * @param flatListPosition The flat list position to be converted.
+ * @return The group and/or child position for the given flat list position
+ * in packed position representation.
+ */
+ public long getExpandableListPosition(int flatListPosition) {
+ return mConnector.getUnflattenedPos(flatListPosition).position.getPackedPosition();
+ }
+
+ /**
+ * Converts a group and/or child position to a flat list position. This is
+ * useful in situations where the caller needs to use the underlying
+ * {@link ListView}'s methods.
+ *
+ * @param packedPosition The group and/or child positions to be converted in
+ * packed position representation. Use
+ * {@link #getPackedPositionForChild(int, int)} or
+ * {@link #getPackedPositionForGroup(int)}.
+ * @return The flat list position for the given child or group.
+ */
+ public int getFlatListPosition(long packedPosition) {
+ return mConnector.getFlattenedPos(ExpandableListPosition.obtainPosition(packedPosition)).
+ position.flatListPos;
+ }
+
+ /**
+ * Gets the position of the currently selected group or child (along with
+ * its type). Can return {@link #PACKED_POSITION_VALUE_NULL} if no selection.
+ *
+ * @return A packed position containing the currently selected group or
+ * child's position and type. #PACKED_POSITION_VALUE_NULL if no selection.
+ */
+ public long getSelectedPosition() {
+ final int selectedPos = getSelectedItemPosition();
+ if (selectedPos == -1) return PACKED_POSITION_VALUE_NULL;
+
+ return getExpandableListPosition(selectedPos);
+ }
+
+ /**
+ * Gets the ID of the currently selected group or child. Can return -1 if no
+ * selection.
+ *
+ * @return The ID of the currently selected group or child. -1 if no
+ * selection.
+ */
+ public long getSelectedId() {
+ long packedPos = getSelectedPosition();
+ if (packedPos == PACKED_POSITION_VALUE_NULL) return -1;
+
+ int groupPos = getPackedPositionGroup(packedPos);
+
+ if (getPackedPositionType(packedPos) == PACKED_POSITION_TYPE_GROUP) {
+ // It's a group
+ return mAdapter.getGroupId(groupPos);
+ } else {
+ // It's a child
+ return mAdapter.getChildId(groupPos, getPackedPositionChild(packedPos));
+ }
+ }
+
+ /**
+ * Sets the selection to the specified group.
+ * @param groupPosition The position of the group that should be selected.
+ */
+ public void setSelectedGroup(int groupPosition) {
+ ExpandableListPosition elGroupPos = ExpandableListPosition
+ .obtainGroupPosition(groupPosition);
+ super.setSelection(mConnector.getFlattenedPos(elGroupPos).position.flatListPos);
+ }
+
+ /**
+ * Sets the selection to the specified child. If the child is in a collapsed
+ * group, the group will only be expanded and child subsequently selected if
+ * shouldExpandGroup is set to true, otherwise the method will return false.
+ *
+ * @param groupPosition The position of the group that contains the child.
+ * @param childPosition The position of the child within the group.
+ * @param shouldExpandGroup Whether the child's group should be expanded if
+ * it is collapsed.
+ * @return Whether the selection was successfully set on the child.
+ */
+ public boolean setSelectedChild(int groupPosition, int childPosition, boolean shouldExpandGroup) {
+ ExpandableListPosition elChildPos = ExpandableListPosition.obtainChildPosition(
+ groupPosition, childPosition);
+ PositionMetadata flatChildPos = mConnector.getFlattenedPos(elChildPos);
+
+ if (flatChildPos == null) {
+ // The child's group isn't expanded
+
+ // Shouldn't expand the group, so return false for we didn't set the selection
+ if (!shouldExpandGroup) return false;
+
+ expandGroup(groupPosition);
+
+ flatChildPos = mConnector.getFlattenedPos(elChildPos);
+
+ // Sanity check
+ if (flatChildPos == null) {
+ throw new IllegalStateException("Could not find child");
+ }
+ }
+
+ super.setSelection(flatChildPos.position.flatListPos);
+
+ return true;
+ }
+
+ /**
+ * Whether the given group is currently expanded.
+ *
+ * @param groupPosition The group to check.
+ * @return Whether the group is currently expanded.
+ */
+ public boolean isGroupExpanded(int groupPosition) {
+ return mConnector.isGroupExpanded(groupPosition);
+ }
+
+ /**
+ * Gets the type of a packed position. See
+ * {@link #getPackedPositionForChild(int, int)}.
+ *
+ * @param packedPosition The packed position for which to return the type.
+ * @return The type of the position contained within the packed position,
+ * either {@link #PACKED_POSITION_TYPE_CHILD}, {@link #PACKED_POSITION_TYPE_GROUP}, or
+ * {@link #PACKED_POSITION_TYPE_NULL}.
+ */
+ public static int getPackedPositionType(long packedPosition) {
+ if (packedPosition == PACKED_POSITION_VALUE_NULL) {
+ return PACKED_POSITION_TYPE_NULL;
+ }
+
+ return (packedPosition & PACKED_POSITION_MASK_TYPE) == PACKED_POSITION_MASK_TYPE
+ ? PACKED_POSITION_TYPE_CHILD
+ : PACKED_POSITION_TYPE_GROUP;
+ }
+
+ /**
+ * Gets the group position from a packed position. See
+ * {@link #getPackedPositionForChild(int, int)}.
+ *
+ * @param packedPosition The packed position from which the group position
+ * will be returned.
+ * @return The group position portion of the packed position. If this does
+ * not contain a group, returns -1.
+ */
+ public static int getPackedPositionGroup(long packedPosition) {
+ // Null
+ if (packedPosition == PACKED_POSITION_VALUE_NULL) return -1;
+
+ return (int) ((packedPosition & PACKED_POSITION_MASK_GROUP) >> PACKED_POSITION_SHIFT_GROUP);
+ }
+
+ /**
+ * Gets the child position from a packed position that is of
+ * {@link #PACKED_POSITION_TYPE_CHILD} type (use {@link #getPackedPositionType(long)}).
+ * To get the group that this child belongs to, use
+ * {@link #getPackedPositionGroup(long)}. See
+ * {@link #getPackedPositionForChild(int, int)}.
+ *
+ * @param packedPosition The packed position from which the child position
+ * will be returned.
+ * @return The child position portion of the packed position. If this does
+ * not contain a child, returns -1.
+ */
+ public static int getPackedPositionChild(long packedPosition) {
+ // Null
+ if (packedPosition == PACKED_POSITION_VALUE_NULL) return -1;
+
+ // Group since a group type clears this bit
+ if ((packedPosition & PACKED_POSITION_MASK_TYPE) != PACKED_POSITION_MASK_TYPE) return -1;
+
+ return (int) (packedPosition & PACKED_POSITION_MASK_CHILD);
+ }
+
+ /**
+ * Returns the packed position representation of a child's position.
+ * <p>
+ * In general, a packed position should be used in
+ * situations where the position given to/returned from an
+ * {@link ExpandableListAdapter} or {@link ExpandableListView} method can
+ * either be a child or group. The two positions are packed into a single
+ * long which can be unpacked using
+ * {@link #getPackedPositionChild(long)},
+ * {@link #getPackedPositionGroup(long)}, and
+ * {@link #getPackedPositionType(long)}.
+ *
+ * @param groupPosition The child's parent group's position.
+ * @param childPosition The child position within the group.
+ * @return The packed position representation of the child (and parent group).
+ */
+ public static long getPackedPositionForChild(int groupPosition, int childPosition) {
+ return (((long)PACKED_POSITION_TYPE_CHILD) << PACKED_POSITION_SHIFT_TYPE)
+ | ((((long)groupPosition) & PACKED_POSITION_INT_MASK_GROUP)
+ << PACKED_POSITION_SHIFT_GROUP)
+ | (childPosition & PACKED_POSITION_INT_MASK_CHILD);
+ }
+
+ /**
+ * Returns the packed position representation of a group's position. See
+ * {@link #getPackedPositionForChild(int, int)}.
+ *
+ * @param groupPosition The child's parent group's position.
+ * @return The packed position representation of the group.
+ */
+ public static long getPackedPositionForGroup(int groupPosition) {
+ // No need to OR a type in because PACKED_POSITION_GROUP == 0
+ return ((((long)groupPosition) & PACKED_POSITION_INT_MASK_GROUP)
+ << PACKED_POSITION_SHIFT_GROUP);
+ }
+
+ @Override
+ ContextMenuInfo createContextMenuInfo(View view, int flatListPosition, long id) {
+ ExpandableListPosition pos = mConnector.getUnflattenedPos(flatListPosition).position;
+
+ id = getChildOrGroupId(pos);
+
+ return new ExpandableListContextMenuInfo(view, pos.getPackedPosition(), id);
+ }
+
+ /**
+ * Gets the ID of the group or child at the given <code>position</code>.
+ * This is useful since there is no ListAdapter ID -> ExpandableListAdapter
+ * ID conversion mechanism (in some cases, it isn't possible).
+ *
+ * @param position The position of the child or group whose ID should be
+ * returned.
+ */
+ private long getChildOrGroupId(ExpandableListPosition position) {
+ if (position.type == ExpandableListPosition.CHILD) {
+ return mAdapter.getChildId(position.groupPos, position.childPos);
+ } else {
+ return mAdapter.getGroupId(position.groupPos);
+ }
+ }
+
+ /**
+ * Sets the indicator to be drawn next to a child.
+ *
+ * @param childIndicator The drawable to be used as an indicator. If the
+ * child is the last child for a group, the state
+ * {@link android.R.attr#state_last} will be set.
+ */
+ public void setChildIndicator(Drawable childIndicator) {
+ mChildIndicator = childIndicator;
+ }
+
+ /**
+ * Sets the drawing bounds for the child indicator. For either, you can
+ * specify {@link #CHILD_INDICATOR_INHERIT} to use inherit from the general
+ * indicator's bounds.
+ *
+ * @see #setIndicatorBounds(int, int)
+ * @param left The left position (relative to the left bounds of this View)
+ * to start drawing the indicator.
+ * @param right The right position (relative to the left bounds of this
+ * View) to end the drawing of the indicator.
+ */
+ public void setChildIndicatorBounds(int left, int right) {
+ mChildIndicatorLeft = left;
+ mChildIndicatorRight = right;
+ }
+
+ /**
+ * Sets the indicator to be drawn next to a group.
+ *
+ * @param groupIndicator The drawable to be used as an indicator. If the
+ * group is empty, the state {@link android.R.attr#state_empty} will be
+ * set. If the group is expanded, the state
+ * {@link android.R.attr#state_expanded} will be set.
+ */
+ public void setGroupIndicator(Drawable groupIndicator) {
+ mGroupIndicator = groupIndicator;
+ }
+
+ /**
+ * Sets the drawing bounds for the indicators (at minimum, the group indicator
+ * is affected by this; the child indicator is affected by this if the
+ * child indicator bounds are set to inherit).
+ *
+ * @see #setChildIndicatorBounds(int, int)
+ * @param left The left position (relative to the left bounds of this View)
+ * to start drawing the indicator.
+ * @param right The right position (relative to the left bounds of this
+ * View) to end the drawing of the indicator.
+ */
+ public void setIndicatorBounds(int left, int right) {
+ mIndicatorLeft = left;
+ mIndicatorRight = right;
+ }
+
+ /**
+ * Extra menu information specific to an {@link ExpandableListView} provided
+ * to the
+ * {@link android.view.View.OnCreateContextMenuListener#onCreateContextMenu(ContextMenu, View, ContextMenuInfo) }
+ * callback when a context menu is brought up for this AdapterView.
+ */
+ public static class ExpandableListContextMenuInfo implements ContextMenu.ContextMenuInfo {
+
+ public ExpandableListContextMenuInfo(View targetView, long packedPosition, long id) {
+ this.targetView = targetView;
+ this.packedPosition = packedPosition;
+ this.id = id;
+ }
+
+ /**
+ * The view for which the context menu is being displayed. This
+ * will be one of the children Views of this {@link ExpandableListView}.
+ */
+ public View targetView;
+
+ /**
+ * The packed position in the list represented by the adapter for which
+ * the context menu is being displayed. Use the methods
+ * {@link ExpandableListView#getPackedPositionType},
+ * {@link ExpandableListView#getPackedPositionChild}, and
+ * {@link ExpandableListView#getPackedPositionGroup} to unpack this.
+ */
+ public long packedPosition;
+
+ /**
+ * The ID of the item (group or child) for which the context menu is
+ * being displayed.
+ */
+ public long id;
+ }
+
+ static class SavedState extends BaseSavedState {
+ ArrayList<ExpandableListConnector.GroupMetadata> expandedGroupMetadataList;
+
+ /**
+ * Constructor called from {@link ExpandableListView#onSaveInstanceState()}
+ */
+ SavedState(
+ Parcelable superState,
+ ArrayList<ExpandableListConnector.GroupMetadata> expandedGroupMetadataList) {
+ super(superState);
+ this.expandedGroupMetadataList = expandedGroupMetadataList;
+ }
+
+ /**
+ * Constructor called from {@link #CREATOR}
+ */
+ private SavedState(Parcel in) {
+ super(in);
+ expandedGroupMetadataList = new ArrayList<ExpandableListConnector.GroupMetadata>();
+ in.readList(expandedGroupMetadataList, ExpandableListConnector.class.getClassLoader());
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ super.writeToParcel(out, flags);
+ out.writeList(expandedGroupMetadataList);
+ }
+
+ public static final Parcelable.Creator<SavedState> CREATOR
+ = new Parcelable.Creator<SavedState>() {
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ Parcelable superState = super.onSaveInstanceState();
+ return new SavedState(superState,
+ mConnector != null ? mConnector.getExpandedGroupMetadataList() : null);
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ SavedState ss = (SavedState) state;
+ super.onRestoreInstanceState(ss.getSuperState());
+
+ if (mConnector != null && ss.expandedGroupMetadataList != null) {
+ mConnector.setExpandedGroupMetadataList(ss.expandedGroupMetadataList);
+ }
+ }
+
+}
diff --git a/core/java/android/widget/Filter.java b/core/java/android/widget/Filter.java
new file mode 100644
index 0000000..49888f7
--- /dev/null
+++ b/core/java/android/widget/Filter.java
@@ -0,0 +1,281 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+
+/**
+ * <p>A filter constrains data with a filtering pattern.</p>
+ *
+ * <p>Filters are usually created by {@link android.widget.Filterable}
+ * classes.</p>
+ *
+ * <p>Filtering operations performed by calling {@link #filter(CharSequence)} or
+ * {@link #filter(CharSequence, android.widget.Filter.FilterListener)} are
+ * performed asynchronously. When these methods are called, a filtering request
+ * is posted in a request queue and processed later. Any call to one of these
+ * methods will cancel any previous non-executed filtering request.</p>
+ *
+ * @see android.widget.Filterable
+ */
+public abstract class Filter {
+ private static final String THREAD_NAME = "Filter";
+ private static final int FILTER_TOKEN = 0xD0D0F00D;
+ private static final int FINISH_TOKEN = 0xDEADBEEF;
+
+ private Handler mThreadHandler;
+ private Handler mResultHandler;
+
+ /**
+ * <p>Creates a new asynchronous filter.</p>
+ */
+ public Filter() {
+ mResultHandler = new ResultsHandler();
+ }
+
+ /**
+ * <p>Starts an asynchronous filtering operation. Calling this method
+ * cancels all previous non-executed filtering requests and posts a new
+ * filtering request that will be executed later.</p>
+ *
+ * @param constraint the constraint used to filter the data
+ *
+ * @see #filter(CharSequence, android.widget.Filter.FilterListener)
+ */
+ public final void filter(CharSequence constraint) {
+ filter(constraint, null);
+ }
+
+ /**
+ * <p>Starts an asynchronous filtering operation. Calling this method
+ * cancels all previous non-executed filtering requests and posts a new
+ * filtering request that will be executed later.</p>
+ *
+ * <p>Upon completion, the listener is notified.</p>
+ *
+ * @param constraint the constraint used to filter the data
+ * @param listener a listener notified upon completion of the operation
+ *
+ * @see #filter(CharSequence)
+ * @see #performFiltering(CharSequence)
+ * @see #publishResults(CharSequence, android.widget.Filter.FilterResults)
+ */
+ public final void filter(CharSequence constraint, FilterListener listener) {
+ synchronized (this) {
+ if (mThreadHandler == null) {
+ HandlerThread thread = new HandlerThread(THREAD_NAME);
+ thread.start();
+ mThreadHandler = new RequestHandler(thread.getLooper());
+ }
+
+ Message message = mThreadHandler.obtainMessage(FILTER_TOKEN);
+
+ RequestArguments args = new RequestArguments();
+ args.constraint = constraint;
+ args.listener = listener;
+ message.obj = args;
+
+ mThreadHandler.removeMessages(FILTER_TOKEN);
+ mThreadHandler.removeMessages(FINISH_TOKEN);
+ mThreadHandler.sendMessage(message);
+ }
+ }
+
+ /**
+ * <p>Invoked in a worker thread to filter the data according to the
+ * constraint. Subclasses must implement this method to perform the
+ * filtering operation. Results computed by the filtering operation
+ * must be returned as a {@link android.widget.Filter.FilterResults} that
+ * will then be published in the UI thread through
+ * {@link #publishResults(CharSequence,
+ * android.widget.Filter.FilterResults)}.</p>
+ *
+ * <p><strong>Contract:</strong> When the constraint is null, the original
+ * data must be restored.</p>
+ *
+ * @param constraint the constraint used to filter the data
+ * @return the results of the filtering operation
+ *
+ * @see #filter(CharSequence, android.widget.Filter.FilterListener)
+ * @see #publishResults(CharSequence, android.widget.Filter.FilterResults)
+ * @see android.widget.Filter.FilterResults
+ */
+ protected abstract FilterResults performFiltering(CharSequence constraint);
+
+ /**
+ * <p>Invoked in the UI thread to publish the filtering results in the
+ * user interface. Subclasses must implement this method to display the
+ * results computed in {@link #performFiltering}.</p>
+ *
+ * @param constraint the constraint used to filter the data
+ * @param results the results of the filtering operation
+ *
+ * @see #filter(CharSequence, android.widget.Filter.FilterListener)
+ * @see #performFiltering(CharSequence)
+ * @see android.widget.Filter.FilterResults
+ */
+ protected abstract void publishResults(CharSequence constraint,
+ FilterResults results);
+
+ /**
+ * <p>Converts a value from the filtered set into a CharSequence. Subclasses
+ * should override this method to convert their results. The default
+ * implementation returns an empty String for null values or the default
+ * String representation of the value.</p>
+ *
+ * @param resultValue the value to convert to a CharSequence
+ * @return a CharSequence representing the value
+ */
+ public CharSequence convertResultToString(Object resultValue) {
+ return resultValue == null ? "" : resultValue.toString();
+ }
+
+ /**
+ * <p>Holds the results of a filtering operation. The results are the values
+ * computed by the filtering operation and the number of these values.</p>
+ */
+ protected static class FilterResults {
+ public FilterResults() {
+ // nothing to see here
+ }
+
+ /**
+ * <p>Contains all the values computed by the filtering operation.</p>
+ */
+ public Object values;
+
+ /**
+ * <p>Contains the number of values computed by the filtering
+ * operation.</p>
+ */
+ public int count;
+ }
+
+ /**
+ * <p>Listener used to receive a notification upon completion of a filtering
+ * operation.</p>
+ */
+ public static interface FilterListener {
+ /**
+ * <p>Notifies the end of a filtering operation.</p>
+ *
+ * @param count the number of values computed by the filter
+ */
+ public void onFilterComplete(int count);
+ }
+
+ /**
+ * <p>Worker thread handler. When a new filtering request is posted from
+ * {@link android.widget.Filter#filter(CharSequence, android.widget.Filter.FilterListener)},
+ * it is sent to this handler.</p>
+ */
+ private class RequestHandler extends Handler {
+ public RequestHandler(Looper looper) {
+ super(looper);
+ }
+
+ /**
+ * <p>Handles filtering requests by calling
+ * {@link Filter#performFiltering} and then sending a message
+ * with the results to the results handler.</p>
+ *
+ * @param msg the filtering request
+ */
+ public void handleMessage(Message msg) {
+ int what = msg.what;
+ Message message;
+ switch (what) {
+ case FILTER_TOKEN:
+ RequestArguments args = (RequestArguments) msg.obj;
+ try {
+ args.results = performFiltering(args.constraint);
+ } finally {
+ message = mResultHandler.obtainMessage(what);
+ message.obj = args;
+ message.sendToTarget();
+ }
+
+ synchronized (this) {
+ if (mThreadHandler != null) {
+ Message finishMessage = mThreadHandler.obtainMessage(FINISH_TOKEN);
+ mThreadHandler.sendMessageDelayed(finishMessage, 3000);
+ }
+ }
+ break;
+ case FINISH_TOKEN:
+ synchronized (this) {
+ if (mThreadHandler != null) {
+ mThreadHandler.getLooper().quit();
+ mThreadHandler = null;
+ }
+ }
+ break;
+ }
+ }
+ }
+
+ /**
+ * <p>Handles the results of a filtering operation. The results are
+ * handled in the UI thread.</p>
+ */
+ private class ResultsHandler extends Handler {
+ /**
+ * <p>Messages received from the request handler are processed in the
+ * UI thread. The processing involves calling
+ * {@link Filter#publishResults(CharSequence,
+ * android.widget.Filter.FilterResults)}
+ * to post the results back in the UI and then notifying the listener,
+ * if any.</p>
+ *
+ * @param msg the filtering results
+ */
+ @Override
+ public void handleMessage(Message msg) {
+ RequestArguments args = (RequestArguments) msg.obj;
+
+ publishResults(args.constraint, args.results);
+ if (args.listener != null) {
+ int count = args.results != null ? args.results.count : -1;
+ args.listener.onFilterComplete(count);
+ }
+ }
+ }
+
+ /**
+ * <p>Holds the arguments of a filtering request as well as the results
+ * of the request.</p>
+ */
+ private static class RequestArguments {
+ /**
+ * <p>The constraint used to filter the data.</p>
+ */
+ CharSequence constraint;
+
+ /**
+ * <p>The listener to notify upon completion. Can be null.</p>
+ */
+ FilterListener listener;
+
+ /**
+ * <p>The results of the filtering operation.</p>
+ */
+ FilterResults results;
+ }
+}
diff --git a/core/java/android/widget/FilterQueryProvider.java b/core/java/android/widget/FilterQueryProvider.java
new file mode 100644
index 0000000..740d2f0
--- /dev/null
+++ b/core/java/android/widget/FilterQueryProvider.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import android.database.Cursor;
+
+/**
+ * This class can be used by external clients of CursorAdapter and
+ * CursorTreeAdapter to define how the content of the adapter should be
+ * filtered.
+ *
+ * @see #runQuery(CharSequence)
+ */
+public interface FilterQueryProvider {
+ /**
+ * Runs a query with the specified constraint. This query is requested
+ * by the filter attached to this adapter.
+ *
+ * Contract: when constraint is null or empty, the original results,
+ * prior to any filtering, must be returned.
+ *
+ * @param constraint the constraint with which the query must
+ * be filtered
+ *
+ * @return a Cursor representing the results of the new query
+ */
+ Cursor runQuery(CharSequence constraint);
+}
diff --git a/core/java/android/widget/Filterable.java b/core/java/android/widget/Filterable.java
new file mode 100644
index 0000000..f7c8d59
--- /dev/null
+++ b/core/java/android/widget/Filterable.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+/**
+ * <p>Defines a filterable behavior. A filterable class can have its data
+ * constrained by a filter. Filterable classes are usually
+ * {@link android.widget.Adapter} implementations.</p>
+ *
+ * @see android.widget.Filter
+ */
+public interface Filterable {
+ /**
+ * <p>Returns a filter that can be used to constrain data with a filtering
+ * pattern.</p>
+ *
+ * <p>This method is usually implemented by {@link android.widget.Adapter}
+ * classes.</p>
+ *
+ * @return a filter used to constrain data
+ */
+ Filter getFilter();
+}
diff --git a/core/java/android/widget/FrameLayout.java b/core/java/android/widget/FrameLayout.java
new file mode 100644
index 0000000..b4ed3ba
--- /dev/null
+++ b/core/java/android/widget/FrameLayout.java
@@ -0,0 +1,448 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Gravity;
+import android.widget.RemoteViews.RemoteView;
+
+
+/**
+ * FrameLayout is designed to block out an area on the screen to display
+ * a single item. You can add multiple children to a FrameLayout, but all
+ * children are pegged to the top left of the screen.
+ * Children are drawn in a stack, with the most recently added child on top.
+ * The size of the frame layout is the size of its largest child (plus padding), visible
+ * or not (if the FrameLayout's parent permits). Views that are GONE are used for sizing
+ * only if {@link #setMeasureAllChildren(boolean) setConsiderGoneChildrenWhenMeasuring()}
+ * is set to true.
+ *
+ * @attr ref android.R.styleable#FrameLayout_foreground
+ * @attr ref android.R.styleable#FrameLayout_foregroundGravity
+ * @attr ref android.R.styleable#FrameLayout_measureAllChildren
+ */
+@RemoteView
+public class FrameLayout extends ViewGroup {
+ boolean mMeasureAllChildren = false;
+
+ private Drawable mForeground;
+ private int mForegroundPaddingLeft = 0;
+ private int mForegroundPaddingTop = 0;
+ private int mForegroundPaddingRight = 0;
+ private int mForegroundPaddingBottom = 0;
+
+ private final Rect mSelfBounds = new Rect();
+ private final Rect mOverlayBounds = new Rect();
+ private int mForegroundGravity = Gravity.FILL;
+
+ public FrameLayout(Context context) {
+ super(context);
+ }
+
+ public FrameLayout(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public FrameLayout(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.FrameLayout,
+ defStyle, 0);
+
+ final Drawable d = a.getDrawable(com.android.internal.R.styleable.FrameLayout_foreground);
+ if (d != null) {
+ setForeground(d);
+ }
+
+ if (a.getBoolean(com.android.internal.R.styleable.FrameLayout_measureAllChildren, false)) {
+ setMeasureAllChildren(true);
+ }
+
+ mForegroundGravity = a.getInt(com.android.internal.R.styleable.FrameLayout_foregroundGravity,
+ mForegroundGravity);
+
+ a.recycle();
+ }
+
+ /**
+ * Describes how the foreground is positioned. Defaults to FILL.
+ *
+ * @param foregroundGravity See {@link android.view.Gravity}
+ *
+ * @attr ref android.R.styleable#FrameLayout_foregroundGravity
+ */
+ public void setForegroundGravity(int foregroundGravity) {
+ if (mForegroundGravity != foregroundGravity) {
+ if ((foregroundGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == 0) {
+ foregroundGravity |= Gravity.LEFT;
+ }
+
+ if ((foregroundGravity & Gravity.VERTICAL_GRAVITY_MASK) == 0) {
+ foregroundGravity |= Gravity.TOP;
+ }
+
+ mForegroundGravity = foregroundGravity;
+ requestLayout();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected boolean verifyDrawable(Drawable who) {
+ return super.verifyDrawable(who) || (who == mForeground);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+ if (mForeground != null && mForeground.isStateful()) {
+ mForeground.setState(getDrawableState());
+ }
+ }
+
+ /**
+ * Returns a set of layout parameters with a width of
+ * {@link android.view.ViewGroup.LayoutParams#FILL_PARENT},
+ * and a height of {@link android.view.ViewGroup.LayoutParams#FILL_PARENT}.
+ */
+ @Override
+ protected LayoutParams generateDefaultLayoutParams() {
+ return new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);
+ }
+
+ /**
+ * Supply a Drawable that is to be rendered on top of all of the child
+ * views in the frame layout. Any padding in the Drawable will be taken
+ * into account by ensuring that the children are inset to be placed
+ * inside of the padding area.
+ *
+ * @param drawable The Drawable to be drawn on top of the children.
+ *
+ * @attr ref android.R.styleable#FrameLayout_foreground
+ */
+ public void setForeground(Drawable drawable) {
+ if (mForeground != drawable) {
+ if (mForeground != null) {
+ mForeground.setCallback(null);
+ unscheduleDrawable(mForeground);
+ }
+
+ mForeground = drawable;
+ mForegroundPaddingLeft = 0;
+ mForegroundPaddingTop = 0;
+ mForegroundPaddingRight = 0;
+ mForegroundPaddingBottom = 0;
+
+ if (drawable != null) {
+ setWillNotDraw(false);
+ drawable.setCallback(this);
+ if (drawable.isStateful()) {
+ drawable.setState(getDrawableState());
+ }
+ Rect padding = new Rect();
+ if (drawable.getPadding(padding)) {
+ mForegroundPaddingLeft = padding.left;
+ mForegroundPaddingTop = padding.top;
+ mForegroundPaddingRight = padding.right;
+ mForegroundPaddingBottom = padding.bottom;
+ }
+ } else {
+ setWillNotDraw(true);
+ }
+ requestLayout();
+ invalidate();
+ }
+ }
+
+ /**
+ * Returns the drawable used as the foreground of this FrameLayout. The
+ * foreground drawable, if non-null, is always drawn on top of the children.
+ *
+ * @return A Drawable or null if no foreground was set.
+ */
+ public Drawable getForeground() {
+ return mForeground;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ final int count = getChildCount();
+
+ int maxHeight = 0;
+ int maxWidth = 0;
+
+ // Find rightmost and bottommost child
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (mMeasureAllChildren || child.getVisibility() != GONE) {
+ measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
+ maxWidth = Math.max(maxWidth, child.getMeasuredWidth());
+ maxHeight = Math.max(maxHeight, child.getMeasuredHeight());
+ }
+ }
+
+ // Account for padding too
+ maxWidth += mPaddingLeft + mPaddingRight + mForegroundPaddingLeft + mForegroundPaddingRight;
+ maxHeight += mPaddingTop + mPaddingBottom + mForegroundPaddingTop + mForegroundPaddingBottom;
+
+ // Check against our minimum height and width
+ maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
+ maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
+
+ // Check against our foreground's minimum height and width
+ final Drawable drawable = getForeground();
+ if (drawable != null) {
+ maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
+ maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
+ }
+
+ setMeasuredDimension(resolveSize(maxWidth, widthMeasureSpec),
+ resolveSize(maxHeight, heightMeasureSpec));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ final int count = getChildCount();
+
+ final int parentLeft = mPaddingLeft + mForegroundPaddingLeft;
+ final int parentRight = right - left - mPaddingRight - mForegroundPaddingRight;
+
+ final int parentTop = mPaddingTop + mForegroundPaddingTop;
+ final int parentBottom = bottom - top - mPaddingBottom - mForegroundPaddingBottom;
+
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() != GONE) {
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+ final int width = child.getMeasuredWidth();
+ final int height = child.getMeasuredHeight();
+
+ int childLeft = parentLeft;
+ int childTop = parentTop;
+
+ final int gravity = lp.gravity;
+
+ if (gravity != -1) {
+ final int horizontalGravity = gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
+ final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
+
+ switch (horizontalGravity) {
+ case Gravity.LEFT:
+ childLeft = parentLeft + lp.leftMargin;
+ break;
+ case Gravity.CENTER_HORIZONTAL:
+ childLeft = parentLeft + (parentRight - parentLeft + lp.leftMargin +
+ lp.rightMargin - width) / 2;
+ break;
+ case Gravity.RIGHT:
+ childLeft = parentRight - width - lp.rightMargin;
+ break;
+ default:
+ childLeft = parentLeft + lp.leftMargin;
+ }
+
+ switch (verticalGravity) {
+ case Gravity.TOP:
+ childTop = parentTop + lp.topMargin;
+ break;
+ case Gravity.CENTER_VERTICAL:
+ childTop = parentTop + (parentBottom - parentTop + lp.topMargin +
+ lp.bottomMargin - height) / 2;
+ break;
+ case Gravity.BOTTOM:
+ childTop = parentBottom - height - lp.bottomMargin;
+ break;
+ default:
+ childTop = parentTop + lp.topMargin;
+ }
+ }
+
+ child.layout(childLeft, childTop, childLeft + width, childTop + height);
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+
+ final Drawable foreground = mForeground;
+ if (foreground != null) {
+ final Rect selfBounds = mSelfBounds;
+ final Rect overlayBounds = mOverlayBounds;
+
+ selfBounds.set(0, 0, w, h);
+ Gravity.apply(mForegroundGravity, foreground.getIntrinsicWidth(),
+ foreground.getIntrinsicHeight(), selfBounds, overlayBounds);
+
+ foreground.setBounds(overlayBounds);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void draw(Canvas canvas) {
+ super.draw(canvas);
+
+ if (mForeground != null) {
+ mForeground.draw(canvas);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean gatherTransparentRegion(Region region) {
+ boolean opaque = super.gatherTransparentRegion(region);
+ if (region != null && mForeground != null) {
+ applyDrawableToTransparentRegion(mForeground, region);
+ }
+ return opaque;
+ }
+
+ /**
+ * Determines whether to measure all children or just those in
+ * the VISIBLE or INVISIBLE state when measuring. Defaults to false.
+ * @param measureAll true to consider children marked GONE, false otherwise.
+ * Default value is false.
+ *
+ * @attr ref android.R.styleable#FrameLayout_measureAllChildren
+ */
+ public void setMeasureAllChildren(boolean measureAll) {
+ mMeasureAllChildren = measureAll;
+ }
+
+ /**
+ * Determines whether to measure all children or just those in
+ * the VISIBLE or INVISIBLE state when measuring.
+ */
+ public boolean getConsiderGoneChildrenWhenMeasuring() {
+ return mMeasureAllChildren;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public LayoutParams generateLayoutParams(AttributeSet attrs) {
+ return new FrameLayout.LayoutParams(getContext(), attrs);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+ return p instanceof LayoutParams;
+ }
+
+ @Override
+ protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
+ return new LayoutParams(p);
+ }
+
+ /**
+ * Per-child layout information for layouts that support margins.
+ * See {@link android.R.styleable#FrameLayout_Layout FrameLayout Layout Attributes}
+ * for a list of all child view attributes that this class supports.
+ */
+ public static class LayoutParams extends MarginLayoutParams {
+ /**
+ * The gravity to apply with the View to which these layout parameters
+ * are associated.
+ *
+ * @see android.view.Gravity
+ */
+ public int gravity = -1;
+
+ /**
+ * {@inheritDoc}
+ */
+ public LayoutParams(Context c, AttributeSet attrs) {
+ super(c, attrs);
+
+ TypedArray a = c.obtainStyledAttributes(attrs, com.android.internal.R.styleable.FrameLayout_Layout);
+ gravity = a.getInt(com.android.internal.R.styleable.FrameLayout_Layout_layout_gravity, -1);
+ a.recycle();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public LayoutParams(int width, int height) {
+ super(width, height);
+ }
+
+ /**
+ * Creates a new set of layout parameters with the specified width, height
+ * and weight.
+ *
+ * @param width the width, either {@link #FILL_PARENT},
+ * {@link #WRAP_CONTENT} or a fixed size in pixels
+ * @param height the height, either {@link #FILL_PARENT},
+ * {@link #WRAP_CONTENT} or a fixed size in pixels
+ * @param gravity the gravity
+ *
+ * @see android.view.Gravity
+ */
+ public LayoutParams(int width, int height, int gravity) {
+ super(width, height);
+ this.gravity = gravity;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public LayoutParams(ViewGroup.LayoutParams source) {
+ super(source);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public LayoutParams(ViewGroup.MarginLayoutParams source) {
+ super(source);
+ }
+ }
+}
+
diff --git a/core/java/android/widget/Gallery.java b/core/java/android/widget/Gallery.java
new file mode 100644
index 0000000..acf9400
--- /dev/null
+++ b/core/java/android/widget/Gallery.java
@@ -0,0 +1,1338 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import com.android.internal.R;
+
+import android.annotation.Widget;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.util.Config;
+import android.util.Log;
+import android.view.GestureDetector;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.SoundEffectConstants;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.animation.Transformation;
+import android.widget.AbsSpinner;
+import android.widget.Scroller;
+
+/**
+ * A view that shows items in a center-locked, horizontally scrolling list.
+ * <p>
+ * The default values for the Gallery assume you will be using
+ * {@link android.R.styleable#Theme_galleryItemBackground} as the background for
+ * each View given to the Gallery from the Adapter. If you are not doing this,
+ * you may need to adjust some Gallery properties, such as the spacing.
+ *
+ * @attr ref android.R.styleable#Gallery_animationDuration
+ * @attr ref android.R.styleable#Gallery_spacing
+ * @attr ref android.R.styleable#Gallery_gravity
+ */
+@Widget
+public class Gallery extends AbsSpinner implements GestureDetector.OnGestureListener {
+
+ private static final String TAG = "Gallery";
+
+ private static final boolean localLOGV = Config.LOGV;
+
+ /**
+ * Horizontal spacing between items.
+ */
+ private int mSpacing = 0;
+
+ /**
+ * How long the transition animation should run when a child view changes
+ * position, measured in milliseconds.
+ */
+ private int mAnimationDuration = 400;
+
+ /**
+ * The alpha of items that are not selected.
+ */
+ private float mUnselectedAlpha;
+
+ /**
+ * Left most edge of a child seen so far during layout.
+ */
+ private int mLeftMost;
+
+ /**
+ * Right most edge of a child seen so far during layout.
+ */
+ private int mRightMost;
+
+ private int mGravity;
+
+ /**
+ * Helper for detecting touch gestures.
+ */
+ private GestureDetector mGestureDetector;
+
+ /**
+ * The position of the item that received the user's down touch.
+ */
+ private int mDownTouchPosition;
+
+ /**
+ * The view of the item that received the user's down touch.
+ */
+ private View mDownTouchView;
+
+ /**
+ * Executes the delta scrolls from a fling or scroll movement.
+ */
+ private FlingRunnable mFlingRunnable = new FlingRunnable();
+
+ /**
+ * When fling runnable runs, it resets this to false. Any method along the
+ * path until the end of its run() can set this to true to abort any
+ * remaining fling. For example, if we've reached either the leftmost or
+ * rightmost item, we will set this to true.
+ */
+ private boolean mShouldStopFling;
+
+ /**
+ * The currently selected item's child.
+ */
+ private View mSelectedChild;
+
+ /**
+ * Whether to continuously callback on the item selected listener during a
+ * fling.
+ */
+ private boolean mShouldCallbackDuringFling;
+
+ /**
+ * Whether to callback when an item that is not selected is clicked.
+ */
+ private boolean mShouldCallbackOnUnselectedItemClick = true;
+
+ /**
+ * If true, do not callback to item selected listener.
+ */
+ private boolean mSuppressSelectionChanged;
+
+ private AdapterContextMenuInfo mContextMenuInfo;
+
+ public Gallery(Context context) {
+ this(context, null);
+ }
+
+ public Gallery(Context context, AttributeSet attrs) {
+ this(context, attrs, R.attr.galleryStyle);
+ }
+
+ public Gallery(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ mGestureDetector = new GestureDetector(this);
+ mGestureDetector.setIsLongpressEnabled(true);
+
+ TypedArray a = context.obtainStyledAttributes(
+ attrs, com.android.internal.R.styleable.Gallery, defStyle, 0);
+
+ int index = a.getInt(com.android.internal.R.styleable.Gallery_gravity, -1);
+ if (index >= 0) {
+ setGravity(index);
+ }
+
+ int animationDuration =
+ a.getInt(com.android.internal.R.styleable.Gallery_animationDuration, -1);
+ if (animationDuration > 0) {
+ setAnimationDuration(animationDuration);
+ }
+
+ int spacing =
+ a.getDimensionPixelOffset(com.android.internal.R.styleable.Gallery_spacing, 0);
+ setSpacing(spacing);
+
+ float unselectedAlpha = a.getFloat(
+ com.android.internal.R.styleable.Gallery_unselectedAlpha, 0.5f);
+ setUnselectedAlpha(unselectedAlpha);
+
+ a.recycle();
+
+ // We draw the selected item last (because otherwise the item to the
+ // right overlaps it)
+ mGroupFlags |= FLAG_USE_CHILD_DRAWING_ORDER;
+
+ mGroupFlags |= FLAG_SUPPORT_STATIC_TRANSFORMATIONS;
+ }
+
+ /**
+ * Whether or not to callback on any {@link #getOnItemSelectedListener()}
+ * while the items are being flinged. If false, only the final selected item
+ * will cause the callback. If true, all items between the first and the
+ * final will cause callbacks.
+ *
+ * @param shouldCallback Whether or not to callback on the listener while
+ * the items are being flinged.
+ */
+ public void setCallbackDuringFling(boolean shouldCallback) {
+ mShouldCallbackDuringFling = shouldCallback;
+ }
+
+ /**
+ * Whether or not to callback when an item that is not selected is clicked.
+ * If false, the item will become selected (and re-centered). If true, the
+ * {@link #getOnItemClickListener()} will get the callback.
+ *
+ * @param shouldCallback Whether or not to callback on the listener when a
+ * item that is not selected is clicked.
+ * @hide
+ */
+ public void setCallbackOnUnselectedItemClick(boolean shouldCallback) {
+ mShouldCallbackOnUnselectedItemClick = shouldCallback;
+ }
+
+ /**
+ * Sets how long the transition animation should run when a child view
+ * changes position. Only relevant if animation is turned on.
+ *
+ * @param animationDurationMillis The duration of the transition, in
+ * milliseconds.
+ *
+ * @attr ref android.R.styleable#Gallery_animationDuration
+ */
+ public void setAnimationDuration(int animationDurationMillis) {
+ mAnimationDuration = animationDurationMillis;
+ }
+
+ /**
+ * Sets the spacing between items in a Gallery
+ *
+ * @param spacing The spacing in pixels between items in the Gallery
+ *
+ * @attr ref android.R.styleable#Gallery_spacing
+ */
+ public void setSpacing(int spacing) {
+ mSpacing = spacing;
+ }
+
+ /**
+ * Sets the alpha of items that are not selected in the Gallery.
+ *
+ * @param unselectedAlpha the alpha for the items that are not selected.
+ *
+ * @attr ref android.R.styleable#Gallery_unselectedAlpha
+ */
+ public void setUnselectedAlpha(float unselectedAlpha) {
+ mUnselectedAlpha = unselectedAlpha;
+ }
+
+ @Override
+ protected boolean getChildStaticTransformation(View child, Transformation t) {
+
+ t.clear();
+ t.setAlpha(child == mSelectedChild ? 1.0f : mUnselectedAlpha);
+
+ return true;
+ }
+
+ @Override
+ protected int computeHorizontalScrollExtent() {
+ // Only 1 item is considered to be selected
+ return 1;
+ }
+
+ @Override
+ protected int computeHorizontalScrollOffset() {
+ // Current scroll position is the same as the selected position
+ return mSelectedPosition;
+ }
+
+ @Override
+ protected int computeHorizontalScrollRange() {
+ // Scroll range is the same as the item count
+ return mItemCount;
+ }
+
+ @Override
+ protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+ return p instanceof LayoutParams;
+ }
+
+ @Override
+ protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
+ return new LayoutParams(p);
+ }
+
+ @Override
+ public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
+ return new LayoutParams(getContext(), attrs);
+ }
+
+ @Override
+ protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
+ /*
+ * Gallery expects Gallery.LayoutParams.
+ */
+ return new Gallery.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ super.onLayout(changed, l, t, r, b);
+
+ /*
+ * Remember that we are in layout to prevent more layout request from
+ * being generated.
+ */
+ mInLayout = true;
+ layout(0, false);
+ mInLayout = false;
+ }
+
+ @Override
+ int getChildHeight(View child) {
+ return child.getMeasuredHeight();
+ }
+
+ /**
+ * Tracks a motion scroll. In reality, this is used to do just about any
+ * movement to items (touch scroll, arrow-key scroll, set an item as selected).
+ *
+ * @param deltaX Change in X from the previous event.
+ */
+ void trackMotionScroll(int deltaX) {
+
+ if (getChildCount() == 0) {
+ return;
+ }
+
+ boolean toLeft = deltaX < 0;
+
+ int limitedDeltaX = getLimitedMotionScrollAmount(toLeft, deltaX);
+ if (limitedDeltaX != deltaX) {
+ // The above call returned a limited amount, so stop any scrolls/flings
+ mFlingRunnable.endFling(false);
+ onFinishedMovement();
+ }
+
+ offsetChildrenLeftAndRight(limitedDeltaX);
+
+ detachOffScreenChildren(toLeft);
+
+ if (toLeft) {
+ // If moved left, there will be empty space on the right
+ fillToGalleryRight();
+ } else {
+ // Similarly, empty space on the left
+ fillToGalleryLeft();
+ }
+
+ // Clear unused views
+ mRecycler.clear();
+
+ setSelectionToCenterChild();
+
+ invalidate();
+ }
+
+ int getLimitedMotionScrollAmount(boolean motionToLeft, int deltaX) {
+ int extremeItemPosition = motionToLeft ? mItemCount - 1 : 0;
+ View extremeChild = getChildAt(extremeItemPosition - mFirstPosition);
+
+ if (extremeChild == null) {
+ return deltaX;
+ }
+
+ int extremeChildCenter = getCenterOfView(extremeChild);
+ int galleryCenter = getCenterOfGallery();
+
+ if (motionToLeft) {
+ if (extremeChildCenter <= galleryCenter) {
+
+ // The extreme child is past his boundary point!
+ return 0;
+ }
+ } else {
+ if (extremeChildCenter >= galleryCenter) {
+
+ // The extreme child is past his boundary point!
+ return 0;
+ }
+ }
+
+ int centerDifference = galleryCenter - extremeChildCenter;
+
+ return motionToLeft
+ ? Math.max(centerDifference, deltaX)
+ : Math.min(centerDifference, deltaX);
+ }
+
+ /**
+ * Offset the horizontal location of all children of this view by the
+ * specified number of pixels.
+ *
+ * @param offset the number of pixels to offset
+ */
+ private void offsetChildrenLeftAndRight(int offset) {
+ for (int i = getChildCount() - 1; i >= 0; i--) {
+ getChildAt(i).offsetLeftAndRight(offset);
+ }
+ }
+
+ /**
+ * @return The center of this Gallery.
+ */
+ private int getCenterOfGallery() {
+ return (getWidth() - mPaddingLeft - mPaddingRight) / 2 + mPaddingLeft;
+ }
+
+ /**
+ * @return The center of the given view.
+ */
+ private static int getCenterOfView(View view) {
+ return view.getLeft() + view.getWidth() / 2;
+ }
+
+ /**
+ * Detaches children that are off the screen (i.e.: Gallery bounds).
+ *
+ * @param toLeft Whether to detach children to the left of the Gallery, or
+ * to the right.
+ */
+ private void detachOffScreenChildren(boolean toLeft) {
+ int numChildren = getChildCount();
+ int firstPosition = mFirstPosition;
+ int start = 0;
+ int count = 0;
+
+ if (toLeft) {
+ final int galleryLeft = mPaddingLeft;
+ for (int i = 0; i < numChildren; i++) {
+ final View child = getChildAt(i);
+ if (child.getRight() >= galleryLeft) {
+ break;
+ } else {
+ count++;
+ mRecycler.put(firstPosition + i, child);
+ }
+ }
+ } else {
+ final int galleryRight = getWidth() - mPaddingRight;
+ for (int i = numChildren - 1; i >= 0; i--) {
+ final View child = getChildAt(i);
+ if (child.getLeft() <= galleryRight) {
+ break;
+ } else {
+ start = i;
+ count++;
+ mRecycler.put(firstPosition + i, child);
+ }
+ }
+ }
+
+ detachViewsFromParent(start, count);
+
+ if (toLeft) {
+ mFirstPosition += count;
+ }
+ }
+
+ /**
+ * Scrolls the items so that the selected item is in its 'slot' (its center
+ * is the gallery's center).
+ */
+ private void scrollIntoSlots() {
+
+ if (getChildCount() == 0 || mSelectedChild == null) return;
+
+ int selectedCenter = getCenterOfView(mSelectedChild);
+ int targetCenter = getCenterOfGallery();
+
+ int scrollAmount = targetCenter - selectedCenter;
+ if (scrollAmount != 0) {
+ mFlingRunnable.startUsingDistance(scrollAmount);
+ } else {
+ onFinishedMovement();
+ }
+ }
+
+ private void onFinishedMovement() {
+ if (mSuppressSelectionChanged) {
+ mSuppressSelectionChanged = false;
+
+ // We haven't been callbacking during the fling, so do it now
+ super.selectionChanged();
+ }
+ }
+
+ @Override
+ void selectionChanged() {
+ if (!mSuppressSelectionChanged) {
+ super.selectionChanged();
+ }
+ }
+
+ /**
+ * Looks for the child that is closest to the center and sets it as the
+ * selected child.
+ */
+ private void setSelectionToCenterChild() {
+
+ View selView = mSelectedChild;
+ if (mSelectedChild == null) return;
+
+ int galleryCenter = getCenterOfGallery();
+
+ if (selView != null) {
+
+ // Common case where the current selected position is correct
+ if (selView.getLeft() <= galleryCenter && selView.getRight() >= galleryCenter) {
+ return;
+ }
+ }
+
+ // TODO better search
+ int closestEdgeDistance = Integer.MAX_VALUE;
+ int newSelectedChildIndex = 0;
+ for (int i = getChildCount() - 1; i >= 0; i--) {
+
+ View child = getChildAt(i);
+
+ if (child.getLeft() <= galleryCenter && child.getRight() >= galleryCenter) {
+ // This child is in the center
+ newSelectedChildIndex = i;
+ break;
+ }
+
+ int childClosestEdgeDistance = Math.min(Math.abs(child.getLeft() - galleryCenter),
+ Math.abs(child.getRight() - galleryCenter));
+ if (childClosestEdgeDistance < closestEdgeDistance) {
+ closestEdgeDistance = childClosestEdgeDistance;
+ newSelectedChildIndex = i;
+ }
+ }
+
+ int newPos = mFirstPosition + newSelectedChildIndex;
+
+ if (newPos != mSelectedPosition) {
+ setSelectedPositionInt(newPos);
+ setNextSelectedPositionInt(newPos);
+ checkSelectionChanged();
+ }
+ }
+
+ /**
+ * Creates and positions all views for this Gallery.
+ * <p>
+ * We layout rarely, most of the time {@link #trackMotionScroll(int)} takes
+ * care of repositioning, adding, and removing children.
+ *
+ * @param delta Change in the selected position. +1 means the selection is
+ * moving to the right, so views are scrolling to the left. -1
+ * means the selection is moving to the left.
+ */
+ @Override
+ void layout(int delta, boolean animate) {
+
+ int childrenLeft = mSpinnerPadding.left;
+ int childrenWidth = mRight - mLeft - mSpinnerPadding.left - mSpinnerPadding.right;
+
+ if (mDataChanged) {
+ handleDataChanged();
+ }
+
+ // Handle an empty gallery by removing all views.
+ if (mItemCount == 0) {
+ resetList();
+ return;
+ }
+
+ // Update to the new selected position.
+ if (mNextSelectedPosition >= 0) {
+ setSelectedPositionInt(mNextSelectedPosition);
+ }
+
+ // All views go in recycler while we are in layout
+ recycleAllViews();
+
+ // Clear out old views
+ //removeAllViewsInLayout();
+ detachAllViewsFromParent();
+
+ /*
+ * These will be used to give initial positions to views entering the
+ * gallery as we scroll
+ */
+ mRightMost = 0;
+ mLeftMost = 0;
+
+ // Make selected view and center it
+
+ /*
+ * mFirstPosition will be decreased as we add views to the left later
+ * on. The 0 for x will be offset in a couple lines down.
+ */
+ mFirstPosition = mSelectedPosition;
+ View sel = makeAndAddView(mSelectedPosition, 0, 0, true);
+
+ // Put the selected child in the center
+ Gallery.LayoutParams lp = (Gallery.LayoutParams) sel.getLayoutParams();
+ int selectedOffset = childrenLeft + (childrenWidth / 2) - (sel.getWidth() / 2);
+ sel.offsetLeftAndRight(selectedOffset);
+
+ fillToGalleryRight();
+ fillToGalleryLeft();
+
+ // Flush any cached views that did not get reused above
+ mRecycler.clear();
+
+ invalidate();
+ checkSelectionChanged();
+
+ mDataChanged = false;
+ mNeedSync = false;
+ setNextSelectedPositionInt(mSelectedPosition);
+
+ updateSelectedItemMetadata();
+ }
+
+ private void fillToGalleryLeft() {
+ int itemSpacing = mSpacing;
+ int galleryLeft = mPaddingLeft;
+
+ // Set state for initial iteration
+ View prevIterationView = getChildAt(0);
+ int curPosition;
+ int curRightEdge;
+
+ if (prevIterationView != null) {
+ curPosition = mFirstPosition - 1;
+ curRightEdge = prevIterationView.getLeft() - itemSpacing;
+ } else {
+ // No children available!
+ curPosition = 0;
+ curRightEdge = mRight - mLeft - mPaddingRight;
+ mShouldStopFling = true;
+ }
+
+ while (curRightEdge > galleryLeft && curPosition >= 0) {
+ prevIterationView = makeAndAddView(curPosition, curPosition - mSelectedPosition,
+ curRightEdge, false);
+
+ // Remember some state
+ mFirstPosition = curPosition;
+
+ // Set state for next iteration
+ curRightEdge = prevIterationView.getLeft() - itemSpacing;
+ curPosition--;
+ }
+ }
+
+ private void fillToGalleryRight() {
+ int itemSpacing = mSpacing;
+ int galleryRight = mRight - mLeft - mPaddingRight;
+ int numChildren = getChildCount();
+ int numItems = mItemCount;
+
+ // Set state for initial iteration
+ View prevIterationView = getChildAt(numChildren - 1);
+ int curPosition;
+ int curLeftEdge;
+
+ if (prevIterationView != null) {
+ curPosition = mFirstPosition + numChildren;
+ curLeftEdge = prevIterationView.getRight() + itemSpacing;
+ } else {
+ mFirstPosition = curPosition = mItemCount - 1;
+ curLeftEdge = mPaddingLeft;
+ mShouldStopFling = true;
+ }
+
+ while (curLeftEdge < galleryRight && curPosition < numItems) {
+ prevIterationView = makeAndAddView(curPosition, curPosition - mSelectedPosition,
+ curLeftEdge, true);
+
+ // Set state for next iteration
+ curLeftEdge = prevIterationView.getRight() + itemSpacing;
+ curPosition++;
+ }
+ }
+
+ /**
+ * Obtain a view, either by pulling an existing view from the recycler or by
+ * getting a new one from the adapter. If we are animating, make sure there
+ * is enough information in the view's layout parameters to animate from the
+ * old to new positions.
+ *
+ * @param position Position in the gallery for the view to obtain
+ * @param offset Offset from the selected position
+ * @param x X-coordintate indicating where this view should be placed. This
+ * will either be the left or right edge of the view, depending on
+ * the fromLeft paramter
+ * @param fromLeft Are we posiitoning views based on the left edge? (i.e.,
+ * building from left to right)?
+ * @return A view that has been added to the gallery
+ */
+ private View makeAndAddView(int position, int offset, int x,
+ boolean fromLeft) {
+
+ View child;
+
+ if (!mDataChanged) {
+ child = mRecycler.get(position);
+ if (child != null) {
+ // Can reuse an existing view
+ Gallery.LayoutParams lp = (Gallery.LayoutParams)
+ child.getLayoutParams();
+
+ int childLeft = child.getLeft();
+
+ // Remember left and right edges of where views have been placed
+ mRightMost = Math.max(mRightMost, childLeft
+ + child.getMeasuredWidth());
+ mLeftMost = Math.min(mLeftMost, childLeft);
+
+ // Position the view
+ setUpChild(child, offset, x, fromLeft);
+
+ return child;
+ }
+ }
+
+ // Nothing found in the recycler -- ask the adapter for a view
+ child = mAdapter.getView(position, null, this);
+
+ // Position the view
+ setUpChild(child, offset, x, fromLeft);
+
+ return child;
+ }
+
+ /**
+ * Helper for makeAndAddView to set the position of a view and fill out its
+ * layout paramters.
+ *
+ * @param child The view to position
+ * @param offset Offset from the selected position
+ * @param x X-coordintate indicating where this view should be placed. This
+ * will either be the left or right edge of the view, depending on
+ * the fromLeft paramter
+ * @param fromLeft Are we posiitoning views based on the left edge? (i.e.,
+ * building from left to right)?
+ */
+ private void setUpChild(View child, int offset, int x, boolean fromLeft) {
+
+ // Respect layout params that are already in the view. Otherwise
+ // make some up...
+ Gallery.LayoutParams lp = (Gallery.LayoutParams)
+ child.getLayoutParams();
+ if (lp == null) {
+ lp = (Gallery.LayoutParams) generateDefaultLayoutParams();
+ }
+
+ addViewInLayout(child, fromLeft ? -1 : 0, lp);
+
+ child.setSelected(offset == 0);
+
+ // Get measure specs
+ int childHeightSpec = ViewGroup.getChildMeasureSpec(mHeightMeasureSpec,
+ mSpinnerPadding.top + mSpinnerPadding.bottom, lp.height);
+ int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
+ mSpinnerPadding.left + mSpinnerPadding.right, lp.width);
+
+ // Measure child
+ child.measure(childWidthSpec, childHeightSpec);
+
+ int childLeft;
+ int childRight;
+
+ // Position vertically based on gravity setting
+ int childTop = calculateTop(child, lp, true);
+ int childBottom = childTop + child.getMeasuredHeight();
+
+ int width = child.getMeasuredWidth();
+ if (fromLeft) {
+ childLeft = x;
+ childRight = childLeft + width;
+ } else {
+ childLeft = x - width;
+ childRight = x;
+ }
+
+ child.layout(childLeft, childTop, childRight, childBottom);
+ }
+
+ /**
+ * Figure out vertical placement based on mGravity
+ *
+ * @param child Child to place
+ * @param lp LayoutParams for this view (just so we don't keep looking them
+ * up)
+ * @return Where the top of the child should be
+ */
+ private int calculateTop(View child, Gallery.LayoutParams lp, boolean duringLayout) {
+ int myHeight = duringLayout ? mMeasuredHeight : getHeight();
+ int childHeight = duringLayout ? child.getMeasuredHeight() : child.getHeight();
+
+ int childTop = 0;
+
+ switch (mGravity) {
+ case Gravity.TOP:
+ childTop = mSpinnerPadding.top;
+ break;
+ case Gravity.CENTER_VERTICAL:
+ int availableSpace = myHeight - mSpinnerPadding.bottom
+ - mSpinnerPadding.top - childHeight;
+ childTop = mSpinnerPadding.top + (availableSpace / 2);
+ break;
+ case Gravity.BOTTOM:
+ childTop = myHeight - mSpinnerPadding.bottom - childHeight;
+ break;
+ }
+ return childTop;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+
+ // Give everything to the gesture detector
+ boolean retValue = mGestureDetector.onTouchEvent(event);
+
+ int action = event.getAction();
+ if (action == MotionEvent.ACTION_UP) {
+ // Helper method for lifted finger
+ onUp();
+ } else if (action == MotionEvent.ACTION_CANCEL) {
+ onCancel();
+ }
+
+ return retValue;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean onSingleTapUp(MotionEvent e) {
+
+ if (mDownTouchPosition >= 0) {
+
+ // An item tap should make it selected, so scroll to this child.
+ scrollToChild(mDownTouchPosition - mFirstPosition);
+
+ // Also pass the click so the client knows, if it wants to.
+ if (mShouldCallbackOnUnselectedItemClick || mDownTouchPosition == mSelectedPosition) {
+ performItemClick(mDownTouchView, mDownTouchPosition, mAdapter
+ .getItemId(mDownTouchPosition));
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+
+ if (!mShouldCallbackDuringFling) {
+ // This will get reset once we scroll into slots
+ mSuppressSelectionChanged = true;
+ }
+
+ // Fling the gallery!
+ mFlingRunnable.startUsingVelocity((int) -velocityX);
+
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+
+ if (localLOGV) Log.v(TAG, String.valueOf(e2.getX() - e1.getX()));
+
+ /*
+ * Now's a good time to tell our parent to stop intercepting our events!
+ * The user has moved more than the slop amount, since GestureDetector
+ * ensures this before calling this method. Also, if a parent is more
+ * interested in this touch's events than we are, it would have
+ * intercepted them by now (for example, we can assume when a Gallery is
+ * in the ListView, a vertical scroll would not end up in this method
+ * since a ListView would have intercepted it by now).
+ */
+ mParent.requestDisallowInterceptTouchEvent(true);
+
+ // As the user scrolls, we want to callback selection changes so related
+ // into on the screen is up-to-date with the user's selection
+ if (mSuppressSelectionChanged) {
+ mSuppressSelectionChanged = false;
+ }
+
+ // Track the motion
+ trackMotionScroll(-1 * (int) distanceX);
+
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean onDown(MotionEvent e) {
+
+ // Kill any existing fling/scroll
+ mFlingRunnable.stop(false);
+
+ // Get the item's view that was touched
+ mDownTouchPosition = pointToPosition((int) e.getX(), (int) e.getY());
+
+ if (mDownTouchPosition >= 0) {
+ mDownTouchView = getChildAt(mDownTouchPosition - mFirstPosition);
+ mDownTouchView.setPressed(true);
+ }
+
+ // Must return true to get matching events for this down event.
+ return true;
+ }
+
+ /**
+ * Called when a touch event's action is MotionEvent.ACTION_UP.
+ */
+ void onUp() {
+
+ if (mFlingRunnable.mScroller.isFinished()) {
+ scrollIntoSlots();
+ }
+
+ dispatchUnpress();
+ }
+
+ /**
+ * Called when a touch event's action is MotionEvent.ACTION_CANCEL.
+ */
+ void onCancel() {
+ onUp();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void onLongPress(MotionEvent e) {
+
+ if (mDownTouchPosition < 0) {
+ return;
+ }
+
+ long id = getItemIdAtPosition(mDownTouchPosition);
+ dispatchLongPress(mDownTouchView, mDownTouchPosition, id);
+ }
+
+ // Unused methods from GestureDetector.OnGestureListener below
+
+ /**
+ * {@inheritDoc}
+ */
+ public void onShowPress(MotionEvent e) {
+ }
+
+ // Unused methods from GestureDetector.OnGestureListener above
+
+ private void dispatchPress(View child) {
+
+ if (child != null) {
+ child.setPressed(true);
+ }
+
+ setPressed(true);
+ }
+
+ private void dispatchUnpress() {
+
+ for (int i = getChildCount() - 1; i >= 0; i--) {
+ getChildAt(i).setPressed(false);
+ }
+
+ setPressed(false);
+ }
+
+ @Override
+ public void dispatchSetSelected(boolean selected) {
+ /*
+ * We don't want to pass the selected state given from its parent to its
+ * children since this widget itself has a selected state to give to its
+ * children.
+ */
+ }
+
+ @Override
+ protected void dispatchSetPressed(boolean pressed) {
+
+ // Show the pressed state on the selected child
+ if (mSelectedChild != null) {
+ mSelectedChild.setPressed(pressed);
+ }
+ }
+
+ @Override
+ protected ContextMenuInfo getContextMenuInfo() {
+ return mContextMenuInfo;
+ }
+
+ @Override
+ public boolean showContextMenuForChild(View originalView) {
+
+ final int longPressPosition = getPositionForView(originalView);
+ if (longPressPosition < 0) {
+ return false;
+ }
+
+ final long longPressId = mAdapter.getItemId(longPressPosition);
+ return dispatchLongPress(originalView, longPressPosition, longPressId);
+ }
+
+ @Override
+ public boolean showContextMenu() {
+
+ if (isPressed() && mSelectedPosition >= 0) {
+ int index = mSelectedPosition - mFirstPosition;
+ View v = getChildAt(index);
+ return dispatchLongPress(v, mSelectedPosition, mSelectedRowId);
+ }
+
+ return false;
+ }
+
+ private boolean dispatchLongPress(View view, int position, long id) {
+ boolean handled = false;
+
+ if (mOnItemLongClickListener != null) {
+ handled = mOnItemLongClickListener.onItemLongClick(this, mDownTouchView,
+ mDownTouchPosition, id);
+ }
+
+ if (!handled) {
+ mContextMenuInfo = new AdapterContextMenuInfo(view, position, id);
+ handled = super.showContextMenuForChild(this);
+ }
+
+ return handled;
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ // Gallery steals all key events
+ return event.dispatch(this);
+ }
+
+ /**
+ * Handles left, right, and clicking
+ * @see android.view.View#onKeyDown
+ */
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ switch (keyCode) {
+
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ if (movePrevious()) {
+ playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT);
+ }
+ return true;
+
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ if (moveNext()) {
+ playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT);
+ }
+ return true;
+ }
+
+ return super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ case KeyEvent.KEYCODE_ENTER: {
+ if (mItemCount > 0) {
+
+ dispatchPress(mSelectedChild);
+ postDelayed(new Runnable() {
+ public void run() {
+ dispatchUnpress();
+ }
+ }, ViewConfiguration.getPressedStateDuration());
+
+ int selectedIndex = mSelectedPosition - mFirstPosition;
+ performItemClick(getChildAt(selectedIndex), mSelectedPosition, mAdapter
+ .getItemId(mSelectedPosition));
+ }
+ return true;
+ }
+ }
+
+ return super.onKeyUp(keyCode, event);
+ }
+
+ boolean movePrevious() {
+ if (mItemCount > 0 && mSelectedPosition > 0) {
+ scrollToChild(mSelectedPosition - mFirstPosition - 1);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ boolean moveNext() {
+ if (mItemCount > 0 && mSelectedPosition < mItemCount - 1) {
+ scrollToChild(mSelectedPosition - mFirstPosition + 1);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ private boolean scrollToChild(int childPosition) {
+ View child = getChildAt(childPosition);
+
+ if (child != null) {
+ int distance = getCenterOfGallery() - getCenterOfView(child);
+ mFlingRunnable.startUsingDistance(distance);
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ void setSelectedPositionInt(int position) {
+ super.setSelectedPositionInt(position);
+
+ // Updates any metadata we keep about the selected item.
+ updateSelectedItemMetadata();
+ }
+
+ private void updateSelectedItemMetadata() {
+
+ View oldSelectedChild = mSelectedChild;
+
+ View child = mSelectedChild = getChildAt(mSelectedPosition - mFirstPosition);
+ if (child == null) {
+ return;
+ }
+
+ child.setSelected(true);
+ child.setFocusable(true);
+
+ if (hasFocus()) {
+ child.requestFocus();
+ }
+
+ // We unfocus the old child down here so the above hasFocus check
+ // returns true
+ if (oldSelectedChild != null) {
+
+ // Make sure its drawable state doesn't contain 'selected'
+ oldSelectedChild.setSelected(false);
+
+ // Make sure it is not focusable anymore, since otherwise arrow keys
+ // can make this one be focused
+ oldSelectedChild.setFocusable(false);
+ }
+
+ }
+
+ /**
+ * Describes how the child views are aligned.
+ * @param gravity
+ *
+ * @attr ref android.R.styleable#Gallery_gravity
+ */
+ public void setGravity(int gravity)
+ {
+ if (mGravity != gravity) {
+ mGravity = gravity;
+ requestLayout();
+ }
+ }
+
+ @Override
+ protected int getChildDrawingOrder(int childCount, int i) {
+ int selectedIndex = mSelectedPosition - mFirstPosition;
+
+ // Just to be safe
+ if (selectedIndex < 0) return i;
+
+ if (i == childCount - 1) {
+ // Draw the selected child last
+ return selectedIndex;
+ } else if (i >= selectedIndex) {
+ // Move the children to the right of the selected child earlier one
+ return i + 1;
+ } else {
+ // Keep the children to the left of the selected child the same
+ return i;
+ }
+ }
+
+ @Override
+ protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
+ super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
+
+ /*
+ * The gallery shows focus by focusing the selected item. So, give
+ * focus to our selected item instead. We steal keys from our
+ * selected item elsewhere.
+ */
+ if (gainFocus && mSelectedChild != null) {
+ mSelectedChild.requestFocus(direction);
+ }
+
+ }
+
+ /**
+ * Responsible for fling behavior. Use {@link #startUsingVelocity(int)} to
+ * initiate a fling. Each frame of the fling is handled in {@link #run()}.
+ * A FlingRunnable will keep re-posting itself until the fling is done.
+ *
+ */
+ private class FlingRunnable implements Runnable {
+ /**
+ * Tracks the decay of a fling scroll
+ */
+ private Scroller mScroller;
+
+ /**
+ * X value reported by mScroller on the previous fling
+ */
+ private int mLastFlingX;
+
+ public FlingRunnable() {
+ mScroller = new Scroller(getContext());
+ }
+
+ private void startCommon() {
+ // Remove any pending flings
+ removeCallbacks(this);
+ }
+
+ public void startUsingVelocity(int initialVelocity) {
+ if (initialVelocity == 0) return;
+
+ startCommon();
+
+ int initialX = initialVelocity < 0 ? Integer.MAX_VALUE : 0;
+ mLastFlingX = initialX;
+ mScroller.fling(initialX, 0, initialVelocity, 0,
+ 0, Integer.MAX_VALUE, 0, Integer.MAX_VALUE);
+ post(this);
+ }
+
+ public void startUsingDistance(int distance) {
+ if (distance == 0) return;
+
+ startCommon();
+
+ mLastFlingX = 0;
+ mScroller.startScroll(0, 0, -distance, 0, mAnimationDuration);
+ post(this);
+ }
+
+ public void stop(boolean scrollIntoSlots) {
+ removeCallbacks(this);
+ endFling(scrollIntoSlots);
+ }
+
+ private void endFling(boolean scrollIntoSlots) {
+ /*
+ * Force the scroller's status to finished (without setting its
+ * position to the end)
+ */
+ mScroller.forceFinished(true);
+
+ if (scrollIntoSlots) scrollIntoSlots();
+ }
+
+ public void run() {
+
+ if (mItemCount == 0) {
+ endFling(true);
+ return;
+ }
+
+ mShouldStopFling = false;
+
+ final Scroller scroller = mScroller;
+ boolean more = scroller.computeScrollOffset();
+ final int x = scroller.getCurrX();
+
+ // Flip sign to convert finger direction to list items direction
+ // (e.g. finger moving down means list is moving towards the top)
+ int delta = mLastFlingX - x;
+
+ // Pretend that each frame of a fling scroll is a touch scroll
+ if (delta > 0) {
+ // Moving towards the left. Use first view as mDownTouchPosition
+ mDownTouchPosition = mFirstPosition;
+
+ // Don't fling more than 1 screen
+ delta = Math.min(getWidth() - mPaddingLeft - mPaddingRight - 1, delta);
+ } else {
+ // Moving towards the right. Use last view as mDownTouchPosition
+ int offsetToLast = getChildCount() - 1;
+ mDownTouchPosition = mFirstPosition + offsetToLast;
+
+ // Don't fling more than 1 screen
+ delta = Math.max(-(getWidth() - mPaddingRight - mPaddingLeft - 1), delta);
+ }
+
+ trackMotionScroll(delta);
+
+ if (more && !mShouldStopFling) {
+ mLastFlingX = x;
+ post(this);
+ } else {
+ endFling(true);
+ }
+ }
+
+ }
+
+ /**
+ * Gallery extends LayoutParams to provide a place to hold current
+ * Transformation information along with previous position/transformation
+ * info.
+ *
+ */
+ public static class LayoutParams extends ViewGroup.LayoutParams {
+ public LayoutParams(Context c, AttributeSet attrs) {
+ super(c, attrs);
+ }
+
+ public LayoutParams(int w, int h) {
+ super(w, h);
+ }
+
+ public LayoutParams(ViewGroup.LayoutParams source) {
+ super(source);
+ }
+ }
+}
diff --git a/core/java/android/widget/GridView.java b/core/java/android/widget/GridView.java
new file mode 100644
index 0000000..268bf84
--- /dev/null
+++ b/core/java/android/widget/GridView.java
@@ -0,0 +1,1828 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.SoundEffectConstants;
+import android.view.animation.GridLayoutAnimationController;
+
+
+/**
+ * A view that shows items in two-dimensional scrolling grid. The items in the
+ * grid come from the {@link ListAdapter} associated with this view.
+ */
+public class GridView extends AbsListView {
+ public static final int NO_STRETCH = 0;
+ public static final int STRETCH_SPACING = 1;
+ public static final int STRETCH_COLUMN_WIDTH = 2;
+
+ public static final int AUTO_FIT = -1;
+
+ private int mNumColumns = AUTO_FIT;
+
+ private int mHorizontalSpacing = 0;
+ private int mRequestedHorizontalSpacing;
+ private int mVerticalSpacing = 0;
+ private int mStretchMode = STRETCH_COLUMN_WIDTH;
+ private int mColumnWidth;
+ private int mRequestedColumnWidth;
+ private int mRequestedNumColumns;
+
+ private View mReferenceView = null;
+ private View mReferenceViewInSelectedRow = null;
+
+ private int mGravity = Gravity.LEFT;
+
+ private final Rect mTempRect = new Rect();
+
+ public GridView(Context context) {
+ super(context);
+ }
+
+ public GridView(Context context, AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.gridViewStyle);
+ }
+
+ public GridView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ TypedArray a = context.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.GridView, defStyle, 0);
+
+ int hSpacing = a.getDimensionPixelOffset(
+ com.android.internal.R.styleable.GridView_horizontalSpacing, 0);
+ setHorizontalSpacing(hSpacing);
+
+ int vSpacing = a.getDimensionPixelOffset(
+ com.android.internal.R.styleable.GridView_verticalSpacing, 0);
+ setVerticalSpacing(vSpacing);
+
+ int index = a.getInt(com.android.internal.R.styleable.GridView_stretchMode, STRETCH_COLUMN_WIDTH);
+ if (index >= 0) {
+ setStretchMode(index);
+ }
+
+ int columnWidth = a.getDimensionPixelOffset(com.android.internal.R.styleable.GridView_columnWidth, -1);
+ if (columnWidth > 0) {
+ setColumnWidth(columnWidth);
+ }
+
+ int numColumns = a.getInt(com.android.internal.R.styleable.GridView_numColumns, 1);
+ setNumColumns(numColumns);
+
+ index = a.getInt(com.android.internal.R.styleable.GridView_gravity, -1);
+ if (index >= 0) {
+ setGravity(index);
+ }
+
+ a.recycle();
+ }
+
+ @Override
+ public ListAdapter getAdapter() {
+ return mAdapter;
+ }
+
+ /**
+ * Sets the data behind this GridView.
+ *
+ * @param adapter the adapter providing the grid's data
+ */
+ @Override
+ public void setAdapter(ListAdapter adapter) {
+ if (null != mAdapter) {
+ mAdapter.unregisterDataSetObserver(mDataSetObserver);
+ }
+
+ resetList();
+ mRecycler.clear();
+ mAdapter = adapter;
+
+ mOldSelectedPosition = INVALID_POSITION;
+ mOldSelectedRowId = INVALID_ROW_ID;
+
+ if (mAdapter != null) {
+ mOldItemCount = mItemCount;
+ mItemCount = mAdapter.getCount();
+ mDataChanged = true;
+ checkFocus();
+
+ mDataSetObserver = new AdapterDataSetObserver();
+ mAdapter.registerDataSetObserver(mDataSetObserver);
+
+ mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());
+
+ int position;
+ if (mStackFromBottom) {
+ position = lookForSelectablePosition(mItemCount - 1, false);
+ } else {
+ position = lookForSelectablePosition(0, true);
+ }
+ setSelectedPositionInt(position);
+ setNextSelectedPositionInt(position);
+ checkSelectionChanged();
+ } else {
+ checkFocus();
+ // Nothing selected
+ checkSelectionChanged();
+ }
+
+ requestLayout();
+ }
+
+ @Override
+ int lookForSelectablePosition(int position, boolean lookDown) {
+ final ListAdapter adapter = mAdapter;
+ if (adapter == null || isInTouchMode()) {
+ return INVALID_POSITION;
+ }
+
+ if (position < 0 || position >= mItemCount) {
+ return INVALID_POSITION;
+ }
+ return position;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ void fillGap(boolean down) {
+ final int numColumns = mNumColumns;
+ final int verticalSpacing = mVerticalSpacing;
+
+ final int count = getChildCount();
+
+ if (down) {
+ final int startOffset = count > 0 ?
+ getChildAt(count - 1).getBottom() + verticalSpacing : getListPaddingTop();
+ int position = mFirstPosition + count;
+ if (mStackFromBottom) {
+ position += numColumns - 1;
+ }
+ fillDown(position, startOffset);
+ correctTooHigh(numColumns, verticalSpacing, getChildCount());
+ } else {
+ final int startOffset = count > 0 ?
+ getChildAt(0).getTop() - verticalSpacing : getHeight() - getListPaddingBottom();
+ int position = mFirstPosition;
+ if (!mStackFromBottom) {
+ position -= numColumns;
+ } else {
+ position--;
+ }
+ fillUp(position, startOffset);
+ correctTooLow(numColumns, verticalSpacing, getChildCount());
+ }
+ }
+
+ /**
+ * Fills the list from pos down to the end of the list view.
+ *
+ * @param pos The first position to put in the list
+ *
+ * @param nextTop The location where the top of the item associated with pos
+ * should be drawn
+ *
+ * @return The view that is currently selected, if it happens to be in the
+ * range that we draw.
+ */
+ private View fillDown(int pos, int nextTop) {
+ View selectedView = null;
+
+ final int end = (mBottom - mTop) - mListPadding.bottom;
+
+ while (nextTop < end && pos < mItemCount) {
+ View temp = makeRow(pos, nextTop, true);
+ if (temp != null) {
+ selectedView = temp;
+ }
+
+ nextTop = mReferenceView.getBottom() + mVerticalSpacing;
+
+ pos += mNumColumns;
+ }
+
+ return selectedView;
+ }
+
+ private View makeRow(int startPos, int y, boolean flow) {
+ int last;
+ int nextLeft = mListPadding.left;
+
+ final int columnWidth = mColumnWidth;
+ final int horizontalSpacing = mHorizontalSpacing;
+
+ if (!mStackFromBottom) {
+ last = Math.min(startPos + mNumColumns, mItemCount);
+ } else {
+ last = startPos + 1;
+ startPos = Math.max(0, startPos - mNumColumns + 1);
+
+ if (last - startPos < mNumColumns) {
+ nextLeft += (mNumColumns - (last - startPos)) * (columnWidth + horizontalSpacing);
+ }
+ }
+
+ View selectedView = null;
+
+ final boolean hasFocus = shouldShowSelector();
+ final boolean inClick = touchModeDrawsInPressedState();
+ final int selectedPosition = mSelectedPosition;
+
+ mReferenceView = null;
+
+ for (int pos = startPos; pos < last; pos++) {
+ // is this the selected item?
+ boolean selected = pos == selectedPosition;
+ // does the list view have focus or contain focus
+
+ final int where = flow ? -1 : pos - startPos;
+ final View child = makeAndAddView(pos, y, flow, nextLeft, selected, where);
+ mReferenceView = child;
+
+ nextLeft += columnWidth;
+ if (pos < last - 1) {
+ nextLeft += horizontalSpacing;
+ }
+
+ if (selected && (hasFocus || inClick)) {
+ selectedView = child;
+ }
+ }
+
+ if (selectedView != null) {
+ mReferenceViewInSelectedRow = mReferenceView;
+ }
+
+ return selectedView;
+ }
+
+ /**
+ * Fills the list from pos up to the top of the list view.
+ *
+ * @param pos The first position to put in the list
+ *
+ * @param nextBottom The location where the bottom of the item associated
+ * with pos should be drawn
+ *
+ * @return The view that is currently selected
+ */
+ private View fillUp(int pos, int nextBottom) {
+ View selectedView = null;
+
+ final int end = mListPadding.top;
+
+ while (nextBottom > end && pos >= 0) {
+
+ View temp = makeRow(pos, nextBottom, false);
+ if (temp != null) {
+ selectedView = temp;
+ }
+
+ nextBottom = mReferenceView.getTop() - mVerticalSpacing;
+
+ mFirstPosition = pos;
+
+ pos -= mNumColumns;
+ }
+
+ if (mStackFromBottom) {
+ mFirstPosition = Math.max(0, pos + 1);
+ }
+
+ return selectedView;
+ }
+
+ /**
+ * Fills the list from top to bottom, starting with mFirstPosition
+ *
+ * @param nextTop The location where the top of the first item should be
+ * drawn
+ *
+ * @return The view that is currently selected
+ */
+ private View fillFromTop(int nextTop) {
+ mFirstPosition = Math.min(mFirstPosition, mSelectedPosition);
+ mFirstPosition = Math.min(mFirstPosition, mItemCount - 1);
+ if (mFirstPosition < 0) {
+ mFirstPosition = 0;
+ }
+ mFirstPosition -= mFirstPosition % mNumColumns;
+ return fillDown(mFirstPosition, nextTop);
+ }
+
+ private View fillFromBottom(int lastPosition, int nextBottom) {
+ lastPosition = Math.max(lastPosition, mSelectedPosition);
+ lastPosition = Math.min(lastPosition, mItemCount - 1);
+
+ final int invertedPosition = mItemCount - 1 - lastPosition;
+ lastPosition = mItemCount - 1 - (invertedPosition - (invertedPosition % mNumColumns));
+
+ return fillUp(lastPosition, nextBottom);
+ }
+
+ private View fillSelection(int childrenTop, int childrenBottom) {
+ final int selectedPosition = reconcileSelectedPosition();
+ final int numColumns = mNumColumns;
+ final int verticalSpacing = mVerticalSpacing;
+
+ int rowStart;
+ int rowEnd = -1;
+
+ if (!mStackFromBottom) {
+ rowStart = selectedPosition - (selectedPosition % numColumns);
+ } else {
+ final int invertedSelection = mItemCount - 1 - selectedPosition;
+
+ rowEnd = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns));
+ rowStart = Math.max(0, rowEnd - numColumns + 1);
+ }
+
+ final int fadingEdgeLength = getVerticalFadingEdgeLength();
+ final int topSelectionPixel = getTopSelectionPixel(childrenTop, fadingEdgeLength, rowStart);
+
+ final View sel = makeRow(mStackFromBottom ? rowEnd : rowStart, topSelectionPixel, true);
+ mFirstPosition = rowStart;
+
+ final View referenceView = mReferenceView;
+
+ if (!mStackFromBottom) {
+ fillDown(rowStart + numColumns, referenceView.getBottom() + verticalSpacing);
+ pinToBottom(childrenBottom);
+ fillUp(rowStart - numColumns, referenceView.getTop() - verticalSpacing);
+ adjustViewsUpOrDown();
+ } else {
+ final int bottomSelectionPixel = getBottomSelectionPixel(childrenBottom,
+ fadingEdgeLength, numColumns, rowStart);
+ final int offset = bottomSelectionPixel - referenceView.getBottom();
+ offsetChildrenTopAndBottom(offset);
+ fillUp(rowStart - 1, referenceView.getTop() - verticalSpacing);
+ pinToTop(childrenTop);
+ fillDown(rowEnd + numColumns, referenceView.getBottom() + verticalSpacing);
+ adjustViewsUpOrDown();
+ }
+
+ return sel;
+ }
+
+ private void pinToTop(int childrenTop) {
+ if (mFirstPosition == 0) {
+ final int top = getChildAt(0).getTop();
+ final int offset = childrenTop - top;
+ if (offset < 0) {
+ offsetChildrenTopAndBottom(offset);
+ }
+ }
+ }
+
+ private void pinToBottom(int childrenBottom) {
+ final int count = getChildCount();
+ if (mFirstPosition + count == mItemCount) {
+ final int bottom = getChildAt(count - 1).getBottom();
+ final int offset = childrenBottom - bottom;
+ if (offset > 0) {
+ offsetChildrenTopAndBottom(offset);
+ }
+ }
+ }
+
+ @Override
+ int findMotionRow(int y) {
+ final int childCount = getChildCount();
+ if (childCount > 0) {
+
+ final int numColumns = mNumColumns;
+ if (!mStackFromBottom) {
+ for (int i = 0; i < childCount; i += numColumns) {
+ if (y <= getChildAt(i).getBottom()) {
+ return mFirstPosition + i;
+ }
+ }
+ } else {
+ for (int i = childCount - 1; i >= 0; i -= numColumns) {
+ if (y >= getChildAt(i).getTop()) {
+ return mFirstPosition + i;
+ }
+ }
+ }
+
+ return mFirstPosition + childCount - 1;
+ }
+ return INVALID_POSITION;
+ }
+
+ /**
+ * Layout during a scroll that results from tracking motion events. Places
+ * the mMotionPosition view at the offset specified by mMotionViewTop, and
+ * then build surrounding views from there.
+ *
+ * @param position the position at which to start filling
+ * @param top the top of the view at that position
+ * @return The selected view, or null if the selected view is outside the
+ * visible area.
+ */
+ private View fillSpecific(int position, int top) {
+ final int numColumns = mNumColumns;
+
+ int motionRowStart;
+ int motionRowEnd = -1;
+
+ if (!mStackFromBottom) {
+ motionRowStart = position - (position % numColumns);
+ } else {
+ final int invertedSelection = mItemCount - 1 - position;
+
+ motionRowEnd = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns));
+ motionRowStart = Math.max(0, motionRowEnd - numColumns + 1);
+ }
+
+ final View temp = makeRow(mStackFromBottom ? motionRowEnd : motionRowStart, top, true);
+
+ // Possibly changed again in fillUp if we add rows above this one.
+ mFirstPosition = motionRowStart;
+
+ final View referenceView = mReferenceView;
+ final int verticalSpacing = mVerticalSpacing;
+
+ View above;
+ View below;
+
+ if (!mStackFromBottom) {
+ above = fillUp(motionRowStart - numColumns, referenceView.getTop() - verticalSpacing);
+ adjustViewsUpOrDown();
+ below = fillDown(motionRowStart + numColumns, referenceView.getBottom() + verticalSpacing);
+ // Check if we have dragged the bottom of the grid too high
+ final int childCount = getChildCount();
+ if (childCount > 0) {
+ correctTooHigh(numColumns, verticalSpacing, childCount);
+ }
+ } else {
+ below = fillDown(motionRowEnd + numColumns, referenceView.getBottom() + verticalSpacing);
+ adjustViewsUpOrDown();
+ above = fillUp(motionRowStart - 1, referenceView.getTop() - verticalSpacing);
+ // Check if we have dragged the bottom of the grid too high
+ final int childCount = getChildCount();
+ if (childCount > 0) {
+ correctTooLow(numColumns, verticalSpacing, childCount);
+ }
+ }
+
+ if (temp != null) {
+ return temp;
+ } else if (above != null) {
+ return above;
+ } else {
+ return below;
+ }
+ }
+
+ private void correctTooHigh(int numColumns, int verticalSpacing, int childCount) {
+ // First see if the last item is visible
+ final int lastPosition = mFirstPosition + childCount - 1;
+ if (lastPosition == mItemCount - 1 && childCount > 0) {
+ // Get the last child ...
+ final View lastChild = getChildAt(childCount - 1);
+
+ // ... and its bottom edge
+ final int lastBottom = lastChild.getBottom();
+ // This is bottom of our drawable area
+ final int end = (mBottom - mTop) - mListPadding.bottom;
+
+ // This is how far the bottom edge of the last view is from the bottom of the
+ // drawable area
+ int bottomOffset = end - lastBottom;
+
+ final View firstChild = getChildAt(0);
+ final int firstTop = firstChild.getTop();
+
+ // Make sure we are 1) Too high, and 2) Either there are more rows above the
+ // first row or the first row is scrolled off the top of the drawable area
+ if (bottomOffset > 0 && (mFirstPosition > 0 || firstTop < mListPadding.top)) {
+ if (mFirstPosition == 0) {
+ // Don't pull the top too far down
+ bottomOffset = Math.min(bottomOffset, mListPadding.top - firstTop);
+ }
+
+ // Move everything down
+ offsetChildrenTopAndBottom(bottomOffset);
+ if (mFirstPosition > 0) {
+ // Fill the gap that was opened above mFirstPosition with more rows, if
+ // possible
+ fillUp(mFirstPosition - (mStackFromBottom ? 1 : numColumns),
+ firstChild.getTop() - verticalSpacing);
+ // Close up the remaining gap
+ adjustViewsUpOrDown();
+ }
+ }
+ }
+ }
+
+ private void correctTooLow(int numColumns, int verticalSpacing, int childCount) {
+ if (mFirstPosition == 0 && childCount > 0) {
+ // Get the first child ...
+ final View firstChild = getChildAt(0);
+
+ // ... and its top edge
+ final int firstTop = firstChild.getTop();
+
+ // This is top of our drawable area
+ final int start = mListPadding.top;
+
+ // This is bottom of our drawable area
+ final int end = (mBottom - mTop) - mListPadding.bottom;
+
+ // This is how far the top edge of the first view is from the top of the
+ // drawable area
+ int topOffset = firstTop - start;
+ final View lastChild = getChildAt(childCount - 1);
+ final int lastBottom = lastChild.getBottom();
+ final int lastPosition = mFirstPosition + childCount - 1;
+
+ // Make sure we are 1) Too low, and 2) Either there are more rows below the
+ // last row or the last row is scrolled off the bottom of the drawable area
+ if (topOffset > 0 && (lastPosition < mItemCount - 1 || lastBottom > end)) {
+ if (lastPosition == mItemCount - 1 ) {
+ // Don't pull the bottom too far up
+ topOffset = Math.min(topOffset, lastBottom - end);
+ }
+
+ // Move everything up
+ offsetChildrenTopAndBottom(-topOffset);
+ if (lastPosition < mItemCount - 1) {
+ // Fill the gap that was opened below the last position with more rows, if
+ // possible
+ fillDown(lastPosition + (!mStackFromBottom ? 1 : numColumns),
+ lastChild.getBottom() + verticalSpacing);
+ // Close up the remaining gap
+ adjustViewsUpOrDown();
+ }
+ }
+ }
+ }
+
+ /**
+ * Fills the grid based on positioning the new selection at a specific
+ * location. The selection may be moved so that it does not intersect the
+ * faded edges. The grid is then filled upwards and downwards from there.
+ *
+ * @param selectedTop Where the selected item should be
+ * @param childrenTop Where to start drawing children
+ * @param childrenBottom Last pixel where children can be drawn
+ * @return The view that currently has selection
+ */
+ private View fillFromSelection(int selectedTop, int childrenTop, int childrenBottom) {
+ final int fadingEdgeLength = getVerticalFadingEdgeLength();
+ final int selectedPosition = mSelectedPosition;
+ final int numColumns = mNumColumns;
+ final int verticalSpacing = mVerticalSpacing;
+
+ int rowStart;
+ int rowEnd = -1;
+
+ if (!mStackFromBottom) {
+ rowStart = selectedPosition - (selectedPosition % numColumns);
+ } else {
+ int invertedSelection = mItemCount - 1 - selectedPosition;
+
+ rowEnd = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns));
+ rowStart = Math.max(0, rowEnd - numColumns + 1);
+ }
+
+ View sel;
+ View referenceView;
+
+ int topSelectionPixel = getTopSelectionPixel(childrenTop, fadingEdgeLength, rowStart);
+ int bottomSelectionPixel = getBottomSelectionPixel(childrenBottom, fadingEdgeLength,
+ numColumns, rowStart);
+
+ sel = makeRow(mStackFromBottom ? rowEnd : rowStart, selectedTop, true);
+ // Possibly changed again in fillUp if we add rows above this one.
+ mFirstPosition = rowStart;
+
+ referenceView = mReferenceView;
+ adjustForTopFadingEdge(referenceView, topSelectionPixel, bottomSelectionPixel);
+ adjustForBottomFadingEdge(referenceView, topSelectionPixel, bottomSelectionPixel);
+
+ if (!mStackFromBottom) {
+ fillUp(rowStart - numColumns, referenceView.getTop() - verticalSpacing);
+ adjustViewsUpOrDown();
+ fillDown(rowStart + numColumns, referenceView.getBottom() + verticalSpacing);
+ } else {
+ fillDown(rowEnd + numColumns, referenceView.getBottom() + verticalSpacing);
+ adjustViewsUpOrDown();
+ fillUp(rowStart - 1, referenceView.getTop() - verticalSpacing);
+ }
+
+
+ return sel;
+ }
+
+ /**
+ * Calculate the bottom-most pixel we can draw the selection into
+ *
+ * @param childrenBottom Bottom pixel were children can be drawn
+ * @param fadingEdgeLength Length of the fading edge in pixels, if present
+ * @param numColumns Number of columns in the grid
+ * @param rowStart The start of the row that will contain the selection
+ * @return The bottom-most pixel we can draw the selection into
+ */
+ private int getBottomSelectionPixel(int childrenBottom, int fadingEdgeLength,
+ int numColumns, int rowStart) {
+ // Last pixel we can draw the selection into
+ int bottomSelectionPixel = childrenBottom;
+ if (rowStart + numColumns - 1 < mItemCount - 1) {
+ bottomSelectionPixel -= fadingEdgeLength;
+ }
+ return bottomSelectionPixel;
+ }
+
+ /**
+ * Calculate the top-most pixel we can draw the selection into
+ *
+ * @param childrenTop Top pixel were children can be drawn
+ * @param fadingEdgeLength Length of the fading edge in pixels, if present
+ * @param rowStart The start of the row that will contain the selection
+ * @return The top-most pixel we can draw the selection into
+ */
+ private int getTopSelectionPixel(int childrenTop, int fadingEdgeLength, int rowStart) {
+ // first pixel we can draw the selection into
+ int topSelectionPixel = childrenTop;
+ if (rowStart > 0) {
+ topSelectionPixel += fadingEdgeLength;
+ }
+ return topSelectionPixel;
+ }
+
+ /**
+ * Move all views upwards so the selected row does not interesect the bottom
+ * fading edge (if necessary).
+ *
+ * @param childInSelectedRow A child in the row that contains the selection
+ * @param topSelectionPixel The topmost pixel we can draw the selection into
+ * @param bottomSelectionPixel The bottommost pixel we can draw the
+ * selection into
+ */
+ private void adjustForBottomFadingEdge(View childInSelectedRow,
+ int topSelectionPixel, int bottomSelectionPixel) {
+ // Some of the newly selected item extends below the bottom of the
+ // list
+ if (childInSelectedRow.getBottom() > bottomSelectionPixel) {
+
+ // Find space available above the selection into which we can
+ // scroll upwards
+ int spaceAbove = childInSelectedRow.getTop() - topSelectionPixel;
+
+ // Find space required to bring the bottom of the selected item
+ // fully into view
+ int spaceBelow = childInSelectedRow.getBottom() - bottomSelectionPixel;
+ int offset = Math.min(spaceAbove, spaceBelow);
+
+ // Now offset the selected item to get it into view
+ offsetChildrenTopAndBottom(-offset);
+ }
+ }
+
+ /**
+ * Move all views upwards so the selected row does not interesect the top
+ * fading edge (if necessary).
+ *
+ * @param childInSelectedRow A child in the row that contains the selection
+ * @param topSelectionPixel The topmost pixel we can draw the selection into
+ * @param bottomSelectionPixel The bottommost pixel we can draw the
+ * selection into
+ */
+ private void adjustForTopFadingEdge(View childInSelectedRow,
+ int topSelectionPixel, int bottomSelectionPixel) {
+ // Some of the newly selected item extends above the top of the list
+ if (childInSelectedRow.getTop() < topSelectionPixel) {
+ // Find space required to bring the top of the selected item
+ // fully into view
+ int spaceAbove = topSelectionPixel - childInSelectedRow.getTop();
+
+ // Find space available below the selection into which we can
+ // scroll downwards
+ int spaceBelow = bottomSelectionPixel - childInSelectedRow.getBottom();
+ int offset = Math.min(spaceAbove, spaceBelow);
+
+ // Now offset the selected item to get it into view
+ offsetChildrenTopAndBottom(offset);
+ }
+ }
+
+ /**
+ * Fills the grid based on positioning the new selection relative to the old
+ * selection. The new selection will be placed at, above, or below the
+ * location of the new selection depending on how the selection is moving.
+ * The selection will then be pinned to the visible part of the screen,
+ * excluding the edges that are faded. The grid is then filled upwards and
+ * downwards from there.
+ *
+ * @param delta Which way we are moving
+ * @param childrenTop Where to start drawing children
+ * @param childrenBottom Last pixel where children can be drawn
+ * @return The view that currently has selection
+ */
+ private View moveSelection(int delta, int childrenTop, int childrenBottom) {
+ final int fadingEdgeLength = getVerticalFadingEdgeLength();
+ final int selectedPosition = mSelectedPosition;
+ final int numColumns = mNumColumns;
+ final int verticalSpacing = mVerticalSpacing;
+
+ int oldRowStart;
+ int rowStart;
+ int rowEnd = -1;
+
+ if (!mStackFromBottom) {
+ oldRowStart = (selectedPosition - delta) - ((selectedPosition - delta) % numColumns);
+
+ rowStart = selectedPosition - (selectedPosition % numColumns);
+ } else {
+ int invertedSelection = mItemCount - 1 - selectedPosition;
+
+ rowEnd = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns));
+ rowStart = Math.max(0, rowEnd - numColumns + 1);
+
+ invertedSelection = mItemCount - 1 - (selectedPosition - delta);
+ oldRowStart = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns));
+ oldRowStart = Math.max(0, oldRowStart - numColumns + 1);
+ }
+
+ final int rowDelta = rowStart - oldRowStart;
+
+ final int topSelectionPixel = getTopSelectionPixel(childrenTop, fadingEdgeLength, rowStart);
+ final int bottomSelectionPixel = getBottomSelectionPixel(childrenBottom, fadingEdgeLength,
+ numColumns, rowStart);
+
+ // Possibly changed again in fillUp if we add rows above this one.
+ mFirstPosition = rowStart;
+
+ View sel;
+ View referenceView;
+
+ if (rowDelta > 0) {
+ /*
+ * Case 1: Scrolling down.
+ */
+
+ final int oldBottom = mReferenceViewInSelectedRow == null ? 0 :
+ mReferenceViewInSelectedRow.getBottom();
+
+ sel = makeRow(mStackFromBottom ? rowEnd : rowStart, oldBottom + verticalSpacing, true);
+ referenceView = mReferenceView;
+
+ adjustForBottomFadingEdge(referenceView, topSelectionPixel, bottomSelectionPixel);
+ } else if (rowDelta < 0) {
+ /*
+ * Case 2: Scrolling up.
+ */
+ final int oldTop = mReferenceViewInSelectedRow == null ?
+ 0 : mReferenceViewInSelectedRow .getTop();
+
+ sel = makeRow(mStackFromBottom ? rowEnd : rowStart, oldTop - verticalSpacing, false);
+ referenceView = mReferenceView;
+
+ adjustForTopFadingEdge(referenceView, topSelectionPixel, bottomSelectionPixel);
+ } else {
+ /*
+ * Keep selection where it was
+ */
+ final int oldTop = mReferenceViewInSelectedRow == null ?
+ 0 : mReferenceViewInSelectedRow .getTop();
+
+ sel = makeRow(mStackFromBottom ? rowEnd : rowStart, oldTop, true);
+ referenceView = mReferenceView;
+ }
+
+ if (!mStackFromBottom) {
+ fillUp(rowStart - numColumns, referenceView.getTop() - verticalSpacing);
+ adjustViewsUpOrDown();
+ fillDown(rowStart + numColumns, referenceView.getBottom() + verticalSpacing);
+ } else {
+ fillDown(rowEnd + numColumns, referenceView.getBottom() + verticalSpacing);
+ adjustViewsUpOrDown();
+ fillUp(rowStart - 1, referenceView.getTop() - verticalSpacing);
+ }
+
+ return sel;
+ }
+
+ private void determineColumns(int availableSpace) {
+ final int requestedHorizontalSpacing = mRequestedHorizontalSpacing;
+ final int stretchMode = mStretchMode;
+ final int requestedColumnWidth = mRequestedColumnWidth;
+
+ if (mRequestedNumColumns == AUTO_FIT) {
+ if (requestedColumnWidth > 0) {
+ // Client told us to pick the number of columns
+ mNumColumns = (availableSpace + requestedHorizontalSpacing) /
+ (requestedColumnWidth + requestedHorizontalSpacing);
+ } else {
+ // Just make up a number if we don't have enough info
+ mNumColumns = 2;
+ }
+ } else {
+ // We picked the columns
+ mNumColumns = mRequestedNumColumns;
+ }
+
+ if (mNumColumns <= 0) {
+ mNumColumns = 1;
+ }
+
+ switch (stretchMode) {
+ case NO_STRETCH:
+ // Nobody stretches
+ mColumnWidth = requestedColumnWidth;
+ mHorizontalSpacing = requestedHorizontalSpacing;
+ break;
+
+ default:
+ int spaceLeftOver = availableSpace - (mNumColumns * requestedColumnWidth) -
+ ((mNumColumns - 1) * requestedHorizontalSpacing);
+ switch (stretchMode) {
+ case STRETCH_COLUMN_WIDTH:
+ // Stretch the columns
+ mColumnWidth = requestedColumnWidth + spaceLeftOver / mNumColumns;
+ mHorizontalSpacing = requestedHorizontalSpacing;
+ break;
+
+ case STRETCH_SPACING:
+ // Stretch the spacing between columns
+ mColumnWidth = requestedColumnWidth;
+ if (mNumColumns > 1) {
+ mHorizontalSpacing = requestedHorizontalSpacing +
+ spaceLeftOver / (mNumColumns - 1);
+ } else {
+ mHorizontalSpacing = requestedHorizontalSpacing + spaceLeftOver;
+ }
+ break;
+ }
+
+ break;
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ // Sets up mListPadding
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+ int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+
+ if (widthMode == MeasureSpec.UNSPECIFIED) {
+ if (mColumnWidth > 0) {
+ widthSize = mColumnWidth + mListPadding.left + mListPadding.right;
+ } else {
+ widthSize = mListPadding.left + mListPadding.right;
+ }
+ widthSize += getVerticalScrollbarWidth();
+ }
+
+ int childWidth = widthSize - mListPadding.left - mListPadding.right;
+ determineColumns(childWidth);
+
+ int childHeight = 0;
+
+ mItemCount = mAdapter == null ? 0 : mAdapter.getCount();
+ final int count = mItemCount;
+ if (count > 0) {
+ final View child = obtainView(0);
+ final int childViewType = mAdapter.getItemViewType(0);
+
+ AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams();
+ if (lp == null) {
+ lp = new AbsListView.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT, 0);
+ child.setLayoutParams(lp);
+ }
+ lp.viewType = childViewType;
+
+ final int childWidthSpec = ViewGroup.getChildMeasureSpec(widthMeasureSpec,
+ mListPadding.left + mListPadding.right, lp.width);
+
+ int lpHeight = lp.height;
+
+ int childHeightSpec;
+ if (lpHeight > 0) {
+ childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
+ } else {
+ childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ }
+
+ child.measure(childWidthSpec, childHeightSpec);
+ childHeight = child.getMeasuredHeight();
+
+ if (mRecycler.shouldRecycleViewType(childViewType)) {
+ mRecycler.addScrapView(child);
+ }
+ }
+
+ if (heightMode == MeasureSpec.UNSPECIFIED) {
+ heightSize = mListPadding.top + mListPadding.bottom + childHeight +
+ getVerticalFadingEdgeLength() * 2;
+ }
+
+ if (heightMode == MeasureSpec.AT_MOST) {
+ int ourSize = mListPadding.top + mListPadding.bottom;
+
+ final int numColumns = mNumColumns;
+ for (int i = 0; i < count; i += numColumns) {
+ ourSize += childHeight;
+ if (i + numColumns < count) {
+ ourSize += mVerticalSpacing;
+ }
+ if (ourSize >= heightSize) {
+ ourSize = heightSize;
+ break;
+ }
+ }
+ heightSize = ourSize;
+ }
+
+ setMeasuredDimension(widthSize, heightSize);
+ mWidthMeasureSpec = widthMeasureSpec;
+ }
+
+ @Override
+ protected void attachLayoutAnimationParameters(View child,
+ ViewGroup.LayoutParams params, int index, int count) {
+
+ GridLayoutAnimationController.AnimationParameters animationParams =
+ (GridLayoutAnimationController.AnimationParameters) params.layoutAnimationParameters;
+
+ if (animationParams == null) {
+ animationParams = new GridLayoutAnimationController.AnimationParameters();
+ params.layoutAnimationParameters = animationParams;
+ }
+
+ animationParams.count = count;
+ animationParams.index = index;
+ animationParams.columnsCount = mNumColumns;
+ animationParams.rowsCount = count / mNumColumns;
+
+ if (!mStackFromBottom) {
+ animationParams.column = index % mNumColumns;
+ animationParams.row = index / mNumColumns;
+ } else {
+ final int invertedIndex = count - 1 - index;
+
+ animationParams.column = mNumColumns - 1 - (invertedIndex % mNumColumns);
+ animationParams.row = animationParams.rowsCount - 1 - invertedIndex / mNumColumns;
+ }
+ }
+
+ @Override
+ protected void layoutChildren() {
+ final boolean blockLayoutRequests = mBlockLayoutRequests;
+ if (!blockLayoutRequests) {
+ mBlockLayoutRequests = true;
+ }
+
+ try {
+ super.layoutChildren();
+
+ invalidate();
+
+ if (mAdapter == null) {
+ resetList();
+ invokeOnItemScrollListener();
+ return;
+ }
+
+ final int childrenTop = mListPadding.top;
+ final int childrenBottom = mBottom - mTop - mListPadding.bottom;
+
+ int childCount = getChildCount();
+ int index;
+ int delta = 0;
+
+ View sel;
+ View oldSel = null;
+ View oldFirst = null;
+ View newSel = null;
+
+ // Remember stuff we will need down below
+ switch (mLayoutMode) {
+ case LAYOUT_SET_SELECTION:
+ index = mNextSelectedPosition - mFirstPosition;
+ if (index >= 0 && index < childCount) {
+ newSel = getChildAt(index);
+ }
+ break;
+ case LAYOUT_FORCE_TOP:
+ case LAYOUT_FORCE_BOTTOM:
+ case LAYOUT_SPECIFIC:
+ case LAYOUT_SYNC:
+ break;
+ case LAYOUT_MOVE_SELECTION:
+ if (mNextSelectedPosition >= 0) {
+ delta = mNextSelectedPosition - mSelectedPosition;
+ }
+ break;
+ default:
+ // Remember the previously selected view
+ index = mSelectedPosition - mFirstPosition;
+ if (index >= 0 && index < childCount) {
+ oldSel = getChildAt(index);
+ }
+
+ // Remember the previous first child
+ oldFirst = getChildAt(0);
+ }
+
+ boolean dataChanged = mDataChanged;
+ if (dataChanged) {
+ handleDataChanged();
+ }
+
+ // Handle the empty set by removing all views that are visible
+ // and calling it a day
+ if (mItemCount == 0) {
+ resetList();
+ invokeOnItemScrollListener();
+ return;
+ }
+
+ setSelectedPositionInt(mNextSelectedPosition);
+
+ // Pull all children into the RecycleBin.
+ // These views will be reused if possible
+ final int firstPosition = mFirstPosition;
+ final RecycleBin recycleBin = mRecycler;
+
+ if (dataChanged) {
+ for (int i = 0; i < childCount; i++) {
+ recycleBin.addScrapView(getChildAt(i));
+ }
+ } else {
+ recycleBin.fillActiveViews(childCount, firstPosition);
+ }
+
+ // Clear out old views
+ //removeAllViewsInLayout();
+ detachAllViewsFromParent();
+
+ switch (mLayoutMode) {
+ case LAYOUT_SET_SELECTION:
+ if (newSel != null) {
+ sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom);
+ } else {
+ sel = fillSelection(childrenTop, childrenBottom);
+ }
+ break;
+ case LAYOUT_FORCE_TOP:
+ mFirstPosition = 0;
+ sel = fillFromTop(childrenTop);
+ adjustViewsUpOrDown();
+ break;
+ case LAYOUT_FORCE_BOTTOM:
+ sel = fillUp(mItemCount - 1, childrenBottom);
+ adjustViewsUpOrDown();
+ break;
+ case LAYOUT_SPECIFIC:
+ sel = fillSpecific(mSelectedPosition, mSpecificTop);
+ break;
+ case LAYOUT_SYNC:
+ sel = fillSpecific(mSyncPosition, mSpecificTop);
+ break;
+ case LAYOUT_MOVE_SELECTION:
+ // Move the selection relative to its old position
+ sel = moveSelection(delta, childrenTop, childrenBottom);
+ break;
+ default:
+ if (childCount == 0) {
+ if (!mStackFromBottom) {
+ setSelectedPositionInt(0);
+ sel = fillFromTop(childrenTop);
+ } else {
+ final int last = mItemCount - 1;
+ setSelectedPositionInt(last);
+ sel = fillFromBottom(last, childrenBottom);
+ }
+ } else {
+ if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
+ sel = fillSpecific(mSelectedPosition, oldSel == null ?
+ childrenTop : oldSel.getTop());
+ } else if (mFirstPosition < mItemCount) {
+ sel = fillSpecific(mFirstPosition, oldFirst == null ?
+ childrenTop : oldFirst.getTop());
+ } else {
+ sel = fillSpecific(0, childrenTop);
+ }
+ }
+ break;
+ }
+
+ // Flush any cached views that did not get reused above
+ recycleBin.scrapActiveViews();
+
+ if (sel != null) {
+ positionSelector(sel);
+ mSelectedTop = sel.getTop();
+ } else {
+ mSelectedTop = 0;
+ mSelectorRect.setEmpty();
+ }
+
+ mLayoutMode = LAYOUT_NORMAL;
+ mDataChanged = false;
+ mNeedSync = false;
+ setNextSelectedPositionInt(mSelectedPosition);
+
+ updateScrollIndicators();
+
+ if (mItemCount > 0) {
+ checkSelectionChanged();
+ }
+
+ invokeOnItemScrollListener();
+ } finally {
+ if (!blockLayoutRequests) {
+ mBlockLayoutRequests = false;
+ }
+ }
+ }
+
+
+ /**
+ * Obtain the view and add it to our list of children. The view can be made
+ * fresh, converted from an unused view, or used as is if it was in the
+ * recycle bin.
+ *
+ * @param position Logical position in the list
+ * @param y Top or bottom edge of the view to add
+ * @param flow if true, align top edge to y. If false, align bottom edge to
+ * y.
+ * @param childrenLeft Left edge where children should be positioned
+ * @param selected Is this position selected?
+ * @param where to add new item in the list
+ * @return View that was added
+ */
+ private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
+ boolean selected, int where) {
+ View child;
+
+ if (!mDataChanged) {
+ // Try to use an exsiting view for this position
+ child = mRecycler.getActiveView(position);
+ if (child != null) {
+ // Found it -- we're using an existing child
+ // This just needs to be positioned
+ setupChild(child, position, y, flow, childrenLeft, selected, true, where);
+ return child;
+ }
+ }
+
+ // Make a new view for this position, or convert an unused view if
+ // possible
+ child = obtainView(position);
+
+ // This needs to be positioned and measured
+ setupChild(child, position, y, flow, childrenLeft, selected, false, where);
+
+ return child;
+ }
+
+ /**
+ * Add a view as a child and make sure it is measured (if necessary) and
+ * positioned properly.
+ *
+ * @param child The view to add
+ * @param position The position of the view
+ * @param y The y position relative to which this view will be positioned
+ * @param flow if true, align top edge to y. If false, align bottom edge
+ * to y.
+ * @param childrenLeft Left edge where children should be positioned
+ * @param selected Is this position selected?
+ * @param recycled Has this view been pulled from the recycle bin? If so it
+ * does not need to be remeasured.
+ * @param where Where to add the item in the list
+ *
+ */
+ private void setupChild(View child, int position, int y, boolean flow, int childrenLeft,
+ boolean selected, boolean recycled, int where) {
+ boolean isSelected = selected && shouldShowSelector();
+
+ final boolean updateChildSelected = isSelected != child.isSelected();
+ boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested();
+
+ // Respect layout params that are already in the view. Otherwise make
+ // some up...
+ AbsListView.LayoutParams p = (AbsListView.LayoutParams)child.getLayoutParams();
+ if (p == null) {
+ p = new AbsListView.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT, 0);
+ }
+ p.viewType = mAdapter.getItemViewType(position);
+
+ if (recycled) {
+ attachViewToParent(child, where, p);
+ } else {
+ addViewInLayout(child, where, p, true);
+ }
+
+ if (updateChildSelected) {
+ child.setSelected(isSelected);
+ if (isSelected) {
+ requestFocus();
+ }
+ }
+
+ if (needToMeasure) {
+ int childHeightSpec = ViewGroup.getChildMeasureSpec(
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 0, p.height);
+
+ int childWidthSpec = ViewGroup.getChildMeasureSpec(
+ MeasureSpec.makeMeasureSpec(mColumnWidth, MeasureSpec.EXACTLY), 0, p.width);
+ child.measure(childWidthSpec, childHeightSpec);
+ } else {
+ cleanupLayoutState(child);
+ }
+
+ final int w = child.getMeasuredWidth();
+ final int h = child.getMeasuredHeight();
+
+ int childLeft;
+ final int childTop = flow ? y : y - h;
+
+ switch (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
+ case Gravity.LEFT:
+ childLeft = childrenLeft;
+ break;
+ case Gravity.CENTER_HORIZONTAL:
+ childLeft = childrenLeft + ((mColumnWidth - w) / 2);
+ break;
+ case Gravity.RIGHT:
+ childLeft = childrenLeft + mColumnWidth - w;
+ break;
+ default:
+ childLeft = childrenLeft;
+ break;
+ }
+
+ if (needToMeasure) {
+ final int childRight = childLeft + w;
+ final int childBottom = childTop + h;
+ child.layout(childLeft, childTop, childRight, childBottom);
+ } else {
+ child.offsetLeftAndRight(childLeft - child.getLeft());
+ child.offsetTopAndBottom(childTop - child.getTop());
+ }
+
+ if (mCachingStarted) {
+ child.setDrawingCacheEnabled(true);
+ }
+ }
+
+ /**
+ * Sets the currently selected item
+ *
+ * @param position Index (starting at 0) of the data item to be selected.
+ *
+ * If in touch mode, the item will not be selected but it will still be positioned
+ * appropriately.
+ */
+ @Override
+ public void setSelection(int position) {
+ if (!isInTouchMode()) {
+ setNextSelectedPositionInt(position);
+ } else {
+ mResurrectToPosition = position;
+ }
+ mLayoutMode = LAYOUT_SET_SELECTION;
+ requestLayout();
+ }
+
+ /**
+ * Makes the item at the supplied position selected.
+ *
+ * @param position the position of the new selection
+ */
+ @Override
+ void setSelectionInt(int position) {
+ mBlockLayoutRequests = true;
+ setNextSelectedPositionInt(position);
+ layoutChildren();
+
+ mBlockLayoutRequests = false;
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ return commonKey(keyCode, 1, event);
+ }
+
+ @Override
+ public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
+ return commonKey(keyCode, repeatCount, event);
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ return commonKey(keyCode, 1, event);
+ }
+
+ private boolean commonKey(int keyCode, int count, KeyEvent event) {
+ if (mAdapter == null) {
+ return false;
+ }
+
+ if (mDataChanged) {
+ layoutChildren();
+ }
+
+ boolean handled = false;
+ int action = event.getAction();
+
+ if (action != KeyEvent.ACTION_UP) {
+ if (mSelectedPosition < 0) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_UP:
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ case KeyEvent.KEYCODE_SPACE:
+ case KeyEvent.KEYCODE_ENTER:
+ resurrectSelection();
+ return true;
+ }
+ }
+
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ handled = arrowScroll(FOCUS_LEFT);
+ break;
+
+
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ handled = arrowScroll(FOCUS_RIGHT);
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_UP:
+ if (!event.isAltPressed()) {
+ handled = arrowScroll(FOCUS_UP);
+
+ } else {
+ handled = fullScroll(FOCUS_UP);
+ }
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ if (!event.isAltPressed()) {
+ handled = arrowScroll(FOCUS_DOWN);
+ } else {
+ handled = fullScroll(FOCUS_DOWN);
+ }
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ case KeyEvent.KEYCODE_ENTER: {
+ if (getChildCount() > 0 && event.getRepeatCount() == 0) {
+ keyPressed();
+ }
+
+ return true;
+ }
+
+ case KeyEvent.KEYCODE_SPACE:
+ if (mPopup == null || !mPopup.isShowing()) {
+ if (!event.isShiftPressed()) {
+ handled = pageScroll(FOCUS_DOWN);
+ } else {
+ handled = pageScroll(FOCUS_UP);
+ }
+ }
+ break;
+ }
+
+ }
+
+ if (!handled) {
+ handled = sendToTextFilter(keyCode, count, event);
+ }
+
+ if (handled) {
+ return true;
+ } else {
+ switch (action) {
+ case KeyEvent.ACTION_DOWN:
+ return super.onKeyDown(keyCode, event);
+ case KeyEvent.ACTION_UP:
+ return super.onKeyUp(keyCode, event);
+ case KeyEvent.ACTION_MULTIPLE:
+ return super.onKeyMultiple(keyCode, count, event);
+ default:
+ return false;
+ }
+ }
+ }
+
+ /**
+ * Scrolls up or down by the number of items currently present on screen.
+ *
+ * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN}
+ * @return whether selection was moved
+ */
+ boolean pageScroll(int direction) {
+ int nextPage = -1;
+
+ if (direction == FOCUS_UP) {
+ nextPage = Math.max(0, mSelectedPosition - getChildCount() - 1);
+ } else if (direction == FOCUS_DOWN) {
+ nextPage = Math.min(mItemCount - 1, mSelectedPosition + getChildCount() - 1);
+ }
+
+ if (nextPage >= 0) {
+ setSelectionInt(nextPage);
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Go to the last or first item if possible.
+ *
+ * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN}.
+ *
+ * @return Whether selection was moved.
+ */
+ boolean fullScroll(int direction) {
+ boolean moved = false;
+ if (direction == FOCUS_UP) {
+ mLayoutMode = LAYOUT_SET_SELECTION;
+ setSelectionInt(0);
+ moved = true;
+ } else if (direction == FOCUS_DOWN) {
+ mLayoutMode = LAYOUT_SET_SELECTION;
+ setSelectionInt(mItemCount - 1);
+ moved = true;
+ }
+
+ return moved;
+ }
+
+ /**
+ * Scrolls to the next or previous item, horizontally or vertically.
+ *
+ * @param direction either {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT},
+ * {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN}
+ *
+ * @return whether selection was moved
+ */
+ boolean arrowScroll(int direction) {
+ final int selectedPosition = mSelectedPosition;
+ final int numColumns = mNumColumns;
+
+ int startOfRowPos;
+ int endOfRowPos;
+
+ boolean moved = false;
+
+ if (!mStackFromBottom) {
+ startOfRowPos = (selectedPosition / numColumns) * numColumns;
+ endOfRowPos = Math.min(startOfRowPos + numColumns - 1, mItemCount - 1);
+ } else {
+ final int invertedSelection = mItemCount - 1 - selectedPosition;
+ endOfRowPos = mItemCount - 1 - (invertedSelection / numColumns) * numColumns;
+ startOfRowPos = Math.max(0, endOfRowPos - numColumns + 1);
+ }
+
+ switch (direction) {
+ case FOCUS_UP:
+ if (startOfRowPos > 0) {
+ mLayoutMode = LAYOUT_MOVE_SELECTION;
+ setSelectionInt(Math.max(0, selectedPosition - numColumns));
+ moved = true;
+ }
+ break;
+ case FOCUS_DOWN:
+ if (endOfRowPos < mItemCount - 1) {
+ mLayoutMode = LAYOUT_MOVE_SELECTION;
+ setSelectionInt(Math.min(selectedPosition + numColumns, mItemCount - 1));
+ moved = true;
+ }
+ break;
+ case FOCUS_LEFT:
+ if (selectedPosition > startOfRowPos) {
+ mLayoutMode = LAYOUT_MOVE_SELECTION;
+ setSelectionInt(selectedPosition - 1);
+ moved = true;
+ }
+ break;
+ case FOCUS_RIGHT:
+ if (selectedPosition < endOfRowPos) {
+ mLayoutMode = LAYOUT_MOVE_SELECTION;
+ setSelectionInt(selectedPosition + 1);
+ moved = true;
+ }
+ break;
+ }
+
+ if (moved) {
+ playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));
+ }
+
+ return moved;
+ }
+
+ @Override
+ protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
+ super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
+
+ int closestChildIndex = -1;
+ if (gainFocus && previouslyFocusedRect != null) {
+ previouslyFocusedRect.offset(mScrollX, mScrollY);
+
+ // figure out which item should be selected based on previously
+ // focused rect
+ Rect otherRect = mTempRect;
+ int minDistance = Integer.MAX_VALUE;
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ // only consider view's on appropriate edge of grid
+ if (!isCandidateSelection(i, direction)) {
+ continue;
+ }
+
+ final View other = getChildAt(i);
+ other.getDrawingRect(otherRect);
+ offsetDescendantRectToMyCoords(other, otherRect);
+ int distance = getDistance(previouslyFocusedRect, otherRect, direction);
+
+ if (distance < minDistance) {
+ minDistance = distance;
+ closestChildIndex = i;
+ }
+ }
+ }
+
+ if (closestChildIndex >= 0) {
+ setSelection(closestChildIndex + mFirstPosition);
+ } else {
+ requestLayout();
+ }
+ }
+
+ /**
+ * Is childIndex a candidate for next focus given the direction the focus
+ * change is coming from?
+ * @param childIndex The index to check.
+ * @param direction The direction, one of
+ * {FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}
+ * @return Whether childIndex is a candidate.
+ */
+ private boolean isCandidateSelection(int childIndex, int direction) {
+ final int count = getChildCount();
+ final int invertedIndex = count - 1 - childIndex;
+
+ int rowStart;
+ int rowEnd;
+
+ if (!mStackFromBottom) {
+ rowStart = childIndex - (childIndex % mNumColumns);
+ rowEnd = Math.max(rowStart + mNumColumns - 1, count);
+ } else {
+ rowEnd = count - 1 - (invertedIndex - (invertedIndex % mNumColumns));
+ rowStart = Math.max(0, rowEnd - mNumColumns + 1);
+ }
+
+ switch (direction) {
+ case View.FOCUS_RIGHT:
+ // coming from left, selection is only valid if it is on left
+ // edge
+ return childIndex == rowStart;
+ case View.FOCUS_DOWN:
+ // coming from top; only valid if in top row
+ return rowStart == 0;
+ case View.FOCUS_LEFT:
+ // coming from right, must be on right edge
+ return childIndex == rowEnd;
+ case View.FOCUS_UP:
+ // coming from bottom, need to be in last row
+ return rowEnd == count - 1;
+ default:
+ throw new IllegalArgumentException("direction must be one of "
+ + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
+ }
+ }
+
+ /**
+ * Describes how the child views are horizontally aligned. Defaults to Gravity.LEFT
+ *
+ * @param gravity the gravity to apply to this grid's children
+ *
+ * @attr ref android.R.styleable#GridView_gravity
+ */
+ public void setGravity(int gravity) {
+ if (mGravity != gravity) {
+ mGravity = gravity;
+ requestLayoutIfNecessary();
+ }
+ }
+
+ /**
+ * Set the amount of horizontal (x) spacing to place between each item
+ * in the grid.
+ *
+ * @param horizontalSpacing The amount of horizontal space between items,
+ * in pixels.
+ *
+ * @attr ref android.R.styleable#GridView_horizontalSpacing
+ */
+ public void setHorizontalSpacing(int horizontalSpacing) {
+ if (horizontalSpacing != mRequestedHorizontalSpacing) {
+ mRequestedHorizontalSpacing = horizontalSpacing;
+ requestLayoutIfNecessary();
+ }
+ }
+
+
+ /**
+ * Set the amount of vertical (y) spacing to place between each item
+ * in the grid.
+ *
+ * @param verticalSpacing The amount of vertical space between items,
+ * in pixels.
+ *
+ * @attr ref android.R.styleable#GridView_verticalSpacing
+ */
+ public void setVerticalSpacing(int verticalSpacing) {
+ if (verticalSpacing != mVerticalSpacing) {
+ mVerticalSpacing = verticalSpacing;
+ requestLayoutIfNecessary();
+ }
+ }
+
+ /**
+ * Control how items are stretched to fill their space.
+ *
+ * @param stretchMode Either {@link #NO_STRETCH},
+ * {@link #STRETCH_SPACING}, or {@link #STRETCH_COLUMN_WIDTH}.
+ *
+ * @attr ref android.R.styleable#GridView_stretchMode
+ */
+ public void setStretchMode(int stretchMode) {
+ if (stretchMode != mStretchMode) {
+ mStretchMode = stretchMode;
+ requestLayoutIfNecessary();
+ }
+ }
+
+ public int getStretchMode() {
+ return mStretchMode;
+ }
+
+ /**
+ * Set the width of columns in the grid.
+ *
+ * @param columnWidth The column width, in pixels.
+ *
+ * @attr ref android.R.styleable#GridView_columnWidth
+ */
+ public void setColumnWidth(int columnWidth) {
+ if (columnWidth != mRequestedColumnWidth) {
+ mRequestedColumnWidth = columnWidth;
+ requestLayoutIfNecessary();
+ }
+ }
+
+ /**
+ * Set the number of columns in the grid
+ *
+ * @param numColumns The desired number of columns.
+ *
+ * @attr ref android.R.styleable#GridView_numColumns
+ */
+ public void setNumColumns(int numColumns) {
+ if (numColumns != mRequestedNumColumns) {
+ mRequestedNumColumns = numColumns;
+ requestLayoutIfNecessary();
+ }
+ }
+
+ /**
+ * Make sure views are touching the top or bottom edge, as appropriate for
+ * our gravity
+ */
+ private void adjustViewsUpOrDown() {
+ final int childCount = getChildCount();
+
+ if (childCount > 0) {
+ int delta;
+ View child;
+
+ if (!mStackFromBottom) {
+ // Uh-oh -- we came up short. Slide all views up to make them
+ // align with the top
+ child = getChildAt(0);
+ delta = child.getTop() - mListPadding.top;
+ if (mFirstPosition != 0) {
+ // It's OK to have some space above the first item if it is
+ // part of the vertical spacing
+ delta -= mVerticalSpacing;
+ }
+ if (delta < 0) {
+ // We only are looking to see if we are too low, not too high
+ delta = 0;
+ }
+ } else {
+ // we are too high, slide all views down to align with bottom
+ child = getChildAt(childCount - 1);
+ delta = child.getBottom() - (getHeight() - mListPadding.bottom);
+
+ if (mFirstPosition + childCount < mItemCount) {
+ // It's OK to have some space below the last item if it is
+ // part of the vertical spacing
+ delta += mVerticalSpacing;
+ }
+
+ if (delta > 0) {
+ // We only are looking to see if we are too high, not too low
+ delta = 0;
+ }
+ }
+
+ if (delta != 0) {
+ offsetChildrenTopAndBottom(-delta);
+ }
+ }
+ }
+
+ @Override
+ protected int computeVerticalScrollExtent() {
+ final int count = getChildCount();
+ if (count > 0) {
+ final int numColumns = mNumColumns;
+ final int rowCount = (count + numColumns - 1) / numColumns;
+
+ int extent = rowCount * 100;
+
+ View view = getChildAt(0);
+ final int top = view.getTop();
+ int height = view.getHeight();
+ if (height > 0) {
+ extent += (top * 100) / height;
+ }
+
+ view = getChildAt(count - 1);
+ final int bottom = view.getBottom();
+ height = view.getHeight();
+ if (height > 0) {
+ extent -= ((bottom - getHeight()) * 100) / height;
+ }
+
+ return extent;
+ }
+ return 0;
+ }
+
+ @Override
+ protected int computeVerticalScrollOffset() {
+ if (mFirstPosition >= 0 && getChildCount() > 0) {
+ final View view = getChildAt(0);
+ final int top = view.getTop();
+ int height = view.getHeight();
+ if (height > 0) {
+ final int whichRow = mFirstPosition / mNumColumns;
+ return Math.max(whichRow * 100 - (top * 100) / height, 0);
+ }
+ }
+ return 0;
+ }
+
+ @Override
+ protected int computeVerticalScrollRange() {
+ // TODO: Account for vertical spacing too
+ final int numColumns = mNumColumns;
+ final int rowCount = (mItemCount + numColumns - 1) / numColumns;
+ return Math.max(rowCount * 100, 0);
+ }
+}
+
diff --git a/core/java/android/widget/HeaderViewListAdapter.java b/core/java/android/widget/HeaderViewListAdapter.java
new file mode 100644
index 0000000..b0e5f7e
--- /dev/null
+++ b/core/java/android/widget/HeaderViewListAdapter.java
@@ -0,0 +1,241 @@
+/*
+ * 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.widget;
+
+import android.database.DataSetObserver;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.ArrayList;
+
+/**
+ * ListAdapter used when a ListView has header views. This ListAdapter
+ * wraps another one and also keeps track of the header views and their
+ * associated data objects.
+ *<p>This is intended as a base class; you will probably not need to
+ * use this class directly in your own code.
+ *
+ */
+public class HeaderViewListAdapter implements WrapperListAdapter, Filterable {
+
+ private ListAdapter mAdapter;
+
+ ArrayList<ListView.FixedViewInfo> mHeaderViewInfos;
+ ArrayList<ListView.FixedViewInfo> mFooterViewInfos;
+ boolean mAreAllFixedViewsSelectable;
+
+ private boolean mIsFilterable;
+
+ public HeaderViewListAdapter(ArrayList<ListView.FixedViewInfo> headerViewInfos,
+ ArrayList<ListView.FixedViewInfo> footerViewInfos,
+ ListAdapter adapter) {
+ mAdapter = adapter;
+ mIsFilterable = adapter instanceof Filterable;
+
+ mHeaderViewInfos = headerViewInfos;
+ mFooterViewInfos = footerViewInfos;
+
+ mAreAllFixedViewsSelectable =
+ areAllListInfosSelectable(mHeaderViewInfos)
+ && areAllListInfosSelectable(mFooterViewInfos);
+ }
+
+ public int getHeadersCount() {
+ return mHeaderViewInfos == null ? 0 : mHeaderViewInfos.size();
+ }
+
+ public int getFootersCount() {
+ return mFooterViewInfos == null ? 0 : mFooterViewInfos.size();
+ }
+
+ public boolean isEmpty() {
+ return mAdapter == null || mAdapter.isEmpty();
+ }
+
+ private boolean areAllListInfosSelectable(ArrayList<ListView.FixedViewInfo> infos) {
+ if (infos != null) {
+ for (ListView.FixedViewInfo info : infos) {
+ if (!info.isSelectable) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ public boolean removeHeader(View v) {
+ for (int i = 0; i < mHeaderViewInfos.size(); i++) {
+ ListView.FixedViewInfo info = mHeaderViewInfos.get(i);
+ if (info.view == v) {
+ mHeaderViewInfos.remove(i);
+
+ mAreAllFixedViewsSelectable =
+ areAllListInfosSelectable(mHeaderViewInfos)
+ && areAllListInfosSelectable(mFooterViewInfos);
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public boolean removeFooter(View v) {
+ for (int i = 0; i < mFooterViewInfos.size(); i++) {
+ ListView.FixedViewInfo info = mFooterViewInfos.get(i);
+ if (info.view == v) {
+ mFooterViewInfos.remove(i);
+
+ mAreAllFixedViewsSelectable =
+ areAllListInfosSelectable(mHeaderViewInfos)
+ && areAllListInfosSelectable(mFooterViewInfos);
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public int getCount() {
+ if (mAdapter != null) {
+ return getFootersCount() + getHeadersCount() + mAdapter.getCount();
+ } else {
+ return getFootersCount() + getHeadersCount();
+ }
+ }
+
+ public boolean areAllItemsEnabled() {
+ if (mAdapter != null) {
+ return mAreAllFixedViewsSelectable && mAdapter.areAllItemsEnabled();
+ } else {
+ return true;
+ }
+ }
+
+ public boolean isEnabled(int position) {
+ int numHeaders = getHeadersCount();
+ if (mAdapter != null && position >= numHeaders) {
+ int adjPosition = position - numHeaders;
+ int adapterCount = mAdapter.getCount();
+ if (adjPosition >= adapterCount && mFooterViewInfos != null) {
+ return mFooterViewInfos.get(adjPosition - adapterCount).isSelectable;
+ } else {
+ return mAdapter.isEnabled(adjPosition);
+ }
+ } else if (position < numHeaders && mHeaderViewInfos != null) {
+ return mHeaderViewInfos.get(position).isSelectable;
+ }
+ return true;
+ }
+
+ public Object getItem(int position) {
+ int numHeaders = getHeadersCount();
+ if (mAdapter != null && position >= numHeaders) {
+ int adjPosition = position - numHeaders;
+ int adapterCount = mAdapter.getCount();
+ if (adjPosition >= adapterCount && mFooterViewInfos != null) {
+ return mFooterViewInfos.get(adjPosition - adapterCount).data;
+ } else {
+ return mAdapter.getItem(adjPosition);
+ }
+ } else if (position < numHeaders && mHeaderViewInfos != null) {
+ return mHeaderViewInfos.get(position).data;
+ }
+ return null;
+ }
+
+ public long getItemId(int position) {
+ int numHeaders = getHeadersCount();
+ if (mAdapter != null && position >= numHeaders) {
+ int adjPosition = position - numHeaders;
+ int adapterCnt = mAdapter.getCount();
+ if (adjPosition < adapterCnt) {
+ return mAdapter.getItemId(adjPosition);
+ }
+ }
+ return -1;
+ }
+
+ public boolean hasStableIds() {
+ if (mAdapter != null) {
+ return mAdapter.hasStableIds();
+ }
+ return false;
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ int numHeaders = getHeadersCount();
+ if (mAdapter != null && position >= numHeaders) {
+ int adjPosition = position - numHeaders;
+ int adapterCount = mAdapter.getCount();
+ if (adjPosition >= adapterCount) {
+ if (mFooterViewInfos != null) {
+ return mFooterViewInfos.get(adjPosition - adapterCount).view;
+ }
+ } else {
+ return mAdapter.getView(adjPosition, convertView, parent);
+ }
+ } else if (position < numHeaders) {
+ return mHeaderViewInfos.get(position).view;
+ }
+ return null;
+ }
+
+ public int getItemViewType(int position) {
+ int numHeaders = getHeadersCount();
+ if (mAdapter != null && position >= numHeaders) {
+ int adjPosition = position - numHeaders;
+ int adapterCount = mAdapter.getCount();
+ if (adjPosition < adapterCount) {
+ return mAdapter.getItemViewType(adjPosition);
+ }
+ }
+
+ return AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER;
+ }
+
+ public int getViewTypeCount() {
+ if (mAdapter != null) {
+ return mAdapter.getViewTypeCount();
+ }
+ return 1;
+ }
+
+ public void registerDataSetObserver(DataSetObserver observer) {
+ if (mAdapter != null) {
+ mAdapter.registerDataSetObserver(observer);
+ }
+ }
+
+ public void unregisterDataSetObserver(DataSetObserver observer) {
+ if (mAdapter != null) {
+ mAdapter.unregisterDataSetObserver(observer);
+ }
+ }
+
+ public Filter getFilter() {
+ if (mIsFilterable) {
+ return ((Filterable) mAdapter).getFilter();
+ }
+ return null;
+ }
+
+ public ListAdapter getWrappedAdapter() {
+ return mAdapter;
+ }
+}
diff --git a/core/java/android/widget/ImageButton.java b/core/java/android/widget/ImageButton.java
new file mode 100644
index 0000000..5c56428
--- /dev/null
+++ b/core/java/android/widget/ImageButton.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Message;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+import java.util.Map;
+
+/**
+ * <p>
+ * An image button displays an image that can be pressed, or clicked, by the
+ * user.
+ * </p>
+ *
+ * <p><strong>XML attributes</strong></p>
+ * <p>
+ * See {@link android.R.styleable#ImageView Button Attributes},
+ * {@link android.R.styleable#View View Attributes}
+ * </p>
+ */
+public class ImageButton extends ImageView {
+ public ImageButton(Context context) {
+ this(context, null);
+ }
+
+ public ImageButton(Context context, AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.imageButtonStyle);
+ }
+
+ public ImageButton(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ setFocusable(true);
+ }
+
+ @Override
+ protected boolean onSetAlpha(int alpha) {
+ return false;
+ }
+}
diff --git a/core/java/android/widget/ImageSwitcher.java b/core/java/android/widget/ImageSwitcher.java
new file mode 100644
index 0000000..bcb750a
--- /dev/null
+++ b/core/java/android/widget/ImageSwitcher.java
@@ -0,0 +1,59 @@
+/*
+ * 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.widget;
+
+import java.util.Map;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.util.AttributeSet;
+
+
+public class ImageSwitcher extends ViewSwitcher
+{
+ public ImageSwitcher(Context context)
+ {
+ super(context);
+ }
+
+ public ImageSwitcher(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public void setImageResource(int resid)
+ {
+ ImageView image = (ImageView)this.getNextView();
+ image.setImageResource(resid);
+ showNext();
+ }
+
+ public void setImageURI(Uri uri)
+ {
+ ImageView image = (ImageView)this.getNextView();
+ image.setImageURI(uri);
+ showNext();
+ }
+
+ public void setImageDrawable(Drawable drawable)
+ {
+ ImageView image = (ImageView)this.getNextView();
+ image.setImageDrawable(drawable);
+ showNext();
+ }
+}
+
diff --git a/core/java/android/widget/ImageView.java b/core/java/android/widget/ImageView.java
new file mode 100644
index 0000000..b5d4e2d
--- /dev/null
+++ b/core/java/android/widget/ImageView.java
@@ -0,0 +1,883 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Matrix;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.RectF;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.widget.RemoteViews.RemoteView;
+
+
+/**
+ * Displays an arbitrary image, such as an icon. The ImageView class
+ * can load images from various sources (such as resources or content
+ * providers), takes care of computing its measurement from the image so that
+ * it can be used in any layout manager, and provides various display options
+ * such as scaling and tinting.
+ *
+ * @attr ref android.R.styleable#ImageView_adjustViewBounds
+ * @attr ref android.R.styleable#ImageView_src
+ * @attr ref android.R.styleable#ImageView_maxWidth
+ * @attr ref android.R.styleable#ImageView_maxHeight
+ * @attr ref android.R.styleable#ImageView_tint
+ * @attr ref android.R.styleable#ImageView_scaleType
+ */
+@RemoteView
+public class ImageView extends View {
+ // settable by the client
+ private Uri mUri;
+ private int mResource = 0;
+ private Matrix mMatrix;
+ private ScaleType mScaleType;
+ private boolean mHaveFrame = false;
+ private boolean mAdjustViewBounds = false;
+ private int mMaxWidth = Integer.MAX_VALUE;
+ private int mMaxHeight = Integer.MAX_VALUE;
+
+ // these are applied to the drawable
+ private ColorFilter mColorFilter;
+ private int mAlpha = 255;
+ private int mViewAlphaScale = 256;
+
+ private Drawable mDrawable = null;
+ private int[] mState = null;
+ private boolean mMergeState = false;
+ private int mLevel = 0;
+ private int mDrawableWidth;
+ private int mDrawableHeight;
+ private Matrix mDrawMatrix = null;
+
+ // Avoid allocations...
+ private RectF mTempSrc = new RectF();
+ private RectF mTempDst = new RectF();
+
+ private boolean mCropToPadding;
+
+ private boolean mBaselineAligned = false;
+
+ private static final ScaleType[] sScaleTypeArray = {
+ ScaleType.MATRIX,
+ ScaleType.FIT_XY,
+ ScaleType.FIT_START,
+ ScaleType.FIT_CENTER,
+ ScaleType.FIT_END,
+ ScaleType.CENTER,
+ ScaleType.CENTER_CROP,
+ ScaleType.CENTER_INSIDE
+ };
+
+ public ImageView(Context context) {
+ super(context);
+ initImageView();
+ }
+
+ public ImageView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ImageView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ initImageView();
+
+ TypedArray a = context.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.ImageView, defStyle, 0);
+
+ Drawable d = a.getDrawable(com.android.internal.R.styleable.ImageView_src);
+ if (d != null) {
+ setImageDrawable(d);
+ }
+
+ mBaselineAligned = a.getBoolean(
+ com.android.internal.R.styleable.ImageView_baselineAlignBottom, false);
+
+ setAdjustViewBounds(
+ a.getBoolean(com.android.internal.R.styleable.ImageView_adjustViewBounds,
+ false));
+
+ setMaxWidth(a.getDimensionPixelSize(
+ com.android.internal.R.styleable.ImageView_maxWidth, Integer.MAX_VALUE));
+
+ setMaxHeight(a.getDimensionPixelSize(
+ com.android.internal.R.styleable.ImageView_maxHeight, Integer.MAX_VALUE));
+
+ int index = a.getInt(com.android.internal.R.styleable.ImageView_scaleType, -1);
+ if (index >= 0) {
+ setScaleType(sScaleTypeArray[index]);
+ }
+
+ int tint = a.getInt(com.android.internal.R.styleable.ImageView_tint, 0);
+ if (tint != 0) {
+ setColorFilter(tint, PorterDuff.Mode.SRC_ATOP);
+ }
+
+ mCropToPadding = a.getBoolean(
+ com.android.internal.R.styleable.ImageView_cropToPadding, false);
+
+ a.recycle();
+
+ //need inflate syntax/reader for matrix
+ }
+
+ private void initImageView() {
+ mMatrix = new Matrix();
+ mScaleType = ScaleType.FIT_CENTER;
+ }
+
+ @Override
+ protected boolean verifyDrawable(Drawable dr) {
+ return mDrawable == dr || super.verifyDrawable(dr);
+ }
+
+ @Override
+ public void invalidateDrawable(Drawable dr) {
+ if (dr == mDrawable) {
+ /* we invalidate the whole view in this case because it's very
+ * hard to know where the drawable actually is. This is made
+ * complicated because of the offsets and transformations that
+ * can be applied. In theory we could get the drawable's bounds
+ * and run them through the transformation and offsets, but this
+ * is probably not worth the effort.
+ */
+ invalidate();
+ } else {
+ super.invalidateDrawable(dr);
+ }
+ }
+
+ @Override
+ protected boolean onSetAlpha(int alpha) {
+ if (getBackground() == null) {
+ int scale = alpha + (alpha >> 7);
+ if (mViewAlphaScale != scale) {
+ mViewAlphaScale = scale;
+ applyColorMod();
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Set this to true if you want the ImageView to adjust its bounds
+ * to preserve the aspect ratio of its drawable.
+ * @param adjustViewBounds Whether to adjust the bounds of this view
+ * to presrve the original aspect ratio of the drawable
+ *
+ * @attr ref android.R.styleable#ImageView_adjustViewBounds
+ */
+ public void setAdjustViewBounds(boolean adjustViewBounds) {
+ mAdjustViewBounds = adjustViewBounds;
+ if (adjustViewBounds) {
+ setScaleType(ScaleType.FIT_CENTER);
+ }
+ }
+
+ /**
+ * An optional argument to supply a maximum width for this view. Only valid if
+ * {@link #setAdjustViewBounds} has been set to true. To set an image to be a maximum of 100 x
+ * 100 while preserving the original aspect ratio, do the following: 1) set adjustViewBounds to
+ * true 2) set maxWidth and maxHeight to 100 3) set the height and width layout params to
+ * WRAP_CONTENT.
+ *
+ * <p>
+ * Note that this view could be still smaller than 100 x 100 using this approach if the original
+ * image is small. To set an image to a fixed size, specify that size in the layout params and
+ * then use {@link #setScaleType} to determine how to fit the image within the bounds.
+ * </p>
+ *
+ * @param maxWidth maximum width for this view
+ *
+ * @attr ref android.R.styleable#ImageView_maxWidth
+ */
+ public void setMaxWidth(int maxWidth) {
+ mMaxWidth = maxWidth;
+ }
+
+ /**
+ * An optional argument to supply a maximum height for this view. Only valid if
+ * {@link #setAdjustViewBounds} has been set to true. To set an image to be a maximum of 100 x
+ * 100 while preserving the original aspect ratio, do the following: 1) set adjustViewBounds to
+ * true 2) set maxWidth and maxHeight to 100 3) set the height and width layout params to
+ * WRAP_CONTENT.
+ *
+ * <p>
+ * Note that this view could be still smaller than 100 x 100 using this approach if the original
+ * image is small. To set an image to a fixed size, specify that size in the layout params and
+ * then use {@link #setScaleType} to determine how to fit the image within the bounds.
+ * </p>
+ *
+ * @param maxHeight maximum height for this view
+ *
+ * @attr ref android.R.styleable#ImageView_maxHeight
+ */
+ public void setMaxHeight(int maxHeight) {
+ mMaxHeight = maxHeight;
+ }
+
+ /** Return the view's drawable, or null if no drawable has been
+ assigned.
+ */
+ public Drawable getDrawable() {
+ return mDrawable;
+ }
+
+ /**
+ * Sets a drawable as the content of this ImageView.
+ *
+ * @param resId the resource identifier of the the drawable
+ *
+ * @attr ref android.R.styleable#ImageView_src
+ */
+ public void setImageResource(int resId) {
+ if (mUri != null || mResource != resId) {
+ updateDrawable(null);
+ mResource = resId;
+ mUri = null;
+ resolveUri();
+ requestLayout();
+ invalidate();
+ }
+ }
+
+ /**
+ * Sets the content of this ImageView to the specified Uri.
+ *
+ * @param uri The Uri of an image
+ */
+ public void setImageURI(Uri uri) {
+ if (mResource != 0 ||
+ (mUri != uri &&
+ (uri == null || mUri == null || !uri.equals(mUri)))) {
+ updateDrawable(null);
+ mResource = 0;
+ mUri = uri;
+ resolveUri();
+ requestLayout();
+ invalidate();
+ }
+ }
+
+
+ /**
+ * Sets a drawable as the content of this ImageView.
+ *
+ * @param drawable The drawable to set
+ */
+ public void setImageDrawable(Drawable drawable) {
+ if (mDrawable != drawable) {
+ mResource = 0;
+ mUri = null;
+ updateDrawable(drawable);
+ requestLayout();
+ invalidate();
+ }
+ }
+
+ /**
+ * Sets a Bitmap as the content of this ImageView.
+ *
+ * @param bm The bitmap to set
+ */
+ public void setImageBitmap(Bitmap bm) {
+ // if this is used frequently, may handle bitmaps explicitly
+ // to reduce the intermediate drawable object
+ setImageDrawable(new BitmapDrawable(bm));
+ }
+
+ public void setImageState(int[] state, boolean merge) {
+ mState = state;
+ mMergeState = merge;
+ if (mDrawable != null) {
+ refreshDrawableState();
+ resizeFromDrawable();
+ }
+ }
+
+ @Override
+ public void setSelected(boolean selected) {
+ super.setSelected(selected);
+ resizeFromDrawable();
+ }
+
+ public void setImageLevel(int level) {
+ mLevel = level;
+ if (mDrawable != null) {
+ mDrawable.setLevel(level);
+ resizeFromDrawable();
+ }
+ }
+
+ /**
+ * Options for scaling the bounds of an image to the bounds of this view.
+ */
+ public enum ScaleType {
+ /**
+ * Scale using the image matrix when drawing. The image matrix can be set using
+ * {@link ImageView#setImageMatrix(Matrix)}. From XML, use this syntax:
+ * <code>android:scaleType="matrix"</code>.
+ */
+ MATRIX (0),
+ /**
+ * Scale the image using {@link Matrix.ScaleToFit#FILL}.
+ * From XML, use this syntax: <code>android:scaleType="fitXY"</code>.
+ */
+ FIT_XY (1),
+ /**
+ * Scale the image using {@link Matrix.ScaleToFit#START}.
+ * From XML, use this syntax: <code>android:scaleType="fitStart"</code>.
+ */
+ FIT_START (2),
+ /**
+ * Scale the image using {@link Matrix.ScaleToFit#CENTER}.
+ * From XML, use this syntax:
+ * <code>android:scaleType="fitCenter"</code>.
+ */
+ FIT_CENTER (3),
+ /**
+ * Scale the image using {@link Matrix.ScaleToFit#END}.
+ * From XML, use this syntax: <code>android:scaleType="fitEnd"</code>.
+ */
+ FIT_END (4),
+ /**
+ * Center the image in the view, but perform no scaling.
+ * From XML, use this syntax: <code>android:scaleType="center"</code>.
+ */
+ CENTER (5),
+ /**
+ * Scale the image uniformly (maintain the image's aspect ratio) so
+ * that both dimensions (width and height) of the image will be equal
+ * to or larger than the corresponding dimension of the view
+ * (minus padding). The image is then centered in the view.
+ * From XML, use this syntax: <code>android:scaleType="centerCrop"</code>.
+ */
+ CENTER_CROP (6),
+ /**
+ * Scale the image uniformly (maintain the image's aspect ratio) so
+ * that both dimensions (width and height) of the image will be equal
+ * to or less than the corresponding dimension of the view
+ * (minus padding). The image is then centered in the view.
+ * From XML, use this syntax: <code>android:scaleType="centerInside"</code>.
+ */
+ CENTER_INSIDE (7);
+
+ ScaleType(int ni) {
+ nativeInt = ni;
+ }
+ final int nativeInt;
+ }
+
+ /**
+ * Controls how the image should be resized or moved to match the size
+ * of this ImageView.
+ *
+ * @param scaleType The desired scaling mode.
+ *
+ * @attr ref android.R.styleable#ImageView_scaleType
+ */
+ public void setScaleType(ScaleType scaleType) {
+ if (scaleType == null) {
+ throw new NullPointerException();
+ }
+
+ if (mScaleType != scaleType) {
+ mScaleType = scaleType;
+
+ setWillNotCacheDrawing(mScaleType == ScaleType.CENTER);
+
+ requestLayout();
+ invalidate();
+ }
+ }
+
+ /**
+ * Return the current scale type in use by this ImageView.
+ *
+ * @see ImageView.ScaleType
+ *
+ * @attr ref android.R.styleable#ImageView_scaleType
+ */
+ public ScaleType getScaleType() {
+ return mScaleType;
+ }
+
+ /** Return the view's optional matrix. This is applied to the
+ view's drawable when it is drawn. If there is not matrix,
+ this method will return null.
+ Do not change this matrix in place. If you want a different matrix
+ applied to the drawable, be sure to call setImageMatrix().
+ */
+ public Matrix getImageMatrix() {
+ return mMatrix;
+ }
+
+ public void setImageMatrix(Matrix matrix) {
+ // collaps null and identity to just null
+ if (matrix != null && matrix.isIdentity()) {
+ matrix = null;
+ }
+
+ // don't invalidate unless we're actually changing our matrix
+ if (matrix == null && !mMatrix.isIdentity() ||
+ matrix != null && !mMatrix.equals(matrix)) {
+ mMatrix.set(matrix);
+ invalidate();
+ }
+ }
+
+ private void resolveUri() {
+ if (mDrawable != null) {
+ return;
+ }
+
+ Resources rsrc = getResources();
+ if (rsrc == null) {
+ return;
+ }
+
+ Drawable d = null;
+
+ if (mResource != 0) {
+ try {
+ d = rsrc.getDrawable(mResource);
+ } catch (Exception e) {
+ Log.w("ImageView", "Unable to find resource: " + mResource, e);
+ // Don't try again.
+ mUri = null;
+ }
+ } else if (mUri != null) {
+ if ("content".equals(mUri.getScheme())) {
+ try {
+ d = Drawable.createFromStream(
+ mContext.getContentResolver().openInputStream(mUri),
+ null);
+ } catch (Exception e) {
+ Log.w("ImageView", "Unable to open content: " + mUri, e);
+ }
+ } else {
+ d = Drawable.createFromPath(mUri.toString());
+ }
+
+ if (d == null) {
+ System.out.println("resolveUri failed on bad bitmap uri: "
+ + mUri);
+ // Don't try again.
+ mUri = null;
+ }
+ } else {
+ return;
+ }
+
+ updateDrawable(d);
+ }
+
+ @Override
+ public int[] onCreateDrawableState(int extraSpace) {
+ if (mState == null) {
+ return super.onCreateDrawableState(extraSpace);
+ } else if (!mMergeState) {
+ return mState;
+ } else {
+ return mergeDrawableStates(
+ super.onCreateDrawableState(extraSpace + mState.length), mState);
+ }
+ }
+
+ private void updateDrawable(Drawable d) {
+ if (mDrawable != null) {
+ mDrawable.setCallback(null);
+ unscheduleDrawable(mDrawable);
+ }
+ mDrawable = d;
+ if (d != null) {
+ d.setCallback(this);
+ if (d.isStateful()) {
+ d.setState(getDrawableState());
+ }
+ d.setLevel(mLevel);
+ mDrawableWidth = d.getIntrinsicWidth();
+ mDrawableHeight = d.getIntrinsicHeight();
+ applyColorMod();
+ configureBounds();
+ }
+ }
+
+ private void resizeFromDrawable() {
+ Drawable d = mDrawable;
+ if (d != null) {
+ int w = d.getIntrinsicWidth();
+ if (w < 0) w = mDrawableWidth;
+ int h = d.getIntrinsicHeight();
+ if (h < 0) h = mDrawableHeight;
+ if (w != mDrawableWidth || h != mDrawableHeight) {
+ mDrawableWidth = w;
+ mDrawableHeight = h;
+ requestLayout();
+ }
+ }
+ }
+
+ private static final Matrix.ScaleToFit[] sS2FArray = {
+ Matrix.ScaleToFit.FILL,
+ Matrix.ScaleToFit.START,
+ Matrix.ScaleToFit.CENTER,
+ Matrix.ScaleToFit.END
+ };
+
+ private static Matrix.ScaleToFit scaleTypeToScaleToFit(ScaleType st) {
+ // ScaleToFit enum to their corresponding Matrix.ScaleToFit values
+ return sS2FArray[st.nativeInt - 1];
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ resolveUri();
+ int w;
+ int h;
+
+ // Desired aspect ratio of the view's contents (not including padding)
+ float desiredAspect = 0.0f;
+
+ // We are allowed to change the view's width
+ boolean resizeWidth = false;
+
+ // We are allowed to change the view's height
+ boolean resizeHeight = false;
+
+ if (mDrawable == null) {
+ // If no drawable, its intrinsic size is 0.
+ mDrawableWidth = -1;
+ mDrawableHeight = -1;
+ w = h = 0;
+ } else {
+ w = mDrawableWidth;
+ h = mDrawableHeight;
+ if (w <= 0) w = 1;
+ if (h <= 0) h = 1;
+
+ // We are supposed to adjust view bounds to match the aspect
+ // ratio of our drawable. See if that is possible.
+ if (mAdjustViewBounds) {
+
+ int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
+ int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
+
+ resizeWidth = widthSpecMode != MeasureSpec.EXACTLY;
+ resizeHeight = heightSpecMode != MeasureSpec.EXACTLY;
+
+ desiredAspect = (float)w/(float)h;
+ }
+ }
+
+ int pleft = mPaddingLeft;
+ int pright = mPaddingRight;
+ int ptop = mPaddingTop;
+ int pbottom = mPaddingBottom;
+
+ int widthSize;
+ int heightSize;
+
+ if (resizeWidth || resizeHeight) {
+ /* If we get here, it means we want to resize to match the
+ drawables aspect ratio, and we have the freedom to change at
+ least one dimension.
+ */
+
+ // Get the max possible width given our constraints
+ widthSize = resolveAdjustedSize(w + pleft + pright,
+ mMaxWidth, widthMeasureSpec);
+
+ // Get the max possible height given our constraints
+ heightSize = resolveAdjustedSize(h + ptop + pbottom,
+ mMaxHeight, heightMeasureSpec);
+
+ if (desiredAspect != 0.0f) {
+ // See what our actual aspect ratio is
+ float actualAspect = (float)(widthSize - pleft - pright) /
+ (heightSize - ptop - pbottom);
+
+ if (Math.abs(actualAspect - desiredAspect) > 0.0000001) {
+
+ boolean done = false;
+
+ // Try adjusting width to be proportional to height
+ if (resizeWidth) {
+ int newWidth = (int)(desiredAspect *
+ (heightSize - ptop - pbottom))
+ + pleft + pright;
+ if (newWidth <= widthSize) {
+ widthSize = newWidth;
+ done = true;
+ }
+ }
+
+ // Try adjusting height to be proportional to width
+ if (!done && resizeHeight) {
+ int newHeight = (int)((widthSize - pleft - pright)
+ / desiredAspect) + ptop + pbottom;
+ if (newHeight <= heightSize) {
+ heightSize = newHeight;
+ }
+ }
+ }
+ }
+ } else {
+ /* We are either don't want to preserve the drawables aspect ratio,
+ or we are not allowed to change view dimensions. Just measure in
+ the normal way.
+ */
+ w += pleft + pright;
+ h += ptop + pbottom;
+
+ w = Math.max(w, getSuggestedMinimumWidth());
+ h = Math.max(h, getSuggestedMinimumHeight());
+
+ widthSize = resolveSize(w, widthMeasureSpec);
+ heightSize = resolveSize(h, heightMeasureSpec);
+ }
+
+ setMeasuredDimension(widthSize, heightSize);
+ }
+
+ private int resolveAdjustedSize(int desiredSize, int maxSize,
+ int measureSpec) {
+ int result = desiredSize;
+ int specMode = MeasureSpec.getMode(measureSpec);
+ int specSize = MeasureSpec.getSize(measureSpec);
+ switch (specMode) {
+ case MeasureSpec.UNSPECIFIED:
+ /* Parent says we can be as big as we want. Just don't be larger
+ than max size imposed on ourselves.
+ */
+ result = Math.min(desiredSize, maxSize);
+ break;
+ case MeasureSpec.AT_MOST:
+ // Parent says we can be as big as we want, up to specSize.
+ // Don't be larger than specSize, and don't be larger than
+ // the max size imposed on ourselves.
+ result = Math.min(Math.min(desiredSize, specSize), maxSize);
+ break;
+ case MeasureSpec.EXACTLY:
+ // No choice. Do what we are told.
+ result = specSize;
+ break;
+ }
+ return result;
+ }
+
+ @Override
+ protected boolean setFrame(int l, int t, int r, int b) {
+ boolean changed = super.setFrame(l, t, r, b);
+ mHaveFrame = true;
+ configureBounds();
+ return changed;
+ }
+
+ private void configureBounds() {
+ if (mDrawable == null || !mHaveFrame) {
+ return;
+ }
+
+ int dwidth = mDrawableWidth;
+ int dheight = mDrawableHeight;
+
+ int vwidth = getWidth() - mPaddingLeft - mPaddingRight;
+ int vheight = getHeight() - mPaddingTop - mPaddingBottom;
+
+ boolean fits = (dwidth < 0 || vwidth == dwidth) &&
+ (dheight < 0 || vheight == dheight);
+
+ if (dwidth <= 0 || dheight <= 0 || ScaleType.FIT_XY == mScaleType) {
+ /* If the drawable has no intrinsic size, or we're told to
+ scaletofit, then we just fill our entire view.
+ */
+ mDrawable.setBounds(0, 0, vwidth, vheight);
+ mDrawMatrix = null;
+ } else {
+ // We need to do the scaling ourself, so have the drawable
+ // use its native size.
+ mDrawable.setBounds(0, 0, dwidth, dheight);
+
+ if (ScaleType.MATRIX == mScaleType) {
+ // Use the specified matrix as-is.
+ if (mMatrix.isIdentity()) {
+ mDrawMatrix = null;
+ } else {
+ mDrawMatrix = mMatrix;
+ }
+ } else if (fits) {
+ // The bitmap fits exactly, no transform needed.
+ mDrawMatrix = null;
+ } else if (ScaleType.CENTER == mScaleType) {
+ // Center bitmap in view, no scaling.
+ mDrawMatrix = mMatrix;
+ mDrawMatrix.setTranslate((vwidth - dwidth) * 0.5f,
+ (vheight - dheight) * 0.5f);
+ } else if (ScaleType.CENTER_CROP == mScaleType) {
+ mDrawMatrix = mMatrix;
+
+ float scale;
+ float dx = 0, dy = 0;
+
+ if (dwidth * vheight > vwidth * dheight) {
+ scale = (float) vheight / (float) dheight;
+ dx = (vwidth - dwidth * scale) * 0.5f;
+ } else {
+ scale = (float) vwidth / (float) dwidth;
+ dy = (vheight - dheight * scale) * 0.5f;
+ }
+
+ mDrawMatrix.setScale(scale, scale);
+ mDrawMatrix.postTranslate(dx, dy);
+ } else if (ScaleType.CENTER_INSIDE == mScaleType) {
+ mDrawMatrix = mMatrix;
+ float scale;
+ float dx;
+ float dy;
+
+ if (dwidth <= vwidth && dheight <= vheight) {
+ scale = 1.0f;
+ } else {
+ scale = Math.min((float) vwidth / (float) dwidth,
+ (float) vheight / (float) dheight);
+ }
+
+ dx = (vwidth - dwidth * scale) * 0.5f;
+ dy = (vheight - dheight * scale) * 0.5f;
+
+ mDrawMatrix.setScale(scale, scale);
+ mDrawMatrix.postTranslate(dx, dy);
+ } else {
+ // Generate the required transform.
+ mTempSrc.set(0, 0, dwidth, dheight);
+ mTempDst.set(0, 0, vwidth, vheight);
+
+ mDrawMatrix = mMatrix;
+ mDrawMatrix.setRectToRect(mTempSrc, mTempDst,
+ scaleTypeToScaleToFit(mScaleType));
+ }
+ }
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+ Drawable d = mDrawable;
+ if (d != null && d.isStateful()) {
+ d.setState(getDrawableState());
+ }
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ if (mDrawable == null) {
+ return; // couldn't resolve the URI
+ }
+
+ if (mDrawableWidth == 0 || mDrawableHeight == 0) {
+ return; // nothing to draw (empty bounds)
+ }
+
+ if (mDrawMatrix == null && mPaddingTop == 0 && mPaddingLeft == 0) {
+ mDrawable.draw(canvas);
+ } else {
+ int saveCount = canvas.getSaveCount();
+ canvas.save();
+
+ if (mCropToPadding) {
+ final int scrollX = mScrollX;
+ final int scrollY = mScrollY;
+ canvas.clipRect(scrollX + mPaddingLeft, scrollY + mPaddingTop,
+ scrollX + mRight - mLeft - mPaddingRight,
+ scrollY + mBottom - mTop - mPaddingBottom);
+ }
+
+ canvas.translate(mPaddingLeft, mPaddingTop);
+
+ if (mDrawMatrix != null) {
+ canvas.concat(mDrawMatrix);
+ }
+ mDrawable.draw(canvas);
+ canvas.restoreToCount(saveCount);
+ }
+ }
+
+ @Override
+ public int getBaseline() {
+ return mBaselineAligned ? getHeight() : -1;
+ }
+
+ /**
+ * Set a tinting option for the image.
+ *
+ * @param color Color tint to apply.
+ * @param mode How to apply the color. The standard mode is
+ * {@link PorterDuff.Mode#SRC_ATOP}
+ *
+ * @attr ref android.R.styleable#ImageView_tint
+ */
+ public final void setColorFilter(int color, PorterDuff.Mode mode) {
+ setColorFilter(new PorterDuffColorFilter(color, mode));
+ }
+
+ public final void clearColorFilter() {
+ setColorFilter(null);
+ }
+
+ /**
+ * Apply an arbitrary colorfilter to the image.
+ *
+ * @param cf the colorfilter to apply (may be null)
+ */
+ public void setColorFilter(ColorFilter cf) {
+ if (mColorFilter != cf) {
+ mColorFilter = cf;
+ applyColorMod();
+ invalidate();
+ }
+ }
+
+ public void setAlpha(int alpha) {
+ alpha &= 0xFF; // keep it legal
+ if (mAlpha != alpha) {
+ mAlpha = alpha;
+ applyColorMod();
+ invalidate();
+ }
+ }
+
+ private void applyColorMod() {
+ if (mDrawable != null) {
+ mDrawable.setColorFilter(mColorFilter);
+ mDrawable.setAlpha(mAlpha * mViewAlphaScale >> 8);
+ }
+ }
+}
diff --git a/core/java/android/widget/LinearLayout.java b/core/java/android/widget/LinearLayout.java
new file mode 100644
index 0000000..de74fa4
--- /dev/null
+++ b/core/java/android/widget/LinearLayout.java
@@ -0,0 +1,1315 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewDebug;
+import android.view.ViewGroup;
+import android.widget.RemoteViews.RemoteView;
+
+import com.android.internal.R;
+
+
+/**
+ * A Layout that arranges its children in a single column or a single row. The direction of
+ * the row can be set by calling {@link #setOrientation(int) setOrientation()}.
+ * You can also specify gravity, which specifies the alignment of all the child elements by
+ * calling {@link #setGravity(int) setGravity()} or specify that specific children
+ * grow to fill up any remaining space in the layout by setting the <em>weight</em> member of
+ * {@link android.widget.LinearLayout.LayoutParams LinearLayout.LayoutParams}.
+ * The default orientation is horizontal.
+ *
+ * <p>
+ * Also see {@link LinearLayout.LayoutParams android.widget.LinearLayout.LayoutParams}
+ * for layout attributes </p>
+ */
+@RemoteView
+public class LinearLayout extends ViewGroup {
+ public static final int HORIZONTAL = 0;
+ public static final int VERTICAL = 1;
+
+ /**
+ * Whether the children of this layout are baseline aligned. Only applicable
+ * if {@link #mOrientation} is horizontal.
+ */
+ private boolean mBaselineAligned = true;
+
+ /**
+ * If this layout is part of another layout that is baseline aligned,
+ * use the child at this index as the baseline.
+ *
+ * Note: this is orthogonal to {@link #mBaselineAligned}, which is concerned
+ * with whether the children of this layout are baseline aligned.
+ */
+ private int mBaselineAlignedChildIndex = 0;
+
+ /**
+ * The additional offset to the child's baseline.
+ * We'll calculate the baseline of this layout as we measure vertically; for
+ * horizontal linear layouts, the offset of 0 is appropriate.
+ */
+ private int mBaselineChildTop = 0;
+
+ private int mOrientation;
+ private int mGravity = Gravity.LEFT | Gravity.TOP;
+ private int mTotalLength;
+
+ private float mWeightSum;
+
+ private int[] mMaxAscent;
+ private int[] mMaxDescent;
+
+ private static final int VERTICAL_GRAVITY_COUNT = 4;
+
+ private static final int INDEX_CENTER_VERTICAL = 0;
+ private static final int INDEX_TOP = 1;
+ private static final int INDEX_BOTTOM = 2;
+ private static final int INDEX_FILL = 3;
+
+ public LinearLayout(Context context) {
+ super(context);
+ }
+
+ public LinearLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ TypedArray a =
+ context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.LinearLayout);
+
+ int index = a.getInt(com.android.internal.R.styleable.LinearLayout_orientation, -1);
+ if (index >= 0) {
+ setOrientation(index);
+ }
+
+ index = a.getInt(com.android.internal.R.styleable.LinearLayout_gravity, -1);
+ if (index >= 0) {
+ setGravity(index);
+ }
+
+ boolean baselineAligned = a.getBoolean(R.styleable.LinearLayout_baselineAligned, true);
+ if (!baselineAligned) {
+ setBaselineAligned(baselineAligned);
+ }
+
+ mWeightSum = a.getFloat(R.styleable.LinearLayout_weightSum, -1.0f);
+
+ mBaselineAlignedChildIndex =
+ a.getInt(com.android.internal.R.styleable.LinearLayout_baselineAlignedChildIndex, -1);
+
+ a.recycle();
+ }
+
+ /**
+ * <p>Indicates whether widgets contained within this layout are aligned
+ * on their baseline or not.</p>
+ *
+ * @return true when widgets are baseline-aligned, false otherwise
+ */
+ public boolean isBaselineAligned() {
+ return mBaselineAligned;
+ }
+
+ /**
+ * <p>Defines whether widgets contained in this layout are
+ * baseline-aligned or not.</p>
+ *
+ * @param baselineAligned true to align widgets on their baseline,
+ * false otherwise
+ *
+ * @attr ref android.R.styleable#LinearLayout_baselineAligned
+ */
+ public void setBaselineAligned(boolean baselineAligned) {
+ mBaselineAligned = baselineAligned;
+ }
+
+ @Override
+ public int getBaseline() {
+ if (mBaselineAlignedChildIndex < 0) {
+ return super.getBaseline();
+ }
+
+ if (getChildCount() <= mBaselineAlignedChildIndex) {
+ throw new RuntimeException("mBaselineAlignedChildIndex of LinearLayout "
+ + "set to an index that is out of bounds.");
+ }
+
+ final View child = getChildAt(mBaselineAlignedChildIndex);
+ final int childBaseline = child.getBaseline();
+
+ if (childBaseline == -1) {
+ if (mBaselineAlignedChildIndex == 0) {
+ // this is just the default case, safe to return -1
+ return -1;
+ }
+ // the user picked an index that points to something that doesn't
+ // know how to calculate its baseline.
+ throw new RuntimeException("mBaselineAlignedChildIndex of LinearLayout "
+ + "points to a View that doesn't know how to get its baseline.");
+ }
+
+ // TODO: This should try to take into account the virtual offsets
+ // (See getNextLocationOffset and getLocationOffset)
+ // We should add to childTop:
+ // sum([getNextLocationOffset(getChildAt(i)) / i < mBaselineAlignedChildIndex])
+ // and also add:
+ // getLocationOffset(child)
+ int childTop = mBaselineChildTop;
+
+ if (mOrientation == VERTICAL) {
+ final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
+ if (majorGravity != Gravity.TOP) {
+ switch (majorGravity) {
+ case Gravity.BOTTOM:
+ childTop = mBottom - mTop - mPaddingBottom - mTotalLength;
+ break;
+
+ case Gravity.CENTER_VERTICAL:
+ childTop += ((mBottom - mTop - mPaddingTop - mPaddingBottom) -
+ mTotalLength) / 2;
+ break;
+ }
+ }
+ }
+
+ LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
+ return childTop + lp.topMargin + childBaseline;
+ }
+
+ /**
+ * @return The index of the child that will be used if this layout is
+ * part of a larger layout that is baseline aligned, or -1 if none has
+ * been set.
+ */
+ public int getBaselineAlignedChildIndex() {
+ return mBaselineAlignedChildIndex;
+ }
+
+ /**
+ * @param i The index of the child that will be used if this layout is
+ * part of a larger layout that is baseline aligned.
+ *
+ * @attr ref android.R.styleable#LinearLayout_baselineAlignedChildIndex
+ */
+ public void setBaselineAlignedChildIndex(int i) {
+ if ((i < 0) || (i >= getChildCount())) {
+ throw new IllegalArgumentException("base aligned child index out "
+ + "of range (0, " + getChildCount() + ")");
+ }
+ mBaselineAlignedChildIndex = i;
+ }
+
+ /**
+ * <p>Returns the view at the specified index. This method can be overriden
+ * to take into account virtual children. Refer to
+ * {@link android.widget.TableLayout} and {@link android.widget.TableRow}
+ * for an example.</p>
+ *
+ * @param index the child's index
+ * @return the child at the specified index
+ */
+ View getVirtualChildAt(int index) {
+ return getChildAt(index);
+ }
+
+ /**
+ * <p>Returns the virtual number of children. This number might be different
+ * than the actual number of children if the layout can hold virtual
+ * children. Refer to
+ * {@link android.widget.TableLayout} and {@link android.widget.TableRow}
+ * for an example.</p>
+ *
+ * @return the virtual number of children
+ */
+ int getVirtualChildCount() {
+ return getChildCount();
+ }
+
+ /**
+ * Returns the desired weights sum.
+ *
+ * @return A number greater than 0.0f if the weight sum is defined, or
+ * a number lower than or equals to 0.0f if not weight sum is
+ * to be used.
+ */
+ public float getWeightSum() {
+ return mWeightSum;
+ }
+
+ /**
+ * Defines the desired weights sum. If unspecified the weights sum is computed
+ * at layout time by adding the layout_weight of each child.
+ *
+ * This can be used for instance to give a single child 50% of the total
+ * available space by giving it a layout_weight of 0.5 and setting the
+ * weightSum to 1.0.
+ *
+ * @param weightSum a number greater than 0.0f, or a number lower than or equals
+ * to 0.0f if the weight sum should be computed from the children's
+ * layout_weight
+ */
+ public void setWeightSum(float weightSum) {
+ mWeightSum = Math.max(0.0f, weightSum);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ if (mOrientation == VERTICAL) {
+ measureVertical(widthMeasureSpec, heightMeasureSpec);
+ } else {
+ measureHorizontal(widthMeasureSpec, heightMeasureSpec);
+ }
+ }
+
+ /**
+ * Measures the children when the orientation of this LinearLayout is set
+ * to {@link #VERTICAL}.
+ *
+ * @param widthMeasureSpec Horizontal space requirements as imposed by the parent.
+ * @param heightMeasureSpec Vertical space requirements as imposed by the parent.
+ *
+ * @see #getOrientation()
+ * @see #setOrientation(int)
+ * @see #onMeasure(int, int)
+ */
+ void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
+ mTotalLength = 0;
+ int maxWidth = 0;
+ int alternativeMaxWidth = 0;
+ int weightedMaxWidth = 0;
+ boolean allFillParent = true;
+ float totalWeight = 0;
+
+ final int count = getVirtualChildCount();
+
+ final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+
+ boolean matchWidth = false;
+
+ final int baselineChildIndex = mBaselineAlignedChildIndex;
+
+ // See how tall everyone is. Also remember max width.
+ for (int i = 0; i < count; ++i) {
+ final View child = getVirtualChildAt(i);
+
+ if (child == null) {
+ mTotalLength += measureNullChild(i);
+ continue;
+ }
+
+ if (child.getVisibility() == View.GONE) {
+ i += getChildrenSkipCount(child, i);
+ continue;
+ }
+
+ LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
+
+ totalWeight += lp.weight;
+
+ if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
+ // Optimization: don't bother measuring children who are going to use
+ // leftover space. These views will get measured again down below if
+ // there is any leftover space.
+ mTotalLength += lp.topMargin + lp.bottomMargin;
+ } else {
+ int oldHeight = Integer.MIN_VALUE;
+
+ if (lp.height == 0 && lp.weight > 0) {
+ // heightMode is either UNSPECIFIED OR AT_MOST, and this child
+ // wanted to stretch to fill available space. Translate that to
+ // WRAP_CONTENT so that it does not end up with a height of 0
+ oldHeight = lp.height;
+ lp.height = LayoutParams.WRAP_CONTENT;
+ }
+
+ // Determine how big this child would like to. If this or
+ // previous children have given a weight, then we allow it to
+ // use all available space (and we will shrink things later
+ // if needed).
+ measureChildBeforeLayout(
+ child, i, widthMeasureSpec, 0, heightMeasureSpec,
+ totalWeight == 0 ? mTotalLength : 0);
+
+ if (oldHeight != Integer.MIN_VALUE) {
+ lp.height = oldHeight;
+ }
+
+ mTotalLength += child.getMeasuredHeight() + lp.topMargin +
+ lp.bottomMargin + getNextLocationOffset(child);
+ }
+
+ /**
+ * If applicable, compute the additional offset to the child's baseline
+ * we'll need later when asked {@link #getBaseline}.
+ */
+ if ((baselineChildIndex >= 0) && (baselineChildIndex == i + 1)) {
+ mBaselineChildTop = mTotalLength;
+ }
+
+ // if we are trying to use a child index for our baseline, the above
+ // book keeping only works if there are no children above it with
+ // weight. fail fast to aid the developer.
+ if (i < baselineChildIndex && lp.weight > 0) {
+ throw new RuntimeException("A child of LinearLayout with index "
+ + "less than mBaselineAlignedChildIndex has weight > 0, which "
+ + "won't work. Either remove the weight, or don't set "
+ + "mBaselineAlignedChildIndex.");
+ }
+
+ boolean matchWidthLocally = false;
+ if (widthMode != MeasureSpec.EXACTLY && lp.width == LayoutParams.FILL_PARENT) {
+ // The width of the linear layout will scale, and at least one
+ // child said it wanted to match our width. Set a flag
+ // indicating that we need to remeasure at least that view when
+ // we know our width.
+ matchWidth = true;
+ matchWidthLocally = true;
+ }
+
+ final int margin = lp.leftMargin + lp.rightMargin;
+ final int measuredWidth = child.getMeasuredWidth() + margin;
+ maxWidth = Math.max(maxWidth, measuredWidth);
+
+ allFillParent = allFillParent && lp.width == LayoutParams.FILL_PARENT;
+ if (lp.weight > 0) {
+ /*
+ * Widths of weighted Views are bogus if we end up
+ * remeasuring, so keep them separate.
+ */
+ weightedMaxWidth = Math.max(weightedMaxWidth,
+ matchWidthLocally ? margin : measuredWidth);
+ } else {
+ alternativeMaxWidth = Math.max(alternativeMaxWidth,
+ matchWidthLocally ? margin : measuredWidth);
+ }
+
+ i += getChildrenSkipCount(child, i);
+ }
+
+ // Add in our padding
+ mTotalLength += mPaddingTop + mPaddingBottom;
+
+ int heightSize = mTotalLength;
+
+ // Check against our minimum height
+ heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
+
+ // Reconcile our calculated size with the heightMeasureSpec
+ heightSize = resolveSize(heightSize, heightMeasureSpec);
+
+ // Either expand children with weight to take up available space or
+ // shrink them if they extend beyond our current bounds
+ int delta = heightSize - mTotalLength;
+ if (delta != 0 && totalWeight > 0.0f) {
+ float weightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;
+
+ mTotalLength = 0;
+
+ for (int i = 0; i < count; ++i) {
+ final View child = getVirtualChildAt(i);
+
+ if (child.getVisibility() == View.GONE) {
+ continue;
+ }
+
+ LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
+
+ float childExtra = lp.weight;
+ if (childExtra > 0) {
+ // Child said it could absorb extra space -- give him his share
+ int share = (int) (childExtra * delta / weightSum);
+ weightSum -= childExtra;
+ delta -= share;
+
+ final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
+ mPaddingLeft + mPaddingRight +
+ lp.leftMargin + lp.rightMargin, lp.width);
+
+ // TODO: Use a field like lp.isMeasured to figure out if this
+ // child has been previously measured
+ if ((lp.height != 0) || (heightMode != MeasureSpec.EXACTLY)) {
+ // child was measured once already above...
+ // base new measurement on stored values
+ int childHeight = child.getMeasuredHeight() + share;
+ if (childHeight < 0) {
+ childHeight = 0;
+ }
+
+ child.measure(childWidthMeasureSpec,
+ MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY));
+ } else {
+ // child was skipped in the loop above.
+ // Measure for this first time here
+ child.measure(childWidthMeasureSpec,
+ MeasureSpec.makeMeasureSpec(share > 0 ? share : 0,
+ MeasureSpec.EXACTLY));
+ }
+ }
+
+ final int margin = lp.leftMargin + lp.rightMargin;
+ final int measuredWidth = child.getMeasuredWidth() + margin;
+ maxWidth = Math.max(maxWidth, measuredWidth);
+
+ boolean matchWidthLocally = widthMode != MeasureSpec.EXACTLY &&
+ lp.width == LayoutParams.FILL_PARENT;
+
+ alternativeMaxWidth = Math.max(alternativeMaxWidth,
+ matchWidthLocally ? margin : measuredWidth);
+
+ allFillParent = allFillParent && lp.width == LayoutParams.FILL_PARENT;
+ alternativeMaxWidth = Math.max(alternativeMaxWidth,
+ matchWidthLocally ? margin : measuredWidth);
+
+ mTotalLength += child.getMeasuredHeight() + lp.topMargin +
+ lp.bottomMargin + getNextLocationOffset(child);
+ }
+
+ // Add in our padding
+ mTotalLength += mPaddingTop + mPaddingBottom;
+ } else {
+ alternativeMaxWidth = Math.max(alternativeMaxWidth,
+ weightedMaxWidth);
+ }
+
+ if (!allFillParent && widthMode != MeasureSpec.EXACTLY) {
+ maxWidth = alternativeMaxWidth;
+ }
+
+ maxWidth += mPaddingLeft + mPaddingRight;
+
+ // Check against our minimum width
+ maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
+
+ setMeasuredDimension(resolveSize(maxWidth, widthMeasureSpec), heightSize);
+
+ if (matchWidth) {
+ forceUniformWidth(count, heightMeasureSpec);
+ }
+ }
+
+ private void forceUniformWidth(int count, int heightMeasureSpec) {
+ // Pretend that the linear layout has an exact size.
+ int uniformMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth(),
+ MeasureSpec.EXACTLY);
+ for (int i = 0; i< count; ++i) {
+ final View child = getVirtualChildAt(i);
+ if (child.getVisibility() != GONE) {
+ LinearLayout.LayoutParams lp = ((LinearLayout.LayoutParams)child.getLayoutParams());
+
+ if (lp.width == LayoutParams.FILL_PARENT) {
+ // Temporarily force children to reuse their old measured height
+ // FIXME: this may not be right for something like wrapping text?
+ int oldHeight = lp.height;
+ lp.height = child.getMeasuredHeight();
+
+ // Remeasue with new dimensions
+ measureChildWithMargins(child, uniformMeasureSpec, 0, heightMeasureSpec, 0);
+ lp.height = oldHeight;
+ }
+ }
+ }
+ }
+
+ /**
+ * Measures the children when the orientation of this LinearLayout is set
+ * to {@link #HORIZONTAL}.
+ *
+ * @param widthMeasureSpec Horizontal space requirements as imposed by the parent.
+ * @param heightMeasureSpec Vertical space requirements as imposed by the parent.
+ *
+ * @see #getOrientation()
+ * @see #setOrientation(int)
+ * @see #onMeasure(int, int)
+ */
+ void measureHorizontal(int widthMeasureSpec, int heightMeasureSpec) {
+ mTotalLength = 0;
+ int maxHeight = 0;
+ int alternativeMaxHeight = 0;
+ int weightedMaxHeight = 0;
+ boolean allFillParent = true;
+ float totalWeight = 0;
+
+ final int count = getVirtualChildCount();
+
+ final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+
+ boolean matchHeight = false;
+
+ if (mMaxAscent == null || mMaxDescent == null) {
+ mMaxAscent = new int[VERTICAL_GRAVITY_COUNT];
+ mMaxDescent = new int[VERTICAL_GRAVITY_COUNT];
+ }
+
+ final int[] maxAscent = mMaxAscent;
+ final int[] maxDescent = mMaxDescent;
+
+ maxAscent[0] = maxAscent[1] = maxAscent[2] = maxAscent[3] = -1;
+ maxDescent[0] = maxDescent[1] = maxDescent[2] = maxDescent[3] = -1;
+
+ final boolean baselineAligned = mBaselineAligned;
+
+ // See how wide everyone is. Also remember max height.
+ for (int i = 0; i < count; ++i) {
+ final View child = getVirtualChildAt(i);
+
+ if (child == null) {
+ mTotalLength += measureNullChild(i);
+ continue;
+ }
+
+ if (child.getVisibility() == GONE) {
+ i += getChildrenSkipCount(child, i);
+ continue;
+ }
+
+ final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
+
+ totalWeight += lp.weight;
+
+ if (widthMode == MeasureSpec.EXACTLY && lp.width == 0 && lp.weight > 0) {
+ // Optimization: don't bother measuring children who are going to use
+ // leftover space. These views will get measured again down below if
+ // there is any leftover space.
+ mTotalLength += lp.leftMargin + lp.rightMargin;
+
+ // Baseline alignment requires to measure widgets to obtain the
+ // baseline offset (in particular for TextViews).
+ // The following defeats the optimization mentioned above.
+ // Allow the child to use as much space as it wants because we
+ // can shrink things later (and re-measure).
+ if (baselineAligned) {
+ final int freeSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ child.measure(freeSpec, freeSpec);
+ }
+ } else {
+ int oldWidth = Integer.MIN_VALUE;
+
+ if (lp.width == 0 && lp.weight > 0) {
+ // widthMode is either UNSPECIFIED OR AT_MOST, and this child
+ // wanted to stretch to fill available space. Translate that to
+ // WRAP_CONTENT so that it does not end up with a width of 0
+ oldWidth = lp.width;
+ lp.width = LayoutParams.WRAP_CONTENT;
+ }
+
+ // Determine how big this child would like to be. If this or
+ // previous children have given a weight, then we allow it to
+ // use all available space (and we will shrink things later
+ // if needed).
+ measureChildBeforeLayout(child, i, widthMeasureSpec,
+ totalWeight == 0 ? mTotalLength : 0,
+ heightMeasureSpec, 0);
+
+ if (oldWidth != Integer.MIN_VALUE) {
+ lp.width = oldWidth;
+ }
+
+ mTotalLength += child.getMeasuredWidth() + lp.leftMargin +
+ lp.rightMargin + getNextLocationOffset(child);
+ }
+
+ boolean matchHeightLocally = false;
+ if (heightMode != MeasureSpec.EXACTLY && lp.height == LayoutParams.FILL_PARENT) {
+ // The height of the linear layout will scale, and at least one
+ // child said it wanted to match our height. Set a flag indicating that
+ // we need to remeasure at least that view when we know our height.
+ matchHeight = true;
+ matchHeightLocally = true;
+ }
+
+ final int margin = lp.topMargin + lp.bottomMargin;
+ final int childHeight = child.getMeasuredHeight() + margin;
+
+ if (baselineAligned) {
+ final int childBaseline = child.getBaseline();
+ if (childBaseline != -1) {
+ // Translates the child's vertical gravity into an index
+ // in the range 0..VERTICAL_GRAVITY_COUNT
+ final int gravity = (lp.gravity < 0 ? mGravity : lp.gravity)
+ & Gravity.VERTICAL_GRAVITY_MASK;
+ final int index = ((gravity >> Gravity.AXIS_Y_SHIFT)
+ & ~Gravity.AXIS_SPECIFIED) >> 1;
+
+ maxAscent[index] = Math.max(maxAscent[index], childBaseline);
+ maxDescent[index] = Math.max(maxDescent[index], childHeight - childBaseline);
+ }
+ }
+
+ maxHeight = Math.max(maxHeight, childHeight);
+
+ allFillParent = allFillParent && lp.height == LayoutParams.FILL_PARENT;
+ if (lp.weight > 0) {
+ /*
+ * Heights of weighted Views are bogus if we end up
+ * remeasuring, so keep them separate.
+ */
+ weightedMaxHeight = Math.max(weightedMaxHeight,
+ matchHeightLocally ? margin : childHeight);
+ } else {
+ alternativeMaxHeight = Math.max(alternativeMaxHeight,
+ matchHeightLocally ? margin : childHeight);
+ }
+
+ i += getChildrenSkipCount(child, i);
+ }
+
+ // Check mMaxAscent[INDEX_TOP] first because it maps to Gravity.TOP,
+ // the most common case
+ if (maxAscent[INDEX_TOP] != -1 ||
+ maxAscent[INDEX_CENTER_VERTICAL] != -1 ||
+ maxAscent[INDEX_BOTTOM] != -1 ||
+ maxAscent[INDEX_FILL] != -1) {
+ final int ascent = Math.max(maxAscent[INDEX_FILL],
+ Math.max(maxAscent[INDEX_CENTER_VERTICAL],
+ Math.max(maxAscent[INDEX_TOP], maxAscent[INDEX_BOTTOM])));
+ final int descent = Math.max(maxDescent[INDEX_FILL],
+ Math.max(maxDescent[INDEX_CENTER_VERTICAL],
+ Math.max(maxDescent[INDEX_TOP], maxDescent[INDEX_BOTTOM])));
+ maxHeight = Math.max(maxHeight, ascent + descent);
+ }
+
+ // Add in our padding
+ mTotalLength += mPaddingLeft + mPaddingRight;
+
+ int widthSize = mTotalLength;
+
+ // Check against our minimum width
+ widthSize = Math.max(widthSize, getSuggestedMinimumWidth());
+
+ // Reconcile our calculated size with the widthMeasureSpec
+ widthSize = resolveSize(widthSize, widthMeasureSpec);
+
+ // Either expand children with weight to take up available space or
+ // shrink them if they extend beyond our current bounds
+ int delta = widthSize - mTotalLength;
+ if (delta != 0 && totalWeight > 0.0f) {
+ float weightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;
+
+ maxAscent[0] = maxAscent[1] = maxAscent[2] = maxAscent[3] = -1;
+ maxDescent[0] = maxDescent[1] = maxDescent[2] = maxDescent[3] = -1;
+ maxHeight = -1;
+
+ mTotalLength = 0;
+
+ for (int i = 0; i < count; ++i) {
+ final View child = getVirtualChildAt(i);
+
+ if (child == null || child.getVisibility() == View.GONE) {
+ continue;
+ }
+
+ final LinearLayout.LayoutParams lp =
+ (LinearLayout.LayoutParams) child.getLayoutParams();
+
+ float childExtra = lp.weight;
+ if (childExtra > 0) {
+ // Child said it could absorb extra space -- give him his share
+ int share = (int) (childExtra * delta / weightSum);
+ weightSum -= childExtra;
+ delta -= share;
+
+ final int childHeightMeasureSpec = getChildMeasureSpec(
+ heightMeasureSpec,
+ mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin,
+ lp.height);
+
+ // TODO: Use a field like lp.isMeasured to figure out if this
+ // child has been previously measured
+ if ((lp.width != 0) || (widthMode != MeasureSpec.EXACTLY)) {
+ // child was measured once already above ... base new measurement
+ // on stored values
+ int childWidth = child.getMeasuredWidth() + share;
+ if (childWidth < 0) {
+ childWidth = 0;
+ }
+
+ child.measure(
+ MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY),
+ childHeightMeasureSpec);
+ } else {
+ // child was skipped in the loop above. Measure for this first time here
+ child.measure(MeasureSpec.makeMeasureSpec(
+ share > 0 ? share : 0, MeasureSpec.EXACTLY),
+ childHeightMeasureSpec);
+ }
+ }
+
+ mTotalLength += child.getMeasuredWidth() + lp.leftMargin +
+ lp.rightMargin + getNextLocationOffset(child);
+
+ boolean matchHeightLocally = heightMode != MeasureSpec.EXACTLY &&
+ lp.height == LayoutParams.FILL_PARENT;
+
+ final int margin = lp.topMargin + lp .bottomMargin;
+ int childHeight = child.getMeasuredHeight() + margin;
+ maxHeight = Math.max(maxHeight, childHeight);
+ alternativeMaxHeight = Math.max(alternativeMaxHeight,
+ matchHeightLocally ? margin : childHeight);
+
+ allFillParent = allFillParent && lp.height == LayoutParams.FILL_PARENT;
+ alternativeMaxHeight = Math.max(alternativeMaxHeight,
+ matchHeightLocally ? margin : childHeight);
+
+ if (baselineAligned) {
+ final int childBaseline = child.getBaseline();
+ if (childBaseline != -1) {
+ // Translates the child's vertical gravity into an index in the range 0..2
+ final int gravity = (lp.gravity < 0 ? mGravity : lp.gravity)
+ & Gravity.VERTICAL_GRAVITY_MASK;
+ final int index = ((gravity >> Gravity.AXIS_Y_SHIFT)
+ & ~Gravity.AXIS_SPECIFIED) >> 1;
+
+ maxAscent[index] = Math.max(maxAscent[index], childBaseline);
+ maxDescent[index] = Math.max(maxDescent[index],
+ childHeight - childBaseline);
+ }
+ }
+ }
+
+ // Add in our padding
+ mTotalLength += mPaddingLeft + mPaddingRight;
+
+ // Check mMaxAscent[INDEX_TOP] first because it maps to Gravity.TOP,
+ // the most common case
+ if (maxAscent[INDEX_TOP] != -1 ||
+ maxAscent[INDEX_CENTER_VERTICAL] != -1 ||
+ maxAscent[INDEX_BOTTOM] != -1 ||
+ maxAscent[INDEX_FILL] != -1) {
+ final int ascent = Math.max(maxAscent[INDEX_FILL],
+ Math.max(maxAscent[INDEX_CENTER_VERTICAL],
+ Math.max(maxAscent[INDEX_TOP], maxAscent[INDEX_BOTTOM])));
+ final int descent = Math.max(maxDescent[INDEX_FILL],
+ Math.max(maxDescent[INDEX_CENTER_VERTICAL],
+ Math.max(maxDescent[INDEX_TOP], maxDescent[INDEX_BOTTOM])));
+ maxHeight = Math.max(maxHeight, ascent + descent);
+ }
+ } else {
+ alternativeMaxHeight = Math.max(alternativeMaxHeight,
+ weightedMaxHeight);
+ }
+
+ if (!allFillParent && heightMode != MeasureSpec.EXACTLY) {
+ maxHeight = alternativeMaxHeight;
+ }
+
+ maxHeight += mPaddingTop + mPaddingBottom;
+
+ // Check against our minimum height
+ maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
+
+ setMeasuredDimension(widthSize, resolveSize(maxHeight, heightMeasureSpec));
+
+ if (matchHeight) {
+ forceUniformHeight(count, widthMeasureSpec);
+ }
+ }
+
+ private void forceUniformHeight(int count, int widthMeasureSpec) {
+ // Pretend that the linear layout has an exact size. This is the measured height of
+ // ourselves. The measured height should be the max height of the children, changed
+ // to accomodate the heightMesureSpec from the parent
+ int uniformMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight(),
+ MeasureSpec.EXACTLY);
+ for (int i = 0; i < count; ++i) {
+ final View child = getVirtualChildAt(i);
+ if (child.getVisibility() != GONE) {
+ LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
+
+ if (lp.height == LayoutParams.FILL_PARENT) {
+ // Temporarily force children to reuse their old measured width
+ // FIXME: this may not be right for something like wrapping text?
+ int oldWidth = lp.width;
+ lp.width = child.getMeasuredWidth();
+
+ // Remeasure with new dimensions
+ measureChildWithMargins(child, widthMeasureSpec, 0, uniformMeasureSpec, 0);
+ lp.width = oldWidth;
+ }
+ }
+ }
+ }
+
+ /**
+ * <p>Returns the number of children to skip after measuring/laying out
+ * the specified child.</p>
+ *
+ * @param child the child after which we want to skip children
+ * @param index the index of the child after which we want to skip children
+ * @return the number of children to skip, 0 by default
+ */
+ int getChildrenSkipCount(View child, int index) {
+ return 0;
+ }
+
+ /**
+ * <p>Returns the size (width or height) that should be occupied by a null
+ * child.</p>
+ *
+ * @param childIndex the index of the null child
+ * @return the width or height of the child depending on the orientation
+ */
+ int measureNullChild(int childIndex) {
+ return 0;
+ }
+
+ /**
+ * <p>Measure the child according to the parent's measure specs. This
+ * method should be overriden by subclasses to force the sizing of
+ * children. This method is called by {@link #measureVertical(int, int)} and
+ * {@link #measureHorizontal(int, int)}.</p>
+ *
+ * @param child the child to measure
+ * @param childIndex the index of the child in this view
+ * @param widthMeasureSpec horizontal space requirements as imposed by the parent
+ * @param totalWidth extra space that has been used up by the parent horizontally
+ * @param heightMeasureSpec vertical space requirements as imposed by the parent
+ * @param totalHeight extra space that has been used up by the parent vertically
+ */
+ void measureChildBeforeLayout(View child, int childIndex,
+ int widthMeasureSpec, int totalWidth, int heightMeasureSpec,
+ int totalHeight) {
+ measureChildWithMargins(child, widthMeasureSpec, totalWidth,
+ heightMeasureSpec, totalHeight);
+ }
+
+ /**
+ * <p>Return the location offset of the specified child. This can be used
+ * by subclasses to change the location of a given widget.</p>
+ *
+ * @param child the child for which to obtain the location offset
+ * @return the location offset in pixels
+ */
+ int getLocationOffset(View child) {
+ return 0;
+ }
+
+ /**
+ * <p>Return the size offset of the next sibling of the specified child.
+ * This can be used by subclasses to change the location of the widget
+ * following <code>child</code>.</p>
+ *
+ * @param child the child whose next sibling will be moved
+ * @return the location offset of the next child in pixels
+ */
+ int getNextLocationOffset(View child) {
+ return 0;
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ if (mOrientation == VERTICAL) {
+ layoutVertical();
+ } else {
+ layoutHorizontal();
+ }
+ }
+
+ /**
+ * Position the children during a layout pass if the orientation of this
+ * LinearLayout is set to {@link #VERTICAL}.
+ *
+ * @see #getOrientation()
+ * @see #setOrientation(int)
+ * @see #onLayout(boolean, int, int, int, int)
+ */
+ void layoutVertical() {
+ final int paddingLeft = mPaddingLeft;
+
+ int childTop = mPaddingTop;
+ int childLeft = paddingLeft;
+
+ // Where right end of child should go
+ final int width = mRight - mLeft;
+ int childRight = width - mPaddingRight;
+
+ // Space available for child
+ int childSpace = width - paddingLeft - mPaddingRight;
+
+ final int count = getVirtualChildCount();
+
+ final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
+ final int minorGravity = mGravity & Gravity.HORIZONTAL_GRAVITY_MASK;
+
+ if (majorGravity != Gravity.TOP) {
+ switch (majorGravity) {
+ case Gravity.BOTTOM:
+ childTop = mBottom - mTop - mPaddingBottom - mTotalLength;
+ break;
+
+ case Gravity.CENTER_VERTICAL:
+ childTop += ((mBottom - mTop - mPaddingTop - mPaddingBottom) -
+ mTotalLength) / 2;
+ break;
+ }
+
+ }
+
+ for (int i = 0; i < count; i++) {
+ final View child = getVirtualChildAt(i);
+ if (child == null) {
+ childTop += measureNullChild(i);
+ } else if (child.getVisibility() != GONE) {
+ final int childWidth = child.getMeasuredWidth();
+ final int childHeight = child.getMeasuredHeight();
+
+ final LinearLayout.LayoutParams lp =
+ (LinearLayout.LayoutParams) child.getLayoutParams();
+
+ int gravity = lp.gravity;
+ if (gravity < 0) {
+ gravity = minorGravity;
+ }
+
+ switch (gravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
+ case Gravity.LEFT:
+ childLeft = paddingLeft + lp.leftMargin;
+ break;
+
+ case Gravity.CENTER_HORIZONTAL:
+ childLeft = paddingLeft + ((childSpace - childWidth) / 2)
+ + lp.leftMargin - lp.rightMargin;
+ break;
+
+ case Gravity.RIGHT:
+ childLeft = childRight - childWidth - lp.rightMargin;
+ break;
+ }
+
+
+ childTop += lp.topMargin;
+ setChildFrame(child, childLeft, childTop + getLocationOffset(child),
+ childWidth, childHeight);
+ childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
+
+ i += getChildrenSkipCount(child, i);
+ }
+ }
+ }
+
+ /**
+ * Position the children during a layout pass if the orientation of this
+ * LinearLayout is set to {@link #HORIZONTAL}.
+ *
+ * @see #getOrientation()
+ * @see #setOrientation(int)
+ * @see #onLayout(boolean, int, int, int, int)
+ */
+ void layoutHorizontal() {
+ final int paddingTop = mPaddingTop;
+
+ int childTop = paddingTop;
+ int childLeft = mPaddingLeft;
+
+ // Where bottom of child should go
+ final int height = mBottom - mTop;
+ int childBottom = height - mPaddingBottom;
+
+ // Space available for child
+ int childSpace = height - paddingTop - mPaddingBottom;
+
+ final int count = getVirtualChildCount();
+
+ final int majorGravity = mGravity & Gravity.HORIZONTAL_GRAVITY_MASK;
+ final int minorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
+
+ final boolean baselineAligned = mBaselineAligned;
+
+ final int[] maxAscent = mMaxAscent;
+ final int[] maxDescent = mMaxDescent;
+
+ if (majorGravity != Gravity.LEFT) {
+ switch (majorGravity) {
+ case Gravity.RIGHT:
+ childLeft = mRight - mLeft - mPaddingRight - mTotalLength;
+ break;
+
+ case Gravity.CENTER_HORIZONTAL:
+ childLeft += ((mRight - mLeft - mPaddingLeft - mPaddingRight) -
+ mTotalLength) / 2;
+ break;
+ }
+ }
+
+ for (int i = 0; i < count; i++) {
+ final View child = getVirtualChildAt(i);
+
+ if (child == null) {
+ childLeft += measureNullChild(i);
+ } else if (child.getVisibility() != GONE) {
+ final int childWidth = child.getMeasuredWidth();
+ final int childHeight = child.getMeasuredHeight();
+ int childBaseline = -1;
+
+ final LinearLayout.LayoutParams lp =
+ (LinearLayout.LayoutParams) child.getLayoutParams();
+
+ if (baselineAligned && lp.height != LayoutParams.FILL_PARENT) {
+ childBaseline = child.getBaseline();
+ }
+
+ int gravity = lp.gravity;
+ if (gravity < 0) {
+ gravity = minorGravity;
+ }
+
+ switch (gravity & Gravity.VERTICAL_GRAVITY_MASK) {
+ case Gravity.TOP:
+ childTop = paddingTop + lp.topMargin;
+ if (childBaseline != -1) {
+ childTop += maxAscent[INDEX_TOP] - childBaseline;
+ }
+ break;
+
+ case Gravity.CENTER_VERTICAL:
+ // Removed support for baselign alignment when layout_gravity or
+ // gravity == center_vertical. See bug #1038483.
+ // Keep the code around if we need to re-enable this feature
+ // if (childBaseline != -1) {
+ // // Align baselines vertically only if the child is smaller than us
+ // if (childSpace - childHeight > 0) {
+ // childTop = paddingTop + (childSpace / 2) - childBaseline;
+ // } else {
+ // childTop = paddingTop + (childSpace - childHeight) / 2;
+ // }
+ // } else {
+ childTop = paddingTop + ((childSpace - childHeight) / 2)
+ + lp.topMargin - lp.bottomMargin;
+ break;
+
+ case Gravity.BOTTOM:
+ childTop = childBottom - childHeight - lp.bottomMargin;
+ if (childBaseline != -1) {
+ int descent = child.getMeasuredHeight() - childBaseline;
+ childTop -= (maxDescent[INDEX_BOTTOM] - descent);
+ }
+ break;
+ }
+
+ childLeft += lp.leftMargin;
+ setChildFrame(child, childLeft + getLocationOffset(child), childTop,
+ childWidth, childHeight);
+ childLeft += childWidth + lp.rightMargin +
+ getNextLocationOffset(child);
+
+ i += getChildrenSkipCount(child, i);
+ }
+ }
+ }
+
+ private void setChildFrame(View child, int left, int top, int width, int height) {
+ child.layout(left, top, left + width, top + height);
+ }
+
+ /**
+ * Should the layout be a column or a row.
+ * @param orientation Pass HORIZONTAL or VERTICAL. Default
+ * value is HORIZONTAL.
+ *
+ * @attr ref android.R.styleable#LinearLayout_orientation
+ */
+ public void setOrientation(int orientation) {
+ if (mOrientation != orientation) {
+ mOrientation = orientation;
+ requestLayout();
+ }
+ }
+
+ /**
+ * Returns the current orientation.
+ *
+ * @return either {@link #HORIZONTAL} or {@link #VERTICAL}
+ */
+ public int getOrientation() {
+ return mOrientation;
+ }
+
+ /**
+ * Describes how the child views are positioned. Defaults to GRAVITY_TOP. If
+ * this layout has a VERTICAL orientation, this controls where all the child
+ * views are placed if there is extra vertical space. If this layout has a
+ * HORIZONTAL orientation, this controls the alignment of the children.
+ *
+ * @param gravity See {@link android.view.Gravity}
+ *
+ * @attr ref android.R.styleable#LinearLayout_gravity
+ */
+ public void setGravity(int gravity) {
+ if (mGravity != gravity) {
+ if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == 0) {
+ gravity |= Gravity.LEFT;
+ }
+
+ if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == 0) {
+ gravity |= Gravity.TOP;
+ }
+
+ mGravity = gravity;
+ requestLayout();
+ }
+ }
+
+ public void setHorizontalGravity(int horizontalGravity) {
+ final int gravity = horizontalGravity & Gravity.HORIZONTAL_GRAVITY_MASK;
+ if ((mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != gravity) {
+ mGravity = (mGravity & ~Gravity.HORIZONTAL_GRAVITY_MASK) | gravity;
+ requestLayout();
+ }
+ }
+
+ public void setVerticalGravity(int verticalGravity) {
+ final int gravity = verticalGravity & Gravity.VERTICAL_GRAVITY_MASK;
+ if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != gravity) {
+ mGravity = (mGravity & ~Gravity.VERTICAL_GRAVITY_MASK) | gravity;
+ requestLayout();
+ }
+ }
+
+ @Override
+ public LayoutParams generateLayoutParams(AttributeSet attrs) {
+ return new LinearLayout.LayoutParams(getContext(), attrs);
+ }
+
+ /**
+ * Returns a set of layout parameters with a width of
+ * {@link android.view.ViewGroup.LayoutParams#FILL_PARENT}
+ * and a height of {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}
+ * when the layout's orientation is {@link #VERTICAL}. When the orientation is
+ * {@link #HORIZONTAL}, the width is set to {@link LayoutParams#WRAP_CONTENT}
+ * and the height to {@link LayoutParams#WRAP_CONTENT}.
+ */
+ @Override
+ protected LayoutParams generateDefaultLayoutParams() {
+ if (mOrientation == HORIZONTAL) {
+ return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+ } else if (mOrientation == VERTICAL) {
+ return new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT);
+ }
+ return null;
+ }
+
+ @Override
+ protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
+ return new LayoutParams(p);
+ }
+
+
+ // Override to allow type-checking of LayoutParams.
+ @Override
+ protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+ return p instanceof LinearLayout.LayoutParams;
+ }
+
+ /**
+ * Per-child layout information associated with ViewLinearLayout.
+ *
+ * @attr ref android.R.styleable#LinearLayout_Layout_layout_weight
+ * @attr ref android.R.styleable#LinearLayout_Layout_layout_gravity
+ */
+ public static class LayoutParams extends ViewGroup.MarginLayoutParams {
+ /**
+ * Indicates how much of the extra space in the LinearLayout will be
+ * allocated to the view associated with these LayoutParams. Specify
+ * 0 if the view should not be stretched. Otherwise the extra pixels
+ * will be pro-rated among all views whose weight is greater than 0.
+ */
+ @ViewDebug.ExportedProperty
+ public float weight;
+
+ /**
+ * Gravity for the view associated with these LayoutParams.
+ *
+ * @see android.view.Gravity
+ */
+ @ViewDebug.ExportedProperty(mapping = {
+ @ViewDebug.IntToString(from = -1, to = "NONE"),
+ @ViewDebug.IntToString(from = Gravity.NO_GRAVITY, to = "NONE"),
+ @ViewDebug.IntToString(from = Gravity.TOP, to = "TOP"),
+ @ViewDebug.IntToString(from = Gravity.BOTTOM, to = "BOTTOM"),
+ @ViewDebug.IntToString(from = Gravity.LEFT, to = "LEFT"),
+ @ViewDebug.IntToString(from = Gravity.RIGHT, to = "RIGHT"),
+ @ViewDebug.IntToString(from = Gravity.CENTER_VERTICAL, to = "CENTER_VERTICAL"),
+ @ViewDebug.IntToString(from = Gravity.FILL_VERTICAL, to = "FILL_VERTICAL"),
+ @ViewDebug.IntToString(from = Gravity.CENTER_HORIZONTAL, to = "CENTER_HORIZONTAL"),
+ @ViewDebug.IntToString(from = Gravity.FILL_HORIZONTAL, to = "FILL_HORIZONTAL"),
+ @ViewDebug.IntToString(from = Gravity.CENTER, to = "CENTER"),
+ @ViewDebug.IntToString(from = Gravity.FILL, to = "FILL")
+ })
+ public int gravity = -1;
+
+ /**
+ * {@inheritDoc}
+ */
+ public LayoutParams(Context c, AttributeSet attrs) {
+ super(c, attrs);
+ TypedArray a =
+ c.obtainStyledAttributes(attrs, com.android.internal.R.styleable.LinearLayout_Layout);
+
+ weight = a.getFloat(com.android.internal.R.styleable.LinearLayout_Layout_layout_weight, 0);
+ gravity = a.getInt(com.android.internal.R.styleable.LinearLayout_Layout_layout_gravity, -1);
+
+ a.recycle();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public LayoutParams(int width, int height) {
+ super(width, height);
+ weight = 0;
+ }
+
+ /**
+ * Creates a new set of layout parameters with the specified width, height
+ * and weight.
+ *
+ * @param width the width, either {@link #FILL_PARENT},
+ * {@link #WRAP_CONTENT} or a fixed size in pixels
+ * @param height the height, either {@link #FILL_PARENT},
+ * {@link #WRAP_CONTENT} or a fixed size in pixels
+ * @param weight the weight
+ */
+ public LayoutParams(int width, int height, float weight) {
+ super(width, height);
+ this.weight = weight;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public LayoutParams(ViewGroup.LayoutParams p) {
+ super(p);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public LayoutParams(MarginLayoutParams source) {
+ super(source);
+ }
+
+ @Override
+ public String debug(String output) {
+ return output + "LinearLayout.LayoutParams={width=" + sizeToString(width) +
+ ", height=" + sizeToString(height) + " weight=" + weight + "}";
+ }
+ }
+}
diff --git a/core/java/android/widget/ListAdapter.java b/core/java/android/widget/ListAdapter.java
new file mode 100644
index 0000000..a035145
--- /dev/null
+++ b/core/java/android/widget/ListAdapter.java
@@ -0,0 +1,44 @@
+/*
+ * 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.widget;
+
+/**
+ * Extended {@link Adapter} that is the bridge between a {@link ListView}
+ * and the data that backs the list. Frequently that data comes from a Cursor,
+ * but that is not
+ * required. The ListView can display any data provided that it is wrapped in a
+ * ListAdapter.
+ */
+public interface ListAdapter extends Adapter {
+
+ /**
+ * Are all items in this ListAdapter enabled?
+ * If yes it means all items are selectable and clickable.
+ *
+ * @return True if all items are enabled
+ */
+ public boolean areAllItemsEnabled();
+
+ /**
+ * Returns true if the item at the specified position is not a separator.
+ * (A separator is a non-selectable, non-clickable item).
+ *
+ * @param position Index of the item
+ * @return True if the item is not a separator
+ */
+ boolean isEnabled(int position);
+}
diff --git a/core/java/android/widget/ListView.java b/core/java/android/widget/ListView.java
new file mode 100644
index 0000000..d52e51f
--- /dev/null
+++ b/core/java/android/widget/ListView.java
@@ -0,0 +1,3204 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.SparseBooleanArray;
+import android.util.SparseArray;
+import android.view.FocusFinder;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewDebug;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.SoundEffectConstants;
+
+import com.google.android.collect.Lists;
+
+import java.util.ArrayList;
+
+/*
+ * Implementation Notes:
+ *
+ * Some terminology:
+ *
+ * index - index of the items that are currently visible
+ * position - index of the items in the cursor
+ */
+
+
+/**
+ * A view that shows items in a vertically scrolling list. The items
+ * come from the {@link ListAdapter} associated with this view.
+ *
+ * @attr ref android.R.styleable#ListView_entries
+ * @attr ref android.R.styleable#ListView_divider
+ * @attr ref android.R.styleable#ListView_dividerHeight
+ * @attr ref android.R.styleable#ListView_choiceMode
+ */
+public class ListView extends AbsListView {
+ /**
+ * Used to indicate a no preference for a position type.
+ */
+ static final int NO_POSITION = -1;
+
+ /**
+ * Normal list that does not indicate choices
+ */
+ public static final int CHOICE_MODE_NONE = 0;
+
+ /**
+ * The list allows up to one choice
+ */
+ public static final int CHOICE_MODE_SINGLE = 1;
+
+ /**
+ * The list allows multiple choices
+ */
+ public static final int CHOICE_MODE_MULTIPLE = 2;
+
+ /**
+ * When arrow scrolling, ListView will never scroll more than this factor
+ * times the height of the list.
+ */
+ private static final float MAX_SCROLL_FACTOR = 0.33f;
+
+ /**
+ * When arrow scrolling, need a certain amount of pixels to preview next
+ * items. This is usually the fading edge, but if that is small enough,
+ * we want to make sure we preview at least this many pixels.
+ */
+ private static final int MIN_SCROLL_PREVIEW_PIXELS = 2;
+
+ // TODO: document
+ class FixedViewInfo {
+ public View view;
+ public Object data;
+ public boolean isSelectable;
+ }
+
+ private ArrayList<FixedViewInfo> mHeaderViewInfos = Lists.newArrayList();
+ private ArrayList<FixedViewInfo> mFooterViewInfos = Lists.newArrayList();
+
+ Drawable mDivider;
+ int mDividerHeight;
+
+ private boolean mAreAllItemsSelectable = true;
+
+ private boolean mItemsCanFocus = false;
+
+ private int mChoiceMode = CHOICE_MODE_NONE;
+
+ private SparseBooleanArray mCheckStates;
+
+ // used for temporary calculations.
+ private Rect mTempRect = new Rect();
+
+ /**
+ * Used to save / restore the state of the focused child in {@link #layoutChildren()}
+ */
+ private SparseArray<Parcelable> mfocusRestoreChildState = new SparseArray<Parcelable>();
+
+
+ // the single allocated result per list view; kinda cheesey but avoids
+ // allocating these thingies too often.
+ private ArrowScrollFocusResult mArrowScrollFocusResult = new ArrowScrollFocusResult();
+
+ public ListView(Context context) {
+ this(context, null);
+ }
+
+ public ListView(Context context, AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.listViewStyle);
+ }
+
+ public ListView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ TypedArray a =
+ context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.ListView, defStyle, 0);
+
+ CharSequence[] entries = a.getTextArray(
+ com.android.internal.R.styleable.ListView_entries);
+ if (entries != null) {
+ setAdapter(new ArrayAdapter<CharSequence>(context,
+ com.android.internal.R.layout.simple_list_item_1, entries));
+ }
+
+ final Drawable d = a.getDrawable(com.android.internal.R.styleable.ListView_divider);
+ if (d != null) {
+
+ // If a divider is specified use its intrinsic height for divider height
+ setDivider(d);
+ } else {
+
+ // Else use the height specified, zero being the default
+ final int dividerHeight = a.getDimensionPixelSize(
+ com.android.internal.R.styleable.ListView_dividerHeight, 0);
+ if (dividerHeight != 0) {
+ setDividerHeight(dividerHeight);
+ }
+ }
+
+ a.recycle();
+ }
+
+ /**
+ * @return The maximum amount a list view will scroll in response to
+ * an arrow event.
+ */
+ public int getMaxScrollAmount() {
+ return (int) (MAX_SCROLL_FACTOR * (mBottom - mTop));
+ }
+
+ /**
+ * Make sure views are touching the top or bottom edge, as appropriate for
+ * our gravity
+ */
+ private void adjustViewsUpOrDown() {
+ final int childCount = getChildCount();
+ int delta;
+
+ if (childCount > 0) {
+ View child;
+
+ if (!mStackFromBottom) {
+ // Uh-oh -- we came up short. Slide all views up to make them
+ // align with the top
+ child = getChildAt(0);
+ delta = child.getTop() - mListPadding.top;
+ if (mFirstPosition != 0) {
+ // It's OK to have some space above the first item if it is
+ // part of the vertical spacing
+ delta -= mDividerHeight;
+ }
+ if (delta < 0) {
+ // We only are looking to see if we are too low, not too high
+ delta = 0;
+ }
+ }
+ else {
+ // we are too high, slide all views down to align with bottom
+ child = getChildAt(childCount - 1);
+ delta = child.getBottom() - (getHeight() - mListPadding.bottom);
+
+ if (mFirstPosition + childCount < mItemCount) {
+ // It's OK to have some space below the last item if it is
+ // part of the vertical spacing
+ delta += mDividerHeight;
+ }
+
+ if (delta > 0) {
+ delta = 0;
+ }
+ }
+
+ if (delta != 0) {
+ offsetChildrenTopAndBottom(-delta);
+ }
+ }
+ }
+
+ /**
+ * Add a fixed view to appear at the top of the list. If addHeaderView is
+ * called more than once, the views will appear in the order they were
+ * added. Views added using this call can take focus if they want.
+ * <p>
+ * NOTE: Call this before calling setAdapter. This is so ListView can wrap
+ * the supplied cursor with one that that will also account for header
+ * views.
+ *
+ * @param v The view to add.
+ * @param data Data to associate with this view
+ * @param isSelectable whether the item is selectable
+ */
+ public void addHeaderView(View v, Object data, boolean isSelectable) {
+
+ if (mAdapter != null) {
+ throw new IllegalStateException(
+ "Cannot add header view to list -- setAdapter has already been called.");
+ }
+
+ FixedViewInfo info = new FixedViewInfo();
+ info.view = v;
+ info.data = data;
+ info.isSelectable = isSelectable;
+ mHeaderViewInfos.add(info);
+ }
+
+ /**
+ * Add a fixed view to appear at the top of the list. If addHeaderView is
+ * called more than once, the views will appear in the order they were
+ * added. Views added using this call can take focus if they want.
+ * <p>
+ * NOTE: Call this before calling setAdapter. This is so ListView can wrap
+ * the supplied cursor with one that that will also account for header
+ * views.
+ *
+ * @param v The view to add.
+ */
+ public void addHeaderView(View v) {
+ addHeaderView(v, null, true);
+ }
+
+ @Override
+ public int getHeaderViewsCount() {
+ return mHeaderViewInfos.size();
+ }
+
+ /**
+ * Removes a previously-added header view.
+ *
+ * @param v The view to remove
+ * @return true if the view was removed, false if the view was not a header
+ * view
+ */
+ public boolean removeHeaderView(View v) {
+ if (mHeaderViewInfos.size() > 0) {
+ boolean result = false;
+ if (((HeaderViewListAdapter) mAdapter).removeHeader(v)) {
+ mDataSetObserver.onChanged();
+ result = true;
+ }
+ removeFixedViewInfo(v, mHeaderViewInfos);
+ return result;
+ }
+ return false;
+ }
+
+ private void removeFixedViewInfo(View v, ArrayList<FixedViewInfo> where) {
+ int len = where.size();
+ for (int i = 0; i < len; ++i) {
+ FixedViewInfo info = where.get(i);
+ if (info.view == v) {
+ where.remove(i);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Add a fixed view to appear at the bottom of the list. If addFooterView is
+ * called more than once, the views will appear in the order they were
+ * added. Views added using this call can take focus if they want.
+ * <p>
+ * NOTE: Call this before calling setAdapter. This is so ListView can wrap
+ * the supplied cursor with one that that will also account for header
+ * views.
+ *
+ * @param v The view to add.
+ * @param data Data to associate with this view
+ * @param isSelectable true if the footer view can be selected
+ */
+ public void addFooterView(View v, Object data, boolean isSelectable) {
+ FixedViewInfo info = new FixedViewInfo();
+ info.view = v;
+ info.data = data;
+ info.isSelectable = isSelectable;
+ mFooterViewInfos.add(info);
+
+ // in the case of re-adding a footer view, or adding one later on,
+ // we need to notify the observer
+ if (mDataSetObserver != null) {
+ mDataSetObserver.onChanged();
+ }
+ }
+
+ /**
+ * Add a fixed view to appear at the bottom of the list. If addFooterView is called more
+ * than once, the views will appear in the order they were added. Views added using
+ * this call can take focus if they want.
+ * <p>NOTE: Call this before calling setAdapter. This is so ListView can wrap the supplied
+ * cursor with one that that will also account for header views.
+ *
+ *
+ * @param v The view to add.
+ */
+ public void addFooterView(View v) {
+ addFooterView(v, null, true);
+ }
+
+ @Override
+ public int getFooterViewsCount() {
+ return mFooterViewInfos.size();
+ }
+
+ /**
+ * Removes a previously-added footer view.
+ *
+ * @param v The view to remove
+ * @return
+ * true if the view was removed, false if the view was not a footer view
+ */
+ public boolean removeFooterView(View v) {
+ if (mFooterViewInfos.size() > 0) {
+ boolean result = false;
+ if (((HeaderViewListAdapter) mAdapter).removeFooter(v)) {
+ mDataSetObserver.onChanged();
+ result = true;
+ }
+ removeFixedViewInfo(v, mFooterViewInfos);
+ return result;
+ }
+ return false;
+ }
+
+ /**
+ * Returns the adapter currently in use in this ListView. The returned adapter
+ * might not be the same adapter passed to {@link #setAdapter(ListAdapter)} but
+ * might be a {@link WrapperListAdapter}.
+ *
+ * @return The adapter currently used to display data in this ListView.
+ *
+ * @see #setAdapter(ListAdapter)
+ */
+ @Override
+ public ListAdapter getAdapter() {
+ return mAdapter;
+ }
+
+ /**
+ * Sets the data behind this ListView.
+ *
+ * The adapter passed to this method may be wrapped by a {@link WrapperListAdapter},
+ * depending on the ListView features currently in use. For instance, adding
+ * headers and/or footers will cause the adapter to be wrapped.
+ *
+ * @param adapter The ListAdapter which is responsible for maintaining the
+ * data backing this list and for producing a view to represent an
+ * item in that data set.
+ *
+ * @see #getAdapter()
+ */
+ @Override
+ public void setAdapter(ListAdapter adapter) {
+ if (null != mAdapter) {
+ mAdapter.unregisterDataSetObserver(mDataSetObserver);
+ }
+
+ resetList();
+ mRecycler.clear();
+
+ if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) {
+ mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, adapter);
+ } else {
+ mAdapter = adapter;
+ }
+
+ mOldSelectedPosition = INVALID_POSITION;
+ mOldSelectedRowId = INVALID_ROW_ID;
+ if (mAdapter != null) {
+ mAreAllItemsSelectable = mAdapter.areAllItemsEnabled();
+ mOldItemCount = mItemCount;
+ mItemCount = mAdapter.getCount();
+ checkFocus();
+
+ mDataSetObserver = new AdapterDataSetObserver();
+ mAdapter.registerDataSetObserver(mDataSetObserver);
+
+ mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());
+
+ int position;
+ if (mStackFromBottom) {
+ position = lookForSelectablePosition(mItemCount - 1, false);
+ } else {
+ position = lookForSelectablePosition(0, true);
+ }
+ setSelectedPositionInt(position);
+ setNextSelectedPositionInt(position);
+
+ if (mItemCount == 0) {
+ // Nothing selected
+ checkSelectionChanged();
+ }
+
+ } else {
+ mAreAllItemsSelectable = true;
+ checkFocus();
+ // Nothing selected
+ checkSelectionChanged();
+ }
+
+ if (mCheckStates != null) {
+ mCheckStates.clear();
+ }
+
+ requestLayout();
+ }
+
+
+ /**
+ * The list is empty. Clear everything out.
+ */
+ @Override
+ void resetList() {
+ super.resetList();
+ mLayoutMode = LAYOUT_NORMAL;
+ }
+
+ /**
+ * @return Whether the list needs to show the top fading edge
+ */
+ private boolean showingTopFadingEdge() {
+ final int listTop = mScrollY + mListPadding.top;
+ return (mFirstPosition > 0) || (getChildAt(0).getTop() > listTop);
+ }
+
+ /**
+ * @return Whether the list needs to show the bottom fading edge
+ */
+ private boolean showingBottomFadingEdge() {
+ final int childCount = getChildCount();
+ final int bottomOfBottomChild = getChildAt(childCount - 1).getBottom();
+ final int lastVisiblePosition = mFirstPosition + childCount - 1;
+
+ final int listBottom = mScrollY + getHeight() - mListPadding.bottom;
+
+ return (lastVisiblePosition < mItemCount - 1)
+ || (bottomOfBottomChild < listBottom);
+ }
+
+
+ @Override
+ public boolean requestChildRectangleOnScreen(View child, Rect rect, boolean immediate) {
+
+ int rectTopWithinChild = rect.top;
+
+ // offset so rect is in coordinates of the this view
+ rect.offset(child.getLeft(), child.getTop());
+ rect.offset(-child.getScrollX(), -child.getScrollY());
+
+ final int height = getHeight();
+ int listUnfadedTop = getScrollY();
+ int listUnfadedBottom = listUnfadedTop + height;
+ final int fadingEdge = getVerticalFadingEdgeLength();
+
+ if (showingTopFadingEdge()) {
+ // leave room for top fading edge as long as rect isn't at very top
+ if ((mSelectedPosition > 0) || (rectTopWithinChild > fadingEdge)) {
+ listUnfadedTop += fadingEdge;
+ }
+ }
+
+ int childCount = getChildCount();
+ int bottomOfBottomChild = getChildAt(childCount - 1).getBottom();
+
+ if (showingBottomFadingEdge()) {
+ // leave room for bottom fading edge as long as rect isn't at very bottom
+ if ((mSelectedPosition < mItemCount - 1)
+ || (rect.bottom < (bottomOfBottomChild - fadingEdge))) {
+ listUnfadedBottom -= fadingEdge;
+ }
+ }
+
+ int scrollYDelta = 0;
+
+ if (rect.bottom > listUnfadedBottom && rect.top > listUnfadedTop) {
+ // need to MOVE DOWN to get it in view: move down just enough so
+ // that the entire rectangle is in view (or at least the first
+ // screen size chunk).
+
+ if (rect.height() > height) {
+ // just enough to get screen size chunk on
+ scrollYDelta += (rect.top - listUnfadedTop);
+ } else {
+ // get entire rect at bottom of screen
+ scrollYDelta += (rect.bottom - listUnfadedBottom);
+ }
+
+ // make sure we aren't scrolling beyond the end of our children
+ int distanceToBottom = bottomOfBottomChild - listUnfadedBottom;
+ scrollYDelta = Math.min(scrollYDelta, distanceToBottom);
+ } else if (rect.top < listUnfadedTop && rect.bottom < listUnfadedBottom) {
+ // need to MOVE UP to get it in view: move up just enough so that
+ // entire rectangle is in view (or at least the first screen
+ // size chunk of it).
+
+ if (rect.height() > height) {
+ // screen size chunk
+ scrollYDelta -= (listUnfadedBottom - rect.bottom);
+ } else {
+ // entire rect at top
+ scrollYDelta -= (listUnfadedTop - rect.top);
+ }
+
+ // make sure we aren't scrolling any further than the top our children
+ int top = getChildAt(0).getTop();
+ int deltaToTop = top - listUnfadedTop;
+ scrollYDelta = Math.max(scrollYDelta, deltaToTop);
+ }
+
+ final boolean scroll = scrollYDelta != 0;
+ if (scroll) {
+ scrollListItemsBy(-scrollYDelta);
+ positionSelector(child);
+ mSelectedTop = child.getTop();
+ invalidate();
+ }
+ return scroll;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ void fillGap(boolean down) {
+ final int count = getChildCount();
+ if (down) {
+ final int startOffset = count > 0 ? getChildAt(count - 1).getBottom() + mDividerHeight :
+ getListPaddingTop();
+ fillDown(mFirstPosition + count, startOffset);
+ correctTooHigh(getChildCount());
+ } else {
+ final int startOffset = count > 0 ? getChildAt(0).getTop() - mDividerHeight :
+ getHeight() - getListPaddingBottom();
+ fillUp(mFirstPosition - 1, startOffset);
+ correctTooLow(getChildCount());
+ }
+ }
+
+ /**
+ * Fills the list from pos down to the end of the list view.
+ *
+ * @param pos The first position to put in the list
+ *
+ * @param nextTop The location where the top of the item associated with pos
+ * should be drawn
+ *
+ * @return The view that is currently selected, if it happens to be in the
+ * range that we draw.
+ */
+ private View fillDown(int pos, int nextTop) {
+ View selectedView = null;
+
+ int end = (mBottom - mTop) - mListPadding.bottom;
+
+ while (nextTop < end && pos < mItemCount) {
+ // is this the selected item?
+ boolean selected = pos == mSelectedPosition;
+ View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);
+
+ nextTop = child.getBottom() + mDividerHeight;
+ if (selected) {
+ selectedView = child;
+ }
+ pos++;
+ }
+
+ return selectedView;
+ }
+
+ /**
+ * Fills the list from pos up to the top of the list view.
+ *
+ * @param pos The first position to put in the list
+ *
+ * @param nextBottom The location where the bottom of the item associated
+ * with pos should be drawn
+ *
+ * @return The view that is currently selected
+ */
+ private View fillUp(int pos, int nextBottom) {
+ View selectedView = null;
+
+ int end = mListPadding.top;
+
+ while (nextBottom > end && pos >= 0) {
+ // is this the selected item?
+ boolean selected = pos == mSelectedPosition;
+ View child = makeAndAddView(pos, nextBottom, false, mListPadding.left, selected);
+ nextBottom = child.getTop() - mDividerHeight;
+ if (selected) {
+ selectedView = child;
+ }
+ pos--;
+ }
+
+ mFirstPosition = pos + 1;
+
+ return selectedView;
+ }
+
+ /**
+ * Fills the list from top to bottom, starting with mFirstPosition
+ *
+ * @param nextTop The location where the top of the first item should be
+ * drawn
+ *
+ * @return The view that is currently selected
+ */
+ private View fillFromTop(int nextTop) {
+ mFirstPosition = Math.min(mFirstPosition, mSelectedPosition);
+ mFirstPosition = Math.min(mFirstPosition, mItemCount - 1);
+ if (mFirstPosition < 0) {
+ mFirstPosition = 0;
+ }
+ return fillDown(mFirstPosition, nextTop);
+ }
+
+
+ /**
+ * Put mSelectedPosition in the middle of the screen and then build up and
+ * down from there. This method forces mSelectedPosition to the center.
+ *
+ * @param childrenTop Top of the area in which children can be drawn, as
+ * measured in pixels
+ * @param childrenBottom Bottom of the area in which children can be drawn,
+ * as measured in pixels
+ * @return Currently selected view
+ */
+ private View fillFromMiddle(int childrenTop, int childrenBottom) {
+ int height = childrenBottom - childrenTop;
+
+ int position = reconcileSelectedPosition();
+
+ View sel = makeAndAddView(position, childrenTop, true,
+ mListPadding.left, true);
+ mFirstPosition = position;
+
+ int selHeight = sel.getMeasuredHeight();
+ if (selHeight <= height) {
+ sel.offsetTopAndBottom((height - selHeight) / 2);
+ }
+
+ fillAboveAndBelow(sel, position);
+
+ if (!mStackFromBottom) {
+ correctTooHigh(getChildCount());
+ } else {
+ correctTooLow(getChildCount());
+ }
+
+ return sel;
+ }
+
+ /**
+ * Once the selected view as been placed, fill up the visible area above and
+ * below it.
+ *
+ * @param sel The selected view
+ * @param position The position corresponding to sel
+ */
+ private void fillAboveAndBelow(View sel, int position) {
+ final int dividerHeight = mDividerHeight;
+ if (!mStackFromBottom) {
+ fillUp(position - 1, sel.getTop() - dividerHeight);
+ adjustViewsUpOrDown();
+ fillDown(position + 1, sel.getBottom() + dividerHeight);
+ } else {
+ fillDown(position + 1, sel.getBottom() + dividerHeight);
+ adjustViewsUpOrDown();
+ fillUp(position - 1, sel.getTop() - dividerHeight);
+ }
+ }
+
+
+ /**
+ * Fills the grid based on positioning the new selection at a specific
+ * location. The selection may be moved so that it does not intersect the
+ * faded edges. The grid is then filled upwards and downwards from there.
+ *
+ * @param selectedTop Where the selected item should be
+ * @param childrenTop Where to start drawing children
+ * @param childrenBottom Last pixel where children can be drawn
+ * @return The view that currently has selection
+ */
+ private View fillFromSelection(int selectedTop, int childrenTop, int childrenBottom) {
+ int fadingEdgeLength = getVerticalFadingEdgeLength();
+ final int selectedPosition = mSelectedPosition;
+
+ View sel;
+
+ final int topSelectionPixel = getTopSelectionPixel(childrenTop, fadingEdgeLength,
+ selectedPosition);
+ final int bottomSelectionPixel = getBottomSelectionPixel(childrenBottom, fadingEdgeLength,
+ selectedPosition);
+
+ sel = makeAndAddView(selectedPosition, selectedTop, true, mListPadding.left, true);
+
+
+ // Some of the newly selected item extends below the bottom of the list
+ if (sel.getBottom() > bottomSelectionPixel) {
+ // Find space available above the selection into which we can scroll
+ // upwards
+ final int spaceAbove = sel.getTop() - topSelectionPixel;
+
+ // Find space required to bring the bottom of the selected item
+ // fully into view
+ final int spaceBelow = sel.getBottom() - bottomSelectionPixel;
+ final int offset = Math.min(spaceAbove, spaceBelow);
+
+ // Now offset the selected item to get it into view
+ sel.offsetTopAndBottom(-offset);
+ } else if (sel.getTop() < topSelectionPixel) {
+ // Find space required to bring the top of the selected item fully
+ // into view
+ final int spaceAbove = topSelectionPixel - sel.getTop();
+
+ // Find space available below the selection into which we can scroll
+ // downwards
+ final int spaceBelow = bottomSelectionPixel - sel.getBottom();
+ final int offset = Math.min(spaceAbove, spaceBelow);
+
+ // Offset the selected item to get it into view
+ sel.offsetTopAndBottom(offset);
+ }
+
+ // Fill in views above and below
+ fillAboveAndBelow(sel, selectedPosition);
+
+ if (!mStackFromBottom) {
+ correctTooHigh(getChildCount());
+ } else {
+ correctTooLow(getChildCount());
+ }
+
+ return sel;
+ }
+
+ /**
+ * Calculate the bottom-most pixel we can draw the selection into
+ *
+ * @param childrenBottom Bottom pixel were children can be drawn
+ * @param fadingEdgeLength Length of the fading edge in pixels, if present
+ * @param selectedPosition The position that will be selected
+ * @return The bottom-most pixel we can draw the selection into
+ */
+ private int getBottomSelectionPixel(int childrenBottom, int fadingEdgeLength,
+ int selectedPosition) {
+ int bottomSelectionPixel = childrenBottom;
+ if (selectedPosition != mItemCount - 1) {
+ bottomSelectionPixel -= fadingEdgeLength;
+ }
+ return bottomSelectionPixel;
+ }
+
+ /**
+ * Calculate the top-most pixel we can draw the selection into
+ *
+ * @param childrenTop Top pixel were children can be drawn
+ * @param fadingEdgeLength Length of the fading edge in pixels, if present
+ * @param selectedPosition The position that will be selected
+ * @return The top-most pixel we can draw the selection into
+ */
+ private int getTopSelectionPixel(int childrenTop, int fadingEdgeLength, int selectedPosition) {
+ // first pixel we can draw the selection into
+ int topSelectionPixel = childrenTop;
+ if (selectedPosition > 0) {
+ topSelectionPixel += fadingEdgeLength;
+ }
+ return topSelectionPixel;
+ }
+
+
+ /**
+ * Fills the list based on positioning the new selection relative to the old
+ * selection. The new selection will be placed at, above, or below the
+ * location of the new selection depending on how the selection is moving.
+ * The selection will then be pinned to the visible part of the screen,
+ * excluding the edges that are faded. The list is then filled upwards and
+ * downwards from there.
+ *
+ * @param oldSel The old selected view. Useful for trying to put the new
+ * selection in the same place
+ * @param newSel The view that is to become selected. Useful for trying to
+ * put the new selection in the same place
+ * @param delta Which way we are moving
+ * @param childrenTop Where to start drawing children
+ * @param childrenBottom Last pixel where children can be drawn
+ * @return The view that currently has selection
+ */
+ private View moveSelection(View oldSel, View newSel, int delta, int childrenTop,
+ int childrenBottom) {
+ int fadingEdgeLength = getVerticalFadingEdgeLength();
+ final int selectedPosition = mSelectedPosition;
+
+ View sel;
+
+ final int topSelectionPixel = getTopSelectionPixel(childrenTop, fadingEdgeLength,
+ selectedPosition);
+ final int bottomSelectionPixel = getBottomSelectionPixel(childrenTop, fadingEdgeLength,
+ selectedPosition);
+
+ if (delta > 0) {
+ /*
+ * Case 1: Scrolling down.
+ */
+
+ /*
+ * Before After
+ * | | | |
+ * +-------+ +-------+
+ * | A | | A |
+ * | 1 | => +-------+
+ * +-------+ | B |
+ * | B | | 2 |
+ * +-------+ +-------+
+ * | | | |
+ *
+ * Try to keep the top of the previously selected item where it was.
+ * oldSel = A
+ * sel = B
+ */
+
+ // Put oldSel (A) where it belongs
+ oldSel = makeAndAddView(selectedPosition - 1, oldSel.getTop(), true,
+ mListPadding.left, false);
+
+ final int dividerHeight = mDividerHeight;
+
+ // Now put the new selection (B) below that
+ sel = makeAndAddView(selectedPosition, oldSel.getBottom() + dividerHeight, true,
+ mListPadding.left, true);
+
+ // Some of the newly selected item extends below the bottom of the list
+ if (sel.getBottom() > bottomSelectionPixel) {
+
+ // Find space available above the selection into which we can scroll upwards
+ int spaceAbove = sel.getTop() - topSelectionPixel;
+
+ // Find space required to bring the bottom of the selected item fully into view
+ int spaceBelow = sel.getBottom() - bottomSelectionPixel;
+
+ // Don't scroll more than half the height of the list
+ int halfVerticalSpace = (childrenBottom - childrenTop) / 2;
+ int offset = Math.min(spaceAbove, spaceBelow);
+ offset = Math.min(offset, halfVerticalSpace);
+
+ // We placed oldSel, so offset that item
+ oldSel.offsetTopAndBottom(-offset);
+ // Now offset the selected item to get it into view
+ sel.offsetTopAndBottom(-offset);
+ }
+
+ // Fill in views above and below
+ if (!mStackFromBottom) {
+ fillUp(mSelectedPosition - 2, sel.getTop() - dividerHeight);
+ adjustViewsUpOrDown();
+ fillDown(mSelectedPosition + 1, sel.getBottom() + dividerHeight);
+ } else {
+ fillDown(mSelectedPosition + 1, sel.getBottom() + dividerHeight);
+ adjustViewsUpOrDown();
+ fillUp(mSelectedPosition - 2, sel.getTop() - dividerHeight);
+ }
+ } else if (delta < 0) {
+ /*
+ * Case 2: Scrolling up.
+ */
+
+ /*
+ * Before After
+ * | | | |
+ * +-------+ +-------+
+ * | A | | A |
+ * +-------+ => | 1 |
+ * | B | +-------+
+ * | 2 | | B |
+ * +-------+ +-------+
+ * | | | |
+ *
+ * Try to keep the top of the item about to become selected where it was.
+ * newSel = A
+ * olSel = B
+ */
+
+ if (newSel != null) {
+ // Try to position the top of newSel (A) where it was before it was selected
+ sel = makeAndAddView(selectedPosition, newSel.getTop(), true, mListPadding.left,
+ true);
+ } else {
+ // If (A) was not on screen and so did not have a view, position
+ // it above the oldSel (B)
+ sel = makeAndAddView(selectedPosition, oldSel.getTop(), false, mListPadding.left,
+ true);
+ }
+
+ // Some of the newly selected item extends above the top of the list
+ if (sel.getTop() < topSelectionPixel) {
+ // Find space required to bring the top of the selected item fully into view
+ int spaceAbove = topSelectionPixel - sel.getTop();
+
+ // Find space available below the selection into which we can scroll downwards
+ int spaceBelow = bottomSelectionPixel - sel.getBottom();
+
+ // Don't scroll more than half the height of the list
+ int halfVerticalSpace = (childrenBottom - childrenTop) / 2;
+ int offset = Math.min(spaceAbove, spaceBelow);
+ offset = Math.min(offset, halfVerticalSpace);
+
+ // Offset the selected item to get it into view
+ sel.offsetTopAndBottom(offset);
+ }
+
+ // Fill in views above and below
+ fillAboveAndBelow(sel, selectedPosition);
+ } else {
+
+ int oldTop = oldSel.getTop();
+
+ /*
+ * Case 3: Staying still
+ */
+ sel = makeAndAddView(selectedPosition, oldTop, true, mListPadding.left, true);
+
+ // We're staying still...
+ if (oldTop < childrenTop) {
+ // ... but the top of the old selection was off screen.
+ // (This can happen if the data changes size out from under us)
+ int newBottom = sel.getBottom();
+ if (newBottom < childrenTop + 20) {
+ // Not enough visible -- bring it onscreen
+ sel.offsetTopAndBottom(childrenTop - sel.getTop());
+ }
+ }
+
+ // Fill in views above and below
+ fillAboveAndBelow(sel, selectedPosition);
+ }
+
+ return sel;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ // Sets up mListPadding
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+ int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+
+ int childWidth = 0;
+ int childHeight = 0;
+
+ mItemCount = mAdapter == null ? 0 : mAdapter.getCount();
+ if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED ||
+ heightMode == MeasureSpec.UNSPECIFIED)) {
+ final View child = obtainView(0);
+ final int childViewType = mAdapter.getItemViewType(0);
+
+ AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams();
+ if (lp == null) {
+ lp = new AbsListView.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT, 0);
+ child.setLayoutParams(lp);
+ }
+ lp.viewType = childViewType;
+
+ final int childWidthSpec = ViewGroup.getChildMeasureSpec(widthMeasureSpec,
+ mListPadding.left + mListPadding.right, lp.width);
+
+ int lpHeight = lp.height;
+
+ int childHeightSpec;
+ if (lpHeight > 0) {
+ childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
+ } else {
+ childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ }
+
+ child.measure(childWidthSpec, childHeightSpec);
+
+ childWidth = child.getMeasuredWidth();
+ childHeight = child.getMeasuredHeight();
+
+ if (mRecycler.shouldRecycleViewType(childViewType)) {
+ mRecycler.addScrapView(child);
+ }
+ }
+
+ if (widthMode == MeasureSpec.UNSPECIFIED) {
+ widthSize = mListPadding.left + mListPadding.right + childWidth +
+ getVerticalScrollbarWidth();
+ }
+
+ if (heightMode == MeasureSpec.UNSPECIFIED) {
+ heightSize = mListPadding.top + mListPadding.bottom + childHeight +
+ getVerticalFadingEdgeLength() * 2;
+ }
+
+ if (heightMode == MeasureSpec.AT_MOST) {
+ // TODO: after first layout we should maybe start at the first visible position, not 0
+ heightSize = measureHeightOfChildren(
+ MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY),
+ 0, NO_POSITION, heightSize, -1);
+ }
+
+ setMeasuredDimension(widthSize, heightSize);
+ mWidthMeasureSpec = widthMeasureSpec;
+ }
+
+ /**
+ * Measures the height of the given range of children (inclusive) and
+ * returns the height with this ListView's padding and divider heights
+ * included. If maxHeight is provided, the measuring will stop when the
+ * current height reaches maxHeight.
+ *
+ * @param widthMeasureSpec The width measure spec to be given to a child's
+ * {@link View#measure(int, int)}.
+ * @param startPosition The position of the first child to be shown.
+ * @param endPosition The (inclusive) position of the last child to be
+ * shown. Specify {@link #NO_POSITION} if the last child should be
+ * the last available child from the adapter.
+ * @param maxHeight The maximum height that will be returned (if all the
+ * children don't fit in this value, this value will be
+ * returned).
+ * @param disallowPartialChildPosition In general, whether the returned
+ * height should only contain entire children. This is more
+ * powerful--it is the first inclusive position at which partial
+ * children will not be allowed. Example: it looks nice to have
+ * at least 3 completely visible children, and in portrait this
+ * will most likely fit; but in landscape there could be times
+ * when even 2 children can not be completely shown, so a value
+ * of 2 (remember, inclusive) would be good (assuming
+ * startPosition is 0).
+ * @return The height of this ListView with the given children.
+ */
+ final int measureHeightOfChildren(final int widthMeasureSpec, final int startPosition,
+ int endPosition, final int maxHeight, int disallowPartialChildPosition) {
+
+ final ListAdapter adapter = mAdapter;
+ if (adapter == null) {
+ return mListPadding.top + mListPadding.bottom;
+ }
+
+ // Include the padding of the list
+ int returnedHeight = mListPadding.top + mListPadding.bottom;
+ final int dividerHeight = ((mDividerHeight > 0) && mDivider != null) ? mDividerHeight : 0;
+ // The previous height value that was less than maxHeight and contained
+ // no partial children
+ int prevHeightWithoutPartialChild = 0;
+ int i;
+ View child;
+
+ // mItemCount - 1 since endPosition parameter is inclusive
+ endPosition = (endPosition == NO_POSITION) ? adapter.getCount() - 1 : endPosition;
+ final AbsListView.RecycleBin recycleBin = mRecycler;
+ for (i = startPosition; i <= endPosition; ++i) {
+ child = obtainView(i);
+ final int childViewType = adapter.getItemViewType(i);
+
+ AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams();
+ if (lp == null) {
+ lp = new AbsListView.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT, 0);
+ child.setLayoutParams(lp);
+ }
+ lp.viewType = childViewType;
+
+ if (i > 0) {
+ // Count the divider for all but one child
+ returnedHeight += dividerHeight;
+ }
+
+ child.measure(widthMeasureSpec, lp.height >= 0
+ ? MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY)
+ : MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+
+ // Recycle the view before we possibly return from the method
+ if (recycleBin.shouldRecycleViewType(childViewType)) {
+ recycleBin.addScrapView(child);
+ }
+
+ returnedHeight += child.getMeasuredHeight();
+
+ if (returnedHeight >= maxHeight) {
+ // We went over, figure out which height to return. If returnedHeight > maxHeight,
+ // then the i'th position did not fit completely.
+ return (disallowPartialChildPosition >= 0) // Disallowing is enabled (> -1)
+ && (i > disallowPartialChildPosition) // We've past the min pos
+ && (prevHeightWithoutPartialChild > 0) // We have a prev height
+ && (returnedHeight != maxHeight) // i'th child did not fit completely
+ ? prevHeightWithoutPartialChild
+ : maxHeight;
+ }
+
+ if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) {
+ prevHeightWithoutPartialChild = returnedHeight;
+ }
+ }
+
+ // At this point, we went through the range of children, and they each
+ // completely fit, so return the returnedHeight
+ return returnedHeight;
+ }
+
+ @Override
+ int findMotionRow(int y) {
+ int childCount = getChildCount();
+ if (childCount > 0) {
+ for (int i = 0; i < childCount; i++) {
+ View v = getChildAt(i);
+ if (y <= v.getBottom()) {
+ return mFirstPosition + i;
+ }
+ }
+ return mFirstPosition + childCount - 1;
+ }
+ return INVALID_POSITION;
+ }
+
+ /**
+ * Put a specific item at a specific location on the screen and then build
+ * up and down from there.
+ *
+ * @param position The reference view to use as the starting point
+ * @param top Pixel offset from the top of this view to the top of the
+ * reference view.
+ *
+ * @return The selected view, or null if the selected view is outside the
+ * visible area.
+ */
+ private View fillSpecific(int position, int top) {
+ boolean tempIsSelected = position == mSelectedPosition;
+ View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected);
+ // Possibly changed again in fillUp if we add rows above this one.
+ mFirstPosition = position;
+
+ View above;
+ View below;
+
+ final int dividerHeight = mDividerHeight;
+ if (!mStackFromBottom) {
+ above = fillUp(position - 1, temp.getTop() - dividerHeight);
+ // This will correct for the top of the first view not touching the top of the list
+ adjustViewsUpOrDown();
+ below = fillDown(position + 1, temp.getBottom() + dividerHeight);
+ int childCount = getChildCount();
+ if (childCount > 0) {
+ correctTooHigh(childCount);
+ }
+ } else {
+ below = fillDown(position + 1, temp.getBottom() + dividerHeight);
+ // This will correct for the bottom of the last view not touching the bottom of the list
+ adjustViewsUpOrDown();
+ above = fillUp(position - 1, temp.getTop() - dividerHeight);
+ int childCount = getChildCount();
+ if (childCount > 0) {
+ correctTooLow(childCount);
+ }
+ }
+
+ if (tempIsSelected) {
+ return temp;
+ } else if (above != null) {
+ return above;
+ } else {
+ return below;
+ }
+ }
+
+ /**
+ * Check if we have dragged the bottom of the list too high (we have pushed the
+ * top element off the top of the screen when we did not need to). Correct by sliding
+ * everything back down.
+ *
+ * @param childCount Number of children
+ */
+ private void correctTooHigh(int childCount) {
+ // First see if the last item is visible. If it is not, it is OK for the
+ // top of the list to be pushed up.
+ int lastPosition = mFirstPosition + childCount - 1;
+ if (lastPosition == mItemCount - 1 && childCount > 0) {
+
+ // Get the last child ...
+ final View lastChild = getChildAt(childCount - 1);
+
+ // ... and its bottom edge
+ final int lastBottom = lastChild.getBottom();
+
+ // This is bottom of our drawable area
+ final int end = (mBottom - mTop) - mListPadding.bottom;
+
+ // This is how far the bottom edge of the last view is from the bottom of the
+ // drawable area
+ int bottomOffset = end - lastBottom;
+ View firstChild = getChildAt(0);
+ final int firstTop = firstChild.getTop();
+
+ // Make sure we are 1) Too high, and 2) Either there are more rows above the
+ // first row or the first row is scrolled off the top of the drawable area
+ if (bottomOffset > 0 && (mFirstPosition > 0 || firstTop < mListPadding.top)) {
+ if (mFirstPosition == 0) {
+ // Don't pull the top too far down
+ bottomOffset = Math.min(bottomOffset, mListPadding.top - firstTop);
+ }
+ // Move everything down
+ offsetChildrenTopAndBottom(bottomOffset);
+ if (mFirstPosition > 0) {
+ // Fill the gap that was opened above mFirstPosition with more rows, if
+ // possible
+ fillUp(mFirstPosition - 1, firstChild.getTop() - mDividerHeight);
+ // Close up the remaining gap
+ adjustViewsUpOrDown();
+ }
+
+ }
+ }
+ }
+
+ /**
+ * Check if we have dragged the bottom of the list too low (we have pushed the
+ * bottom element off the bottom of the screen when we did not need to). Correct by sliding
+ * everything back up.
+ *
+ * @param childCount Number of children
+ */
+ private void correctTooLow(int childCount) {
+ // First see if the first item is visible. If it is not, it is OK for the
+ // bottom of the list to be pushed down.
+ if (mFirstPosition == 0 && childCount > 0) {
+
+ // Get the first child ...
+ final View firstChild = getChildAt(0);
+
+ // ... and its top edge
+ final int firstTop = firstChild.getTop();
+
+ // This is top of our drawable area
+ final int start = mListPadding.top;
+
+ // This is bottom of our drawable area
+ final int end = (mBottom - mTop) - mListPadding.bottom;
+
+ // This is how far the top edge of the first view is from the top of the
+ // drawable area
+ int topOffset = firstTop - start;
+ View lastChild = getChildAt(childCount - 1);
+ final int lastBottom = lastChild.getBottom();
+ int lastPosition = mFirstPosition + childCount - 1;
+
+ // Make sure we are 1) Too low, and 2) Either there are more rows below the
+ // last row or the last row is scrolled off the bottom of the drawable area
+ if (topOffset > 0 && (lastPosition < mItemCount - 1 || lastBottom > end)) {
+ if (lastPosition == mItemCount - 1 ) {
+ // Don't pull the bottom too far up
+ topOffset = Math.min(topOffset, lastBottom - end);
+ }
+ // Move everything up
+ offsetChildrenTopAndBottom(-topOffset);
+ if (lastPosition < mItemCount - 1) {
+ // Fill the gap that was opened below the last position with more rows, if
+ // possible
+ fillDown(lastPosition + 1, lastChild.getBottom() + mDividerHeight);
+ // Close up the remaining gap
+ adjustViewsUpOrDown();
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void layoutChildren() {
+ final boolean blockLayoutRequests = mBlockLayoutRequests;
+ if (!blockLayoutRequests) {
+ mBlockLayoutRequests = true;
+ }
+
+ try {
+ super.layoutChildren();
+
+ invalidate();
+
+ if (mAdapter == null) {
+ resetList();
+ invokeOnItemScrollListener();
+ return;
+ }
+
+ int childrenTop = mListPadding.top;
+ int childrenBottom = mBottom - mTop - mListPadding.bottom;
+
+ int childCount = getChildCount();
+ int index;
+ int delta = 0;
+
+ View sel;
+ View oldSel = null;
+ View oldFirst = null;
+ View newSel = null;
+
+ View focusLayoutRestoreView = null;
+
+ // Remember stuff we will need down below
+ switch (mLayoutMode) {
+ case LAYOUT_SET_SELECTION:
+ index = mNextSelectedPosition - mFirstPosition;
+ if (index >= 0 && index < childCount) {
+ newSel = getChildAt(index);
+ }
+ break;
+ case LAYOUT_FORCE_TOP:
+ case LAYOUT_FORCE_BOTTOM:
+ case LAYOUT_SPECIFIC:
+ case LAYOUT_SYNC:
+ break;
+ case LAYOUT_MOVE_SELECTION:
+ default:
+ // Remember the previously selected view
+ index = mSelectedPosition - mFirstPosition;
+ if (index >= 0 && index < childCount) {
+ oldSel = getChildAt(index);
+ }
+
+ // Remember the previous first child
+ oldFirst = getChildAt(0);
+
+ if (mNextSelectedPosition >= 0) {
+ delta = mNextSelectedPosition - mSelectedPosition;
+ }
+
+ // Caution: newSel might be null
+ newSel = getChildAt(index + delta);
+ }
+
+
+ boolean dataChanged = mDataChanged;
+ if (dataChanged) {
+ handleDataChanged();
+ }
+
+ // Handle the empty set by removing all views that are visible
+ // and calling it a day
+ if (mItemCount == 0) {
+ resetList();
+ invokeOnItemScrollListener();
+ return;
+ }
+
+ setSelectedPositionInt(mNextSelectedPosition);
+
+ // Pull all children into the RecycleBin.
+ // These views will be reused if possible
+ final int firstPosition = mFirstPosition;
+ final RecycleBin recycleBin = mRecycler;
+
+ // reset the focus restoration
+ View focusLayoutRestoreDirectChild = null;
+
+
+ // Don't put header or footer views into the Recycler. Those are
+ // already cached in mHeaderViews;
+ if (dataChanged) {
+ for (int i = 0; i < childCount; i++) {
+ recycleBin.addScrapView(getChildAt(i));
+ if (ViewDebug.TRACE_RECYCLER) {
+ ViewDebug.trace(getChildAt(i),
+ ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, index, i);
+ }
+ }
+ } else {
+ recycleBin.fillActiveViews(childCount, firstPosition);
+ }
+
+ // take focus back to us temporarily to avoid the eventual
+ // call to clear focus when removing the focused child below
+ // from messing things up when ViewRoot assigns focus back
+ // to someone else
+ final View focusedChild = getFocusedChild();
+ if (focusedChild != null) {
+ // TODO: in some cases focusedChild.getParent() == null
+
+ // we can remember the focused view to restore after relayout if the
+ // data hasn't changed, or if the focused position is a header or footer
+ if (!dataChanged || isDirectChildHeaderOrFooter(focusedChild)) {
+ focusLayoutRestoreDirectChild = getFocusedChild();
+ if (focusLayoutRestoreDirectChild != null) {
+
+ // remember its state
+ focusLayoutRestoreDirectChild.saveHierarchyState(mfocusRestoreChildState);
+
+ // remember the specific view that had focus
+ focusLayoutRestoreView = findFocus();
+ }
+ }
+ requestFocus();
+ }
+
+ // Clear out old views
+ //removeAllViewsInLayout();
+ detachAllViewsFromParent();
+
+ switch (mLayoutMode) {
+ case LAYOUT_SET_SELECTION:
+ if (newSel != null) {
+ sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom);
+ } else {
+ sel = fillFromMiddle(childrenTop, childrenBottom);
+ }
+ break;
+ case LAYOUT_SYNC:
+ sel = fillSpecific(mSyncPosition, mSpecificTop);
+ break;
+ case LAYOUT_FORCE_BOTTOM:
+ sel = fillUp(mItemCount - 1, childrenBottom);
+ adjustViewsUpOrDown();
+ break;
+ case LAYOUT_FORCE_TOP:
+ mFirstPosition = 0;
+ sel = fillFromTop(childrenTop);
+ adjustViewsUpOrDown();
+ break;
+ case LAYOUT_SPECIFIC:
+ sel = fillSpecific(reconcileSelectedPosition(), mSpecificTop);
+ break;
+ case LAYOUT_MOVE_SELECTION:
+ sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom);
+ break;
+ default:
+ if (childCount == 0) {
+ if (!mStackFromBottom) {
+ final int position = lookForSelectablePosition(0, true);
+ setSelectedPositionInt(position);
+ sel = fillFromTop(childrenTop);
+ } else {
+ final int position = lookForSelectablePosition(mItemCount - 1, false);
+ setSelectedPositionInt(position);
+ sel = fillUp(mItemCount - 1, childrenBottom);
+ }
+ } else {
+ if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
+ sel = fillSpecific(mSelectedPosition,
+ oldSel == null ? childrenTop : oldSel.getTop());
+ } else if (mFirstPosition < mItemCount) {
+ sel = fillSpecific(mFirstPosition,
+ oldFirst == null ? childrenTop : oldFirst.getTop());
+ } else {
+ sel = fillSpecific(0, childrenTop);
+ }
+ }
+ break;
+ }
+
+ // Flush any cached views that did not get reused above
+ recycleBin.scrapActiveViews();
+
+ if (sel != null) {
+ // the current selected item should get focus if items
+ // are focusable
+ if (mItemsCanFocus && hasFocus() && !sel.hasFocus()) {
+ final boolean focusWasTaken = (sel == focusLayoutRestoreDirectChild &&
+ focusLayoutRestoreView.requestFocus()) || sel.requestFocus();
+ if (!focusWasTaken) {
+ // selected item didn't take focus, fine, but still want
+ // to make sure something else outside of the selected view
+ // has focus
+ final View focused = getFocusedChild();
+ if (focused != null) {
+ focused.clearFocus();
+ }
+ positionSelector(sel);
+ } else {
+ sel.setSelected(false);
+ mSelectorRect.setEmpty();
+ }
+
+ if (sel == focusLayoutRestoreDirectChild) {
+ focusLayoutRestoreDirectChild.restoreHierarchyState(mfocusRestoreChildState);
+ }
+ } else {
+ positionSelector(sel);
+ }
+ mSelectedTop = sel.getTop();
+ } else {
+ mSelectedTop = 0;
+ mSelectorRect.setEmpty();
+
+ // even if there is not selected position, we may need to restore
+ // focus (i.e. something focusable in touch mode)
+ if (hasFocus() && focusLayoutRestoreView != null) {
+ focusLayoutRestoreView.requestFocus();
+ focusLayoutRestoreDirectChild.restoreHierarchyState(mfocusRestoreChildState);
+ }
+ }
+
+ mLayoutMode = LAYOUT_NORMAL;
+ mDataChanged = false;
+ mNeedSync = false;
+ setNextSelectedPositionInt(mSelectedPosition);
+
+ updateScrollIndicators();
+
+ if (mItemCount > 0) {
+ checkSelectionChanged();
+ }
+
+ invokeOnItemScrollListener();
+ } finally {
+ if (!blockLayoutRequests) {
+ mBlockLayoutRequests = false;
+ }
+ }
+ }
+
+ /**
+ * @param child a direct child of this list.
+ * @return Whether child is a header or footer view.
+ */
+ private boolean isDirectChildHeaderOrFooter(View child) {
+
+ final ArrayList<FixedViewInfo> headers = mHeaderViewInfos;
+ final int numHeaders = headers.size();
+ for (int i = 0; i < numHeaders; i++) {
+ if (child == headers.get(i).view) {
+ return true;
+ }
+ }
+ final ArrayList<FixedViewInfo> footers = mFooterViewInfos;
+ final int numFooters = footers.size();
+ for (int i = 0; i < numFooters; i++) {
+ if (child == footers.get(i).view) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Obtain the view and add it to our list of children. The view can be made
+ * fresh, converted from an unused view, or used as is if it was in the
+ * recycle bin.
+ *
+ * @param position Logical position in the list
+ * @param y Top or bottom edge of the view to add
+ * @param flow If flow is true, align top edge to y. If false, align bottom
+ * edge to y.
+ * @param childrenLeft Left edge where children should be positioned
+ * @param selected Is this position selected?
+ * @return View that was added
+ */
+ private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
+ boolean selected) {
+ View child;
+
+
+ if (!mDataChanged) {
+ // Try to use an exsiting view for this position
+ child = mRecycler.getActiveView(position);
+ if (child != null) {
+ if (ViewDebug.TRACE_RECYCLER) {
+ ViewDebug.trace(child, ViewDebug.RecyclerTraceType.RECYCLE_FROM_ACTIVE_HEAP,
+ position, getChildCount());
+ }
+
+ // Found it -- we're using an existing child
+ // This just needs to be positioned
+ setupChild(child, position, y, flow, childrenLeft, selected, true);
+
+ return child;
+ }
+ }
+
+ // Make a new view for this position, or convert an unused view if possible
+ child = obtainView(position);
+
+ // This needs to be positioned and measured
+ setupChild(child, position, y, flow, childrenLeft, selected, false);
+
+ return child;
+ }
+
+ /**
+ * Add a view as a child and make sure it is measured (if necessary) and
+ * positioned properly.
+ *
+ * @param child The view to add
+ * @param position The position of this child
+ * @param y The y position relative to which this view will be positioned
+ * @param flowDown If true, align top edge to y. If false, align bottom
+ * edge to y.
+ * @param childrenLeft Left edge where children should be positioned
+ * @param selected Is this position selected?
+ * @param recycled Has this view been pulled from the recycle bin? If so it
+ * does not need to be remeasured.
+ */
+ private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
+ boolean selected, boolean recycled) {
+ final boolean isSelected = selected && shouldShowSelector();
+ final boolean updateChildSelected = isSelected != child.isSelected();
+ final boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested();
+
+ // Respect layout params that are already in the view. Otherwise make some up...
+ // noinspection unchecked
+ AbsListView.LayoutParams p = (AbsListView.LayoutParams)child.getLayoutParams();
+ if (p == null) {
+ p = new AbsListView.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT, 0);
+ }
+ p.viewType = mAdapter.getItemViewType(position);
+
+ if (recycled) {
+ attachViewToParent(child, flowDown ? -1 : 0, p);
+ } else {
+ addViewInLayout(child, flowDown ? -1 : 0, p, true);
+ }
+
+ if (updateChildSelected) {
+ child.setSelected(isSelected);
+ }
+
+ if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null) {
+ if (child instanceof Checkable) {
+ ((Checkable)child).setChecked(mCheckStates.get(position));
+ }
+ }
+
+ if (needToMeasure) {
+ int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
+ mListPadding.left + mListPadding.right, p.width);
+ int lpHeight = p.height;
+ int childHeightSpec;
+ if (lpHeight > 0) {
+ childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
+ } else {
+ childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ }
+ child.measure(childWidthSpec, childHeightSpec);
+ } else {
+ cleanupLayoutState(child);
+ }
+
+ final int w = child.getMeasuredWidth();
+ final int h = child.getMeasuredHeight();
+ final int childTop = flowDown ? y : y - h;
+
+ if (needToMeasure) {
+ final int childRight = childrenLeft + w;
+ final int childBottom = childTop + h;
+ child.layout(childrenLeft, childTop, childRight, childBottom);
+ } else {
+ child.offsetLeftAndRight(childrenLeft - child.getLeft());
+ child.offsetTopAndBottom(childTop - child.getTop());
+ }
+
+ if (mCachingStarted && !child.isDrawingCacheEnabled()) {
+ child.setDrawingCacheEnabled(true);
+ }
+ }
+
+ @Override
+ protected boolean canAnimate() {
+ return super.canAnimate() && mItemCount > 0;
+ }
+
+ /**
+ * Sets the currently selected item
+ *
+ * @param position Index (starting at 0) of the data item to be selected.
+ *
+ * If in touch mode, the item will not be selected but it will still be positioned
+ * appropriately.
+ */
+ @Override
+ public void setSelection(int position) {
+ setSelectionFromTop(position, 0);
+ }
+
+ /**
+ * Sets the selected item and positions the selection y pixels from the top edge
+ * of the ListView. (If in touch mode, the item will not be selected but it will
+ * still be positioned appropriately.)
+ *
+ * @param position Index (starting at 0) of the data item to be selected.
+ * @param y The distance from the top edge of the ListView (plus padding) that the
+ * item will be positioned.
+ */
+ public void setSelectionFromTop(int position, int y) {
+ if (mAdapter == null) {
+ return;
+ }
+
+ if (!isInTouchMode()) {
+ position = lookForSelectablePosition(position, true);
+ if (position >= 0) {
+ setNextSelectedPositionInt(position);
+ }
+ } else {
+ mResurrectToPosition = position;
+ }
+
+ if (position >= 0) {
+ mLayoutMode = LAYOUT_SPECIFIC;
+ mSpecificTop = mListPadding.top + y;
+
+ if (mNeedSync) {
+ mSyncPosition = position;
+ mSyncRowId = mAdapter.getItemId(position);
+ }
+
+ requestLayout();
+ }
+ }
+
+ /**
+ * Makes the item at the supplied position selected.
+ *
+ * @param position the position of the item to select
+ */
+ @Override
+ void setSelectionInt(int position) {
+ mBlockLayoutRequests = true;
+ setNextSelectedPositionInt(position);
+ layoutChildren();
+ mBlockLayoutRequests = false;
+ }
+
+ /**
+ * Find a position that can be selected (i.e., is not a separator).
+ *
+ * @param position The starting position to look at.
+ * @param lookDown Whether to look down for other positions.
+ * @return The next selectable position starting at position and then searching either up or
+ * down. Returns {@link #INVALID_POSITION} if nothing can be found.
+ */
+ @Override
+ int lookForSelectablePosition(int position, boolean lookDown) {
+ final ListAdapter adapter = mAdapter;
+ if (adapter == null || isInTouchMode()) {
+ return INVALID_POSITION;
+ }
+
+ final int count = adapter.getCount();
+ if (!mAreAllItemsSelectable) {
+ if (lookDown) {
+ position = Math.max(0, position);
+ while (position < count && !adapter.isEnabled(position)) {
+ position++;
+ }
+ } else {
+ position = Math.min(position, count - 1);
+ while (position >= 0 && !adapter.isEnabled(position)) {
+ position--;
+ }
+ }
+
+ if (position < 0 || position >= count) {
+ return INVALID_POSITION;
+ }
+ return position;
+ } else {
+ if (position < 0 || position >= count) {
+ return INVALID_POSITION;
+ }
+ return position;
+ }
+ }
+
+ /**
+ * setSelectionAfterHeaderView set the selection to be the first list item
+ * after the header views.
+ */
+ public void setSelectionAfterHeaderView() {
+ final int count = mHeaderViewInfos.size();
+ if (count > 0) {
+ mNextSelectedPosition = 0;
+ return;
+ }
+
+ if (mAdapter != null) {
+ setSelection(count);
+ } else {
+ mNextSelectedPosition = count;
+ mLayoutMode = LAYOUT_SET_SELECTION;
+ }
+
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ // Dispatch in the normal way
+ boolean handled = super.dispatchKeyEvent(event);
+ if (!handled) {
+ // If we didn't handle it...
+ View focused = getFocusedChild();
+ if (focused != null && event.getAction() == KeyEvent.ACTION_DOWN) {
+ // ... and our focused child didn't handle it
+ // ... give it to ourselves so we can scroll if necessary
+ handled = onKeyDown(event.getKeyCode(), event);
+ }
+ }
+ return handled;
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ return commonKey(keyCode, 1, event);
+ }
+
+ @Override
+ public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
+ return commonKey(keyCode, repeatCount, event);
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ return commonKey(keyCode, 1, event);
+ }
+
+ private boolean commonKey(int keyCode, int count, KeyEvent event) {
+ if (mAdapter == null) {
+ return false;
+ }
+
+ if (mDataChanged) {
+ layoutChildren();
+ }
+
+ boolean handled = false;
+ int action = event.getAction();
+
+ if (action != KeyEvent.ACTION_UP) {
+ if (mSelectedPosition < 0) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_UP:
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ case KeyEvent.KEYCODE_ENTER:
+ case KeyEvent.KEYCODE_SPACE:
+ if (resurrectSelection()) {
+ return true;
+ }
+ }
+ }
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_UP:
+ if (!event.isAltPressed()) {
+ while (count > 0) {
+ handled = arrowScroll(FOCUS_UP);
+ count--;
+ }
+ } else {
+ handled = fullScroll(FOCUS_UP);
+ }
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ if (!event.isAltPressed()) {
+ while (count > 0) {
+ handled = arrowScroll(FOCUS_DOWN);
+ count--;
+ }
+ } else {
+ handled = fullScroll(FOCUS_DOWN);
+ }
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ handled = handleHorizontalFocusWithinListItem(View.FOCUS_LEFT);
+ break;
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ handled = handleHorizontalFocusWithinListItem(View.FOCUS_RIGHT);
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ case KeyEvent.KEYCODE_ENTER:
+ if (mItemCount > 0 && event.getRepeatCount() == 0) {
+ keyPressed();
+ }
+ handled = true;
+ break;
+
+ case KeyEvent.KEYCODE_SPACE:
+ if (mPopup == null || !mPopup.isShowing()) {
+ if (!event.isShiftPressed()) {
+ pageScroll(FOCUS_DOWN);
+ } else {
+ pageScroll(FOCUS_UP);
+ }
+ handled = true;
+ }
+ break;
+ }
+ }
+
+ if (!handled) {
+ handled = sendToTextFilter(keyCode, count, event);
+ }
+
+ if (handled) {
+ return true;
+ } else {
+ switch (action) {
+ case KeyEvent.ACTION_DOWN:
+ return super.onKeyDown(keyCode, event);
+
+ case KeyEvent.ACTION_UP:
+ return super.onKeyUp(keyCode, event);
+
+ case KeyEvent.ACTION_MULTIPLE:
+ return super.onKeyMultiple(keyCode, count, event);
+
+ default: // shouldn't happen
+ return false;
+ }
+ }
+ }
+
+ /**
+ * Scrolls up or down by the number of items currently present on screen.
+ *
+ * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN}
+ * @return whether selection was moved
+ */
+ boolean pageScroll(int direction) {
+ int nextPage = -1;
+ boolean down = false;
+
+ if (direction == FOCUS_UP) {
+ nextPage = Math.max(0, mSelectedPosition - getChildCount() - 1);
+ } else if (direction == FOCUS_DOWN) {
+ nextPage = Math.min(mItemCount - 1, mSelectedPosition + getChildCount() - 1);
+ down = true;
+ }
+
+ if (nextPage >= 0) {
+ int position = lookForSelectablePosition(nextPage, down);
+ if (position >= 0) {
+ mLayoutMode = LAYOUT_SPECIFIC;
+ mSpecificTop = mPaddingTop + getVerticalFadingEdgeLength();
+
+ if (down && position > mItemCount - getChildCount()) {
+ mLayoutMode = LAYOUT_FORCE_BOTTOM;
+ }
+
+ if (!down && position < getChildCount()) {
+ mLayoutMode = LAYOUT_FORCE_TOP;
+ }
+
+ setSelectionInt(position);
+ invalidate();
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Go to the last or first item if possible (not worrying about panning across or navigating
+ * within the internal focus of the currently selected item.)
+ *
+ * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN}
+ *
+ * @return whether selection was moved
+ */
+ boolean fullScroll(int direction) {
+ boolean moved = false;
+ if (direction == FOCUS_UP) {
+ if (mSelectedPosition != 0) {
+ int position = lookForSelectablePosition(0, true);
+ if (position >= 0) {
+ mLayoutMode = LAYOUT_FORCE_TOP;
+ setSelectionInt(position);
+ }
+ moved = true;
+ }
+ } else if (direction == FOCUS_DOWN) {
+ if (mSelectedPosition < mItemCount - 1) {
+ int position = lookForSelectablePosition(mItemCount - 1, true);
+ if (position >= 0) {
+ mLayoutMode = LAYOUT_FORCE_BOTTOM;
+ setSelectionInt(position);
+ }
+ moved = true;
+ }
+ }
+
+ if (moved) {
+ invalidate();
+ }
+
+ return moved;
+ }
+
+ /**
+ * To avoid horizontal focus searches changing the selected item, we
+ * manually focus search within the selected item (as applicable), and
+ * prevent focus from jumping to something within another item.
+ * @param direction one of {View.FOCUS_LEFT, View.FOCUS_RIGHT}
+ * @return Whether this consumes the key event.
+ */
+ private boolean handleHorizontalFocusWithinListItem(int direction) {
+ if (direction != View.FOCUS_LEFT && direction != View.FOCUS_RIGHT) {
+ throw new IllegalArgumentException("direction must be one of {View.FOCUS_LEFT, View.FOCUS_RIGHT}");
+ }
+
+ final int numChildren = getChildCount();
+ if (mItemsCanFocus && numChildren > 0 && mSelectedPosition != INVALID_POSITION) {
+ final View selectedView = getSelectedView();
+ if (selectedView.hasFocus() && selectedView instanceof ViewGroup) {
+ final View currentFocus = selectedView.findFocus();
+ final View nextFocus = FocusFinder.getInstance().findNextFocus(
+ (ViewGroup) selectedView,
+ currentFocus,
+ direction);
+ if (nextFocus != null) {
+ // do the math to get interesting rect in next focus' coordinates
+ currentFocus.getFocusedRect(mTempRect);
+ offsetDescendantRectToMyCoords(currentFocus, mTempRect);
+ offsetRectIntoDescendantCoords(nextFocus, mTempRect);
+ if (nextFocus.requestFocus(direction, mTempRect)) {
+ return true;
+ }
+ }
+ // we are blocking the key from being handled (by returning true)
+ // if the global result is going to be some other view within this
+ // list. this is to acheive the overall goal of having
+ // horizontal d-pad navigation remain in the current item.
+ final View globalNextFocus = FocusFinder.getInstance()
+ .findNextFocus(
+ (ViewGroup) getRootView(),
+ currentFocus,
+ direction);
+ if (globalNextFocus != null) {
+ return isViewAncestorOf(globalNextFocus, this);
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Scrolls to the next or previous item if possible.
+ *
+ * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN}
+ *
+ * @return whether selection was moved
+ */
+ boolean arrowScroll(int direction) {
+ try {
+ mInLayout = true;
+ final boolean handled = arrowScrollImpl(direction);
+ if (handled) {
+ playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));
+ }
+ return handled;
+ } finally {
+ mInLayout = false;
+ }
+ }
+
+ /**
+ * Handle an arrow scroll going up or down. Take into account whether items are selectable,
+ * whether there are focusable items etc.
+ *
+ * @param direction Either {@link android.view.View#FOCUS_UP} or {@link android.view.View#FOCUS_DOWN}.
+ * @return Whether any scrolling, selection or focus change occured.
+ */
+ private boolean arrowScrollImpl(int direction) {
+ if (getChildCount() <= 0) {
+ return false;
+ }
+
+ View selectedView = getSelectedView();
+
+ int nextSelectedPosition = lookForSelectablePositionOnScreen(direction);
+ int amountToScroll = amountToScroll(direction, nextSelectedPosition);
+
+ // if we are moving focus, we may OVERRIDE the default behavior
+ final ArrowScrollFocusResult focusResult = mItemsCanFocus ? arrowScrollFocused(direction) : null;
+ if (focusResult != null) {
+ nextSelectedPosition = focusResult.getSelectedPosition();
+ amountToScroll = focusResult.getAmountToScroll();
+ }
+
+ boolean needToRedraw = focusResult != null;
+ if (nextSelectedPosition != INVALID_POSITION) {
+ handleNewSelectionChange(selectedView, direction, nextSelectedPosition, focusResult != null);
+ setSelectedPositionInt(nextSelectedPosition);
+ setNextSelectedPositionInt(nextSelectedPosition);
+ selectedView = getSelectedView();
+ if (mItemsCanFocus && focusResult == null) {
+ // there was no new view found to take focus, make sure we
+ // don't leave focus with the old selection
+ final View focused = getFocusedChild();
+ if (focused != null) {
+ focused.clearFocus();
+ }
+ }
+ needToRedraw = true;
+ checkSelectionChanged();
+ }
+
+ if (amountToScroll > 0) {
+ scrollListItemsBy((direction == View.FOCUS_UP) ? amountToScroll : -amountToScroll);
+ needToRedraw = true;
+ }
+
+ // if we didn't find a new focusable, make sure any existing focused
+ // item that was panned off screen gives up focus.
+ if (mItemsCanFocus && (focusResult == null)
+ && selectedView != null && selectedView.hasFocus()) {
+ final View focused = selectedView.findFocus();
+ if (distanceToView(focused) > 0) {
+ focused.clearFocus();
+ }
+ }
+
+ // if the current selection is panned off, we need to remove the selection
+ if (nextSelectedPosition == INVALID_POSITION && selectedView != null
+ && !isViewAncestorOf(selectedView, this)) {
+ selectedView = null;
+ hideSelector();
+ }
+
+ if (needToRedraw) {
+ if (selectedView != null) {
+ positionSelector(selectedView);
+ mSelectedTop = selectedView.getTop();
+ }
+ invalidate();
+ invokeOnItemScrollListener();
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * When selection changes, it is possible that the previously selected or the
+ * next selected item will change its size. If so, we need to offset some folks,
+ * and re-layout the items as appropriate.
+ *
+ * @param selectedView The currently selected view (before changing selection).
+ * should be <code>null</code> if there was no previous selection.
+ * @param direction Either {@link android.view.View#FOCUS_UP} or
+ * {@link android.view.View#FOCUS_DOWN}.
+ * @param newSelectedPosition The position of the next selection.
+ * @param newFocusAssigned whether new focus was assigned. This matters because
+ * when something has focus, we don't want to show selection (ugh).
+ */
+ private void handleNewSelectionChange(View selectedView, int direction, int newSelectedPosition,
+ boolean newFocusAssigned) {
+ if (newSelectedPosition == INVALID_POSITION) {
+ throw new IllegalArgumentException("newSelectedPosition needs to be valid");
+ }
+
+ // whether or not we are moving down or up, we want to preserve the
+ // top of whatever view is on top:
+ // - moving down: the view that had selection
+ // - moving up: the view that is getting selection
+ View topView;
+ View bottomView;
+ int topViewIndex, bottomViewIndex;
+ boolean topSelected = false;
+ final int selectedIndex = mSelectedPosition - mFirstPosition;
+ final int nextSelectedIndex = newSelectedPosition - mFirstPosition;
+ if (direction == View.FOCUS_UP) {
+ topViewIndex = nextSelectedIndex;
+ bottomViewIndex = selectedIndex;
+ topView = getChildAt(topViewIndex);
+ bottomView = selectedView;
+ topSelected = true;
+ } else {
+ topViewIndex = selectedIndex;
+ bottomViewIndex = nextSelectedIndex;
+ topView = selectedView;
+ bottomView = getChildAt(bottomViewIndex);
+ }
+
+ final int numChildren = getChildCount();
+
+ // start with top view: is it changing size?
+ if (topView != null) {
+ topView.setSelected(!newFocusAssigned && topSelected);
+ measureAndAdjustDown(topView, topViewIndex, numChildren);
+ }
+
+ // is the bottom view changing size?
+ if (bottomView != null) {
+ bottomView.setSelected(!newFocusAssigned && !topSelected);
+ measureAndAdjustDown(bottomView, bottomViewIndex, numChildren);
+ }
+ }
+
+ /**
+ * Re-measure a child, and if its height changes, lay it out preserving its
+ * top, and adjust the children below it appropriately.
+ * @param child The child
+ * @param childIndex The view group index of the child.
+ * @param numChildren The number of children in the view group.
+ */
+ private void measureAndAdjustDown(View child, int childIndex, int numChildren) {
+ int oldHeight = child.getHeight();
+ measureItem(child);
+ if (child.getMeasuredHeight() != oldHeight) {
+ // lay out the view, preserving its top
+ relayoutMeasuredItem(child);
+
+ // adjust views below appropriately
+ final int heightDelta = child.getMeasuredHeight() - oldHeight;
+ for (int i = childIndex + 1; i < numChildren; i++) {
+ getChildAt(i).offsetTopAndBottom(heightDelta);
+ }
+ }
+ }
+
+ /**
+ * Measure a particular list child.
+ * TODO: unify with setUpChild.
+ * @param child The child.
+ */
+ private void measureItem(View child) {
+ ViewGroup.LayoutParams p = child.getLayoutParams();
+ if (p == null) {
+ p = new ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.FILL_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+ }
+
+ int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
+ mListPadding.left + mListPadding.right, p.width);
+ int lpHeight = p.height;
+ int childHeightSpec;
+ if (lpHeight > 0) {
+ childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
+ } else {
+ childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ }
+ child.measure(childWidthSpec, childHeightSpec);
+ }
+
+ /**
+ * Layout a child that has been measured, preserving its top position.
+ * TODO: unify with setUpChild.
+ * @param child The child.
+ */
+ private void relayoutMeasuredItem(View child) {
+ final int w = child.getMeasuredWidth();
+ final int h = child.getMeasuredHeight();
+ final int childLeft = mListPadding.left;
+ final int childRight = childLeft + w;
+ final int childTop = child.getTop();
+ final int childBottom = childTop + h;
+ child.layout(childLeft, childTop, childRight, childBottom);
+ }
+
+ /**
+ * @return The amount to preview next items when arrow srolling.
+ */
+ private int getArrowScrollPreviewLength() {
+ return Math.max(MIN_SCROLL_PREVIEW_PIXELS, getVerticalFadingEdgeLength());
+ }
+
+ /**
+ * Determine how much we need to scroll in order to get the next selected view
+ * visible, with a fading edge showing below as applicable. The amount is
+ * capped at {@link #getMaxScrollAmount()} .
+ *
+ * @param direction either {@link android.view.View#FOCUS_UP} or
+ * {@link android.view.View#FOCUS_DOWN}.
+ * @param nextSelectedPosition The position of the next selection, or
+ * {@link #INVALID_POSITION} if there is no next selectable position
+ * @return The amount to scroll. Note: this is always positive! Direction
+ * needs to be taken into account when actually scrolling.
+ */
+ private int amountToScroll(int direction, int nextSelectedPosition) {
+ final int listBottom = getHeight() - mListPadding.bottom;
+ final int listTop = mListPadding.top;
+
+ final int numChildren = getChildCount();
+
+ if (direction == View.FOCUS_DOWN) {
+ int indexToMakeVisible = numChildren - 1;
+ if (nextSelectedPosition != INVALID_POSITION) {
+ indexToMakeVisible = nextSelectedPosition - mFirstPosition;
+ }
+
+ final int positionToMakeVisible = mFirstPosition + indexToMakeVisible;
+ final View viewToMakeVisible = getChildAt(indexToMakeVisible);
+
+ int goalBottom = listBottom;
+ if (positionToMakeVisible < mItemCount - 1) {
+ goalBottom -= getArrowScrollPreviewLength();
+ }
+
+ if (viewToMakeVisible.getBottom() <= goalBottom) {
+ // item is fully visible.
+ return 0;
+ }
+
+ if (nextSelectedPosition != INVALID_POSITION
+ && (goalBottom - viewToMakeVisible.getTop()) >= getMaxScrollAmount()) {
+ // item already has enough of it visible, changing selection is good enough
+ return 0;
+ }
+
+ int amountToScroll = (viewToMakeVisible.getBottom() - goalBottom);
+
+ if ((mFirstPosition + numChildren) == mItemCount) {
+ // last is last in list -> make sure we don't scroll past it
+ final int max = getChildAt(numChildren - 1).getBottom() - listBottom;
+ amountToScroll = Math.min(amountToScroll, max);
+ }
+
+ return Math.min(amountToScroll, getMaxScrollAmount());
+ } else {
+ int indexToMakeVisible = 0;
+ if (nextSelectedPosition != INVALID_POSITION) {
+ indexToMakeVisible = nextSelectedPosition - mFirstPosition;
+ }
+ final int positionToMakeVisible = mFirstPosition + indexToMakeVisible;
+ final View viewToMakeVisible = getChildAt(indexToMakeVisible);
+ int goalTop = listTop;
+ if (positionToMakeVisible > 0) {
+ goalTop += getArrowScrollPreviewLength();
+ }
+ if (viewToMakeVisible.getTop() >= goalTop) {
+ // item is fully visible.
+ return 0;
+ }
+
+ if (nextSelectedPosition != INVALID_POSITION &&
+ (viewToMakeVisible.getBottom() - goalTop) >= getMaxScrollAmount()) {
+ // item already has enough of it visible, changing selection is good enough
+ return 0;
+ }
+
+ int amountToScroll = (goalTop - viewToMakeVisible.getTop());
+ if (mFirstPosition == 0) {
+ // first is first in list -> make sure we don't scroll past it
+ final int max = listTop - getChildAt(0).getTop();
+ amountToScroll = Math.min(amountToScroll, max);
+ }
+ return Math.min(amountToScroll, getMaxScrollAmount());
+ }
+ }
+
+ /**
+ * Holds results of focus aware arrow scrolling.
+ */
+ static private class ArrowScrollFocusResult {
+ private int mSelectedPosition;
+ private int mAmountToScroll;
+
+ /**
+ * How {@link android.widget.ListView#arrowScrollFocused} returns its values.
+ */
+ void populate(int selectedPosition, int amountToScroll) {
+ mSelectedPosition = selectedPosition;
+ mAmountToScroll = amountToScroll;
+ }
+
+ public int getSelectedPosition() {
+ return mSelectedPosition;
+ }
+
+ public int getAmountToScroll() {
+ return mAmountToScroll;
+ }
+ }
+
+ /**
+ * @param direction either {@link android.view.View#FOCUS_UP} or
+ * {@link android.view.View#FOCUS_DOWN}.
+ * @return The position of the next selectable position of the views that
+ * are currently visible, taking into account the fact that there might
+ * be no selection. Returns {@link #INVALID_POSITION} if there is no
+ * selectable view on screen in the given direction.
+ */
+ private int lookForSelectablePositionOnScreen(int direction) {
+ final int firstPosition = mFirstPosition;
+ if (direction == View.FOCUS_DOWN) {
+ int startPos = (mSelectedPosition != INVALID_POSITION) ?
+ mSelectedPosition + 1 :
+ firstPosition;
+ if (startPos >= mAdapter.getCount()) {
+ return INVALID_POSITION;
+ }
+ if (startPos < firstPosition) {
+ startPos = firstPosition;
+ }
+
+ final int lastVisiblePos = getLastVisiblePosition();
+ final ListAdapter adapter = getAdapter();
+ for (int pos = startPos; pos <= lastVisiblePos; pos++) {
+ if (adapter.isEnabled(pos)
+ && getChildAt(pos - firstPosition).getVisibility() == View.VISIBLE) {
+ return pos;
+ }
+ }
+ } else {
+ int last = firstPosition + getChildCount() - 1;
+ int startPos = (mSelectedPosition != INVALID_POSITION) ?
+ mSelectedPosition - 1 :
+ firstPosition + getChildCount() - 1;
+ if (startPos < 0) {
+ return INVALID_POSITION;
+ }
+ if (startPos > last) {
+ startPos = last;
+ }
+
+ final ListAdapter adapter = getAdapter();
+ for (int pos = startPos; pos >= firstPosition; pos--) {
+ if (adapter.isEnabled(pos)
+ && getChildAt(pos - firstPosition).getVisibility() == View.VISIBLE) {
+ return pos;
+ }
+ }
+ }
+ return INVALID_POSITION;
+ }
+
+ /**
+ * Do an arrow scroll based on focus searching. If a new view is
+ * given focus, return the selection delta and amount to scroll via
+ * an {@link ArrowScrollFocusResult}, otherwise, return null.
+ *
+ * @param direction either {@link android.view.View#FOCUS_UP} or
+ * {@link android.view.View#FOCUS_DOWN}.
+ * @return The result if focus has changed, or <code>null</code>.
+ */
+ private ArrowScrollFocusResult arrowScrollFocused(final int direction) {
+ final View selectedView = getSelectedView();
+ View newFocus;
+ if (selectedView != null && selectedView.hasFocus()) {
+ View oldFocus = selectedView.findFocus();
+ newFocus = FocusFinder.getInstance().findNextFocus(this, oldFocus, direction);
+ } else {
+ if (direction == View.FOCUS_DOWN) {
+ final boolean topFadingEdgeShowing = (mFirstPosition > 0);
+ final int listTop = mListPadding.top +
+ (topFadingEdgeShowing ? getArrowScrollPreviewLength() : 0);
+ final int ySearchPoint =
+ (selectedView != null && selectedView.getTop() > listTop) ?
+ selectedView.getTop() :
+ listTop;
+ mTempRect.set(0, ySearchPoint, 0, ySearchPoint);
+ } else {
+ final boolean bottomFadingEdgeShowing =
+ (mFirstPosition + getChildCount() - 1) < mItemCount;
+ final int listBottom = getHeight() - mListPadding.bottom -
+ (bottomFadingEdgeShowing ? getArrowScrollPreviewLength() : 0);
+ final int ySearchPoint =
+ (selectedView != null && selectedView.getBottom() < listBottom) ?
+ selectedView.getBottom() :
+ listBottom;
+ mTempRect.set(0, ySearchPoint, 0, ySearchPoint);
+ }
+ newFocus = FocusFinder.getInstance().findNextFocusFromRect(this, mTempRect, direction);
+ }
+
+ if (newFocus != null) {
+ final int positionOfNewFocus = positionOfNewFocus(newFocus);
+
+ // if the focus change is in a different new position, make sure
+ // we aren't jumping over another selectable position
+ if (mSelectedPosition != INVALID_POSITION && positionOfNewFocus != mSelectedPosition) {
+ final int selectablePosition = lookForSelectablePositionOnScreen(direction);
+ if (selectablePosition != INVALID_POSITION &&
+ ((direction == View.FOCUS_DOWN && selectablePosition < positionOfNewFocus) ||
+ (direction == View.FOCUS_UP && selectablePosition > positionOfNewFocus))) {
+ return null;
+ }
+ }
+
+ int focusScroll = amountToScrollToNewFocus(direction, newFocus, positionOfNewFocus);
+
+ final int maxScrollAmount = getMaxScrollAmount();
+ if (focusScroll < maxScrollAmount) {
+ // not moving too far, safe to give next view focus
+ newFocus.requestFocus(direction);
+ mArrowScrollFocusResult.populate(positionOfNewFocus, focusScroll);
+ return mArrowScrollFocusResult;
+ } else if (distanceToView(newFocus) < maxScrollAmount){
+ // Case to consider:
+ // too far to get entire next focusable on screen, but by going
+ // max scroll amount, we are getting it at least partially in view,
+ // so give it focus and scroll the max ammount.
+ newFocus.requestFocus(direction);
+ mArrowScrollFocusResult.populate(positionOfNewFocus, maxScrollAmount);
+ return mArrowScrollFocusResult;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @param newFocus The view that would have focus.
+ * @return the position that contains newFocus
+ */
+ private int positionOfNewFocus(View newFocus) {
+ final int numChildren = getChildCount();
+ for (int i = 0; i < numChildren; i++) {
+ final View child = getChildAt(i);
+ if (isViewAncestorOf(newFocus, child)) {
+ return mFirstPosition + i;
+ }
+ }
+ throw new IllegalArgumentException("newFocus is not a child of any of the"
+ + " children of the list!");
+ }
+
+ /**
+ * Return true if child is an ancestor of parent, (or equal to the parent).
+ */
+ private boolean isViewAncestorOf(View child, View parent) {
+ if (child == parent) {
+ return true;
+ }
+
+ final ViewParent theParent = child.getParent();
+ return (theParent instanceof ViewGroup) && isViewAncestorOf((View) theParent, parent);
+ }
+
+ /**
+ * Determine how much we need to scroll in order to get newFocus in view.
+ * @param direction either {@link android.view.View#FOCUS_UP} or
+ * {@link android.view.View#FOCUS_DOWN}.
+ * @param newFocus The view that would take focus.
+ * @param positionOfNewFocus The position of the list item containing newFocus
+ * @return The amount to scroll. Note: this is always positive! Direction
+ * needs to be taken into account when actually scrolling.
+ */
+ private int amountToScrollToNewFocus(int direction, View newFocus, int positionOfNewFocus) {
+ int amountToScroll = 0;
+ newFocus.getDrawingRect(mTempRect);
+ offsetDescendantRectToMyCoords(newFocus, mTempRect);
+ if (direction == View.FOCUS_UP) {
+ if (mTempRect.top < mListPadding.top) {
+ amountToScroll = mListPadding.top - mTempRect.top;
+ if (positionOfNewFocus > 0) {
+ amountToScroll += getArrowScrollPreviewLength();
+ }
+ }
+ } else {
+ final int listBottom = getHeight() - mListPadding.bottom;
+ if (mTempRect.bottom > listBottom) {
+ amountToScroll = mTempRect.bottom - listBottom;
+ if (positionOfNewFocus < mItemCount - 1) {
+ amountToScroll += getArrowScrollPreviewLength();
+ }
+ }
+ }
+ return amountToScroll;
+ }
+
+ /**
+ * Determine the distance to the nearest edge of a view in a particular
+ * direciton.
+ * @param descendant A descendant of this list.
+ * @return The distance, or 0 if the nearest edge is already on screen.
+ */
+ private int distanceToView(View descendant) {
+ int distance = 0;
+ descendant.getDrawingRect(mTempRect);
+ offsetDescendantRectToMyCoords(descendant, mTempRect);
+ final int listBottom = mBottom - mTop - mListPadding.bottom;
+ if (mTempRect.bottom < mListPadding.top) {
+ distance = mListPadding.top - mTempRect.bottom;
+ } else if (mTempRect.top > listBottom) {
+ distance = mTempRect.top - listBottom;
+ }
+ return distance;
+ }
+
+
+ /**
+ * Scroll the children by amount, adding a view at the end and removing
+ * views that fall off as necessary.
+ *
+ * @param amount The amount (positive or negative) to scroll.
+ */
+ private void scrollListItemsBy(int amount) {
+ offsetChildrenTopAndBottom(amount);
+
+ final int listBottom = getHeight() - mListPadding.bottom;
+ final int listTop = mListPadding.top;
+
+ if (amount < 0) {
+ // shifted items up
+
+ // may need to pan views into the bottom space
+ int numChildren = getChildCount();
+ View last = getChildAt(numChildren - 1);
+ while (last.getBottom() < listBottom) {
+ final int lastVisiblePosition = mFirstPosition + numChildren - 1;
+ if (lastVisiblePosition < mItemCount - 1) {
+ last = addViewBelow(last, lastVisiblePosition);
+ numChildren++;
+ } else {
+ break;
+ }
+ }
+
+ // may have brought in the last child of the list that is skinnier
+ // than the fading edge, thereby leaving space at the end. need
+ // to shift back
+ if (last.getBottom() < listBottom) {
+ offsetChildrenTopAndBottom(listBottom - last.getBottom());
+ }
+
+ // top views may be panned off screen
+ View first = getChildAt(0);
+ while (first.getBottom() < listTop) {
+ removeViewInLayout(first);
+ mRecycler.addScrapView(first);
+ first = getChildAt(0);
+ mFirstPosition++;
+ }
+ } else {
+ // shifted items down
+ View first = getChildAt(0);
+
+ // may need to pan views into top
+ while ((first.getTop() > listTop) && (mFirstPosition > 0)) {
+ first = addViewAbove(first, mFirstPosition);
+ mFirstPosition--;
+ }
+
+ // may have brought the very first child of the list in too far and
+ // need to shift it back
+ if (first.getTop() > listTop) {
+ offsetChildrenTopAndBottom(listTop - first.getTop());
+ }
+
+ int lastIndex = getChildCount() - 1;
+ View last = getChildAt(lastIndex);
+
+ // bottom view may be panned off screen
+ while (last.getTop() > listBottom) {
+ removeViewInLayout(last);
+ mRecycler.addScrapView(last);
+ last = getChildAt(--lastIndex);
+ }
+ }
+ }
+
+ private View addViewAbove(View theView, int position) {
+ int abovePosition = position - 1;
+ View view = obtainView(abovePosition);
+ int edgeOfNewChild = theView.getTop() - mDividerHeight;
+ setupChild(view, abovePosition, edgeOfNewChild, false, mListPadding.left, false, false);
+ return view;
+ }
+
+ private View addViewBelow(View theView, int position) {
+ int belowPosition = position + 1;
+ View view = obtainView(belowPosition);
+ int edgeOfNewChild = theView.getBottom() + mDividerHeight;
+ setupChild(view, belowPosition, edgeOfNewChild, true, mListPadding.left, false, false);
+ return view;
+ }
+
+ /**
+ * Indicates that the views created by the ListAdapter can contain focusable
+ * items.
+ *
+ * @param itemsCanFocus true if items can get focus, false otherwise
+ */
+ public void setItemsCanFocus(boolean itemsCanFocus) {
+ mItemsCanFocus = itemsCanFocus;
+ if (!itemsCanFocus) {
+ setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
+ }
+ }
+
+ /**
+ * @return Whether the views created by the ListAdapter can contain focusable
+ * items.
+ */
+ public boolean getItemsCanFocus() {
+ return mItemsCanFocus;
+ }
+
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ // Draw the dividers
+ final int dividerHeight = mDividerHeight;
+
+ if (dividerHeight > 0 && mDivider != null) {
+ // Only modify the top and bottom in the loop, we set the left and right here
+ final Rect bounds = mTempRect;
+ bounds.left = mPaddingLeft;
+ bounds.right = mRight - mLeft - mPaddingRight;
+
+ final int count = getChildCount();
+ int i;
+
+ if (mStackFromBottom) {
+ int top;
+ int listTop = mListPadding.top;
+
+ for (i = 0; i < count; ++i) {
+ View child = getChildAt(i);
+ top = child.getTop();
+ if (top > listTop) {
+ bounds.top = top - dividerHeight;
+ bounds.bottom = top;
+ // Give the method the child ABOVE the divider, so we
+ // subtract one from our child
+ // position. Give -1 when there is no child above the
+ // divider.
+ drawDivider(canvas, bounds, i - 1);
+ }
+ }
+ } else {
+ int bottom;
+ int listBottom = getHeight() - mListPadding.bottom;
+
+ for (i = 0; i < count; ++i) {
+ View child = getChildAt(i);
+ bottom = child.getBottom();
+ if (bottom < listBottom) {
+ bounds.top = bottom;
+ bounds.bottom = bottom + dividerHeight;
+ drawDivider(canvas, bounds, i);
+ }
+ }
+ }
+ }
+
+ // Draw the indicators (these should be drawn above the dividers) and children
+ super.dispatchDraw(canvas);
+ }
+
+ /**
+ * Draws a divider for the given child in the given bounds.
+ *
+ * @param canvas The canvas to draw to.
+ * @param bounds The bounds of the divider.
+ * @param childIndex The index of child (of the View) above the divider.
+ * This will be -1 if there is no child above the divider to be
+ * drawn.
+ */
+ void drawDivider(Canvas canvas, Rect bounds, int childIndex) {
+ // This widget draws the same divider for all children
+ mDivider.setBounds(bounds);
+ mDivider.draw(canvas);
+ }
+
+ /**
+ * Returns the drawable that will be drawn between each item in the list.
+ *
+ * @return the current drawable drawn between list elements
+ */
+ public Drawable getDivider() {
+ return mDivider;
+ }
+
+ /**
+ * Sets the drawable that will be drawn between each item in the list. If the drawable does
+ * not have an intrinsic height, you should also call {@link #setDividerHeight(int)}
+ *
+ * @param divider The drawable to use.
+ */
+ public void setDivider(Drawable divider) {
+ if (divider != null) {
+ mDividerHeight = divider.getIntrinsicHeight();
+ } else {
+ mDividerHeight = 0;
+ }
+ mDivider = divider;
+ requestLayoutIfNecessary();
+ }
+
+ /**
+ * @return Returns the height of the divider that will be drawn between each item in the list.
+ */
+ public int getDividerHeight() {
+ return mDividerHeight;
+ }
+
+ /**
+ * Sets the height of the divider that will be drawn between each item in the list. Calling
+ * this will override the intrinsic height as set by {@link #setDivider(Drawable)}
+ *
+ * @param height The new height of the divider in pixels.
+ */
+ public void setDividerHeight(int height) {
+ mDividerHeight = height;
+ requestLayoutIfNecessary();
+ }
+
+ @Override
+ protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
+ super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
+
+ int closetChildIndex = -1;
+ if (gainFocus && previouslyFocusedRect != null) {
+ previouslyFocusedRect.offset(mScrollX, mScrollY);
+
+ // figure out which item should be selected based on previously
+ // focused rect
+ Rect otherRect = mTempRect;
+ int minDistance = Integer.MAX_VALUE;
+ final int childCount = getChildCount();
+ final int firstPosition = mFirstPosition;
+ final ListAdapter adapter = mAdapter;
+
+ for (int i = 0; i < childCount; i++) {
+ // only consider selectable views
+ if (!adapter.isEnabled(firstPosition + i)) {
+ continue;
+ }
+
+ View other = getChildAt(i);
+ other.getDrawingRect(otherRect);
+ offsetDescendantRectToMyCoords(other, otherRect);
+ int distance = getDistance(previouslyFocusedRect, otherRect, direction);
+
+ if (distance < minDistance) {
+ minDistance = distance;
+ closetChildIndex = i;
+ }
+ }
+ }
+
+ if (closetChildIndex >= 0) {
+ setSelection(closetChildIndex + mFirstPosition);
+ } else {
+ requestLayout();
+ }
+ }
+
+
+ /*
+ * (non-Javadoc)
+ *
+ * Children specified in XML are assumed to be header views. After we have
+ * parsed them move them out of the children list and into mHeaderViews.
+ */
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ int count = getChildCount();
+ if (count > 0) {
+ for (int i = 0; i < count; ++i) {
+ addHeaderView(getChildAt(i));
+ }
+ removeAllViews();
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see android.view.View#findViewById(int)
+ * First look in our children, then in any header and footer views that may be scrolled off.
+ */
+ @Override
+ protected View findViewTraversal(int id) {
+ View v;
+ v = super.findViewTraversal(id);
+ if (v == null) {
+ v = findViewInHeadersOrFooters(mHeaderViewInfos, id);
+ if (v != null) {
+ return v;
+ }
+ v = findViewInHeadersOrFooters(mFooterViewInfos, id);
+ if (v != null) {
+ return v;
+ }
+ }
+ return v;
+ }
+
+ /* (non-Javadoc)
+ *
+ * Look in the passed in list of headers or footers for the view.
+ */
+ View findViewInHeadersOrFooters(ArrayList<FixedViewInfo> where, int id) {
+ if (where != null) {
+ int len = where.size();
+ View v;
+
+ for (int i = 0; i < len; i++) {
+ v = where.get(i).view;
+
+ if (!v.isRootNamespace()) {
+ v = v.findViewById(id);
+
+ if (v != null) {
+ return v;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ /* (non-Javadoc)
+ * @see android.view.View#findViewWithTag(String)
+ * First look in our children, then in any header and footer views that may be scrolled off.
+ */
+ @Override
+ protected View findViewWithTagTraversal(Object tag) {
+ View v;
+ v = super.findViewWithTagTraversal(tag);
+ if (v == null) {
+ v = findViewTagInHeadersOrFooters(mHeaderViewInfos, tag);
+ if (v != null) {
+ return v;
+ }
+
+ v = findViewTagInHeadersOrFooters(mFooterViewInfos, tag);
+ if (v != null) {
+ return v;
+ }
+ }
+ return v;
+ }
+
+ /* (non-Javadoc)
+ *
+ * Look in the passed in list of headers or footers for the view with the tag.
+ */
+ View findViewTagInHeadersOrFooters(ArrayList<FixedViewInfo> where, Object tag) {
+ if (where != null) {
+ int len = where.size();
+ View v;
+
+ for (int i = 0; i < len; i++) {
+ v = where.get(i).view;
+
+ if (!v.isRootNamespace()) {
+ v = v.findViewWithTag(tag);
+
+ if (v != null) {
+ return v;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ if (mItemsCanFocus && ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) {
+ // Don't handle edge touches immediately -- they may actually belong to one of our
+ // descendants.
+ return false;
+ }
+ return super.onTouchEvent(ev);
+ }
+
+ /**
+ * @see #setChoiceMode(int)
+ *
+ * @return The current choice mode
+ */
+ public int getChoiceMode() {
+ return mChoiceMode;
+ }
+
+ /**
+ * Defines the choice behavior for the List. By default, Lists do not have any choice behavior
+ * ({@link #CHOICE_MODE_NONE}). By setting the choiceMode to {@link #CHOICE_MODE_SINGLE}, the
+ * List allows up to one item to be in a chosen state. By setting the choiceMode to
+ * {@link #CHOICE_MODE_MULTIPLE}, the list allows any number of items to be chosen.
+ *
+ * @param choiceMode One of {@link #CHOICE_MODE_NONE}, {@link #CHOICE_MODE_SINGLE}, or
+ * {@link #CHOICE_MODE_MULTIPLE}
+ */
+ public void setChoiceMode(int choiceMode) {
+ mChoiceMode = choiceMode;
+ if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates == null) {
+ mCheckStates = new SparseBooleanArray();
+ }
+ }
+
+ @Override
+ public boolean performItemClick(View view, int position, long id) {
+ boolean handled = false;
+
+ if (mChoiceMode != CHOICE_MODE_NONE) {
+ handled = true;
+
+ if (mChoiceMode == CHOICE_MODE_MULTIPLE) {
+ boolean oldValue = mCheckStates.get(position, false);
+ mCheckStates.put(position, !oldValue);
+ } else {
+ boolean oldValue = mCheckStates.get(position, false);
+ if (!oldValue) {
+ mCheckStates.clear();
+ mCheckStates.put(position, true);
+ }
+ }
+
+ mDataChanged = true;
+ rememberSyncState();
+ requestLayout();
+ }
+
+ handled |= super.performItemClick(view, position, id);
+
+ return handled;
+ }
+
+ /**
+ * Sets the checked state of the specified position. The is only valid if
+ * the choice mode has been set to {@link #CHOICE_MODE_SINGLE} or
+ * {@link #CHOICE_MODE_MULTIPLE}.
+ *
+ * @param position The item whose checked state is to be checked
+ * @param value The new checked sate for the item
+ */
+ public void setItemChecked(int position, boolean value) {
+ if (mChoiceMode == CHOICE_MODE_NONE) {
+ return;
+ }
+
+ if (mChoiceMode == CHOICE_MODE_MULTIPLE) {
+ mCheckStates.put(position, value);
+ } else {
+ boolean oldValue = mCheckStates.get(position, false);
+ mCheckStates.clear();
+ if (!oldValue) {
+ mCheckStates.put(position, true);
+ }
+ }
+
+ // Do not generate a data change while we are in the layout phase
+ if (!mInLayout && !mBlockLayoutRequests) {
+ mDataChanged = true;
+ rememberSyncState();
+ requestLayout();
+ }
+ }
+
+ /**
+ * Returns the checked state of the specified position. The result is only
+ * valid if the choice mode has not been set to {@link #CHOICE_MODE_SINGLE}
+ * or {@link #CHOICE_MODE_MULTIPLE}.
+ *
+ * @param position The item whose checked state to return
+ * @return The item's checked state
+ *
+ * @see #setChoiceMode(int)
+ */
+ public boolean isItemChecked(int position) {
+ if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null) {
+ return mCheckStates.get(position);
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the currently checked item. The result is only valid if the choice
+ * mode has not been set to {@link #CHOICE_MODE_SINGLE}.
+ *
+ * @return The position of the currently checked item or
+ * {@link #INVALID_POSITION} if nothing is selected
+ *
+ * @see #setChoiceMode(int)
+ */
+ public int getCheckedItemPosition() {
+ if (mChoiceMode == CHOICE_MODE_SINGLE && mCheckStates != null && mCheckStates.size() == 1) {
+ return mCheckStates.keyAt(0);
+ }
+
+ return INVALID_POSITION;
+ }
+
+ /**
+ * Returns the set of checked items in the list. The result is only valid if
+ * the choice mode has not been set to {@link #CHOICE_MODE_SINGLE}.
+ *
+ * @return A SparseBooleanArray which will return true for each call to
+ * get(int position) where position is a position in the list.
+ */
+ public SparseBooleanArray getCheckedItemPositions() {
+ if (mChoiceMode != CHOICE_MODE_NONE) {
+ return mCheckStates;
+ }
+ return null;
+ }
+
+ /**
+ * Clear any choices previously set
+ */
+ public void clearChoices() {
+ if (mCheckStates != null) {
+ mCheckStates.clear();
+ }
+ }
+
+ static class SavedState extends BaseSavedState {
+ SparseBooleanArray checkState;
+
+ /**
+ * Constructor called from {@link ListView#onSaveInstanceState()}
+ */
+ SavedState(Parcelable superState, SparseBooleanArray checkState) {
+ super(superState);
+ this.checkState = checkState;
+ }
+
+ /**
+ * Constructor called from {@link #CREATOR}
+ */
+ private SavedState(Parcel in) {
+ super(in);
+ checkState = in.readSparseBooleanArray();
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ super.writeToParcel(out, flags);
+ out.writeSparseBooleanArray(checkState);
+ }
+
+ @Override
+ public String toString() {
+ return "ListView.SavedState{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " checkState=" + checkState + "}";
+ }
+
+ public static final Parcelable.Creator<SavedState> CREATOR
+ = new Parcelable.Creator<SavedState>() {
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ Parcelable superState = super.onSaveInstanceState();
+ return new SavedState(superState, mCheckStates);
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ SavedState ss = (SavedState) state;
+
+ super.onRestoreInstanceState(ss.getSuperState());
+
+ if (ss.checkState != null) {
+ mCheckStates = ss.checkState;
+ }
+
+ }
+}
diff --git a/core/java/android/widget/MediaController.java b/core/java/android/widget/MediaController.java
new file mode 100644
index 0000000..ad8433f
--- /dev/null
+++ b/core/java/android/widget/MediaController.java
@@ -0,0 +1,544 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.graphics.PixelFormat;
+import android.media.AudioManager;
+import android.os.Handler;
+import android.os.Message;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+
+import com.android.internal.policy.PolicyManager;
+
+import java.util.Formatter;
+import java.util.Locale;
+
+/**
+ * A view containing controls for a MediaPlayer. Typically contains the
+ * buttons like "Play/Pause", "Rewind", "Fast Forward" and a progress
+ * slider. It takes care of synchronizing the controls with the state
+ * of the MediaPlayer.
+ * <p>
+ * The way to use this class is to instantiate it programatically.
+ * The MediaController will create a default set of controls
+ * and put them in a window floating above your application. Specifically,
+ * the controls will float above the view specified with setAnchorView().
+ * The window will disappear if left idle for three seconds and reappear
+ * when the user touches the anchor view.
+ * <p>
+ * Functions like show() and hide() have no effect when MediaController
+ * is created in an xml layout.
+ *
+ * MediaController will hide and
+ * show the buttons according to these rules:
+ * <ul>
+ * <li> The "previous" and "next" buttons are hidden until setPrevNextListeners()
+ * has been called
+ * <li> The "previous" and "next" buttons are visible but disabled if
+ * setPrevNextListeners() was called with null listeners
+ * <li> The "rewind" and "fastforward" buttons are shown unless requested
+ * otherwise by using the MediaController(Context, boolean) constructor
+ * with the boolean set to false
+ * </ul>
+ */
+public class MediaController extends FrameLayout {
+
+ private MediaPlayerControl mPlayer;
+ private Context mContext;
+ private View mAnchor;
+ private View mRoot;
+ private WindowManager mWindowManager;
+ private Window mWindow;
+ private View mDecor;
+ private ProgressBar mProgress;
+ private TextView mEndTime, mCurrentTime;
+ private boolean mShowing;
+ private boolean mDragging;
+ private static final int sDefaultTimeout = 3000;
+ private static final int FADE_OUT = 1;
+ private static final int SHOW_PROGRESS = 2;
+ private boolean mUseFastForward;
+ private boolean mFromXml;
+ private boolean mListenersSet;
+ private View.OnClickListener mNextListener, mPrevListener;
+ StringBuilder mFormatBuilder;
+ Formatter mFormatter;
+ private ImageButton mPauseButton;
+ private ImageButton mFfwdButton;
+ private ImageButton mRewButton;
+ private ImageButton mNextButton;
+ private ImageButton mPrevButton;
+
+ public MediaController(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mRoot = this;
+ mContext = context;
+ mUseFastForward = true;
+ mFromXml = true;
+ }
+
+ @Override
+ public void onFinishInflate() {
+ if (mRoot != null)
+ initControllerView(mRoot);
+ }
+
+ public MediaController(Context context, boolean useFastForward) {
+ super(context);
+ mContext = context;
+ mUseFastForward = useFastForward;
+ initFloatingWindow();
+ }
+
+ public MediaController(Context context) {
+ super(context);
+ mContext = context;
+ mUseFastForward = true;
+ initFloatingWindow();
+ }
+
+ private void initFloatingWindow() {
+ mWindowManager = (WindowManager)mContext.getSystemService("window");
+ mWindow = PolicyManager.makeNewWindow(mContext);
+ mWindow.setWindowManager(mWindowManager, null, null);
+ mWindow.requestFeature(Window.FEATURE_NO_TITLE);
+ mDecor = mWindow.getDecorView();
+ mDecor.setOnTouchListener(mTouchListener);
+ mWindow.setContentView(this);
+ mWindow.setBackgroundDrawableResource(android.R.color.transparent);
+
+ // While the media controller is up, the volume control keys should
+ // affect the media stream type
+ mWindow.setVolumeControlStream(AudioManager.STREAM_MUSIC);
+
+ setFocusable(true);
+ setFocusableInTouchMode(true);
+ setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
+ requestFocus();
+ }
+
+ private OnTouchListener mTouchListener = new OnTouchListener() {
+ public boolean onTouch(View v, MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ if (mShowing) {
+ hide();
+ }
+ }
+ return false;
+ }
+ };
+
+ public void setMediaPlayer(MediaPlayerControl player) {
+ mPlayer = player;
+ updatePausePlay();
+ }
+
+ /**
+ * Set the view that acts as the anchor for the control view.
+ * This can for example be a VideoView, or your Activity's main view.
+ * @param view The view to which to anchor the controller when it is visible.
+ */
+ public void setAnchorView(View view) {
+ mAnchor = view;
+
+ FrameLayout.LayoutParams frameParams = new FrameLayout.LayoutParams(
+ ViewGroup.LayoutParams.FILL_PARENT,
+ ViewGroup.LayoutParams.FILL_PARENT
+ );
+
+ removeAllViews();
+ View v = makeControllerView();
+ addView(v, frameParams);
+ }
+
+ /**
+ * Create the view that holds the widgets that control playback.
+ * Derived classes can override this to create their own.
+ * @return The controller view.
+ * @hide This doesn't work as advertised
+ */
+ protected View makeControllerView() {
+ LayoutInflater inflate = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mRoot = inflate.inflate(com.android.internal.R.layout.media_controller, null);
+
+ initControllerView(mRoot);
+
+ return mRoot;
+ }
+
+ private void initControllerView(View v) {
+ mPauseButton = (ImageButton) v.findViewById(com.android.internal.R.id.pause);
+ if (mPauseButton != null) {
+ mPauseButton.requestFocus();
+ mPauseButton.setOnClickListener(mPauseListener);
+ }
+
+ mFfwdButton = (ImageButton) v.findViewById(com.android.internal.R.id.ffwd);
+ if (mFfwdButton != null) {
+ mFfwdButton.setOnClickListener(mFfwdListener);
+ if (!mFromXml) {
+ mFfwdButton.setVisibility(mUseFastForward ? View.VISIBLE : View.GONE);
+ }
+ }
+
+ mRewButton = (ImageButton) v.findViewById(com.android.internal.R.id.rew);
+ if (mRewButton != null) {
+ mRewButton.setOnClickListener(mRewListener);
+ if (!mFromXml) {
+ mRewButton.setVisibility(mUseFastForward ? View.VISIBLE : View.GONE);
+ }
+ }
+
+ // By default these are hidden. They will be enabled when setPrevNextListeners() is called
+ mNextButton = (ImageButton) v.findViewById(com.android.internal.R.id.next);
+ if (mNextButton != null && !mFromXml && !mListenersSet) {
+ mNextButton.setVisibility(View.GONE);
+ }
+ mPrevButton = (ImageButton) v.findViewById(com.android.internal.R.id.prev);
+ if (mPrevButton != null && !mFromXml && !mListenersSet) {
+ mPrevButton.setVisibility(View.GONE);
+ }
+
+ mProgress = (ProgressBar) v.findViewById(com.android.internal.R.id.mediacontroller_progress);
+ if (mProgress != null) {
+ if (mProgress instanceof SeekBar) {
+ SeekBar seeker = (SeekBar) mProgress;
+ seeker.setOnSeekBarChangeListener(mSeekListener);
+ }
+ mProgress.setMax(1000);
+ }
+
+ mEndTime = (TextView) v.findViewById(com.android.internal.R.id.time);
+ mCurrentTime = (TextView) v.findViewById(com.android.internal.R.id.time_current);
+ mFormatBuilder = new StringBuilder();
+ mFormatter = new Formatter(mFormatBuilder, Locale.getDefault());
+
+ installPrevNextListeners();
+ }
+
+ /**
+ * Show the controller on screen. It will go away
+ * automatically after 3 seconds of inactivity.
+ */
+ public void show() {
+ show(sDefaultTimeout);
+ }
+
+ /**
+ * Show the controller on screen. It will go away
+ * automatically after 'timeout' milliseconds of inactivity.
+ * @param timeout The timeout in milliseconds. Use 0 to show
+ * the controller until hide() is called.
+ */
+ public void show(int timeout) {
+
+ if (!mShowing && mAnchor != null) {
+ setProgress();
+
+ int [] anchorpos = new int[2];
+ mAnchor.getLocationOnScreen(anchorpos);
+
+ WindowManager.LayoutParams p = new WindowManager.LayoutParams();
+ p.gravity = Gravity.TOP;
+ p.width = mAnchor.getWidth();
+ p.height = LayoutParams.WRAP_CONTENT;
+ p.x = 0;
+ p.y = anchorpos[1] + mAnchor.getHeight() - p.height;
+ p.format = PixelFormat.TRANSLUCENT;
+ p.type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;
+ p.token = null;
+ p.windowAnimations = 0; // android.R.style.DropDownAnimationDown;
+ mWindowManager.addView(mDecor, p);
+ mShowing = true;
+ }
+ updatePausePlay();
+
+ // cause the progress bar to be updated even if mShowing
+ // was already true. This happens, for example, if we're
+ // paused with the progress bar showing the user hits play.
+ mHandler.sendEmptyMessage(SHOW_PROGRESS);
+
+ Message msg = mHandler.obtainMessage(FADE_OUT);
+ if (timeout != 0) {
+ mHandler.removeMessages(FADE_OUT);
+ mHandler.sendMessageDelayed(msg, timeout);
+ }
+ }
+
+ public boolean isShowing() {
+ return mShowing;
+ }
+
+ /**
+ * Remove the controller from the screen.
+ */
+ public void hide() {
+ if (mAnchor == null)
+ return;
+
+ if (mShowing) {
+ try {
+ mHandler.removeMessages(SHOW_PROGRESS);
+ mWindowManager.removeView(mDecor);
+ } catch (IllegalArgumentException ex) {
+ Log.w("MediaController", "already removed");
+ }
+ mShowing = false;
+ }
+ }
+
+ private Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ int pos;
+ switch (msg.what) {
+ case FADE_OUT:
+ hide();
+ break;
+ case SHOW_PROGRESS:
+ pos = setProgress();
+ if (!mDragging && mShowing && mPlayer.isPlaying()) {
+ msg = obtainMessage(SHOW_PROGRESS);
+ sendMessageDelayed(msg, 1000 - (pos % 1000));
+ }
+ break;
+ }
+ }
+ };
+
+ private String stringForTime(int timeMs) {
+ int totalSeconds = timeMs / 1000;
+
+ int seconds = totalSeconds % 60;
+ int minutes = (totalSeconds / 60) % 60;
+ int hours = totalSeconds / 3600;
+
+ mFormatBuilder.setLength(0);
+ if (hours > 0) {
+ return mFormatter.format("%d:%02d:%02d", hours, minutes, seconds).toString();
+ } else {
+ return mFormatter.format("%02d:%02d", minutes, seconds).toString();
+ }
+ }
+
+ private int setProgress() {
+ if (mPlayer == null || mDragging) {
+ return 0;
+ }
+ int position = mPlayer.getCurrentPosition();
+ int duration = mPlayer.getDuration();
+ if (mProgress != null) {
+ if (duration > 0) {
+ // use long to avoid overflow
+ long pos = 1000L * position / duration;
+ mProgress.setProgress( (int) pos);
+ }
+ int percent = mPlayer.getBufferPercentage();
+ mProgress.setSecondaryProgress(percent * 10);
+ }
+
+ if (mEndTime != null)
+ mEndTime.setText(stringForTime(duration));
+ if (mCurrentTime != null)
+ mCurrentTime.setText(stringForTime(position));
+
+ return position;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ show(sDefaultTimeout);
+ return true;
+ }
+
+ @Override
+ public boolean onTrackballEvent(MotionEvent ev) {
+ show(sDefaultTimeout);
+ return false;
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ int keyCode = event.getKeyCode();
+ if (event.getRepeatCount() == 0 && event.isDown() && (
+ keyCode == KeyEvent.KEYCODE_HEADSETHOOK ||
+ keyCode == KeyEvent.KEYCODE_SPACE)) {
+ doPauseResume();
+ show(sDefaultTimeout);
+ return true;
+ } else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN ||
+ keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
+ // don't show the controls for volume adjustment
+ return super.dispatchKeyEvent(event);
+ } else if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_MENU) {
+ hide();
+ } else {
+ show(sDefaultTimeout);
+ }
+ return super.dispatchKeyEvent(event);
+ }
+
+ private View.OnClickListener mPauseListener = new View.OnClickListener() {
+ public void onClick(View v) {
+ doPauseResume();
+ show(sDefaultTimeout);
+ }
+ };
+
+ private void updatePausePlay() {
+ if (mRoot == null)
+ return;
+
+ ImageButton button = (ImageButton) mRoot.findViewById(com.android.internal.R.id.pause);
+ if (button == null)
+ return;
+
+ if (mPlayer.isPlaying()) {
+ button.setImageResource(com.android.internal.R.drawable.ic_media_pause);
+ } else {
+ button.setImageResource(com.android.internal.R.drawable.ic_media_play);
+ }
+ }
+
+ private void doPauseResume() {
+ if (mPlayer.isPlaying()) {
+ mPlayer.pause();
+ } else {
+ mPlayer.start();
+ }
+ updatePausePlay();
+ }
+
+ private OnSeekBarChangeListener mSeekListener = new OnSeekBarChangeListener() {
+ long duration;
+ public void onStartTrackingTouch(SeekBar bar) {
+ show(3600000);
+ duration = mPlayer.getDuration();
+ }
+ public void onProgressChanged(SeekBar bar, int progress, boolean fromtouch) {
+ if (fromtouch) {
+ mDragging = true;
+ long newposition = (duration * progress) / 1000L;
+ mPlayer.seekTo( (int) newposition);
+ if (mCurrentTime != null)
+ mCurrentTime.setText(stringForTime( (int) newposition));
+ }
+ }
+ public void onStopTrackingTouch(SeekBar bar) {
+ mDragging = false;
+ setProgress();
+ updatePausePlay();
+ show(sDefaultTimeout);
+ }
+ };
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ if (mPauseButton != null) {
+ mPauseButton.setEnabled(enabled);
+ }
+ if (mFfwdButton != null) {
+ mFfwdButton.setEnabled(enabled);
+ }
+ if (mRewButton != null) {
+ mRewButton.setEnabled(enabled);
+ }
+ if (mNextButton != null) {
+ mNextButton.setEnabled(enabled && mNextListener != null);
+ }
+ if (mPrevButton != null) {
+ mPrevButton.setEnabled(enabled && mPrevListener != null);
+ }
+ if (mProgress != null) {
+ mProgress.setEnabled(enabled);
+ }
+
+ super.setEnabled(enabled);
+ }
+
+ private View.OnClickListener mRewListener = new View.OnClickListener() {
+ public void onClick(View v) {
+ int pos = mPlayer.getCurrentPosition();
+ pos -= 5000; // milliseconds
+ mPlayer.seekTo(pos);
+ setProgress();
+
+ show(sDefaultTimeout);
+ }
+ };
+
+ private View.OnClickListener mFfwdListener = new View.OnClickListener() {
+ public void onClick(View v) {
+ int pos = mPlayer.getCurrentPosition();
+ pos += 15000; // milliseconds
+ mPlayer.seekTo(pos);
+ setProgress();
+
+ show(sDefaultTimeout);
+ }
+ };
+
+ private void installPrevNextListeners() {
+ if (mNextButton != null) {
+ mNextButton.setOnClickListener(mNextListener);
+ mNextButton.setEnabled(mNextListener != null);
+ }
+
+ if (mPrevButton != null) {
+ mPrevButton.setOnClickListener(mPrevListener);
+ mPrevButton.setEnabled(mPrevListener != null);
+ }
+ }
+
+ public void setPrevNextListeners(View.OnClickListener next, View.OnClickListener prev) {
+ mNextListener = next;
+ mPrevListener = prev;
+ mListenersSet = true;
+
+ if (mRoot != null) {
+ installPrevNextListeners();
+
+ if (mNextButton != null && !mFromXml) {
+ mNextButton.setVisibility(View.VISIBLE);
+ }
+ if (mPrevButton != null && !mFromXml) {
+ mPrevButton.setVisibility(View.VISIBLE);
+ }
+ }
+ }
+
+ public interface MediaPlayerControl {
+ void start();
+ void pause();
+ int getDuration();
+ int getCurrentPosition();
+ void seekTo(int pos);
+ boolean isPlaying();
+ int getBufferPercentage();
+ };
+}
diff --git a/core/java/android/widget/MultiAutoCompleteTextView.java b/core/java/android/widget/MultiAutoCompleteTextView.java
new file mode 100644
index 0000000..59a9310
--- /dev/null
+++ b/core/java/android/widget/MultiAutoCompleteTextView.java
@@ -0,0 +1,282 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.text.Editable;
+import android.text.Selection;
+import android.text.Spanned;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.TextUtils;
+import android.text.method.QwertyKeyListener;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.internal.R;
+
+/**
+ * An editable text view, extending {@link AutoCompleteTextView}, that
+ * can show completion suggestions for the substring of the text where
+ * the user is typing instead of necessarily for the entire thing.
+ * <p>
+ * You must must provide a {@link Tokenizer} to distinguish the
+ * various substrings.
+ *
+ * <p>The following code snippet shows how to create a text view which suggests
+ * various countries names while the user is typing:</p>
+ *
+ * <pre class="prettyprint">
+ * public class CountriesActivity extends Activity {
+ * protected void onCreate(Bundle savedInstanceState) {
+ * super.onCreate(savedInstanceState);
+ * setContentView(R.layout.autocomplete_7);
+ *
+ * ArrayAdapter&lt;String&gt; adapter = new ArrayAdapter&lt;String&gt;(this,
+ * android.R.layout.simple_dropdown_item_1line, COUNTRIES);
+ * MultiAutoCompleteTextView textView = (MultiAutoCompleteTextView) findViewById(R.id.edit);
+ * textView.setAdapter(adapter);
+ * textView.setTokenizer(new MultiAutoCompleteTextView.CommaTokenizer());
+ * }
+ *
+ * private static final String[] COUNTRIES = new String[] {
+ * "Belgium", "France", "Italy", "Germany", "Spain"
+ * };
+ * }</pre>
+ */
+
+public class MultiAutoCompleteTextView extends AutoCompleteTextView {
+ private Tokenizer mTokenizer;
+
+ public MultiAutoCompleteTextView(Context context) {
+ this(context, null);
+ }
+
+ public MultiAutoCompleteTextView(Context context, AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.autoCompleteTextViewStyle);
+ }
+
+ public MultiAutoCompleteTextView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ /* package */ void finishInit() { }
+
+ /**
+ * Sets the Tokenizer that will be used to determine the relevant
+ * range of the text where the user is typing.
+ */
+ public void setTokenizer(Tokenizer t) {
+ mTokenizer = t;
+ }
+
+ /**
+ * Instead of filtering on the entire contents of the edit box,
+ * this subclass method filters on the range from
+ * {@link Tokenizer#findTokenStart} to {@link #getSelectionEnd}
+ * if the length of that range meets or exceeds {@link #getThreshold}.
+ */
+ @Override
+ protected void performFiltering(CharSequence text, int keyCode) {
+ if (enoughToFilter()) {
+ int end = getSelectionEnd();
+ int start = mTokenizer.findTokenStart(text, end);
+
+ performFiltering(text, start, end, keyCode);
+ } else {
+ dismissDropDown();
+
+ Filter f = getFilter();
+ if (f != null) {
+ f.filter(null);
+ }
+ }
+ }
+
+ /**
+ * Instead of filtering whenever the total length of the text
+ * exceeds the threshhold, this subclass filters only when the
+ * length of the range from
+ * {@link Tokenizer#findTokenStart} to {@link #getSelectionEnd}
+ * meets or exceeds {@link #getThreshold}.
+ */
+ @Override
+ public boolean enoughToFilter() {
+ Editable text = getText();
+
+ int end = getSelectionEnd();
+ if (end < 0) {
+ return false;
+ }
+
+ int start = mTokenizer.findTokenStart(text, end);
+
+ if (end - start >= getThreshold()) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Instead of validating the entire text, this subclass method validates
+ * each token of the text individually. Empty tokens are removed.
+ */
+ @Override
+ public void performValidation() {
+ Validator v = getValidator();
+
+ if (v == null) {
+ return;
+ }
+
+ Editable e = getText();
+ int i = getText().length();
+ while (i > 0) {
+ int start = mTokenizer.findTokenStart(e, i);
+ int end = mTokenizer.findTokenEnd(e, start);
+
+ CharSequence sub = e.subSequence(start, end);
+ if (TextUtils.isEmpty(sub)) {
+ e.replace(start, i, "");
+ } else if (!v.isValid(sub)) {
+ e.replace(start, i,
+ mTokenizer.terminateToken(v.fixText(sub)));
+ }
+
+ i = start;
+ }
+ }
+
+ /**
+ * <p>Starts filtering the content of the drop down list. The filtering
+ * pattern is the specified range of text from the edit box. Subclasses may
+ * override this method to filter with a different pattern, for
+ * instance a smaller substring of <code>text</code>.</p>
+ */
+ protected void performFiltering(CharSequence text, int start, int end,
+ int keyCode) {
+ getFilter().filter(text.subSequence(start, end), this);
+ }
+
+ /**
+ * <p>Performs the text completion by replacing the range from
+ * {@link Tokenizer#findTokenStart} to {@link #getSelectionEnd} by the
+ * the result of passing <code>text</code> through
+ * {@link Tokenizer#terminateToken}.
+ * In addition, the replaced region will be marked as an AutoText
+ * substition so that if the user immediately presses DEL, the
+ * completion will be undone.
+ * Subclasses may override this method to do some different
+ * insertion of the content into the edit box.</p>
+ *
+ * @param text the selected suggestion in the drop down list
+ */
+ @Override
+ protected void replaceText(CharSequence text) {
+ int end = getSelectionEnd();
+ int start = mTokenizer.findTokenStart(getText(), end);
+
+ Editable editable = getText();
+ String original = TextUtils.substring(editable, start, end);
+
+ QwertyKeyListener.markAsReplaced(editable, start, end, original);
+ editable.replace(start, end, mTokenizer.terminateToken(text));
+ }
+
+ public static interface Tokenizer {
+ /**
+ * Returns the start of the token that ends at offset
+ * <code>cursor</code> within <code>text</code>.
+ */
+ public int findTokenStart(CharSequence text, int cursor);
+
+ /**
+ * Returns the end of the token (minus trailing punctuation)
+ * that begins at offset <code>cursor</code> within <code>text</code>.
+ */
+ public int findTokenEnd(CharSequence text, int cursor);
+
+ /**
+ * Returns <code>text</code>, modified, if necessary, to ensure that
+ * it ends with a token terminator (for example a space or comma).
+ */
+ public CharSequence terminateToken(CharSequence text);
+ }
+
+ /**
+ * This simple Tokenizer can be used for lists where the items are
+ * separated by a comma and one or more spaces.
+ */
+ public static class CommaTokenizer implements Tokenizer {
+ public int findTokenStart(CharSequence text, int cursor) {
+ int i = cursor;
+
+ while (i > 0 && text.charAt(i - 1) != ',') {
+ i--;
+ }
+ while (i < cursor && text.charAt(i) == ' ') {
+ i++;
+ }
+
+ return i;
+ }
+
+ public int findTokenEnd(CharSequence text, int cursor) {
+ int i = cursor;
+ int len = text.length();
+
+ while (i < len) {
+ if (text.charAt(i) == ',') {
+ return i;
+ } else {
+ i++;
+ }
+ }
+
+ return len;
+ }
+
+ public CharSequence terminateToken(CharSequence text) {
+ int i = text.length();
+
+ while (i > 0 && text.charAt(i - 1) == ' ') {
+ i--;
+ }
+
+ if (i > 0 && text.charAt(i - 1) == ',') {
+ return text;
+ } else {
+ if (text instanceof Spanned) {
+ SpannableString sp = new SpannableString(text + ", ");
+ TextUtils.copySpansFrom((Spanned) text, 0, text.length(),
+ Object.class, sp, 0);
+ return sp;
+ } else {
+ return text + ", ";
+ }
+ }
+ }
+ }
+}
diff --git a/core/java/android/widget/PopupWindow.java b/core/java/android/widget/PopupWindow.java
new file mode 100644
index 0000000..6a7b1fb
--- /dev/null
+++ b/core/java/android/widget/PopupWindow.java
@@ -0,0 +1,803 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import com.android.internal.R;
+
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.WindowManagerImpl;
+import android.view.Gravity;
+import android.view.ViewGroup;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.IBinder;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+
+/**
+ * <p>A popup window that can be used to display an arbitrary view. The popup
+ * windows is a floating container that appears on top of the current
+ * activity.</p>
+ *
+ * @see android.widget.AutoCompleteTextView
+ * @see android.widget.Spinner
+ */
+public class PopupWindow {
+ /**
+ * The height of the status bar so we know how much of the screen we can
+ * actually be displayed in.
+ * <p>
+ * TODO: This IS NOT the right way to do this.
+ * Instead of knowing how much of the screen is available, a popup that
+ * wants anchor and maximize space shouldn't be setting a height, instead
+ * the PopupViewContainer should have its layout height as fill_parent and
+ * properly position the popup.
+ */
+ private static final int STATUS_BAR_HEIGHT = 30;
+
+ private boolean mIsShowing;
+
+ private View mContentView;
+ private View mPopupView;
+ private boolean mFocusable;
+
+ private int mWidth;
+ private int mHeight;
+
+ private int[] mDrawingLocation = new int[2];
+ private int[] mRootLocation = new int[2];
+ private Rect mTempRect = new Rect();
+
+ private Context mContext;
+ private Drawable mBackground;
+
+ private boolean mAboveAnchor;
+
+ private OnDismissListener mOnDismissListener;
+ private boolean mIgnoreCheekPress = false;
+
+ private int mAnimationStyle = -1;
+
+ private static final int[] ABOVE_ANCHOR_STATE_SET = new int[] {
+ com.android.internal.R.attr.state_above_anchor
+ };
+
+ /**
+ * <p>Create a new empty, non focusable popup window of dimension (0,0).</p>
+ *
+ * <p>The popup does provide a background.</p>
+ */
+ public PopupWindow(Context context) {
+ this(context, null);
+ }
+
+ /**
+ * <p>Create a new empty, non focusable popup window of dimension (0,0).</p>
+ *
+ * <p>The popup does provide a background.</p>
+ */
+ public PopupWindow(Context context, AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.popupWindowStyle);
+ }
+
+ /**
+ * <p>Create a new empty, non focusable popup window of dimension (0,0).</p>
+ *
+ * <p>The popup does provide a background.</p>
+ */
+ public PopupWindow(Context context, AttributeSet attrs, int defStyle) {
+ mContext = context;
+
+ TypedArray a =
+ context.obtainStyledAttributes(
+ attrs, com.android.internal.R.styleable.PopupWindow, defStyle, 0);
+
+ mBackground = a.getDrawable(R.styleable.PopupWindow_popupBackground);
+
+ a.recycle();
+ }
+
+ /**
+ * <p>Create a new empty, non focusable popup window of dimension (0,0).</p>
+ *
+ * <p>The popup does not provide any background. This should be handled
+ * by the content view.</p>
+ */
+ public PopupWindow() {
+ this(null, 0, 0);
+ }
+
+ /**
+ * <p>Create a new non focusable popup window which can display the
+ * <tt>contentView</tt>. The dimension of the window are (0,0).</p>
+ *
+ * <p>The popup does not provide any background. This should be handled
+ * by the content view.</p>
+ *
+ * @param contentView the popup's content
+ */
+ public PopupWindow(View contentView) {
+ this(contentView, 0, 0);
+ }
+
+ /**
+ * <p>Create a new empty, non focusable popup window. The dimension of the
+ * window must be passed to this constructor.</p>
+ *
+ * <p>The popup does not provide any background. This should be handled
+ * by the content view.</p>
+ *
+ * @param width the popup's width
+ * @param height the popup's height
+ */
+ public PopupWindow(int width, int height) {
+ this(null, width, height);
+ }
+
+ /**
+ * <p>Create a new non focusable popup window which can display the
+ * <tt>contentView</tt>. The dimension of the window must be passed to
+ * this constructor.</p>
+ *
+ * <p>The popup does not provide any background. This should be handled
+ * by the content view.</p>
+ *
+ * @param contentView the popup's content
+ * @param width the popup's width
+ * @param height the popup's height
+ */
+ public PopupWindow(View contentView, int width, int height) {
+ this(contentView, width, height, false);
+ }
+
+ /**
+ * <p>Create a new popup window which can display the <tt>contentView</tt>.
+ * The dimension of the window must be passed to this constructor.</p>
+ *
+ * <p>The popup does not provide any background. This should be handled
+ * by the content view.</p>
+ *
+ * @param contentView the popup's content
+ * @param width the popup's width
+ * @param height the popup's height
+ * @param focusable true if the popup can be focused, false otherwise
+ */
+ public PopupWindow(View contentView, int width, int height,
+ boolean focusable) {
+ setContentView(contentView);
+ setWidth(width);
+ setHeight(height);
+ setFocusable(focusable);
+ }
+
+ /**
+ * <p>Return the drawable used as the popup window's background.</p>
+ *
+ * @return the background drawable or null
+ */
+ public Drawable getBackground() {
+ return mBackground;
+ }
+
+ /**
+ * <p>Change the background drawable for this popup window. The background
+ * can be set to null.</p>
+ *
+ * @param background the popup's background
+ */
+ public void setBackgroundDrawable(Drawable background) {
+ mBackground = background;
+ }
+
+ /**
+ * <p>Return the animation style to use the popup appears and disappears</p>
+ *
+ * @return the animation style to use the popup appears and disappears
+ */
+ public int getAnimationStyle() {
+ return mAnimationStyle;
+ }
+
+ /**
+ * set the flag on popup to ignore cheek press events
+ * This method has to be invoked before displaying the content view
+ * of the popup for the window flags to take effect and will be ignored
+ * if the pop up is already displayed. By default this flag is set to false
+ * which means the pop wont ignore cheek press dispatch events.
+ */
+ public void setIgnoreCheekPress() {
+ mIgnoreCheekPress = true;
+ }
+
+
+ /**
+ * <p>Change the animation style for this popup.</p>
+ *
+ * @param animationStyle animation style to use when the popup appears and disappears
+ */
+ public void setAnimationStyle(int animationStyle) {
+ mAnimationStyle = animationStyle;
+ }
+
+ /**
+ * <p>Return the view used as the content of the popup window.</p>
+ *
+ * @return a {@link android.view.View} representing the popup's content
+ *
+ * @see #setContentView(android.view.View)
+ */
+ public View getContentView() {
+ return mContentView;
+ }
+
+ /**
+ * <p>Change the popup's content. The content is represented by an instance
+ * of {@link android.view.View}.</p>
+ *
+ * <p>This method has no effect if called when the popup is showing.</p>
+ *
+ * @param contentView the new content for the popup
+ *
+ * @see #getContentView()
+ * @see #isShowing()
+ */
+ public void setContentView(View contentView) {
+ if (isShowing()) {
+ return;
+ }
+
+ mContentView = contentView;
+ }
+
+ /**
+ * <p>Indicate whether the popup window can grab the focus.</p>
+ *
+ * @return true if the popup is focusable, false otherwise
+ *
+ * @see #setFocusable(boolean)
+ */
+ public boolean isFocusable() {
+ return mFocusable;
+ }
+
+ /**
+ * <p>Changes the focusability of the popup window. When focusable, the
+ * window will grab the focus from the current focused widget if the popup
+ * contains a focusable {@link android.view.View}.</p>
+ *
+ * <p>If the popup is showing, calling this method will take effect only
+ * the next time the popup is shown.</p>
+ *
+ * @param focusable true if the popup should grab focus, false otherwise
+ *
+ * @see #isFocusable()
+ * @see #isShowing()
+ */
+ public void setFocusable(boolean focusable) {
+ mFocusable = focusable;
+ }
+
+ /**
+ * <p>Return this popup's height MeasureSpec</p>
+ *
+ * @return the height MeasureSpec of the popup
+ *
+ * @see #setHeight(int)
+ */
+ public int getHeight() {
+ return mHeight;
+ }
+
+ /**
+ * <p>Change the popup's height MeasureSpec</p>
+ *
+ * <p>If the popup is showing, calling this method will take effect only
+ * the next time the popup is shown.</p>
+ *
+ * @param height the height MeasureSpec of the popup
+ *
+ * @see #getHeight()
+ * @see #isShowing()
+ */
+ public void setHeight(int height) {
+ mHeight = height;
+ }
+
+ /**
+ * <p>Return this popup's width MeasureSpec</p>
+ *
+ * @return the width MeasureSpec of the popup
+ *
+ * @see #setWidth(int)
+ */
+ public int getWidth() {
+ return mWidth;
+ }
+
+ /**
+ * <p>Change the popup's width MeasureSpec</p>
+ *
+ * <p>If the popup is showing, calling this method will take effect only
+ * the next time the popup is shown.</p>
+ *
+ * @param width the width MeasureSpec of the popup
+ *
+ * @see #getWidth()
+ * @see #isShowing()
+ */
+ public void setWidth(int width) {
+ mWidth = width;
+ }
+
+ /**
+ * <p>Indicate whether this popup window is showing on screen.</p>
+ *
+ * @return true if the popup is showing, false otherwise
+ */
+ public boolean isShowing() {
+ return mIsShowing;
+ }
+
+ /**
+ * <p>
+ * Display the content view in a popup window at the specified location. If the popup window
+ * cannot fit on screen, it will be clipped. See {@link android.view.WindowManager.LayoutParams}
+ * for more information on how gravity and the x and y parameters are related. Specifying
+ * a gravity of {@link android.view.Gravity#NO_GRAVITY} is similar to specifying
+ * <code>Gravity.LEFT | Gravity.TOP</code>.
+ * </p>
+ *
+ * @param parent a parent view to get the {@link android.view.View#getWindowToken()} token from
+ * @param gravity the gravity which controls the placement of the popup window
+ * @param x the popup's x location offset
+ * @param y the popup's y location offset
+ */
+ public void showAtLocation(View parent, int gravity, int x, int y) {
+ if (isShowing() || mContentView == null) {
+ return;
+ }
+
+ mIsShowing = true;
+
+ WindowManager.LayoutParams p = createPopupLayout(parent.getWindowToken());
+ if (mAnimationStyle != -1) {
+ p.windowAnimations = mAnimationStyle;
+ }
+
+ preparePopup(p);
+ if (gravity == Gravity.NO_GRAVITY) {
+ gravity = Gravity.TOP | Gravity.LEFT;
+ }
+ p.gravity = gravity;
+ p.x = x;
+ p.y = y;
+ invokePopup(p);
+ }
+
+ /**
+ * <p>Display the content view in a popup window anchored to the bottom-left
+ * corner of the anchor view. If there is not enough room on screen to show
+ * the popup in its entirety, this method tries to find a parent scroll
+ * view to scroll. If no parent scroll view can be scrolled, the bottom-left
+ * corner of the popup is pinned at the top left corner of the anchor view.</p>
+ *
+ * @param anchor the view on which to pin the popup window
+ *
+ * @see #dismiss()
+ */
+ public void showAsDropDown(View anchor) {
+ showAsDropDown(anchor, 0, 0);
+ }
+
+ /**
+ * <p>Display the content view in a popup window anchored to the bottom-left
+ * corner of the anchor view offset by the specified x and y coordinates.
+ * If there is not enough room on screen to show
+ * the popup in its entirety, this method tries to find a parent scroll
+ * view to scroll. If no parent scroll view can be scrolled, the bottom-left
+ * corner of the popup is pinned at the top left corner of the anchor view.</p>
+ *
+ * @param anchor the view on which to pin the popup window
+ *
+ * @see #dismiss()
+ */
+ public void showAsDropDown(View anchor, int xoff, int yoff) {
+ if (isShowing() || mContentView == null) {
+ return;
+ }
+
+ mIsShowing = true;
+
+ WindowManager.LayoutParams p = createPopupLayout(anchor.getWindowToken());
+ preparePopup(p);
+ if (mBackground != null) {
+ mPopupView.refreshDrawableState();
+ }
+ mAboveAnchor = findDropDownPosition(anchor, p, xoff, yoff);
+ if (mAnimationStyle == -1) {
+ p.windowAnimations = mAboveAnchor
+ ? com.android.internal.R.style.Animation_DropDownUp
+ : com.android.internal.R.style.Animation_DropDownDown;
+ } else {
+ p.windowAnimations = mAnimationStyle;
+ }
+ invokePopup(p);
+ }
+
+ /**
+ * <p>Prepare the popup by embedding in into a new ViewGroup if the
+ * background drawable is not null. If embedding is required, the layout
+ * parameters' height is mnodified to take into account the background's
+ * padding.</p>
+ *
+ * @param p the layout parameters of the popup's content view
+ */
+ private void preparePopup(WindowManager.LayoutParams p) {
+ if (mBackground != null) {
+ // when a background is available, we embed the content view
+ // within another view that owns the background drawable
+ PopupViewContainer popupViewContainer = new PopupViewContainer(mContext);
+ PopupViewContainer.LayoutParams listParams = new PopupViewContainer.LayoutParams(
+ ViewGroup.LayoutParams.FILL_PARENT,
+ ViewGroup.LayoutParams.FILL_PARENT
+ );
+ popupViewContainer.setBackgroundDrawable(mBackground);
+ popupViewContainer.addView(mContentView, listParams);
+
+ if (p.height >= 0) {
+ // accomodate the popup's height to take into account the
+ // background's padding
+ p.height += popupViewContainer.getPaddingTop() +
+ popupViewContainer.getPaddingBottom();
+ }
+ if (p.width >= 0) {
+ // accomodate the popup's width to take into account the
+ // background's padding
+ p.width += popupViewContainer.getPaddingLeft() +
+ popupViewContainer.getPaddingRight();
+ }
+ mPopupView = popupViewContainer;
+ } else {
+ mPopupView = mContentView;
+ }
+
+ }
+
+ /**
+ * <p>Invoke the popup window by adding the content view to the window
+ * manager.</p>
+ *
+ * <p>The content view must be non-null when this method is invoked.</p>
+ *
+ * @param p the layout parameters of the popup's content view
+ */
+ private void invokePopup(WindowManager.LayoutParams p) {
+ WindowManagerImpl wm = WindowManagerImpl.getDefault();
+ wm.addView(mPopupView, p);
+ }
+
+ /**
+ * <p>Generate the layout parameters for the popup window.</p>
+ *
+ * @param token the window token used to bind the popup's window
+ *
+ * @return the layout parameters to pass to the window manager
+ */
+ private WindowManager.LayoutParams createPopupLayout(IBinder token) {
+ // generates the layout parameters for the drop down
+ // we want a fixed size view located at the bottom left of the anchor
+ WindowManager.LayoutParams p = new WindowManager.LayoutParams();
+ // these gravity settings put the view at the top left corner of the
+ // screen. The view is then positioned to the appropriate location
+ // by setting the x and y offsets to match the anchor's bottom
+ // left corner
+ p.gravity = Gravity.LEFT | Gravity.TOP;
+ p.width = mWidth;
+ p.height = mHeight;
+ if (mBackground != null) {
+ p.format = mBackground.getOpacity();
+ } else {
+ p.format = PixelFormat.TRANSLUCENT;
+ }
+ if(mIgnoreCheekPress) {
+ p.flags |= WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES;
+ }
+ if (!mFocusable) {
+ p.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+ }
+ p.type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;
+ p.token = token;
+
+ return p;
+ }
+
+ /**
+ * <p>Positions the popup window on screen. When the popup window is too
+ * tall to fit under the anchor, a parent scroll view is seeked and scrolled
+ * up to reclaim space. If scrolling is not possible or not enough, the
+ * popup window gets moved on top of the anchor.</p>
+ *
+ * <p>The height must have been set on the layout parameters prior to
+ * calling this method.</p>
+ *
+ * @param anchor the view on which the popup window must be anchored
+ * @param p the layout parameters used to display the drop down
+ *
+ * @return true if the popup is translated upwards to fit on screen
+ */
+ private boolean findDropDownPosition(View anchor, WindowManager.LayoutParams p, int xoff, int yoff) {
+ anchor.getLocationInWindow(mDrawingLocation);
+ p.x = mDrawingLocation[0] + xoff;
+ p.y = mDrawingLocation[1] + anchor.getMeasuredHeight() + yoff;
+
+ boolean onTop = false;
+
+ if (p.y + p.height > WindowManagerImpl.getDefault().getDefaultDisplay().getHeight()) {
+ // if the drop down disappears at the bottom of the screen. we try to
+ // scroll a parent scrollview or move the drop down back up on top of
+ // the edit box
+ View root = anchor.getRootView();
+ root.getLocationInWindow(mRootLocation);
+ int delta = p.y + p.height - mRootLocation[1] - root.getHeight();
+
+ if (delta > 0 || p.x + p.width - mRootLocation[0] - root.getWidth() > 0) {
+ Rect r = new Rect(anchor.getScrollX(), anchor.getScrollY(),
+ p.width, p.height + anchor.getMeasuredHeight());
+
+ onTop = !anchor.requestRectangleOnScreen(r, true);
+
+ if (onTop) {
+ p.y -= anchor.getMeasuredHeight() + p.height;
+ } else {
+ anchor.getLocationOnScreen(mDrawingLocation);
+ p.x = mDrawingLocation[0] + xoff;
+ p.y = mDrawingLocation[1] + anchor.getMeasuredHeight() + yoff;
+ }
+ }
+ }
+
+ return onTop;
+ }
+
+ /**
+ * Returns the maximum height that is available for the popup to be
+ * completely shown. It is recommended that this height be the maximum for
+ * the popup's height, otherwise it is possible that the popup will be
+ * clipped.
+ *
+ * @param anchor The view on which the popup window must be anchored.
+ * @return The maximum available height for the popup to be completely
+ * shown.
+ */
+ public int getMaxAvailableHeight(View anchor) {
+ // TODO: read comment on STATUS_BAR_HEIGHT
+ final int screenHeight = WindowManagerImpl.getDefault().getDefaultDisplay().getHeight()
+ - STATUS_BAR_HEIGHT;
+
+ final int[] anchorPos = mDrawingLocation;
+ anchor.getLocationOnScreen(anchorPos);
+ anchorPos[1] -= STATUS_BAR_HEIGHT;
+
+ final int distanceFromAnchorToBottom = screenHeight - (anchorPos[1] + anchor.getHeight());
+
+ // anchorPos[1] is distance from anchor to top of screen
+ int returnedHeight = Math.max(anchorPos[1], distanceFromAnchorToBottom);
+ if (mBackground != null) {
+ mBackground.getPadding(mTempRect);
+ returnedHeight -= mTempRect.top + mTempRect.bottom;
+ }
+
+ return returnedHeight;
+ }
+
+ /**
+ * <p>Dispose of the popup window. This method can be invoked only after
+ * {@link #showAsDropDown(android.view.View)} has been executed. Failing that, calling
+ * this method will have no effect.</p>
+ *
+ * @see #showAsDropDown(android.view.View)
+ */
+ public void dismiss() {
+ if (isShowing() && mPopupView != null) {
+ WindowManagerImpl wm = WindowManagerImpl.getDefault();
+ wm.removeView(mPopupView);
+ if (mPopupView != mContentView && mPopupView instanceof ViewGroup) {
+ ((ViewGroup) mPopupView).removeView(mContentView);
+ }
+ mIsShowing = false;
+
+ if (mOnDismissListener != null) {
+ mOnDismissListener.onDismiss();
+ }
+ }
+ }
+
+ /**
+ * Sets the listener to be called when the window is dismissed.
+ *
+ * @param onDismissListener The listener.
+ */
+ public void setOnDismissListener(OnDismissListener onDismissListener) {
+ mOnDismissListener = onDismissListener;
+ }
+
+ /**
+ * <p>Updates the position and the dimension of the popup window. Width and
+ * height can be set to -1 to update location only.</p>
+ *
+ * @param x the new x location
+ * @param y the new y location
+ * @param width the new width, can be -1 to ignore
+ * @param height the new height, can be -1 to ignore
+ */
+ public void update(int x, int y, int width, int height) {
+ if (width != -1) {
+ setWidth(width);
+ }
+
+ if (height != -1) {
+ setHeight(height);
+ }
+
+ if (!isShowing() || mContentView == null) {
+ return;
+ }
+
+ WindowManager.LayoutParams p = (WindowManager.LayoutParams)
+ mPopupView.getLayoutParams();
+
+ boolean update = false;
+
+ if (width != -1 && p.width != width) {
+ p.width = width;
+ update = true;
+ }
+
+ if (height != -1 && p.height != height) {
+ p.height = height;
+ update = true;
+ }
+
+ if (p.x != x) {
+ p.x = x;
+ update = true;
+ }
+
+ if (p.y != y) {
+ p.y = y;
+ update = true;
+ }
+
+ if (update) {
+ if (mPopupView != mContentView) {
+ final View popupViewContainer = mPopupView;
+ if (p.height >= 0) {
+ // accomodate the popup's height to take into account the
+ // background's padding
+ p.height += popupViewContainer.getPaddingTop() +
+ popupViewContainer.getPaddingBottom();
+ }
+ if (p.width >= 0) {
+ // accomodate the popup's width to take into account the
+ // background's padding
+ p.width += popupViewContainer.getPaddingLeft() +
+ popupViewContainer.getPaddingRight();
+ }
+ }
+
+ WindowManagerImpl wm = WindowManagerImpl.getDefault();
+ wm.updateViewLayout(mPopupView, p);
+ }
+ }
+
+ /**
+ * <p>Updates the position and the dimension of the popup window. Width and
+ * height can be set to -1 to update location only.</p>
+ *
+ * @param anchor the popup's anchor view
+ * @param width the new width, can be -1 to ignore
+ * @param height the new height, can be -1 to ignore
+ */
+ public void update(View anchor, int width, int height) {
+ update(anchor, 0, 0, width, height);
+ }
+
+ /**
+ * <p>Updates the position and the dimension of the popup window. Width and
+ * height can be set to -1 to update location only.</p>
+ *
+ * @param anchor the popup's anchor view
+ * @param xoff x offset from the view's left edge
+ * @param yoff y offset from the view's bottom edge
+ * @param width the new width, can be -1 to ignore
+ * @param height the new height, can be -1 to ignore
+ */
+ public void update(View anchor, int xoff, int yoff, int width, int height) {
+ if (!isShowing() || mContentView == null) {
+ return;
+ }
+
+ WindowManager.LayoutParams p = (WindowManager.LayoutParams)
+ mPopupView.getLayoutParams();
+
+ int x = p.x;
+ int y = p.y;
+ findDropDownPosition(anchor, p, xoff, yoff);
+
+ update(x, y, width, height);
+ }
+
+ /**
+ * Listener that is called when this popup window is dismissed.
+ */
+ interface OnDismissListener {
+ /**
+ * Called when this popup window is dismissed.
+ */
+ public void onDismiss();
+ }
+
+ private class PopupViewContainer extends FrameLayout {
+
+ public PopupViewContainer(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected int[] onCreateDrawableState(int extraSpace) {
+ if (mAboveAnchor) {
+ // 1 more needed for the above anchor state
+ final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
+ View.mergeDrawableStates(drawableState, ABOVE_ANCHOR_STATE_SET);
+ return drawableState;
+ } else {
+ return super.onCreateDrawableState(extraSpace);
+ }
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
+ dismiss();
+ return true;
+ } else {
+ return super.dispatchKeyEvent(event);
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ final int x = (int) event.getX();
+ final int y = (int) event.getY();
+
+ if ((event.getAction() == MotionEvent.ACTION_DOWN)
+ && ((x < 0) || (x >= getWidth()) || (y < 0) || (y >= getHeight()))) {
+ dismiss();
+ return true;
+ } else {
+ return super.onTouchEvent(event);
+ }
+ }
+
+ }
+
+}
diff --git a/core/java/android/widget/ProgressBar.java b/core/java/android/widget/ProgressBar.java
new file mode 100644
index 0000000..c1de010
--- /dev/null
+++ b/core/java/android/widget/ProgressBar.java
@@ -0,0 +1,820 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.BitmapShader;
+import android.graphics.Canvas;
+import android.graphics.Shader;
+import android.graphics.drawable.AnimationDrawable;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.ClipDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
+import android.graphics.drawable.ShapeDrawable;
+import android.graphics.drawable.shapes.RoundRectShape;
+import android.graphics.drawable.shapes.Shape;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+import android.view.animation.LinearInterpolator;
+import android.view.animation.Transformation;
+import android.widget.RemoteViews.RemoteView;
+import android.os.SystemClock;
+
+import com.android.internal.R;
+
+
+/**
+ * <p>
+ * Visual indicator of progress in some operation. Displays a bar to the user
+ * representing how far the operation has progressed; the application can
+ * change the amount of progress (modifying the length of the bar) as it moves
+ * forward. There is also a secondary progress displayable on a progress bar
+ * which is useful for displaying intermediate progress, such as the buffer
+ * level during a streaming playback progress bar.
+ * </p>
+ *
+ * <p>
+ * A progress bar can also be made indeterminate. In indeterminate mode, the
+ * progress bar shows a cyclic animation. This mode is used by applications
+ * when the length of the task is unknown.
+ * </p>
+ *
+ * <p>The following code example shows how a progress bar can be used from
+ * a worker thread to update the user interface to notify the user of progress:
+ * </p>
+ *
+ * <pre class="prettyprint">
+ * public class MyActivity extends Activity {
+ * private static final int PROGRESS = 0x1;
+ *
+ * private ProgressBar mProgress;
+ * private int mProgressStatus = 0;
+ *
+ * private Handler mHandler = new Handler();
+ *
+ * protected void onCreate(Bundle icicle) {
+ * super.onCreate(icicle);
+ *
+ * setContentView(R.layout.progressbar_activity);
+ *
+ * mProgress = (ProgressBar) findViewById(R.id.progress_bar);
+ *
+ * // Start lengthy operation in a background thread
+ * new Thread(new Runnable() {
+ * public void run() {
+ * while (mProgressStatus < 100) {
+ * mProgressStatus = doWork();
+ *
+ * // Update the progress bar
+ * mHandler.post(new Runnable() {
+ * public void run() {
+ * mProgress.setProgress(mProgressStatus);
+ * }
+ * });
+ * }
+ * }
+ * }).start();
+ * }
+ * }
+ * </pre>
+ *
+ * <p><strong>XML attributes</b></strong>
+ * <p>
+ * See {@link android.R.styleable#ProgressBar ProgressBar Attributes},
+ * {@link android.R.styleable#View View Attributes}
+ * </p>
+ *
+ * <p><strong>Styles</b></strong>
+ * <p>
+ * @attr ref android.R.styleable#Theme_progressBarStyle
+ * @attr ref android.R.styleable#Theme_progressBarStyleSmall
+ * @attr ref android.R.styleable#Theme_progressBarStyleLarge
+ * @attr ref android.R.styleable#Theme_progressBarStyleHorizontal
+ * </p>
+ */
+@RemoteView
+public class ProgressBar extends View {
+ private static final int MAX_LEVEL = 10000;
+ private static final int ANIMATION_RESOLUTION = 200;
+
+ int mMinWidth;
+ int mMaxWidth;
+ int mMinHeight;
+ int mMaxHeight;
+
+ private int mProgress;
+ private int mSecondaryProgress;
+ private int mMax;
+
+ private int mBehavior;
+ private int mDuration;
+ private boolean mIndeterminate;
+ private boolean mOnlyIndeterminate;
+ private Transformation mTransformation;
+ private AlphaAnimation mAnimation;
+ private Drawable mIndeterminateDrawable;
+ private Drawable mProgressDrawable;
+ private Drawable mCurrentDrawable;
+ Bitmap mSampleTile;
+ private boolean mNoInvalidate;
+ private Interpolator mInterpolator;
+ private RefreshProgressRunnable mRefreshProgressRunnable;
+ private long mUiThreadId;
+ private boolean mShouldStartAnimationDrawable;
+ private long mLastDrawTime;
+
+ private boolean mInDrawing;
+
+ /**
+ * Create a new progress bar with range 0...100 and initial progress of 0.
+ * @param context the application environment
+ */
+ public ProgressBar(Context context) {
+ this(context, null);
+ }
+
+ public ProgressBar(Context context, AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.progressBarStyle);
+ }
+
+ public ProgressBar(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ mUiThreadId = Thread.currentThread().getId();
+ initProgressBar();
+
+ TypedArray a =
+ context.obtainStyledAttributes(attrs, R.styleable.ProgressBar, defStyle, 0);
+
+ mNoInvalidate = true;
+
+ Drawable drawable = a.getDrawable(R.styleable.ProgressBar_progressDrawable);
+ if (drawable != null) {
+ drawable = tileify(drawable);
+ setProgressDrawable(drawable);
+ }
+
+
+ mDuration = a.getInt(R.styleable.ProgressBar_indeterminateDuration, mDuration);
+
+ mMinWidth = a.getDimensionPixelSize(R.styleable.ProgressBar_minWidth, mMinWidth);
+ mMaxWidth = a.getDimensionPixelSize(R.styleable.ProgressBar_maxWidth, mMaxWidth);
+ mMinHeight = a.getDimensionPixelSize(R.styleable.ProgressBar_minHeight, mMinHeight);
+ mMaxHeight = a.getDimensionPixelSize(R.styleable.ProgressBar_maxHeight, mMaxHeight);
+
+ mBehavior = a.getInt(R.styleable.ProgressBar_indeterminateBehavior, mBehavior);
+
+ final int resID = a.getResourceId(com.android.internal.R.styleable.ProgressBar_interpolator, -1);
+ if (resID > 0) {
+ setInterpolator(context, resID);
+ }
+
+ setMax(a.getInt(R.styleable.ProgressBar_max, mMax));
+
+ setProgress(a.getInt(R.styleable.ProgressBar_progress, mProgress));
+
+ setSecondaryProgress(
+ a.getInt(R.styleable.ProgressBar_secondaryProgress, mSecondaryProgress));
+
+ drawable = a.getDrawable(R.styleable.ProgressBar_indeterminateDrawable);
+ if (drawable != null) {
+ drawable = tileifyIndeterminate(drawable);
+ setIndeterminateDrawable(drawable);
+ }
+
+ mOnlyIndeterminate = a.getBoolean(
+ R.styleable.ProgressBar_indeterminateOnly, mOnlyIndeterminate);
+
+ mNoInvalidate = false;
+
+ setIndeterminate(mOnlyIndeterminate || a.getBoolean(
+ R.styleable.ProgressBar_indeterminate, mIndeterminate));
+
+ a.recycle();
+ }
+
+ /*
+ * TODO: This is almost ready to be removed. This was used to support our
+ * old style of progress bars with the ticks. Need to check with designers
+ * on whether they can give us a transparent 'tick' overlay tile for our new
+ * gradient-based progress bars. (We still need the ticked progress bar for
+ * media player apps.) I'll remove this and add XML support if they want to
+ * do the overlay approach. If they want to just have a separate style for
+ * this legacy stuff, then we can keep it around.
+ */
+
+ // TODO Remove all this once ShapeDrawable + shaders are supported through XML
+ private Drawable tileify(Drawable drawable) {
+ if (drawable instanceof LayerDrawable) {
+ LayerDrawable background = (LayerDrawable) drawable;
+ final int N = background.getNumberOfLayers();
+ Drawable[] outDrawables = new Drawable[N];
+
+ for (int i = 0; i < N; i++) {
+ int id = background.getId(i);
+ outDrawables[i] = createDrawableForTile(background.getDrawable(i),
+ (id == R.id.progress || id == R.id.secondaryProgress));
+ }
+
+ LayerDrawable newBg = new LayerDrawable(outDrawables);
+
+ for (int i = 0; i < N; i++) {
+ newBg.setId(i, background.getId(i));
+ }
+
+ drawable = newBg;
+ }
+ return drawable;
+ }
+
+ // TODO Remove all this once ShapeDrawable + shaders are supported through XML
+ private Drawable createDrawableForTile(Drawable tileDrawable, boolean clip) {
+ if (!(tileDrawable instanceof BitmapDrawable)) return tileDrawable;
+
+ final Bitmap tileBitmap = ((BitmapDrawable) tileDrawable).getBitmap();
+ if (mSampleTile == null) {
+ mSampleTile = tileBitmap;
+ }
+
+ final ShapeDrawable shapeDrawable = new ShapeDrawable(getDrawableShape());
+
+ final BitmapShader bitmapShader = new BitmapShader(tileBitmap,
+ Shader.TileMode.REPEAT, Shader.TileMode.CLAMP);
+ shapeDrawable.getPaint().setShader(bitmapShader);
+
+ return (clip) ? new ClipDrawable(shapeDrawable, Gravity.LEFT,
+ ClipDrawable.HORIZONTAL) : shapeDrawable;
+ }
+
+ Shape getDrawableShape() {
+ final float[] roundedCorners = new float[] { 5, 5, 5, 5, 5, 5, 5, 5 };
+ return new RoundRectShape(roundedCorners, null, null);
+ }
+
+ /**
+ * Convert a AnimationDrawable for use as a barberpole animation.
+ * Each frame of the animation is wrapped in a ClipDrawable and
+ * given a tiling BitmapShader.
+ */
+ private Drawable tileifyIndeterminate(Drawable drawable) {
+ if (drawable instanceof AnimationDrawable) {
+ AnimationDrawable background = (AnimationDrawable) drawable;
+ final int N = background.getNumberOfFrames();
+ AnimationDrawable newBg = new AnimationDrawable();
+ newBg.setOneShot(background.isOneShot());
+
+ for (int i = 0; i < N; i++) {
+ Drawable frame = createDrawableForTile(background.getFrame(i), true);
+ frame.setLevel(10000);
+ newBg.addFrame(frame, background.getDuration(i));
+ }
+ newBg.setLevel(10000);
+ drawable = newBg;
+ }
+ return drawable;
+ }
+
+ /**
+ * <p>
+ * Initialize the progress bar's default values:
+ * </p>
+ * <ul>
+ * <li>progress = 0</li>
+ * <li>max = 100</li>
+ * <li>animation duration = 4000 ms</li>
+ * <li>indeterminate = false</li>
+ * <li>behavior = repeat</li>
+ * </ul>
+ */
+ private void initProgressBar() {
+ mMax = 100;
+ mProgress = 0;
+ mSecondaryProgress = 0;
+ mIndeterminate = false;
+ mOnlyIndeterminate = false;
+ mDuration = 4000;
+ mBehavior = AlphaAnimation.RESTART;
+ mMinWidth = 24;
+ mMaxWidth = 48;
+ mMinHeight = 24;
+ mMaxHeight = 48;
+ }
+
+ /**
+ * <p>Indicate whether this progress bar is in indeterminate mode.</p>
+ *
+ * @return true if the progress bar is in indeterminate mode
+ */
+ public synchronized boolean isIndeterminate() {
+ return mIndeterminate;
+ }
+
+ /**
+ * <p>Change the indeterminate mode for this progress bar. In indeterminate
+ * mode, the progress is ignored and the progress bar shows an infinite
+ * animation instead.</p>
+ *
+ * If this progress bar's style only supports indeterminate mode (such as the circular
+ * progress bars), then this will be ignored.
+ *
+ * @param indeterminate true to enable the indeterminate mode
+ */
+ public synchronized void setIndeterminate(boolean indeterminate) {
+ if ((!mOnlyIndeterminate || !mIndeterminate) && indeterminate != mIndeterminate) {
+ mIndeterminate = indeterminate;
+
+ if (indeterminate) {
+ // swap between indeterminate and regular backgrounds
+ mCurrentDrawable = mIndeterminateDrawable;
+ startAnimation();
+ } else {
+ mCurrentDrawable = mProgressDrawable;
+ stopAnimation();
+ }
+ }
+ }
+
+ /**
+ * <p>Get the drawable used to draw the progress bar in
+ * indeterminate mode.</p>
+ *
+ * @return a {@link android.graphics.drawable.Drawable} instance
+ *
+ * @see #setIndeterminateDrawable(android.graphics.drawable.Drawable)
+ * @see #setIndeterminate(boolean)
+ */
+ public Drawable getIndeterminateDrawable() {
+ return mIndeterminateDrawable;
+ }
+
+ /**
+ * <p>Define the drawable used to draw the progress bar in
+ * indeterminate mode.</p>
+ *
+ * @param d the new drawable
+ *
+ * @see #getIndeterminateDrawable()
+ * @see #setIndeterminate(boolean)
+ */
+ public void setIndeterminateDrawable(Drawable d) {
+ if (d != null) {
+ d.setCallback(this);
+ }
+ mIndeterminateDrawable = d;
+ if (mIndeterminate) {
+ mCurrentDrawable = d;
+ postInvalidate();
+ }
+ }
+
+ /**
+ * <p>Get the drawable used to draw the progress bar in
+ * progress mode.</p>
+ *
+ * @return a {@link android.graphics.drawable.Drawable} instance
+ *
+ * @see #setProgressDrawable(android.graphics.drawable.Drawable)
+ * @see #setIndeterminate(boolean)
+ */
+ public Drawable getProgressDrawable() {
+ return mProgressDrawable;
+ }
+
+ /**
+ * <p>Define the drawable used to draw the progress bar in
+ * progress mode.</p>
+ *
+ * @param d the new drawable
+ *
+ * @see #getProgressDrawable()
+ * @see #setIndeterminate(boolean)
+ */
+ public void setProgressDrawable(Drawable d) {
+ if (d != null) {
+ d.setCallback(this);
+ }
+ mProgressDrawable = d;
+ if (!mIndeterminate) {
+ mCurrentDrawable = d;
+ postInvalidate();
+ }
+ }
+
+ /**
+ * @return The drawable currently used to draw the progress bar
+ */
+ Drawable getCurrentDrawable() {
+ return mCurrentDrawable;
+ }
+
+ @Override
+ protected boolean verifyDrawable(Drawable who) {
+ return who == mProgressDrawable || who == mIndeterminateDrawable
+ || super.verifyDrawable(who);
+ }
+
+ @Override
+ public void postInvalidate() {
+ if (!mNoInvalidate) {
+ super.postInvalidate();
+ }
+ }
+
+ private class RefreshProgressRunnable implements Runnable {
+
+ private int mId;
+ private int mProgress;
+ private boolean mFromTouch;
+
+ RefreshProgressRunnable(int id, int progress, boolean fromTouch) {
+ mId = id;
+ mProgress = progress;
+ mFromTouch = fromTouch;
+ }
+
+ public void run() {
+ doRefreshProgress(mId, mProgress, mFromTouch);
+ // Put ourselves back in the cache when we are done
+ mRefreshProgressRunnable = this;
+ }
+
+ public void setup(int id, int progress, boolean fromTouch) {
+ mId = id;
+ mProgress = progress;
+ mFromTouch = fromTouch;
+ }
+
+ }
+
+ private synchronized void doRefreshProgress(int id, int progress, boolean fromTouch) {
+ float scale = mMax > 0 ? (float) progress / (float) mMax : 0;
+ final Drawable d = mCurrentDrawable;
+ if (d != null) {
+ Drawable progressDrawable = null;
+
+ if (d instanceof LayerDrawable) {
+ progressDrawable = ((LayerDrawable) d).findDrawableByLayerId(id);
+ }
+
+ final int level = (int) (scale * MAX_LEVEL);
+ (progressDrawable != null ? progressDrawable : d).setLevel(level);
+ } else {
+ invalidate();
+ }
+
+ if (id == R.id.progress) {
+ onProgressRefresh(scale, fromTouch);
+ }
+ }
+
+ void onProgressRefresh(float scale, boolean fromTouch) {
+ }
+
+ private synchronized void refreshProgress(int id, int progress, boolean fromTouch) {
+ if (mUiThreadId == Thread.currentThread().getId()) {
+ doRefreshProgress(id, progress, fromTouch);
+ } else {
+ RefreshProgressRunnable r;
+ if (mRefreshProgressRunnable != null) {
+ // Use cached RefreshProgressRunnable if available
+ r = mRefreshProgressRunnable;
+ // Uncache it
+ mRefreshProgressRunnable = null;
+ r.setup(id, progress, fromTouch);
+ } else {
+ // Make a new one
+ r = new RefreshProgressRunnable(id, progress, fromTouch);
+ }
+ post(r);
+ }
+ }
+
+ /**
+ * <p>Set the current progress to the specified value. Does not do anything
+ * if the progress bar is in indeterminate mode.</p>
+ *
+ * @param progress the new progress, between 0 and {@link #getMax()}
+ *
+ * @see #setIndeterminate(boolean)
+ * @see #isIndeterminate()
+ * @see #getProgress()
+ * @see #incrementProgressBy(int)
+ */
+ public synchronized void setProgress(int progress) {
+ setProgress(progress, false);
+ }
+
+ synchronized void setProgress(int progress, boolean fromTouch) {
+ if (mIndeterminate) {
+ return;
+ }
+
+ if (progress < 0) {
+ progress = 0;
+ }
+
+ if (progress > mMax) {
+ progress = mMax;
+ }
+
+ if (progress != mProgress) {
+ mProgress = progress;
+ refreshProgress(R.id.progress, mProgress, fromTouch);
+ }
+ }
+
+ /**
+ * <p>
+ * Set the current secondary progress to the specified value. Does not do
+ * anything if the progress bar is in indeterminate mode.
+ * </p>
+ *
+ * @param secondaryProgress the new secondary progress, between 0 and {@link #getMax()}
+ * @see #setIndeterminate(boolean)
+ * @see #isIndeterminate()
+ * @see #getSecondaryProgress()
+ * @see #incrementSecondaryProgressBy(int)
+ */
+ public synchronized void setSecondaryProgress(int secondaryProgress) {
+ if (mIndeterminate) {
+ return;
+ }
+
+ if (secondaryProgress < 0) {
+ secondaryProgress = 0;
+ }
+
+ if (secondaryProgress > mMax) {
+ secondaryProgress = mMax;
+ }
+
+ if (secondaryProgress != mSecondaryProgress) {
+ mSecondaryProgress = secondaryProgress;
+ refreshProgress(R.id.secondaryProgress, mSecondaryProgress, false);
+ }
+ }
+
+ /**
+ * <p>Get the progress bar's current level of progress. Return 0 when the
+ * progress bar is in indeterminate mode.</p>
+ *
+ * @return the current progress, between 0 and {@link #getMax()}
+ *
+ * @see #setIndeterminate(boolean)
+ * @see #isIndeterminate()
+ * @see #setProgress(int)
+ * @see #setMax(int)
+ * @see #getMax()
+ */
+ public synchronized int getProgress() {
+ return mIndeterminate ? 0 : mProgress;
+ }
+
+ /**
+ * <p>Get the progress bar's current level of secondary progress. Return 0 when the
+ * progress bar is in indeterminate mode.</p>
+ *
+ * @return the current secondary progress, between 0 and {@link #getMax()}
+ *
+ * @see #setIndeterminate(boolean)
+ * @see #isIndeterminate()
+ * @see #setSecondaryProgress(int)
+ * @see #setMax(int)
+ * @see #getMax()
+ */
+ public synchronized int getSecondaryProgress() {
+ return mIndeterminate ? 0 : mSecondaryProgress;
+ }
+
+ /**
+ * <p>Return the upper limit of this progress bar's range.</p>
+ *
+ * @return a positive integer
+ *
+ * @see #setMax(int)
+ * @see #getProgress()
+ * @see #getSecondaryProgress()
+ */
+ public synchronized int getMax() {
+ return mMax;
+ }
+
+ /**
+ * <p>Set the range of the progress bar to 0...<tt>max</tt>.</p>
+ *
+ * @param max the upper range of this progress bar
+ *
+ * @see #getMax()
+ * @see #setProgress(int)
+ * @see #setSecondaryProgress(int)
+ */
+ public synchronized void setMax(int max) {
+ if (max < 0) {
+ max = 0;
+ }
+ if (max != mMax) {
+ mMax = max;
+ postInvalidate();
+
+ if (mProgress > max) {
+ mProgress = max;
+ }
+ }
+ }
+
+ /**
+ * <p>Increase the progress bar's progress by the specified amount.</p>
+ *
+ * @param diff the amount by which the progress must be increased
+ *
+ * @see #setProgress(int)
+ */
+ public synchronized final void incrementProgressBy(int diff) {
+ setProgress(mProgress + diff);
+ }
+
+ /**
+ * <p>Increase the progress bar's secondary progress by the specified amount.</p>
+ *
+ * @param diff the amount by which the secondary progress must be increased
+ *
+ * @see #setSecondaryProgress(int)
+ */
+ public synchronized final void incrementSecondaryProgressBy(int diff) {
+ setSecondaryProgress(mSecondaryProgress + diff);
+ }
+
+ /**
+ * <p>Start the indeterminate progress animation.</p>
+ */
+ void startAnimation() {
+ int visibility = getVisibility();
+ if (visibility != VISIBLE) {
+ return;
+ }
+
+ if (mIndeterminateDrawable instanceof AnimationDrawable) {
+ mShouldStartAnimationDrawable = true;
+ mAnimation = null;
+ } else {
+ if (mInterpolator == null) {
+ mInterpolator = new LinearInterpolator();
+ }
+
+ mTransformation = new Transformation();
+ mAnimation = new AlphaAnimation(0.0f, 1.0f);
+ mAnimation.setRepeatMode(mBehavior);
+ mAnimation.setRepeatCount(Animation.INFINITE);
+ mAnimation.setDuration(mDuration);
+ mAnimation.setInterpolator(mInterpolator);
+ mAnimation.setStartTime(Animation.START_ON_FIRST_FRAME);
+ postInvalidate();
+ }
+ }
+
+ /**
+ * <p>Stop the indeterminate progress animation.</p>
+ */
+ void stopAnimation() {
+ mAnimation = null;
+ mTransformation = null;
+ if (mIndeterminateDrawable instanceof AnimationDrawable) {
+ ((AnimationDrawable)mIndeterminateDrawable).stop();
+ mShouldStartAnimationDrawable = false;
+ }
+ }
+
+ /**
+ * Sets the acceleration curve for the indeterminate animation.
+ * The interpolator is loaded as a resource from the specified context.
+ *
+ * @param context The application environment
+ * @param resID The resource identifier of the interpolator to load
+ */
+ public void setInterpolator(Context context, int resID) {
+ setInterpolator(AnimationUtils.loadInterpolator(context, resID));
+ }
+
+ /**
+ * Sets the acceleration curve for the indeterminate animation.
+ * Defaults to a linear interpolation.
+ *
+ * @param interpolator The interpolator which defines the acceleration curve
+ */
+ public void setInterpolator(Interpolator interpolator) {
+ mInterpolator = interpolator;
+ }
+
+ /**
+ * Gets the acceleration curve type for the indeterminate animation.
+ *
+ * @return the {@link Interpolator} associated to this animation
+ */
+ public Interpolator getInterpolator() {
+ return mInterpolator;
+ }
+
+ @Override
+ public void setVisibility(int v) {
+ if (getVisibility() != v) {
+ super.setVisibility(v);
+
+ if (mIndeterminate) {
+ // let's be nice with the UI thread
+ if (v == GONE || v == INVISIBLE) {
+ stopAnimation();
+ } else if (v == VISIBLE) {
+ startAnimation();
+ }
+ }
+ }
+ }
+
+ @Override
+ public void invalidateDrawable(Drawable dr) {
+ if (!mInDrawing) {
+ super.invalidateDrawable(dr);
+ }
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ Drawable d = mCurrentDrawable;
+ if (d != null) {
+ // onDraw will translate the canvas so we draw starting at 0,0
+ d.setBounds(0, 0, w - mPaddingRight - mPaddingLeft,
+ h - mPaddingBottom - mPaddingTop);
+ }
+ }
+
+ @Override
+ protected synchronized void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ Drawable d = mCurrentDrawable;
+ if (d != null) {
+ // Translate canvas so a indeterminate circular progress bar with padding
+ // rotates properly in its animation
+ canvas.save();
+ canvas.translate(mPaddingLeft, mPaddingTop);
+ long time = getDrawingTime();
+ if (mAnimation != null) {
+ mAnimation.getTransformation(time, mTransformation);
+ float scale = mTransformation.getAlpha();
+ try {
+ mInDrawing = true;
+ d.setLevel((int) (scale * MAX_LEVEL));
+ } finally {
+ mInDrawing = false;
+ }
+ if (SystemClock.uptimeMillis() - mLastDrawTime >= ANIMATION_RESOLUTION) {
+ mLastDrawTime = SystemClock.uptimeMillis();
+ postInvalidateDelayed(ANIMATION_RESOLUTION);
+ }
+ }
+ d.draw(canvas);
+ canvas.restore();
+ if (mShouldStartAnimationDrawable && mCurrentDrawable instanceof AnimationDrawable) {
+ ((AnimationDrawable)mCurrentDrawable).start();
+ }
+ }
+ }
+
+ @Override
+ protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ Drawable d = mCurrentDrawable;
+
+ int dw = 0;
+ int dh = 0;
+ if (d != null) {
+ dw = Math.max(mMinWidth, Math.min(mMaxWidth, d.getIntrinsicWidth()));
+ dh = Math.max(mMinHeight, Math.min(mMaxHeight, d.getIntrinsicHeight()));
+ }
+ dw += mPaddingLeft + mPaddingRight;
+ dh += mPaddingTop + mPaddingBottom;
+
+ setMeasuredDimension(resolveSize(dw, widthMeasureSpec),
+ resolveSize(dh, heightMeasureSpec));
+ }
+}
diff --git a/core/java/android/widget/RadioButton.java b/core/java/android/widget/RadioButton.java
new file mode 100644
index 0000000..14ec8c6
--- /dev/null
+++ b/core/java/android/widget/RadioButton.java
@@ -0,0 +1,72 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+
+/**
+ * <p>
+ * A radio button is a two-states button that can be either checked or
+ * unchecked. When the radio button is unchecked, the user can press or click it
+ * to check it. However, contrary to a {@link android.widget.CheckBox}, a radio
+ * button cannot be unchecked by the user once checked.
+ * </p>
+ *
+ * <p>
+ * Radio buttons are normally used together in a
+ * {@link android.widget.RadioGroup}. When several radio buttons live inside
+ * a radio group, checking one radio button unchecks all the others.</p>
+ * </p>
+ *
+ * <p><strong>XML attributes</strong></p>
+ * <p>
+ * See {@link android.R.styleable#CompoundButton CompoundButton Attributes},
+ * {@link android.R.styleable#Button Button Attributes},
+ * {@link android.R.styleable#TextView TextView Attributes},
+ * {@link android.R.styleable#View View Attributes}
+ * </p>
+ */
+public class RadioButton extends CompoundButton {
+
+ public RadioButton(Context context) {
+ this(context, null);
+ }
+
+ public RadioButton(Context context, AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.radioButtonStyle);
+ }
+
+ public RadioButton(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * If the radio button is already checked, this method will not toggle the radio button.
+ */
+ @Override
+ public void toggle() {
+ // we override to prevent toggle when the radio is already
+ // checked (as opposed to check boxes widgets)
+ if (!isChecked()) {
+ super.toggle();
+ }
+ }
+}
diff --git a/core/java/android/widget/RadioGroup.java b/core/java/android/widget/RadioGroup.java
new file mode 100644
index 0000000..ed8df22
--- /dev/null
+++ b/core/java/android/widget/RadioGroup.java
@@ -0,0 +1,371 @@
+/*
+ * 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.widget;
+
+import com.android.internal.R;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+
+
+/**
+ * <p>This class is used to create a multiple-exclusion scope for a set of radio
+ * buttons. Checking one radio button that belongs to a radio group unchecks
+ * any previously checked radio button within the same group.</p>
+ *
+ * <p>Intially, all of the radio buttons are unchecked. While it is not possible
+ * to uncheck a particular radio button, the radio group can be cleared to
+ * remove the checked state.</p>
+ *
+ * <p>The selection is identified by the unique id of the radio button as defined
+ * in the XML layout file.</p>
+ *
+ * <p><strong>XML Attributes</strong></p>
+ * <p>See {@link android.R.styleable#RadioGroup RadioGroup Attributes},
+ * {@link android.R.styleable#LinearLayout LinearLayout Attributes},
+ * {@link android.R.styleable#ViewGroup ViewGroup Attributes},
+ * {@link android.R.styleable#View View Attributes}</p>
+ * <p>Also see
+ * {@link android.widget.LinearLayout.LayoutParams LinearLayout.LayoutParams}
+ * for layout attributes.</p>
+ *
+ * @see RadioButton
+ *
+ */
+public class RadioGroup extends LinearLayout {
+ // holds the checked id; the selection is empty by default
+ private int mCheckedId = -1;
+ // tracks children radio buttons checked state
+ private CompoundButton.OnCheckedChangeListener mChildOnCheckedChangeListener;
+ // when true, mOnCheckedChangeListener discards events
+ private boolean mProtectFromCheckedChange = false;
+ private OnCheckedChangeListener mOnCheckedChangeListener;
+ private PassThroughHierarchyChangeListener mPassThroughListener;
+
+ /**
+ * {@inheritDoc}
+ */
+ public RadioGroup(Context context) {
+ super(context);
+ setOrientation(VERTICAL);
+ init();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public RadioGroup(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ // retrieve selected radio button as requested by the user in the
+ // XML layout file
+ TypedArray attributes = context.obtainStyledAttributes(
+ attrs, com.android.internal.R.styleable.RadioGroup, com.android.internal.R.attr.radioButtonStyle, 0);
+
+ int value = attributes.getResourceId(R.styleable.RadioGroup_checkedButton, View.NO_ID);
+ if (value != View.NO_ID) {
+ mCheckedId = value;
+ }
+
+ final int index = attributes.getInt(com.android.internal.R.styleable.RadioGroup_orientation, VERTICAL);
+ setOrientation(index);
+
+ attributes.recycle();
+ init();
+ }
+
+ private void init() {
+ mChildOnCheckedChangeListener = new CheckedStateTracker();
+ mPassThroughListener = new PassThroughHierarchyChangeListener();
+ super.setOnHierarchyChangeListener(mPassThroughListener);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setOnHierarchyChangeListener(OnHierarchyChangeListener listener) {
+ // the user listener is delegated to our pass-through listener
+ mPassThroughListener.mOnHierarchyChangeListener = listener;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ // checks the appropriate radio button as requested in the XML file
+ if (mCheckedId != -1) {
+ mProtectFromCheckedChange = true;
+ setCheckedStateForView(mCheckedId, true);
+ mProtectFromCheckedChange = false;
+ setCheckedId(mCheckedId);
+ }
+ }
+
+ /**
+ * <p>Sets the selection to the radio button whose identifier is passed in
+ * parameter. Using -1 as the selection identifier clears the selection;
+ * such an operation is equivalent to invoking {@link #clearCheck()}.</p>
+ *
+ * @param id the unique id of the radio button to select in this group
+ *
+ * @see #getCheckedRadioButtonId()
+ * @see #clearCheck()
+ */
+ public void check(int id) {
+ // don't even bother
+ if (id != -1 && (id == mCheckedId)) {
+ return;
+ }
+
+ if (mCheckedId != -1) {
+ setCheckedStateForView(mCheckedId, false);
+ }
+
+ if (id != -1) {
+ setCheckedStateForView(id, true);
+ }
+
+ setCheckedId(id);
+ }
+
+ private void setCheckedId(int id) {
+ mCheckedId = id;
+ if (mOnCheckedChangeListener != null) {
+ mOnCheckedChangeListener.onCheckedChanged(this, mCheckedId);
+ }
+ }
+
+ private void setCheckedStateForView(int viewId, boolean checked) {
+ View checkedView = findViewById(viewId);
+ if (checkedView != null && checkedView instanceof RadioButton) {
+ ((RadioButton) checkedView).setChecked(checked);
+ }
+ }
+
+ /**
+ * <p>Returns the identifier of the selected radio button in this group.
+ * Upon empty selection, the returned value is -1.</p>
+ *
+ * @return the unique id of the selected radio button in this group
+ *
+ * @see #check(int)
+ * @see #clearCheck()
+ */
+ public int getCheckedRadioButtonId() {
+ return mCheckedId;
+ }
+
+ /**
+ * <p>Clears the selection. When the selection is cleared, no radio button
+ * in this group is selected and {@link #getCheckedRadioButtonId()} returns
+ * null.</p>
+ *
+ * @see #check(int)
+ * @see #getCheckedRadioButtonId()
+ */
+ public void clearCheck() {
+ check(-1);
+ }
+
+ /**
+ * <p>Register a callback to be invoked when the checked radio button
+ * changes in this group.</p>
+ *
+ * @param listener the callback to call on checked state change
+ */
+ public void setOnCheckedChangeListener(OnCheckedChangeListener listener) {
+ mOnCheckedChangeListener = listener;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public LayoutParams generateLayoutParams(AttributeSet attrs) {
+ return new RadioGroup.LayoutParams(getContext(), attrs);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+ return p instanceof RadioGroup.LayoutParams;
+ }
+
+ @Override
+ protected LinearLayout.LayoutParams generateDefaultLayoutParams() {
+ return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+ }
+
+ /**
+ * <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
+ * XML file. Otherwise, this class ussed the value read from the XML file.</p>
+ *
+ * <p>See
+ * {@link android.R.styleable#LinearLayout_Layout LinearLayout Attributes}
+ * for a list of all child view attributes that this class supports.</p>
+ *
+ */
+ public static class LayoutParams extends LinearLayout.LayoutParams {
+ /**
+ * {@inheritDoc}
+ */
+ public LayoutParams(Context c, AttributeSet attrs) {
+ super(c, attrs);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public LayoutParams(int w, int h) {
+ super(w, h);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public LayoutParams(int w, int h, float initWeight) {
+ super(w, h, initWeight);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public LayoutParams(ViewGroup.LayoutParams p) {
+ super(p);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public LayoutParams(MarginLayoutParams source) {
+ super(source);
+ }
+
+ /**
+ * <p>Fixes the child's width to
+ * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} and the child's
+ * height to {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}
+ * when not specified in the XML file.</p>
+ *
+ * @param a the styled attributes set
+ * @param widthAttr the width attribute to fetch
+ * @param heightAttr the height attribute to fetch
+ */
+ @Override
+ protected void setBaseAttributes(TypedArray a,
+ int widthAttr, int heightAttr) {
+
+ if (a.hasValue(widthAttr)) {
+ width = a.getLayoutDimension(widthAttr, "layout_width");
+ } else {
+ width = WRAP_CONTENT;
+ }
+
+ if (a.hasValue(heightAttr)) {
+ height = a.getLayoutDimension(heightAttr, "layout_height");
+ } else {
+ height = WRAP_CONTENT;
+ }
+ }
+ }
+
+ /**
+ * <p>Interface definition for a callback to be invoked when the checked
+ * radio button changed in this group.</p>
+ */
+ public interface OnCheckedChangeListener {
+ /**
+ * <p>Called when the checked radio button has changed. When the
+ * selection is cleared, checkedId is -1.</p>
+ *
+ * @param group the group in which the checked radio button has changed
+ * @param checkedId the unique identifier of the newly checked radio button
+ */
+ public void onCheckedChanged(RadioGroup group, int checkedId);
+ }
+
+ private class CheckedStateTracker implements CompoundButton.OnCheckedChangeListener {
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ // prevents from infinite recursion
+ if (mProtectFromCheckedChange) {
+ return;
+ }
+
+ mProtectFromCheckedChange = true;
+ if (mCheckedId != -1) {
+ setCheckedStateForView(mCheckedId, false);
+ }
+ mProtectFromCheckedChange = false;
+
+ int id = buttonView.getId();
+ setCheckedId(id);
+ }
+ }
+
+ /**
+ * <p>A pass-through listener acts upon the events and dispatches them
+ * to another listener. This allows the table layout to set its own internal
+ * hierarchy change listener without preventing the user to setup his.</p>
+ */
+ private class PassThroughHierarchyChangeListener implements
+ ViewGroup.OnHierarchyChangeListener {
+ private ViewGroup.OnHierarchyChangeListener mOnHierarchyChangeListener;
+
+ /**
+ * {@inheritDoc}
+ */
+ public void onChildViewAdded(View parent, View child) {
+ if (parent == RadioGroup.this && child instanceof RadioButton) {
+ int id = child.getId();
+ // generates an id if it's missing
+ if (id == View.NO_ID) {
+ id = child.hashCode();
+ child.setId(id);
+ }
+ ((RadioButton) child).setOnCheckedChangeWidgetListener(
+ mChildOnCheckedChangeListener);
+ }
+
+ if (mOnHierarchyChangeListener != null) {
+ mOnHierarchyChangeListener.onChildViewAdded(parent, child);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void onChildViewRemoved(View parent, View child) {
+ if (parent == RadioGroup.this && child instanceof RadioButton) {
+ ((RadioButton) child).setOnCheckedChangeWidgetListener(null);
+ }
+
+ if (mOnHierarchyChangeListener != null) {
+ mOnHierarchyChangeListener.onChildViewRemoved(parent, child);
+ }
+ }
+ }
+}
diff --git a/core/java/android/widget/RatingBar.java b/core/java/android/widget/RatingBar.java
new file mode 100644
index 0000000..845b542
--- /dev/null
+++ b/core/java/android/widget/RatingBar.java
@@ -0,0 +1,311 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.shapes.RectShape;
+import android.graphics.drawable.shapes.Shape;
+import android.util.AttributeSet;
+
+import com.android.internal.R;
+
+/**
+ * A RatingBar is an extension of SeekBar and ProgressBar that shows a rating in
+ * stars. The user can touch and/or drag to set the rating when using the
+ * default size RatingBar. The smaller RatingBar style ({@link android.R.attr#ratingBarStyleSmall})
+ * and the larger indicator-only style ({@link android.R.attr#ratingBarStyleIndicator})
+ * do not support user interaction and should only be used as indicators.
+ * <p>
+ * The number of stars set (via {@link #setNumStars(int)} or in an XML layout)
+ * will be shown when the layout width is set to wrap content (if another layout
+ * width is set, the results may be unpredictable).
+ * <p>
+ * The secondary progress should not be modified by the client as it is used
+ * internally as the background for a fractionally filled star.
+ *
+ * @attr ref android.R.styleable#RatingBar_numStars
+ * @attr ref android.R.styleable#RatingBar_rating
+ * @attr ref android.R.styleable#RatingBar_stepSize
+ * @attr ref android.R.styleable#RatingBar_isIndicator
+ */
+public class RatingBar extends AbsSeekBar {
+
+ /**
+ * A callback that notifies clients when the rating has been changed. This
+ * includes changes that were initiated by the user through a touch gesture as well
+ * as changes that were initiated programmatically.
+ */
+ public interface OnRatingBarChangeListener {
+
+ /**
+ * Notification that the rating has changed. Clients can use the
+ * fromTouch parameter to distinguish user-initiated changes from those
+ * that occurred programmatically. This will not be called continuously
+ * while the user is dragging, only when the user finalizes a rating by
+ * lifting the touch.
+ *
+ * @param ratingBar The RatingBar whose rating has changed.
+ * @param rating The current rating. This will be in the range
+ * 0..numStars.
+ * @param fromTouch True if the rating change was initiated by a user's
+ * touch gesture.
+ */
+ void onRatingChanged(RatingBar ratingBar, float rating, boolean fromTouch);
+
+ }
+
+ private int mNumStars = 5;
+
+ private int mProgressOnStartTracking;
+
+ private OnRatingBarChangeListener mOnRatingBarChangeListener;
+
+ public RatingBar(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RatingBar,
+ defStyle, 0);
+ final int numStars = a.getInt(R.styleable.RatingBar_numStars, mNumStars);
+ setIsIndicator(a.getBoolean(R.styleable.RatingBar_isIndicator, !mIsUserSeekable));
+ final float rating = a.getFloat(R.styleable.RatingBar_rating, -1);
+ final float stepSize = a.getFloat(R.styleable.RatingBar_stepSize, -1);
+ a.recycle();
+
+ if (numStars > 0 && numStars != mNumStars) {
+ setNumStars(numStars);
+ }
+
+ if (stepSize >= 0) {
+ setStepSize(stepSize);
+ } else {
+ setStepSize(0.5f);
+ }
+
+ if (rating >= 0) {
+ setRating(rating);
+ }
+
+ // A touch inside a star fill up to that fractional area (slightly more
+ // than 1 so boundaries round up).
+ mTouchProgressOffset = 1.1f;
+ }
+
+ public RatingBar(Context context, AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.ratingBarStyle);
+ }
+
+ public RatingBar(Context context) {
+ this(context, null);
+ }
+
+ /**
+ * Sets the listener to be called when the rating changes.
+ *
+ * @param listener The listener.
+ */
+ public void setOnRatingBarChangeListener(OnRatingBarChangeListener listener) {
+ mOnRatingBarChangeListener = listener;
+ }
+
+ /**
+ * @return The listener (may be null) that is listening for rating change
+ * events.
+ */
+ public OnRatingBarChangeListener getOnRatingBarChangeListener() {
+ return mOnRatingBarChangeListener;
+ }
+
+ /**
+ * Whether this rating bar should only be an indicator (thus non-changeable
+ * by the user).
+ *
+ * @param isIndicator Whether it should be an indicator.
+ */
+ public void setIsIndicator(boolean isIndicator) {
+ mIsUserSeekable = !isIndicator;
+ }
+
+ /**
+ * @return Whether this rating bar is only an indicator.
+ */
+ public boolean isIndicator() {
+ return !mIsUserSeekable;
+ }
+
+ /**
+ * Sets the number of stars to show. In order for these to be shown
+ * properly, it is recommended the layout width of this widget be wrap
+ * content.
+ *
+ * @param numStars The number of stars.
+ */
+ public void setNumStars(final int numStars) {
+ if (numStars <= 0) {
+ return;
+ }
+
+ mNumStars = numStars;
+
+ // This causes the width to change, so re-layout
+ requestLayout();
+ }
+
+ /**
+ * Returns the number of stars shown.
+ * @return The number of stars shown.
+ */
+ public int getNumStars() {
+ return mNumStars;
+ }
+
+ /**
+ * Sets the rating (the number of stars filled).
+ *
+ * @param rating The rating to set.
+ */
+ public void setRating(float rating) {
+ setProgress((int) (rating * getProgressPerStar()));
+ }
+
+ /**
+ * Gets the current rating (number of stars filled).
+ *
+ * @return The current rating.
+ */
+ public float getRating() {
+ return getProgress() / getProgressPerStar();
+ }
+
+ /**
+ * Sets the step size (granularity) of this rating bar.
+ *
+ * @param stepSize The step size of this rating bar. For example, if
+ * half-star granularity is wanted, this would be 0.5.
+ */
+ public void setStepSize(float stepSize) {
+ if (stepSize <= 0) {
+ return;
+ }
+
+ final float newMax = mNumStars / stepSize;
+ final int newProgress = (int) (newMax / getMax() * getProgress());
+ setMax((int) newMax);
+ setProgress(newProgress);
+ }
+
+ /**
+ * Gets the step size of this rating bar.
+ *
+ * @return The step size.
+ */
+ public float getStepSize() {
+ return (float) getNumStars() / getMax();
+ }
+
+ /**
+ * @return The amount of progress that fits into a star
+ */
+ private float getProgressPerStar() {
+ if (mNumStars > 0) {
+ return 1f * getMax() / mNumStars;
+ } else {
+ return 1;
+ }
+ }
+
+ @Override
+ Shape getDrawableShape() {
+ // TODO: Once ProgressBar's TODOs are fixed, this won't be needed
+ return new RectShape();
+ }
+
+ @Override
+ void onProgressRefresh(float scale, boolean fromTouch) {
+ super.onProgressRefresh(scale, fromTouch);
+
+ // Keep secondary progress in sync with primary
+ updateSecondaryProgress(getProgress());
+
+ if (!fromTouch) {
+ // Callback for non-touch rating changes
+ dispatchRatingChange(false);
+ }
+ }
+
+ /**
+ * The secondary progress is used to differentiate the background of a
+ * partially filled star. This method keeps the secondary progress in sync
+ * with the progress.
+ *
+ * @param progress The primary progress level.
+ */
+ private void updateSecondaryProgress(int progress) {
+ final float ratio = getProgressPerStar();
+ if (ratio > 0) {
+ final float progressInStars = progress / ratio;
+ final int secondaryProgress = (int) (Math.ceil(progressInStars) * ratio);
+ setSecondaryProgress(secondaryProgress);
+ }
+ }
+
+ @Override
+ protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ if (mSampleTile != null) {
+ // TODO: Once ProgressBar's TODOs are gone, this can be done more
+ // cleanly than mSampleTile
+ final int width = mSampleTile.getWidth() * mNumStars;
+ setMeasuredDimension(resolveSize(width, widthMeasureSpec), mMeasuredHeight);
+ }
+ }
+
+ @Override
+ void onStartTrackingTouch() {
+ mProgressOnStartTracking = getProgress();
+
+ super.onStartTrackingTouch();
+ }
+
+ @Override
+ void onStopTrackingTouch() {
+ super.onStopTrackingTouch();
+
+ if (getProgress() != mProgressOnStartTracking) {
+ dispatchRatingChange(true);
+ }
+ }
+
+ void dispatchRatingChange(boolean fromTouch) {
+ if (mOnRatingBarChangeListener != null) {
+ mOnRatingBarChangeListener.onRatingChanged(this, getRating(),
+ fromTouch);
+ }
+ }
+
+ @Override
+ public synchronized void setMax(int max) {
+ // Disallow max progress = 0
+ if (max <= 0) {
+ return;
+ }
+
+ super.setMax(max);
+ }
+
+}
diff --git a/core/java/android/widget/RelativeLayout.java b/core/java/android/widget/RelativeLayout.java
new file mode 100644
index 0000000..91d5805
--- /dev/null
+++ b/core/java/android/widget/RelativeLayout.java
@@ -0,0 +1,950 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Gravity;
+import android.widget.RemoteViews.RemoteView;
+import android.graphics.Rect;
+import com.android.internal.R;
+
+
+/**
+ * A Layout where the positions of the children can be described in relation to each other or to the
+ * parent. For the sake of efficiency, the relations between views are evaluated in one pass, so if
+ * view Y is dependent on the position of view X, make sure the view X comes first in the layout.
+ *
+ * <p>
+ * Note that you cannot have a circular dependency between the size of the RelativeLayout and the
+ * position of its children. For example, you cannot have a RelativeLayout whose height is set to
+ * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT WRAP_CONTENT} and a child set to
+ * {@link #ALIGN_PARENT_BOTTOM}.
+ * </p>
+ *
+ * <p>
+ * Also see {@link android.widget.RelativeLayout.LayoutParams RelativeLayout.LayoutParams} for
+ * layout attributes
+ * </p>
+ *
+ * @attr ref android.R.styleable#RelativeLayout_gravity
+ * @attr ref android.R.styleable#RelativeLayout_ignoreGravity
+ */
+@RemoteView
+public class RelativeLayout extends ViewGroup {
+ public static final int TRUE = -1;
+
+ /**
+ * Rule that aligns a child's right edge with another child's left edge.
+ */
+ public static final int LEFT_OF = 0;
+ /**
+ * Rule that aligns a child's left edge with another child's right edge.
+ */
+ public static final int RIGHT_OF = 1;
+ /**
+ * Rule that aligns a child's bottom edge with another child's top edge.
+ */
+ public static final int ABOVE = 2;
+ /**
+ * Rule that aligns a child's top edge with another child's bottom edge.
+ */
+ public static final int BELOW = 3;
+
+ /**
+ * Rule that aligns a child's baseline with another child's baseline.
+ */
+ public static final int ALIGN_BASELINE = 4;
+ /**
+ * Rule that aligns a child's left edge with another child's left edge.
+ */
+ public static final int ALIGN_LEFT = 5;
+ /**
+ * Rule that aligns a child's top edge with another child's top edge.
+ */
+ public static final int ALIGN_TOP = 6;
+ /**
+ * Rule that aligns a child's right edge with another child's right edge.
+ */
+ public static final int ALIGN_RIGHT = 7;
+ /**
+ * Rule that aligns a child's bottom edge with another child's bottom edge.
+ */
+ public static final int ALIGN_BOTTOM = 8;
+
+ /**
+ * Rule that aligns the child's left edge with its RelativeLayout
+ * parent's left edge.
+ */
+ public static final int ALIGN_PARENT_LEFT = 9;
+ /**
+ * Rule that aligns the child's top edge with its RelativeLayout
+ * parent's top edge.
+ */
+ public static final int ALIGN_PARENT_TOP = 10;
+ /**
+ * Rule that aligns the child's right edge with its RelativeLayout
+ * parent's right edge.
+ */
+ public static final int ALIGN_PARENT_RIGHT = 11;
+ /**
+ * Rule that aligns the child's bottom edge with its RelativeLayout
+ * parent's bottom edge.
+ */
+ public static final int ALIGN_PARENT_BOTTOM = 12;
+
+ /**
+ * Rule that centers the child with respect to the bounds of its
+ * RelativeLayout parent.
+ */
+ public static final int CENTER_IN_PARENT = 13;
+ /**
+ * Rule that centers the child horizontally with respect to the
+ * bounds of its RelativeLayout parent.
+ */
+ public static final int CENTER_HORIZONTAL = 14;
+ /**
+ * Rule that centers the child vertically with respect to the
+ * bounds of its RelativeLayout parent.
+ */
+ public static final int CENTER_VERTICAL = 15;
+
+ private static final int VERB_COUNT = 16;
+
+ private View mBaselineView = null;
+ private boolean mHasBaselineAlignedChild;
+
+ private int mGravity = Gravity.LEFT | Gravity.TOP;
+ private final Rect mContentBounds = new Rect();
+ private final Rect mSelfBounds = new Rect();
+ private int mIgnoreGravity;
+
+ public RelativeLayout(Context context) {
+ super(context);
+ }
+
+ public RelativeLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initFromAttributes(context, attrs);
+ }
+
+ public RelativeLayout(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ initFromAttributes(context, attrs);
+ }
+
+ private void initFromAttributes(Context context, AttributeSet attrs) {
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RelativeLayout);
+ mIgnoreGravity = a.getResourceId(R.styleable.RelativeLayout_ignoreGravity, 0);
+ mGravity = a.getInt(R.styleable.RelativeLayout_gravity, mGravity);
+ a.recycle();
+ }
+
+ /**
+ * Defines which View is ignored when the gravity is applied. This setting has no
+ * effect if the gravity is <code>Gravity.LEFT | Gravity.TOP</code>.
+ *
+ * @param viewId The id of the View to be ignored by gravity, or 0 if no View
+ * should be ignored.
+ *
+ * @see #setGravity(int)
+ *
+ * @attr ref android.R.styleable#RelativeLayout_ignoreGravity
+ */
+ public void setIgnoreGravity(int viewId) {
+ mIgnoreGravity = viewId;
+ }
+
+ /**
+ * Describes how the child views are positioned. Defaults to
+ * <code>Gravity.LEFT | Gravity.TOP</code>.
+ *
+ * @param gravity See {@link android.view.Gravity}
+ *
+ * @see #setHorizontalGravity(int)
+ * @see #setVerticalGravity(int)
+ *
+ * @attr ref android.R.styleable#RelativeLayout_gravity
+ */
+ public void setGravity(int gravity) {
+ if (mGravity != gravity) {
+ if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == 0) {
+ gravity |= Gravity.LEFT;
+ }
+
+ if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == 0) {
+ gravity |= Gravity.TOP;
+ }
+
+ mGravity = gravity;
+ requestLayout();
+ }
+ }
+
+ public void setHorizontalGravity(int horizontalGravity) {
+ final int gravity = horizontalGravity & Gravity.HORIZONTAL_GRAVITY_MASK;
+ if ((mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != gravity) {
+ mGravity = (mGravity & ~Gravity.HORIZONTAL_GRAVITY_MASK) | gravity;
+ requestLayout();
+ }
+ }
+
+ public void setVerticalGravity(int verticalGravity) {
+ final int gravity = verticalGravity & Gravity.VERTICAL_GRAVITY_MASK;
+ if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != gravity) {
+ mGravity = (mGravity & ~Gravity.VERTICAL_GRAVITY_MASK) | gravity;
+ requestLayout();
+ }
+ }
+
+ @Override
+ public int getBaseline() {
+ return mBaselineView != null ? mBaselineView.getBaseline() : super.getBaseline();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int myWidth = -1;
+ int myHeight = -1;
+
+ int width = 0;
+ int height = 0;
+
+ int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+ int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+
+ // Record our dimensions if they are known;
+ if (widthMode != MeasureSpec.UNSPECIFIED) {
+ myWidth = widthSize;
+ }
+
+ if (heightMode != MeasureSpec.UNSPECIFIED) {
+ myHeight = heightSize;
+ }
+
+ if (widthMode == MeasureSpec.EXACTLY) {
+ width = myWidth;
+ }
+
+ if (heightMode == MeasureSpec.EXACTLY) {
+ height = myHeight;
+ }
+
+ int len = this.getChildCount();
+ mHasBaselineAlignedChild = false;
+
+ View ignore = null;
+ int gravity = mGravity & Gravity.HORIZONTAL_GRAVITY_MASK;
+ final boolean horizontalGravity = gravity != Gravity.LEFT && gravity != 0;
+ gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
+ final boolean verticalGravity = gravity != Gravity.TOP && gravity != 0;
+
+ int left = Integer.MAX_VALUE;
+ int top = Integer.MAX_VALUE;
+ int right = Integer.MIN_VALUE;
+ int bottom = Integer.MIN_VALUE;
+
+ if ((horizontalGravity || verticalGravity) && mIgnoreGravity != 0) {
+ ignore = findViewById(mIgnoreGravity);
+ }
+
+ for (int i = 0; i < len; i++) {
+ View child = getChildAt(i);
+ if (child.getVisibility() != GONE) {
+ LayoutParams params = (LayoutParams) child.getLayoutParams();
+ applySizeRules(params, myWidth, myHeight);
+ measureChild(child, params, myWidth, myHeight);
+ positionChild(child, params, myWidth, myHeight);
+
+ if (widthMode != MeasureSpec.EXACTLY) {
+ width = Math.max(width, params.mRight);
+ }
+ if (heightMode != MeasureSpec.EXACTLY) {
+ height = Math.max(height, params.mBottom);
+ }
+
+ if (child != ignore || verticalGravity) {
+ left = Math.min(left, params.mLeft - params.leftMargin);
+ top = Math.min(top, params.mTop - params.topMargin);
+ }
+
+ if (child != ignore || horizontalGravity) {
+ right = Math.max(right, params.mRight + params.rightMargin);
+ bottom = Math.max(bottom, params.mBottom + params.bottomMargin);
+ }
+ }
+ }
+
+ if (mHasBaselineAlignedChild) {
+ for (int i = 0; i < len; i++) {
+ View child = getChildAt(i);
+ if (child.getVisibility() != GONE) {
+ LayoutParams params = (LayoutParams) child.getLayoutParams();
+ alignBaseline(child, params);
+
+ if (child != ignore || verticalGravity) {
+ left = Math.min(left, params.mLeft - params.leftMargin);
+ top = Math.min(top, params.mTop - params.topMargin);
+ }
+
+ if (child != ignore || horizontalGravity) {
+ right = Math.max(right, params.mRight + params.rightMargin);
+ bottom = Math.max(bottom, params.mBottom + params.bottomMargin);
+ }
+ }
+ }
+ }
+
+ if (widthMode != MeasureSpec.EXACTLY) {
+ // Width already has left padding in it since it was calculated by looking at
+ // the right of each child view
+ width += mPaddingRight;
+
+ if (mLayoutParams.width >= 0) {
+ width = Math.max(width, mLayoutParams.width);
+ }
+
+ width = Math.max(width, getSuggestedMinimumWidth());
+ width = resolveSize(width, widthMeasureSpec);
+ }
+ if (heightMode != MeasureSpec.EXACTLY) {
+ // Height already has top padding in it since it was calculated by looking at
+ // the bottom of each child view
+ height += mPaddingBottom;
+
+ if (mLayoutParams.height >= 0) {
+ height = Math.max(height, mLayoutParams.height);
+ }
+
+ height = Math.max(height, getSuggestedMinimumHeight());
+ height = resolveSize(height, heightMeasureSpec);
+ }
+
+ if (horizontalGravity || verticalGravity) {
+ final Rect selfBounds = mSelfBounds;
+ selfBounds.set(mPaddingLeft, mPaddingTop, width - mPaddingRight,
+ height - mPaddingBottom);
+
+ final Rect contentBounds = mContentBounds;
+ Gravity.apply(mGravity, right - left, bottom - top, selfBounds, contentBounds);
+
+ final int horizontalOffset = contentBounds.left - left;
+ final int verticalOffset = contentBounds.top - top;
+ if (horizontalOffset != 0 || verticalOffset != 0) {
+ for (int i = 0; i < len; i++) {
+ View child = getChildAt(i);
+ if (child.getVisibility() != GONE && child != ignore) {
+ LayoutParams params = (LayoutParams) child.getLayoutParams();
+ params.mLeft += horizontalOffset;
+ params.mRight += horizontalOffset;
+ params.mTop += verticalOffset;
+ params.mBottom += verticalOffset;
+ }
+ }
+ }
+ }
+
+ setMeasuredDimension(width, height);
+ }
+
+ private void alignBaseline(View child, LayoutParams params) {
+ int[] rules = params.getRules();
+ int anchorBaseline = getRelatedViewBaseline(rules, ALIGN_BASELINE);
+
+ if (anchorBaseline != -1) {
+ LayoutParams anchorParams = getRelatedViewParams(rules, ALIGN_BASELINE);
+ if (anchorParams != null) {
+ int offset = anchorParams.mTop + anchorBaseline;
+ int baseline = child.getBaseline();
+ if (baseline != -1) {
+ offset -= baseline;
+ }
+ int height = params.mBottom - params.mTop;
+ params.mTop = offset;
+ params.mBottom = params.mTop + height;
+ }
+ }
+
+ if (mBaselineView == null) {
+ mBaselineView = child;
+ } else {
+ LayoutParams lp = (LayoutParams) mBaselineView.getLayoutParams();
+ if (params.mTop < lp.mTop || (params.mTop == lp.mTop && params.mLeft < lp.mLeft)) {
+ mBaselineView = child;
+ }
+ }
+ }
+
+ /**
+ * Measure a child. The child should have left, top, right and bottom information
+ * stored in its LayoutParams. If any of these values is -1 it means that the view
+ * can extend up to the corresponding edge.
+ *
+ * @param child Child to measure
+ * @param params LayoutParams associated with child
+ * @param myWidth Width of the the RelativeLayout
+ * @param myHeight Height of the RelativeLayout
+ */
+ private void measureChild(View child, LayoutParams params, int myWidth,
+ int myHeight) {
+
+ int childWidthMeasureSpec = getChildMeasureSpec(params.mLeft,
+ params.mRight, params.width,
+ params.leftMargin, params.rightMargin,
+ mPaddingLeft, mPaddingRight,
+ myWidth);
+ int childHeightMeasureSpec = getChildMeasureSpec(params.mTop,
+ params.mBottom, params.height,
+ params.topMargin, params.bottomMargin,
+ mPaddingTop, mPaddingBottom,
+ myHeight);
+ child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+ }
+
+ /**
+ * Get a measure spec that accounts for all of the constraints on this view.
+ * This includes size contstraints imposed by the RelativeLayout as well as
+ * the View's desired dimension.
+ *
+ * @param childStart The left or top field of the child's layout params
+ * @param childEnd The right or bottom field of the child's layout params
+ * @param childSize The child's desired size (the width or height field of
+ * the child's layout params)
+ * @param startMargin The left or top margin
+ * @param endMargin The right or bottom margin
+ * @param startPadding mPaddingLeft or mPaddingTop
+ * @param endPadding mPaddingRight or mPaddingBottom
+ * @param mySize The width or height of this view (the RelativeLayout)
+ * @return MeasureSpec for the child
+ */
+ private int getChildMeasureSpec(int childStart, int childEnd,
+ int childSize, int startMargin, int endMargin, int startPadding,
+ int endPadding, int mySize) {
+ int childSpecMode = 0;
+ int childSpecSize = 0;
+
+ // Figure out start and end bounds.
+ int tempStart = childStart;
+ int tempEnd = childEnd;
+
+ // If the view did not express a layout constraint for an edge, use
+ // view's margins and our padding
+ if (tempStart < 0) {
+ tempStart = startPadding + startMargin;
+ }
+ if (tempEnd < 0) {
+ tempEnd = mySize - endPadding - endMargin;
+ }
+
+ // Figure out maximum size available to this view
+ int maxAvailable = tempEnd - tempStart;
+
+ if (childStart >= 0 && childEnd >= 0) {
+ // Constraints fixed both edges, so child must be an exact size
+ childSpecMode = MeasureSpec.EXACTLY;
+ childSpecSize = maxAvailable;
+ } else {
+ if (childSize >= 0) {
+ // Child wanted an exact size. Give as much as possible
+ childSpecMode = MeasureSpec.EXACTLY;
+
+ if (maxAvailable >= 0) {
+ // We have a maxmum size in this dimension.
+ childSpecSize = Math.min(maxAvailable, childSize);
+ } else {
+ // We can grow in this dimension.
+ childSpecSize = childSize;
+ }
+ } else if (childSize == LayoutParams.FILL_PARENT) {
+ // Child wanted to be as big as possible. Give all availble
+ // space
+ childSpecMode = MeasureSpec.EXACTLY;
+ childSpecSize = maxAvailable;
+ } else if (childSize == LayoutParams.WRAP_CONTENT) {
+ // Child wants to wrap content. Use AT_MOST
+ // to communicate available space if we know
+ // our max size
+ if (maxAvailable >= 0) {
+ // We have a maxmum size in this dimension.
+ childSpecMode = MeasureSpec.AT_MOST;
+ childSpecSize = maxAvailable;
+ } else {
+ // We can grow in this dimension. Child can be as big as it
+ // wants
+ childSpecMode = MeasureSpec.UNSPECIFIED;
+ childSpecSize = 0;
+ }
+ }
+ }
+
+ return MeasureSpec.makeMeasureSpec(childSpecSize, childSpecMode);
+ }
+
+ /**
+ * After the child has been measured, assign it a position. Some views may
+ * already have final values for l,t,r,b. Others may have one or both edges
+ * unfixed (i.e. set to -1) in each dimension. These will get positioned
+ * based on which edge is fixed, the view's desired dimension, and whether
+ * or not it is centered.
+ *
+ * @param child Child to position
+ * @param params LayoutParams associated with child
+ * @param myWidth Width of the the RelativeLayout
+ * @param myHeight Height of the RelativeLayout
+ */
+ private void positionChild(View child, LayoutParams params, int myWidth, int myHeight) {
+ int[] rules = params.getRules();
+
+ if (params.mLeft < 0 && params.mRight >= 0) {
+ // Right is fixed, but left varies
+ params.mLeft = params.mRight - child.getMeasuredWidth();
+ } else if (params.mLeft >= 0 && params.mRight < 0) {
+ // Left is fixed, but right varies
+ params.mRight = params.mLeft + child.getMeasuredWidth();
+ } else if (params.mLeft < 0 && params.mRight < 0) {
+ // Both left and right vary
+ if (0 != rules[CENTER_IN_PARENT] || 0 != rules[CENTER_HORIZONTAL]) {
+ centerHorizontal(child, params, myWidth);
+ } else {
+ params.mLeft = mPaddingLeft + params.leftMargin;
+ params.mRight = params.mLeft + child.getMeasuredWidth();
+ }
+ }
+
+ if (params.mTop < 0 && params.mBottom >= 0) {
+ // Bottom is fixed, but top varies
+ params.mTop = params.mBottom - child.getMeasuredHeight();
+ } else if (params.mTop >= 0 && params.mBottom < 0) {
+ // Top is fixed, but bottom varies
+ params.mBottom = params.mTop + child.getMeasuredHeight();
+ } else if (params.mTop < 0 && params.mBottom < 0) {
+ // Both top and bottom vary
+ if (0 != rules[CENTER_IN_PARENT] || 0 != rules[CENTER_VERTICAL]) {
+ centerVertical(child, params, myHeight);
+ } else {
+ params.mTop = mPaddingTop + params.topMargin;
+ params.mBottom = params.mTop + child.getMeasuredHeight();
+ }
+ }
+ }
+
+ /**
+ * Set l,t,r,b values in the LayoutParams for one view based on its layout rules.
+ * Big assumption #1: All antecedents of this view have been sized & positioned
+ * Big assumption #2: The dimensions of the parent view (the RelativeLayout)
+ * are already known if they are needed.
+ *
+ * @param childParams LayoutParams for the view being positioned
+ * @param myWidth Width of the the RelativeLayout
+ * @param myHeight Height of the RelativeLayout
+ */
+ private void applySizeRules(LayoutParams childParams, int myWidth, int myHeight) {
+ int[] rules = childParams.getRules();
+ RelativeLayout.LayoutParams anchorParams;
+
+ // -1 indicated a "soft requirement" in that direction. For example:
+ // left=10, right=-1 means the view must start at 10, but can go as far as it wants to the right
+ // left =-1, right=10 means the view must end at 10, but can go as far as it wants to the left
+ // left=10, right=20 means the left and right ends are both fixed
+ childParams.mLeft = -1;
+ childParams.mRight = -1;
+
+ anchorParams = getRelatedViewParams(rules, LEFT_OF);
+ if (anchorParams != null) {
+ childParams.mRight = anchorParams.mLeft - (anchorParams.leftMargin +
+ childParams.rightMargin);
+ } else if (childParams.alignWithParent && rules[LEFT_OF] != 0) {
+ if (myWidth >= 0) {
+ childParams.mRight = myWidth - mPaddingRight - childParams.rightMargin;
+ } else {
+ // FIXME uh oh...
+ }
+ }
+
+ anchorParams = getRelatedViewParams(rules, RIGHT_OF);
+ if (anchorParams != null) {
+ childParams.mLeft = anchorParams.mRight + (anchorParams.rightMargin +
+ childParams.leftMargin);
+ } else if (childParams.alignWithParent && rules[RIGHT_OF] != 0) {
+ childParams.mLeft = mPaddingLeft + childParams.leftMargin;
+ }
+
+ anchorParams = getRelatedViewParams(rules, ALIGN_LEFT);
+ if (anchorParams != null) {
+ childParams.mLeft = anchorParams.mLeft + childParams.leftMargin;
+ } else if (childParams.alignWithParent && rules[ALIGN_LEFT] != 0) {
+ childParams.mLeft = mPaddingLeft + childParams.leftMargin;
+ }
+
+ anchorParams = getRelatedViewParams(rules, ALIGN_RIGHT);
+ if (anchorParams != null) {
+ childParams.mRight = anchorParams.mRight - childParams.rightMargin;
+ } else if (childParams.alignWithParent && rules[ALIGN_RIGHT] != 0) {
+ if (myWidth >= 0) {
+ childParams.mRight = myWidth - mPaddingRight - childParams.rightMargin;
+ } else {
+ // FIXME uh oh...
+ }
+ }
+
+ if (0 != rules[ALIGN_PARENT_LEFT]) {
+ childParams.mLeft = mPaddingLeft + childParams.leftMargin;
+ }
+
+ if (0 != rules[ALIGN_PARENT_RIGHT]) {
+ if (myWidth >= 0) {
+ childParams.mRight = myWidth - mPaddingRight - childParams.rightMargin;
+ } else {
+ // FIXME uh oh...
+ }
+ }
+
+ childParams.mTop = -1;
+ childParams.mBottom = -1;
+
+ anchorParams = getRelatedViewParams(rules, ABOVE);
+ if (anchorParams != null) {
+ childParams.mBottom = anchorParams.mTop - (anchorParams.topMargin +
+ childParams.bottomMargin);
+ } else if (childParams.alignWithParent && rules[ABOVE] != 0) {
+ if (myHeight >= 0) {
+ childParams.mBottom = myHeight - mPaddingBottom - childParams.bottomMargin;
+ } else {
+ // FIXME uh oh...
+ }
+ }
+
+ anchorParams = getRelatedViewParams(rules, BELOW);
+ if (anchorParams != null) {
+ childParams.mTop = anchorParams.mBottom + (anchorParams.bottomMargin +
+ childParams.topMargin);
+ } else if (childParams.alignWithParent && rules[BELOW] != 0) {
+ childParams.mTop = mPaddingTop + childParams.topMargin;
+ }
+
+ anchorParams = getRelatedViewParams(rules, ALIGN_TOP);
+ if (anchorParams != null) {
+ childParams.mTop = anchorParams.mTop + childParams.topMargin;
+ } else if (childParams.alignWithParent && rules[ALIGN_TOP] != 0) {
+ childParams.mTop = mPaddingTop + childParams.topMargin;
+ }
+
+ anchorParams = getRelatedViewParams(rules, ALIGN_BOTTOM);
+ if (anchorParams != null) {
+ childParams.mBottom = anchorParams.mBottom - childParams.bottomMargin;
+ } else if (childParams.alignWithParent && rules[ALIGN_BOTTOM] != 0) {
+ if (myHeight >= 0) {
+ childParams.mBottom = myHeight - mPaddingBottom - childParams.bottomMargin;
+ } else {
+ // FIXME uh oh...
+ }
+ }
+
+ if (0 != rules[ALIGN_PARENT_TOP]) {
+ childParams.mTop = mPaddingTop + childParams.topMargin;
+ }
+
+ if (0 != rules[ALIGN_PARENT_BOTTOM]) {
+ if (myHeight >= 0) {
+ childParams.mBottom = myHeight - mPaddingBottom - childParams.bottomMargin;
+ } else {
+ // FIXME uh oh...
+ }
+ }
+
+ if (rules[ALIGN_BASELINE] != 0) {
+ mHasBaselineAlignedChild = true;
+ }
+ }
+
+ private View getRelatedView(int[] rules, int relation) {
+ int id = rules[relation];
+ if (id != 0) {
+ View v = findViewById(id);
+ if (v == null) {
+ return null;
+ }
+
+ // Find the first non-GONE view up the chain
+ while (v.getVisibility() == View.GONE) {
+ rules = ((LayoutParams) v.getLayoutParams()).getRules();
+ v = v.findViewById(rules[relation]);
+ if (v == null) {
+ return null;
+ }
+ }
+
+ return v;
+ }
+
+ return null;
+ }
+
+ private LayoutParams getRelatedViewParams(int[] rules, int relation) {
+ View v = getRelatedView(rules, relation);
+ if (v != null) {
+ ViewGroup.LayoutParams params = v.getLayoutParams();
+ if (params instanceof LayoutParams) {
+ return (LayoutParams) v.getLayoutParams();
+ }
+ }
+ return null;
+ }
+
+ private int getRelatedViewBaseline(int[] rules, int relation) {
+ View v = getRelatedView(rules, relation);
+ if (v != null) {
+ return v.getBaseline();
+ }
+ return -1;
+ }
+
+ private void centerHorizontal(View child, LayoutParams params, int myWidth) {
+ int childWidth = child.getMeasuredWidth();
+ int left = (myWidth - childWidth) / 2;
+
+ params.mLeft = left;
+ params.mRight = left + childWidth;
+ }
+
+ private void centerVertical(View child, LayoutParams params, int myHeight) {
+ int childHeight = child.getMeasuredHeight();
+ int top = (myHeight - childHeight) / 2;
+
+ params.mTop = top;
+ params.mBottom = top + childHeight;
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ // The layout has actually already been performed and the positions
+ // cached. Apply the cached values to the children.
+ int count = getChildCount();
+
+ for (int i = 0; i < count; i++) {
+ View child = getChildAt(i);
+ if (child.getVisibility() != GONE) {
+ RelativeLayout.LayoutParams st =
+ (RelativeLayout.LayoutParams) child.getLayoutParams();
+ child.layout(st.mLeft, st.mTop, st.mRight, st.mBottom);
+
+ }
+ }
+ }
+
+ @Override
+ public LayoutParams generateLayoutParams(AttributeSet attrs) {
+ return new RelativeLayout.LayoutParams(getContext(), attrs);
+ }
+
+ /**
+ * Returns a set of layout parameters with a width of
+ * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT},
+ * a height of {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} and no spanning.
+ */
+ @Override
+ protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
+ return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+ }
+
+ // Override to allow type-checking of LayoutParams.
+ @Override
+ protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+ return p instanceof RelativeLayout.LayoutParams;
+ }
+
+ @Override
+ protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
+ return new LayoutParams(p);
+ }
+
+ /**
+ * Per-child layout information associated with RelativeLayout.
+ *
+ * @attr ref android.R.styleable#RelativeLayout_Layout_layout_alignWithParentIfMissing
+ * @attr ref android.R.styleable#RelativeLayout_Layout_layout_toLeftOf
+ * @attr ref android.R.styleable#RelativeLayout_Layout_layout_toRightOf
+ * @attr ref android.R.styleable#RelativeLayout_Layout_layout_above
+ * @attr ref android.R.styleable#RelativeLayout_Layout_layout_below
+ * @attr ref android.R.styleable#RelativeLayout_Layout_layout_alignBaseline
+ * @attr ref android.R.styleable#RelativeLayout_Layout_layout_alignLeft
+ * @attr ref android.R.styleable#RelativeLayout_Layout_layout_alignTop
+ * @attr ref android.R.styleable#RelativeLayout_Layout_layout_alignRight
+ * @attr ref android.R.styleable#RelativeLayout_Layout_layout_alignBottom
+ * @attr ref android.R.styleable#RelativeLayout_Layout_layout_alignParentLeft
+ * @attr ref android.R.styleable#RelativeLayout_Layout_layout_alignParentTop
+ * @attr ref android.R.styleable#RelativeLayout_Layout_layout_alignParentRight
+ * @attr ref android.R.styleable#RelativeLayout_Layout_layout_alignParentBottom
+ * @attr ref android.R.styleable#RelativeLayout_Layout_layout_centerInParent
+ * @attr ref android.R.styleable#RelativeLayout_Layout_layout_centerHorizontal
+ * @attr ref android.R.styleable#RelativeLayout_Layout_layout_centerVertical
+ */
+ public static class LayoutParams extends ViewGroup.MarginLayoutParams {
+ private int[] mRules = new int[VERB_COUNT];
+ private int mLeft, mTop, mRight, mBottom;
+
+ /**
+ * When true, uses the parent as the anchor if the anchor doesn't exist or if
+ * the anchor's visibility is GONE.
+ */
+ public boolean alignWithParent;
+
+ public LayoutParams(Context c, AttributeSet attrs) {
+ super(c, attrs);
+
+ TypedArray a = c.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.RelativeLayout_Layout);
+
+ final int[] rules = mRules;
+
+ final int N = a.getIndexCount();
+ for (int i = 0; i < N; i++) {
+ int attr = a.getIndex(i);
+ switch (attr) {
+ case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignWithParentIfMissing:
+ alignWithParent = a.getBoolean(attr, false);
+ break;
+ case com.android.internal.R.styleable.RelativeLayout_Layout_layout_toLeftOf:
+ rules[LEFT_OF] = a.getResourceId(attr, 0);
+ break;
+ case com.android.internal.R.styleable.RelativeLayout_Layout_layout_toRightOf:
+ rules[RIGHT_OF] = a.getResourceId(attr, 0);
+ break;
+ case com.android.internal.R.styleable.RelativeLayout_Layout_layout_above:
+ rules[ABOVE] = a.getResourceId(attr, 0);
+ break;
+ case com.android.internal.R.styleable.RelativeLayout_Layout_layout_below:
+ rules[BELOW] = a.getResourceId(attr, 0);
+ break;
+ case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignBaseline:
+ rules[ALIGN_BASELINE] = a.getResourceId(attr, 0);
+ break;
+ case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignLeft:
+ rules[ALIGN_LEFT] = a.getResourceId(attr, 0);
+ break;
+ case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignTop:
+ rules[ALIGN_TOP] = a.getResourceId(attr, 0);
+ break;
+ case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignRight:
+ rules[ALIGN_RIGHT] = a.getResourceId(attr, 0);
+ break;
+ case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignBottom:
+ rules[ALIGN_BOTTOM] = a.getResourceId(attr, 0);
+ break;
+ case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignParentLeft:
+ rules[ALIGN_PARENT_LEFT] = a.getBoolean(attr, false) ? TRUE : 0;
+ break;
+ case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignParentTop:
+ rules[ALIGN_PARENT_TOP] = a.getBoolean(attr, false) ? TRUE : 0;
+ break;
+ case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignParentRight:
+ rules[ALIGN_PARENT_RIGHT] = a.getBoolean(attr, false) ? TRUE : 0;
+ break;
+ case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignParentBottom:
+ rules[ALIGN_PARENT_BOTTOM] = a.getBoolean(attr, false) ? TRUE : 0;
+ break;
+ case com.android.internal.R.styleable.RelativeLayout_Layout_layout_centerInParent:
+ rules[CENTER_IN_PARENT] = a.getBoolean(attr, false) ? TRUE : 0;
+ break;
+ case com.android.internal.R.styleable.RelativeLayout_Layout_layout_centerHorizontal:
+ rules[CENTER_HORIZONTAL] = a.getBoolean(attr, false) ? TRUE : 0;
+ break;
+ case com.android.internal.R.styleable.RelativeLayout_Layout_layout_centerVertical:
+ rules[CENTER_VERTICAL] = a.getBoolean(attr, false) ? TRUE : 0;
+ break;
+ }
+ }
+
+ a.recycle();
+ }
+
+ public LayoutParams(int w, int h) {
+ super(w, h);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public LayoutParams(ViewGroup.LayoutParams source) {
+ super(source);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public LayoutParams(ViewGroup.MarginLayoutParams source) {
+ super(source);
+ }
+
+ @Override
+ public String debug(String output) {
+ return output + "ViewGroup.LayoutParams={ width=" + sizeToString(width) +
+ ", height=" + sizeToString(height) + " }";
+ }
+
+ /**
+ * Adds a layout rule to be interpreted by the RelativeLayout. This
+ * method should only be used for constraints that don't refer to another sibling
+ * (e.g., CENTER_IN_PARENT) or take a boolean value ({@link RelativeLayout#TRUE}
+ * for true or - for false). To specify a verb that takes a subject, use
+ * {@link #addRule(int, int)} instead.
+ *
+ * @param verb One of the verbs defined by
+ * {@link android.widget.RelativeLayout RelativeLayout}, such as
+ * ALIGN_WITH_PARENT_LEFT.
+ * @see #addRule(int, int)
+ */
+ public void addRule(int verb) {
+ mRules[verb] = TRUE;
+ }
+
+ /**
+ * Adds a layout rule to be interpreted by the RelativeLayout. Use this for
+ * verbs that take a target, such as a sibling (ALIGN_RIGHT) or a boolean
+ * value (VISIBLE).
+ *
+ * @param verb One of the verbs defined by
+ * {@link android.widget.RelativeLayout RelativeLayout}, such as
+ * ALIGN_WITH_PARENT_LEFT.
+ * @param anchor The id of another view to use as an anchor,
+ * or a boolean value(represented as {@link RelativeLayout#TRUE})
+ * for true or 0 for false). For verbs that don't refer to another sibling
+ * (for example, ALIGN_WITH_PARENT_BOTTOM) just use -1.
+ * @see #addRule(int)
+ */
+ public void addRule(int verb, int anchor) {
+ mRules[verb] = anchor;
+ }
+
+ /**
+ * Retrieves a complete list of all supported rules, where the index is the rule
+ * verb, and the element value is the value specified, or "false" if it was never
+ * set.
+ *
+ * @return the supported rules
+ * @see #addRule(int, int)
+ */
+ public int[] getRules() {
+ return mRules;
+ }
+ }
+}
diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java
new file mode 100644
index 0000000..54951b7
--- /dev/null
+++ b/core/java/android/widget/RemoteViews.java
@@ -0,0 +1,649 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import android.content.Context;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.LayoutInflater.Filter;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.ArrayList;
+
+
+/**
+ * A class that describes a view hierarchy that can be displayed in
+ * another process. The hierarchy is inflated from a layout resource
+ * file, and this class provides some basic operations for modifying
+ * the content of the inflated hierarchy.
+ */
+public class RemoteViews implements Parcelable, Filter {
+
+ private static final String LOG_TAG = "RemoteViews";
+
+ /**
+ * The package name of the package containing the layout
+ * resource. (Added to the parcel)
+ */
+ private String mPackage;
+
+ /**
+ * The resource ID of the layout file. (Added to the parcel)
+ */
+ private int mLayoutId;
+
+ /**
+ * The Context object used to inflate the layout file. Also may
+ * be used by actions if they need access to the senders resources.
+ */
+ private Context mContext;
+
+ /**
+ * An array of actions to perform on the view tree once it has been
+ * inflated
+ */
+ private ArrayList<Action> mActions;
+
+
+ /**
+ * This annotation indicates that a subclass of View is alllowed to be used with the
+ * {@link android.widget.RemoteViews} mechanism.
+ */
+ @Target({ ElementType.TYPE })
+ @Retention(RetentionPolicy.RUNTIME)
+ public @interface RemoteView {
+ }
+
+ /**
+ * Exception to send when something goes wrong executing an action
+ *
+ */
+ public static class ActionException extends RuntimeException {
+ public ActionException(String message) {
+ super(message);
+ }
+ }
+
+ /**
+ * Base class for all actions that can be performed on an
+ * inflated view.
+ *
+ */
+ private abstract static class Action implements Parcelable {
+ public abstract void apply(View root) throws ActionException;
+
+ public int describeContents() {
+ return 0;
+ }
+ };
+
+ /**
+ * Equivalent to calling View.setVisibility
+ */
+ private class SetViewVisibility extends Action {
+ public SetViewVisibility(int id, int vis) {
+ viewId = id;
+ visibility = vis;
+ }
+
+ public SetViewVisibility(Parcel parcel) {
+ viewId = parcel.readInt();
+ visibility = parcel.readInt();
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(TAG);
+ dest.writeInt(viewId);
+ dest.writeInt(visibility);
+ }
+
+ @Override
+ public void apply(View root) {
+ View target = root.findViewById(viewId);
+ if (target != null) {
+ target.setVisibility(visibility);
+ }
+ }
+
+ private int viewId;
+ private int visibility;
+ public final static int TAG = 0;
+ }
+
+ /**
+ * Equivalent to calling TextView.setText
+ */
+ private class SetTextViewText extends Action {
+ public SetTextViewText(int id, CharSequence t) {
+ viewId = id;
+ text = t;
+ }
+
+ public SetTextViewText(Parcel parcel) {
+ viewId = parcel.readInt();
+ text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel);
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(TAG);
+ dest.writeInt(viewId);
+ TextUtils.writeToParcel(text, dest, flags);
+ }
+
+ @Override
+ public void apply(View root) {
+ TextView target = (TextView) root.findViewById(viewId);
+ if (target != null) {
+ target.setText(text);
+ }
+ }
+
+ int viewId;
+ CharSequence text;
+ public final static int TAG = 1;
+ }
+
+ /**
+ * Equivalent to calling ImageView.setResource
+ */
+ private class SetImageViewResource extends Action {
+ public SetImageViewResource(int id, int src) {
+ viewId = id;
+ srcId = src;
+ }
+
+ public SetImageViewResource(Parcel parcel) {
+ viewId = parcel.readInt();
+ srcId = parcel.readInt();
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(TAG);
+ dest.writeInt(viewId);
+ dest.writeInt(srcId);
+ }
+
+ @Override
+ public void apply(View root) {
+ ImageView target = (ImageView) root.findViewById(viewId);
+ Drawable d = mContext.getResources().getDrawable(srcId);
+ if (target != null) {
+ target.setImageDrawable(d);
+ }
+ }
+
+ int viewId;
+ int srcId;
+ public final static int TAG = 2;
+ }
+
+ /**
+ * Equivalent to calling ImageView.setImageURI
+ */
+ private class SetImageViewUri extends Action {
+ public SetImageViewUri(int id, Uri u) {
+ viewId = id;
+ uri = u;
+ }
+
+ public SetImageViewUri(Parcel parcel) {
+ viewId = parcel.readInt();
+ uri = Uri.CREATOR.createFromParcel(parcel);
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(TAG);
+ dest.writeInt(viewId);
+ Uri.writeToParcel(dest, uri);
+ }
+
+ @Override
+ public void apply(View root) {
+ ImageView target = (ImageView) root.findViewById(viewId);
+ if (target != null) {
+ target.setImageURI(uri);
+ }
+ }
+
+ int viewId;
+ Uri uri;
+ public final static int TAG = 3;
+ }
+
+ /**
+ * Equivalent to calling ImageView.setImageBitmap
+ */
+ private class SetImageViewBitmap extends Action {
+ public SetImageViewBitmap(int id, Bitmap src) {
+ viewId = id;
+ bitmap = src;
+ }
+
+ public SetImageViewBitmap(Parcel parcel) {
+ viewId = parcel.readInt();
+ bitmap = Bitmap.CREATOR.createFromParcel(parcel);
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(TAG);
+ dest.writeInt(viewId);
+ if (bitmap != null) {
+ bitmap.writeToParcel(dest, flags);
+ }
+ }
+
+ @Override
+ public void apply(View root) {
+ if (bitmap != null) {
+ ImageView target = (ImageView) root.findViewById(viewId);
+ Drawable d = new BitmapDrawable(bitmap);
+ if (target != null) {
+ target.setImageDrawable(d);
+ }
+ }
+ }
+
+ int viewId;
+ Bitmap bitmap;
+ public final static int TAG = 4;
+ }
+
+ /**
+ * Equivalent to calling Chronometer.setBase, Chronometer.setFormat,
+ * and Chronometer.start/stop.
+ */
+ private class SetChronometer extends Action {
+ public SetChronometer(int id, long base, String format, boolean running) {
+ this.viewId = id;
+ this.base = base;
+ this.format = format;
+ this.running = running;
+ }
+
+ public SetChronometer(Parcel parcel) {
+ viewId = parcel.readInt();
+ base = parcel.readLong();
+ format = parcel.readString();
+ running = parcel.readInt() != 0;
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(TAG);
+ dest.writeInt(viewId);
+ dest.writeLong(base);
+ dest.writeString(format);
+ dest.writeInt(running ? 1 : 0);
+ }
+
+ @Override
+ public void apply(View root) {
+ Chronometer target = (Chronometer) root.findViewById(viewId);
+ if (target != null) {
+ target.setBase(base);
+ target.setFormat(format);
+ if (running) {
+ target.start();
+ } else {
+ target.stop();
+ }
+ }
+ }
+
+ int viewId;
+ boolean running;
+ long base;
+ String format;
+
+ public final static int TAG = 5;
+ }
+
+ /**
+ * Equivalent to calling ProgressBar.setMax, ProgressBar.setProgress and
+ * ProgressBar.setIndeterminate
+ */
+ private class SetProgressBar extends Action {
+ public SetProgressBar(int id, int max, int progress, boolean indeterminate) {
+ this.viewId = id;
+ this.progress = progress;
+ this.max = max;
+ this.indeterminate = indeterminate;
+ }
+
+ public SetProgressBar(Parcel parcel) {
+ viewId = parcel.readInt();
+ progress = parcel.readInt();
+ max = parcel.readInt();
+ indeterminate = parcel.readInt() != 0;
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(TAG);
+ dest.writeInt(viewId);
+ dest.writeInt(progress);
+ dest.writeInt(max);
+ dest.writeInt(indeterminate ? 1 : 0);
+ }
+
+ @Override
+ public void apply(View root) {
+ ProgressBar target = (ProgressBar) root.findViewById(viewId);
+ if (target != null) {
+ target.setIndeterminate(indeterminate);
+ if (!indeterminate) {
+ target.setMax(max);
+ target.setProgress(progress);
+ }
+ }
+ }
+
+ int viewId;
+ boolean indeterminate;
+ int progress;
+ int max;
+
+ public final static int TAG = 6;
+ }
+
+ /**
+ * Create a new RemoteViews object that will display the views contained
+ * in the specified layout file.
+ *
+ * @param packageName Name of the package that contains the layout resource
+ * @param layoutId The id of the layout resource
+ */
+ public RemoteViews(String packageName, int layoutId) {
+ mPackage = packageName;
+ mLayoutId = layoutId;
+ }
+
+ /**
+ * Reads a RemoteViews object from a parcel.
+ *
+ * @param parcel
+ */
+ public RemoteViews(Parcel parcel) {
+ mPackage = parcel.readString();
+ mLayoutId = parcel.readInt();
+ int count = parcel.readInt();
+ if (count > 0) {
+ mActions = new ArrayList<Action>(count);
+ for (int i=0; i<count; i++) {
+ int tag = parcel.readInt();
+ switch (tag) {
+ case SetViewVisibility.TAG:
+ mActions.add(new SetViewVisibility(parcel));
+ break;
+ case SetTextViewText.TAG:
+ mActions.add(new SetTextViewText(parcel));
+ break;
+ case SetImageViewResource.TAG:
+ mActions.add(new SetImageViewResource(parcel));
+ break;
+ case SetImageViewUri.TAG:
+ mActions.add(new SetImageViewUri(parcel));
+ break;
+ case SetImageViewBitmap.TAG:
+ mActions.add(new SetImageViewBitmap(parcel));
+ break;
+ case SetChronometer.TAG:
+ mActions.add(new SetChronometer(parcel));
+ break;
+ case SetProgressBar.TAG:
+ mActions.add(new SetProgressBar(parcel));
+ break;
+ default:
+ throw new ActionException("Tag " + tag + "not found");
+ }
+ }
+ }
+ }
+
+ public String getPackage() {
+ return mPackage;
+ }
+
+ public int getLayoutId() {
+ return mLayoutId;
+ }
+
+ /**
+ * Add an action to be executed on the remote side when apply is called.
+ *
+ * @param a The action to add
+ */
+ private void addAction(Action a) {
+ if (mActions == null) {
+ mActions = new ArrayList<Action>();
+ }
+ mActions.add(a);
+ }
+
+ /**
+ * Equivalent to calling View.setVisibility
+ *
+ * @param viewId The id of the view whose visibility should change
+ * @param visibility The new visibility for the view
+ */
+ public void setViewVisibility(int viewId, int visibility) {
+ addAction(new SetViewVisibility(viewId, visibility));
+ }
+
+ /**
+ * Equivalent to calling TextView.setText
+ *
+ * @param viewId The id of the view whose text should change
+ * @param text The new text for the view
+ */
+ public void setTextViewText(int viewId, CharSequence text) {
+ addAction(new SetTextViewText(viewId, text));
+ }
+
+ /**
+ * Equivalent to calling ImageView.setImageResource
+ *
+ * @param viewId The id of the view whose drawable should change
+ * @param srcId The new resource id for the drawable
+ */
+ public void setImageViewResource(int viewId, int srcId) {
+ addAction(new SetImageViewResource(viewId, srcId));
+ }
+
+ /**
+ * Equivalent to calling ImageView.setImageURI
+ *
+ * @param viewId The id of the view whose drawable should change
+ * @param uri The Uri for the image
+ */
+ public void setImageViewUri(int viewId, Uri uri) {
+ addAction(new SetImageViewUri(viewId, uri));
+ }
+
+ /**
+ * Equivalent to calling ImageView.setImageBitmap
+ *
+ * @param viewId The id of the view whose drawable should change
+ * @param bitmap The new Bitmap for the drawable
+ *
+ * @hide pending API Council approval to extend the public API
+ */
+ public void setImageViewBitmap(int viewId, Bitmap bitmap) {
+ addAction(new SetImageViewBitmap(viewId, bitmap));
+ }
+
+ /**
+ * Equivalent to calling {@link Chronometer#setBase Chronometer.setBase},
+ * {@link Chronometer#setFormat Chronometer.setFormat},
+ * and {@link Chronometer#start Chronometer.start()} or
+ * {@link Chronometer#stop Chronometer.stop()}.
+ *
+ * @param viewId The id of the view whose text should change
+ * @param base The time at which the timer would have read 0:00. This
+ * time should be based off of
+ * {@link android.os.SystemClock#elapsedRealtime SystemClock.elapsedRealtime()}.
+ * @param format The Chronometer format string, or null to
+ * simply display the timer value.
+ * @param running True if you want the clock to be running, false if not.
+ */
+ public void setChronometer(int viewId, long base, String format, boolean running) {
+ addAction(new SetChronometer(viewId, base, format, running));
+ }
+
+ /**
+ * Equivalent to calling {@link ProgressBar#setMax ProgressBar.setMax},
+ * {@link ProgressBar#setProgress ProgressBar.setProgress}, and
+ * {@link ProgressBar#setIndeterminate ProgressBar.setIndeterminate}
+ *
+ * @param viewId The id of the view whose text should change
+ * @param max The 100% value for the progress bar
+ * @param progress The current value of the progress bar.
+ * @param indeterminate True if the progress bar is indeterminate,
+ * false if not.
+ */
+ public void setProgressBar(int viewId, int max, int progress,
+ boolean indeterminate) {
+ addAction(new SetProgressBar(viewId, max, progress, indeterminate));
+ }
+
+ /**
+ * Inflates the view hierarchy represented by this object and applies
+ * all of the actions.
+ *
+ * <p><strong>Caller beware: this may throw</strong>
+ *
+ * @param context Default context to use
+ * @param parent Parent that the resulting view hierarchy will be attached to. This method
+ * does <strong>not</strong> attach the hierarchy. The caller should do so when appropriate.
+ * @return The inflated view hierarchy
+ */
+ public View apply(Context context, ViewGroup parent) {
+ View result = null;
+
+ Context c = prepareContext(context);
+
+ Resources r = c.getResources();
+ LayoutInflater inflater = (LayoutInflater) c
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ inflater = inflater.cloneInContext(c);
+ inflater.setFilter(this);
+
+ result = inflater.inflate(mLayoutId, parent, false);
+
+ performApply(result);
+
+ return result;
+ }
+
+ /**
+ * Applies all of the actions to the provided view.
+ *
+ * <p><strong>Caller beware: this may throw</strong>
+ *
+ * @param v The view to apply the actions to. This should be the result of
+ * the {@link #apply(Context,ViewGroup)} call.
+ */
+ public void reapply(Context context, View v) {
+ prepareContext(context);
+ performApply(v);
+ }
+
+ private void performApply(View v) {
+ if (mActions != null) {
+ final int count = mActions.size();
+ for (int i = 0; i < count; i++) {
+ Action a = mActions.get(i);
+ a.apply(v);
+ }
+ }
+ }
+
+ private Context prepareContext(Context context) {
+ Context c = null;
+ String packageName = mPackage;
+
+ if (packageName != null) {
+ try {
+ c = context.createPackageContext(packageName, 0);
+ } catch (NameNotFoundException e) {
+ Log.e(LOG_TAG, "Package name " + packageName + " not found");
+ c = context;
+ }
+ } else {
+ c = context;
+ }
+
+ mContext = c;
+
+ return c;
+ }
+
+ /* (non-Javadoc)
+ * Used to restrict the views which can be inflated
+ *
+ * @see android.view.LayoutInflater.Filter#onLoadClass(java.lang.Class)
+ */
+ public boolean onLoadClass(Class clazz) {
+ return clazz.isAnnotationPresent(RemoteView.class);
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mPackage);
+ dest.writeInt(mLayoutId);
+ int count;
+ if (mActions != null) {
+ count = mActions.size();
+ } else {
+ count = 0;
+ }
+ dest.writeInt(count);
+ for (int i=0; i<count; i++) {
+ Action a = mActions.get(i);
+ a.writeToParcel(dest, 0);
+ }
+ }
+
+ /**
+ * Parcelable.Creator that instantiates RemoteViews objects
+ */
+ public static final Parcelable.Creator<RemoteViews> CREATOR = new Parcelable.Creator<RemoteViews>() {
+ public RemoteViews createFromParcel(Parcel parcel) {
+ return new RemoteViews(parcel);
+ }
+
+ public RemoteViews[] newArray(int size) {
+ return new RemoteViews[size];
+ }
+ };
+}
diff --git a/core/java/android/widget/ResourceCursorAdapter.java b/core/java/android/widget/ResourceCursorAdapter.java
new file mode 100644
index 0000000..456d58d
--- /dev/null
+++ b/core/java/android/widget/ResourceCursorAdapter.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.LayoutInflater;
+
+
+/**
+ * An easy adapter that creates views defined in an XML file. You can specify
+ * the XML file that defines the appearance of the views.
+ */
+public abstract class ResourceCursorAdapter extends CursorAdapter {
+ private int mLayout;
+
+ private int mDropDownLayout;
+
+ private LayoutInflater mInflater;
+
+ /**
+ * Constructor.
+ *
+ * @param context The context where the ListView associated with this
+ * SimpleListItemFactory is running
+ * @param layout resource identifier of a layout file that defines the views
+ * for this list item.
+ */
+ public ResourceCursorAdapter(Context context, int layout, Cursor c) {
+ super(context, c);
+ mLayout = mDropDownLayout = layout;
+ mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ }
+
+ /**
+ * Inflates view(s) from the specified XML file.
+ *
+ * @see android.widget.CursorAdapter#newView(android.content.Context,
+ * android.database.Cursor, ViewGroup)
+ */
+ @Override
+ public View newView(Context context, Cursor cursor, ViewGroup parent) {
+ return mInflater.inflate(mLayout, parent, false);
+ }
+
+ @Override
+ public View newDropDownView(Context context, Cursor cursor, ViewGroup parent) {
+ return mInflater.inflate(mDropDownLayout, parent, false);
+ }
+
+ /**
+ * <p>Sets the layout resource of the drop down views.</p>
+ *
+ * @param dropDownLayout the layout resources used to create drop down views
+ */
+ public void setDropDownViewResource(int dropDownLayout) {
+ mDropDownLayout = dropDownLayout;
+ }
+}
diff --git a/core/java/android/widget/ResourceCursorTreeAdapter.java b/core/java/android/widget/ResourceCursorTreeAdapter.java
new file mode 100644
index 0000000..ddce515
--- /dev/null
+++ b/core/java/android/widget/ResourceCursorTreeAdapter.java
@@ -0,0 +1,109 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.LayoutInflater;
+
+/**
+ * A fairly simple ExpandableListAdapter that creates views defined in an XML
+ * file. You can specify the XML file that defines the appearance of the views.
+ */
+public abstract class ResourceCursorTreeAdapter extends CursorTreeAdapter {
+ private int mCollapsedGroupLayout;
+ private int mExpandedGroupLayout;
+ private int mChildLayout;
+ private int mLastChildLayout;
+ private LayoutInflater mInflater;
+
+ /**
+ * Constructor.
+ *
+ * @param context The context where the ListView associated with this
+ * SimpleListItemFactory is running
+ * @param cursor The database cursor
+ * @param collapsedGroupLayout resource identifier of a layout file that
+ * defines the views for collapsed groups.
+ * @param expandedGroupLayout resource identifier of a layout file that
+ * defines the views for expanded groups.
+ * @param childLayout resource identifier of a layout file that defines the
+ * views for all children but the last..
+ * @param lastChildLayout resource identifier of a layout file that defines
+ * the views for the last child of a group.
+ */
+ public ResourceCursorTreeAdapter(Context context, Cursor cursor, int collapsedGroupLayout,
+ int expandedGroupLayout, int childLayout, int lastChildLayout) {
+ super(cursor, context);
+
+ mCollapsedGroupLayout = collapsedGroupLayout;
+ mExpandedGroupLayout = expandedGroupLayout;
+ mChildLayout = childLayout;
+ mLastChildLayout = lastChildLayout;
+
+ mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param context The context where the ListView associated with this
+ * SimpleListItemFactory is running
+ * @param cursor The database cursor
+ * @param collapsedGroupLayout resource identifier of a layout file that
+ * defines the views for collapsed groups.
+ * @param expandedGroupLayout resource identifier of a layout file that
+ * defines the views for expanded groups.
+ * @param childLayout resource identifier of a layout file that defines the
+ * views for all children.
+ */
+ public ResourceCursorTreeAdapter(Context context, Cursor cursor, int collapsedGroupLayout,
+ int expandedGroupLayout, int childLayout) {
+ this(context, cursor, collapsedGroupLayout, expandedGroupLayout, childLayout, childLayout);
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param context The context where the ListView associated with this
+ * SimpleListItemFactory is running
+ * @param cursor The database cursor
+ * @param groupLayout resource identifier of a layout file that defines the
+ * views for all groups.
+ * @param childLayout resource identifier of a layout file that defines the
+ * views for all children.
+ */
+ public ResourceCursorTreeAdapter(Context context, Cursor cursor, int groupLayout,
+ int childLayout) {
+ this(context, cursor, groupLayout, groupLayout, childLayout, childLayout);
+ }
+
+ @Override
+ public View newChildView(Context context, Cursor cursor, boolean isLastChild,
+ ViewGroup parent) {
+ return mInflater.inflate((isLastChild) ? mLastChildLayout : mChildLayout, parent, false);
+ }
+
+ @Override
+ public View newGroupView(Context context, Cursor cursor, boolean isExpanded, ViewGroup parent) {
+ return mInflater.inflate((isExpanded) ? mExpandedGroupLayout : mCollapsedGroupLayout,
+ parent, false);
+ }
+
+}
diff --git a/core/java/android/widget/ScrollBarDrawable.java b/core/java/android/widget/ScrollBarDrawable.java
new file mode 100644
index 0000000..5df2b6d
--- /dev/null
+++ b/core/java/android/widget/ScrollBarDrawable.java
@@ -0,0 +1,248 @@
+/*
+ * 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.widget;
+
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+
+/**
+ * This is only used by View for displaying its scroll bars. It should probably
+ * be moved in to the view package since it is used in that lower-level layer.
+ * For now, we'll hide it so it can be cleaned up later.
+ * {@hide}
+ */
+public class ScrollBarDrawable extends Drawable {
+ private Drawable mVerticalTrack;
+ private Drawable mHorizontalTrack;
+ private Drawable mVerticalThumb;
+ private Drawable mHorizontalThumb;
+ private int mRange;
+ private int mOffset;
+ private int mExtent;
+ private boolean mVertical;
+ private boolean mChanged;
+ private boolean mRangeChanged;
+ private final Rect mTempBounds = new Rect();
+ private boolean mAlwaysDrawHorizontalTrack;
+ private boolean mAlwaysDrawVerticalTrack;
+
+ public ScrollBarDrawable() {
+ }
+
+ /**
+ * Indicate whether the horizontal scrollbar track should always be drawn regardless of the
+ * extent. Defaults to false.
+ *
+ * @param alwaysDrawTrack Set to true if the track should always be drawn
+ */
+ public void setAlwaysDrawHorizontalTrack(boolean alwaysDrawTrack) {
+ mAlwaysDrawHorizontalTrack = alwaysDrawTrack;
+ }
+
+ /**
+ * Indicate whether the vertical scrollbar track should always be drawn regardless of the
+ * extent. Defaults to false.
+ *
+ * @param alwaysDrawTrack Set to true if the track should always be drawn
+ */
+ public void setAlwaysDrawVerticalTrack(boolean alwaysDrawTrack) {
+ mAlwaysDrawVerticalTrack = alwaysDrawTrack;
+ }
+
+ /**
+ * Indicates whether the vertical scrollbar track should always be drawn regardless of the
+ * extent.
+ */
+ public boolean getAlwaysDrawVerticalTrack() {
+ return mAlwaysDrawVerticalTrack;
+ }
+
+ /**
+ * Indicates whether the horizontal scrollbar track should always be drawn regardless of the
+ * extent.
+ */
+ public boolean getAlwaysDrawHorizontalTrack() {
+ return mAlwaysDrawHorizontalTrack;
+ }
+
+ public void setParameters(int range, int offset, int extent, boolean vertical) {
+ if (mVertical != vertical) {
+ mChanged = true;
+ }
+
+ if (mRange != range || mOffset != offset || mExtent != extent) {
+ mRangeChanged = true;
+ }
+
+ mRange = range;
+ mOffset = offset;
+ mExtent = extent;
+ mVertical = vertical;
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ final boolean vertical = mVertical;
+ final int extent = mExtent;
+ final int range = mRange;
+
+ boolean drawTrack = true;
+ boolean drawThumb = true;
+ if (extent <= 0 || range <= extent) {
+ drawTrack = vertical ? mAlwaysDrawVerticalTrack : mAlwaysDrawHorizontalTrack;
+ drawThumb = false;
+ }
+
+ Rect r = getBounds();
+
+ if (drawTrack) {
+ drawTrack(canvas, r, vertical);
+ }
+
+ if (drawThumb) {
+ int size = vertical ? r.height() : r.width();
+ int thickness = vertical ? r.width() : r.height();
+ int length = Math.round((float) size * extent / range);
+ int offset = Math.round((float) (size - length) * mOffset / (range - extent));
+
+ // avoid the tiny thumb
+ int minLength = thickness * 2;
+ if (length < minLength) {
+ length = minLength;
+ }
+ // avoid the too-big thumb
+ if (offset + length > size) {
+ offset = size - length;
+ }
+
+ drawThumb(canvas, r, offset, length, vertical);
+ }
+ }
+
+ @Override
+ protected void onBoundsChange(Rect bounds) {
+ super.onBoundsChange(bounds);
+ mChanged = true;
+ }
+
+ protected void drawTrack(Canvas canvas, Rect bounds, boolean vertical) {
+ Drawable track;
+ if (vertical) {
+ track = mVerticalTrack;
+ } else {
+ track = mHorizontalTrack;
+ }
+ if (mChanged) {
+ track.setBounds(bounds);
+ }
+ track.draw(canvas);
+ }
+
+ protected void drawThumb(Canvas canvas, Rect bounds, int offset, int length, boolean vertical) {
+ final Rect thumbRect = mTempBounds;
+ final boolean changed = mRangeChanged || mChanged;
+ if (changed) {
+ if (vertical) {
+ thumbRect.set(bounds.left, bounds.top + offset,
+ bounds.right, bounds.top + offset + length);
+ } else {
+ thumbRect.set(bounds.left + offset, bounds.top,
+ bounds.left + offset + length, bounds.bottom);
+ }
+ }
+
+ if (vertical) {
+ final Drawable thumb = mVerticalThumb;
+ if (changed) thumb.setBounds(thumbRect);
+ thumb.draw(canvas);
+ } else {
+ final Drawable thumb = mHorizontalThumb;
+ if (changed) thumb.setBounds(thumbRect);
+ thumb.draw(canvas);
+ }
+ }
+
+ public void setVerticalThumbDrawable(Drawable thumb) {
+ if (thumb != null) {
+ mVerticalThumb = thumb;
+ }
+ }
+
+ public void setVerticalTrackDrawable(Drawable track) {
+ mVerticalTrack = track;
+ }
+
+ public void setHorizontalThumbDrawable(Drawable thumb) {
+ if (thumb != null) {
+ mHorizontalThumb = thumb;
+ }
+ }
+
+ public void setHorizontalTrackDrawable(Drawable track) {
+ mHorizontalTrack = track;
+ }
+
+ public int getSize(boolean vertical) {
+ if (vertical) {
+ return (mVerticalTrack != null ?
+ mVerticalTrack : mVerticalThumb).getIntrinsicWidth();
+ } else {
+ return (mHorizontalTrack != null ?
+ mHorizontalTrack : mHorizontalThumb).getIntrinsicHeight();
+ }
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ if (mVerticalTrack != null) {
+ mVerticalTrack.setAlpha(alpha);
+ }
+ mVerticalThumb.setAlpha(alpha);
+ if (mHorizontalTrack != null) {
+ mHorizontalTrack.setAlpha(alpha);
+ }
+ mHorizontalThumb.setAlpha(alpha);
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter cf) {
+ if (mVerticalTrack != null) {
+ mVerticalTrack.setColorFilter(cf);
+ }
+ mVerticalThumb.setColorFilter(cf);
+ if (mHorizontalTrack != null) {
+ mHorizontalTrack.setColorFilter(cf);
+ }
+ mHorizontalThumb.setColorFilter(cf);
+ }
+
+ @Override
+ public int getOpacity() {
+ return PixelFormat.TRANSLUCENT;
+ }
+
+ @Override
+ public String toString() {
+ return "ScrollBarDrawable: range=" + mRange + " offset=" + mOffset +
+ " extent=" + mExtent + (mVertical ? " V" : " H");
+ }
+}
+
+
diff --git a/core/java/android/widget/ScrollView.java b/core/java/android/widget/ScrollView.java
new file mode 100644
index 0000000..23a27ac
--- /dev/null
+++ b/core/java/android/widget/ScrollView.java
@@ -0,0 +1,1213 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.FocusFinder;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.animation.AnimationUtils;
+
+import com.android.internal.R;
+
+import java.util.List;
+
+/**
+ * Layout container for a view hierarchy that can be scrolled by the user,
+ * allowing it to be larger than the physical display. A ScrollView
+ * is a {@link FrameLayout}, meaning you should place one child in it
+ * containing the entire contents to scroll; this child may itself be a layout
+ * manager with a complex hierarchy of objects. A child that is often used
+ * is a {@link LinearLayout} in a vertical orientation, presenting a vertical
+ * array of top-level items that the user can scroll through.
+ *
+ * <p>You should never use a ScrollView with a {@link ListView}, since
+ * ListView takes care of its own scrolling. Most importantly, doing this
+ * defeats all of the important optimizations in ListView for dealing with
+ * large lists, since it effectively forces the ListView to display its entire
+ * list of items to fill up the infinite container supplied by ScrollView.
+ *
+ * <p>The {@link TextView} class also
+ * takes care of its own scrolling, so does not require a ScrollView, but
+ * using the two together is possible to achieve the effect of a text view
+ * within a larger container.
+ *
+ * <p>ScrollView only supports vertical scrolling.
+ */
+public class ScrollView extends FrameLayout {
+ private static final int ANIMATED_SCROLL_GAP = 250;
+
+ /**
+ * When arrow scrolling, ListView will never scroll more than this factor
+ * times the height of the list.
+ */
+ private static final float MAX_SCROLL_FACTOR = 0.5f;
+
+
+ private long mLastScroll;
+
+ private final Rect mTempRect = new Rect();
+ private Scroller mScroller;
+
+ /**
+ * Flag to indicate that we are moving focus ourselves. This is so the
+ * code that watches for focus changes initiated outside this ScrollView
+ * knows that it does not have to do anything.
+ */
+ private boolean mScrollViewMovedFocus;
+
+ /**
+ * Position of the last motion event.
+ */
+ private float mLastMotionY;
+
+ /**
+ * True when the layout has changed but the traversal has not come through yet.
+ * Ideally the view hierarchy would keep track of this for us.
+ */
+ private boolean mIsLayoutDirty = true;
+
+ /**
+ * The child to give focus to in the event that a child has requested focus while the
+ * layout is dirty. This prevents the scroll from being wrong if the child has not been
+ * laid out before requesting focus.
+ */
+ private View mChildToScrollTo = null;
+
+ /**
+ * True if the user is currently dragging this ScrollView around. This is
+ * not the same as 'is being flinged', which can be checked by
+ * mScroller.isFinished() (flinging begins when the user lifts his finger).
+ */
+ private boolean mIsBeingDragged = false;
+
+ /**
+ * Determines speed during touch scrolling
+ */
+ private VelocityTracker mVelocityTracker;
+
+ /**
+ * When set to true, the scroll view measure its child to make it fill the currently
+ * visible area.
+ */
+ private boolean mFillViewport;
+
+ /**
+ * Whether arrow scrolling is animated.
+ */
+ private boolean mSmoothScrollingEnabled = true;
+
+ public ScrollView(Context context) {
+ super(context);
+ initScrollView();
+
+ setVerticalScrollBarEnabled(true);
+ setVerticalFadingEdgeEnabled(true);
+
+ TypedArray a = context.obtainStyledAttributes(R.styleable.View);
+
+ initializeScrollbars(a);
+ initializeFadingEdge(a);
+
+ a.recycle();
+ }
+
+ public ScrollView(Context context, AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.scrollViewStyle);
+ }
+
+ public ScrollView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ initScrollView();
+
+ TypedArray a =
+ context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.ScrollView, defStyle, 0);
+
+ setFillViewport(a.getBoolean(R.styleable.ScrollView_fillViewport, false));
+
+ a.recycle();
+ }
+
+ @Override
+ protected float getTopFadingEdgeStrength() {
+ if (getChildCount() == 0) {
+ return 0.0f;
+ }
+
+ final int length = getVerticalFadingEdgeLength();
+ if (mScrollY < length) {
+ return mScrollY / (float) length;
+ }
+
+ return 1.0f;
+ }
+
+ @Override
+ protected float getBottomFadingEdgeStrength() {
+ if (getChildCount() == 0) {
+ return 0.0f;
+ }
+
+ final int length = getVerticalFadingEdgeLength();
+ final int bottom = getChildAt(0).getBottom();
+ final int span = bottom - mScrollY - getHeight();
+ if (span < length) {
+ return span / (float) length;
+ }
+
+ return 1.0f;
+ }
+
+ /**
+ * @return The maximum amount this scroll view will scroll in response to
+ * an arrow event.
+ */
+ public int getMaxScrollAmount() {
+ return (int) (MAX_SCROLL_FACTOR * (mBottom - mTop));
+ }
+
+
+ private void initScrollView() {
+ mScroller = new Scroller(getContext());
+ setFocusable(true);
+ setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
+ setWillNotDraw(false);
+ }
+
+ @Override
+ public void addView(View child) {
+ if (getChildCount() > 0) {
+ throw new IllegalStateException("ScrollView can host only one direct child");
+ }
+
+ super.addView(child);
+ }
+
+ @Override
+ public void addView(View child, int index) {
+ if (getChildCount() > 0) {
+ throw new IllegalStateException("ScrollView can host only one direct child");
+ }
+
+ super.addView(child, index);
+ }
+
+ @Override
+ public void addView(View child, ViewGroup.LayoutParams params) {
+ if (getChildCount() > 0) {
+ throw new IllegalStateException("ScrollView can host only one direct child");
+ }
+
+ super.addView(child, params);
+ }
+
+ @Override
+ public void addView(View child, int index, ViewGroup.LayoutParams params) {
+ if (getChildCount() > 0) {
+ throw new IllegalStateException("ScrollView can host only one direct child");
+ }
+
+ super.addView(child, index, params);
+ }
+
+ /**
+ * @return Returns true this ScrollView can be scrolled
+ */
+ private boolean canScroll() {
+ View child = getChildAt(0);
+ if (child != null) {
+ int childHeight = child.getHeight();
+ return getHeight() < childHeight + mPaddingTop + mPaddingBottom;
+ }
+ return false;
+ }
+
+ /**
+ * Indicates whether this ScrollView's content is stretched to fill the viewport.
+ *
+ * @return True if the content fills the viewport, false otherwise.
+ */
+ public boolean isFillViewport() {
+ return mFillViewport;
+ }
+
+ /**
+ * Indicates this ScrollView whether it should stretch its content height to fill
+ * the viewport or not.
+ *
+ * @param fillViewport True to stretch the content's height to the viewport's
+ * boundaries, false otherwise.
+ */
+ public void setFillViewport(boolean fillViewport) {
+ if (fillViewport != mFillViewport) {
+ mFillViewport = fillViewport;
+ requestLayout();
+ }
+ }
+
+ /**
+ * @return Whether arrow scrolling will animate its transition.
+ */
+ public boolean isSmoothScrollingEnabled() {
+ return mSmoothScrollingEnabled;
+ }
+
+ /**
+ * Set whether arrow scrolling will animate its transition.
+ * @param smoothScrollingEnabled whether arrow scrolling will animate its transition
+ */
+ public void setSmoothScrollingEnabled(boolean smoothScrollingEnabled) {
+ mSmoothScrollingEnabled = smoothScrollingEnabled;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ if (!mFillViewport) {
+ return;
+ }
+
+ final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ if (heightMode == MeasureSpec.UNSPECIFIED) {
+ return;
+ }
+
+ final View child = getChildAt(0);
+ int height = getMeasuredHeight();
+ if (child.getMeasuredHeight() < height) {
+ final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+ int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, mPaddingLeft
+ + mPaddingRight, lp.width);
+ height -= mPaddingTop;
+ height -= mPaddingBottom;
+ int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
+
+ child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+ }
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ // Let the focused view and/or our descendants get the key first
+ boolean handled = super.dispatchKeyEvent(event);
+ if (handled) {
+ return true;
+ }
+ return executeKeyEvent(event);
+ }
+
+ /**
+ * You can call this function yourself to have the scroll view perform
+ * scrolling from a key event, just as if the event had been dispatched to
+ * it by the view hierarchy.
+ *
+ * @param event The key event to execute.
+ * @return Return true if the event was handled, else false.
+ */
+ public boolean executeKeyEvent(KeyEvent event) {
+ mTempRect.setEmpty();
+
+ if (!canScroll()) {
+ if (isFocused()) {
+ View currentFocused = findFocus();
+ if (currentFocused == this) currentFocused = null;
+ View nextFocused = FocusFinder.getInstance().findNextFocus(this,
+ currentFocused, View.FOCUS_DOWN);
+ return nextFocused != null
+ && nextFocused != this
+ && nextFocused.requestFocus(View.FOCUS_DOWN);
+ }
+ return false;
+ }
+
+ boolean handled = false;
+ if (event.getAction() == KeyEvent.ACTION_DOWN) {
+ switch (event.getKeyCode()) {
+ case KeyEvent.KEYCODE_DPAD_UP:
+ if (!event.isAltPressed()) {
+ handled = arrowScroll(View.FOCUS_UP);
+ } else {
+ handled = fullScroll(View.FOCUS_UP);
+ }
+ break;
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ if (!event.isAltPressed()) {
+ handled = arrowScroll(View.FOCUS_DOWN);
+ } else {
+ handled = fullScroll(View.FOCUS_DOWN);
+ }
+ break;
+ case KeyEvent.KEYCODE_SPACE:
+ pageScroll(event.isShiftPressed() ? View.FOCUS_UP : View.FOCUS_DOWN);
+ break;
+ }
+ }
+
+ return handled;
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ /*
+ * This method JUST determines whether we want to intercept the motion.
+ * If we return true, onMotionEvent will be called and we do the actual
+ * scrolling there.
+ */
+
+ /*
+ * Shortcut the most recurring case: the user is in the dragging
+ * state and he is moving his finger. We want to intercept this
+ * motion.
+ */
+ final int action = ev.getAction();
+ if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
+ return true;
+ }
+
+ if (!canScroll()) {
+ mIsBeingDragged = false;
+ return false;
+ }
+
+ final float y = ev.getY();
+
+ switch (action) {
+ case MotionEvent.ACTION_MOVE:
+ /*
+ * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
+ * whether the user has moved far enough from his original down touch.
+ */
+
+ /*
+ * Locally do absolute value. mLastMotionY is set to the y value
+ * of the down event.
+ */
+ final int yDiff = (int) Math.abs(y - mLastMotionY);
+ if (yDiff > ViewConfiguration.getTouchSlop()) {
+ mIsBeingDragged = true;
+ }
+ break;
+
+ case MotionEvent.ACTION_DOWN:
+ /* Remember location of down touch */
+ mLastMotionY = y;
+
+ /*
+ * If being flinged and user touches the screen, initiate drag;
+ * otherwise don't. mScroller.isFinished should be false when
+ * being flinged.
+ */
+ mIsBeingDragged = !mScroller.isFinished();
+ break;
+
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ /* Release the drag */
+ mIsBeingDragged = false;
+ break;
+ }
+
+ /*
+ * The only time we want to intercept motion events is if we are in the
+ * drag mode.
+ */
+ return mIsBeingDragged;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+
+ if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) {
+ // Don't handle edge touches immediately -- they may actually belong to one of our
+ // descendants.
+ return false;
+ }
+
+ if (!canScroll()) {
+ return false;
+ }
+
+ if (mVelocityTracker == null) {
+ mVelocityTracker = VelocityTracker.obtain();
+ }
+ mVelocityTracker.addMovement(ev);
+
+ final int action = ev.getAction();
+ final float y = ev.getY();
+
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ /*
+ * If being flinged and user touches, stop the fling. isFinished
+ * will be false if being flinged.
+ */
+ if (!mScroller.isFinished()) {
+ mScroller.abortAnimation();
+ }
+
+ // Remember where the motion event started
+ mLastMotionY = y;
+ break;
+ case MotionEvent.ACTION_MOVE:
+ // Scroll to follow the motion event
+ final int deltaY = (int) (mLastMotionY - y);
+ mLastMotionY = y;
+
+ if (deltaY < 0) {
+ if (mScrollY > 0) {
+ scrollBy(0, deltaY);
+ }
+ } else if (deltaY > 0) {
+ final int bottomEdge = getHeight() - mPaddingBottom;
+ final int availableToScroll = getChildAt(0).getBottom() - mScrollY - bottomEdge;
+ if (availableToScroll > 0) {
+ scrollBy(0, Math.min(availableToScroll, deltaY));
+ }
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ final VelocityTracker velocityTracker = mVelocityTracker;
+ velocityTracker.computeCurrentVelocity(1000);
+ int initialVelocity = (int) velocityTracker.getYVelocity();
+
+ if ((Math.abs(initialVelocity) > ViewConfiguration.getMinimumFlingVelocity()) &&
+ (getChildCount() > 0)) {
+ fling(-initialVelocity);
+ }
+
+ if (mVelocityTracker != null) {
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * <p>
+ * Finds the next focusable component that fits in this View's bounds
+ * (excluding fading edges) pretending that this View's top is located at
+ * the parameter top.
+ * </p>
+ *
+ * @param topFocus look for a candidate is the one at the top of the bounds
+ * if topFocus is true, or at the bottom of the bounds if topFocus is
+ * false
+ * @param top the top offset of the bounds in which a focusable must be
+ * found (the fading edge is assumed to start at this position)
+ * @param preferredFocusable the View that has highest priority and will be
+ * returned if it is within my bounds (null is valid)
+ * @return the next focusable component in the bounds or null if none can be
+ * found
+ */
+ private View findFocusableViewInMyBounds(final boolean topFocus,
+ final int top, View preferredFocusable) {
+ /*
+ * The fading edge's transparent side should be considered for focus
+ * since it's mostly visible, so we divide the actual fading edge length
+ * by 2.
+ */
+ final int fadingEdgeLength = getVerticalFadingEdgeLength() / 2;
+ final int topWithoutFadingEdge = top + fadingEdgeLength;
+ final int bottomWithoutFadingEdge = top + getHeight() - fadingEdgeLength;
+
+ if ((preferredFocusable != null)
+ && (preferredFocusable.getTop() < bottomWithoutFadingEdge)
+ && (preferredFocusable.getBottom() > topWithoutFadingEdge)) {
+ return preferredFocusable;
+ }
+
+ return findFocusableViewInBounds(topFocus, topWithoutFadingEdge,
+ bottomWithoutFadingEdge);
+ }
+
+ /**
+ * <p>
+ * Finds the next focusable component that fits in the specified bounds.
+ * </p>
+ *
+ * @param topFocus look for a candidate is the one at the top of the bounds
+ * if topFocus is true, or at the bottom of the bounds if topFocus is
+ * false
+ * @param top the top offset of the bounds in which a focusable must be
+ * found
+ * @param bottom the bottom offset of the bounds in which a focusable must
+ * be found
+ * @return the next focusable component in the bounds or null if none can
+ * be found
+ */
+ private View findFocusableViewInBounds(boolean topFocus, int top, int bottom) {
+
+ List<View> focusables = getFocusables(View.FOCUS_FORWARD);
+ View focusCandidate = null;
+
+ /*
+ * A fully contained focusable is one where its top is below the bound's
+ * top, and its bottom is above the bound's bottom. A partially
+ * contained focusable is one where some part of it is within the
+ * bounds, but it also has some part that is not within bounds. A fully contained
+ * focusable is preferred to a partially contained focusable.
+ */
+ boolean foundFullyContainedFocusable = false;
+
+ int count = focusables.size();
+ for (int i = 0; i < count; i++) {
+ View view = focusables.get(i);
+ int viewTop = view.getTop();
+ int viewBottom = view.getBottom();
+
+ if (top < viewBottom && viewTop < bottom) {
+ /*
+ * the focusable is in the target area, it is a candidate for
+ * focusing
+ */
+
+ final boolean viewIsFullyContained = (top < viewTop) &&
+ (viewBottom < bottom);
+
+ if (focusCandidate == null) {
+ /* No candidate, take this one */
+ focusCandidate = view;
+ foundFullyContainedFocusable = viewIsFullyContained;
+ } else {
+ final boolean viewIsCloserToBoundary =
+ (topFocus && viewTop < focusCandidate.getTop()) ||
+ (!topFocus && viewBottom > focusCandidate
+ .getBottom());
+
+ if (foundFullyContainedFocusable) {
+ if (viewIsFullyContained && viewIsCloserToBoundary) {
+ /*
+ * We're dealing with only fully contained views, so
+ * it has to be closer to the boundary to beat our
+ * candidate
+ */
+ focusCandidate = view;
+ }
+ } else {
+ if (viewIsFullyContained) {
+ /* Any fully contained view beats a partially contained view */
+ focusCandidate = view;
+ foundFullyContainedFocusable = true;
+ } else if (viewIsCloserToBoundary) {
+ /*
+ * Partially contained view beats another partially
+ * contained view if it's closer
+ */
+ focusCandidate = view;
+ }
+ }
+ }
+ }
+ }
+
+ return focusCandidate;
+ }
+
+ /**
+ * <p>Handles scrolling in response to a "page up/down" shortcut press. This
+ * method will scroll the view by one page up or down and give the focus
+ * to the topmost/bottommost component in the new visible area. If no
+ * component is a good candidate for focus, this scrollview reclaims the
+ * focus.</p>
+ *
+ * @param direction the scroll direction: {@link android.view.View#FOCUS_UP}
+ * to go one page up or
+ * {@link android.view.View#FOCUS_DOWN} to go one page down
+ * @return true if the key event is consumed by this method, false otherwise
+ */
+ public boolean pageScroll(int direction) {
+ boolean down = direction == View.FOCUS_DOWN;
+ int height = getHeight();
+
+ if (down) {
+ mTempRect.top = getScrollY() + height;
+ int count = getChildCount();
+ if (count > 0) {
+ View view = getChildAt(count - 1);
+ if (mTempRect.top + height > view.getBottom()) {
+ mTempRect.top = view.getBottom() - height;
+ }
+ }
+ } else {
+ mTempRect.top = getScrollY() - height;
+ if (mTempRect.top < 0) {
+ mTempRect.top = 0;
+ }
+ }
+ mTempRect.bottom = mTempRect.top + height;
+
+ return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom);
+ }
+
+ /**
+ * <p>Handles scrolling in response to a "home/end" shortcut press. This
+ * method will scroll the view to the top or bottom and give the focus
+ * to the topmost/bottommost component in the new visible area. If no
+ * component is a good candidate for focus, this scrollview reclaims the
+ * focus.</p>
+ *
+ * @param direction the scroll direction: {@link android.view.View#FOCUS_UP}
+ * to go the top of the view or
+ * {@link android.view.View#FOCUS_DOWN} to go the bottom
+ * @return true if the key event is consumed by this method, false otherwise
+ */
+ public boolean fullScroll(int direction) {
+ boolean down = direction == View.FOCUS_DOWN;
+ int height = getHeight();
+
+ mTempRect.top = 0;
+ mTempRect.bottom = height;
+
+ if (down) {
+ int count = getChildCount();
+ if (count > 0) {
+ View view = getChildAt(count - 1);
+ mTempRect.bottom = view.getBottom();
+ mTempRect.top = mTempRect.bottom - height;
+ }
+ }
+
+ return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom);
+ }
+
+ /**
+ * <p>Scrolls the view to make the area defined by <code>top</code> and
+ * <code>bottom</code> visible. This method attempts to give the focus
+ * to a component visible in this area. If no component can be focused in
+ * the new visible area, the focus is reclaimed by this scrollview.</p>
+ *
+ * @param direction the scroll direction: {@link android.view.View#FOCUS_UP}
+ * to go upward
+ * {@link android.view.View#FOCUS_DOWN} to downward
+ * @param top the top offset of the new area to be made visible
+ * @param bottom the bottom offset of the new area to be made visible
+ * @return true if the key event is consumed by this method, false otherwise
+ */
+ private boolean scrollAndFocus(int direction, int top, int bottom) {
+ boolean handled = true;
+
+ int height = getHeight();
+ int containerTop = getScrollY();
+ int containerBottom = containerTop + height;
+ boolean up = direction == View.FOCUS_UP;
+
+ View newFocused = findFocusableViewInBounds(up, top, bottom);
+ if (newFocused == null) {
+ newFocused = this;
+ }
+
+ if (top >= containerTop && bottom <= containerBottom) {
+ handled = false;
+ } else {
+ int delta = up ? (top - containerTop) : (bottom - containerBottom);
+ doScrollY(delta);
+ }
+
+ if (newFocused != findFocus() && newFocused.requestFocus(direction)) {
+ mScrollViewMovedFocus = true;
+ mScrollViewMovedFocus = false;
+ }
+
+ return handled;
+ }
+
+ /**
+ * Handle scrolling in response to an up or down arrow click.
+ *
+ * @param direction The direction corresponding to the arrow key that was
+ * pressed
+ * @return True if we consumed the event, false otherwise
+ */
+ public boolean arrowScroll(int direction) {
+
+ View currentFocused = findFocus();
+ if (currentFocused == this) currentFocused = null;
+
+ View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction);
+
+ final int maxJump = getMaxScrollAmount();
+
+ if (nextFocused != null && isWithinDeltaOfScreen(nextFocused, maxJump)) {
+ nextFocused.getDrawingRect(mTempRect);
+ offsetDescendantRectToMyCoords(nextFocused, mTempRect);
+ int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
+ doScrollY(scrollDelta);
+ nextFocused.requestFocus(direction);
+ } else {
+ // no new focus
+ int scrollDelta = maxJump;
+
+ if (direction == View.FOCUS_UP && getScrollY() < scrollDelta) {
+ scrollDelta = getScrollY();
+ } else if (direction == View.FOCUS_DOWN) {
+
+ int daBottom = getChildAt(getChildCount() - 1).getBottom();
+
+ int screenBottom = getScrollY() + getHeight();
+
+ if (daBottom - screenBottom < maxJump) {
+ scrollDelta = daBottom - screenBottom;
+ }
+ }
+ if (scrollDelta == 0) {
+ return false;
+ }
+ doScrollY(direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta);
+ }
+
+ if (currentFocused != null && currentFocused.isFocused()
+ && isOffScreen(currentFocused)) {
+ // previously focused item still has focus and is off screen, give
+ // it up (take it back to ourselves)
+ // (also, need to temporarily force FOCUS_BEFORE_DESCENDANTS so we are
+ // sure to
+ // get it)
+ final int descendantFocusability = getDescendantFocusability(); // save
+ setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
+ requestFocus();
+ setDescendantFocusability(descendantFocusability); // restore
+ }
+ return true;
+ }
+
+ /**
+ * @return whether the descendant of this scroll view is scrolled off
+ * screen.
+ */
+ private boolean isOffScreen(View descendant) {
+ return !isWithinDeltaOfScreen(descendant, 0);
+ }
+
+ /**
+ * @return whether the descendant of this scroll view is within delta
+ * pixels of being on the screen.
+ */
+ private boolean isWithinDeltaOfScreen(View descendant, int delta) {
+ descendant.getDrawingRect(mTempRect);
+ offsetDescendantRectToMyCoords(descendant, mTempRect);
+
+ return (mTempRect.bottom + delta) >= getScrollY()
+ && (mTempRect.top - delta) <= (getScrollY() + getHeight());
+ }
+
+ /**
+ * Smooth scroll by a Y delta
+ *
+ * @param delta the number of pixels to scroll by on the X axis
+ */
+ private void doScrollY(int delta) {
+ if (delta != 0) {
+ if (mSmoothScrollingEnabled) {
+ smoothScrollBy(0, delta);
+ } else {
+ scrollBy(0, delta);
+ }
+ }
+ }
+
+ /**
+ * Like {@link View#scrollBy}, but scroll smoothly instead of immediately.
+ *
+ * @param dx the number of pixels to scroll by on the X axis
+ * @param dy the number of pixels to scroll by on the Y axis
+ */
+ public final void smoothScrollBy(int dx, int dy) {
+ long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
+ if (duration > ANIMATED_SCROLL_GAP) {
+ mScroller.startScroll(mScrollX, mScrollY, dx, dy);
+ invalidate();
+ } else {
+ if (!mScroller.isFinished()) {
+ mScroller.abortAnimation();
+ }
+ scrollBy(dx, dy);
+ }
+ mLastScroll = AnimationUtils.currentAnimationTimeMillis();
+ }
+
+ /**
+ * Like {@link #scrollTo}, but scroll smoothly instead of immediately.
+ *
+ * @param x the position where to scroll on the X axis
+ * @param y the position where to scroll on the Y axis
+ */
+ public final void smoothScrollTo(int x, int y) {
+ smoothScrollBy(x - mScrollX, y - mScrollY);
+ }
+
+ /**
+ * <p>The scroll range of a scroll view is the overall height of all of its
+ * children.</p>
+ */
+ @Override
+ protected int computeVerticalScrollRange() {
+ int count = getChildCount();
+ return count == 0 ? getHeight() : (getChildAt(0)).getBottom();
+ }
+
+
+ @Override
+ protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
+ ViewGroup.LayoutParams lp = child.getLayoutParams();
+
+ int childWidthMeasureSpec;
+ int childHeightMeasureSpec;
+
+ childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft
+ + mPaddingRight, lp.width);
+
+ childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+
+ child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+ }
+
+ @Override
+ protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
+ int parentHeightMeasureSpec, int heightUsed) {
+ final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
+
+ final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
+ mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ + widthUsed, lp.width);
+ final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
+ lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED);
+
+ child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+ }
+
+ @Override
+ public void computeScroll() {
+ if (mScroller.computeScrollOffset()) {
+ // This is called at drawing time by ViewGroup. We don't want to
+ // re-show the scrollbars at this point, which scrollTo will do,
+ // so we replicate most of scrollTo here.
+ //
+ // It's a little odd to call onScrollChanged from inside the drawing.
+ //
+ // It is, except when you remember that computeScroll() is used to
+ // animate scrolling. So unless we want to defer the onScrollChanged()
+ // until the end of the animated scrolling, we don't really have a
+ // choice here.
+ //
+ // I agree. The alternative, which I think would be worse, is to post
+ // something and tell the subclasses later. This is bad because there
+ // will be a window where mScrollX/Y is different from what the app
+ // thinks it is.
+ //
+ int oldX = mScrollX;
+ int oldY = mScrollY;
+ int x = mScroller.getCurrX();
+ int y = mScroller.getCurrY();
+ if (getChildCount() > 0) {
+ View child = getChildAt(0);
+ mScrollX = clamp(x, this.getWidth(), child.getWidth());
+ mScrollY = clamp(y, this.getHeight(), child.getHeight());
+ } else {
+ mScrollX = x;
+ mScrollY = y;
+ }
+ if (oldX != mScrollX || oldY != mScrollY) {
+ onScrollChanged(mScrollX, mScrollY, oldX, oldY);
+ postInvalidate(); // So we draw again
+ }
+ }
+ }
+
+ /**
+ * Scrolls the view to the given child.
+ *
+ * @param child the View to scroll to
+ */
+ private void scrollToChild(View child) {
+ child.getDrawingRect(mTempRect);
+
+ /* Offset from child's local coordinates to ScrollView coordinates */
+ offsetDescendantRectToMyCoords(child, mTempRect);
+
+ int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
+
+ if (scrollDelta != 0) {
+ scrollBy(0, scrollDelta);
+ }
+ }
+
+ /**
+ * If rect is off screen, scroll just enough to get it (or at least the
+ * first screen size chunk of it) on screen.
+ *
+ * @param rect The rectangle.
+ * @param immediate True to scroll immediately without animation
+ * @return true if scrolling was performed
+ */
+ private boolean scrollToChildRect(Rect rect, boolean immediate) {
+ final int delta = computeScrollDeltaToGetChildRectOnScreen(rect);
+ final boolean scroll = delta != 0;
+ if (scroll) {
+ if (immediate) {
+ scrollBy(0, delta);
+ } else {
+ smoothScrollBy(0, delta);
+ }
+ }
+ return scroll;
+ }
+
+ /**
+ * Compute the amount to scroll in the Y direction in order to get
+ * a rectangle completely on the screen (or, if taller than the screen,
+ * at least the first screen size chunk of it).
+ *
+ * @param rect The rect.
+ * @return The scroll delta.
+ */
+ protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect) {
+
+ int height = getHeight();
+ int screenTop = getScrollY();
+ int screenBottom = screenTop + height;
+
+ int fadingEdge = getVerticalFadingEdgeLength();
+
+ // leave room for top fading edge as long as rect isn't at very top
+ if (rect.top > 0) {
+ screenTop += fadingEdge;
+ }
+
+ // leave room for bottom fading edge as long as rect isn't at very bottom
+ if (rect.bottom < getChildAt(0).getHeight()) {
+ screenBottom -= fadingEdge;
+ }
+
+ int scrollYDelta = 0;
+
+ if (rect.bottom > screenBottom && rect.top > screenTop) {
+ // need to move down to get it in view: move down just enough so
+ // that the entire rectangle is in view (or at least the first
+ // screen size chunk).
+
+ if (rect.height() > height) {
+ // just enough to get screen size chunk on
+ scrollYDelta += (rect.top - screenTop);
+ } else {
+ // get entire rect at bottom of screen
+ scrollYDelta += (rect.bottom - screenBottom);
+ }
+
+ // make sure we aren't scrolling beyond the end of our content
+ int bottom = getChildAt(getChildCount() - 1).getBottom();
+ int distanceToBottom = bottom - screenBottom;
+ scrollYDelta = Math.min(scrollYDelta, distanceToBottom);
+
+ } else if (rect.top < screenTop && rect.bottom < screenBottom) {
+ // need to move up to get it in view: move up just enough so that
+ // entire rectangle is in view (or at least the first screen
+ // size chunk of it).
+
+ if (rect.height() > height) {
+ // screen size chunk
+ scrollYDelta -= (screenBottom - rect.bottom);
+ } else {
+ // entire rect at top
+ scrollYDelta -= (screenTop - rect.top);
+ }
+
+ // make sure we aren't scrolling any further than the top our content
+ scrollYDelta = Math.max(scrollYDelta, -getScrollY());
+ }
+ return scrollYDelta;
+ }
+
+ @Override
+ public void requestChildFocus(View child, View focused) {
+ if (!mScrollViewMovedFocus) {
+ if (!mIsLayoutDirty) {
+ scrollToChild(focused);
+ } else {
+ // The child may not be laid out yet, we can't compute the scroll yet
+ mChildToScrollTo = focused;
+ }
+ }
+ super.requestChildFocus(child, focused);
+ }
+
+
+ /**
+ * When looking for focus in children of a scroll view, need to be a little
+ * more careful not to give focus to something that is scrolled off screen.
+ *
+ * This is more expensive than the default {@link android.view.ViewGroup}
+ * implementation, otherwise this behavior might have been made the default.
+ */
+ @Override
+ protected boolean onRequestFocusInDescendants(int direction,
+ Rect previouslyFocusedRect) {
+
+ // convert from forward / backward notation to up / down / left / right
+ // (ugh).
+ if (direction == View.FOCUS_FORWARD) {
+ direction = View.FOCUS_DOWN;
+ } else if (direction == View.FOCUS_BACKWARD) {
+ direction = View.FOCUS_UP;
+ }
+
+ final View nextFocus = previouslyFocusedRect == null ?
+ FocusFinder.getInstance().findNextFocus(this, null, direction) :
+ FocusFinder.getInstance().findNextFocusFromRect(this,
+ previouslyFocusedRect, direction);
+
+ if (nextFocus == null) {
+ return false;
+ }
+
+ if (isOffScreen(nextFocus)) {
+ return false;
+ }
+
+ return nextFocus.requestFocus(direction, previouslyFocusedRect);
+ }
+
+ @Override
+ public boolean requestChildRectangleOnScreen(View child, Rect rectangle,
+ boolean immediate) {
+ // offset into coordinate space of this scroll view
+ rectangle.offset(child.getLeft() - child.getScrollX(),
+ child.getTop() - child.getScrollY());
+
+ // note: until bug 1137695 is fixed, disable smooth scrolling for this api
+ return scrollToChildRect(rectangle, true);//immediate);
+ }
+
+ @Override
+ public void requestLayout() {
+ mIsLayoutDirty = true;
+ super.requestLayout();
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ super.onLayout(changed, l, t, r, b);
+ mIsLayoutDirty = false;
+ // Give a child focus if it needs it
+ if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) {
+ scrollToChild(mChildToScrollTo);
+ }
+ mChildToScrollTo = null;
+
+ // Calling this with the present values causes it to re-clam them
+ scrollTo(mScrollX, mScrollY);
+ }
+
+ /**
+ * Return true if child is an descendant of parent, (or equal to the parent).
+ */
+ private boolean isViewDescendantOf(View child, View parent) {
+ if (child == parent) {
+ return true;
+ }
+
+ final ViewParent theParent = child.getParent();
+ return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent);
+ }
+
+ /**
+ * Fling the scroll view
+ *
+ * @param velocityY The initial velocity in the Y direction. Positive
+ * numbers mean that the finger/curor is moving down the screen,
+ * which means we want to scroll towards the top.
+ */
+ public void fling(int velocityY) {
+ int height = getHeight();
+ int bottom = getChildAt(getChildCount() - 1).getBottom();
+
+ mScroller.fling(mScrollX, mScrollY, 0, velocityY, 0, 0, 0, bottom - height);
+
+ final boolean movingDown = velocityY > 0;
+
+ View newFocused =
+ findFocusableViewInMyBounds(movingDown, mScroller.getFinalY(), findFocus());
+ if (newFocused == null) {
+ newFocused = this;
+ }
+
+ if (newFocused != findFocus()
+ && newFocused.requestFocus(movingDown ? View.FOCUS_DOWN : View.FOCUS_UP)) {
+ mScrollViewMovedFocus = true;
+ mScrollViewMovedFocus = false;
+ }
+
+ invalidate();
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>This version also clamps the scrolling to the bounds of our child.
+ */
+ public void scrollTo(int x, int y) {
+ // we rely on the fact the View.scrollBy calls scrollTo.
+ if (getChildCount() > 0) {
+ View child = getChildAt(0);
+ x = clamp(x, this.getWidth(), child.getWidth());
+ y = clamp(y, this.getHeight(), child.getHeight());
+ if (x != mScrollX || y != mScrollY) {
+ super.scrollTo(x, y);
+ }
+ }
+ }
+
+ private int clamp(int n, int my, int child) {
+ if (my >= child || n < 0) {
+ /* my >= child is this case:
+ * |--------------- me ---------------|
+ * |------ child ------|
+ * or
+ * |--------------- me ---------------|
+ * |------ child ------|
+ * or
+ * |--------------- me ---------------|
+ * |------ child ------|
+ *
+ * n < 0 is this case:
+ * |------ me ------|
+ * |-------- child --------|
+ * |-- mScrollX --|
+ */
+ return 0;
+ }
+ if ((my+n) > child) {
+ /* this case:
+ * |------ me ------|
+ * |------ child ------|
+ * |-- mScrollX --|
+ */
+ return child-my;
+ }
+ return n;
+ }
+}
diff --git a/core/java/android/widget/Scroller.java b/core/java/android/widget/Scroller.java
new file mode 100644
index 0000000..fbe5e57
--- /dev/null
+++ b/core/java/android/widget/Scroller.java
@@ -0,0 +1,368 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.view.ViewConfiguration;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+
+
+/**
+ * This class encapsulates scrolling. The duration of the scroll
+ * can be passed in the constructor and specifies the maximum time that
+ * the scrolling animation should take. Past this time, the scrolling is
+ * automatically moved to its final stage and computeScrollOffset()
+ * will always return false to indicate that scrolling is over.
+ */
+public class Scroller {
+ private int mMode;
+
+ private int mStartX;
+ private int mStartY;
+ private int mFinalX;
+ private int mFinalY;
+
+ private int mMinX;
+ private int mMaxX;
+ private int mMinY;
+ private int mMaxY;
+
+ private int mCurrX;
+ private int mCurrY;
+ private long mStartTime;
+ private int mDuration;
+ private float mDurationReciprocal;
+ private float mDeltaX;
+ private float mDeltaY;
+ private float mViscousFluidScale;
+ private float mViscousFluidNormalize;
+ private boolean mFinished;
+ private Interpolator mInterpolator;
+
+ private float mCoeffX = 0.0f;
+ private float mCoeffY = 1.0f;
+ private float mVelocity;
+
+ private static final int DEFAULT_DURATION = 250;
+ private static final int SCROLL_MODE = 0;
+ private static final int FLING_MODE = 1;
+
+ private final float mDeceleration;
+
+ /**
+ * Create a Scroller with the default duration and interpolator.
+ */
+ public Scroller(Context context) {
+ this(context, null);
+ }
+
+ /**
+ * Create a Scroller with the specified interpolator. If the interpolator is
+ * null, the default (viscous) interpolator will be used.
+ */
+ public Scroller(Context context, Interpolator interpolator) {
+ mFinished = true;
+ mInterpolator = interpolator;
+ float ppi = context.getResources().getDisplayMetrics().density * 160.0f;
+ mDeceleration = 9.8f // g (m/s^2)
+ * 39.37f // inch/meter
+ * ppi // pixels per inch
+ * ViewConfiguration.getScrollFriction();
+ }
+
+ /**
+ *
+ * Returns whether the scroller has finished scrolling.
+ *
+ * @return True if the scroller has finished scrolling, false otherwise.
+ */
+ public final boolean isFinished() {
+ return mFinished;
+ }
+
+ /**
+ * Force the finished field to a particular value.
+ *
+ * @param finished The new finished value.
+ */
+ public final void forceFinished(boolean finished) {
+ mFinished = finished;
+ }
+
+ /**
+ * Returns how long the scroll event will take, in milliseconds.
+ *
+ * @return The duration of the scroll in milliseconds.
+ */
+ public final int getDuration() {
+ return mDuration;
+ }
+
+ /**
+ * Returns the current X offset in the scroll.
+ *
+ * @return The new X offset as an absolute distance from the origin.
+ */
+ public final int getCurrX() {
+ return mCurrX;
+ }
+
+ /**
+ * Returns the current Y offset in the scroll.
+ *
+ * @return The new Y offset as an absolute distance from the origin.
+ */
+ public final int getCurrY() {
+ return mCurrY;
+ }
+
+ /**
+ * Returns where the scroll will end. Valid only for "fling" scrolls.
+ *
+ * @return The final X offset as an absolute distance from the origin.
+ */
+ public final int getFinalX() {
+ return mFinalX;
+ }
+
+ /**
+ * Returns where the scroll will end. Valid only for "fling" scrolls.
+ *
+ * @return The final Y offset as an absolute distance from the origin.
+ */
+ public final int getFinalY() {
+ return mFinalY;
+ }
+
+ /**
+ * Call this when you want to know the new location. If it returns true,
+ * the animation is not yet finished. loc will be altered to provide the
+ * new location.
+ */
+ public boolean computeScrollOffset() {
+ if (mFinished) {
+ return false;
+ }
+
+ int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
+
+ if (timePassed < mDuration) {
+ switch (mMode) {
+ case SCROLL_MODE:
+ float x = (float)timePassed * mDurationReciprocal;
+
+ if (mInterpolator == null)
+ x = viscousFluid(x);
+ else
+ x = mInterpolator.getInterpolation(x);
+
+ mCurrX = mStartX + Math.round(x * mDeltaX);
+ mCurrY = mStartY + Math.round(x * mDeltaY);
+ if ((mCurrX == mFinalX) && (mCurrY == mFinalY)) {
+ mFinished = true;
+ }
+ break;
+ case FLING_MODE:
+ float timePassedSeconds = timePassed / 1000.0f;
+ float distance = (mVelocity * timePassedSeconds)
+ - (mDeceleration * timePassedSeconds * timePassedSeconds / 2.0f);
+
+ mCurrX = mStartX + Math.round(distance * mCoeffX);
+ // Pin to mMinX <= mCurrX <= mMaxX
+ mCurrX = Math.min(mCurrX, mMaxX);
+ mCurrX = Math.max(mCurrX, mMinX);
+
+ mCurrY = mStartY + Math.round(distance * mCoeffY);
+ // Pin to mMinY <= mCurrY <= mMaxY
+ mCurrY = Math.min(mCurrY, mMaxY);
+ mCurrY = Math.max(mCurrY, mMinY);
+
+ if (mCurrX == mFinalX && mCurrY == mFinalY) {
+ mFinished = true;
+ }
+
+ break;
+ }
+ }
+ else {
+ mCurrX = mFinalX;
+ mCurrY = mFinalY;
+ mFinished = true;
+ }
+ return true;
+ }
+
+ /**
+ * Start scrolling by providing a starting point and the distance to travel.
+ * The scroll will use the default value of 250 milliseconds for the
+ * duration.
+ *
+ * @param startX Starting horizontal scroll offset in pixels. Positive
+ * numbers will scroll the content to the left.
+ * @param startY Starting vertical scroll offset in pixels. Positive numbers
+ * will scroll the content up.
+ * @param dx Horizontal distance to travel. Positive numbers will scroll the
+ * content to the left.
+ * @param dy Vertical distance to travel. Positive numbers will scroll the
+ * content up.
+ */
+ public void startScroll(int startX, int startY, int dx, int dy) {
+ startScroll(startX, startY, dx, dy, DEFAULT_DURATION);
+ }
+
+ /**
+ * Start scrolling by providing a starting point and the distance to travel.
+ *
+ * @param startX Starting horizontal scroll offset in pixels. Positive
+ * numbers will scroll the content to the left.
+ * @param startY Starting vertical scroll offset in pixels. Positive numbers
+ * will scroll the content up.
+ * @param dx Horizontal distance to travel. Positive numbers will scroll the
+ * content to the left.
+ * @param dy Vertical distance to travel. Positive numbers will scroll the
+ * content up.
+ * @param duration Duration of the scroll in milliseconds.
+ */
+ public void startScroll(int startX, int startY, int dx, int dy, int duration) {
+ mMode = SCROLL_MODE;
+ mFinished = false;
+ mDuration = duration;
+ mStartTime = AnimationUtils.currentAnimationTimeMillis();
+ mStartX = startX;
+ mStartY = startY;
+ mFinalX = startX + dx;
+ mFinalY = startY + dy;
+ mDeltaX = dx;
+ mDeltaY = dy;
+ mDurationReciprocal = 1.0f / (float) mDuration;
+ // This controls the viscous fluid effect (how much of it)
+ mViscousFluidScale = 8.0f;
+ // must be set to 1.0 (used in viscousFluid())
+ mViscousFluidNormalize = 1.0f;
+ mViscousFluidNormalize = 1.0f / viscousFluid(1.0f);
+ }
+
+ /**
+ * Start scrolling based on a fling gesture. The distance travelled will
+ * depend on the initial velocity of the fling.
+ *
+ * @param startX Starting point of the scroll (X)
+ * @param startY Starting point of the scroll (Y)
+ * @param velocityX Initial velocity of the fling (X) measured in pixels per
+ * second.
+ * @param velocityY Initial velocity of the fling (Y) measured in pixels per
+ * second
+ * @param minX Minimum X value. The scroller will not scroll past this
+ * point.
+ * @param maxX Maximum X value. The scroller will not scroll past this
+ * point.
+ * @param minY Minimum Y value. The scroller will not scroll past this
+ * point.
+ * @param maxY Maximum Y value. The scroller will not scroll past this
+ * point.
+ */
+ public void fling(int startX, int startY, int velocityX, int velocityY,
+ int minX, int maxX, int minY, int maxY) {
+ mMode = FLING_MODE;
+ mFinished = false;
+
+ float velocity = (float)Math.hypot(velocityX, velocityY);
+
+ mVelocity = velocity;
+ mDuration = (int) (1000 * velocity / mDeceleration); // Duration is in
+ // milliseconds
+ mStartTime = AnimationUtils.currentAnimationTimeMillis();
+ mStartX = startX;
+ mStartY = startY;
+
+ mCoeffX = velocity == 0 ? 1.0f : velocityX / velocity;
+ mCoeffY = velocity == 0 ? 1.0f : velocityY / velocity;
+
+ int totalDistance = (int) ((velocity * velocity) / (2 * mDeceleration));
+
+ mMinX = minX;
+ mMaxX = maxX;
+ mMinY = minY;
+ mMaxY = maxY;
+
+
+ mFinalX = startX + Math.round(totalDistance * mCoeffX);
+ // Pin to mMinX <= mFinalX <= mMaxX
+ mFinalX = Math.min(mFinalX, mMaxX);
+ mFinalX = Math.max(mFinalX, mMinX);
+
+ mFinalY = startY + Math.round(totalDistance * mCoeffY);
+ // Pin to mMinY <= mFinalY <= mMaxY
+ mFinalY = Math.min(mFinalY, mMaxY);
+ mFinalY = Math.max(mFinalY, mMinY);
+ }
+
+
+
+ private float viscousFluid(float x)
+ {
+ x *= mViscousFluidScale;
+ if (x < 1.0f) {
+ x -= (1.0f - (float)Math.exp(-x));
+ } else {
+ float start = 0.36787944117f; // 1/e == exp(-1)
+ x = 1.0f - (float)Math.exp(1.0f - x);
+ x = start + x * (1.0f - start);
+ }
+ x *= mViscousFluidNormalize;
+ return x;
+ }
+
+ /**
+ *
+ */
+ public void abortAnimation() {
+ mCurrX = mFinalX;
+ mCurrY = mFinalY;
+ mFinished = true;
+ }
+
+ /**
+ * Extend the scroll animation. This allows a running animation to
+ * scroll further and longer, when used with setFinalX() or setFinalY().
+ *
+ * @param extend Additional time to scroll in milliseconds.
+ */
+ public void extendDuration(int extend) {
+ int passed = timePassed();
+ mDuration = passed + extend;
+ mDurationReciprocal = 1.0f / (float)mDuration;
+ mFinished = false;
+ }
+
+ public int timePassed() {
+ return (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
+ }
+
+ public void setFinalX(int newX) {
+ mFinalX = newX;
+ mDeltaX = mFinalX - mStartX;
+ mFinished = false;
+ }
+
+ public void setFinalY(int newY) {
+ mFinalY = newY;
+ mDeltaY = mFinalY - mStartY;
+ mFinished = false;
+ }
+}
diff --git a/core/java/android/widget/SeekBar.java b/core/java/android/widget/SeekBar.java
new file mode 100644
index 0000000..e87dc2d
--- /dev/null
+++ b/core/java/android/widget/SeekBar.java
@@ -0,0 +1,116 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+
+
+/**
+ * A Seekbar is an extension of ProgressBar that adds a draggable thumb. The user can touch
+ * the thumb and drag left or right to set the current progress level.
+ *
+ * be notified of the user's actions.
+ * Clients of the Seekbar can attach a {@link SeekBar.OnSeekBarChangeListener} to
+ *
+ * @attr ref android.R.styleable#SeekBar_thumb
+ */
+public class SeekBar extends AbsSeekBar {
+
+ /**
+ * A callback that notifies clients when the progress level has been changed. This
+ * includes changes that were initiated by the user through a touch gesture as well
+ * as changes that were initiated programmatically.
+ */
+ public interface OnSeekBarChangeListener {
+
+ /**
+ * Notification that the progress level has changed. Clients can use the fromTouch parameter
+ * to distinguish user-initiated changes from those that occurred programmatically.
+ *
+ * @param seekBar The SeekBar whose progress has changed
+ * @param progress The current progress level. This will be in the range 0..max where max
+ * was set by {@link ProgressBar#setMax(int)}. (The default value for max is 100.)
+ * @param fromTouch True if the progress change was initiated by a user's touch gesture.
+ */
+ void onProgressChanged(SeekBar seekBar, int progress, boolean fromTouch);
+
+ /**
+ * Notification that the user has started a touch gesture. Clients may want to use this
+ * to disable advancing the seekbar.
+ * @param seekBar The SeekBar in which the touch gesture began
+ */
+ void onStartTrackingTouch(SeekBar seekBar);
+
+ /**
+ * Notification that the user has finished a touch gesture. Clients may want to use this
+ * to re-enable advancing the seekbar.
+ * @param seekBar The SeekBar in which the touch gesture began
+ */
+ void onStopTrackingTouch(SeekBar seekBar);
+ }
+
+ private OnSeekBarChangeListener mOnSeekBarChangeListener;
+
+ public SeekBar(Context context) {
+ this(context, null);
+ }
+
+ public SeekBar(Context context, AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.seekBarStyle);
+ }
+
+ public SeekBar(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ void onProgressRefresh(float scale, boolean fromTouch) {
+ super.onProgressRefresh(scale, fromTouch);
+
+ if (mOnSeekBarChangeListener != null) {
+ mOnSeekBarChangeListener.onProgressChanged(this, getProgress(), fromTouch);
+ }
+ }
+
+ /**
+ * Sets a listener to receive notifications of changes to the SeekBar's progress level. Also
+ * provides notifications of when the user starts and stops a touch gesture within the SeekBar.
+ *
+ * @param l The seek bar notification listener
+ *
+ * @see SeekBar.OnSeekBarChangeListener
+ */
+ public void setOnSeekBarChangeListener(OnSeekBarChangeListener l) {
+ mOnSeekBarChangeListener = l;
+ }
+
+ @Override
+ void onStartTrackingTouch() {
+ if (mOnSeekBarChangeListener != null) {
+ mOnSeekBarChangeListener.onStartTrackingTouch(this);
+ }
+ }
+
+ @Override
+ void onStopTrackingTouch() {
+ if (mOnSeekBarChangeListener != null) {
+ mOnSeekBarChangeListener.onStopTrackingTouch(this);
+ }
+ }
+}
diff --git a/core/java/android/widget/SimpleAdapter.java b/core/java/android/widget/SimpleAdapter.java
new file mode 100644
index 0000000..df52b69
--- /dev/null
+++ b/core/java/android/widget/SimpleAdapter.java
@@ -0,0 +1,366 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.LayoutInflater;
+import android.net.Uri;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * An easy adapter to map static data to views defined in an XML file. You can specify the data
+ * backing the list as an ArrayList of Maps. Each entry in the ArrayList corresponds to one row
+ * in the list. The Maps contain the data for each row. You also specify an XML file that
+ * defines the views used to display the row, and a mapping from keys in the Map to specific
+ * views.
+ *
+ * Binding data to views occurs in two phases. First, if a
+ * {@link android.widget.SimpleAdapter.ViewBinder} is available,
+ * {@link ViewBinder#setViewValue(android.view.View, Object, String)}
+ * is invoked. If the returned value is true, binding has occured. If the
+ * returned value is false and the view to bind is a TextView,
+ * {@link #setViewText(TextView, String)} is invoked. If the returned value
+ * is false and the view to bind is an ImageView,
+ * {@link #setViewImage(ImageView, int)} or {@link #setViewImage(ImageView, String)} is
+ * invoked. If no appropriate binding can be found, an {@link IllegalStateException} is thrown.
+ */
+public class SimpleAdapter extends BaseAdapter implements Filterable {
+ private int[] mTo;
+ private String[] mFrom;
+ private ViewBinder mViewBinder;
+
+ private List<? extends Map<String, ?>> mData;
+
+ private int mResource;
+ private int mDropDownResource;
+ private LayoutInflater mInflater;
+
+ private SimpleFilter mFilter;
+ private ArrayList<Map<String, ?>> mUnfilteredData;
+
+ /**
+ * Constructor
+ *
+ * @param context The context where the View associated with this SimpleAdapter is running
+ * @param data A List of Maps. Each entry in the List corresponds to one row in the list. The
+ * Maps contain the data for each row, and should include all the entries specified in
+ * "from"
+ * @param resource Resource identifier of a view layout that defines the views for this list
+ * item. The layout file should include at least those named views defined in "to"
+ * @param from A list of column names that will be added to the Map associated with each
+ * item.
+ * @param to The views that should display column in the "from" parameter. These should all be
+ * TextViews. The first N views in this list are given the values of the first N columns
+ * in the from parameter.
+ */
+ public SimpleAdapter(Context context, List<? extends Map<String, ?>> data,
+ int resource, String[] from, int[] to) {
+ mData = data;
+ mResource = mDropDownResource = resource;
+ mFrom = from;
+ mTo = to;
+ mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ }
+
+
+ /**
+ * @see android.widget.Adapter#getCount()
+ */
+ public int getCount() {
+ return mData.size();
+ }
+
+ /**
+ * @see android.widget.Adapter#getItem(int)
+ */
+ public Object getItem(int position) {
+ return mData.get(position);
+ }
+
+ /**
+ * @see android.widget.Adapter#getItemId(int)
+ */
+ public long getItemId(int position) {
+ return position;
+ }
+
+ /**
+ * @see android.widget.Adapter#getView(int, View, ViewGroup)
+ */
+ public View getView(int position, View convertView, ViewGroup parent) {
+ return createViewFromResource(position, convertView, parent, mResource);
+ }
+
+ private View createViewFromResource(int position, View convertView,
+ ViewGroup parent, int resource) {
+ View v;
+ if (convertView == null) {
+ v = mInflater.inflate(resource, parent, false);
+ } else {
+ v = convertView;
+ }
+ bindView(position, v);
+ return v;
+ }
+
+ /**
+ * <p>Sets the layout resource to create the drop down views.</p>
+ *
+ * @param resource the layout resource defining the drop down views
+ * @see #getDropDownView(int, android.view.View, android.view.ViewGroup)
+ */
+ public void setDropDownViewResource(int resource) {
+ this.mDropDownResource = resource;
+ }
+
+ @Override
+ public View getDropDownView(int position, View convertView, ViewGroup parent) {
+ return createViewFromResource(position, convertView, parent, mDropDownResource);
+ }
+
+ private void bindView(int position, View view) {
+ final Map dataSet = mData.get(position);
+ if (dataSet == null) {
+ return;
+ }
+
+ final String[] from = mFrom;
+ final int[] to = mTo;
+ final int len = to.length;
+
+ for (int i = 0; i < len; i++) {
+ final View v = view.findViewById(to[i]);
+ if (v != null) {
+ final Object data = dataSet.get(from[i]);
+ String text = data == null ? "" : data.toString();
+ if (text == null) {
+ text = "";
+ }
+
+ boolean bound = false;
+ if (mViewBinder != null) {
+ bound = mViewBinder.setViewValue(v, data, text);
+ }
+
+ if (!bound) {
+ if (v instanceof TextView) {
+ setViewText((TextView) v, text);
+ } else if (v instanceof ImageView) {
+ if (data instanceof Integer) {
+ setViewImage((ImageView) v, (Integer) data);
+ } else {
+ setViewImage((ImageView) v, text);
+ }
+ } else {
+ throw new IllegalStateException(v.getClass().getName() + " is not a " +
+ " view that can be bounds by this SimpleAdapter");
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the {@link ViewBinder} used to bind data to views.
+ *
+ * @return a ViewBinder or null if the binder does not exist
+ *
+ * @see #setViewBinder(android.widget.SimpleAdapter.ViewBinder)
+ */
+ public ViewBinder getViewBinder() {
+ return mViewBinder;
+ }
+
+ /**
+ * Sets the binder used to bind data to views.
+ *
+ * @param viewBinder the binder used to bind data to views, can be null to
+ * remove the existing binder
+ *
+ * @see #getViewBinder()
+ */
+ public void setViewBinder(ViewBinder viewBinder) {
+ mViewBinder = viewBinder;
+ }
+
+ /**
+ * Called by bindView() to set the image for an ImageView but only if
+ * there is no existing ViewBinder or if the existing ViewBinder cannot
+ * handle binding to an ImageView.
+ *
+ * This method is called instead of {@link #setViewImage(ImageView, String)}
+ * if the supplied data is an int or Integer.
+ *
+ * @param v ImageView to receive an image
+ * @param value the value retrieved from the data set
+ *
+ * @see #setViewImage(ImageView, String)
+ */
+ public void setViewImage(ImageView v, int value) {
+ v.setImageResource(value);
+ }
+
+ /**
+ * Called by bindView() to set the image for an ImageView but only if
+ * there is no existing ViewBinder or if the existing ViewBinder cannot
+ * handle binding to an ImageView.
+ *
+ * By default, the value will be treated as an image resource. If the
+ * value cannot be used as an image resource, the value is used as an
+ * image Uri.
+ *
+ * This method is called instead of {@link #setViewImage(ImageView, int)}
+ * if the supplied data is not an int or Integer.
+ *
+ * @param v ImageView to receive an image
+ * @param value the value retrieved from the data set
+ *
+ * @see #setViewImage(ImageView, int)
+ */
+ public void setViewImage(ImageView v, String value) {
+ try {
+ v.setImageResource(Integer.parseInt(value));
+ } catch (NumberFormatException nfe) {
+ v.setImageURI(Uri.parse(value));
+ }
+ }
+
+ /**
+ * Called by bindView() to set the text for a TextView but only if
+ * there is no existing ViewBinder or if the existing ViewBinder cannot
+ * handle binding to an TextView.
+ *
+ * @param v TextView to receive text
+ * @param text the text to be set for the TextView
+ */
+ public void setViewText(TextView v, String text) {
+ v.setText(text);
+ }
+
+ public Filter getFilter() {
+ if (mFilter == null) {
+ mFilter = new SimpleFilter();
+ }
+ return mFilter;
+ }
+
+ /**
+ * This class can be used by external clients of SimpleAdapter to bind
+ * values to views.
+ *
+ * You should use this class to bind values to views that are not
+ * directly supported by SimpleAdapter or to change the way binding
+ * occurs for views supported by SimpleAdapter.
+ *
+ * @see SimpleAdapter#setViewImage(ImageView, int)
+ * @see SimpleAdapter#setViewImage(ImageView, String)
+ * @see SimpleAdapter#setViewText(TextView, String)
+ */
+ public static interface ViewBinder {
+ /**
+ * Binds the specified data to the specified view.
+ *
+ * When binding is handled by this ViewBinder, this method must return true.
+ * If this method returns false, SimpleAdapter will attempts to handle
+ * the binding on its own.
+ *
+ * @param view the view to bind the data to
+ * @param data the data to bind to the view
+ * @param textRepresentation a safe String representation of the supplied data:
+ * it is either the result of data.toString() or an empty String but it
+ * is never null
+ *
+ * @return true if the data was bound to the view, false otherwise
+ */
+ boolean setViewValue(View view, Object data, String textRepresentation);
+ }
+
+ /**
+ * <p>An array filters constrains the content of the array adapter with
+ * a prefix. Each item that does not start with the supplied prefix
+ * is removed from the list.</p>
+ */
+ private class SimpleFilter extends Filter {
+
+ @Override
+ protected FilterResults performFiltering(CharSequence prefix) {
+ FilterResults results = new FilterResults();
+
+ if (mUnfilteredData == null) {
+ mUnfilteredData = new ArrayList<Map<String, ?>>(mData);
+ }
+
+ if (prefix == null || prefix.length() == 0) {
+ ArrayList<Map<String, ?>> list = mUnfilteredData;
+ results.values = list;
+ results.count = list.size();
+ } else {
+ String prefixString = prefix.toString().toLowerCase();
+
+ ArrayList<Map<String, ?>> unfilteredValues = mUnfilteredData;
+ int count = unfilteredValues.size();
+
+ ArrayList<Map<String, ?>> newValues = new ArrayList<Map<String, ?>>(count);
+
+ for (int i = 0; i < count; i++) {
+ Map<String, ?> h = unfilteredValues.get(i);
+ if (h != null) {
+
+ int len = mTo.length;
+
+ for (int j=0; j<len; j++) {
+ String str = (String)h.get(mFrom[j]);
+
+ String[] words = str.split(" ");
+ int wordCount = words.length;
+
+ for (int k = 0; k < wordCount; k++) {
+ String word = words[k];
+
+ if (word.toLowerCase().startsWith(prefixString)) {
+ newValues.add(h);
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ results.values = newValues;
+ results.count = newValues.size();
+ }
+
+ return results;
+ }
+
+ @Override
+ protected void publishResults(CharSequence constraint, FilterResults results) {
+ //noinspection unchecked
+ mData = (List<Map<String, ?>>) results.values;
+ if (results.count > 0) {
+ notifyDataSetChanged();
+ } else {
+ notifyDataSetInvalidated();
+ }
+ }
+ }
+}
diff --git a/core/java/android/widget/SimpleCursorAdapter.java b/core/java/android/widget/SimpleCursorAdapter.java
new file mode 100644
index 0000000..4d2fab3
--- /dev/null
+++ b/core/java/android/widget/SimpleCursorAdapter.java
@@ -0,0 +1,365 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.view.View;
+
+/**
+ * An easy adapter to map columns from a cursor to TextViews or ImageViews
+ * defined in an XML file. You can specify which columns you want, which
+ * views you want to display the columns, and the XML file that defines
+ * the appearance of these views.
+ *
+ * Binding occurs in two phases. First, if a
+ * {@link android.widget.SimpleCursorAdapter.ViewBinder} is available,
+ * {@link ViewBinder#setViewValue(android.view.View, android.database.Cursor, int)}
+ * is invoked. If the returned value is true, binding has occured. If the
+ * returned value is false and the view to bind is a TextView,
+ * {@link #setViewText(TextView, String)} is invoked. If the returned value
+ * is false and the view to bind is an ImageView,
+ * {@link #setViewImage(ImageView, String)} is invoked. If no appropriate
+ * binding can be found, an {@link IllegalStateException} is thrown.
+ *
+ * If this adapter is used with filtering, for instance in an
+ * {@link android.widget.AutoCompleteTextView}, you can use the
+ * {@link android.widget.SimpleCursorAdapter.CursorToStringConverter} and the
+ * {@link android.widget.FilterQueryProvider} interfaces
+ * to get control over the filtering process. You can refer to
+ * {@link #convertToString(android.database.Cursor)} and
+ * {@link #runQueryOnBackgroundThread(CharSequence)} for more information.
+ */
+public class SimpleCursorAdapter extends ResourceCursorAdapter {
+ /**
+ * A list of columns containing the data to bind to the UI.
+ * This field should be made private, so it is hidden from the SDK.
+ * {@hide}
+ */
+ protected int[] mFrom;
+ /**
+ * A list of View ids representing the views to which the data must be bound.
+ * This field should be made private, so it is hidden from the SDK.
+ * {@hide}
+ */
+ protected int[] mTo;
+
+ private int mStringConversionColumn = -1;
+ private CursorToStringConverter mCursorToStringConverter;
+ private ViewBinder mViewBinder;
+ private String[] mOriginalFrom;
+
+ /**
+ * Constructor.
+ *
+ * @param context The context where the ListView associated with this
+ * SimpleListItemFactory is running
+ * @param layout resource identifier of a layout file that defines the views
+ * for this list item. Thelayout file should include at least
+ * those named views defined in "to"
+ * @param c The database cursor. Can be null if the cursor is not available yet.
+ * @param from A list of column names representing the data to bind to the UI
+ * @param to The views that should display column in the "from" parameter.
+ * These should all be TextViews. The first N views in this list
+ * are given the values of the first N columns in the from
+ * parameter.
+ */
+ public SimpleCursorAdapter(Context context, int layout, Cursor c,
+ String[] from, int[] to) {
+ super(context, layout, c);
+ mTo = to;
+ mOriginalFrom = from;
+ findColumns(from);
+ }
+
+ /**
+ * Binds all of the field names passed into the "to" parameter of the
+ * constructor with their corresponding cursor columns as specified in the
+ * "from" parameter.
+ *
+ * Binding occurs in two phases. First, if a
+ * {@link android.widget.SimpleCursorAdapter.ViewBinder} is available,
+ * {@link ViewBinder#setViewValue(android.view.View, android.database.Cursor, int)}
+ * is invoked. If the returned value is true, binding has occured. If the
+ * returned value is false and the view to bind is a TextView,
+ * {@link #setViewText(TextView, String)} is invoked. If the returned value is
+ * false and the view to bind is an ImageView,
+ * {@link #setViewImage(ImageView, String)} is invoked. If no appropriate
+ * binding can be found, an {@link IllegalStateException} is thrown.
+ *
+ * @throws IllegalStateException if binding cannot occur
+ *
+ * @see android.widget.CursorAdapter#bindView(android.view.View,
+ * android.content.Context, android.database.Cursor)
+ * @see #getViewBinder()
+ * @see #setViewBinder(android.widget.SimpleCursorAdapter.ViewBinder)
+ * @see #setViewImage(ImageView, String)
+ * @see #setViewText(TextView, String)
+ */
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ for (int i = 0; i < mTo.length; i++) {
+ final View v = view.findViewById(mTo[i]);
+ if (v != null) {
+ String text = cursor.getString(mFrom[i]);
+ if (text == null) {
+ text = "";
+ }
+
+ boolean bound = false;
+ if (mViewBinder != null) {
+ bound = mViewBinder.setViewValue(v, cursor, mFrom[i]);
+ }
+
+ if (!bound) {
+ if (v instanceof TextView) {
+ setViewText((TextView) v, text);
+ } else if (v instanceof ImageView) {
+ setViewImage((ImageView) v, text);
+ } else {
+ throw new IllegalStateException(v.getClass().getName() + " is not a " +
+ " view that can be bounds by this SimpleCursorAdapter");
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the {@link ViewBinder} used to bind data to views.
+ *
+ * @return a ViewBinder or null if the binder does not exist
+ *
+ * @see #bindView(android.view.View, android.content.Context, android.database.Cursor)
+ * @see #setViewBinder(android.widget.SimpleCursorAdapter.ViewBinder)
+ */
+ public ViewBinder getViewBinder() {
+ return mViewBinder;
+ }
+
+ /**
+ * Sets the binder used to bind data to views.
+ *
+ * @param viewBinder the binder used to bind data to views, can be null to
+ * remove the existing binder
+ *
+ * @see #bindView(android.view.View, android.content.Context, android.database.Cursor)
+ * @see #getViewBinder()
+ */
+ public void setViewBinder(ViewBinder viewBinder) {
+ mViewBinder = viewBinder;
+ }
+
+ /**
+ * Called by bindView() to set the image for an ImageView but only if
+ * there is no existing ViewBinder or if the existing ViewBinder cannot
+ * handle binding to an ImageView.
+ *
+ * By default, the value will be treated as an image resource. If the
+ * value cannot be used as an image resource, the value is used as an
+ * image Uri.
+ *
+ * Intended to be overridden by Adapters that need to filter strings
+ * retrieved from the database.
+ *
+ * @param v ImageView to receive an image
+ * @param value the value retrieved from the cursor
+ */
+ public void setViewImage(ImageView v, String value) {
+ try {
+ v.setImageResource(Integer.parseInt(value));
+ } catch (NumberFormatException nfe) {
+ v.setImageURI(Uri.parse(value));
+ }
+ }
+
+ /**
+ * Called by bindView() to set the text for a TextView but only if
+ * there is no existing ViewBinder or if the existing ViewBinder cannot
+ * handle binding to an TextView.
+ *
+ * Intended to be overridden by Adapters that need to filter strings
+ * retrieved from the database.
+ *
+ * @param v TextView to receive text
+ * @param text the text to be set for the TextView
+ */
+ public void setViewText(TextView v, String text) {
+ v.setText(text);
+ }
+
+ /**
+ * Return the index of the column used to get a String representation
+ * of the Cursor.
+ *
+ * @return a valid index in the current Cursor or -1
+ *
+ * @see android.widget.CursorAdapter#convertToString(android.database.Cursor)
+ * @see #setStringConversionColumn(int)
+ * @see #setCursorToStringConverter(android.widget.SimpleCursorAdapter.CursorToStringConverter)
+ * @see #getCursorToStringConverter()
+ */
+ public int getStringConversionColumn() {
+ return mStringConversionColumn;
+ }
+
+ /**
+ * Defines the index of the column in the Cursor used to get a String
+ * representation of that Cursor. The column is used to convert the
+ * Cursor to a String only when the current CursorToStringConverter
+ * is null.
+ *
+ * @param stringConversionColumn a valid index in the current Cursor or -1 to use the default
+ * conversion mechanism
+ *
+ * @see android.widget.CursorAdapter#convertToString(android.database.Cursor)
+ * @see #getStringConversionColumn()
+ * @see #setCursorToStringConverter(android.widget.SimpleCursorAdapter.CursorToStringConverter)
+ * @see #getCursorToStringConverter()
+ */
+ public void setStringConversionColumn(int stringConversionColumn) {
+ mStringConversionColumn = stringConversionColumn;
+ }
+
+ /**
+ * Returns the converter used to convert the filtering Cursor
+ * into a String.
+ *
+ * @return null if the converter does not exist or an instance of
+ * {@link android.widget.SimpleCursorAdapter.CursorToStringConverter}
+ *
+ * @see #setCursorToStringConverter(android.widget.SimpleCursorAdapter.CursorToStringConverter)
+ * @see #getStringConversionColumn()
+ * @see #setStringConversionColumn(int)
+ * @see android.widget.CursorAdapter#convertToString(android.database.Cursor)
+ */
+ public CursorToStringConverter getCursorToStringConverter() {
+ return mCursorToStringConverter;
+ }
+
+ /**
+ * Sets the converter used to convert the filtering Cursor
+ * into a String.
+ *
+ * @param cursorToStringConverter the Cursor to String converter, or
+ * null to remove the converter
+ *
+ * @see #setCursorToStringConverter(android.widget.SimpleCursorAdapter.CursorToStringConverter)
+ * @see #getStringConversionColumn()
+ * @see #setStringConversionColumn(int)
+ * @see android.widget.CursorAdapter#convertToString(android.database.Cursor)
+ */
+ public void setCursorToStringConverter(CursorToStringConverter cursorToStringConverter) {
+ mCursorToStringConverter = cursorToStringConverter;
+ }
+
+ /**
+ * Returns a CharSequence representation of the specified Cursor as defined
+ * by the current CursorToStringConverter. If no CursorToStringConverter
+ * has been set, the String conversion column is used instead. If the
+ * conversion column is -1, the returned String is empty if the cursor
+ * is null or Cursor.toString().
+ *
+ * @param cursor the Cursor to convert to a CharSequence
+ *
+ * @return a non-null CharSequence representing the cursor
+ */
+ @Override
+ public CharSequence convertToString(Cursor cursor) {
+ if (mCursorToStringConverter != null) {
+ return mCursorToStringConverter.convertToString(cursor);
+ } else if (mStringConversionColumn > -1) {
+ return cursor.getString(mStringConversionColumn);
+ }
+
+ return super.convertToString(cursor);
+ }
+
+ private void findColumns(String[] from) {
+ int i;
+ int count = from.length;
+ if (mFrom == null) {
+ mFrom = new int[count];
+ }
+ if (mCursor != null) {
+ for (i = 0; i < count; i++) {
+ mFrom[i] = mCursor.getColumnIndexOrThrow(from[i]);
+ }
+ } else {
+ for (i = 0; i < count; i++) {
+ mFrom[i] = -1;
+ }
+ }
+ }
+
+ @Override
+ public void changeCursor(Cursor c) {
+ super.changeCursor(c);
+ // rescan columns in case cursor layout is different
+ findColumns(mOriginalFrom);
+ }
+
+ /**
+ * This class can be used by external clients of SimpleCursorAdapter
+ * to bind values fom the Cursor to views.
+ *
+ * You should use this class to bind values from the Cursor to views
+ * that are not directly supported by SimpleCursorAdapter or to
+ * change the way binding occurs for views supported by
+ * SimpleCursorAdapter.
+ *
+ * @see SimpleCursorAdapter#bindView(android.view.View, android.content.Context, android.database.Cursor)
+ * @see SimpleCursorAdapter#setViewImage(ImageView, String)
+ * @see SimpleCursorAdapter#setViewText(TextView, String)
+ */
+ public static interface ViewBinder {
+ /**
+ * Binds the Cursor column defined by the specified index to the specified view.
+ *
+ * When binding is handled by this ViewBinder, this method must return true.
+ * If this method returns false, SimpleCursorAdapter will attempts to handle
+ * the binding on its own.
+ *
+ * @param view the view to bind the data to
+ * @param cursor the cursor to get the data from
+ * @param columnIndex the column at which the data can be found in the cursor
+ *
+ * @return true if the data was bound to the view, false otherwise
+ */
+ boolean setViewValue(View view, Cursor cursor, int columnIndex);
+ }
+
+ /**
+ * This class can be used by external clients of SimpleCursorAdapter
+ * to define how the Cursor should be converted to a String.
+ *
+ * @see android.widget.CursorAdapter#convertToString(android.database.Cursor)
+ */
+ public static interface CursorToStringConverter {
+ /**
+ * Returns a CharSequence representing the specified Cursor.
+ *
+ * @param cursor the cursor for which a CharSequence representation
+ * is requested
+ *
+ * @return a non-null CharSequence representing the cursor
+ */
+ CharSequence convertToString(Cursor cursor);
+ }
+
+}
diff --git a/core/java/android/widget/SimpleCursorTreeAdapter.java b/core/java/android/widget/SimpleCursorTreeAdapter.java
new file mode 100644
index 0000000..c456f56
--- /dev/null
+++ b/core/java/android/widget/SimpleCursorTreeAdapter.java
@@ -0,0 +1,241 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.view.View;
+
+/**
+ * An easy adapter to map columns from a cursor to TextViews or ImageViews
+ * defined in an XML file. You can specify which columns you want, which views
+ * you want to display the columns, and the XML file that defines the appearance
+ * of these views. Separate XML files for child and groups are possible.
+ * TextViews bind the values to their text property (see
+ * {@link TextView#setText(CharSequence)}). ImageViews bind the values to their
+ * image's Uri property (see {@link ImageView#setImageURI(android.net.Uri)}).
+ */
+public abstract class SimpleCursorTreeAdapter extends ResourceCursorTreeAdapter {
+ /** The indices of columns that contain data to display for a group. */
+ private int[] mGroupFrom;
+ /**
+ * The View IDs that will display a group's data fetched from the
+ * corresponding column.
+ */
+ private int[] mGroupTo;
+
+ /** The indices of columns that contain data to display for a child. */
+ private int[] mChildFrom;
+ /**
+ * The View IDs that will display a child's data fetched from the
+ * corresponding column.
+ */
+ private int[] mChildTo;
+
+ /**
+ * Constructor.
+ *
+ * @param context The context where the {@link ExpandableListView}
+ * associated with this {@link SimpleCursorTreeAdapter} is
+ * running
+ * @param cursor The database cursor
+ * @param collapsedGroupLayout The resource identifier of a layout file that
+ * defines the views for a collapsed group. The layout file
+ * should include at least those named views defined in groupTo.
+ * @param expandedGroupLayout The resource identifier of a layout file that
+ * defines the views for an expanded group. The layout file
+ * should include at least those named views defined in groupTo.
+ * @param groupFrom A list of column names that will be used to display the
+ * data for a group.
+ * @param groupTo The group views (from the group layouts) that should
+ * display column in the "from" parameter. These should all be
+ * TextViews or ImageViews. The first N views in this list are
+ * given the values of the first N columns in the from parameter.
+ * @param childLayout The resource identifier of a layout file that defines
+ * the views for a child (except the last). The layout file
+ * should include at least those named views defined in childTo.
+ * @param lastChildLayout The resource identifier of a layout file that
+ * defines the views for the last child within a group. The
+ * layout file should include at least those named views defined
+ * in childTo.
+ * @param childFrom A list of column names that will be used to display the
+ * data for a child.
+ * @param childTo The child views (from the child layouts) that should
+ * display column in the "from" parameter. These should all be
+ * TextViews or ImageViews. The first N views in this list are
+ * given the values of the first N columns in the from parameter.
+ */
+ public SimpleCursorTreeAdapter(Context context, Cursor cursor, int collapsedGroupLayout,
+ int expandedGroupLayout, String[] groupFrom, int[] groupTo, int childLayout,
+ int lastChildLayout, String[] childFrom, int[] childTo) {
+ super(context, cursor, collapsedGroupLayout, expandedGroupLayout, childLayout,
+ lastChildLayout);
+ init(groupFrom, groupTo, childFrom, childTo);
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param context The context where the {@link ExpandableListView}
+ * associated with this {@link SimpleCursorTreeAdapter} is
+ * running
+ * @param cursor The database cursor
+ * @param collapsedGroupLayout The resource identifier of a layout file that
+ * defines the views for a collapsed group. The layout file
+ * should include at least those named views defined in groupTo.
+ * @param expandedGroupLayout The resource identifier of a layout file that
+ * defines the views for an expanded group. The layout file
+ * should include at least those named views defined in groupTo.
+ * @param groupFrom A list of column names that will be used to display the
+ * data for a group.
+ * @param groupTo The group views (from the group layouts) that should
+ * display column in the "from" parameter. These should all be
+ * TextViews or ImageViews. The first N views in this list are
+ * given the values of the first N columns in the from parameter.
+ * @param childLayout The resource identifier of a layout file that defines
+ * the views for a child. The layout file
+ * should include at least those named views defined in childTo.
+ * @param childFrom A list of column names that will be used to display the
+ * data for a child.
+ * @param childTo The child views (from the child layouts) that should
+ * display column in the "from" parameter. These should all be
+ * TextViews or ImageViews. The first N views in this list are
+ * given the values of the first N columns in the from parameter.
+ */
+ public SimpleCursorTreeAdapter(Context context, Cursor cursor, int collapsedGroupLayout,
+ int expandedGroupLayout, String[] groupFrom, int[] groupTo,
+ int childLayout, String[] childFrom, int[] childTo) {
+ super(context, cursor, collapsedGroupLayout, expandedGroupLayout, childLayout);
+ init(groupFrom, groupTo, childFrom, childTo);
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param context The context where the {@link ExpandableListView}
+ * associated with this {@link SimpleCursorTreeAdapter} is
+ * running
+ * @param cursor The database cursor
+ * @param groupLayout The resource identifier of a layout file that defines
+ * the views for a group. The layout file should include at least
+ * those named views defined in groupTo.
+ * @param groupFrom A list of column names that will be used to display the
+ * data for a group.
+ * @param groupTo The group views (from the group layouts) that should
+ * display column in the "from" parameter. These should all be
+ * TextViews or ImageViews. The first N views in this list are
+ * given the values of the first N columns in the from parameter.
+ * @param childLayout The resource identifier of a layout file that defines
+ * the views for a child. The layout file should include at least
+ * those named views defined in childTo.
+ * @param childFrom A list of column names that will be used to display the
+ * data for a child.
+ * @param childTo The child views (from the child layouts) that should
+ * display column in the "from" parameter. These should all be
+ * TextViews or ImageViews. The first N views in this list are
+ * given the values of the first N columns in the from parameter.
+ */
+ public SimpleCursorTreeAdapter(Context context, Cursor cursor, int groupLayout,
+ String[] groupFrom, int[] groupTo, int childLayout, String[] childFrom,
+ int[] childTo) {
+ super(context, cursor, groupLayout, childLayout);
+ init(groupFrom, groupTo, childFrom, childTo);
+ }
+
+ private void init(String[] groupFromNames, int[] groupTo, String[] childFromNames,
+ int[] childTo) {
+ mGroupTo = groupTo;
+
+ mChildTo = childTo;
+
+ // Get the group cursor column indices, the child cursor column indices will come
+ // when needed
+ initGroupFromColumns(groupFromNames);
+
+ // Get a temporary child cursor to init the column indices
+ if (getGroupCount() > 0) {
+ MyCursorHelper tmpCursorHelper = getChildrenCursorHelper(0, true);
+ if (tmpCursorHelper != null) {
+ initChildrenFromColumns(childFromNames, tmpCursorHelper.getCursor());
+ deactivateChildrenCursorHelper(0);
+ }
+ }
+ }
+
+ private void initFromColumns(Cursor cursor, String[] fromColumnNames, int[] fromColumns) {
+ for (int i = fromColumnNames.length - 1; i >= 0; i--) {
+ fromColumns[i] = cursor.getColumnIndexOrThrow(fromColumnNames[i]);
+ }
+ }
+
+ private void initGroupFromColumns(String[] groupFromNames) {
+ mGroupFrom = new int[groupFromNames.length];
+ initFromColumns(mGroupCursorHelper.getCursor(), groupFromNames, mGroupFrom);
+ }
+
+ private void initChildrenFromColumns(String[] childFromNames, Cursor childCursor) {
+ mChildFrom = new int[childFromNames.length];
+ initFromColumns(childCursor, childFromNames, mChildFrom);
+ }
+
+ private void bindView(View view, Context context, Cursor cursor, int[] from, int[] to) {
+ for (int i = 0; i < to.length; i++) {
+ View v = view.findViewById(to[i]);
+ if (v != null) {
+ String text = cursor.getString(from[i]);
+ if (text == null) {
+ text = "";
+ }
+ if (v instanceof TextView) {
+ ((TextView) v).setText(text);
+ } else if (v instanceof ImageView) {
+ setViewImage((ImageView) v, text);
+ } else {
+ throw new IllegalStateException("SimpleCursorAdapter can bind values only to" +
+ " TextView and ImageView!");
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void bindChildView(View view, Context context, Cursor cursor, boolean isLastChild) {
+ bindView(view, context, cursor, mChildFrom, mChildTo);
+ }
+
+ @Override
+ protected void bindGroupView(View view, Context context, Cursor cursor, boolean isExpanded) {
+ bindView(view, context, cursor, mGroupFrom, mGroupTo);
+ }
+
+ /**
+ * Called by bindView() to set the image for an ImageView. By default, the
+ * value will be treated as a Uri. Intended to be overridden by Adapters
+ * that need to filter strings retrieved from the database.
+ *
+ * @param v ImageView to receive an image
+ * @param value the value retrieved from the cursor
+ */
+ protected void setViewImage(ImageView v, String value) {
+ try {
+ v.setImageResource(Integer.parseInt(value));
+ } catch (NumberFormatException nfe) {
+ v.setImageURI(Uri.parse(value));
+ }
+ }
+}
diff --git a/core/java/android/widget/SimpleExpandableListAdapter.java b/core/java/android/widget/SimpleExpandableListAdapter.java
new file mode 100644
index 0000000..015c169
--- /dev/null
+++ b/core/java/android/widget/SimpleExpandableListAdapter.java
@@ -0,0 +1,301 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.LayoutInflater;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * An easy adapter to map static data to group and child views defined in an XML
+ * file. You can separately specify the data backing the group as a List of
+ * Maps. Each entry in the ArrayList corresponds to one group in the expandable
+ * list. The Maps contain the data for each row. You also specify an XML file
+ * that defines the views used to display a group, and a mapping from keys in
+ * the Map to specific views. This process is similar for a child, except it is
+ * one-level deeper so the data backing is specified as a List<List<Map>>,
+ * where the first List corresponds to the group of the child, the second List
+ * corresponds to the position of the child within the group, and finally the
+ * Map holds the data for that particular child.
+ */
+public class SimpleExpandableListAdapter extends BaseExpandableListAdapter {
+ private List<? extends Map<String, ?>> mGroupData;
+ private int mExpandedGroupLayout;
+ private int mCollapsedGroupLayout;
+ private String[] mGroupFrom;
+ private int[] mGroupTo;
+
+ private List<? extends List<? extends Map<String, ?>>> mChildData;
+ private int mChildLayout;
+ private int mLastChildLayout;
+ private String[] mChildFrom;
+ private int[] mChildTo;
+
+ private LayoutInflater mInflater;
+
+ /**
+ * Constructor
+ *
+ * @param context The context where the {@link ExpandableListView}
+ * associated with this {@link SimpleExpandableListAdapter} is
+ * running
+ * @param groupData A List of Maps. Each entry in the List corresponds to
+ * one group in the list. The Maps contain the data for each
+ * group, and should include all the entries specified in
+ * "groupFrom"
+ * @param groupFrom A list of keys that will be fetched from the Map
+ * associated with each group.
+ * @param groupTo The group views that should display column in the
+ * "groupFrom" parameter. These should all be TextViews. The
+ * first N views in this list are given the values of the first N
+ * columns in the groupFrom parameter.
+ * @param groupLayout resource identifier of a view layout that defines the
+ * views for a group. The layout file should include at least
+ * those named views defined in "groupTo"
+ * @param childData A List of List of Maps. Each entry in the outer List
+ * corresponds to a group (index by group position), each entry
+ * in the inner List corresponds to a child within the group
+ * (index by child position), and the Map corresponds to the data
+ * for a child (index by values in the childFrom array). The Map
+ * contains the data for each child, and should include all the
+ * entries specified in "childFrom"
+ * @param childFrom A list of keys that will be fetched from the Map
+ * associated with each child.
+ * @param childTo The child views that should display column in the
+ * "childFrom" parameter. These should all be TextViews. The
+ * first N views in this list are given the values of the first N
+ * columns in the childFrom parameter.
+ * @param childLayout resource identifier of a view layout that defines the
+ * views for a child. The layout file should include at least
+ * those named views defined in "childTo"
+ */
+ public SimpleExpandableListAdapter(Context context,
+ List<? extends Map<String, ?>> groupData, int groupLayout,
+ String[] groupFrom, int[] groupTo,
+ List<? extends List<? extends Map<String, ?>>> childData,
+ int childLayout, String[] childFrom, int[] childTo) {
+ this(context, groupData, groupLayout, groupLayout, groupFrom, groupTo, childData,
+ childLayout, childLayout, childFrom, childTo);
+ }
+
+ /**
+ * Constructor
+ *
+ * @param context The context where the {@link ExpandableListView}
+ * associated with this {@link SimpleExpandableListAdapter} is
+ * running
+ * @param groupData A List of Maps. Each entry in the List corresponds to
+ * one group in the list. The Maps contain the data for each
+ * group, and should include all the entries specified in
+ * "groupFrom"
+ * @param groupFrom A list of keys that will be fetched from the Map
+ * associated with each group.
+ * @param groupTo The group views that should display column in the
+ * "groupFrom" parameter. These should all be TextViews. The
+ * first N views in this list are given the values of the first N
+ * columns in the groupFrom parameter.
+ * @param expandedGroupLayout resource identifier of a view layout that
+ * defines the views for an expanded group. The layout file
+ * should include at least those named views defined in "groupTo"
+ * @param collapsedGroupLayout resource identifier of a view layout that
+ * defines the views for a collapsed group. The layout file
+ * should include at least those named views defined in "groupTo"
+ * @param childData A List of List of Maps. Each entry in the outer List
+ * corresponds to a group (index by group position), each entry
+ * in the inner List corresponds to a child within the group
+ * (index by child position), and the Map corresponds to the data
+ * for a child (index by values in the childFrom array). The Map
+ * contains the data for each child, and should include all the
+ * entries specified in "childFrom"
+ * @param childFrom A list of keys that will be fetched from the Map
+ * associated with each child.
+ * @param childTo The child views that should display column in the
+ * "childFrom" parameter. These should all be TextViews. The
+ * first N views in this list are given the values of the first N
+ * columns in the childFrom parameter.
+ * @param childLayout resource identifier of a view layout that defines the
+ * views for a child. The layout file should include at least
+ * those named views defined in "childTo"
+ */
+ public SimpleExpandableListAdapter(Context context,
+ List<? extends Map<String, ?>> groupData, int expandedGroupLayout,
+ int collapsedGroupLayout, String[] groupFrom, int[] groupTo,
+ List<? extends List<? extends Map<String, ?>>> childData,
+ int childLayout, String[] childFrom, int[] childTo) {
+ this(context, groupData, expandedGroupLayout, collapsedGroupLayout,
+ groupFrom, groupTo, childData, childLayout, childLayout,
+ childFrom, childTo);
+ }
+
+ /**
+ * Constructor
+ *
+ * @param context The context where the {@link ExpandableListView}
+ * associated with this {@link SimpleExpandableListAdapter} is
+ * running
+ * @param groupData A List of Maps. Each entry in the List corresponds to
+ * one group in the list. The Maps contain the data for each
+ * group, and should include all the entries specified in
+ * "groupFrom"
+ * @param groupFrom A list of keys that will be fetched from the Map
+ * associated with each group.
+ * @param groupTo The group views that should display column in the
+ * "groupFrom" parameter. These should all be TextViews. The
+ * first N views in this list are given the values of the first N
+ * columns in the groupFrom parameter.
+ * @param expandedGroupLayout resource identifier of a view layout that
+ * defines the views for an expanded group. The layout file
+ * should include at least those named views defined in "groupTo"
+ * @param collapsedGroupLayout resource identifier of a view layout that
+ * defines the views for a collapsed group. The layout file
+ * should include at least those named views defined in "groupTo"
+ * @param childData A List of List of Maps. Each entry in the outer List
+ * corresponds to a group (index by group position), each entry
+ * in the inner List corresponds to a child within the group
+ * (index by child position), and the Map corresponds to the data
+ * for a child (index by values in the childFrom array). The Map
+ * contains the data for each child, and should include all the
+ * entries specified in "childFrom"
+ * @param childFrom A list of keys that will be fetched from the Map
+ * associated with each child.
+ * @param childTo The child views that should display column in the
+ * "childFrom" parameter. These should all be TextViews. The
+ * first N views in this list are given the values of the first N
+ * columns in the childFrom parameter.
+ * @param childLayout resource identifier of a view layout that defines the
+ * views for a child (unless it is the last child within a group,
+ * in which case the lastChildLayout is used). The layout file
+ * should include at least those named views defined in "childTo"
+ * @param lastChildLayout resource identifier of a view layout that defines
+ * the views for the last child within each group. The layout
+ * file should include at least those named views defined in
+ * "childTo"
+ */
+ public SimpleExpandableListAdapter(Context context,
+ List<? extends Map<String, ?>> groupData, int expandedGroupLayout,
+ int collapsedGroupLayout, String[] groupFrom, int[] groupTo,
+ List<? extends List<? extends Map<String, ?>>> childData,
+ int childLayout, int lastChildLayout, String[] childFrom,
+ int[] childTo) {
+ mGroupData = groupData;
+ mExpandedGroupLayout = expandedGroupLayout;
+ mCollapsedGroupLayout = collapsedGroupLayout;
+ mGroupFrom = groupFrom;
+ mGroupTo = groupTo;
+
+ mChildData = childData;
+ mChildLayout = childLayout;
+ mLastChildLayout = lastChildLayout;
+ mChildFrom = childFrom;
+ mChildTo = childTo;
+
+ mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ }
+
+ public Object getChild(int groupPosition, int childPosition) {
+ return mChildData.get(groupPosition).get(childPosition);
+ }
+
+ public long getChildId(int groupPosition, int childPosition) {
+ return childPosition;
+ }
+
+ public View getChildView(int groupPosition, int childPosition, boolean isLastChild,
+ View convertView, ViewGroup parent) {
+ View v;
+ if (convertView == null) {
+ v = newChildView(isLastChild, parent);
+ } else {
+ v = convertView;
+ }
+ bindView(v, mChildData.get(groupPosition).get(childPosition), mChildFrom, mChildTo);
+ return v;
+ }
+
+ /**
+ * Instantiates a new View for a child.
+ * @param isLastChild Whether the child is the last child within its group.
+ * @param parent The eventual parent of this new View.
+ * @return A new child View
+ */
+ public View newChildView(boolean isLastChild, ViewGroup parent) {
+ return mInflater.inflate((isLastChild) ? mLastChildLayout : mChildLayout, parent, false);
+ }
+
+ private void bindView(View view, Map<String, ?> data, String[] from, int[] to) {
+ int len = to.length;
+
+ for (int i = 0; i < len; i++) {
+ TextView v = (TextView)view.findViewById(to[i]);
+ if (v != null) {
+ v.setText((String)data.get(from[i]));
+ }
+ }
+ }
+
+ public int getChildrenCount(int groupPosition) {
+ return mChildData.get(groupPosition).size();
+ }
+
+ public Object getGroup(int groupPosition) {
+ return mGroupData.get(groupPosition);
+ }
+
+ public int getGroupCount() {
+ return mGroupData.size();
+ }
+
+ public long getGroupId(int groupPosition) {
+ return groupPosition;
+ }
+
+ public View getGroupView(int groupPosition, boolean isExpanded, View convertView,
+ ViewGroup parent) {
+ View v;
+ if (convertView == null) {
+ v = newGroupView(isExpanded, parent);
+ } else {
+ v = convertView;
+ }
+ bindView(v, mGroupData.get(groupPosition), mGroupFrom, mGroupTo);
+ return v;
+ }
+
+ /**
+ * Instantiates a new View for a group.
+ * @param isExpanded Whether the group is currently expanded.
+ * @param parent The eventual parent of this new View.
+ * @return A new group View
+ */
+ public View newGroupView(boolean isExpanded, ViewGroup parent) {
+ return mInflater.inflate((isExpanded) ? mExpandedGroupLayout : mCollapsedGroupLayout,
+ parent, false);
+ }
+
+ public boolean isChildSelectable(int groupPosition, int childPosition) {
+ return true;
+ }
+
+ public boolean hasStableIds() {
+ return true;
+ }
+
+}
diff --git a/core/java/android/widget/Spinner.java b/core/java/android/widget/Spinner.java
new file mode 100644
index 0000000..80d688e
--- /dev/null
+++ b/core/java/android/widget/Spinner.java
@@ -0,0 +1,364 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import android.annotation.Widget;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.res.TypedArray;
+import android.database.DataSetObserver;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewGroup;
+
+
+/**
+ * A view that displays one child at a time and lets the user pick among them.
+ * The items in the Spinner come from the {@link Adapter} associated with
+ * this view.
+ *
+ * @attr ref android.R.styleable#Spinner_prompt
+ */
+@Widget
+public class Spinner extends AbsSpinner implements OnClickListener {
+
+ private CharSequence mPrompt;
+
+ public Spinner(Context context) {
+ this(context, null);
+ }
+
+ public Spinner(Context context, AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.spinnerStyle);
+ }
+
+ public Spinner(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ TypedArray a = context.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.Spinner, defStyle, 0);
+
+ mPrompt = a.getString(com.android.internal.R.styleable.Spinner_prompt);
+
+ a.recycle();
+ }
+
+ @Override
+ public int getBaseline() {
+ View child = null;
+
+ if (getChildCount() > 0) {
+ child = getChildAt(0);
+ } else if (mAdapter != null && mAdapter.getCount() > 0) {
+ child = makeAndAddView(0);
+ // TODO: We should probably put the child in the recycler
+ }
+
+ if (child != null) {
+ return child.getTop() + child.getBaseline();
+ } else {
+ return -1;
+ }
+ }
+
+ /**
+ * <p>A spinner does not support item click events. Calling this method
+ * will raise an exception.</p>
+ *
+ * @param l this listener will be ignored
+ */
+ @Override
+ public void setOnItemClickListener(OnItemClickListener l) {
+ throw new RuntimeException("setOnItemClickListener cannot be used with a spinner.");
+ }
+
+ /**
+ * @see android.view.View#onLayout(boolean,int,int,int,int)
+ *
+ * Creates and positions all views
+ *
+ */
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ super.onLayout(changed, l, t, r, b);
+ mInLayout = true;
+ layout(0, false);
+ mInLayout = false;
+ }
+
+ /**
+ * Creates and positions all views for this Spinner.
+ *
+ * @param delta Change in the selected position. +1 moves selection is moving to the right,
+ * so views are scrolling to the left. -1 means selection is moving to the left.
+ */
+ @Override
+ void layout(int delta, boolean animate) {
+ int childrenLeft = mSpinnerPadding.left;
+ int childrenWidth = mRight - mLeft - mSpinnerPadding.left - mSpinnerPadding.right;
+
+ if (mDataChanged) {
+ handleDataChanged();
+ }
+
+ // Handle the empty set by removing all views
+ if (mItemCount == 0) {
+ resetList();
+ return;
+ }
+
+ if (mNextSelectedPosition >= 0) {
+ setSelectedPositionInt(mNextSelectedPosition);
+ }
+
+ recycleAllViews();
+
+ // Clear out old views
+ removeAllViewsInLayout();
+
+ // Make selected view and center it
+ mFirstPosition = mSelectedPosition;
+ View sel = makeAndAddView(mSelectedPosition);
+ int width = sel.getMeasuredWidth();
+ int selectedOffset = childrenLeft + (childrenWidth / 2) - (width / 2);
+ sel.offsetLeftAndRight(selectedOffset);
+
+ // Flush any cached views that did not get reused above
+ mRecycler.clear();
+
+ invalidate();
+
+ checkSelectionChanged();
+
+ mDataChanged = false;
+ mNeedSync = false;
+ setNextSelectedPositionInt(mSelectedPosition);
+ }
+
+ /**
+ * Obtain a view, either by pulling an existing view from the recycler or
+ * by getting a new one from the adapter. If we are animating, make sure
+ * there is enough information in the view's layout parameters to animate
+ * from the old to new positions.
+ *
+ * @param position Position in the spinner for the view to obtain
+ * @return A view that has been added to the spinner
+ */
+ private View makeAndAddView(int position) {
+
+ View child;
+
+ if (!mDataChanged) {
+ child = mRecycler.get(position);
+ if (child != null) {
+ // Position the view
+ setUpChild(child);
+
+ return child;
+ }
+ }
+
+ // Nothing found in the recycler -- ask the adapter for a view
+ child = mAdapter.getView(position, null, this);
+
+ // Position the view
+ setUpChild(child);
+
+ return child;
+ }
+
+
+
+ /**
+ * Helper for makeAndAddView to set the position of a view
+ * and fill out its layout paramters.
+ *
+ * @param child The view to position
+ */
+ private void setUpChild(View child) {
+
+ // Respect layout params that are already in the view. Otherwise
+ // make some up...
+ ViewGroup.LayoutParams lp = child.getLayoutParams();
+ if (lp == null) {
+ lp = generateDefaultLayoutParams();
+ }
+
+ addViewInLayout(child, 0, lp);
+
+ child.setSelected(hasFocus());
+
+ // Get measure specs
+ int childHeightSpec = ViewGroup.getChildMeasureSpec(mHeightMeasureSpec,
+ mSpinnerPadding.top + mSpinnerPadding.bottom, lp.height);
+ int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
+ mSpinnerPadding.left + mSpinnerPadding.right, lp.width);
+
+ // Measure child
+ child.measure(childWidthSpec, childHeightSpec);
+
+ int childLeft;
+ int childRight;
+
+ // Position vertically based on gravity setting
+ int childTop = mSpinnerPadding.top
+ + ((mMeasuredHeight - mSpinnerPadding.bottom -
+ mSpinnerPadding.top - child.getMeasuredHeight()) / 2);
+ int childBottom = childTop + child.getMeasuredHeight();
+
+ int width = child.getMeasuredWidth();
+ childLeft = 0;
+ childRight = childLeft + width;
+
+ child.layout(childLeft, childTop, childRight, childBottom);
+ }
+
+ @Override
+ public boolean performClick() {
+ boolean handled = super.performClick();
+
+ if (!handled) {
+ handled = true;
+ Context context = getContext();
+
+ final DropDownAdapter adapter = new DropDownAdapter(getAdapter());
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ if (mPrompt != null) {
+ builder.setTitle(mPrompt);
+ }
+ builder.setSingleChoiceItems(adapter, getSelectedItemPosition(), this).show();
+ }
+
+ return handled;
+ }
+
+ public void onClick(DialogInterface dialog, int which) {
+ setSelection(which);
+ dialog.dismiss();
+ }
+
+ /**
+ * Sets the prompt to display when the dialog is shown.
+ * @param prompt the prompt to set
+ */
+ public void setPrompt(CharSequence prompt) {
+ mPrompt = prompt;
+ }
+
+ /**
+ * Sets the prompt to display when the dialog is shown.
+ * @param promptId the resource ID of the prompt to display when the dialog is shown
+ */
+ public void setPromptId(int promptId) {
+ mPrompt = getContext().getText(promptId);
+ }
+
+ /**
+ * @return The prompt to display when the dialog is shown
+ */
+ public CharSequence getPrompt() {
+ return mPrompt;
+ }
+
+ /**
+ * <p>Wrapper class for an Adapter. Transforms the embedded Adapter instance
+ * into a ListAdapter.</p>
+ */
+ private static class DropDownAdapter implements ListAdapter, SpinnerAdapter {
+ private SpinnerAdapter mAdapter;
+
+ /**
+ * <p>Creates a new ListAddapter wrapper for the specified adapter.</p>
+ *
+ * @param adapter the Adapter to transform into a ListAdapter
+ */
+ public DropDownAdapter(SpinnerAdapter adapter) {
+ this.mAdapter = adapter;
+ }
+
+ public int getCount() {
+ return mAdapter == null ? 0 : mAdapter.getCount();
+ }
+
+ public Object getItem(int position) {
+ return mAdapter == null ? null : mAdapter.getItem(position);
+ }
+
+ public long getItemId(int position) {
+ return mAdapter == null ? -1 : mAdapter.getItemId(position);
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ return getDropDownView(position, convertView, parent);
+ }
+
+ public View getDropDownView(int position, View convertView, ViewGroup parent) {
+ return mAdapter == null ? null :
+ mAdapter.getDropDownView(position, convertView, parent);
+ }
+
+ public boolean hasStableIds() {
+ return mAdapter != null && mAdapter.hasStableIds();
+ }
+
+ public void registerDataSetObserver(DataSetObserver observer) {
+ if (mAdapter != null) {
+ mAdapter.registerDataSetObserver(observer);
+ }
+ }
+
+ public void unregisterDataSetObserver(DataSetObserver observer) {
+ if (mAdapter != null) {
+ mAdapter.unregisterDataSetObserver(observer);
+ }
+ }
+
+ /**
+ * <p>Always returns false.</p>
+ *
+ * @return false
+ */
+ public boolean areAllItemsEnabled() {
+ return true;
+ }
+
+ /**
+ * <p>Always returns false.</p>
+ *
+ * @return false
+ */
+ public boolean isEnabled(int position) {
+ return true;
+ }
+
+ public int getItemViewType(int position) {
+ return 0;
+ }
+
+ public int getViewTypeCount() {
+ return 1;
+ }
+
+ public boolean isEmpty() {
+ return getCount() == 0;
+ }
+ }
+}
diff --git a/core/java/android/widget/SpinnerAdapter.java b/core/java/android/widget/SpinnerAdapter.java
new file mode 100644
index 0000000..91504cf
--- /dev/null
+++ b/core/java/android/widget/SpinnerAdapter.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Extended {@link Adapter} that is the bridge between a
+ * {@link android.widget.Spinner} and its data. A spinner adapter allows to
+ * define two different views: one that shows the data in the spinner itself and
+ * one that shows the data in the drop down list when the spinner is pressed.</p>
+ */
+public interface SpinnerAdapter extends Adapter {
+ /**
+ * <p>Get a {@link android.view.View} that displays in the drop down popup
+ * the data at the specified position in the data set.</p>
+ *
+ * @param position index of the item whose view we want.
+ * @param convertView the old view to reuse, if possible. Note: You should
+ * check that this view is non-null and of an appropriate type before
+ * using. If it is not possible to convert this view to display the
+ * correct data, this method can create a new view.
+ * @param parent the parent that this view will eventually be attached to
+ * @return a {@link android.view.View} corresponding to the data at the
+ * specified position.
+ */
+ public View getDropDownView(int position, View convertView, ViewGroup parent);
+}
diff --git a/core/java/android/widget/TabHost.java b/core/java/android/widget/TabHost.java
new file mode 100644
index 0000000..da4a077
--- /dev/null
+++ b/core/java/android/widget/TabHost.java
@@ -0,0 +1,632 @@
+/*
+ * 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.widget;
+
+import android.app.LocalActivityManager;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.SoundEffectConstants;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.Window;
+import com.android.internal.R;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Container for a tabbed window view. This object holds two children: a set of tab labels that the
+ * user clicks to select a specific tab, and a FrameLayout object that displays the contents of that
+ * page. The individual elements are typically controlled using this container object, rather than
+ * setting values on the child elements themselves.
+ */
+public class TabHost extends FrameLayout implements ViewTreeObserver.OnTouchModeChangeListener {
+
+ private TabWidget mTabWidget;
+ private FrameLayout mTabContent;
+ private List<TabSpec> mTabSpecs = new ArrayList<TabSpec>(2);
+ /**
+ * This field should be made private, so it is hidden from the SDK.
+ * {@hide}
+ */
+ protected int mCurrentTab = -1;
+ private View mCurrentView = null;
+ /**
+ * This field should be made private, so it is hidden from the SDK.
+ * {@hide}
+ */
+ protected LocalActivityManager mLocalActivityManager = null;
+ private OnTabChangeListener mOnTabChangeListener;
+ private OnKeyListener mTabKeyListener;
+
+ public TabHost(Context context) {
+ super(context);
+ initTabHost();
+ }
+
+ public TabHost(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initTabHost();
+ }
+
+ private final void initTabHost() {
+ setFocusableInTouchMode(true);
+ setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
+
+ mCurrentTab = -1;
+ mCurrentView = null;
+ }
+
+ /**
+ * Get a new {@link TabSpec} associated with this tab host.
+ * @param tag required tag of tab.
+ */
+ public TabSpec newTabSpec(String tag) {
+ return new TabSpec(tag);
+ }
+
+
+
+ /**
+ * <p>Call setup() before adding tabs if loading TabHost using findViewById(). <i><b>However</i></b>: You do
+ * not need to call setup() after getTabHost() in {@link android.app.TabActivity TabActivity}.
+ * Example:</p>
+<pre>mTabHost = (TabHost)findViewById(R.id.tabhost);
+mTabHost.setup();
+mTabHost.addTab(TAB_TAG_1, "Hello, world!", "Tab 1");
+ */
+ public void setup() {
+ mTabWidget = (TabWidget) findViewById(com.android.internal.R.id.tabs);
+ if (mTabWidget == null) {
+ throw new RuntimeException(
+ "Your TabHost must have a TabWidget whose id attribute is 'android.R.id.tabs'");
+ }
+
+ // KeyListener to attach to all tabs. Detects non-navigation keys
+ // and relays them to the tab content.
+ mTabKeyListener = new OnKeyListener() {
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ case KeyEvent.KEYCODE_DPAD_UP:
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ case KeyEvent.KEYCODE_ENTER:
+ return false;
+
+ }
+ mTabContent.requestFocus(View.FOCUS_FORWARD);
+ return mTabContent.dispatchKeyEvent(event);
+ }
+
+ };
+
+ mTabWidget.setTabSelectionListener(new TabWidget.OnTabSelectionChanged() {
+ public void onTabSelectionChanged(int tabIndex, boolean clicked) {
+ setCurrentTab(tabIndex);
+ if (clicked) {
+ mTabContent.requestFocus(View.FOCUS_FORWARD);
+ }
+ }
+ });
+
+ mTabContent = (FrameLayout) findViewById(com.android.internal.R.id.tabcontent);
+ if (mTabContent == null) {
+ throw new RuntimeException(
+ "Your TabHost must have a FrameLayout whose id attribute is 'android.R.id.tabcontent'");
+ }
+ }
+
+ /**
+ * If you are using {@link TabSpec#setContent(android.content.Intent)}, this
+ * must be called since the activityGroup is needed to launch the local activity.
+ *
+ * This is done for you if you extend {@link android.app.TabActivity}.
+ * @param activityGroup Used to launch activities for tab content.
+ */
+ public void setup(LocalActivityManager activityGroup) {
+ setup();
+ mLocalActivityManager = activityGroup;
+ }
+
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ final ViewTreeObserver treeObserver = getViewTreeObserver();
+ if (treeObserver != null) {
+ treeObserver.addOnTouchModeChangeListener(this);
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ final ViewTreeObserver treeObserver = getViewTreeObserver();
+ if (treeObserver != null) {
+ treeObserver.removeOnTouchModeChangeListener(this);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void onTouchModeChanged(boolean isInTouchMode) {
+ if (!isInTouchMode) {
+ // leaving touch mode.. if nothing has focus, let's give it to
+ // the indicator of the current tab
+ if (!mCurrentView.hasFocus() || mCurrentView.isFocused()) {
+ mTabWidget.getChildAt(mCurrentTab).requestFocus();
+ }
+ }
+ }
+
+ /**
+ * Add a tab.
+ * @param tabSpec Specifies how to create the indicator and content.
+ */
+ public void addTab(TabSpec tabSpec) {
+
+ if (tabSpec.mIndicatorStrategy == null) {
+ throw new IllegalArgumentException("you must specify a way to create the tab indicator.");
+ }
+
+ if (tabSpec.mContentStrategy == null) {
+ throw new IllegalArgumentException("you must specify a way to create the tab content");
+ }
+ View tabIndicator = tabSpec.mIndicatorStrategy.createIndicatorView();
+ tabIndicator.setOnKeyListener(mTabKeyListener);
+ mTabWidget.addView(tabIndicator);
+ mTabSpecs.add(tabSpec);
+
+ if (mCurrentTab == -1) {
+ setCurrentTab(0);
+ }
+ }
+
+
+ /**
+ * Removes all tabs from the tab widget associated with this tab host.
+ */
+ public void clearAllTabs() {
+ mTabWidget.removeAllViews();
+ initTabHost();
+ mTabContent.removeAllViews();
+ mTabSpecs.clear();
+ requestLayout();
+ invalidate();
+ }
+
+ public TabWidget getTabWidget() {
+ return mTabWidget;
+ }
+
+ public int getCurrentTab() {
+ return mCurrentTab;
+ }
+
+ public String getCurrentTabTag() {
+ if (mCurrentTab >= 0 && mCurrentTab < mTabSpecs.size()) {
+ return mTabSpecs.get(mCurrentTab).getTag();
+ }
+ return null;
+ }
+
+ public View getCurrentTabView() {
+ if (mCurrentTab >= 0 && mCurrentTab < mTabSpecs.size()) {
+ return mTabWidget.getChildAt(mCurrentTab);
+ }
+ return null;
+ }
+
+ public View getCurrentView() {
+ return mCurrentView;
+ }
+
+ public void setCurrentTabByTag(String tag) {
+ int i;
+ for (i = 0; i < mTabSpecs.size(); i++) {
+ if (mTabSpecs.get(i).getTag().equals(tag)) {
+ setCurrentTab(i);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Get the FrameLayout which holds tab content
+ */
+ public FrameLayout getTabContentView() {
+ return mTabContent;
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ final boolean handled = super.dispatchKeyEvent(event);
+
+ // unhandled key ups change focus to tab indicator for embedded activities
+ // when there is nothing that will take focus from default focus searching
+ if (!handled
+ && (event.getAction() == KeyEvent.ACTION_DOWN)
+ && (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_UP)
+ && (mCurrentView.isRootNamespace())
+ && (mCurrentView.hasFocus())
+ && (mCurrentView.findFocus().focusSearch(View.FOCUS_UP) == null)) {
+ mTabWidget.getChildAt(mCurrentTab).requestFocus();
+ playSoundEffect(SoundEffectConstants.NAVIGATION_UP);
+ return true;
+ }
+ return handled;
+ }
+
+
+ @Override
+ public void dispatchWindowFocusChanged(boolean hasFocus) {
+ mCurrentView.dispatchWindowFocusChanged(hasFocus);
+ }
+
+ public void setCurrentTab(int index) {
+ if (index < 0 || index >= mTabSpecs.size()) {
+ return;
+ }
+
+ if (index == mCurrentTab) {
+ return;
+ }
+
+ // notify old tab content
+ if (mCurrentTab != -1) {
+ mTabSpecs.get(mCurrentTab).mContentStrategy.tabClosed();
+ }
+
+ mCurrentTab = index;
+ final TabHost.TabSpec spec = mTabSpecs.get(index);
+
+ // Call the tab widget's focusCurrentTab(), instead of just
+ // selecting the tab.
+ mTabWidget.focusCurrentTab(mCurrentTab);
+
+ // tab content
+ mCurrentView = spec.mContentStrategy.getContentView();
+
+ if (mCurrentView.getParent() == null) {
+ mTabContent
+ .addView(
+ mCurrentView,
+ new ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.FILL_PARENT,
+ ViewGroup.LayoutParams.FILL_PARENT));
+ }
+
+ if (!mTabWidget.hasFocus()) {
+ // if the tab widget didn't take focus (likely because we're in touch mode)
+ // give the current tab content view a shot
+ mCurrentView.requestFocus();
+ }
+
+ //mTabContent.requestFocus(View.FOCUS_FORWARD);
+ invokeOnTabChangeListener();
+ }
+
+ /**
+ * Register a callback to be invoked when the selected state of any of the items
+ * in this list changes
+ * @param l
+ * The callback that will run
+ */
+ public void setOnTabChangedListener(OnTabChangeListener l) {
+ mOnTabChangeListener = l;
+ }
+
+ private void invokeOnTabChangeListener() {
+ if (mOnTabChangeListener != null) {
+ mOnTabChangeListener.onTabChanged(getCurrentTabTag());
+ }
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when tab changed
+ */
+ public interface OnTabChangeListener {
+ void onTabChanged(String tabId);
+ }
+
+
+ /**
+ * Makes the content of a tab when it is selected. Use this if your tab
+ * content needs to be created on demand, i.e. you are not showing an
+ * existing view or starting an activity.
+ */
+ public interface TabContentFactory {
+ /**
+ * Callback to make the tab contents
+ *
+ * @param tag
+ * Which tab was selected.
+ * @return The view to distplay the contents of the selected tab.
+ */
+ View createTabContent(String tag);
+ }
+
+
+ /**
+ * A tab has a tab indictor, content, and a tag that is used to keep
+ * track of it. This builder helps choose among these options.
+ *
+ * For the tab indicator, your choices are:
+ * 1) set a label
+ * 2) set a label and an icon
+ *
+ * For the tab content, your choices are:
+ * 1) the id of a {@link View}
+ * 2) a {@link TabContentFactory} that creates the {@link View} content.
+ * 3) an {@link Intent} that launches an {@link android.app.Activity}.
+ */
+ public class TabSpec {
+
+ private String mTag;
+
+ private IndicatorStrategy mIndicatorStrategy;
+ private ContentStrategy mContentStrategy;
+
+ private TabSpec(String tag) {
+ mTag = tag;
+ }
+
+ /**
+ * Specify a label as the tab indicator.
+ */
+ public TabSpec setIndicator(CharSequence label) {
+ mIndicatorStrategy = new LabelIndicatorStrategy(label);
+ return this;
+ }
+
+ /**
+ * Specify a label and icon as the tab indicator.
+ */
+ public TabSpec setIndicator(CharSequence label, Drawable icon) {
+ mIndicatorStrategy = new LabelAndIconIndicatorStategy(label, icon);
+ return this;
+ }
+
+ /**
+ * Specify the id of the view that should be used as the content
+ * of the tab.
+ */
+ public TabSpec setContent(int viewId) {
+ mContentStrategy = new ViewIdContentStrategy(viewId);
+ return this;
+ }
+
+ /**
+ * Specify a {@link android.widget.TabHost.TabContentFactory} to use to
+ * create the content of the tab.
+ */
+ public TabSpec setContent(TabContentFactory contentFactory) {
+ mContentStrategy = new FactoryContentStrategy(mTag, contentFactory);
+ return this;
+ }
+
+ /**
+ * Specify an intent to use to launch an activity as the tab content.
+ */
+ public TabSpec setContent(Intent intent) {
+ mContentStrategy = new IntentContentStrategy(mTag, intent);
+ return this;
+ }
+
+
+ String getTag() {
+ return mTag;
+ }
+ }
+
+ /**
+ * Specifies what you do to create a tab indicator.
+ */
+ private static interface IndicatorStrategy {
+
+ /**
+ * Return the view for the indicator.
+ */
+ View createIndicatorView();
+ }
+
+ /**
+ * Specifies what you do to manage the tab content.
+ */
+ private static interface ContentStrategy {
+
+ /**
+ * Return the content view. The view should may be cached locally.
+ */
+ View getContentView();
+
+ /**
+ * Perhaps do something when the tab associated with this content has
+ * been closed (i.e make it invisible, or remove it).
+ */
+ void tabClosed();
+ }
+
+ /**
+ * How to create a tab indicator that just has a label.
+ */
+ private class LabelIndicatorStrategy implements IndicatorStrategy {
+
+ private final CharSequence mLabel;
+
+ private LabelIndicatorStrategy(CharSequence label) {
+ mLabel = label;
+ }
+
+ public View createIndicatorView() {
+ LayoutInflater inflater =
+ (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View tabIndicator = inflater.inflate(R.layout.tab_indicator,
+ mTabWidget, // tab widget is the parent
+ false); // no inflate params
+
+ final TextView tv = (TextView) tabIndicator.findViewById(R.id.title);
+ tv.setText(mLabel);
+
+ return tabIndicator;
+ }
+ }
+
+ /**
+ * How we create a tab indicator that has a label and an icon
+ */
+ private class LabelAndIconIndicatorStategy implements IndicatorStrategy {
+
+ private final CharSequence mLabel;
+ private final Drawable mIcon;
+
+ private LabelAndIconIndicatorStategy(CharSequence label, Drawable icon) {
+ mLabel = label;
+ mIcon = icon;
+ }
+
+ public View createIndicatorView() {
+ LayoutInflater inflater =
+ (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View tabIndicator = inflater.inflate(R.layout.tab_indicator,
+ mTabWidget, // tab widget is the parent
+ false); // no inflate params
+
+ final TextView tv = (TextView) tabIndicator.findViewById(R.id.title);
+ tv.setText(mLabel);
+
+ final ImageView iconView = (ImageView) tabIndicator.findViewById(R.id.icon);
+ iconView.setImageDrawable(mIcon);
+
+ return tabIndicator;
+ }
+ }
+
+ /**
+ * How to create the tab content via a view id.
+ */
+ private class ViewIdContentStrategy implements ContentStrategy {
+
+ private final View mView;
+
+ private ViewIdContentStrategy(int viewId) {
+ mView = mTabContent.findViewById(viewId);
+ if (mView != null) {
+ mView.setVisibility(View.GONE);
+ } else {
+ throw new RuntimeException("Could not create tab content because " +
+ "could not find view with id " + viewId);
+ }
+ }
+
+ public View getContentView() {
+ mView.setVisibility(View.VISIBLE);
+ return mView;
+ }
+
+ public void tabClosed() {
+ mView.setVisibility(View.GONE);
+ }
+ }
+
+ /**
+ * How tab content is managed using {@link TabContentFactory}.
+ */
+ private class FactoryContentStrategy implements ContentStrategy {
+ private View mTabContent;
+ private final CharSequence mTag;
+ private TabContentFactory mFactory;
+
+ public FactoryContentStrategy(CharSequence tag, TabContentFactory factory) {
+ mTag = tag;
+ mFactory = factory;
+ }
+
+ public View getContentView() {
+ if (mTabContent == null) {
+ mTabContent = mFactory.createTabContent(mTag.toString());
+ }
+ mTabContent.setVisibility(View.VISIBLE);
+ return mTabContent;
+ }
+
+ public void tabClosed() {
+ mTabContent.setVisibility(View.INVISIBLE);
+ }
+ }
+
+ /**
+ * How tab content is managed via an {@link Intent}: the content view is the
+ * decorview of the launched activity.
+ */
+ private class IntentContentStrategy implements ContentStrategy {
+
+ private final String mTag;
+ private final Intent mIntent;
+
+ private View mLaunchedView;
+
+ private IntentContentStrategy(String tag, Intent intent) {
+ mTag = tag;
+ mIntent = intent;
+ }
+
+ public View getContentView() {
+ if (mLocalActivityManager == null) {
+ throw new IllegalStateException("Did you forget to call 'public void setup(LocalActivityManager activityGroup)'?");
+ }
+ final Window w = mLocalActivityManager.startActivity(
+ mTag, mIntent);
+ final View wd = w != null ? w.getDecorView() : null;
+ if (mLaunchedView != wd && mLaunchedView != null) {
+ if (mLaunchedView.getParent() != null) {
+ mTabContent.removeView(mLaunchedView);
+ }
+ }
+ mLaunchedView = wd;
+
+ // XXX Set FOCUS_AFTER_DESCENDANTS on embedded activies for now so they can get
+ // focus if none of their children have it. They need focus to be able to
+ // display menu items.
+ //
+ // Replace this with something better when Bug 628886 is fixed...
+ //
+ if (mLaunchedView != null) {
+ mLaunchedView.setVisibility(View.VISIBLE);
+ mLaunchedView.setFocusableInTouchMode(true);
+ ((ViewGroup) mLaunchedView).setDescendantFocusability(
+ FOCUS_AFTER_DESCENDANTS);
+ }
+ return mLaunchedView;
+ }
+
+ public void tabClosed() {
+ if (mLaunchedView != null) {
+ mLaunchedView.setVisibility(View.GONE);
+ }
+ }
+ }
+
+}
diff --git a/core/java/android/widget/TabWidget.java b/core/java/android/widget/TabWidget.java
new file mode 100644
index 0000000..20cddcb
--- /dev/null
+++ b/core/java/android/widget/TabWidget.java
@@ -0,0 +1,289 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.View.OnFocusChangeListener;
+
+
+
+/**
+ *
+ * Displays a list of tab labels representing each page in the parent's tab
+ * collection. The container object for this widget is
+ * {@link android.widget.TabHost TabHost}. When the user selects a tab, this
+ * object sends a message to the parent container, TabHost, to tell it to switch
+ * the displayed page. You typically won't use many methods directly on this
+ * object. The container TabHost is used to add labels, add the callback
+ * handler, and manage callbacks. You might call this object to iterate the list
+ * of tabs, or to tweak the layout of the tab list, but most methods should be
+ * called on the containing TabHost object.
+ */
+public class TabWidget extends LinearLayout implements OnFocusChangeListener {
+
+
+ private OnTabSelectionChanged mSelectionChangedListener;
+ private int mSelectedTab = 0;
+ private Drawable mBottomLeftStrip;
+ private Drawable mBottomRightStrip;
+ private boolean mStripMoved;
+
+ public TabWidget(Context context) {
+ this(context, null);
+ }
+
+ public TabWidget(Context context, AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.tabWidgetStyle);
+ }
+
+ public TabWidget(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs);
+ initTabWidget();
+
+ TypedArray a =
+ context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.TabWidget,
+ defStyle, 0);
+
+ a.recycle();
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ mStripMoved = true;
+ super.onSizeChanged(w, h, oldw, oldh);
+ }
+
+ private void initTabWidget() {
+ setOrientation(LinearLayout.HORIZONTAL);
+ mBottomLeftStrip = mContext.getResources().getDrawable(
+ com.android.internal.R.drawable.tab_bottom_left);
+ mBottomRightStrip = mContext.getResources().getDrawable(
+ com.android.internal.R.drawable.tab_bottom_right);
+ // Deal with focus, as we don't want the focus to go by default
+ // to a tab other than the current tab
+ setFocusable(true);
+ setOnFocusChangeListener(this);
+ }
+
+ @Override
+ public void childDrawableStateChanged(View child) {
+ if (child == getChildAt(mSelectedTab)) {
+ // To make sure that the bottom strip is redrawn
+ invalidate();
+ }
+ super.childDrawableStateChanged(child);
+ }
+
+ @Override
+ public void dispatchDraw(Canvas canvas) {
+ super.dispatchDraw(canvas);
+
+ View selectedChild = getChildAt(mSelectedTab);
+
+ mBottomLeftStrip.setState(selectedChild.getDrawableState());
+ mBottomRightStrip.setState(selectedChild.getDrawableState());
+
+ if (mStripMoved) {
+ Rect selBounds = new Rect(); // Bounds of the selected tab indicator
+ selBounds.left = selectedChild.getLeft();
+ selBounds.right = selectedChild.getRight();
+ final int myHeight = getHeight();
+ mBottomLeftStrip.setBounds(
+ Math.min(0, selBounds.left
+ - mBottomLeftStrip.getIntrinsicWidth()),
+ myHeight - mBottomLeftStrip.getIntrinsicHeight(),
+ selBounds.left,
+ getHeight());
+ mBottomRightStrip.setBounds(
+ selBounds.right,
+ myHeight - mBottomRightStrip.getIntrinsicHeight(),
+ Math.max(getWidth(),
+ selBounds.right + mBottomRightStrip.getIntrinsicWidth()),
+ myHeight);
+ mStripMoved = false;
+ }
+
+ mBottomLeftStrip.draw(canvas);
+ mBottomRightStrip.draw(canvas);
+ }
+
+ /**
+ * Sets the current tab.
+ * This method is used to bring a tab to the front of the Widget,
+ * and is used to post to the rest of the UI that a different tab
+ * has been brought to the foreground.
+ *
+ * Note, this is separate from the traditional "focus" that is
+ * employed from the view logic.
+ *
+ * For instance, if we have a list in a tabbed view, a user may be
+ * navigating up and down the list, moving the UI focus (orange
+ * highlighting) through the list items. The cursor movement does
+ * not effect the "selected" tab though, because what is being
+ * scrolled through is all on the same tab. The selected tab only
+ * changes when we navigate between tabs (moving from the list view
+ * to the next tabbed view, in this example).
+ *
+ * To move both the focus AND the selected tab at once, please use
+ * {@link #setCurrentTab}. Normally, the view logic takes care of
+ * adjusting the focus, so unless you're circumventing the UI,
+ * you'll probably just focus your interest here.
+ *
+ * @param index The tab that you want to indicate as the selected
+ * tab (tab brought to the front of the widget)
+ *
+ * @see #focusCurrentTab
+ */
+ public void setCurrentTab(int index) {
+ if (index < 0 || index >= getChildCount()) {
+ return;
+ }
+
+ getChildAt(mSelectedTab).setSelected(false);
+ mSelectedTab = index;
+ getChildAt(mSelectedTab).setSelected(true);
+ mStripMoved = true;
+ }
+
+ /**
+ * Sets the current tab and focuses the UI on it.
+ * This method makes sure that the focused tab matches the selected
+ * tab, normally at {@link #setCurrentTab}. Normally this would not
+ * be an issue if we go through the UI, since the UI is responsible
+ * for calling TabWidget.onFocusChanged(), but in the case where we
+ * are selecting the tab programmatically, we'll need to make sure
+ * focus keeps up.
+ *
+ * @param index The tab that you want focused (highlighted in orange)
+ * and selected (tab brought to the front of the widget)
+ *
+ * @see #setCurrentTab
+ */
+ public void focusCurrentTab(int index) {
+ final int oldTab = mSelectedTab;
+
+ // set the tab
+ setCurrentTab(index);
+
+ // change the focus if applicable.
+ if (oldTab != index) {
+ getChildAt(index).requestFocus();
+ }
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ super.setEnabled(enabled);
+ int count = getChildCount();
+
+ for (int i=0; i<count; i++) {
+ View child = getChildAt(i);
+ child.setEnabled(enabled);
+ }
+ }
+
+ @Override
+ public void addView(View child) {
+ if (child.getLayoutParams() == null) {
+ final LinearLayout.LayoutParams lp = new LayoutParams(
+ 0,
+ ViewGroup.LayoutParams.WRAP_CONTENT, 1);
+ lp.setMargins(0, 0, 0, 0);
+ child.setLayoutParams(lp);
+ }
+
+ // Ensure you can navigate to the tab with the keyboard, and you can touch it
+ child.setFocusable(true);
+ child.setClickable(true);
+
+ super.addView(child);
+
+ // TODO: detect this via geometry with a tabwidget listener rather
+ // than potentially interfere with the view's listener
+ child.setOnClickListener(new TabClickListener(getChildCount() - 1));
+ child.setOnFocusChangeListener(this);
+ }
+
+
+
+
+ /**
+ * Provides a way for {@link TabHost} to be notified that the user clicked on a tab indicator.
+ */
+ void setTabSelectionListener(OnTabSelectionChanged listener) {
+ mSelectionChangedListener = listener;
+ }
+
+ public void onFocusChange(View v, boolean hasFocus) {
+ if (v == this && hasFocus) {
+ getChildAt(mSelectedTab).requestFocus();
+ return;
+ }
+
+ if (hasFocus) {
+ int i = 0;
+ while (i < getChildCount()) {
+ if (getChildAt(i) == v) {
+ setCurrentTab(i);
+ mSelectionChangedListener.onTabSelectionChanged(i, false);
+ break;
+ }
+ i++;
+ }
+ }
+ }
+
+ // registered with each tab indicator so we can notify tab host
+ private class TabClickListener implements OnClickListener {
+
+ private final int mTabIndex;
+
+ private TabClickListener(int tabIndex) {
+ mTabIndex = tabIndex;
+ }
+
+ public void onClick(View v) {
+ mSelectionChangedListener.onTabSelectionChanged(mTabIndex, true);
+ }
+ }
+
+ /**
+ * Let {@link TabHost} know that the user clicked on a tab indicator.
+ */
+ static interface OnTabSelectionChanged {
+ /**
+ * Informs the TabHost which tab was selected. It also indicates
+ * if the tab was clicked/pressed or just focused into.
+ *
+ * @param tabIndex index of the tab that was selected
+ * @param clicked whether the selection changed due to a touch/click
+ * or due to focus entering the tab through navigation. Pass true
+ * if it was due to a press/click and false otherwise.
+ */
+ void onTabSelectionChanged(int tabIndex, boolean clicked);
+ }
+
+}
+
diff --git a/core/java/android/widget/TableLayout.java b/core/java/android/widget/TableLayout.java
new file mode 100644
index 0000000..d72ffb1
--- /dev/null
+++ b/core/java/android/widget/TableLayout.java
@@ -0,0 +1,755 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import com.android.internal.R;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.util.SparseBooleanArray;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.regex.Pattern;
+
+/**
+ * <p>A layout that arranges its children into rows and columns.
+ * A TableLayout consists of a number of {@link android.widget.TableRow} objects,
+ * each defining a row (actually, you can have other children, which will be
+ * explained below). TableLayout containers do not display border lines for
+ * their rows, columns, or cells. Each row has zero or more cells; each cell can
+ * hold one {@link android.view.View View} object. The table has as many columns
+ * as the row with the most cells. A table can leave cells empty. Cells can span
+ * columns, as they can in HTML.</p>
+ *
+ * <p>The width of a column is defined by the row with the widest cell in that
+ * column. However, a TableLayout can specify certain columns as shrinkable or
+ * stretchable by calling
+ * {@link #setColumnShrinkable(int, boolean) setColumnShrinkable()}
+ * or {@link #setColumnStretchable(int, boolean) setColumnStretchable()}. If
+ * marked as shrinkable, the column width can be shrunk to fit the table into
+ * its parent object. If marked as stretchable, it can expand in width to fit
+ * any extra space. The total width of the table is defined by its parent
+ * container. It is important to remember that a column can be both shrinkable
+ * and stretchable. In such a situation, the column will change its size to
+ * always use up the available space, but never more. Finally, you can hide a
+ * column by calling
+ * {@link #setColumnCollapsed(int,boolean) setColumnCollapsed()}.</p>
+ *
+ * <p>The children of a TableLayout cannot specify the <code>layout_width</code>
+ * attribute. Width is always <code>FILL_PARENT</code>. However, the
+ * <code>layout_height</code> attribute can be defined by a child; default value
+ * is {@link android.widget.TableLayout.LayoutParams#WRAP_CONTENT}. If the child
+ * is a {@link android.widget.TableRow}, then the height is always
+ * {@link android.widget.TableLayout.LayoutParams#WRAP_CONTENT}.</p>
+ *
+ * <p> Cells must be added to a row in increasing column order, both in code and
+ * XML. Column numbers are zero-based. If you don't specify a column number for
+ * a child cell, it will autoincrement to the next available column. If you skip
+ * a column number, it will be considered an empty cell in that row. See the
+ * TableLayout examples in ApiDemos for examples of creating tables in XML.</p>
+ *
+ * <p>Although the typical child of a TableLayout is a TableRow, you can
+ * actually use any View subclass as a direct child of TableLayout. The View
+ * will be displayed as a single row that spans all the table columns.</p>
+ */
+public class TableLayout extends LinearLayout {
+ private int[] mMaxWidths;
+ private SparseBooleanArray mStretchableColumns;
+ private SparseBooleanArray mShrinkableColumns;
+ private SparseBooleanArray mCollapsedColumns;
+
+ private boolean mShrinkAllColumns;
+ private boolean mStretchAllColumns;
+
+ private TableLayout.PassThroughHierarchyChangeListener mPassThroughListener;
+
+ private boolean mInitialized;
+
+ /**
+ * <p>Creates a new TableLayout for the given context.</p>
+ *
+ * @param context the application environment
+ */
+ public TableLayout(Context context) {
+ super(context);
+ initTableLayout();
+ }
+
+ /**
+ * <p>Creates a new TableLayout for the given context and with the
+ * specified set attributes.</p>
+ *
+ * @param context the application environment
+ * @param attrs a collection of attributes
+ */
+ public TableLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ TypedArray a =
+ context.obtainStyledAttributes(attrs, R.styleable.TableLayout);
+
+ String stretchedColumns =
+ a.getString(R.styleable.TableLayout_stretchColumns);
+ if (stretchedColumns != null) {
+ if (stretchedColumns.charAt(0) == '*') {
+ mStretchAllColumns = true;
+ } else {
+ mStretchableColumns = parseColumns(stretchedColumns);
+ }
+ }
+
+ String shrinkedColumns =
+ a.getString(R.styleable.TableLayout_shrinkColumns);
+ if (shrinkedColumns != null) {
+ if (shrinkedColumns.charAt(0) == '*') {
+ mShrinkAllColumns = true;
+ } else {
+ mShrinkableColumns = parseColumns(shrinkedColumns);
+ }
+ }
+
+ String collapsedColumns =
+ a.getString(R.styleable.TableLayout_collapseColumns);
+ if (collapsedColumns != null) {
+ mCollapsedColumns = parseColumns(collapsedColumns);
+ }
+
+ a.recycle();
+ initTableLayout();
+ }
+
+ /**
+ * <p>Parses a sequence of columns ids defined in a CharSequence with the
+ * following pattern (regex): \d+(\s*,\s*\d+)*</p>
+ *
+ * <p>Examples: "1" or "13, 7, 6" or "".</p>
+ *
+ * <p>The result of the parsing is stored in a sparse boolean array. The
+ * parsed column ids are used as the keys of the sparse array. The values
+ * are always true.</p>
+ *
+ * @param sequence a sequence of column ids, can be empty but not null
+ * @return a sparse array of boolean mapping column indexes to the columns
+ * collapse state
+ */
+ private static SparseBooleanArray parseColumns(String sequence) {
+ SparseBooleanArray columns = new SparseBooleanArray();
+ Pattern pattern = Pattern.compile("\\s*,\\s*");
+ String[] columnDefs = pattern.split(sequence);
+
+ for (String columnIdentifier : columnDefs) {
+ try {
+ int columnIndex = Integer.parseInt(columnIdentifier);
+ // only valid, i.e. positive, columns indexes are handled
+ if (columnIndex >= 0) {
+ // putting true in this sparse array indicates that the
+ // column index was defined in the XML file
+ columns.put(columnIndex, true);
+ }
+ } catch (NumberFormatException e) {
+ // we just ignore columns that don't exist
+ }
+ }
+
+ return columns;
+ }
+
+ /**
+ * <p>Performs initialization common to prorgrammatic use and XML use of
+ * this widget.</p>
+ */
+ private void initTableLayout() {
+ if (mCollapsedColumns == null) {
+ mCollapsedColumns = new SparseBooleanArray();
+ }
+ if (mStretchableColumns == null) {
+ mStretchableColumns = new SparseBooleanArray();
+ }
+ if (mShrinkableColumns == null) {
+ mShrinkableColumns = new SparseBooleanArray();
+ }
+
+ mPassThroughListener = new PassThroughHierarchyChangeListener();
+ // make sure to call the parent class method to avoid potential
+ // infinite loops
+ super.setOnHierarchyChangeListener(mPassThroughListener);
+
+ mInitialized = true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setOnHierarchyChangeListener(
+ OnHierarchyChangeListener listener) {
+ // the user listener is delegated to our pass-through listener
+ mPassThroughListener.mOnHierarchyChangeListener = listener;
+ }
+
+ private void requestRowsLayout() {
+ if (mInitialized) {
+ final int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ getChildAt(i).requestLayout();
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void requestLayout() {
+ if (mInitialized) {
+ int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ getChildAt(i).forceLayout();
+ }
+ }
+
+ super.requestLayout();
+ }
+
+ /**
+ * <p>Indicates whether all columns are shrinkable or not.</p>
+ *
+ * @return true if all columns are shrinkable, false otherwise
+ */
+ public boolean isShrinkAllColumns() {
+ return mShrinkAllColumns;
+ }
+
+ /**
+ * <p>Convenience method to mark all columns as shrinkable.</p>
+ *
+ * @param shrinkAllColumns true to mark all columns shrinkable
+ *
+ * @attr ref android.R.styleable#TableLayout_shrinkColumns
+ */
+ public void setShrinkAllColumns(boolean shrinkAllColumns) {
+ mShrinkAllColumns = shrinkAllColumns;
+ }
+
+ /**
+ * <p>Indicates whether all columns are stretchable or not.</p>
+ *
+ * @return true if all columns are stretchable, false otherwise
+ */
+ public boolean isStretchAllColumns() {
+ return mStretchAllColumns;
+ }
+
+ /**
+ * <p>Convenience method to mark all columns as stretchable.</p>
+ *
+ * @param stretchAllColumns true to mark all columns stretchable
+ *
+ * @attr ref android.R.styleable#TableLayout_stretchColumns
+ */
+ public void setStretchAllColumns(boolean stretchAllColumns) {
+ mStretchAllColumns = stretchAllColumns;
+ }
+
+ /**
+ * <p>Collapses or restores a given column. When collapsed, a column
+ * does not appear on screen and the extra space is reclaimed by the
+ * other columns. A column is collapsed/restored only when it belongs to
+ * a {@link android.widget.TableRow}.</p>
+ *
+ * <p>Calling this method requests a layout operation.</p>
+ *
+ * @param columnIndex the index of the column
+ * @param isCollapsed true if the column must be collapsed, false otherwise
+ *
+ * @attr ref android.R.styleable#TableLayout_collapseColumns
+ */
+ public void setColumnCollapsed(int columnIndex, boolean isCollapsed) {
+ // update the collapse status of the column
+ mCollapsedColumns.put(columnIndex, isCollapsed);
+
+ int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ final View view = getChildAt(i);
+ if (view instanceof TableRow) {
+ ((TableRow) view).setColumnCollapsed(columnIndex, isCollapsed);
+ }
+ }
+
+ requestRowsLayout();
+ }
+
+ /**
+ * <p>Returns the collapsed state of the specified column.</p>
+ *
+ * @param columnIndex the index of the column
+ * @return true if the column is collapsed, false otherwise
+ */
+ public boolean isColumnCollapsed(int columnIndex) {
+ return mCollapsedColumns.get(columnIndex);
+ }
+
+ /**
+ * <p>Makes the given column stretchable or not. When stretchable, a column
+ * takes up as much as available space as possible in its row.</p>
+ *
+ * <p>Calling this method requests a layout operation.</p>
+ *
+ * @param columnIndex the index of the column
+ * @param isStretchable true if the column must be stretchable,
+ * false otherwise. Default is false.
+ *
+ * @attr ref android.R.styleable#TableLayout_stretchColumns
+ */
+ public void setColumnStretchable(int columnIndex, boolean isStretchable) {
+ mStretchableColumns.put(columnIndex, isStretchable);
+ requestRowsLayout();
+ }
+
+ /**
+ * <p>Returns whether the specified column is stretchable or not.</p>
+ *
+ * @param columnIndex the index of the column
+ * @return true if the column is stretchable, false otherwise
+ */
+ public boolean isColumnStretchable(int columnIndex) {
+ return mStretchAllColumns || mStretchableColumns.get(columnIndex);
+ }
+
+ /**
+ * <p>Makes the given column shrinkable or not. When a row is too wide, the
+ * table can reclaim extra space from shrinkable columns.</p>
+ *
+ * <p>Calling this method requests a layout operation.</p>
+ *
+ * @param columnIndex the index of the column
+ * @param isShrinkable true if the column must be shrinkable,
+ * false otherwise. Default is false.
+ *
+ * @attr ref android.R.styleable#TableLayout_shrinkColumns
+ */
+ public void setColumnShrinkable(int columnIndex, boolean isShrinkable) {
+ mShrinkableColumns.put(columnIndex, isShrinkable);
+ requestRowsLayout();
+ }
+
+ /**
+ * <p>Returns whether the specified column is shrinkable or not.</p>
+ *
+ * @param columnIndex the index of the column
+ * @return true if the column is shrinkable, false otherwise. Default is false.
+ */
+ public boolean isColumnShrinkable(int columnIndex) {
+ return mShrinkAllColumns || mStretchableColumns.get(columnIndex);
+ }
+
+ /**
+ * <p>Applies the columns collapse status to a new row added to this
+ * table. This method is invoked by PassThroughHierarchyChangeListener
+ * upon child insertion.</p>
+ *
+ * <p>This method only applies to {@link android.widget.TableRow}
+ * instances.</p>
+ *
+ * @param child the newly added child
+ */
+ private void trackCollapsedColumns(View child) {
+ if (child instanceof TableRow) {
+ final TableRow row = (TableRow) child;
+ final SparseBooleanArray collapsedColumns = mCollapsedColumns;
+ final int count = collapsedColumns.size();
+ for (int i = 0; i < count; i++) {
+ int columnIndex = collapsedColumns.keyAt(i);
+ boolean isCollapsed = collapsedColumns.valueAt(i);
+ // the collapse status is set only when the column should be
+ // collapsed; otherwise, this might affect the default
+ // visibility of the row's children
+ if (isCollapsed) {
+ row.setColumnCollapsed(columnIndex, isCollapsed);
+ }
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void addView(View child) {
+ super.addView(child);
+ requestRowsLayout();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void addView(View child, int index) {
+ super.addView(child, index);
+ requestRowsLayout();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void addView(View child, ViewGroup.LayoutParams params) {
+ super.addView(child, params);
+ requestRowsLayout();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void addView(View child, int index, ViewGroup.LayoutParams params) {
+ super.addView(child, index, params);
+ requestRowsLayout();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ // enforce vertical layout
+ measureVertical(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ // enforce vertical layout
+ layoutVertical();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ void measureChildBeforeLayout(View child, int childIndex,
+ int widthMeasureSpec, int totalWidth,
+ int heightMeasureSpec, int totalHeight) {
+ // when the measured child is a table row, we force the width of its
+ // children with the widths computed in findLargestCells()
+ if (child instanceof TableRow) {
+ ((TableRow) child).setColumnsWidthConstraints(mMaxWidths);
+ }
+
+ super.measureChildBeforeLayout(child, childIndex,
+ widthMeasureSpec, totalWidth, heightMeasureSpec, totalHeight);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
+ findLargestCells(widthMeasureSpec);
+ shrinkAndStretchColumns(widthMeasureSpec);
+
+ super.measureVertical(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ /**
+ * <p>Finds the largest cell in each column. For each column, the width of
+ * the largest cell is applied to all the other cells.</p>
+ *
+ * @param widthMeasureSpec the measure constraint imposed by our parent
+ */
+ private void findLargestCells(int widthMeasureSpec) {
+ boolean firstRow = true;
+
+ // find the maximum width for each column
+ // the total number of columns is dynamically changed if we find
+ // wider rows as we go through the children
+ // the array is reused for each layout operation; the array can grow
+ // but never shrinks. Unused extra cells in the array are just ignored
+ // this behavior avoids to unnecessary grow the array after the first
+ // layout operation
+ final int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() == GONE) {
+ continue;
+ }
+
+ if (child instanceof TableRow) {
+ final TableRow row = (TableRow) child;
+ // forces the row's height
+ final ViewGroup.LayoutParams layoutParams = row.getLayoutParams();
+ layoutParams.height = LayoutParams.WRAP_CONTENT;
+
+ final int[] widths = row.getColumnsWidths(widthMeasureSpec);
+ final int newLength = widths.length;
+ // this is the first row, we just need to copy the values
+ if (firstRow) {
+ if (mMaxWidths == null || mMaxWidths.length != newLength) {
+ mMaxWidths = new int[newLength];
+ }
+ System.arraycopy(widths, 0, mMaxWidths, 0, newLength);
+ firstRow = false;
+ } else {
+ int length = mMaxWidths.length;
+ final int difference = newLength - length;
+ // the current row is wider than the previous rows, so
+ // we just grow the array and copy the values
+ if (difference > 0) {
+ final int[] oldMaxWidths = mMaxWidths;
+ mMaxWidths = new int[newLength];
+ System.arraycopy(oldMaxWidths, 0, mMaxWidths, 0,
+ oldMaxWidths.length);
+ System.arraycopy(widths, oldMaxWidths.length,
+ mMaxWidths, oldMaxWidths.length, difference);
+ }
+
+ // the row is narrower or of the same width as the previous
+ // rows, so we find the maximum width for each column
+ // if the row is narrower than the previous ones,
+ // difference will be negative
+ final int[] maxWidths = mMaxWidths;
+ length = Math.min(length, newLength);
+ for (int j = 0; j < length; j++) {
+ maxWidths[j] = Math.max(maxWidths[j], widths[j]);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * <p>Shrinks the columns if their total width is greater than the
+ * width allocated by widthMeasureSpec. When the total width is less
+ * than the allocated width, this method attempts to stretch columns
+ * to fill the remaining space.</p>
+ *
+ * @param widthMeasureSpec the width measure specification as indicated
+ * by this widget's parent
+ */
+ private void shrinkAndStretchColumns(int widthMeasureSpec) {
+ // when we have no row, mMaxWidths is not initialized and the loop
+ // below could cause a NPE
+ if (mMaxWidths == null) {
+ return;
+ }
+
+ // should we honor AT_MOST, EXACTLY and UNSPECIFIED?
+ int totalWidth = 0;
+ for (int width : mMaxWidths) {
+ totalWidth += width;
+ }
+
+ int size = MeasureSpec.getSize(widthMeasureSpec) - mPaddingLeft - mPaddingRight;
+
+ if ((totalWidth > size) && (mShrinkAllColumns || mShrinkableColumns.size() > 0)) {
+ // oops, the largest columns are wider than the row itself
+ // fairly redistribute the row's widh among the columns
+ mutateColumnsWidth(mShrinkableColumns, mShrinkAllColumns, size, totalWidth);
+ } else if ((totalWidth < size) && (mStretchAllColumns || mStretchableColumns.size() > 0)) {
+ // if we have some space left, we distribute it among the
+ // expandable columns
+ mutateColumnsWidth(mStretchableColumns, mStretchAllColumns, size, totalWidth);
+ }
+ }
+
+ private void mutateColumnsWidth(SparseBooleanArray columns,
+ boolean allColumns, int size, int totalWidth) {
+ int skipped = 0;
+ final int[] maxWidths = mMaxWidths;
+ final int length = maxWidths.length;
+ final int count = allColumns ? length : columns.size();
+ final int totalExtraSpace = size - totalWidth;
+ int extraSpace = totalExtraSpace / count;
+
+ if (!allColumns) {
+ for (int i = 0; i < count; i++) {
+ int column = columns.keyAt(i);
+ if (columns.valueAt(i)) {
+ if (column < length) {
+ maxWidths[column] += extraSpace;
+ } else {
+ skipped++;
+ }
+ }
+ }
+ } else {
+ for (int i = 0; i < count; i++) {
+ maxWidths[i] += extraSpace;
+ }
+
+ // we don't skip any column so we can return right away
+ return;
+ }
+
+ if (skipped > 0 && skipped < count) {
+ // reclaim any extra space we left to columns that don't exist
+ extraSpace = skipped * extraSpace / (count - skipped);
+ for (int i = 0; i < count; i++) {
+ int column = columns.keyAt(i);
+ if (columns.valueAt(i) && column < length) {
+ if (extraSpace > maxWidths[column]) {
+ maxWidths[column] = 0;
+ } else {
+ maxWidths[column] += extraSpace;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public LayoutParams generateLayoutParams(AttributeSet attrs) {
+ return new TableLayout.LayoutParams(getContext(), attrs);
+ }
+
+ /**
+ * Returns a set of layout parameters with a width of
+ * {@link android.view.ViewGroup.LayoutParams#FILL_PARENT},
+ * and a height of {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}.
+ */
+ @Override
+ protected LinearLayout.LayoutParams generateDefaultLayoutParams() {
+ return new LayoutParams();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+ return p instanceof TableLayout.LayoutParams;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected LinearLayout.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
+ return new LayoutParams(p);
+ }
+
+ /**
+ * <p>This set of layout parameters enforces the width of each child to be
+ * {@link #FILL_PARENT} and the height of each child to be
+ * {@link #WRAP_CONTENT}, but only if the height is not specified.</p>
+ */
+ @SuppressWarnings({"UnusedDeclaration"})
+ public static class LayoutParams extends LinearLayout.LayoutParams {
+ /**
+ * {@inheritDoc}
+ */
+ public LayoutParams(Context c, AttributeSet attrs) {
+ super(c, attrs);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public LayoutParams(int w, int h) {
+ super(FILL_PARENT, h);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public LayoutParams(int w, int h, float initWeight) {
+ super(FILL_PARENT, h, initWeight);
+ }
+
+ /**
+ * <p>Sets the child width to
+ * {@link android.view.ViewGroup.LayoutParams} and the child height to
+ * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}.</p>
+ */
+ public LayoutParams() {
+ super(FILL_PARENT, WRAP_CONTENT);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public LayoutParams(ViewGroup.LayoutParams p) {
+ super(p);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public LayoutParams(MarginLayoutParams source) {
+ super(source);
+ }
+
+ /**
+ * <p>Fixes the row's width to
+ * {@link android.view.ViewGroup.LayoutParams#FILL_PARENT}; the row's
+ * height is fixed to
+ * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} if no layout
+ * height is specified.</p>
+ *
+ * @param a the styled attributes set
+ * @param widthAttr the width attribute to fetch
+ * @param heightAttr the height attribute to fetch
+ */
+ @Override
+ protected void setBaseAttributes(TypedArray a,
+ int widthAttr, int heightAttr) {
+ this.width = FILL_PARENT;
+ if (a.hasValue(heightAttr)) {
+ this.height = a.getLayoutDimension(heightAttr, "layout_height");
+ } else {
+ this.height = WRAP_CONTENT;
+ }
+ }
+ }
+
+ /**
+ * <p>A pass-through listener acts upon the events and dispatches them
+ * to another listener. This allows the table layout to set its own internal
+ * hierarchy change listener without preventing the user to setup his.</p>
+ */
+ private class PassThroughHierarchyChangeListener implements
+ OnHierarchyChangeListener {
+ private OnHierarchyChangeListener mOnHierarchyChangeListener;
+
+ /**
+ * {@inheritDoc}
+ */
+ public void onChildViewAdded(View parent, View child) {
+ trackCollapsedColumns(child);
+
+ if (mOnHierarchyChangeListener != null) {
+ mOnHierarchyChangeListener.onChildViewAdded(parent, child);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void onChildViewRemoved(View parent, View child) {
+ if (mOnHierarchyChangeListener != null) {
+ mOnHierarchyChangeListener.onChildViewRemoved(parent, child);
+ }
+ }
+ }
+}
diff --git a/core/java/android/widget/TableRow.java b/core/java/android/widget/TableRow.java
new file mode 100644
index 0000000..5628cab
--- /dev/null
+++ b/core/java/android/widget/TableRow.java
@@ -0,0 +1,531 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.util.SparseIntArray;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewDebug;
+
+
+/**
+ * <p>A layout that arranges its children horizontally. A TableRow should
+ * always be used as a child of a {@link android.widget.TableLayout}. If a
+ * TableRow's parent is not a TableLayout, the TableRow will behave as
+ * an horizontal {@link android.widget.LinearLayout}.</p>
+ *
+ * <p>The children of a TableRow do not need to specify the
+ * <code>layout_width</code> and <code>layout_height</code> attributes in the
+ * XML file. TableRow always enforces those values to be respectively
+ * {@link android.widget.TableLayout.LayoutParams#FILL_PARENT} and
+ * {@link android.widget.TableLayout.LayoutParams#WRAP_CONTENT}.</p>
+ *
+ * <p>
+ * Also see {@link TableRow.LayoutParams android.widget.TableRow.LayoutParams}
+ * for layout attributes </p>
+ */
+public class TableRow extends LinearLayout {
+ private int mNumColumns = 0;
+ private int[] mColumnWidths;
+ private int[] mConstrainedColumnWidths;
+ private SparseIntArray mColumnToChildIndex;
+
+ private ChildrenTracker mChildrenTracker;
+
+ /**
+ * <p>Creates a new TableRow for the given context.</p>
+ *
+ * @param context the application environment
+ */
+ public TableRow(Context context) {
+ super(context);
+ initTableRow();
+ }
+
+ /**
+ * <p>Creates a new TableRow for the given context and with the
+ * specified set attributes.</p>
+ *
+ * @param context the application environment
+ * @param attrs a collection of attributes
+ */
+ public TableRow(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initTableRow();
+ }
+
+ private void initTableRow() {
+ OnHierarchyChangeListener oldListener = mOnHierarchyChangeListener;
+ mChildrenTracker = new ChildrenTracker();
+ if (oldListener != null) {
+ mChildrenTracker.setOnHierarchyChangeListener(oldListener);
+ }
+ super.setOnHierarchyChangeListener(mChildrenTracker);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setOnHierarchyChangeListener(OnHierarchyChangeListener listener) {
+ mChildrenTracker.setOnHierarchyChangeListener(listener);
+ }
+
+ /**
+ * <p>Collapses or restores a given column.</p>
+ *
+ * @param columnIndex the index of the column
+ * @param collapsed true if the column must be collapsed, false otherwise
+ * {@hide}
+ */
+ void setColumnCollapsed(int columnIndex, boolean collapsed) {
+ View child = getVirtualChildAt(columnIndex);
+ if (child != null) {
+ child.setVisibility(collapsed ? GONE : VISIBLE);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ // enforce horizontal layout
+ measureHorizontal(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ // enforce horizontal layout
+ layoutHorizontal();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View getVirtualChildAt(int i) {
+ if (mColumnToChildIndex == null) {
+ mapIndexAndColumns();
+ }
+
+ final int deflectedIndex = mColumnToChildIndex.get(i, -1);
+ if (deflectedIndex != -1) {
+ return getChildAt(deflectedIndex);
+ }
+
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getVirtualChildCount() {
+ if (mColumnToChildIndex == null) {
+ mapIndexAndColumns();
+ }
+ return mNumColumns;
+ }
+
+ private void mapIndexAndColumns() {
+ if (mColumnToChildIndex == null) {
+ int virtualCount = 0;
+ final int count = getChildCount();
+
+ mColumnToChildIndex = new SparseIntArray();
+ final SparseIntArray columnToChild = mColumnToChildIndex;
+
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ final LayoutParams layoutParams = (LayoutParams) child.getLayoutParams();
+
+ if (layoutParams.column >= virtualCount) {
+ virtualCount = layoutParams.column;
+ }
+
+ for (int j = 0; j < layoutParams.span; j++) {
+ columnToChild.put(virtualCount++, i);
+ }
+ }
+
+ mNumColumns = virtualCount;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ int measureNullChild(int childIndex) {
+ return mConstrainedColumnWidths[childIndex];
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ void measureChildBeforeLayout(View child, int childIndex,
+ int widthMeasureSpec, int totalWidth,
+ int heightMeasureSpec, int totalHeight) {
+ if (mConstrainedColumnWidths != null) {
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+ int measureMode = MeasureSpec.EXACTLY;
+ int columnWidth = 0;
+
+ final int span = lp.span;
+ final int[] constrainedColumnWidths = mConstrainedColumnWidths;
+ for (int i = 0; i < span; i++) {
+ columnWidth += constrainedColumnWidths[childIndex + i];
+ }
+
+ final int gravity = lp.gravity;
+ final boolean isHorizontalGravity = Gravity.isHorizontal(gravity);
+
+ if (isHorizontalGravity) {
+ measureMode = MeasureSpec.AT_MOST;
+ }
+
+ // no need to care about padding here,
+ // ViewGroup.getChildMeasureSpec() would get rid of it anyway
+ // because of the EXACTLY measure spec we use
+ int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
+ Math.max(0, columnWidth - lp.leftMargin - lp.rightMargin), measureMode
+ );
+ int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
+ mPaddingTop + mPaddingBottom + lp.topMargin +
+ lp .bottomMargin + totalHeight, lp.height);
+
+ child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+
+ if (isHorizontalGravity) {
+ final int childWidth = child.getMeasuredWidth();
+ lp.mOffset[LayoutParams.LOCATION_NEXT] = columnWidth - childWidth;
+
+ switch (gravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
+ case Gravity.LEFT:
+ // don't offset on X axis
+ break;
+ case Gravity.RIGHT:
+ lp.mOffset[LayoutParams.LOCATION] = lp.mOffset[LayoutParams.LOCATION_NEXT];
+ break;
+ case Gravity.CENTER_HORIZONTAL:
+ lp.mOffset[LayoutParams.LOCATION] = lp.mOffset[LayoutParams.LOCATION_NEXT] / 2;
+ break;
+ }
+ } else {
+ lp.mOffset[LayoutParams.LOCATION] = lp.mOffset[LayoutParams.LOCATION_NEXT] = 0;
+ }
+ } else {
+ // fail silently when column widths are not available
+ super.measureChildBeforeLayout(child, childIndex, widthMeasureSpec,
+ totalWidth, heightMeasureSpec, totalHeight);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ int getChildrenSkipCount(View child, int index) {
+ LayoutParams layoutParams = (LayoutParams) child.getLayoutParams();
+
+ // when the span is 1 (default), we need to skip 0 child
+ return layoutParams.span - 1;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ int getLocationOffset(View child) {
+ return ((TableRow.LayoutParams) child.getLayoutParams()).mOffset[LayoutParams.LOCATION];
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ int getNextLocationOffset(View child) {
+ return ((TableRow.LayoutParams) child.getLayoutParams()).mOffset[LayoutParams.LOCATION_NEXT];
+ }
+
+ /**
+ * <p>Measures the preferred width of each child, including its margins.</p>
+ *
+ * @param widthMeasureSpec the width constraint imposed by our parent
+ *
+ * @return an array of integers corresponding to the width of each cell, or
+ * column, in this row
+ * {@hide}
+ */
+ int[] getColumnsWidths(int widthMeasureSpec) {
+ final int numColumns = getVirtualChildCount();
+ if (mColumnWidths == null || numColumns != mColumnWidths.length) {
+ mColumnWidths = new int[numColumns];
+ }
+
+ final int[] columnWidths = mColumnWidths;
+
+ for (int i = 0; i < numColumns; i++) {
+ final View child = getVirtualChildAt(i);
+ if (child != null && child.getVisibility() != GONE) {
+ final LayoutParams layoutParams = (LayoutParams) child.getLayoutParams();
+ if (layoutParams.span == 1) {
+ int spec;
+ switch (layoutParams.width) {
+ case LayoutParams.WRAP_CONTENT:
+ spec = getChildMeasureSpec(widthMeasureSpec, 0, LayoutParams.WRAP_CONTENT);
+ break;
+ case LayoutParams.FILL_PARENT:
+ spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ break;
+ default:
+ spec = MeasureSpec.makeMeasureSpec(layoutParams.width, MeasureSpec.EXACTLY);
+ }
+ child.measure(spec, spec);
+
+ final int width = child.getMeasuredWidth() + layoutParams.leftMargin +
+ layoutParams.rightMargin;
+ columnWidths[i] = width;
+ } else {
+ columnWidths[i] = 0;
+ }
+ } else {
+ columnWidths[i] = 0;
+ }
+ }
+
+ return columnWidths;
+ }
+
+ /**
+ * <p>Sets the width of all of the columns in this row. At layout time,
+ * this row sets a fixed width, as defined by <code>columnWidths</code>,
+ * on each child (or cell, or column.)</p>
+ *
+ * @param columnWidths the fixed width of each column that this row must
+ * honor
+ * @throws IllegalArgumentException when columnWidths' length is smaller
+ * than the number of children in this row
+ * {@hide}
+ */
+ void setColumnsWidthConstraints(int[] columnWidths) {
+ if (columnWidths == null || columnWidths.length < getVirtualChildCount()) {
+ throw new IllegalArgumentException(
+ "columnWidths should be >= getVirtualChildCount()");
+ }
+
+ mConstrainedColumnWidths = columnWidths;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public LayoutParams generateLayoutParams(AttributeSet attrs) {
+ return new TableRow.LayoutParams(getContext(), attrs);
+ }
+
+ /**
+ * Returns a set of layout parameters with a width of
+ * {@link android.view.ViewGroup.LayoutParams#FILL_PARENT},
+ * a height of {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} and no spanning.
+ */
+ @Override
+ protected LinearLayout.LayoutParams generateDefaultLayoutParams() {
+ return new LayoutParams();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+ return p instanceof TableRow.LayoutParams;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected LinearLayout.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
+ return new LayoutParams(p);
+ }
+
+ /**
+ * <p>Set of layout parameters used in table rows.</p>
+ *
+ * @see android.widget.TableLayout.LayoutParams
+ *
+ * @attr ref android.R.styleable#TableRow_Cell_layout_column
+ * @attr ref android.R.styleable#TableRow_Cell_layout_span
+ */
+ public static class LayoutParams extends LinearLayout.LayoutParams {
+ /**
+ * <p>The column index of the cell represented by the widget.</p>
+ */
+ @ViewDebug.ExportedProperty
+ public int column;
+
+ /**
+ * <p>The number of columns the widgets spans over.</p>
+ */
+ @ViewDebug.ExportedProperty
+ public int span;
+
+ private static final int LOCATION = 0;
+ private static final int LOCATION_NEXT = 1;
+
+ private int[] mOffset = new int[2];
+
+ /**
+ * {@inheritDoc}
+ */
+ public LayoutParams(Context c, AttributeSet attrs) {
+ super(c, attrs);
+
+ TypedArray a =
+ c.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.TableRow_Cell);
+
+ column = a.getInt(com.android.internal.R.styleable.TableRow_Cell_layout_column, -1);
+ span = a.getInt(com.android.internal.R.styleable.TableRow_Cell_layout_span, 1);
+ if (span <= 1) {
+ span = 1;
+ }
+
+ a.recycle();
+ }
+
+ /**
+ * <p>Sets the child width and the child height.</p>
+ *
+ * @param w the desired width
+ * @param h the desired height
+ */
+ public LayoutParams(int w, int h) {
+ super(w, h);
+ column = -1;
+ span = 1;
+ }
+
+ /**
+ * <p>Sets the child width, height and weight.</p>
+ *
+ * @param w the desired width
+ * @param h the desired height
+ * @param initWeight the desired weight
+ */
+ public LayoutParams(int w, int h, float initWeight) {
+ super(w, h, initWeight);
+ column = -1;
+ span = 1;
+ }
+
+ /**
+ * <p>Sets the child width to {@link android.view.ViewGroup.LayoutParams}
+ * and the child height to
+ * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}.</p>
+ */
+ public LayoutParams() {
+ super(FILL_PARENT, WRAP_CONTENT);
+ column = -1;
+ span = 1;
+ }
+
+ /**
+ * <p>Puts the view in the specified column.</p>
+ *
+ * <p>Sets the child width to {@link android.view.ViewGroup.LayoutParams#FILL_PARENT}
+ * and the child height to
+ * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}.</p>
+ *
+ * @param column the column index for the view
+ */
+ public LayoutParams(int column) {
+ this();
+ this.column = column;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public LayoutParams(ViewGroup.LayoutParams p) {
+ super(p);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public LayoutParams(MarginLayoutParams source) {
+ super(source);
+ }
+
+ @Override
+ protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
+ // We don't want to force users to specify a layout_width
+ if (a.hasValue(widthAttr)) {
+ width = a.getLayoutDimension(widthAttr, "layout_width");
+ } else {
+ width = FILL_PARENT;
+ }
+
+ // We don't want to force users to specify a layout_height
+ if (a.hasValue(heightAttr)) {
+ height = a.getLayoutDimension(heightAttr, "layout_height");
+ } else {
+ height = WRAP_CONTENT;
+ }
+ }
+ }
+
+ // special transparent hierarchy change listener
+ private class ChildrenTracker implements OnHierarchyChangeListener {
+ private OnHierarchyChangeListener listener;
+
+ private void setOnHierarchyChangeListener(OnHierarchyChangeListener listener) {
+ this.listener = listener;
+ }
+
+ public void onChildViewAdded(View parent, View child) {
+ // dirties the index to column map
+ mColumnToChildIndex = null;
+
+ if (this.listener != null) {
+ this.listener.onChildViewAdded(parent, child);
+ }
+ }
+
+ public void onChildViewRemoved(View parent, View child) {
+ // dirties the index to column map
+ mColumnToChildIndex = null;
+
+ if (this.listener != null) {
+ this.listener.onChildViewRemoved(parent, child);
+ }
+ }
+ }
+}
diff --git a/core/java/android/widget/TextSwitcher.java b/core/java/android/widget/TextSwitcher.java
new file mode 100644
index 0000000..a8794a3
--- /dev/null
+++ b/core/java/android/widget/TextSwitcher.java
@@ -0,0 +1,91 @@
+/*
+ * 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.widget;
+
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Specialized {@link android.widget.ViewSwitcher} that contains
+ * only children of type {@link android.widget.TextView}.
+ *
+ * A TextSwitcher is useful to animate a label on screen. Whenever
+ * {@link #setText(CharSequence)} is called, TextSwitcher animates the current text
+ * out and animates the new text in.
+ */
+public class TextSwitcher extends ViewSwitcher {
+ /**
+ * Creates a new empty TextSwitcher.
+ *
+ * @param context the application's environment
+ */
+ public TextSwitcher(Context context) {
+ super(context);
+ }
+
+ /**
+ * Creates a new empty TextSwitcher for the given context and with the
+ * specified set attributes.
+ *
+ * @param context the application environment
+ * @param attrs a collection of attributes
+ */
+ public TextSwitcher(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @throws IllegalArgumentException if child is not an instance of
+ * {@link android.widget.TextView}
+ */
+ @Override
+ public void addView(View child, int index, ViewGroup.LayoutParams params) {
+ if (!(child instanceof TextView)) {
+ throw new IllegalArgumentException(
+ "TextSwitcher children must be instances of TextView");
+ }
+
+ super.addView(child, index, params);
+ }
+
+ /**
+ * Sets the text of the next view and switches to the next view. This can
+ * be used to animate the old text out and animate the next text in.
+ *
+ * @param text the new text to display
+ */
+ public void setText(CharSequence text) {
+ final TextView t = (TextView) getNextView();
+ t.setText(text);
+ showNext();
+ }
+
+ /**
+ * Sets the text of the text view that is currently showing. This does
+ * not perform the animations.
+ *
+ * @param text the new text to display
+ */
+ public void setCurrentText(CharSequence text) {
+ ((TextView)getCurrentView()).setText(text);
+ }
+}
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
new file mode 100644
index 0000000..bd5db33
--- /dev/null
+++ b/core/java/android/widget/TextView.java
@@ -0,0 +1,4866 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.text.BoringLayout;
+import android.text.DynamicLayout;
+import android.text.Editable;
+import android.text.GetChars;
+import android.text.GraphicsOperations;
+import android.text.ClipboardManager;
+import android.text.InputFilter;
+import android.text.Layout;
+import android.text.Selection;
+import android.text.SpanWatcher;
+import android.text.Spannable;
+import android.text.Spanned;
+import android.text.SpannedString;
+import android.text.SpannableString;
+import android.text.StaticLayout;
+import android.text.TextPaint;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.text.method.DialerKeyListener;
+import android.text.method.DigitsKeyListener;
+import android.text.method.KeyListener;
+import android.text.method.LinkMovementMethod;
+import android.text.method.MetaKeyKeyListener;
+import android.text.method.MovementMethod;
+import android.text.method.PasswordTransformationMethod;
+import android.text.method.SingleLineTransformationMethod;
+import android.text.method.TextKeyListener;
+import android.text.method.TransformationMethod;
+import android.text.style.ParagraphStyle;
+import android.text.style.URLSpan;
+import android.text.style.UpdateLayout;
+import android.text.util.Linkify;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.FloatMath;
+import android.util.TypedValue;
+import android.view.ContextMenu;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewDebug;
+import android.view.ViewTreeObserver;
+import android.view.ViewGroup.LayoutParams;
+import android.view.animation.AnimationUtils;
+import android.widget.RemoteViews.RemoteView;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+
+import com.android.internal.util.FastMath;
+
+/**
+ * Displays text to the user and optionally allows them to edit it. A TextView
+ * is a complete text editor, however the basic class is configured to not
+ * allow editing; see {@link EditText} for a subclass that configures the text
+ * view for editing.
+ *
+ * <p>
+ * <b>XML attributes</b>
+ * <p>
+ * See {@link android.R.styleable#TextView TextView Attributes},
+ * {@link android.R.styleable#View View Attributes}
+ *
+ * @attr ref android.R.styleable#TextView_text
+ * @attr ref android.R.styleable#TextView_bufferType
+ * @attr ref android.R.styleable#TextView_hint
+ * @attr ref android.R.styleable#TextView_textColor
+ * @attr ref android.R.styleable#TextView_textColorHighlight
+ * @attr ref android.R.styleable#TextView_textColorHint
+ * @attr ref android.R.styleable#TextView_textSize
+ * @attr ref android.R.styleable#TextView_textScaleX
+ * @attr ref android.R.styleable#TextView_typeface
+ * @attr ref android.R.styleable#TextView_textStyle
+ * @attr ref android.R.styleable#TextView_cursorVisible
+ * @attr ref android.R.styleable#TextView_maxLines
+ * @attr ref android.R.styleable#TextView_maxHeight
+ * @attr ref android.R.styleable#TextView_lines
+ * @attr ref android.R.styleable#TextView_height
+ * @attr ref android.R.styleable#TextView_minLines
+ * @attr ref android.R.styleable#TextView_minHeight
+ * @attr ref android.R.styleable#TextView_maxEms
+ * @attr ref android.R.styleable#TextView_maxWidth
+ * @attr ref android.R.styleable#TextView_ems
+ * @attr ref android.R.styleable#TextView_width
+ * @attr ref android.R.styleable#TextView_minEms
+ * @attr ref android.R.styleable#TextView_minWidth
+ * @attr ref android.R.styleable#TextView_gravity
+ * @attr ref android.R.styleable#TextView_scrollHorizontally
+ * @attr ref android.R.styleable#TextView_password
+ * @attr ref android.R.styleable#TextView_singleLine
+ * @attr ref android.R.styleable#TextView_selectAllOnFocus
+ * @attr ref android.R.styleable#TextView_includeFontPadding
+ * @attr ref android.R.styleable#TextView_maxLength
+ * @attr ref android.R.styleable#TextView_shadowColor
+ * @attr ref android.R.styleable#TextView_shadowDx
+ * @attr ref android.R.styleable#TextView_shadowDy
+ * @attr ref android.R.styleable#TextView_shadowRadius
+ * @attr ref android.R.styleable#TextView_autoLink
+ * @attr ref android.R.styleable#TextView_linksClickable
+ * @attr ref android.R.styleable#TextView_numeric
+ * @attr ref android.R.styleable#TextView_digits
+ * @attr ref android.R.styleable#TextView_phoneNumber
+ * @attr ref android.R.styleable#TextView_inputMethod
+ * @attr ref android.R.styleable#TextView_capitalize
+ * @attr ref android.R.styleable#TextView_autoText
+ * @attr ref android.R.styleable#TextView_editable
+ * @attr ref android.R.styleable#TextView_drawableTop
+ * @attr ref android.R.styleable#TextView_drawableBottom
+ * @attr ref android.R.styleable#TextView_drawableRight
+ * @attr ref android.R.styleable#TextView_drawableLeft
+ * @attr ref android.R.styleable#TextView_lineSpacingExtra
+ * @attr ref android.R.styleable#TextView_lineSpacingMultiplier
+ */
+@RemoteView
+public class TextView extends View implements ViewTreeObserver.OnPreDrawListener {
+ private static int PRIORITY = 100;
+
+ private ColorStateList mTextColor;
+ private int mCurTextColor;
+ private ColorStateList mHintTextColor;
+ private ColorStateList mLinkTextColor;
+ private int mCurHintTextColor;
+ private boolean mFreezesText;
+ private boolean mFrozenWithFocus;
+
+ private Editable.Factory mEditableFactory = Editable.Factory.getInstance();
+ private Spannable.Factory mSpannableFactory = Spannable.Factory.getInstance();
+
+ 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 TextUtils.TruncateAt mEllipsize = null;
+
+ // Enum for the "typeface" XML parameter.
+ // TODO: How can we get this from the XML instead of hardcoding it here?
+ private static final int SANS = 1;
+ private static final int SERIF = 2;
+ private static final int MONOSPACE = 3;
+
+ // Bitfield for the "numeric" XML parameter.
+ // TODO: How can we get this from the XML instead of hardcoding it here?
+ private static final int SIGNED = 2;
+ private static final int DECIMAL = 4;
+
+ private Drawable mDrawableTop, mDrawableBottom, mDrawableLeft, mDrawableRight;
+ private int mDrawableSizeTop, mDrawableSizeBottom, mDrawableSizeLeft, mDrawableSizeRight;
+ private int mDrawableWidthTop, mDrawableWidthBottom, mDrawableHeightLeft, mDrawableHeightRight;
+ private boolean mDrawables;
+ private int mDrawablePadding;
+
+ private CharSequence mError;
+ private boolean mErrorWasChanged;
+ private PopupWindow mPopup;
+
+ private CharWrapper mCharWrapper = null;
+ private Rect mCompoundRect;
+
+ private boolean mSelectionMoved = false;
+
+ /*
+ * Kick-start the font cache for the zygote process (to pay the cost of
+ * initializing freetype for our default font only once).
+ */
+ static {
+ Paint p = new Paint();
+ p.setAntiAlias(true);
+ // We don't care about the result, just the side-effect of measuring.
+ p.measureText("H");
+ }
+
+ public TextView(Context context) {
+ this(context, null);
+ }
+
+ public TextView(Context context,
+ AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.textViewStyle);
+ }
+
+ public TextView(Context context,
+ AttributeSet attrs,
+ int defStyle) {
+ super(context, attrs, defStyle);
+
+ mText = "";
+
+ mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
+ // If we get the paint from the skin, we should set it to left, since
+ // the layout always wants it to be left.
+ // mTextPaint.setTextAlign(Paint.Align.LEFT);
+
+ mHighlightPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+
+ mMovement = getDefaultMovementMethod();
+ mTransformation = null;
+
+ TypedArray a =
+ context.obtainStyledAttributes(
+ attrs, com.android.internal.R.styleable.TextView, defStyle, 0);
+
+ int textColorHighlight = 0;
+ ColorStateList textColor = null;
+ ColorStateList textColorHint = null;
+ ColorStateList textColorLink = null;
+ int textSize = 15;
+ int typefaceIndex = -1;
+ int styleIndex = -1;
+
+ /*
+ * Look the appearance up without checking first if it exists because
+ * almost every TextView has one and it greatly simplifies the logic
+ * to be able to parse the appearance first and then let specific tags
+ * for this View override it.
+ */
+ TypedArray appearance = null;
+ int ap = a.getResourceId(com.android.internal.R.styleable.TextView_textAppearance, -1);
+ if (ap != -1) {
+ appearance = context.obtainStyledAttributes(ap,
+ com.android.internal.R.styleable.
+ TextAppearance);
+ }
+ if (appearance != null) {
+ int n = appearance.getIndexCount();
+ for (int i = 0; i < n; i++) {
+ int attr = appearance.getIndex(i);
+
+ switch (attr) {
+ case com.android.internal.R.styleable.TextAppearance_textColorHighlight:
+ textColorHighlight = appearance.getColor(attr, textColorHighlight);
+ break;
+
+ case com.android.internal.R.styleable.TextAppearance_textColor:
+ textColor = appearance.getColorStateList(attr);
+ break;
+
+ case com.android.internal.R.styleable.TextAppearance_textColorHint:
+ textColorHint = appearance.getColorStateList(attr);
+ break;
+
+ case com.android.internal.R.styleable.TextAppearance_textColorLink:
+ textColorLink = appearance.getColorStateList(attr);
+ break;
+
+ case com.android.internal.R.styleable.TextAppearance_textSize:
+ textSize = appearance.getDimensionPixelSize(attr, textSize);
+ break;
+
+ case com.android.internal.R.styleable.TextAppearance_typeface:
+ typefaceIndex = appearance.getInt(attr, -1);
+ break;
+
+ case com.android.internal.R.styleable.TextAppearance_textStyle:
+ styleIndex = appearance.getInt(attr, -1);
+ break;
+ }
+ }
+
+ appearance.recycle();
+ }
+
+ boolean editable = getDefaultEditable();
+ CharSequence inputMethod = null;
+ int numeric = 0;
+ CharSequence digits = null;
+ boolean phone = false;
+ boolean autotext = false;
+ int autocap = -1;
+ int buffertype = 0;
+ boolean selectallonfocus = false;
+ Drawable drawableLeft = null, drawableTop = null, drawableRight = null,
+ drawableBottom = null;
+ int drawablePadding = 0;
+ int ellipsize = -1;
+ boolean singleLine = false;
+ int maxlength = -1;
+ CharSequence text = "";
+ int shadowcolor = 0;
+ float dx = 0, dy = 0, r = 0;
+ boolean password = false;
+
+ int n = a.getIndexCount();
+ for (int i = 0; i < n; i++) {
+ int attr = a.getIndex(i);
+
+ switch (attr) {
+ case com.android.internal.R.styleable.TextView_editable:
+ editable = a.getBoolean(attr, editable);
+ break;
+
+ case com.android.internal.R.styleable.TextView_inputMethod:
+ inputMethod = a.getText(attr);
+ break;
+
+ case com.android.internal.R.styleable.TextView_numeric:
+ numeric = a.getInt(attr, numeric);
+ break;
+
+ case com.android.internal.R.styleable.TextView_digits:
+ digits = a.getText(attr);
+ break;
+
+ case com.android.internal.R.styleable.TextView_phoneNumber:
+ phone = a.getBoolean(attr, phone);
+ break;
+
+ case com.android.internal.R.styleable.TextView_autoText:
+ autotext = a.getBoolean(attr, autotext);
+ break;
+
+ case com.android.internal.R.styleable.TextView_capitalize:
+ autocap = a.getInt(attr, autocap);
+ break;
+
+ case com.android.internal.R.styleable.TextView_bufferType:
+ buffertype = a.getInt(attr, buffertype);
+ break;
+
+ case com.android.internal.R.styleable.TextView_selectAllOnFocus:
+ selectallonfocus = a.getBoolean(attr, selectallonfocus);
+ break;
+
+ case com.android.internal.R.styleable.TextView_autoLink:
+ mAutoLinkMask = a.getInt(attr, 0);
+ break;
+
+ case com.android.internal.R.styleable.TextView_linksClickable:
+ mLinksClickable = a.getBoolean(attr, true);
+ break;
+
+ case com.android.internal.R.styleable.TextView_drawableLeft:
+ drawableLeft = a.getDrawable(attr);
+ break;
+
+ case com.android.internal.R.styleable.TextView_drawableTop:
+ drawableTop = a.getDrawable(attr);
+ break;
+
+ case com.android.internal.R.styleable.TextView_drawableRight:
+ drawableRight = a.getDrawable(attr);
+ break;
+
+ case com.android.internal.R.styleable.TextView_drawableBottom:
+ drawableBottom = a.getDrawable(attr);
+ break;
+
+ case com.android.internal.R.styleable.TextView_drawablePadding:
+ drawablePadding = a.getDimensionPixelSize(attr, drawablePadding);
+ break;
+
+ case com.android.internal.R.styleable.TextView_maxLines:
+ setMaxLines(a.getInt(attr, -1));
+ break;
+
+ case com.android.internal.R.styleable.TextView_maxHeight:
+ setMaxHeight(a.getDimensionPixelSize(attr, -1));
+ break;
+
+ case com.android.internal.R.styleable.TextView_lines:
+ setLines(a.getInt(attr, -1));
+ break;
+
+ case com.android.internal.R.styleable.TextView_height:
+ setHeight(a.getDimensionPixelSize(attr, -1));
+ break;
+
+ case com.android.internal.R.styleable.TextView_minLines:
+ setMinLines(a.getInt(attr, -1));
+ break;
+
+ case com.android.internal.R.styleable.TextView_minHeight:
+ setMinHeight(a.getDimensionPixelSize(attr, -1));
+ break;
+
+ case com.android.internal.R.styleable.TextView_maxEms:
+ setMaxEms(a.getInt(attr, -1));
+ break;
+
+ case com.android.internal.R.styleable.TextView_maxWidth:
+ setMaxWidth(a.getDimensionPixelSize(attr, -1));
+ break;
+
+ case com.android.internal.R.styleable.TextView_ems:
+ setEms(a.getInt(attr, -1));
+ break;
+
+ case com.android.internal.R.styleable.TextView_width:
+ setWidth(a.getDimensionPixelSize(attr, -1));
+ break;
+
+ case com.android.internal.R.styleable.TextView_minEms:
+ setMinEms(a.getInt(attr, -1));
+ break;
+
+ case com.android.internal.R.styleable.TextView_minWidth:
+ setMinWidth(a.getDimensionPixelSize(attr, -1));
+ break;
+
+ case com.android.internal.R.styleable.TextView_gravity:
+ setGravity(a.getInt(attr, -1));
+ break;
+
+ case com.android.internal.R.styleable.TextView_hint:
+ setHint(a.getText(attr));
+ break;
+
+ case com.android.internal.R.styleable.TextView_text:
+ text = a.getText(attr);
+ break;
+
+ case com.android.internal.R.styleable.TextView_scrollHorizontally:
+ if (a.getBoolean(attr, false)) {
+ setHorizontallyScrolling(true);
+ }
+ break;
+
+ case com.android.internal.R.styleable.TextView_singleLine:
+ singleLine = a.getBoolean(attr, singleLine);
+ break;
+
+ case com.android.internal.R.styleable.TextView_ellipsize:
+ ellipsize = a.getInt(attr, ellipsize);
+ break;
+
+ case com.android.internal.R.styleable.TextView_includeFontPadding:
+ if (!a.getBoolean(attr, true)) {
+ setIncludeFontPadding(false);
+ }
+ break;
+
+ case com.android.internal.R.styleable.TextView_cursorVisible:
+ if (!a.getBoolean(attr, true)) {
+ setCursorVisible(false);
+ }
+ break;
+
+ case com.android.internal.R.styleable.TextView_maxLength:
+ maxlength = a.getInt(attr, -1);
+ break;
+
+ case com.android.internal.R.styleable.TextView_textScaleX:
+ setTextScaleX(a.getFloat(attr, 1.0f));
+ break;
+
+ case com.android.internal.R.styleable.TextView_freezesText:
+ mFreezesText = a.getBoolean(attr, false);
+ break;
+
+ case com.android.internal.R.styleable.TextView_shadowColor:
+ shadowcolor = a.getInt(attr, 0);
+ break;
+
+ case com.android.internal.R.styleable.TextView_shadowDx:
+ dx = a.getFloat(attr, 0);
+ break;
+
+ case com.android.internal.R.styleable.TextView_shadowDy:
+ dy = a.getFloat(attr, 0);
+ break;
+
+ case com.android.internal.R.styleable.TextView_shadowRadius:
+ r = a.getFloat(attr, 0);
+ break;
+
+ case com.android.internal.R.styleable.TextView_enabled:
+ setEnabled(a.getBoolean(attr, isEnabled()));
+ break;
+
+ case com.android.internal.R.styleable.TextView_textColorHighlight:
+ textColorHighlight = a.getColor(attr, textColorHighlight);
+ break;
+
+ case com.android.internal.R.styleable.TextView_textColor:
+ textColor = a.getColorStateList(attr);
+ break;
+
+ case com.android.internal.R.styleable.TextView_textColorHint:
+ textColorHint = a.getColorStateList(attr);
+ break;
+
+ case com.android.internal.R.styleable.TextView_textColorLink:
+ textColorLink = a.getColorStateList(attr);
+ break;
+
+ case com.android.internal.R.styleable.TextView_textSize:
+ textSize = a.getDimensionPixelSize(attr, textSize);
+ break;
+
+ case com.android.internal.R.styleable.TextView_typeface:
+ typefaceIndex = a.getInt(attr, typefaceIndex);
+ break;
+
+ case com.android.internal.R.styleable.TextView_textStyle:
+ styleIndex = a.getInt(attr, styleIndex);
+ break;
+
+ case com.android.internal.R.styleable.TextView_password:
+ password = a.getBoolean(attr, password);
+ break;
+
+ case com.android.internal.R.styleable.TextView_lineSpacingExtra:
+ mSpacingAdd = a.getDimensionPixelSize(attr, (int) mSpacingAdd);
+ break;
+
+ case com.android.internal.R.styleable.TextView_lineSpacingMultiplier:
+ mSpacingMult = a.getFloat(attr, mSpacingMult);
+ break;
+ }
+ }
+ a.recycle();
+
+ BufferType bufferType = BufferType.EDITABLE;
+
+ if (inputMethod != null) {
+ Class c;
+
+ try {
+ c = Class.forName(inputMethod.toString());
+ } catch (ClassNotFoundException ex) {
+ throw new RuntimeException(ex);
+ }
+
+ try {
+ mInput = (KeyListener) c.newInstance();
+ } catch (InstantiationException ex) {
+ throw new RuntimeException(ex);
+ } catch (IllegalAccessException ex) {
+ throw new RuntimeException(ex);
+ }
+ } else if (digits != null) {
+ mInput = DigitsKeyListener.getInstance(digits.toString());
+ } else if (phone) {
+ mInput = DialerKeyListener.getInstance();
+ } else if (numeric != 0) {
+ mInput = DigitsKeyListener.getInstance((numeric & SIGNED) != 0,
+ (numeric & DECIMAL) != 0);
+ } else if (autotext || autocap != -1) {
+ TextKeyListener.Capitalize cap;
+
+ switch (autocap) {
+ case 1:
+ cap = TextKeyListener.Capitalize.SENTENCES;
+ break;
+
+ case 2:
+ cap = TextKeyListener.Capitalize.WORDS;
+ break;
+
+ case 3:
+ cap = TextKeyListener.Capitalize.CHARACTERS;
+ break;
+
+ default:
+ cap = TextKeyListener.Capitalize.NONE;
+ break;
+ }
+
+ mInput = TextKeyListener.getInstance(autotext, cap);
+ } else if (editable) {
+ mInput = TextKeyListener.getInstance();
+ } else {
+ mInput = null;
+
+ switch (buffertype) {
+ case 0:
+ bufferType = BufferType.NORMAL;
+ break;
+ case 1:
+ bufferType = BufferType.SPANNABLE;
+ break;
+ case 2:
+ bufferType = BufferType.EDITABLE;
+ break;
+ }
+ }
+
+ if (selectallonfocus) {
+ mSelectAllOnFocus = true;
+
+ if (bufferType == BufferType.NORMAL)
+ bufferType = BufferType.SPANNABLE;
+ }
+
+ setCompoundDrawablesWithIntrinsicBounds(
+ drawableLeft, drawableTop, drawableRight, drawableBottom);
+ setCompoundDrawablePadding(drawablePadding);
+
+ if (singleLine) {
+ setSingleLine();
+
+ if (mInput == null && ellipsize < 0) {
+ ellipsize = 3; // END
+ }
+ }
+
+ switch (ellipsize) {
+ case 1:
+ setEllipsize(TextUtils.TruncateAt.START);
+ break;
+ case 2:
+ setEllipsize(TextUtils.TruncateAt.MIDDLE);
+ break;
+ case 3:
+ setEllipsize(TextUtils.TruncateAt.END);
+ break;
+ }
+
+ setTextColor(textColor != null ? textColor : ColorStateList.valueOf(0xFF000000));
+ setHintTextColor(textColorHint);
+ setLinkTextColor(textColorLink);
+ if (textColorHighlight != 0) {
+ setHighlightColor(textColorHighlight);
+ }
+ setRawTextSize(textSize);
+
+ if (password) {
+ setTransformationMethod(PasswordTransformationMethod.getInstance());
+ typefaceIndex = MONOSPACE;
+ }
+
+ setTypefaceByIndex(typefaceIndex, styleIndex);
+
+ if (shadowcolor != 0) {
+ setShadowLayer(r, dx, dy, shadowcolor);
+ }
+
+ if (maxlength >= 0) {
+ setFilters(new InputFilter[] { new InputFilter.LengthFilter(maxlength) });
+ } else {
+ setFilters(NO_FILTERS);
+ }
+
+ setText(text, bufferType);
+
+ /*
+ * Views are not normally focusable unless specified to be.
+ * However, TextViews that have input or movement methods *are*
+ * focusable by default.
+ */
+ a = context.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.View,
+ defStyle, 0);
+
+ boolean focusable = mMovement != null || mInput != null;
+ boolean clickable = focusable;
+ boolean longClickable = focusable;
+
+ n = a.getIndexCount();
+ for (int i = 0; i < n; i++) {
+ int attr = a.getIndex(i);
+
+ switch (attr) {
+ case com.android.internal.R.styleable.View_focusable:
+ focusable = a.getBoolean(attr, focusable);
+ break;
+
+ case com.android.internal.R.styleable.View_clickable:
+ clickable = a.getBoolean(attr, clickable);
+ break;
+
+ case com.android.internal.R.styleable.View_longClickable:
+ longClickable = a.getBoolean(attr, longClickable);
+ break;
+ }
+ }
+ a.recycle();
+
+ setFocusable(focusable);
+ setClickable(clickable);
+ setLongClickable(longClickable);
+ }
+
+ private void setTypefaceByIndex(int typefaceIndex, int styleIndex) {
+ Typeface tf = null;
+ switch (typefaceIndex) {
+ case SANS:
+ tf = Typeface.SANS_SERIF;
+ break;
+
+ case SERIF:
+ tf = Typeface.SERIF;
+ break;
+
+ case MONOSPACE:
+ tf = Typeface.MONOSPACE;
+ break;
+ }
+
+ setTypeface(tf, styleIndex);
+ }
+
+ /**
+ * Sets the typeface and style in which the text should be displayed,
+ * and turns on the fake bold and italic bits in the Paint if the
+ * Typeface that you provided does not have all the bits in the
+ * style that you specified.
+ *
+ * @attr ref android.R.styleable#TextView_typeface
+ * @attr ref android.R.styleable#TextView_textStyle
+ */
+ public void setTypeface(Typeface tf, int style) {
+ if (style > 0) {
+ if (tf == null) {
+ tf = Typeface.defaultFromStyle(style);
+ } else {
+ tf = Typeface.create(tf, style);
+ }
+
+ setTypeface(tf);
+ // now compute what (if any) algorithmic styling is needed
+ int typefaceStyle = tf != null ? tf.getStyle() : 0;
+ int need = style & ~typefaceStyle;
+ mTextPaint.setFakeBoldText((need & Typeface.BOLD) != 0);
+ mTextPaint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0);
+ } else {
+ mTextPaint.setFakeBoldText(false);
+ mTextPaint.setTextSkewX(0);
+ setTypeface(tf);
+ }
+ }
+
+ /**
+ * Subclasses override this to specify that they have a KeyListener
+ * by default even if not specifically called for in the XML options.
+ */
+ protected boolean getDefaultEditable() {
+ return false;
+ }
+
+ /**
+ * Subclasses override this to specify a default movement method.
+ */
+ protected MovementMethod getDefaultMovementMethod() {
+ return null;
+ }
+
+ /**
+ * Return the text the TextView is displaying. If setText() was called
+ * with an argument of BufferType.SPANNABLE or BufferType.EDITABLE,
+ * you can cast the return value from this method to Spannable
+ * or Editable, respectively.
+ */
+ public CharSequence getText() {
+ return mText;
+ }
+
+ /**
+ * Returns the length, in characters, of the text managed by this TextView
+ */
+ public int length() {
+ return mText.length();
+ }
+
+ /**
+ * @return the height of one standard line in pixels. Note that markup
+ * within the text can cause individual lines to be taller or shorter
+ * than this height, and the layout may contain additional first-
+ * or last-line padding.
+ */
+ public int getLineHeight() {
+ return FastMath.round(mTextPaint.getFontMetricsInt(null) * mSpacingMult
+ + mSpacingAdd);
+ }
+
+ /**
+ * @return the Layout that is currently being used to display the text.
+ * This can be null if the text or width has recently changes.
+ */
+ public final Layout getLayout() {
+ return mLayout;
+ }
+
+ /**
+ * @return the current key listener for this TextView.
+ * This will frequently be null for non-EditText TextViews.
+ */
+ public final KeyListener getKeyListener() {
+ return mInput;
+ }
+
+ /**
+ * Sets the key listener to be used with this TextView. This can be null
+ * to disallow user input.
+ * <p>
+ * Be warned that if you want a TextView with a key listener or movement
+ * method not to be focusable, or if you want a TextView without a
+ * key listener or movement method to be focusable, you must call
+ * {@link #setFocusable} again after calling this to get the focusability
+ * back the way you want it.
+ *
+ * @attr ref android.R.styleable#TextView_numeric
+ * @attr ref android.R.styleable#TextView_digits
+ * @attr ref android.R.styleable#TextView_phoneNumber
+ * @attr ref android.R.styleable#TextView_inputMethod
+ * @attr ref android.R.styleable#TextView_capitalize
+ * @attr ref android.R.styleable#TextView_autoText
+ */
+ public void setKeyListener(KeyListener input) {
+ mInput = input;
+
+ if (mInput != null && !(mText instanceof Editable))
+ setText(mText);
+
+ setFilters((Editable) mText, mFilters);
+ fixFocusableAndClickableSettings();
+ }
+
+ /**
+ * @return the movement method being used for this TextView.
+ * This will frequently be null for non-EditText TextViews.
+ */
+ public final MovementMethod getMovementMethod() {
+ return mMovement;
+ }
+
+ /**
+ * Sets the movement method (arrow key handler) to be used for
+ * this TextView. This can be null to disallow using the arrow keys
+ * to move the cursor or scroll the view.
+ * <p>
+ * Be warned that if you want a TextView with a key listener or movement
+ * method not to be focusable, or if you want a TextView without a
+ * key listener or movement method to be focusable, you must call
+ * {@link #setFocusable} again after calling this to get the focusability
+ * back the way you want it.
+ */
+ public final void setMovementMethod(MovementMethod movement) {
+ mMovement = movement;
+
+ if (mMovement != null && !(mText instanceof Spannable))
+ setText(mText);
+
+ fixFocusableAndClickableSettings();
+ }
+
+ private void fixFocusableAndClickableSettings() {
+ if (mMovement != null || mInput != null) {
+ setFocusable(true);
+ setClickable(true);
+ setLongClickable(true);
+ } else {
+ setFocusable(false);
+ setClickable(false);
+ setLongClickable(false);
+ }
+ }
+
+ /**
+ * @return the current transformation method for this TextView.
+ * This will frequently be null except for single-line and password
+ * fields.
+ */
+ public final TransformationMethod getTransformationMethod() {
+ return mTransformation;
+ }
+
+ /**
+ * Sets the transformation that is applied to the text that this
+ * TextView is displaying.
+ *
+ * @attr ref android.R.styleable#TextView_password
+ * @attr ref android.R.styleable#TextView_singleLine
+ */
+ public final void setTransformationMethod(TransformationMethod method) {
+ if (mTransformation != null) {
+ if (mText instanceof Spannable) {
+ ((Spannable) mText).removeSpan(mTransformation);
+ }
+ }
+
+ mTransformation = method;
+
+ setText(mText);
+ }
+
+ /**
+ * Returns the top padding of the view, plus space for the top
+ * Drawable if any.
+ */
+ public int getCompoundPaddingTop() {
+ if (mDrawableTop == null) {
+ return mPaddingTop;
+ } else {
+ return mPaddingTop + mDrawablePadding + mDrawableSizeTop;
+ }
+ }
+
+ /**
+ * Returns the bottom padding of the view, plus space for the bottom
+ * Drawable if any.
+ */
+ public int getCompoundPaddingBottom() {
+ if (mDrawableBottom == null) {
+ return mPaddingBottom;
+ } else {
+ return mPaddingBottom + mDrawablePadding + mDrawableSizeBottom;
+ }
+ }
+
+ /**
+ * Returns the left padding of the view, plus space for the left
+ * Drawable if any.
+ */
+ public int getCompoundPaddingLeft() {
+ if (mDrawableLeft == null) {
+ return mPaddingLeft;
+ } else {
+ return mPaddingLeft + mDrawablePadding + mDrawableSizeLeft;
+ }
+ }
+
+ /**
+ * Returns the right padding of the view, plus space for the right
+ * Drawable if any.
+ */
+ public int getCompoundPaddingRight() {
+ if (mDrawableRight == null) {
+ return mPaddingRight;
+ } else {
+ return mPaddingRight + mDrawablePadding + mDrawableSizeRight;
+ }
+ }
+
+ /**
+ * Returns the extended top padding of the view, including both the
+ * top Drawable if any and any extra space to keep more than maxLines
+ * of text from showing. It is only valid to call this after measuring.
+ */
+ public int getExtendedPaddingTop() {
+ if (mMaxMode != LINES) {
+ return getCompoundPaddingTop();
+ }
+
+ if (mLayout.getLineCount() <= mMaximum) {
+ return getCompoundPaddingTop();
+ }
+
+ int top = getCompoundPaddingTop();
+ int bottom = getCompoundPaddingBottom();
+ int viewht = getHeight() - top - bottom;
+ int layoutht = mLayout.getLineTop(mMaximum);
+
+ if (layoutht >= viewht) {
+ return top;
+ }
+
+ final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
+ if (gravity == Gravity.TOP) {
+ return top;
+ } else if (gravity == Gravity.BOTTOM) {
+ return top + viewht - layoutht;
+ } else { // (gravity == Gravity.CENTER_VERTICAL)
+ return top + (viewht - layoutht) / 2;
+ }
+ }
+
+ /**
+ * Returns the extended bottom padding of the view, including both the
+ * bottom Drawable if any and any extra space to keep more than maxLines
+ * of text from showing. It is only valid to call this after measuring.
+ */
+ public int getExtendedPaddingBottom() {
+ if (mMaxMode != LINES) {
+ return getCompoundPaddingBottom();
+ }
+
+ if (mLayout.getLineCount() <= mMaximum) {
+ return getCompoundPaddingBottom();
+ }
+
+ int top = getCompoundPaddingTop();
+ int bottom = getCompoundPaddingBottom();
+ int viewht = getHeight() - top - bottom;
+ int layoutht = mLayout.getLineTop(mMaximum);
+
+ if (layoutht >= viewht) {
+ return bottom;
+ }
+
+ final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
+ if (gravity == Gravity.TOP) {
+ return bottom + viewht - layoutht;
+ } else if (gravity == Gravity.BOTTOM) {
+ return bottom;
+ } else { // (gravity == Gravity.CENTER_VERTICAL)
+ return bottom + (viewht - layoutht) / 2;
+ }
+ }
+
+ /**
+ * Returns the total left padding of the view, including the left
+ * Drawable if any.
+ */
+ public int getTotalPaddingLeft() {
+ return getCompoundPaddingLeft();
+ }
+
+ /**
+ * Returns the total right padding of the view, including the right
+ * Drawable if any.
+ */
+ public int getTotalPaddingRight() {
+ return getCompoundPaddingRight();
+ }
+
+ /**
+ * Returns the total top padding of the view, including the top
+ * Drawable if any, the extra space to keep more than maxLines
+ * from showing, and the vertical offset for gravity, if any.
+ */
+ public int getTotalPaddingTop() {
+ return getExtendedPaddingTop() + getVerticalOffset(true);
+ }
+
+ /**
+ * Returns the total bottom padding of the view, including the bottom
+ * Drawable if any, the extra space to keep more than maxLines
+ * from showing, and the vertical offset for gravity, if any.
+ */
+ public int getTotalPaddingBottom() {
+ return getExtendedPaddingBottom() + getBottomVerticalOffset(true);
+ }
+
+ /**
+ * Sets the Drawables (if any) to appear to the left of, above,
+ * to the right of, and below the text. Use null if you do not
+ * want a Drawable there. The Drawables must already have had
+ * {@link Drawable#setBounds} called.
+ *
+ * @attr ref android.R.styleable#TextView_drawableLeft
+ * @attr ref android.R.styleable#TextView_drawableTop
+ * @attr ref android.R.styleable#TextView_drawableRight
+ * @attr ref android.R.styleable#TextView_drawableBottom
+ */
+ public void setCompoundDrawables(Drawable left, Drawable top,
+ Drawable right, Drawable bottom) {
+ mDrawableLeft = left;
+ mDrawableTop = top;
+ mDrawableRight = right;
+ mDrawableBottom = bottom;
+
+ mDrawables = mDrawableLeft != null
+ || mDrawableRight != null
+ || mDrawableTop != null
+ || mDrawableBottom != null;
+
+ if (mCompoundRect == null &&
+ (left != null || top != null || right != null || bottom != null)) {
+ mCompoundRect = new Rect();
+ }
+
+ final Rect compoundRect = mCompoundRect;
+ int[] state = null;
+
+ if (mDrawables) {
+ state = getDrawableState();
+ }
+
+ if (mDrawableLeft != null) {
+ mDrawableLeft.setState(state);
+ mDrawableLeft.copyBounds(compoundRect);
+ mDrawableSizeLeft = compoundRect.width();
+ mDrawableHeightLeft = compoundRect.height();
+ } else {
+ mDrawableSizeLeft = mDrawableHeightLeft = 0;
+ }
+
+ if (mDrawableRight != null) {
+ mDrawableRight.setState(state);
+ mDrawableRight.copyBounds(compoundRect);
+ mDrawableSizeRight = compoundRect.width();
+ mDrawableHeightRight = compoundRect.height();
+ } else {
+ mDrawableSizeRight = mDrawableHeightRight = 0;
+ }
+
+ if (mDrawableTop != null) {
+ mDrawableTop.setState(state);
+ mDrawableTop.copyBounds(compoundRect);
+ mDrawableSizeTop = compoundRect.height();
+ mDrawableWidthTop = compoundRect.width();
+ } else {
+ mDrawableSizeTop = mDrawableWidthTop = 0;
+ }
+
+ if (mDrawableBottom != null) {
+ mDrawableBottom.setState(state);
+ mDrawableBottom.copyBounds(compoundRect);
+ mDrawableSizeBottom = compoundRect.height();
+ mDrawableWidthBottom = compoundRect.width();
+ } else {
+ mDrawableSizeBottom = mDrawableWidthBottom = 0;
+ }
+
+ invalidate();
+ requestLayout();
+ }
+
+ /**
+ * Sets the Drawables (if any) to appear to the left of, above,
+ * to the right of, and below the text. Use null if you do not
+ * want a Drawable there. The Drawables' bounds will be set to
+ * their intrinsic bounds.
+ *
+ * @attr ref android.R.styleable#TextView_drawableLeft
+ * @attr ref android.R.styleable#TextView_drawableTop
+ * @attr ref android.R.styleable#TextView_drawableRight
+ * @attr ref android.R.styleable#TextView_drawableBottom
+ */
+ public void setCompoundDrawablesWithIntrinsicBounds(Drawable left,
+ Drawable top,
+ Drawable right, Drawable bottom) {
+ if (left != null) {
+ left.setBounds(0, 0,
+ left.getIntrinsicWidth(), left.getIntrinsicHeight());
+ }
+ if (right != null) {
+ right.setBounds(0, 0,
+ right.getIntrinsicWidth(), right.getIntrinsicHeight());
+ }
+ if (top != null) {
+ top.setBounds(0, 0,
+ top.getIntrinsicWidth(), top.getIntrinsicHeight());
+ }
+ if (bottom != null) {
+ bottom.setBounds(0, 0,
+ bottom.getIntrinsicWidth(), bottom.getIntrinsicHeight());
+ }
+ setCompoundDrawables(left, top, right, bottom);
+ }
+
+ /**
+ * Returns drawables for the left, top, right, and bottom borders.
+ */
+ public Drawable[] getCompoundDrawables() {
+ return new Drawable[] {
+ mDrawableLeft, mDrawableTop, mDrawableRight, mDrawableBottom
+ };
+ }
+
+ /**
+ * Sets the size of the padding between the compound drawables and
+ * the text.
+ *
+ * @attr ref android.R.styleable#TextView_drawablePadding
+ */
+ public void setCompoundDrawablePadding(int pad) {
+ mDrawablePadding = pad;
+
+ invalidate();
+ requestLayout();
+ }
+
+ /**
+ * Returns the padding between the compound drawables and the text.
+ */
+ public int getCompoundDrawablePadding() {
+ return mDrawablePadding;
+ }
+
+ @Override
+ public void setPadding(int left, int top, int right, int bottom) {
+ if (left != getPaddingLeft() ||
+ right != getPaddingRight() ||
+ top != getPaddingTop() ||
+ bottom != getPaddingBottom()) {
+ nullLayouts();
+ }
+
+ // the super call will requestLayout()
+ super.setPadding(left, top, right, bottom);
+ invalidate();
+ }
+
+ /**
+ * Gets the autolink mask of the text. See {@link
+ * android.text.util.Linkify#ALL Linkify.ALL} and peers for
+ * possible values.
+ *
+ * @attr ref android.R.styleable#TextView_autoLink
+ */
+ public final int getAutoLinkMask() {
+ return mAutoLinkMask;
+ }
+
+ /**
+ * Sets the text color, size, style, hint color, and highlight color
+ * from the specified TextAppearance resource.
+ */
+ public void setTextAppearance(Context context, int resid) {
+ TypedArray appearance =
+ context.obtainStyledAttributes(resid,
+ com.android.internal.R.styleable.TextAppearance);
+
+ int color;
+ ColorStateList colors;
+ int ts;
+
+ color = appearance.getColor(com.android.internal.R.styleable.TextAppearance_textColorHighlight, 0);
+ if (color != 0) {
+ setHighlightColor(color);
+ }
+
+ colors = appearance.getColorStateList(com.android.internal.R.styleable.
+ TextAppearance_textColor);
+ if (colors != null) {
+ setTextColor(colors);
+ }
+
+ ts = appearance.getDimensionPixelSize(com.android.internal.R.styleable.
+ TextAppearance_textSize, 0);
+ if (ts != 0) {
+ setRawTextSize(ts);
+ }
+
+ colors = appearance.getColorStateList(com.android.internal.R.styleable.
+ TextAppearance_textColorHint);
+ if (colors != null) {
+ setHintTextColor(colors);
+ }
+
+ colors = appearance.getColorStateList(com.android.internal.R.styleable.
+ TextAppearance_textColorLink);
+ if (colors != null) {
+ setLinkTextColor(colors);
+ }
+
+ int typefaceIndex, styleIndex;
+
+ typefaceIndex = appearance.getInt(com.android.internal.R.styleable.
+ TextAppearance_typeface, -1);
+ styleIndex = appearance.getInt(com.android.internal.R.styleable.
+ TextAppearance_textStyle, -1);
+
+ setTypefaceByIndex(typefaceIndex, styleIndex);
+ appearance.recycle();
+ }
+
+ /**
+ * @return the size (in pixels) of the default text size in this TextView.
+ */
+ public float getTextSize() {
+ return mTextPaint.getTextSize();
+ }
+
+ /**
+ * Set the default text size to the given value, interpreted as "scaled
+ * pixel" units. This size is adjusted based on the current density and
+ * user font size preference.
+ *
+ * @param size The scaled pixel size.
+ *
+ * @attr ref android.R.styleable#TextView_textSize
+ */
+ public void setTextSize(float size) {
+ setTextSize(TypedValue.COMPLEX_UNIT_SP, size);
+ }
+
+ /**
+ * Set the default text size to a given unit and value. See {@link
+ * TypedValue} for the possible dimension units.
+ *
+ * @param unit The desired dimension unit.
+ * @param size The desired size in the given units.
+ *
+ * @attr ref android.R.styleable#TextView_textSize
+ */
+ public void setTextSize(int unit, float size) {
+ Context c = getContext();
+ Resources r;
+
+ if (c == null)
+ r = Resources.getSystem();
+ else
+ r = c.getResources();
+
+ setRawTextSize(TypedValue.applyDimension(
+ unit, size, r.getDisplayMetrics()));
+ }
+
+ private void setRawTextSize(float size) {
+ if (size != mTextPaint.getTextSize()) {
+ mTextPaint.setTextSize(size);
+
+ if (mLayout != null) {
+ nullLayouts();
+ requestLayout();
+ invalidate();
+ }
+ }
+ }
+
+ /**
+ * @return the extent by which text is currently being stretched
+ * horizontally. This will usually be 1.
+ */
+ public float getTextScaleX() {
+ return mTextPaint.getTextScaleX();
+ }
+
+ /**
+ * Sets the extent by which text should be stretched horizontally.
+ *
+ * @attr ref android.R.styleable#TextView_textScaleX
+ */
+ public void setTextScaleX(float size) {
+ if (size != mTextPaint.getTextScaleX()) {
+ mTextPaint.setTextScaleX(size);
+
+ if (mLayout != null) {
+ nullLayouts();
+ requestLayout();
+ invalidate();
+ }
+ }
+ }
+
+ /**
+ * Sets the typeface and style in which the text should be displayed.
+ * Note that not all Typeface families actually have bold and italic
+ * variants, so you may need to use
+ * {@link #setTypeface(Typeface, int)} to get the appearance
+ * that you actually want.
+ *
+ * @attr ref android.R.styleable#TextView_typeface
+ * @attr ref android.R.styleable#TextView_textStyle
+ */
+ public void setTypeface(Typeface tf) {
+ if (mTextPaint.getTypeface() != tf) {
+ mTextPaint.setTypeface(tf);
+
+ if (mLayout != null) {
+ nullLayouts();
+ requestLayout();
+ invalidate();
+ }
+ }
+ }
+
+ /**
+ * @return the current typeface and style in which the text is being
+ * displayed.
+ */
+ public Typeface getTypeface() {
+ return mTextPaint.getTypeface();
+ }
+
+ /**
+ * Sets the text color for all the states (normal, selected,
+ * focused) to be this color.
+ *
+ * @attr ref android.R.styleable#TextView_textColor
+ */
+ public void setTextColor(int color) {
+ mTextColor = ColorStateList.valueOf(color);
+ updateTextColors();
+ }
+
+ /**
+ * Sets the text color.
+ *
+ * @attr ref android.R.styleable#TextView_textColor
+ */
+ public void setTextColor(ColorStateList colors) {
+ if (colors == null) {
+ throw new NullPointerException();
+ }
+
+ mTextColor = colors;
+ updateTextColors();
+ }
+
+ /**
+ * Return the set of text colors.
+ *
+ * @return Returns the set of text colors.
+ */
+ public final ColorStateList getTextColors() {
+ return mTextColor;
+ }
+
+ /**
+ * <p>Return the current color selected for normal text.</p>
+ *
+ * @return Returns the current text color.
+ */
+ public final int getCurrentTextColor() {
+ return mCurTextColor;
+ }
+
+ /**
+ * Sets the color used to display the selection highlight.
+ *
+ * @attr ref android.R.styleable#TextView_textColorHighlight
+ */
+ public void setHighlightColor(int color) {
+ if (mHighlightColor != color) {
+ mHighlightColor = color;
+ invalidate();
+ }
+ }
+
+ /**
+ * Gives the text a shadow of the specified radius and color, the specified
+ * distance from its normal position.
+ *
+ * @attr ref android.R.styleable#TextView_shadowColor
+ * @attr ref android.R.styleable#TextView_shadowDx
+ * @attr ref android.R.styleable#TextView_shadowDy
+ * @attr ref android.R.styleable#TextView_shadowRadius
+ */
+ public void setShadowLayer(float radius, float dx, float dy, int color) {
+ mTextPaint.setShadowLayer(radius, dx, dy, color);
+
+ mShadowRadius = radius;
+ mShadowDx = dx;
+ mShadowDy = dy;
+
+ invalidate();
+ }
+
+ /**
+ * @return the base paint used for the text. Please use this only to
+ * consult the Paint's properties and not to change them.
+ */
+ public TextPaint getPaint() {
+ return mTextPaint;
+ }
+
+ /**
+ * Sets the autolink mask of the text. See {@link
+ * android.text.util.Linkify#ALL Linkify.ALL} and peers for
+ * possible values.
+ *
+ * @attr ref android.R.styleable#TextView_autoLink
+ */
+ public final void setAutoLinkMask(int mask) {
+ mAutoLinkMask = mask;
+ }
+
+ /**
+ * Sets whether the movement method will automatically be set to
+ * {@link LinkMovementMethod} if {@link #setAutoLinkMask} has been
+ * set to nonzero and links are detected in {@link #setText}.
+ * The default is true.
+ *
+ * @attr ref android.R.styleable#TextView_linksClickable
+ */
+ public final void setLinksClickable(boolean whether) {
+ mLinksClickable = whether;
+ }
+
+ /**
+ * Returns whether the movement method will automatically be set to
+ * {@link LinkMovementMethod} if {@link #setAutoLinkMask} has been
+ * set to nonzero and links are detected in {@link #setText}.
+ * The default is true.
+ *
+ * @attr ref android.R.styleable#TextView_linksClickable
+ */
+ public final boolean getLinksClickable() {
+ return mLinksClickable;
+ }
+
+ /**
+ * Returns the list of URLSpans attached to the text
+ * (by {@link Linkify} or otherwise) if any. You can call
+ * {@link URLSpan#getURL} on them to find where they link to
+ * or use {@link Spanned#getSpanStart} and {@link Spanned#getSpanEnd}
+ * to find the region of the text they are attached to.
+ */
+ public URLSpan[] getUrls() {
+ if (mText instanceof Spanned) {
+ return ((Spanned) mText).getSpans(0, mText.length(), URLSpan.class);
+ } else {
+ return new URLSpan[0];
+ }
+ }
+
+ /**
+ * Sets the color of the hint text.
+ *
+ * @attr ref android.R.styleable#TextView_textColorHint
+ */
+ public final void setHintTextColor(int color) {
+ mHintTextColor = ColorStateList.valueOf(color);
+ updateTextColors();
+ }
+
+ /**
+ * Sets the color of the hint text.
+ *
+ * @attr ref android.R.styleable#TextView_textColorHint
+ */
+ public final void setHintTextColor(ColorStateList colors) {
+ mHintTextColor = colors;
+ updateTextColors();
+ }
+
+ /**
+ * <p>Return the color used to paint the hint text.</p>
+ *
+ * @return Returns the list of hint text colors.
+ */
+ public final ColorStateList getHintTextColors() {
+ return mHintTextColor;
+ }
+
+ /**
+ * <p>Return the current color selected to paint the hint text.</p>
+ *
+ * @return Returns the current hint text color.
+ */
+ public final int getCurrentHintTextColor() {
+ return mHintTextColor != null ? mCurHintTextColor : mCurTextColor;
+ }
+
+ /**
+ * Sets the color of links in the text.
+ *
+ * @attr ref android.R.styleable#TextView_textColorLink
+ */
+ public final void setLinkTextColor(int color) {
+ mLinkTextColor = ColorStateList.valueOf(color);
+ updateTextColors();
+ }
+
+ /**
+ * Sets the color of links in the text.
+ *
+ * @attr ref android.R.styleable#TextView_textColorLink
+ */
+ public final void setLinkTextColor(ColorStateList colors) {
+ mLinkTextColor = colors;
+ updateTextColors();
+ }
+
+ /**
+ * <p>Returns the color used to paint links in the text.</p>
+ *
+ * @return Returns the list of link text colors.
+ */
+ public final ColorStateList getLinkTextColors() {
+ return mLinkTextColor;
+ }
+
+ /**
+ * Sets the horizontal alignment of the text and the
+ * vertical gravity that will be used when there is extra space
+ * in the TextView beyond what is required for the text itself.
+ *
+ * @see android.view.Gravity
+ * @attr ref android.R.styleable#TextView_gravity
+ */
+ public void setGravity(int gravity) {
+ if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == 0) {
+ gravity |= Gravity.LEFT;
+ }
+ if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == 0) {
+ gravity |= Gravity.TOP;
+ }
+
+ boolean newLayout = false;
+
+ if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) !=
+ (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK)) {
+ newLayout = true;
+ }
+
+ if (gravity != mGravity) {
+ invalidate();
+ }
+
+ mGravity = gravity;
+
+ if (mLayout != null && newLayout) {
+ // XXX this is heavy-handed because no actual content changes.
+ int want = mLayout.getWidth();
+ int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth();
+
+ makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,
+ mRight - mLeft - getCompoundPaddingLeft() -
+ getCompoundPaddingRight(), true);
+ }
+ }
+
+ /**
+ * Returns the horizontal and vertical alignment of this TextView.
+ *
+ * @see android.view.Gravity
+ * @attr ref android.R.styleable#TextView_gravity
+ */
+ public int getGravity() {
+ return mGravity;
+ }
+
+ /**
+ * @return the flags on the Paint being used to display the text.
+ * @see Paint#getFlags
+ */
+ public int getPaintFlags() {
+ return mTextPaint.getFlags();
+ }
+
+ /**
+ * Sets flags on the Paint being used to display the text and
+ * reflows the text if they are different from the old flags.
+ * @see Paint#setFlags
+ */
+ public void setPaintFlags(int flags) {
+ if (mTextPaint.getFlags() != flags) {
+ mTextPaint.setFlags(flags);
+
+ if (mLayout != null) {
+ nullLayouts();
+ requestLayout();
+ invalidate();
+ }
+ }
+ }
+
+ /**
+ * Sets whether the text should be allowed to be wider than the
+ * View is. If false, it will be wrapped to the width of the View.
+ *
+ * @attr ref android.R.styleable#TextView_scrollHorizontally
+ */
+ public void setHorizontallyScrolling(boolean whether) {
+ mHorizontallyScrolling = whether;
+
+ if (mLayout != null) {
+ nullLayouts();
+ requestLayout();
+ invalidate();
+ }
+ }
+
+ /**
+ * Makes the TextView at least this many lines tall
+ *
+ * @attr ref android.R.styleable#TextView_minLines
+ */
+ public void setMinLines(int minlines) {
+ mMinimum = minlines;
+ mMinMode = LINES;
+
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Makes the TextView at least this many pixels tall
+ *
+ * @attr ref android.R.styleable#TextView_minHeight
+ */
+ public void setMinHeight(int minHeight) {
+ mMinimum = minHeight;
+ mMinMode = PIXELS;
+
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Makes the TextView at most this many lines tall
+ *
+ * @attr ref android.R.styleable#TextView_maxLines
+ */
+ public void setMaxLines(int maxlines) {
+ mMaximum = maxlines;
+ mMaxMode = LINES;
+
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Makes the TextView at most this many pixels tall
+ *
+ * @attr ref android.R.styleable#TextView_maxHeight
+ */
+ public void setMaxHeight(int maxHeight) {
+ mMaximum = maxHeight;
+ mMaxMode = PIXELS;
+
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Makes the TextView exactly this many lines tall
+ *
+ * @attr ref android.R.styleable#TextView_lines
+ */
+ public void setLines(int lines) {
+ mMaximum = mMinimum = lines;
+ mMaxMode = mMinMode = LINES;
+
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Makes the TextView exactly this many pixels tall.
+ * You could do the same thing by specifying this number in the
+ * LayoutParams.
+ *
+ * @attr ref android.R.styleable#TextView_height
+ */
+ public void setHeight(int pixels) {
+ mMaximum = mMinimum = pixels;
+ mMaxMode = mMinMode = PIXELS;
+
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Makes the TextView at least this many ems wide
+ *
+ * @attr ref android.R.styleable#TextView_minEms
+ */
+ public void setMinEms(int minems) {
+ mMinWidth = minems;
+ mMinWidthMode = EMS;
+
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Makes the TextView at least this many pixels wide
+ *
+ * @attr ref android.R.styleable#TextView_minWidth
+ */
+ public void setMinWidth(int minpixels) {
+ mMinWidth = minpixels;
+ mMinWidthMode = PIXELS;
+
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Makes the TextView at most this many ems wide
+ *
+ * @attr ref android.R.styleable#TextView_maxEms
+ */
+ public void setMaxEms(int maxems) {
+ mMaxWidth = maxems;
+ mMaxWidthMode = EMS;
+
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Makes the TextView at most this many pixels wide
+ *
+ * @attr ref android.R.styleable#TextView_maxWidth
+ */
+ public void setMaxWidth(int maxpixels) {
+ mMaxWidth = maxpixels;
+ mMaxWidthMode = PIXELS;
+
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Makes the TextView exactly this many ems wide
+ *
+ * @attr ref android.R.styleable#TextView_ems
+ */
+ public void setEms(int ems) {
+ mMaxWidth = mMinWidth = ems;
+ mMaxWidthMode = mMinWidthMode = EMS;
+
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Makes the TextView exactly this many pixels wide.
+ * You could do the same thing by specifying this number in the
+ * LayoutParams.
+ *
+ * @attr ref android.R.styleable#TextView_width
+ */
+ public void setWidth(int pixels) {
+ mMaxWidth = mMinWidth = pixels;
+ mMaxWidthMode = mMinWidthMode = PIXELS;
+
+ requestLayout();
+ invalidate();
+ }
+
+
+ /**
+ * Sets line spacing for this TextView. Each line will have its height
+ * multiplied by <code>mult</code> and have <code>add</code> added to it.
+ *
+ * @attr ref android.R.styleable#TextView_lineSpacingExtra
+ * @attr ref android.R.styleable#TextView_lineSpacingMultiplier
+ */
+ public void setLineSpacing(float add, float mult) {
+ mSpacingMult = mult;
+ mSpacingAdd = add;
+
+ if (mLayout != null) {
+ nullLayouts();
+ requestLayout();
+ invalidate();
+ }
+ }
+
+ /**
+ * Convenience method: Append the specified text to the TextView's
+ * display buffer, upgrading it to BufferType.EDITABLE if it was
+ * not already editable.
+ */
+ public final void append(CharSequence text) {
+ append(text, 0, text.length());
+ }
+
+ /**
+ * Convenience method: Append the specified text slice to the TextView's
+ * display buffer, upgrading it to BufferType.EDITABLE if it was
+ * not already editable.
+ */
+ public void append(CharSequence text, int start, int end) {
+ if (!(mText instanceof Editable)) {
+ setText(mText, BufferType.EDITABLE);
+ }
+
+ ((Editable) mText).append(text, start, end);
+ }
+
+ private void updateTextColors() {
+ boolean inval = false;
+ int color = mTextColor.getColorForState(getDrawableState(), 0);
+ if (color != mCurTextColor) {
+ mCurTextColor = color;
+ inval = true;
+ }
+ if (mLinkTextColor != null) {
+ color = mLinkTextColor.getColorForState(getDrawableState(), 0);
+ if (color != mTextPaint.linkColor) {
+ mTextPaint.linkColor = color;
+ inval = true;
+ }
+ }
+ if (mHintTextColor != null) {
+ color = mHintTextColor.getColorForState(getDrawableState(), 0);
+ if (color != mCurHintTextColor && mText.length() == 0) {
+ mCurHintTextColor = color;
+ inval = true;
+ }
+ }
+ if (inval) {
+ invalidate();
+ }
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+ if (mTextColor != null && mTextColor.isStateful()
+ || (mHintTextColor != null && mHintTextColor.isStateful())
+ || (mLinkTextColor != null && mLinkTextColor.isStateful())) {
+ updateTextColors();
+ }
+
+ int[] state = getDrawableState();
+ if (mDrawableTop != null && mDrawableTop.isStateful()) {
+ mDrawableTop.setState(state);
+ }
+ if (mDrawableBottom != null && mDrawableBottom.isStateful()) {
+ mDrawableBottom.setState(state);
+ }
+ if (mDrawableLeft != null && mDrawableLeft.isStateful()) {
+ mDrawableLeft.setState(state);
+ }
+ if (mDrawableRight != null && mDrawableRight.isStateful()) {
+ mDrawableRight.setState(state);
+ }
+ }
+
+ /**
+ * User interface state that is stored by TextView for implementing
+ * {@link View#onSaveInstanceState}.
+ */
+ public static class SavedState extends BaseSavedState {
+ int selStart;
+ int selEnd;
+ CharSequence text;
+ boolean frozenWithFocus;
+
+ SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ super.writeToParcel(out, flags);
+ out.writeInt(selStart);
+ out.writeInt(selEnd);
+ out.writeInt(frozenWithFocus ? 1 : 0);
+ TextUtils.writeToParcel(text, out, flags);
+ }
+
+ @Override
+ public String toString() {
+ String str = "TextView.SavedState{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " start=" + selStart + " end=" + selEnd;
+ if (text != null) {
+ str += " text=" + text;
+ }
+ return str + "}";
+ }
+
+ public static final Parcelable.Creator<SavedState> CREATOR
+ = new Parcelable.Creator<SavedState>() {
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+
+ private SavedState(Parcel in) {
+ super(in);
+ selStart = in.readInt();
+ selEnd = in.readInt();
+ frozenWithFocus = (in.readInt() != 0);
+ text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
+ }
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ Parcelable superState = super.onSaveInstanceState();
+
+ // Save state if we are forced to
+ boolean save = mFreezesText;
+ int start = 0;
+ int end = 0;
+
+ if (mText != null) {
+ start = Selection.getSelectionStart(mText);
+ end = Selection.getSelectionEnd(mText);
+ if (start >= 0 || end >= 0) {
+ // Or save state if there is a selection
+ save = true;
+ }
+ }
+
+ if (save) {
+ SavedState ss = new SavedState(superState);
+ // XXX Should also save the current scroll position!
+ ss.selStart = start;
+ ss.selEnd = end;
+
+ if (mText instanceof Spanned) {
+ /*
+ * Calling setText() strips off any ChangeWatchers;
+ * strip them now to avoid leaking references.
+ * But do it to a copy so that if there are any
+ * further changes to the text of this view, it
+ * won't get into an inconsistent state.
+ */
+
+ Spannable sp = new SpannableString(mText);
+
+ for (ChangeWatcher cw :
+ sp.getSpans(0, sp.length(), ChangeWatcher.class)) {
+ sp.removeSpan(cw);
+ }
+
+ ss.text = sp;
+ } else {
+ ss.text = mText.toString();
+ }
+
+ if (isFocused() && start >= 0 && end >= 0) {
+ ss.frozenWithFocus = true;
+ }
+
+ return ss;
+ }
+
+ return null;
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ SavedState ss = (SavedState)state;
+ super.onRestoreInstanceState(ss.getSuperState());
+
+ // XXX restore buffer type too, as well as lots of other stuff
+ if (ss.text != null) {
+ setText(ss.text);
+ }
+
+ if (ss.selStart >= 0 && ss.selEnd >= 0) {
+ if (mText instanceof Spannable) {
+ int len = mText.length();
+
+ if (ss.selStart > len || ss.selEnd > len) {
+ String restored = "";
+
+ if (ss.text != null) {
+ restored = "(restored) ";
+ }
+
+ Log.e("TextView", "Saved cursor position " + ss.selStart +
+ "/" + ss.selEnd + " out of range for " + restored +
+ "text " + mText);
+ } else {
+ Selection.setSelection((Spannable) mText, ss.selStart,
+ ss.selEnd);
+
+ if (ss.frozenWithFocus) {
+ mFrozenWithFocus = true;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Control whether this text view saves its entire text contents when
+ * freezing to an icicle, in addition to dynamic state such as cursor
+ * position. By default this is false, not saving the text. Set to true
+ * if the text in the text view is not being saved somewhere else in
+ * persistent storage (such as in a content provider) so that if the
+ * view is later thawed the user will not lose their data.
+ *
+ * @param freezesText Controls whether a frozen icicle should include the
+ * entire text data: true to include it, false to not.
+ *
+ * @attr ref android.R.styleable#TextView_freezesText
+ */
+ public void setFreezesText(boolean freezesText) {
+ mFreezesText = freezesText;
+ }
+
+ /**
+ * Return whether this text view is including its entire text contents
+ * in frozen icicles.
+ *
+ * @return Returns true if text is included, false if it isn't.
+ *
+ * @see #setFreezesText
+ */
+ public boolean getFreezesText() {
+ return mFreezesText;
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Sets the Factory used to create new Editables.
+ */
+ public final void setEditableFactory(Editable.Factory factory) {
+ mEditableFactory = factory;
+ setText(mText);
+ }
+
+ /**
+ * Sets the Factory used to create new Spannables.
+ */
+ public final void setSpannableFactory(Spannable.Factory factory) {
+ mSpannableFactory = factory;
+ setText(mText);
+ }
+
+ /**
+ * Sets the string value of the TextView. TextView <em>does not</em> accept
+ * HTML-like formatting, which you can do with text strings in XML resource files.
+ * To style your strings, attach android.text.style.* objects to a
+ * {@link android.text.SpannableString SpannableString}, or see
+ * <a href="{@docRoot}reference/available-resources.html#stringresources">
+ * String Resources</a> for an example of setting formatted text in the XML resource file.
+ *
+ * @attr ref android.R.styleable#TextView_text
+ */
+ public final void setText(CharSequence text) {
+ setText(text, mBufferType);
+ }
+
+ /**
+ * Like {@link #setText(CharSequence)},
+ * except that the cursor position (if any) is retained in the new text.
+ *
+ * @param text The new text to place in the text view.
+ *
+ * @see #setText(CharSequence)
+ */
+ public final void setTextKeepState(CharSequence text) {
+ setTextKeepState(text, mBufferType);
+ }
+
+ /**
+ * Sets the text that this TextView is to display (see
+ * {@link #setText(CharSequence)}) and also sets whether it is stored
+ * in a styleable/spannable buffer and whether it is editable.
+ *
+ * @attr ref android.R.styleable#TextView_text
+ * @attr ref android.R.styleable#TextView_bufferType
+ */
+ public void setText(CharSequence text, BufferType type) {
+ setText(text, type, true, 0);
+
+ if (mCharWrapper != null) {
+ mCharWrapper.mChars = null;
+ }
+ }
+
+ private void setText(CharSequence text, BufferType type,
+ boolean notifyBefore, int oldlen) {
+ if (text == null) {
+ text = "";
+ }
+
+ int n = mFilters.length;
+ for (int i = 0; i < n; i++) {
+ CharSequence out = mFilters[i].filter(text, 0, text.length(),
+ EMPTY_SPANNED, 0, 0);
+ if (out != null) {
+ text = out;
+ }
+ }
+
+ if (notifyBefore) {
+ if (mText != null) {
+ oldlen = mText.length();
+ sendBeforeTextChanged(mText, 0, oldlen, text.length());
+ } else {
+ sendBeforeTextChanged("", 0, 0, text.length());
+ }
+ }
+
+ if (type == BufferType.EDITABLE || mInput != null) {
+ Editable t = mEditableFactory.newEditable(text);
+ text = t;
+
+ setFilters(t, mFilters);
+ } else if (type == BufferType.SPANNABLE || mMovement != null) {
+ text = mSpannableFactory.newSpannable(text);
+ } else if (!(text instanceof CharWrapper)) {
+ text = TextUtils.stringOrSpannedString(text);
+ }
+
+ if (mAutoLinkMask != 0) {
+ Spannable s2;
+
+ if (type == BufferType.EDITABLE || text instanceof Spannable) {
+ s2 = (Spannable) text;
+ } else {
+ s2 = mSpannableFactory.newSpannable(text);
+ }
+
+ if (Linkify.addLinks(s2, mAutoLinkMask)) {
+ text = s2;
+ type = (type == BufferType.EDITABLE) ? BufferType.EDITABLE : BufferType.SPANNABLE;
+
+ /*
+ * We must go ahead and set the text before changing the
+ * movement method, because setMovementMethod() may call
+ * setText() again to try to upgrade the buffer type.
+ */
+ mText = text;
+
+ if (mLinksClickable) {
+ setMovementMethod(LinkMovementMethod.getInstance());
+ }
+ }
+ }
+
+ mBufferType = type;
+ mText = text;
+
+ if (mTransformation == null)
+ mTransformed = text;
+ else
+ mTransformed = mTransformation.getTransformation(text, this);
+
+ final int textLength = text.length();
+
+ if (text instanceof Spannable) {
+ Spannable sp = (Spannable) text;
+
+ // Remove any ChangeWatchers that might have come
+ // from other TextViews.
+ final ChangeWatcher[] watchers = sp.getSpans(0, sp.length(), ChangeWatcher.class);
+ final int count = watchers.length;
+ for (int i = 0; i < count; i++)
+ sp.removeSpan(watchers[i]);
+
+ if (mChangeWatcher == null)
+ mChangeWatcher = new ChangeWatcher();
+
+ sp.setSpan(mChangeWatcher, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE |
+ (PRIORITY << Spanned.SPAN_PRIORITY_SHIFT));
+
+ if (mInput != null) {
+ sp.setSpan(mInput, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+ }
+
+ if (mTransformation != null) {
+ sp.setSpan(mTransformation, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+
+ }
+
+ if (mMovement != null) {
+ mMovement.initialize(this, (Spannable) text);
+
+ /*
+ * Initializing the movement method will have set the
+ * selection, so reset mSelectionMoved to keep that from
+ * interfering with the normal on-focus selection-setting.
+ */
+ mSelectionMoved = false;
+ }
+ }
+
+ if (mLayout != null) {
+ checkForRelayout();
+ }
+
+ sendOnTextChanged(text, 0, oldlen, textLength);
+ onTextChanged(text, 0, oldlen, textLength);
+ }
+
+ /**
+ * Sets the TextView to display the specified slice of the specified
+ * char array. You must promise that you will not change the contents
+ * of the array except for right before another call to setText(),
+ * since the TextView has no way to know that the text
+ * has changed and that it needs to invalidate and re-layout.
+ */
+ public final void setText(char[] text, int start, int len) {
+ int oldlen = 0;
+
+ if (start < 0 || len < 0 || start + len > text.length) {
+ throw new IndexOutOfBoundsException(start + ", " + len);
+ }
+
+ /*
+ * We must do the before-notification here ourselves because if
+ * the old text is a CharWrapper we destroy it before calling
+ * into the normal path.
+ */
+ if (mText != null) {
+ oldlen = mText.length();
+ sendBeforeTextChanged(mText, 0, oldlen, len);
+ } else {
+ sendBeforeTextChanged("", 0, 0, len);
+ }
+
+ if (mCharWrapper == null) {
+ mCharWrapper = new CharWrapper(text, start, len);
+ } else {
+ mCharWrapper.set(text, start, len);
+ }
+
+ setText(mCharWrapper, mBufferType, false, oldlen);
+ }
+
+ private static class CharWrapper
+ implements CharSequence, GetChars, GraphicsOperations {
+ private char[] mChars;
+ private int mStart, mLength;
+
+ public CharWrapper(char[] chars, int start, int len) {
+ mChars = chars;
+ mStart = start;
+ mLength = len;
+ }
+
+ /* package */ void set(char[] chars, int start, int len) {
+ mChars = chars;
+ mStart = start;
+ mLength = len;
+ }
+
+ public int length() {
+ return mLength;
+ }
+
+ public char charAt(int off) {
+ return mChars[off + mStart];
+ }
+
+ public String toString() {
+ return new String(mChars, mStart, mLength);
+ }
+
+ public CharSequence subSequence(int start, int end) {
+ if (start < 0 || end < 0 || start > mLength || end > mLength) {
+ throw new IndexOutOfBoundsException(start + ", " + end);
+ }
+
+ return new String(mChars, start + mStart, end - start);
+ }
+
+ public void getChars(int start, int end, char[] buf, int off) {
+ if (start < 0 || end < 0 || start > mLength || end > mLength) {
+ throw new IndexOutOfBoundsException(start + ", " + end);
+ }
+
+ System.arraycopy(mChars, start + mStart, buf, off, end - start);
+ }
+
+ public void drawText(Canvas c, int start, int end,
+ float x, float y, Paint p) {
+ c.drawText(mChars, start + mStart, end - start, x, y, p);
+ }
+
+ public float measureText(int start, int end, Paint p) {
+ return p.measureText(mChars, start + mStart, end - start);
+ }
+
+ public int getTextWidths(int start, int end, float[] widths, Paint p) {
+ return p.getTextWidths(mChars, start + mStart, end - start, widths);
+ }
+ }
+
+ /**
+ * Like {@link #setText(CharSequence, android.widget.TextView.BufferType)},
+ * except that the cursor position (if any) is retained in the new text.
+ *
+ * @see #setText(CharSequence, android.widget.TextView.BufferType)
+ */
+ public final void setTextKeepState(CharSequence text, BufferType type) {
+ int start = getSelectionStart();
+ int end = getSelectionEnd();
+ int len = text.length();
+
+ setText(text, type);
+
+ if (start >= 0 || end >= 0) {
+ if (mText instanceof Spannable) {
+ Selection.setSelection((Spannable) mText,
+ Math.max(0, Math.min(start, len)),
+ Math.max(0, Math.min(end, len)));
+ }
+ }
+ }
+
+ public final void setText(int resid) {
+ setText(getContext().getResources().getText(resid));
+ }
+
+ public final void setText(int resid, BufferType type) {
+ setText(getContext().getResources().getText(resid), type);
+ }
+
+ /**
+ * Sets the text to be displayed when the text of the TextView is empty.
+ * Null means to use the normal empty text. The hint does not
+ * currently participate in determining the size of the view.
+ *
+ * @attr ref android.R.styleable#TextView_hint
+ */
+ public final void setHint(CharSequence hint) {
+ mHint = TextUtils.stringOrSpannedString(hint);
+
+ if (mLayout != null) {
+ checkForRelayout();
+ }
+
+ if (mText.length() == 0)
+ invalidate();
+ }
+
+ /**
+ * Sets the text to be displayed when the text of the TextView is empty,
+ * from a resource.
+ *
+ * @attr ref android.R.styleable#TextView_hint
+ */
+ public final void setHint(int resid) {
+ setHint(getContext().getResources().getText(resid));
+ }
+
+ /**
+ * Returns the hint that is displayed when the text of the TextView
+ * is empty.
+ *
+ * @attr ref android.R.styleable#TextView_hint
+ */
+ public CharSequence getHint() {
+ return mHint;
+ }
+
+ /**
+ * Returns the error message that was set to be displayed with
+ * {@link #setError}, or <code>null</code> if no error was set
+ * or if it the error was cleared by the widget after user input.
+ */
+ public CharSequence getError() {
+ return mError;
+ }
+
+ /**
+ * Sets the right-hand compound drawable of the TextView to the "error"
+ * icon and sets an error message that will be displayed in a popup when
+ * the TextView has focus. The icon and error message will be reset to
+ * null when any key events cause changes to the TextView's text. If the
+ * <code>error</code> is <code>null</code>, the error message and icon
+ * will be cleared.
+ */
+ public void setError(CharSequence error) {
+ if (error == null) {
+ setError(null, null);
+ } else {
+ Drawable dr = getContext().getResources().
+ getDrawable(com.android.internal.R.drawable.
+ indicator_input_error);
+
+ dr.setBounds(0, 0, dr.getIntrinsicWidth(), dr.getIntrinsicHeight());
+ setError(error, dr);
+ }
+ }
+
+ /**
+ * Sets the right-hand compound drawable of the TextView to the specified
+ * icon and sets an error message that will be displayed in a popup when
+ * the TextView has focus. The icon and error message will be reset to
+ * null when any key events cause changes to the TextView's text. The
+ * drawable must already have had {@link Drawable#setBounds} set on it.
+ * If the <code>error</code> is <code>null</code>, the error message will
+ * be cleared (and you should provide a <code>null</code> icon as well).
+ */
+ public void setError(CharSequence error, Drawable icon) {
+ error = TextUtils.stringOrSpannedString(error);
+
+ mError = error;
+ mErrorWasChanged = true;
+ setCompoundDrawables(mDrawableLeft, mDrawableTop,
+ icon, mDrawableBottom);
+
+ if (error == null) {
+ if (mPopup != null) {
+ if (mPopup.isShowing()) {
+ mPopup.dismiss();
+ }
+
+ mPopup = null;
+ }
+ } else {
+ if (isFocused()) {
+ showError();
+ }
+ }
+ }
+
+ private void showError() {
+ if (mPopup == null) {
+ LayoutInflater inflater = LayoutInflater.from(getContext());
+ TextView err = (TextView) inflater.inflate(com.android.internal.R.layout.textview_hint,
+ null);
+
+ mPopup = new PopupWindow(err, 200, 50);
+ mPopup.setFocusable(false);
+ }
+
+ TextView tv = (TextView) mPopup.getContentView();
+ chooseSize(mPopup, mError, tv);
+ tv.setText(mError);
+
+ mPopup.showAsDropDown(this, getErrorX(), getErrorY());
+ }
+
+ /**
+ * Returns the Y offset to make the pointy top of the error point
+ * at the middle of the error icon.
+ */
+ private int getErrorX() {
+ /*
+ * The "25" is the distance between the point and the right edge
+ * of the background
+ */
+
+ return getWidth() - mPopup.getWidth()
+ - getPaddingRight() - mDrawableSizeRight / 2 + 25;
+ }
+
+ /**
+ * Returns the Y offset to make the pointy top of the error point
+ * at the bottom of the error icon.
+ */
+ private int getErrorY() {
+ /*
+ * Compound, not extended, because the icon is not clipped
+ * if the text height is smaller.
+ */
+ int vspace = mBottom - mTop -
+ getCompoundPaddingBottom() - getCompoundPaddingTop();
+
+ int icontop = getCompoundPaddingTop() +
+ (vspace - mDrawableHeightRight) / 2;
+
+ /*
+ * The "2" is the distance between the point and the top edge
+ * of the background.
+ */
+
+ return icontop + mDrawableHeightRight - getHeight() - 2;
+ }
+
+ private void hideError() {
+ if (mPopup != null) {
+ if (mPopup.isShowing()) {
+ mPopup.dismiss();
+ }
+ }
+ }
+
+ private void chooseSize(PopupWindow pop, CharSequence text, TextView tv) {
+ int wid = tv.getPaddingLeft() + tv.getPaddingRight();
+ int ht = tv.getPaddingTop() + tv.getPaddingBottom();
+
+ /*
+ * Figure out how big the text would be if we laid it out to the
+ * full width of this view minus the border.
+ */
+ int cap = getWidth() - wid;
+ if (cap < 0) {
+ cap = 200; // We must not be measured yet -- setFrame() will fix it.
+ }
+
+ Layout l = new StaticLayout(text, tv.getPaint(), cap,
+ Layout.Alignment.ALIGN_NORMAL, 1, 0, true);
+ float max = 0;
+ for (int i = 0; i < l.getLineCount(); i++) {
+ max = Math.max(max, l.getLineWidth(i));
+ }
+
+ /*
+ * Now set the popup size to be big enough for the text plus the border.
+ */
+ pop.setWidth(wid + (int) Math.ceil(max));
+ pop.setHeight(ht + l.getHeight());
+ }
+
+
+ @Override
+ protected boolean setFrame(int l, int t, int r, int b) {
+ boolean result = super.setFrame(l, t, r, b);
+
+ if (mPopup != null) {
+ TextView tv = (TextView) mPopup.getContentView();
+ chooseSize(mPopup, mError, tv);
+ mPopup.update(this, getErrorX(), getErrorY(), -1, -1);
+ }
+
+ return result;
+ }
+
+ /**
+ * Sets the list of input filters that will be used if the buffer is
+ * Editable. Has no effect otherwise.
+ *
+ * @attr ref android.R.styleable#TextView_maxLength
+ */
+ public void setFilters(InputFilter[] filters) {
+ if (filters == null) {
+ throw new IllegalArgumentException();
+ }
+
+ mFilters = filters;
+
+ if (mText instanceof Editable) {
+ setFilters((Editable) mText, filters);
+ }
+ }
+
+ /**
+ * Sets the list of input filters on the specified Editable,
+ * and includes mInput in the list if it is an InputFilter.
+ */
+ private void setFilters(Editable e, InputFilter[] filters) {
+ if (mInput instanceof InputFilter) {
+ InputFilter[] nf = new InputFilter[filters.length + 1];
+
+ System.arraycopy(filters, 0, nf, 0, filters.length);
+ nf[filters.length] = (InputFilter) mInput;
+
+ e.setFilters(nf);
+ } else {
+ e.setFilters(filters);
+ }
+ }
+
+ /**
+ * Returns the current list of input filters.
+ */
+ public InputFilter[] getFilters() {
+ return mFilters;
+ }
+
+ /////////////////////////////////////////////////////////////////////////
+
+ private int getVerticalOffset(boolean forceNormal) {
+ int voffset = 0;
+ final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
+
+ Layout l = mLayout;
+ if (!forceNormal && mText.length() == 0 && mHintLayout != null) {
+ l = mHintLayout;
+ }
+
+ if (gravity != Gravity.TOP) {
+ int boxht;
+
+ if (l == mHintLayout) {
+ boxht = getMeasuredHeight() - getCompoundPaddingTop() -
+ getCompoundPaddingBottom();
+ } else {
+ boxht = getMeasuredHeight() - getExtendedPaddingTop() -
+ getExtendedPaddingBottom();
+ }
+ int textht = l.getHeight();
+
+ if (textht < boxht) {
+ if (gravity == Gravity.BOTTOM)
+ voffset = boxht - textht;
+ else // (gravity == Gravity.CENTER_VERTICAL)
+ voffset = (boxht - textht) >> 1;
+ }
+ }
+ return voffset;
+ }
+
+ private int getBottomVerticalOffset(boolean forceNormal) {
+ int voffset = 0;
+ final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
+
+ Layout l = mLayout;
+ if (!forceNormal && mText.length() == 0 && mHintLayout != null) {
+ l = mHintLayout;
+ }
+
+ if (gravity != Gravity.BOTTOM) {
+ int boxht;
+
+ if (l == mHintLayout) {
+ boxht = getMeasuredHeight() - getCompoundPaddingTop() -
+ getCompoundPaddingBottom();
+ } else {
+ boxht = getMeasuredHeight() - getExtendedPaddingTop() -
+ getExtendedPaddingBottom();
+ }
+ int textht = l.getHeight();
+
+ if (textht < boxht) {
+ if (gravity == Gravity.TOP)
+ voffset = boxht - textht;
+ else // (gravity == Gravity.CENTER_VERTICAL)
+ voffset = (boxht - textht) >> 1;
+ }
+ }
+ return voffset;
+ }
+
+ private void invalidateCursorPath() {
+ if (mHighlightPathBogus) {
+ invalidateCursor();
+ } else {
+ synchronized (sTempRect) {
+ mHighlightPath.computeBounds(sTempRect, false);
+
+ int left = getCompoundPaddingLeft();
+ int top = getExtendedPaddingTop() + getVerticalOffset(true);
+
+ invalidate((int) sTempRect.left + left,
+ (int) sTempRect.top + top,
+ (int) sTempRect.right + left + 1,
+ (int) sTempRect.bottom + top + 1);
+ }
+ }
+ }
+
+ private void invalidateCursor() {
+ int where = Selection.getSelectionEnd(mText);
+
+ invalidateCursor(where, where, where);
+ }
+
+ private void invalidateCursor(int a, int b, int c) {
+ if (mLayout == null) {
+ invalidate();
+ } else {
+ if (a >= 0 || b >= 0 || c >= 0) {
+ int first = Math.min(Math.min(a, b), c);
+ int last = Math.max(Math.max(a, b), c);
+
+ int line = mLayout.getLineForOffset(first);
+ int top = mLayout.getLineTop(line);
+
+ // This is ridiculous, but the descent from the line above
+ // can hang down into the line we really want to redraw,
+ // so we have to invalidate part of the line above to make
+ // sure everything that needs to be redrawn really is.
+ // (But not the whole line above, because that would cause
+ // the same problem with the descenders on the line above it!)
+ if (line > 0) {
+ top -= mLayout.getLineDescent(line - 1);
+ }
+
+ int line2;
+
+ if (first == last)
+ line2 = line;
+ else
+ line2 = mLayout.getLineForOffset(last);
+
+ int bottom = mLayout.getLineTop(line2 + 1);
+ int voffset = getVerticalOffset(true);
+
+ int left = getCompoundPaddingLeft() + mScrollX;
+ invalidate(left, top + voffset + getExtendedPaddingTop(),
+ left + getWidth() - getCompoundPaddingLeft() -
+ getCompoundPaddingRight(),
+ bottom + voffset + getExtendedPaddingTop());
+ }
+ }
+ }
+
+ private void registerForPreDraw() {
+ final ViewTreeObserver observer = getViewTreeObserver();
+ if (observer == null) {
+ return;
+ }
+
+ if (mPreDrawState == PREDRAW_NOT_REGISTERED) {
+ observer.addOnPreDrawListener(this);
+ mPreDrawState = PREDRAW_PENDING;
+ } else if (mPreDrawState == PREDRAW_DONE) {
+ mPreDrawState = PREDRAW_PENDING;
+ }
+
+ // else state is PREDRAW_PENDING, so keep waiting.
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean onPreDraw() {
+ if (mPreDrawState != PREDRAW_PENDING) {
+ return true;
+ }
+
+ if (mLayout == null) {
+ assumeLayout();
+ }
+
+ boolean changed = false;
+
+ if (mMovement != null) {
+ int curs = Selection.getSelectionEnd(mText);
+
+ /*
+ * TODO: This should really only keep the end in view if
+ * it already was before the text changed. I'm not sure
+ * of a good way to tell from here if it was.
+ */
+ if (curs < 0 &&
+ (mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
+ curs = mText.length();
+ }
+
+ if (curs >= 0) {
+ changed = bringPointIntoView(curs);
+ }
+ } else {
+ changed = bringTextIntoView();
+ }
+
+ mPreDrawState = PREDRAW_DONE;
+ return !changed;
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+
+ if (mPreDrawState != PREDRAW_NOT_REGISTERED) {
+ final ViewTreeObserver observer = getViewTreeObserver();
+ if (observer != null) {
+ observer.removeOnPreDrawListener(this);
+ mPreDrawState = PREDRAW_NOT_REGISTERED;
+ }
+ }
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ // Draw the background for this view
+ super.onDraw(canvas);
+
+ final int compoundPaddingLeft = getCompoundPaddingLeft();
+ final int compoundPaddingTop = getCompoundPaddingTop();
+ final int compoundPaddingRight = getCompoundPaddingRight();
+ final int compoundPaddingBottom = getCompoundPaddingBottom();
+ final int scrollX = mScrollX;
+ final int scrollY = mScrollY;
+ final int right = mRight;
+ final int left = mLeft;
+ final int bottom = mBottom;
+ final int top = mTop;
+
+ if (mDrawables) {
+ /*
+ * Compound, not extended, because the icon is not clipped
+ * if the text height is smaller.
+ */
+
+ int vspace = bottom - top - compoundPaddingBottom - compoundPaddingTop;
+ int hspace = right - left - compoundPaddingRight - compoundPaddingLeft;
+
+ if (mDrawableLeft != null) {
+ canvas.save();
+ canvas.translate(scrollX + mPaddingLeft,
+ scrollY + compoundPaddingTop +
+ (vspace - mDrawableHeightLeft) / 2);
+ mDrawableLeft.draw(canvas);
+ canvas.restore();
+ }
+
+ if (mDrawableRight != null) {
+ canvas.save();
+ canvas.translate(scrollX + right - left - mPaddingRight - mDrawableSizeRight,
+ scrollY + compoundPaddingTop + (vspace - mDrawableHeightRight) / 2);
+ mDrawableRight.draw(canvas);
+ canvas.restore();
+ }
+
+ if (mDrawableTop != null) {
+ canvas.save();
+ canvas.translate(scrollX + compoundPaddingLeft + (hspace - mDrawableWidthTop) / 2,
+ scrollY + mPaddingTop);
+ mDrawableTop.draw(canvas);
+ canvas.restore();
+ }
+
+ if (mDrawableBottom != null) {
+ canvas.save();
+ canvas.translate(scrollX + compoundPaddingLeft +
+ (hspace - mDrawableWidthBottom) / 2,
+ scrollY + bottom - top - mPaddingBottom - mDrawableSizeBottom);
+ mDrawableBottom.draw(canvas);
+ canvas.restore();
+ }
+ }
+
+ if (mPreDrawState == PREDRAW_DONE) {
+ final ViewTreeObserver observer = getViewTreeObserver();
+ if (observer != null) {
+ observer.removeOnPreDrawListener(this);
+ mPreDrawState = PREDRAW_NOT_REGISTERED;
+ }
+ }
+
+ int color = mCurTextColor;
+
+ if (mLayout == null) {
+ assumeLayout();
+ }
+
+ Layout layout = mLayout;
+ int cursorcolor = color;
+
+ if (mHint != null && mText.length() == 0) {
+ if (mHintTextColor != null) {
+ color = mCurHintTextColor;
+ }
+
+ layout = mHintLayout;
+ }
+
+ mTextPaint.setColor(color);
+ mTextPaint.drawableState = getDrawableState();
+
+ canvas.save();
+ /* Would be faster if we didn't have to do this. Can we chop the
+ (displayable) text so that we don't need to do this ever?
+ */
+
+ int extendedPaddingTop = getExtendedPaddingTop();
+ int extendedPaddingBottom = getExtendedPaddingBottom();
+
+ float clipLeft = compoundPaddingLeft + scrollX;
+ float clipTop = extendedPaddingTop + scrollY;
+ float clipRight = right - left - compoundPaddingRight + scrollX;
+ float clipBottom = bottom - top - extendedPaddingBottom + scrollY;
+
+ if (mShadowRadius != 0) {
+ clipLeft += Math.min(0, mShadowDx - mShadowRadius);
+ clipRight += Math.max(0, mShadowDx + mShadowRadius);
+
+ clipTop += Math.min(0, mShadowDy - mShadowRadius);
+ clipBottom += Math.max(0, mShadowDy + mShadowRadius);
+ }
+
+ canvas.clipRect(clipLeft, clipTop, clipRight, clipBottom);
+
+ int voffsetText = 0;
+ int voffsetCursor = 0;
+
+ // translate in by our padding
+ {
+ /* shortcircuit calling getVerticaOffset() */
+ if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
+ voffsetText = getVerticalOffset(false);
+ voffsetCursor = getVerticalOffset(true);
+ }
+ canvas.translate(compoundPaddingLeft, extendedPaddingTop + voffsetText);
+ }
+
+ Path highlight = null;
+
+ // If there is no movement method, then there can be no selection.
+ // Check that first and attempt to skip everything having to do with
+ // the cursor.
+ // XXX This is not strictly true -- a program could set the
+ // selection manually if it really wanted to.
+ if (mMovement != null && (isFocused() || isPressed())) {
+ int start = Selection.getSelectionStart(mText);
+ int end = Selection.getSelectionEnd(mText);
+
+ if (mCursorVisible && start >= 0 && isEnabled()) {
+ if (mHighlightPath == null)
+ mHighlightPath = new Path();
+
+ if (start == end) {
+ if ((SystemClock.uptimeMillis() - mShowCursor) % (2 * BLINK)
+ < BLINK) {
+ if (mHighlightPathBogus) {
+ mHighlightPath.reset();
+ mLayout.getCursorPath(start, mHighlightPath, mText);
+ mHighlightPathBogus = false;
+ }
+
+ // XXX should pass to skin instead of drawing directly
+ mHighlightPaint.setColor(cursorcolor);
+ mHighlightPaint.setStyle(Paint.Style.STROKE);
+
+ highlight = mHighlightPath;
+ }
+ } else {
+ if (mHighlightPathBogus) {
+ mHighlightPath.reset();
+ mLayout.getSelectionPath(start, end, mHighlightPath);
+ mHighlightPathBogus = false;
+ }
+
+ // XXX should pass to skin instead of drawing directly
+ mHighlightPaint.setColor(mHighlightColor);
+ mHighlightPaint.setStyle(Paint.Style.FILL);
+
+ highlight = mHighlightPath;
+ }
+ }
+ }
+
+ /* 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);
+ }
+ }
+ */
+
+ layout.draw(canvas, highlight, mHighlightPaint, voffsetCursor - voffsetText);
+
+ /* Comment out until we decide what to do about animations
+ if (currentTransformation != null) {
+ mTextPaint.setLinearTextOn(isLinearTextOn);
+ }
+ */
+
+ canvas.restore();
+ }
+
+ @Override
+ public void getFocusedRect(Rect r) {
+ if (mLayout == null) {
+ super.getFocusedRect(r);
+ return;
+ }
+
+ int sel = getSelectionEnd();
+ if (sel < 0) {
+ super.getFocusedRect(r);
+ return;
+ }
+
+ int line = mLayout.getLineForOffset(sel);
+ r.top = mLayout.getLineTop(line);
+ r.bottom = mLayout.getLineBottom(line);
+
+ r.left = (int) mLayout.getPrimaryHorizontal(sel);
+ r.right = r.left + 1;
+ }
+
+ /**
+ * Return the number of lines of text, or 0 if the internal Layout has not
+ * been built.
+ */
+ public int getLineCount() {
+ return mLayout != null ? mLayout.getLineCount() : 0;
+ }
+
+ /**
+ * Return the baseline for the specified line (0...getLineCount() - 1)
+ * If bounds is not null, return the top, left, right, bottom extents
+ * of the specified line in it. If the internal Layout has not been built,
+ * return 0 and set bounds to (0, 0, 0, 0)
+ * @param line which line to examine (0..getLineCount() - 1)
+ * @param bounds Optional. If not null, it returns the extent of the line
+ * @return the Y-coordinate of the baseline
+ */
+ public int getLineBounds(int line, Rect bounds) {
+ if (mLayout == null) {
+ if (bounds != null) {
+ bounds.set(0, 0, 0, 0);
+ }
+ return 0;
+ }
+ else {
+ int baseline = mLayout.getLineBounds(line, bounds);
+
+ int voffset = getExtendedPaddingTop();
+ if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
+ voffset += getVerticalOffset(true);
+ }
+ if (bounds != null) {
+ bounds.offset(getCompoundPaddingLeft(), voffset);
+ }
+ return baseline + voffset;
+ }
+ }
+
+ @Override
+ public int getBaseline() {
+ if (mLayout == null) {
+ return super.getBaseline();
+ }
+
+ int voffset = 0;
+ if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
+ voffset = getVerticalOffset(true);
+ }
+
+ return getExtendedPaddingTop() + voffset + mLayout.getLineBaseline(0);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (!isEnabled()) {
+ return super.onKeyDown(keyCode, event);
+ }
+
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ case KeyEvent.KEYCODE_ENTER:
+ if (mSingleLine && mInput != null) {
+ return super.onKeyDown(keyCode, event);
+ }
+ }
+
+ if (mInput != null) {
+ /*
+ * Keep track of what the error was before doing the input
+ * so that if an input filter changed the error, we leave
+ * that error showing. Otherwise, we take down whatever
+ * error was showing when the user types something.
+ */
+ mErrorWasChanged = false;
+
+ if (mInput.onKeyDown(this, (Editable) mText, keyCode, event)) {
+ if (mError != null && !mErrorWasChanged) {
+ setError(null, null);
+ }
+ return true;
+ }
+ }
+
+ // bug 650865: sometimes we get a key event before a layout.
+ // don't try to move around if we don't know the layout.
+
+ if (mMovement != null && mLayout != null)
+ if (mMovement.onKeyDown(this, (Spannable)mText, keyCode, event))
+ return true;
+
+ return super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ if (!isEnabled()) {
+ return super.onKeyUp(keyCode, event);
+ }
+
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ case KeyEvent.KEYCODE_ENTER:
+ if (mSingleLine && mInput != null) {
+ /*
+ * If there is a click listener, just call through to
+ * super, which will invoke it.
+ *
+ * If there isn't a click listener, try to advance focus,
+ * but still call through to super, which will reset the
+ * pressed state and longpress state. (It will also
+ * call performClick(), but that won't do anything in
+ * this case.)
+ */
+ if (mOnClickListener == null) {
+ View v = focusSearch(FOCUS_DOWN);
+
+ if (v != null) {
+ if (!v.requestFocus(FOCUS_DOWN)) {
+ throw new IllegalStateException("focus search returned a view " +
+ "that wasn't able to take focus!");
+ }
+
+ /*
+ * Return true because we handled the key; super
+ * will return false because there was no click
+ * listener.
+ */
+ super.onKeyUp(keyCode, event);
+ return true;
+ }
+ }
+
+ return super.onKeyUp(keyCode, event);
+ }
+ }
+
+ if (mInput != null)
+ if (mInput.onKeyUp(this, (Editable) mText, keyCode, event))
+ return true;
+
+ if (mMovement != null && mLayout != null)
+ if (mMovement.onKeyUp(this, (Spannable) mText, keyCode, event))
+ return true;
+
+ return super.onKeyUp(keyCode, event);
+ }
+
+ private void nullLayouts() {
+ if (mLayout instanceof BoringLayout && mSavedLayout == null) {
+ mSavedLayout = (BoringLayout) mLayout;
+ }
+ if (mHintLayout instanceof BoringLayout && mSavedHintLayout == null) {
+ mSavedHintLayout = (BoringLayout) mHintLayout;
+ }
+
+ mLayout = mHintLayout = null;
+ }
+
+ /**
+ * Make a new Layout based on the already-measured size of the view,
+ * on the assumption that it was measured correctly at some point.
+ */
+ private void assumeLayout() {
+ int width = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
+
+ if (width < 1) {
+ width = 0;
+ }
+
+ int physicalWidth = width;
+
+ if (mHorizontallyScrolling) {
+ width = VERY_WIDE;
+ }
+
+ makeNewLayout(width, physicalWidth, UNKNOWN_BORING, UNKNOWN_BORING,
+ physicalWidth, false);
+ }
+
+ /**
+ * The width passed in is now the desired layout width,
+ * not the full view width with padding.
+ * {@hide}
+ */
+ protected void makeNewLayout(int w, int hintWidth,
+ BoringLayout.Metrics boring,
+ BoringLayout.Metrics hintBoring,
+ int ellipsisWidth, boolean bringIntoView) {
+ mHighlightPathBogus = true;
+
+ if (w < 0) {
+ w = 0;
+ }
+ if (hintWidth < 0) {
+ hintWidth = 0;
+ }
+
+ Layout.Alignment alignment;
+ switch (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
+ case Gravity.CENTER_HORIZONTAL:
+ alignment = Layout.Alignment.ALIGN_CENTER;
+ break;
+
+ case Gravity.RIGHT:
+ alignment = Layout.Alignment.ALIGN_OPPOSITE;
+ break;
+
+ default:
+ alignment = Layout.Alignment.ALIGN_NORMAL;
+ }
+
+ if (mText instanceof Spannable) {
+ mLayout = new DynamicLayout(mText, mTransformed, mTextPaint, w,
+ alignment, mSpacingMult,
+ mSpacingAdd, mIncludePad, mEllipsize,
+ ellipsisWidth);
+ } else {
+ if (boring == UNKNOWN_BORING) {
+ boring = BoringLayout.isBoring(mTransformed, mTextPaint,
+ mBoring);
+ if (boring != null) {
+ mBoring = boring;
+ }
+ }
+
+ if (boring != null) {
+ if (boring.width <= w &&
+ (mEllipsize == null || boring.width <= ellipsisWidth)) {
+ if (mSavedLayout != null) {
+ mLayout = mSavedLayout.
+ replaceOrMake(mTransformed, mTextPaint,
+ w, alignment, mSpacingMult, mSpacingAdd,
+ boring, mIncludePad);
+ } else {
+ mLayout = BoringLayout.make(mTransformed, mTextPaint,
+ w, alignment, mSpacingMult, mSpacingAdd,
+ boring, mIncludePad);
+ }
+ // Log.e("aaa", "Boring: " + mTransformed);
+
+ mSavedLayout = (BoringLayout) mLayout;
+ } else if (mEllipsize != null && boring.width <= w) {
+ if (mSavedLayout != null) {
+ mLayout = mSavedLayout.
+ replaceOrMake(mTransformed, mTextPaint,
+ w, alignment, mSpacingMult, mSpacingAdd,
+ boring, mIncludePad, mEllipsize,
+ ellipsisWidth);
+ } else {
+ mLayout = BoringLayout.make(mTransformed, mTextPaint,
+ w, alignment, mSpacingMult, mSpacingAdd,
+ boring, mIncludePad, mEllipsize,
+ ellipsisWidth);
+ }
+ } else if (mEllipsize != null) {
+ mLayout = new StaticLayout(mTransformed,
+ 0, mTransformed.length(),
+ mTextPaint, w, alignment, mSpacingMult,
+ mSpacingAdd, mIncludePad, mEllipsize,
+ ellipsisWidth);
+ } else {
+ mLayout = new StaticLayout(mTransformed, mTextPaint,
+ w, alignment, mSpacingMult, mSpacingAdd,
+ mIncludePad);
+ // Log.e("aaa", "Boring but wide: " + mTransformed);
+ }
+ } else if (mEllipsize != null) {
+ mLayout = new StaticLayout(mTransformed,
+ 0, mTransformed.length(),
+ mTextPaint, w, alignment, mSpacingMult,
+ mSpacingAdd, mIncludePad, mEllipsize,
+ ellipsisWidth);
+ } else {
+ mLayout = new StaticLayout(mTransformed, mTextPaint,
+ w, alignment, mSpacingMult, mSpacingAdd,
+ mIncludePad);
+ }
+ }
+
+ mHintLayout = null;
+
+ if (mHint != null) {
+ if (hintBoring == UNKNOWN_BORING) {
+ hintBoring = BoringLayout.isBoring(mHint, mTextPaint,
+ mHintBoring);
+ if (hintBoring != null) {
+ mHintBoring = hintBoring;
+ }
+ }
+
+ if (hintBoring != null) {
+ if (hintBoring.width <= hintWidth) {
+ if (mSavedHintLayout != null) {
+ mHintLayout = mSavedHintLayout.
+ replaceOrMake(mHint, mTextPaint,
+ hintWidth, alignment, mSpacingMult,
+ mSpacingAdd, hintBoring, mIncludePad);
+ } else {
+ mHintLayout = BoringLayout.make(mHint, mTextPaint,
+ hintWidth, alignment, mSpacingMult,
+ mSpacingAdd, hintBoring, mIncludePad);
+ }
+
+ mSavedHintLayout = (BoringLayout) mHintLayout;
+ } else {
+ mHintLayout = new StaticLayout(mHint, mTextPaint,
+ hintWidth, alignment, mSpacingMult, mSpacingAdd,
+ mIncludePad);
+ }
+ } else {
+ mHintLayout = new StaticLayout(mHint, mTextPaint,
+ hintWidth, alignment, mSpacingMult, mSpacingAdd,
+ mIncludePad);
+ }
+ }
+
+ if (bringIntoView) {
+ registerForPreDraw();
+ }
+ }
+
+ private static int desired(Layout layout) {
+ int n = layout.getLineCount();
+ CharSequence text = layout.getText();
+ float max = 0;
+
+ // if any line was wrapped, we can't use it.
+ // but it's ok for the last line not to have a newline
+
+ for (int i = 0; i < n - 1; i++) {
+ if (text.charAt(layout.getLineEnd(i) - 1) != '\n')
+ return -1;
+ }
+
+ for (int i = 0; i < n; i++) {
+ max = Math.max(max, layout.getLineWidth(i));
+ }
+
+ return (int) FloatMath.ceil(max);
+ }
+
+ /**
+ * Set whether the TextView includes extra top and bottom padding to make
+ * room for accents that go above the normal ascent and descent.
+ * The default is true.
+ *
+ * @attr ref android.R.styleable#TextView_includeFontPadding
+ */
+ public void setIncludeFontPadding(boolean includepad) {
+ mIncludePad = includepad;
+
+ if (mLayout != null) {
+ nullLayouts();
+ requestLayout();
+ invalidate();
+ }
+ }
+
+ private static final BoringLayout.Metrics UNKNOWN_BORING =
+ new BoringLayout.Metrics();
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+ int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+
+ int width;
+ int height;
+
+ BoringLayout.Metrics boring = UNKNOWN_BORING;
+ BoringLayout.Metrics hintBoring = UNKNOWN_BORING;
+
+ int des = -1;
+ boolean fromexisting = false;
+
+ if (widthMode == MeasureSpec.EXACTLY) {
+ // Parent has told us how big to be. So be it.
+ width = widthSize;
+ } else {
+ if (mLayout != null && mEllipsize == null) {
+ des = desired(mLayout);
+ }
+
+ if (des < 0) {
+ boring = BoringLayout.isBoring(mTransformed, mTextPaint,
+ mBoring);
+ if (boring != null) {
+ mBoring = boring;
+ }
+ } else {
+ fromexisting = true;
+ }
+
+ if (boring == null || boring == UNKNOWN_BORING) {
+ if (des < 0) {
+ des = (int) FloatMath.ceil(Layout.
+ getDesiredWidth(mTransformed, mTextPaint));
+ }
+
+ width = des;
+ } else {
+ width = boring.width;
+ }
+
+ width = Math.max(width, mDrawableWidthTop);
+ width = Math.max(width, mDrawableWidthBottom);
+
+ if (mHint != null) {
+ int hintDes = -1;
+ int hintWidth;
+
+ if (mHintLayout != null) {
+ hintDes = desired(mHintLayout);
+ }
+
+ if (hintDes < 0) {
+ hintBoring = BoringLayout.isBoring(mHint, mTextPaint,
+ mHintBoring);
+ if (hintBoring != null) {
+ mHintBoring = hintBoring;
+ }
+ }
+
+ if (hintBoring == null || hintBoring == UNKNOWN_BORING) {
+ if (hintDes < 0) {
+ hintDes = (int) FloatMath.ceil(Layout.
+ getDesiredWidth(mHint, mTextPaint));
+ }
+
+ hintWidth = hintDes;
+ } else {
+ hintWidth = hintBoring.width;
+ }
+
+ if (hintWidth > width) {
+ width = hintWidth;
+ }
+ }
+
+ width += getCompoundPaddingLeft() + getCompoundPaddingRight();
+
+ if (mMaxWidthMode == EMS) {
+ width = Math.min(width, mMaxWidth * getLineHeight());
+ } else {
+ width = Math.min(width, mMaxWidth);
+ }
+
+ if (mMinWidthMode == EMS) {
+ width = Math.max(width, mMinWidth * getLineHeight());
+ } else {
+ width = Math.max(width, mMinWidth);
+ }
+
+ // Check against our minimum width
+ width = Math.max(width, getSuggestedMinimumWidth());
+
+ if (widthMode == MeasureSpec.AT_MOST) {
+ width = Math.min(widthSize, width);
+ }
+ }
+
+ int want = width - getCompoundPaddingLeft() - getCompoundPaddingRight();
+ int unpaddedWidth = want;
+ int hintWant = want;
+
+ if (mHorizontallyScrolling)
+ want = VERY_WIDE;
+
+ int hintWidth = mHintLayout == null ? hintWant : mHintLayout.getWidth();
+
+ if (mLayout == null) {
+ makeNewLayout(want, hintWant, boring, hintBoring,
+ width - getCompoundPaddingLeft() - getCompoundPaddingRight(),
+ false);
+ } else if ((mLayout.getWidth() != want) || (hintWidth != hintWant) ||
+ (mLayout.getEllipsizedWidth() !=
+ width - getCompoundPaddingLeft() - getCompoundPaddingRight())) {
+ if (mHint == null && mEllipsize == null &&
+ want > mLayout.getWidth() &&
+ (mLayout instanceof BoringLayout ||
+ (fromexisting && des >= 0 && des <= want))) {
+ mLayout.increaseWidthTo(want);
+ } else {
+ makeNewLayout(want, hintWant, boring, hintBoring,
+ width - getCompoundPaddingLeft() - getCompoundPaddingRight(),
+ false);
+ }
+ } else {
+ // Width has not changed.
+ }
+
+ if (heightMode == MeasureSpec.EXACTLY) {
+ // Parent has told us how big to be. So be it.
+ height = heightSize;
+ mDesiredHeightAtMeasure = -1;
+ } else {
+ int desired = getDesiredHeight();
+
+ height = desired;
+ mDesiredHeightAtMeasure = desired;
+
+ if (heightMode == MeasureSpec.AT_MOST) {
+ height = Math.min(desired, height);
+ }
+ }
+
+ int unpaddedHeight = height - getCompoundPaddingTop() -
+ getCompoundPaddingBottom();
+ if (mMaxMode == LINES && mLayout.getLineCount() > mMaximum) {
+ unpaddedHeight = Math.min(unpaddedHeight,
+ mLayout.getLineTop(mMaximum));
+ }
+
+ /*
+ * We didn't let makeNewLayout() register to bring the cursor into view,
+ * so do it here if there is any possibility that it is needed.
+ */
+ if (mMovement != null ||
+ mLayout.getWidth() > unpaddedWidth ||
+ mLayout.getHeight() > unpaddedHeight) {
+ registerForPreDraw();
+ } else {
+ scrollTo(0, 0);
+ }
+
+ setMeasuredDimension(width, height);
+ }
+
+ private int getDesiredHeight() {
+ return Math.max(getDesiredHeight(mLayout, true),
+ getDesiredHeight(mHintLayout, false));
+ }
+
+ private int getDesiredHeight(Layout layout, boolean cap) {
+ if (layout == null) {
+ return 0;
+ }
+
+ int linecount = layout.getLineCount();
+ int pad = getCompoundPaddingTop() + getCompoundPaddingBottom();
+ int desired = layout.getLineTop(linecount);
+
+ desired = Math.max(desired, mDrawableHeightLeft);
+ desired = Math.max(desired, mDrawableHeightRight);
+
+ desired += pad;
+
+ if (mMaxMode == LINES) {
+ /*
+ * Don't cap the hint to a certain number of lines.
+ * (Do cap it, though, if we have a maximum pixel height.)
+ */
+ if (cap) {
+ if (linecount > mMaximum) {
+ desired = layout.getLineTop(mMaximum) +
+ layout.getBottomPadding();
+
+ desired = Math.max(desired, mDrawableHeightLeft);
+ desired = Math.max(desired, mDrawableHeightRight);
+
+ desired += pad;
+ linecount = mMaximum;
+ }
+ }
+ } else {
+ desired = Math.min(desired, mMaximum);
+ }
+
+ if (mMinMode == LINES) {
+ if (linecount < mMinimum) {
+ desired += getLineHeight() * (mMinimum - linecount);
+ }
+ } else {
+ desired = Math.max(desired, mMinimum);
+ }
+
+ // Check against our minimum height
+ desired = Math.max(desired, getSuggestedMinimumHeight());
+
+ return desired;
+ }
+
+ /**
+ * Check whether a change to the existing text layout requires a
+ * new view layout.
+ */
+ private void checkForResize() {
+ boolean sizeChanged = false;
+
+ if (mLayout != null) {
+ // Check if our width changed
+ if (mLayoutParams.width == LayoutParams.WRAP_CONTENT) {
+ sizeChanged = true;
+ invalidate();
+ }
+
+ // Check if our height changed
+ if (mLayoutParams.height == LayoutParams.WRAP_CONTENT) {
+ int desiredHeight = getDesiredHeight();
+
+ if (desiredHeight != this.getHeight()) {
+ sizeChanged = true;
+ }
+ } else if (mLayoutParams.height == LayoutParams.FILL_PARENT) {
+ if (mDesiredHeightAtMeasure >= 0) {
+ int desiredHeight = getDesiredHeight();
+
+ if (desiredHeight != mDesiredHeightAtMeasure) {
+ sizeChanged = true;
+ }
+ }
+ }
+ }
+
+ if (sizeChanged) {
+ requestLayout();
+ // caller will have already invalidated
+ }
+ }
+
+ /**
+ * Check whether entirely new text requires a new view layout
+ * or merely a new text layout.
+ */
+ private void checkForRelayout() {
+ // If we have a fixed width, we can just swap in a new text layout
+ // if the text height stays the same or if the view height is fixed.
+
+ if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT ||
+ (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth)) &&
+ (mHint == null || mHintLayout != null) &&
+ (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {
+ // Static width, so try making a new text layout.
+
+ int oldht = mLayout.getHeight();
+ int want = mLayout.getWidth();
+ int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth();
+
+ /*
+ * No need to bring the text into view, since the size is not
+ * changing (unless we do the requestLayout(), in which case it
+ * will happen at measure).
+ */
+ makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,
+ mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
+
+ // In a fixed-height view, so use our new text layout.
+ if (mLayoutParams.height != LayoutParams.WRAP_CONTENT &&
+ mLayoutParams.height != LayoutParams.FILL_PARENT) {
+ invalidate();
+ return;
+ }
+
+ // Dynamic height, but height has stayed the same,
+ // so use our new text layout.
+ if (mLayout.getHeight() == oldht &&
+ (mHintLayout == null || mHintLayout.getHeight() == oldht)) {
+ invalidate();
+ return;
+ }
+
+ // We lose: the height has changed and we have a dynamic height.
+ // Request a new view layout using our new text layout.
+ requestLayout();
+ invalidate();
+ } else {
+ // Dynamic width, so we have no choice but to request a new
+ // view layout with a new text layout.
+
+ nullLayouts();
+ requestLayout();
+ invalidate();
+ }
+ }
+
+ /**
+ * Returns true if anything changed.
+ */
+ private boolean bringTextIntoView() {
+ int line = 0;
+ if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
+ line = mLayout.getLineCount() - 1;
+ }
+
+ Layout.Alignment a = mLayout.getParagraphAlignment(line);
+ int dir = mLayout.getParagraphDirection(line);
+ int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
+ int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom();
+ int ht = mLayout.getHeight();
+
+ int scrollx, scrolly;
+
+ if (a == Layout.Alignment.ALIGN_CENTER) {
+ /*
+ * Keep centered if possible, or, if it is too wide to fit,
+ * keep leading edge in view.
+ */
+
+ int left = (int) FloatMath.floor(mLayout.getLineLeft(line));
+ int right = (int) FloatMath.ceil(mLayout.getLineRight(line));
+
+ if (right - left < hspace) {
+ scrollx = (right + left) / 2 - hspace / 2;
+ } else {
+ if (dir < 0) {
+ scrollx = right - hspace;
+ } else {
+ scrollx = left;
+ }
+ }
+ } else if (a == Layout.Alignment.ALIGN_NORMAL) {
+ /*
+ * Keep leading edge in view.
+ */
+
+ if (dir < 0) {
+ int right = (int) FloatMath.ceil(mLayout.getLineRight(line));
+ scrollx = right - hspace;
+ } else {
+ scrollx = (int) FloatMath.floor(mLayout.getLineLeft(line));
+ }
+ } else /* a == Layout.Alignment.ALIGN_OPPOSITE */ {
+ /*
+ * Keep trailing edge in view.
+ */
+
+ if (dir < 0) {
+ scrollx = (int) FloatMath.floor(mLayout.getLineLeft(line));
+ } else {
+ int right = (int) FloatMath.ceil(mLayout.getLineRight(line));
+ scrollx = right - hspace;
+ }
+ }
+
+ if (ht < vspace) {
+ scrolly = 0;
+ } else {
+ if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
+ scrolly = ht - vspace;
+ } else {
+ scrolly = 0;
+ }
+ }
+
+ if (scrollx != mScrollX || scrolly != mScrollY) {
+ scrollTo(scrollx, scrolly);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Returns true if anything changed.
+ */
+ private boolean bringPointIntoView(int offset) {
+ boolean changed = false;
+
+ int line = mLayout.getLineForOffset(offset);
+
+ // FIXME: Is it okay to truncate this, or should we round?
+ final int x = (int)mLayout.getPrimaryHorizontal(offset);
+ final int top = mLayout.getLineTop(line);
+ final int bottom = mLayout.getLineTop(line+1);
+
+ int left = (int) FloatMath.floor(mLayout.getLineLeft(line));
+ int right = (int) FloatMath.ceil(mLayout.getLineRight(line));
+ int ht = mLayout.getHeight();
+
+ int grav;
+
+ switch (mLayout.getParagraphAlignment(line)) {
+ case ALIGN_NORMAL:
+ grav = 1;
+ break;
+
+ case ALIGN_OPPOSITE:
+ grav = -1;
+ break;
+
+ default:
+ grav = 0;
+ }
+
+ grav *= mLayout.getParagraphDirection(line);
+
+ int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
+ int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom();
+
+ int hslack = (bottom - top) / 2;
+ int vslack = hslack;
+
+ if (vslack > vspace / 4)
+ vslack = vspace / 4;
+ if (hslack > hspace / 4)
+ hslack = hspace / 4;
+
+ int hs = mScrollX;
+ int vs = mScrollY;
+
+ if (top - vs < vslack)
+ vs = top - vslack;
+ if (bottom - vs > vspace - vslack)
+ vs = bottom - (vspace - vslack);
+ if (ht - vs < vspace)
+ vs = ht - vspace;
+ if (0 - vs > 0)
+ vs = 0;
+
+ if (grav != 0) {
+ if (x - hs < hslack) {
+ hs = x - hslack;
+ }
+ if (x - hs > hspace - hslack) {
+ hs = x - (hspace - hslack);
+ }
+ }
+
+ if (grav < 0) {
+ if (left - hs > 0)
+ hs = left;
+ if (right - hs < hspace)
+ hs = right - hspace;
+ } else if (grav > 0) {
+ if (right - hs < hspace)
+ hs = right - hspace;
+ if (left - hs > 0)
+ hs = left;
+ } else /* grav == 0 */ {
+ if (right - left <= hspace) {
+ /*
+ * If the entire text fits, center it exactly.
+ */
+ hs = left - (hspace - (right - left)) / 2;
+ } else if (x > right - hslack) {
+ /*
+ * If we are near the right edge, keep the right edge
+ * at the edge of the view.
+ */
+ hs = right - hspace;
+ } else if (x < left + hslack) {
+ /*
+ * If we are near the left edge, keep the left edge
+ * at the edge of the view.
+ */
+ hs = left;
+ } else if (left > hs) {
+ /*
+ * Is there whitespace visible at the left? Fix it if so.
+ */
+ hs = left;
+ } else if (right < hs + hspace) {
+ /*
+ * Is there whitespace visible at the right? Fix it if so.
+ */
+ hs = right - hspace;
+ } else {
+ /*
+ * Otherwise, float as needed.
+ */
+ if (x - hs < hslack) {
+ hs = x - hslack;
+ }
+ if (x - hs > hspace - hslack) {
+ hs = x - (hspace - hslack);
+ }
+ }
+ }
+
+ if (hs != mScrollX || vs != mScrollY) {
+ if (mScroller == null) {
+ scrollTo(hs, vs);
+ } else {
+ long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
+ int dx = hs - mScrollX;
+ int dy = vs - mScrollY;
+
+ if (duration > ANIMATED_SCROLL_GAP) {
+ mScroller.startScroll(mScrollX, mScrollY, dx, dy);
+ invalidate();
+ } else {
+ if (!mScroller.isFinished()) {
+ mScroller.abortAnimation();
+ }
+
+ scrollBy(dx, dy);
+ }
+
+ mLastScroll = AnimationUtils.currentAnimationTimeMillis();
+ }
+
+ changed = true;
+ }
+
+ if (isFocused()) {
+ // This offsets because getInterestingRect() is in terms of
+ // viewport coordinates, but requestRectangleOnScreen()
+ // is in terms of content coordinates.
+
+ Rect r = new Rect();
+ getInterestingRect(r, x, top, bottom, line);
+ r.offset(mScrollX, mScrollY);
+
+ if (requestRectangleOnScreen(r)) {
+ changed = true;
+ }
+ }
+
+ return changed;
+ }
+
+ @Override
+ public void computeScroll() {
+ if (mScroller != null) {
+ if (mScroller.computeScrollOffset()) {
+ mScrollX = mScroller.getCurrX();
+ mScrollY = mScroller.getCurrY();
+ postInvalidate(); // So we draw again
+ }
+ }
+ }
+
+ private void getInterestingRect(Rect r, int h, int top, int bottom,
+ int line) {
+ top += getExtendedPaddingTop();
+ bottom += getExtendedPaddingTop();
+ h += getCompoundPaddingLeft();
+
+ if (line == 0)
+ top -= getExtendedPaddingTop();
+ if (line == mLayout.getLineCount() - 1)
+ bottom += getExtendedPaddingBottom();
+
+ r.set(h, top, h, bottom);
+ r.offset(-mScrollX, -mScrollY);
+ }
+
+ @Override
+ public void debug(int depth) {
+ super.debug(depth);
+
+ String output = debugIndent(depth);
+ output += "frame={" + mLeft + ", " + mTop + ", " + mRight
+ + ", " + mBottom + "} scroll={" + mScrollX + ", " + mScrollY
+ + "} ";
+
+ if (mText != null) {
+
+ output += "mText=\"" + mText + "\" ";
+ if (mLayout != null) {
+ output += "mLayout width=" + mLayout.getWidth()
+ + " height=" + mLayout.getHeight();
+ }
+ } else {
+ output += "mText=NULL";
+ }
+ Log.d(VIEW_LOG_TAG, output);
+ }
+
+ /**
+ * Convenience for {@link Selection#getSelectionStart}.
+ */
+ public int getSelectionStart() {
+ return Selection.getSelectionStart(getText());
+ }
+
+ /**
+ * Convenience for {@link Selection#getSelectionEnd}.
+ */
+ public int getSelectionEnd() {
+ return Selection.getSelectionEnd(getText());
+ }
+
+ /**
+ * Return true iff there is a selection inside this text view.
+ */
+ public boolean hasSelection() {
+ return getSelectionStart() != getSelectionEnd();
+ }
+
+ /**
+ * Sets the properties of this field (lines, horizontally scrolling,
+ * transformation method) to be for a single-line input.
+ *
+ * @attr ref android.R.styleable#TextView_singleLine
+ */
+ public void setSingleLine() {
+ setSingleLine(true);
+ }
+
+ /**
+ * If true, sets the properties of this field (lines, horizontally
+ * scrolling, transformation method) to be for a single-line input;
+ * if false, restores these to the default conditions.
+ * Note that calling this with false restores default conditions,
+ * not necessarily those that were in effect prior to calling
+ * it with true.
+ *
+ * @attr ref android.R.styleable#TextView_singleLine
+ */
+ public void setSingleLine(boolean singleLine) {
+ mSingleLine = singleLine;
+
+ if (singleLine) {
+ setLines(1);
+ setHorizontallyScrolling(true);
+ setTransformationMethod(SingleLineTransformationMethod.
+ getInstance());
+ } else {
+ setMaxLines(Integer.MAX_VALUE);
+ setHorizontallyScrolling(false);
+ setTransformationMethod(null);
+ }
+ }
+
+ /**
+ * Causes words in the text that are longer than the view is wide
+ * to be ellipsized instead of broken in the middle. You may also
+ * want to {@link #setSingleLine} or {@link #setHorizontallyScrolling}
+ * to constrain the text toa single line. Use <code>null</code>
+ * to turn off ellipsizing.
+ *
+ * @attr ref android.R.styleable#TextView_ellipsize
+ */
+ public void setEllipsize(TextUtils.TruncateAt where) {
+ mEllipsize = where;
+
+ if (mLayout != null) {
+ nullLayouts();
+ requestLayout();
+ invalidate();
+ }
+ }
+
+ /**
+ * Returns where, if anywhere, words that are longer than the view
+ * is wide should be ellipsized.
+ */
+ public TextUtils.TruncateAt getEllipsize() {
+ return mEllipsize;
+ }
+
+ /**
+ * Set the TextView so that when it takes focus, all the text is
+ * selected.
+ *
+ * @attr ref android.R.styleable#TextView_selectAllOnFocus
+ */
+ public void setSelectAllOnFocus(boolean selectAllOnFocus) {
+ mSelectAllOnFocus = selectAllOnFocus;
+
+ if (selectAllOnFocus && !(mText instanceof Spannable)) {
+ setText(mText, BufferType.SPANNABLE);
+ }
+ }
+
+ /**
+ * Set whether the cursor is visible. The default is true.
+ *
+ * @attr ref android.R.styleable#TextView_cursorVisible
+ */
+ public void setCursorVisible(boolean visible) {
+ mCursorVisible = visible;
+ invalidate();
+
+ if (visible) {
+ makeBlink();
+ } else if (mBlink != null) {
+ mBlink.removeCallbacks(mBlink);
+ }
+ }
+
+ /**
+ * This method is called when the text is changed, in case any
+ * subclasses would like to know.
+ *
+ * @param text The text the TextView is displaying.
+ * @param start The offset of the start of the range of the text
+ * that was modified.
+ * @param before The offset of the former end of the range of the
+ * text that was modified. If text was simply inserted,
+ * this will be the same as <code>start</code>.
+ * If text was replaced with new text or deleted, the
+ * length of the old text was <code>before-start</code>.
+ * @param after The offset of the end of the range of the text
+ * that was modified. If text was simply deleted,
+ * this will be the same as <code>start</code>.
+ * If text was replaced with new text or inserted,
+ * the length of the new text is <code>after-start</code>.
+ */
+ protected void onTextChanged(CharSequence text,
+ int start, int before, int after) {
+ }
+
+ /**
+ * Adds a TextWatcher to the list of those whose methods are called
+ * whenever this TextView's text changes.
+ */
+ public void addTextChangedListener(TextWatcher watcher) {
+ if (mListeners == null) {
+ mListeners = new ArrayList<TextWatcher>();
+ }
+
+ mListeners.add(watcher);
+ }
+
+ /**
+ * Removes the specified TextWatcher from the list of those whose
+ * methods are called
+ * whenever this TextView's text changes.
+ */
+ public void removeTextChangedListener(TextWatcher watcher) {
+ if (mListeners != null) {
+ int i = mListeners.indexOf(watcher);
+
+ if (i >= 0) {
+ mListeners.remove(i);
+ }
+ }
+ }
+
+ private void sendBeforeTextChanged(CharSequence text, int start, int before,
+ int after) {
+ if (mListeners != null) {
+ final ArrayList<TextWatcher> list = mListeners;
+ final int count = list.size();
+ for (int i = 0; i < count; i++) {
+ list.get(i).beforeTextChanged(text, start, before, after);
+ }
+ }
+ }
+
+ private void sendOnTextChanged(CharSequence text, int start, int before,
+ int after) {
+ if (mListeners != null) {
+ final ArrayList<TextWatcher> list = mListeners;
+ final int count = list.size();
+ for (int i = 0; i < count; i++) {
+ list.get(i).onTextChanged(text, start, before, after);
+ }
+ }
+ }
+
+ private void sendAfterTextChanged(Editable text) {
+ if (mListeners != null) {
+ final ArrayList<TextWatcher> list = mListeners;
+ final int count = list.size();
+ for (int i = 0; i < count; i++) {
+ list.get(i).afterTextChanged(text);
+ }
+ }
+ }
+
+ private class ChangeWatcher
+ extends Handler
+ implements TextWatcher, SpanWatcher {
+ public void beforeTextChanged(CharSequence buffer, int start,
+ int before, int after) {
+ TextView.this.sendBeforeTextChanged(buffer, start, before, after);
+ }
+
+ public void onTextChanged(CharSequence buffer, int start,
+ int before, int after) {
+ invalidate();
+
+ int curs = Selection.getSelectionStart(buffer);
+
+ if (curs >= 0 || (mGravity & Gravity.VERTICAL_GRAVITY_MASK) ==
+ Gravity.BOTTOM) {
+ registerForPreDraw();
+ }
+
+ if (curs >= 0) {
+ mHighlightPathBogus = true;
+
+ if (isFocused()) {
+ mShowCursor = SystemClock.uptimeMillis();
+ makeBlink();
+ }
+ }
+
+ checkForResize();
+
+ TextView.this.sendOnTextChanged(buffer, start, before, after);
+ TextView.this.onTextChanged(buffer, start, before, after);
+ }
+
+ public void afterTextChanged(Editable buffer) {
+ TextView.this.sendAfterTextChanged(buffer);
+ }
+
+ private void spanChange(Spanned buf, Object what, int o, int n) {
+ // XXX Make the start and end move together if this ends up
+ // spending too much time invalidating.
+
+ if (what == Selection.SELECTION_END) {
+ mHighlightPathBogus = true;
+
+ if (!isFocused()) {
+ mSelectionMoved = true;
+ }
+
+ if (o >= 0 || n >= 0) {
+ invalidateCursor(Selection.getSelectionStart(buf), o, n);
+ registerForPreDraw();
+
+ if (isFocused()) {
+ mShowCursor = SystemClock.uptimeMillis();
+ makeBlink();
+ }
+ }
+ }
+
+ if (what == Selection.SELECTION_START) {
+ mHighlightPathBogus = true;
+
+ if (!isFocused()) {
+ mSelectionMoved = true;
+ }
+
+ if (o >= 0 || n >= 0) {
+ invalidateCursor(Selection.getSelectionEnd(buf), o, n);
+ }
+ }
+
+ if (what instanceof UpdateLayout ||
+ what instanceof ParagraphStyle) {
+ invalidate();
+ mHighlightPathBogus = true;
+ checkForResize();
+ }
+
+ if (MetaKeyKeyListener.isMetaTracker(buf, what)) {
+ mHighlightPathBogus = true;
+
+ if (Selection.getSelectionStart(buf) >= 0) {
+ invalidateCursor();
+ }
+ }
+ }
+
+ public void onSpanChanged(Spannable buf,
+ Object what, int s, int e, int st, int en) {
+ spanChange(buf, what, s, st);
+ }
+
+ public void onSpanAdded(Spannable buf, Object what, int s, int e) {
+ spanChange(buf, what, -1, s);
+ }
+
+ public void onSpanRemoved(Spannable buf, Object what, int s, int e) {
+ spanChange(buf, what, s, -1);
+ }
+ }
+
+ private void makeBlink() {
+ if (!mCursorVisible) {
+ if (mBlink != null) {
+ mBlink.removeCallbacks(mBlink);
+ }
+
+ return;
+ }
+
+ if (mBlink == null)
+ mBlink = new Blink(this);
+
+ mBlink.removeCallbacks(mBlink);
+ mBlink.postAtTime(mBlink, mShowCursor + BLINK);
+ }
+
+ @Override
+ protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
+ mShowCursor = SystemClock.uptimeMillis();
+
+ if (focused) {
+ int selStart = getSelectionStart();
+ int selEnd = getSelectionEnd();
+
+ if (!mFrozenWithFocus || (selStart < 0 || selEnd < 0)) {
+ boolean selMoved = mSelectionMoved;
+
+ if (mMovement != null) {
+ mMovement.onTakeFocus(this, (Spannable) mText, direction);
+ }
+
+ if (mSelectAllOnFocus) {
+ Selection.setSelection((Spannable) mText, 0, mText.length());
+ }
+
+ if (selMoved && selStart >= 0 && selEnd >= 0) {
+ /*
+ * Someone intentionally set the selection, so let them
+ * do whatever it is that they wanted to do instead of
+ * the default on-focus behavior. We reset the selection
+ * here instead of just skipping the onTakeFocus() call
+ * because some movement methods do something other than
+ * just setting the selection in theirs and we still
+ * need to go through that path.
+ */
+
+ Selection.setSelection((Spannable) mText, selStart, selEnd);
+ }
+ }
+
+ mFrozenWithFocus = false;
+ mSelectionMoved = false;
+
+ if (mText instanceof Spannable) {
+ Spannable sp = (Spannable) mText;
+ MetaKeyKeyListener.resetMetaState(sp);
+ }
+
+ makeBlink();
+
+ if (mError != null) {
+ showError();
+ }
+ } else {
+ if (mError != null) {
+ hideError();
+ }
+ }
+
+ if (mTransformation != null) {
+ mTransformation.onFocusChanged(this, mText, focused, direction,
+ previouslyFocusedRect);
+ }
+
+ super.onFocusChanged(focused, direction, previouslyFocusedRect);
+ }
+
+ public void onWindowFocusChanged(boolean hasWindowFocus) {
+ super.onWindowFocusChanged(hasWindowFocus);
+
+ if (hasWindowFocus) {
+ if (mBlink != null) {
+ mBlink.uncancel();
+
+ if (isFocused()) {
+ mShowCursor = SystemClock.uptimeMillis();
+ makeBlink();
+ }
+ }
+ } else {
+ if (mBlink != null) {
+ mBlink.cancel();
+ }
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ final boolean superResult = super.onTouchEvent(event);
+
+ /*
+ * Don't handle the release after a long press, because it will
+ * move the selection away from whatever the menu action was
+ * trying to affect.
+ */
+ if (mEatTouchRelease && event.getAction() == MotionEvent.ACTION_UP) {
+ mEatTouchRelease = false;
+ return superResult;
+ }
+
+ if (mMovement != null && mText instanceof Spannable &&
+ mLayout != null) {
+ if (mMovement.onTouchEvent(this, (Spannable) mText, event)) {
+ return true;
+ }
+ }
+
+ return superResult;
+ }
+
+ @Override
+ public boolean onTrackballEvent(MotionEvent event) {
+ if (mMovement != null && mText instanceof Spannable &&
+ mLayout != null) {
+ if (mMovement.onTrackballEvent(this, (Spannable) mText, event)) {
+ return true;
+ }
+ }
+
+ return super.onTrackballEvent(event);
+ }
+
+ public void setScroller(Scroller s) {
+ mScroller = s;
+ }
+
+ private static class Blink extends Handler
+ implements Runnable {
+ private WeakReference<TextView> mView;
+ private boolean mCancelled;
+
+ public Blink(TextView v) {
+ mView = new WeakReference<TextView>(v);
+ }
+
+ public void run() {
+ if (mCancelled) {
+ return;
+ }
+
+ removeCallbacks(Blink.this);
+
+ TextView tv = mView.get();
+
+ if (tv != null && tv.isFocused()) {
+ int st = Selection.getSelectionStart(tv.mText);
+ int en = Selection.getSelectionEnd(tv.mText);
+
+ if (st == en && st >= 0 && en >= 0) {
+ if (tv.mLayout != null) {
+ tv.invalidateCursorPath();
+ }
+
+ postAtTime(this, SystemClock.uptimeMillis() + BLINK);
+ }
+ }
+ }
+
+ void cancel() {
+ if (!mCancelled) {
+ removeCallbacks(Blink.this);
+ mCancelled = true;
+ }
+ }
+
+ void uncancel() {
+ mCancelled = false;
+ }
+ }
+
+ @Override
+ protected int computeHorizontalScrollRange() {
+ if (mLayout != null)
+ return mLayout.getWidth();
+
+ return super.computeHorizontalScrollRange();
+ }
+
+ @Override
+ protected int computeVerticalScrollRange() {
+ if (mLayout != null)
+ return mLayout.getHeight();
+
+ return super.computeVerticalScrollRange();
+ }
+
+ public enum BufferType {
+ NORMAL, SPANNABLE, EDITABLE,
+ }
+
+ /**
+ * Returns the TextView_textColor attribute from the
+ * Resources.StyledAttributes, if set, or the TextAppearance_textColor
+ * from the TextView_textAppearance attribute, if TextView_textColor
+ * was not set directly.
+ */
+ public static ColorStateList getTextColors(Context context, TypedArray attrs) {
+ ColorStateList colors;
+ colors = attrs.getColorStateList(com.android.internal.R.styleable.
+ TextView_textColor);
+
+ if (colors == null) {
+ int ap = attrs.getResourceId(com.android.internal.R.styleable.
+ TextView_textAppearance, -1);
+ if (ap != -1) {
+ TypedArray appearance;
+ appearance = context.obtainStyledAttributes(ap,
+ com.android.internal.R.styleable.TextAppearance);
+ colors = appearance.getColorStateList(com.android.internal.R.styleable.
+ TextAppearance_textColor);
+ appearance.recycle();
+ }
+ }
+
+ return colors;
+ }
+
+ /**
+ * Returns the default color from the TextView_textColor attribute
+ * from the AttributeSet, if set, or the default color from the
+ * TextAppearance_textColor from the TextView_textAppearance attribute,
+ * if TextView_textColor was not set directly.
+ */
+ public static int getTextColor(Context context,
+ TypedArray attrs,
+ int def) {
+ ColorStateList colors = getTextColors(context, attrs);
+
+ if (colors == null) {
+ return def;
+ } else {
+ return colors.getDefaultColor();
+ }
+ }
+
+ @Override
+ public boolean onKeyShortcut(int keyCode, KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_A:
+ if (canSelectAll()) {
+ return onMenu(ID_SELECT_ALL);
+ }
+
+ break;
+
+ case KeyEvent.KEYCODE_X:
+ if (canCut()) {
+ return onMenu(ID_CUT);
+ }
+
+ break;
+
+ case KeyEvent.KEYCODE_C:
+ if (canCopy()) {
+ return onMenu(ID_COPY);
+ }
+
+ break;
+
+ case KeyEvent.KEYCODE_V:
+ if (canPaste()) {
+ return onMenu(ID_PASTE);
+ }
+
+ break;
+ }
+
+ return super.onKeyShortcut(keyCode, event);
+ }
+
+ private boolean canSelectAll() {
+ if (mText instanceof Spannable && mText.length() != 0 &&
+ mMovement != null && mMovement.canSelectArbitrarily()) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private boolean canCut() {
+ if (mText.length() > 0 && getSelectionStart() >= 0) {
+ if (mText instanceof Editable && mInput != null) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private boolean canCopy() {
+ if (mText.length() > 0 && getSelectionStart() >= 0) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private boolean canPaste() {
+ if (mText instanceof Editable && mInput != null &&
+ getSelectionStart() >= 0 && getSelectionEnd() >= 0) {
+ ClipboardManager clip = (ClipboardManager)getContext()
+ .getSystemService(Context.CLIPBOARD_SERVICE);
+ if (clip.hasText()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ protected void onCreateContextMenu(ContextMenu menu) {
+ super.onCreateContextMenu(menu);
+
+ if (!isFocused()) {
+ return;
+ }
+
+ MenuHandler handler = new MenuHandler();
+
+ if (canSelectAll()) {
+ menu.add(0, ID_SELECT_ALL, 0,
+ com.android.internal.R.string.selectAll).
+ setOnMenuItemClickListener(handler).
+ setAlphabeticShortcut('a');
+ }
+
+ boolean selection = getSelectionStart() != getSelectionEnd();
+
+ if (canCut()) {
+ int name;
+ if (selection) {
+ name = com.android.internal.R.string.cut;
+ } else {
+ name = com.android.internal.R.string.cutAll;
+ }
+
+ menu.add(0, ID_CUT, 0, name).
+ setOnMenuItemClickListener(handler).
+ setAlphabeticShortcut('x');
+ }
+
+ if (canCopy()) {
+ int name;
+ if (selection) {
+ name = com.android.internal.R.string.copy;
+ } else {
+ name = com.android.internal.R.string.copyAll;
+ }
+
+ menu.add(0, ID_COPY, 0, name).
+ setOnMenuItemClickListener(handler).
+ setAlphabeticShortcut('c');
+ }
+
+ if (canPaste()) {
+ menu.add(0, ID_PASTE, 0, com.android.internal.R.string.paste).
+ setOnMenuItemClickListener(handler).
+ setAlphabeticShortcut('v');
+ }
+
+ if (mText instanceof Spanned) {
+ int selStart = getSelectionStart();
+ int selEnd = getSelectionEnd();
+
+ int min = Math.min(selStart, selEnd);
+ int max = Math.max(selStart, selEnd);
+
+ URLSpan[] urls = ((Spanned) mText).getSpans(min, max,
+ URLSpan.class);
+ if (urls.length == 1) {
+ menu.add(0, ID_COPY_URL, 0,
+ com.android.internal.R.string.copyUrl).
+ setOnMenuItemClickListener(handler);
+ }
+ }
+ }
+
+ private static final int ID_SELECT_ALL = 101;
+ private static final int ID_CUT = 102;
+ private static final int ID_COPY = 103;
+ private static final int ID_PASTE = 104;
+ private static final int ID_COPY_URL = 105;
+
+ private class MenuHandler implements MenuItem.OnMenuItemClickListener {
+ public boolean onMenuItemClick(MenuItem item) {
+ return onMenu(item.getItemId());
+ }
+ }
+
+ private boolean onMenu(int id) {
+ int selStart = getSelectionStart();
+ int selEnd = getSelectionEnd();
+
+ int min = Math.min(selStart, selEnd);
+ int max = Math.max(selStart, selEnd);
+
+ if (min < 0) {
+ min = 0;
+ }
+ if (max < 0) {
+ max = 0;
+ }
+
+ ClipboardManager clip = (ClipboardManager)getContext()
+ .getSystemService(Context.CLIPBOARD_SERVICE);
+
+ switch (id) {
+ case ID_SELECT_ALL:
+ Selection.setSelection((Spannable) mText, 0,
+ mText.length());
+ return true;
+
+ case ID_CUT:
+ if (min == max) {
+ min = 0;
+ max = mText.length();
+ }
+
+ clip.setText(mTransformed.subSequence(min, max));
+ ((Editable) mText).delete(min, max);
+ return true;
+
+ case ID_COPY:
+ if (min == max) {
+ min = 0;
+ max = mText.length();
+ }
+
+ clip.setText(mTransformed.subSequence(min, max));
+ return true;
+
+ case ID_PASTE:
+ CharSequence paste = clip.getText();
+
+ if (paste != null) {
+ Selection.setSelection((Spannable) mText, max);
+ ((Editable) mText).replace(min, max, paste);
+ }
+
+ return true;
+
+ case ID_COPY_URL:
+ URLSpan[] urls = ((Spanned) mText).getSpans(min, max,
+ URLSpan.class);
+ if (urls.length == 1) {
+ clip.setText(urls[0].getURL());
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ public boolean performLongClick() {
+ if (super.performLongClick()) {
+ mEatTouchRelease = true;
+ return true;
+ }
+
+ return false;
+ }
+
+ private boolean mEatTouchRelease = false;
+
+ @ViewDebug.ExportedProperty
+ private CharSequence mText;
+ private CharSequence mTransformed;
+ private BufferType mBufferType = BufferType.NORMAL;
+
+ private CharSequence mHint;
+ private Layout mHintLayout;
+
+ private KeyListener mInput;
+ private MovementMethod mMovement;
+ private TransformationMethod mTransformation;
+ private ChangeWatcher mChangeWatcher;
+
+ private ArrayList<TextWatcher> mListeners = null;
+
+ // display attributes
+ private TextPaint mTextPaint;
+ private Paint mHighlightPaint;
+ private int mHighlightColor = 0xFFBBDDFF;
+ private Layout mLayout;
+
+ private long mShowCursor;
+ private Blink mBlink;
+ private boolean mCursorVisible = true;
+
+ private boolean mSelectAllOnFocus = false;
+
+ private int mGravity = Gravity.TOP | Gravity.LEFT;
+ private boolean mHorizontallyScrolling;
+
+ private int mAutoLinkMask;
+ private boolean mLinksClickable = true;
+
+ private float mSpacingMult = 1;
+ private float mSpacingAdd = 0;
+
+ private static final int LINES = 1;
+ private static final int EMS = LINES;
+ private static final int PIXELS = 2;
+
+ private int mMaximum = Integer.MAX_VALUE;
+ private int mMaxMode = LINES;
+ private int mMinimum = 0;
+ private int mMinMode = LINES;
+
+ private int mMaxWidth = Integer.MAX_VALUE;
+ private int mMaxWidthMode = PIXELS;
+ private int mMinWidth = 0;
+ private int mMinWidthMode = PIXELS;
+
+ private boolean mSingleLine;
+ private int mDesiredHeightAtMeasure = -1;
+ private boolean mIncludePad = true;
+
+ // tmp primitives, so we don't alloc them on each draw
+ private Path mHighlightPath;
+ private boolean mHighlightPathBogus = true;
+ private static RectF sTempRect = new RectF();
+
+ // XXX should be much larger
+ private static final int VERY_WIDE = 16384;
+
+ private static final int BLINK = 500;
+
+ private static final int ANIMATED_SCROLL_GAP = 250;
+ private long mLastScroll;
+ private Scroller mScroller = null;
+
+ private BoringLayout.Metrics mBoring;
+ private BoringLayout.Metrics mHintBoring;
+
+ private BoringLayout mSavedLayout, mSavedHintLayout;
+
+
+
+ private static final InputFilter[] NO_FILTERS = new InputFilter[0];
+ private InputFilter[] mFilters = NO_FILTERS;
+ private static final Spanned EMPTY_SPANNED = new SpannedString("");
+}
diff --git a/core/java/android/widget/TimePicker.java b/core/java/android/widget/TimePicker.java
new file mode 100644
index 0000000..ab4edc5
--- /dev/null
+++ b/core/java/android/widget/TimePicker.java
@@ -0,0 +1,360 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import android.annotation.Widget;
+import android.content.Context;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+
+import com.android.internal.R;
+import com.android.internal.widget.NumberPicker;
+
+import java.text.DateFormatSymbols;
+import java.util.Calendar;
+
+/**
+ * A view for selecting the time of day, in either 24 hour or AM/PM mode.
+ *
+ * The hour, each minute digit, and AM/PM (if applicable) can be conrolled by
+ * vertical spinners.
+ *
+ * The hour can be entered by keyboard input. Entering in two digit hours
+ * can be accomplished by hitting two digits within a timeout of about a
+ * second (e.g. '1' then '2' to select 12).
+ *
+ * The minutes can be entered by entering single digits.
+ *
+ * Under AM/PM mode, the user can hit 'a', 'A", 'p' or 'P' to pick.
+ *
+ * For a dialog using this view, see {@link android.app.TimePickerDialog}.
+ */
+@Widget
+public class TimePicker extends FrameLayout {
+
+ /**
+ * A no-op callback used in the constructor to avoid null checks
+ * later in the code.
+ */
+ private static final OnTimeChangedListener NO_OP_CHANGE_LISTENER = new OnTimeChangedListener() {
+ public void onTimeChanged(TimePicker view, int hourOfDay, int minute) {
+ }
+ };
+
+ // state
+ private int mCurrentHour = 0; // 0-23
+ private int mCurrentMinute = 0; // 0-59
+ private Boolean mIs24HourView = false;
+ private boolean mIsAm;
+
+ // ui components
+ private final NumberPicker mHourPicker;
+ private final NumberPicker mMinutePicker;
+ private final Button mAmPmButton;
+ private final String mAmText;
+ private final String mPmText;
+
+ // callbacks
+ private OnTimeChangedListener mOnTimeChangedListener;
+
+ /**
+ * The callback interface used to indicate the time has been adjusted.
+ */
+ public interface OnTimeChangedListener {
+
+ /**
+ * @param view The view associated with this listener.
+ * @param hourOfDay The current hour.
+ * @param minute The current minute.
+ */
+ void onTimeChanged(TimePicker view, int hourOfDay, int minute);
+ }
+
+ public TimePicker(Context context) {
+ this(context, null);
+ }
+
+ public TimePicker(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public TimePicker(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ LayoutInflater inflater =
+ (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ inflater.inflate(R.layout.time_picker,
+ this, // we are the parent
+ true);
+
+ // hour
+ mHourPicker = (NumberPicker) findViewById(R.id.hour);
+ mHourPicker.setOnChangeListener(new NumberPicker.OnChangedListener() {
+ public void onChanged(NumberPicker spinner, int oldVal, int newVal) {
+ mCurrentHour = newVal;
+ if (!mIs24HourView) {
+ // adjust from [1-12] to [0-11] internally, with the times
+ // written "12:xx" being the start of the half-day
+ if (mCurrentHour == 12) {
+ mCurrentHour = 0;
+ }
+ if (!mIsAm) {
+ // PM means 12 hours later than nominal
+ mCurrentHour += 12;
+ }
+ }
+ onTimeChanged();
+ }
+ });
+
+ // digits of minute
+ mMinutePicker = (NumberPicker) findViewById(R.id.minute);
+ mMinutePicker.setRange(0, 59);
+ mMinutePicker.setSpeed(100);
+ mMinutePicker.setFormatter(NumberPicker.TWO_DIGIT_FORMATTER);
+ mMinutePicker.setOnChangeListener(new NumberPicker.OnChangedListener() {
+ public void onChanged(NumberPicker spinner, int oldVal, int newVal) {
+ mCurrentMinute = newVal;
+ onTimeChanged();
+ }
+ });
+
+ // am/pm
+ mAmPmButton = (Button) findViewById(R.id.amPm);
+
+ // now that the hour/minute picker objects have been initialized, set
+ // the hour range properly based on the 12/24 hour display mode.
+ configurePickerRanges();
+
+ // initialize to current time
+ Calendar cal = Calendar.getInstance();
+ setOnTimeChangedListener(NO_OP_CHANGE_LISTENER);
+
+ // by default we're not in 24 hour mode
+ setCurrentHour(cal.get(Calendar.HOUR_OF_DAY));
+ setCurrentMinute(cal.get(Calendar.MINUTE));
+
+ mIsAm = (mCurrentHour < 12);
+
+ /* Get the localized am/pm strings and use them in the spinner */
+ DateFormatSymbols dfs = new DateFormatSymbols();
+ String[] dfsAmPm = dfs.getAmPmStrings();
+ mAmText = dfsAmPm[Calendar.AM];
+ mPmText = dfsAmPm[Calendar.PM];
+ mAmPmButton.setText(mIsAm ? mAmText : mPmText);
+ mAmPmButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ requestFocus();
+ if (mIsAm) {
+
+ // Currently AM switching to PM
+ if (mCurrentHour < 12) {
+ mCurrentHour += 12;
+ }
+ } else {
+
+ // Currently PM switching to AM
+ if (mCurrentHour >= 12) {
+ mCurrentHour -= 12;
+ }
+ }
+ mIsAm = !mIsAm;
+ mAmPmButton.setText(mIsAm ? mAmText : mPmText);
+ onTimeChanged();
+ }
+ });
+
+ if (!isEnabled()) {
+ setEnabled(false);
+ }
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ super.setEnabled(enabled);
+ mMinutePicker.setEnabled(enabled);
+ mHourPicker.setEnabled(enabled);
+ mAmPmButton.setEnabled(enabled);
+ }
+
+ /**
+ * Used to save / restore state of time picker
+ */
+ private static class SavedState extends BaseSavedState {
+
+ private final int mHour;
+ private final int mMinute;
+
+ private SavedState(Parcelable superState, int hour, int minute) {
+ super(superState);
+ mHour = hour;
+ mMinute = minute;
+ }
+
+ private SavedState(Parcel in) {
+ super(in);
+ mHour = in.readInt();
+ mMinute = in.readInt();
+ }
+
+ public int getHour() {
+ return mHour;
+ }
+
+ public int getMinute() {
+ return mMinute;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeInt(mHour);
+ dest.writeInt(mMinute);
+ }
+
+ public static final Parcelable.Creator<SavedState> CREATOR
+ = new Creator<SavedState>() {
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ Parcelable superState = super.onSaveInstanceState();
+ return new SavedState(superState, mCurrentHour, mCurrentMinute);
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ SavedState ss = (SavedState) state;
+ super.onRestoreInstanceState(ss.getSuperState());
+ setCurrentHour(ss.getHour());
+ setCurrentMinute(ss.getMinute());
+ }
+
+ /**
+ * Set the callback that indicates the time has been adjusted by the user.
+ * @param onTimeChangedListener the callback, should not be null.
+ */
+ public void setOnTimeChangedListener(OnTimeChangedListener onTimeChangedListener) {
+ mOnTimeChangedListener = onTimeChangedListener;
+ }
+
+ /**
+ * @return The current hour (0-23).
+ */
+ public Integer getCurrentHour() {
+ return mCurrentHour;
+ }
+
+ /**
+ * Set the current hour.
+ */
+ public void setCurrentHour(Integer currentHour) {
+ this.mCurrentHour = currentHour;
+ updateHourDisplay();
+ }
+
+ /**
+ * Set whether in 24 hour or AM/PM mode.
+ * @param is24HourView True = 24 hour mode. False = AM/PM.
+ */
+ public void setIs24HourView(Boolean is24HourView) {
+ if (mIs24HourView != is24HourView) {
+ mIs24HourView = is24HourView;
+ configurePickerRanges();
+ updateHourDisplay();
+ }
+ }
+
+ /**
+ * @return true if this is in 24 hour view else false.
+ */
+ public boolean is24HourView() {
+ return mIs24HourView;
+ }
+
+ /**
+ * @return The current minute.
+ */
+ public Integer getCurrentMinute() {
+ return mCurrentMinute;
+ }
+
+ /**
+ * Set the current minute (0-59).
+ */
+ public void setCurrentMinute(Integer currentMinute) {
+ this.mCurrentMinute = currentMinute;
+ updateMinuteDisplay();
+ }
+
+ @Override
+ public int getBaseline() {
+ return mHourPicker.getBaseline();
+ }
+
+ /**
+ * Set the state of the spinners appropriate to the current hour.
+ */
+ private void updateHourDisplay() {
+ int currentHour = mCurrentHour;
+ if (!mIs24HourView) {
+ // convert [0,23] ordinal to wall clock display
+ if (currentHour > 12) currentHour -= 12;
+ else if (currentHour == 0) currentHour = 12;
+ }
+ mHourPicker.setCurrent(currentHour);
+ mIsAm = mCurrentHour < 12;
+ mAmPmButton.setText(mIsAm ? mAmText : mPmText);
+ onTimeChanged();
+ }
+
+ private void configurePickerRanges() {
+ if (mIs24HourView) {
+ mHourPicker.setRange(0, 23);
+ mHourPicker.setFormatter(NumberPicker.TWO_DIGIT_FORMATTER);
+ mAmPmButton.setVisibility(View.GONE);
+ } else {
+ mHourPicker.setRange(1, 12);
+ mHourPicker.setFormatter(null);
+ mAmPmButton.setVisibility(View.VISIBLE);
+ }
+ }
+
+ private void onTimeChanged() {
+ mOnTimeChangedListener.onTimeChanged(this, getCurrentHour(), getCurrentMinute());
+ }
+
+ /**
+ * Set the state of the spinners appropriate to the current minute.
+ */
+ private void updateMinuteDisplay() {
+ mMinutePicker.setCurrent(mCurrentMinute);
+ mOnTimeChangedListener.onTimeChanged(this, getCurrentHour(), getCurrentMinute());
+ }
+}
+
diff --git a/core/java/android/widget/Toast.java b/core/java/android/widget/Toast.java
new file mode 100644
index 0000000..ff74787
--- /dev/null
+++ b/core/java/android/widget/Toast.java
@@ -0,0 +1,399 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import android.app.INotificationManager;
+import android.app.ITransientNotification;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.PixelFormat;
+import android.os.RemoteException;
+import android.os.Handler;
+import android.os.ServiceManager;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.WindowManagerImpl;
+
+/**
+ * A toast is a view containing a quick little message for the user. The toast class
+ * helps you create and show those.
+ * {@more}
+ *
+ * <p>
+ * When the view is shown to the user, appears as a floating view over the
+ * application. It will never receive focus. The user will probably be in the
+ * middle of typing something else. The idea is to be as unobtrusive as
+ * possible, while still showing the user the information you want them to see.
+ * Two examples are the volume control, and the brief message saying that your
+ * settings have been saved.
+ * <p>
+ * The easiest way to use this class is to call one of the static methods that constructs
+ * everything you need and returns a new Toast object.
+ */
+public class Toast {
+ static final String TAG = "Toast";
+ static final boolean localLOGV = false;
+
+ /**
+ * Show the view or text notification for a short period of time. This time
+ * could be user-definable. This is the default.
+ * @see #setDuration
+ */
+ public static final int LENGTH_SHORT = 0;
+
+ /**
+ * Show the view or text notification for a long period of time. This time
+ * could be user-definable.
+ * @see #setDuration
+ */
+ public static final int LENGTH_LONG = 1;
+
+ final Context mContext;
+ final TN mTN;
+ int mDuration;
+ int mGravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM;
+ int mX, mY;
+ float mHorizontalMargin;
+ float mVerticalMargin;
+ View mView;
+ View mNextView;
+
+ /**
+ * Construct an empty Toast object. You must call {@link #setView} before you
+ * can call {@link #show}.
+ *
+ * @param context The context to use. Usually your {@link android.app.Application}
+ * or {@link android.app.Activity} object.
+ */
+ public Toast(Context context) {
+ mContext = context;
+ mTN = new TN(context);
+ mY = context.getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.toast_y_offset);
+ }
+
+ /**
+ * Show the view for the specified duration.
+ */
+ public void show() {
+ if (mNextView == null) {
+ throw new RuntimeException("setView must have been called");
+ }
+
+ INotificationManager service = getService();
+
+ String pkg = mContext.getPackageName();
+
+ TN tn = mTN;
+
+ try {
+ service.enqueueToast(pkg, tn, mDuration);
+ } catch (RemoteException e) {
+ // Empty
+ }
+ }
+
+ /**
+ * Close the view if it's showing, or don't show it if it isn't showing yet.
+ * You do not normally have to call this. Normally view will disappear on its own
+ * after the appropriate duration.
+ */
+ public void cancel() {
+ mTN.hide();
+ // TODO this still needs to cancel the inflight notification if any
+ }
+
+ /**
+ * Set the view to show.
+ * @see #getView
+ */
+ public void setView(View view) {
+ mNextView = view;
+ }
+
+ /**
+ * Return the view.
+ * @see #setView
+ */
+ public View getView() {
+ return mNextView;
+ }
+
+ /**
+ * Set how long to show the view for.
+ * @see #LENGTH_SHORT
+ * @see #LENGTH_LONG
+ */
+ public void setDuration(int duration) {
+ mDuration = duration;
+ }
+
+ /**
+ * Return the duration.
+ * @see #setDuration
+ */
+ public int getDuration() {
+ return mDuration;
+ }
+
+ /**
+ * Set the margins of the view.
+ *
+ * @param horizontalMargin The horizontal margin, in percentage of the
+ * container width, between the container's edges and the
+ * notification
+ * @param verticalMargin The vertical margin, in percentage of the
+ * container height, between the container's edges and the
+ * notification
+ */
+ public void setMargin(float horizontalMargin, float verticalMargin) {
+ mHorizontalMargin = horizontalMargin;
+ mVerticalMargin = verticalMargin;
+ }
+
+ /**
+ * Return the horizontal margin.
+ */
+ public float getHorizontalMargin() {
+ return mHorizontalMargin;
+ }
+
+ /**
+ * Return the vertical margin.
+ */
+ public float getVerticalMargin() {
+ return mVerticalMargin;
+ }
+
+ /**
+ * Set the location at which the notification should appear on the screen.
+ * @see android.view.Gravity
+ * @see #getGravity
+ */
+ public void setGravity(int gravity, int xOffset, int yOffset) {
+ mGravity = gravity;
+ mX = xOffset;
+ mY = yOffset;
+ }
+
+ /**
+ * Get the location at which the notification should appear on the screen.
+ * @see android.view.Gravity
+ * @see #getGravity
+ */
+ public int getGravity() {
+ return mGravity;
+ }
+
+ /**
+ * Return the X offset in pixels to apply to the gravity's location.
+ */
+ public int getXOffset() {
+ return mX;
+ }
+
+ /**
+ * Return the Y offset in pixels to apply to the gravity's location.
+ */
+ public int getYOffset() {
+ return mY;
+ }
+
+ /**
+ * Make a standard toast that just contains a text view.
+ *
+ * @param context The context to use. Usually your {@link android.app.Application}
+ * or {@link android.app.Activity} object.
+ * @param text The text to show. Can be formatted text.
+ * @param duration How long to display the message. Either {@link #LENGTH_SHORT} or
+ * {@link #LENGTH_LONG}
+ *
+ */
+ public static Toast makeText(Context context, CharSequence text, int duration) {
+ Toast result = new Toast(context);
+
+ LayoutInflater inflate = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
+ TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
+ tv.setText(text);
+
+ result.mNextView = v;
+ result.mDuration = duration;
+
+ return result;
+ }
+
+ /**
+ * Make a standard toast that just contains a text view with the text from a resource.
+ *
+ * @param context The context to use. Usually your {@link android.app.Application}
+ * or {@link android.app.Activity} object.
+ * @param resId The resource id of the string resource to use. Can be formatted text.
+ * @param duration How long to display the message. Either {@link #LENGTH_SHORT} or
+ * {@link #LENGTH_LONG}
+ *
+ * @throws Resources.NotFoundException if the resource can't be found.
+ */
+ public static Toast makeText(Context context, int resId, int duration)
+ throws Resources.NotFoundException {
+ return makeText(context, context.getResources().getText(resId), duration);
+ }
+
+ /**
+ * Update the text in a Toast that was previously created using one of the makeText() methods.
+ * @param resId The new text for the Toast.
+ */
+ public void setText(int resId) {
+ setText(mContext.getText(resId));
+ }
+
+ /**
+ * Update the text in a Toast that was previously created using one of the makeText() methods.
+ * @param s The new text for the Toast.
+ */
+ public void setText(CharSequence s) {
+ if (mNextView == null) {
+ throw new RuntimeException("This Toast was not created with Toast.makeText()");
+ }
+ TextView tv = (TextView) mNextView.findViewById(com.android.internal.R.id.message);
+ if (tv == null) {
+ throw new RuntimeException("This Toast was not created with Toast.makeText()");
+ }
+ tv.setText(s);
+ }
+
+ // =======================================================================================
+ // All the gunk below is the interaction with the Notification Service, which handles
+ // the proper ordering of these system-wide.
+ // =======================================================================================
+
+ private static INotificationManager sService;
+
+ static private INotificationManager getService()
+ {
+ if (sService != null) {
+ return sService;
+ }
+ sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
+ return sService;
+ }
+
+ private class TN extends ITransientNotification.Stub
+ {
+ TN(Context context)
+ {
+ // XXX This should be changed to use a Dialog, with a Theme.Toast
+ // defined that sets up the layout params appropriately.
+ mParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
+ mParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
+ mParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+ | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
+ | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
+ mParams.format = PixelFormat.TRANSLUCENT;
+ mParams.windowAnimations = com.android.internal.R.style.Animation_Toast;
+ mParams.type = WindowManager.LayoutParams.TYPE_TOAST;
+ mParams.setTitle("Toast");
+ }
+
+ /**
+ * schedule handleShow into the right thread
+ */
+ public void show()
+ {
+ if (localLOGV) Log.v(TAG, "SHOW: " + this);
+ mHandler.post(mShow);
+ }
+
+ /**
+ * schedule handleHide into the right thread
+ */
+ public void hide()
+ {
+ if (localLOGV) Log.v(TAG, "HIDE: " + this);
+ mHandler.post(mHide);
+ }
+
+ public void handleShow()
+ {
+ if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
+ + " mNextView=" + mNextView);
+ if (mView != mNextView) {
+ // remove the old view if necessary
+ handleHide();
+ mView = mNextView;
+ mWM = WindowManagerImpl.getDefault();
+ final int gravity = mGravity;
+ mParams.gravity = gravity;
+ if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
+ mParams.horizontalWeight = 1.0f;
+ }
+ if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
+ mParams.verticalWeight = 1.0f;
+ }
+ mParams.x = mX;
+ mParams.y = mY;
+ mParams.verticalMargin = mVerticalMargin;
+ mParams.horizontalMargin = mHorizontalMargin;
+ if (mView.getParent() != null) {
+ if (localLOGV) Log.v(
+ TAG, "REMOVE! " + mView + " in " + this);
+ mWM.removeView(mView);
+ }
+ if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this);
+ mWM.addView(mView, mParams);
+ }
+ }
+
+ public void handleHide()
+ {
+ if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
+ if (mView != null) {
+ // note: checking parent() just to make sure the view has
+ // been added... i have seen cases where we get here when
+ // the view isn't yet added, so let's try not to crash.
+ if (mView.getParent() != null) {
+ if (localLOGV) Log.v(
+ TAG, "REMOVE! " + mView + " in " + this);
+ mWM.removeView(mView);
+ }
+ mView = null;
+ }
+ }
+
+ Runnable mShow = new Runnable() {
+ public void run() {
+ handleShow();
+ }
+ };
+
+ Runnable mHide = new Runnable() {
+ public void run() {
+ handleHide();
+ }
+ };
+
+ private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();
+
+ WindowManagerImpl mWM;
+ }
+
+ final Handler mHandler = new Handler();
+}
+
diff --git a/core/java/android/widget/ToggleButton.java b/core/java/android/widget/ToggleButton.java
new file mode 100644
index 0000000..dc791e3
--- /dev/null
+++ b/core/java/android/widget/ToggleButton.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
+import android.util.AttributeSet;
+
+/**
+ * Displays checked/unchecked states as a button
+ * with a "light" indicator and by default accompanied with the text "ON" or "OFF".
+ *
+ * @attr ref android.R.styleable#ToggleButton_textOn
+ * @attr ref android.R.styleable#ToggleButton_textOff
+ * @attr ref android.R.styleable#ToggleButton_disabledAlpha
+ */
+public class ToggleButton extends CompoundButton {
+ private CharSequence mTextOn;
+ private CharSequence mTextOff;
+
+ private Drawable mIndicatorDrawable;
+
+ private static final int NO_ALPHA = 0xFF;
+ private float mDisabledAlpha;
+
+ public ToggleButton(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ TypedArray a =
+ context.obtainStyledAttributes(
+ attrs, com.android.internal.R.styleable.ToggleButton, defStyle, 0);
+ mTextOn = a.getText(com.android.internal.R.styleable.ToggleButton_textOn);
+ mTextOff = a.getText(com.android.internal.R.styleable.ToggleButton_textOff);
+ mDisabledAlpha = a.getFloat(com.android.internal.R.styleable.ToggleButton_disabledAlpha, 0.5f);
+ syncTextState();
+ a.recycle();
+ }
+
+ public ToggleButton(Context context, AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.buttonStyleToggle);
+ }
+
+ public ToggleButton(Context context) {
+ this(context, null);
+ }
+
+ @Override
+ public void setChecked(boolean checked) {
+ super.setChecked(checked);
+
+ syncTextState();
+ }
+
+ private void syncTextState() {
+ boolean checked = isChecked();
+ if (checked && mTextOn != null) {
+ setText(mTextOn);
+ } else if (!checked && mTextOff != null) {
+ setText(mTextOff);
+ }
+ }
+
+ /**
+ * Returns the text for when the button is in the checked state.
+ *
+ * @return The text.
+ */
+ public CharSequence getTextOn() {
+ return mTextOn;
+ }
+
+ /**
+ * Sets the text for when the button is in the checked state.
+ *
+ * @param textOn The text.
+ */
+ public void setTextOn(CharSequence textOn) {
+ mTextOn = textOn;
+ }
+
+ /**
+ * Returns the text for when the button is not in the checked state.
+ *
+ * @return The text.
+ */
+ public CharSequence getTextOff() {
+ return mTextOff;
+ }
+
+ /**
+ * Sets the text for when the button is not in the checked state.
+ *
+ * @param textOff The text.
+ */
+ public void setTextOff(CharSequence textOff) {
+ mTextOff = textOff;
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ updateReferenceToIndicatorDrawable(getBackground());
+ }
+
+ @Override
+ public void setBackgroundDrawable(Drawable d) {
+ super.setBackgroundDrawable(d);
+
+ updateReferenceToIndicatorDrawable(d);
+ }
+
+ private void updateReferenceToIndicatorDrawable(Drawable backgroundDrawable) {
+ if (backgroundDrawable instanceof LayerDrawable) {
+ LayerDrawable layerDrawable = (LayerDrawable) backgroundDrawable;
+ mIndicatorDrawable =
+ layerDrawable.findDrawableByLayerId(com.android.internal.R.id.toggle);
+ }
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+
+ if (mIndicatorDrawable != null) {
+ mIndicatorDrawable.setAlpha(isEnabled() ? NO_ALPHA : (int) (NO_ALPHA * mDisabledAlpha));
+ }
+ }
+
+}
diff --git a/core/java/android/widget/TwoLineListItem.java b/core/java/android/widget/TwoLineListItem.java
new file mode 100644
index 0000000..77ea645
--- /dev/null
+++ b/core/java/android/widget/TwoLineListItem.java
@@ -0,0 +1,90 @@
+/*
+ * 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.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.widget.RelativeLayout;
+
+/**
+ * <p>A view group with two children, intended for use in ListViews. This item has two
+ * {@link android.widget.TextView TextViews} elements (or subclasses) with the ID values
+ * {@link android.R.id#text1 text1}
+ * and {@link android.R.id#text2 text2}. There is an optional third View element with the
+ * ID {@link android.R.id#selectedIcon selectedIcon}, which can be any View subclass
+ * (though it is typically a graphic View, such as {@link android.widget.ImageView ImageView})
+ * that can be displayed when a TwoLineListItem has focus. Android supplies a
+ * {@link android.R.layout#two_line_list_item standard layout resource for TwoLineListView}
+ * (which does not include a selected item icon), but you can design your own custom XML
+ * layout for this object as shown here:</p>
+ * {@sample packages/apps/Phone/res/layout/dialer_list_item.xml}
+ *
+ * @attr ref android.R.styleable#TwoLineListItem_mode
+ */
+@Widget
+public class TwoLineListItem extends RelativeLayout {
+
+ private TextView mText1;
+ private TextView mText2;
+
+ public TwoLineListItem(Context context) {
+ this(context, null, 0);
+ }
+
+ public TwoLineListItem(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public TwoLineListItem(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ TypedArray a = context.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.TwoLineListItem, defStyle, 0);
+
+ a.recycle();
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ mText1 = (TextView) findViewById(com.android.internal.R.id.text1);
+ mText2 = (TextView) findViewById(com.android.internal.R.id.text2);
+ }
+
+ /**
+ * Returns a handle to the item with ID text1.
+ * @return A handle to the item with ID text1.
+ */
+ public TextView getText1() {
+ return mText1;
+ }
+
+ /**
+ * Returns a handle to the item with ID text2.
+ * @return A handle to the item with ID text2.
+ */
+ public TextView getText2() {
+ return mText2;
+ }
+}
diff --git a/core/java/android/widget/VideoView.java b/core/java/android/widget/VideoView.java
new file mode 100644
index 0000000..da3c2aa
--- /dev/null
+++ b/core/java/android/widget/VideoView.java
@@ -0,0 +1,509 @@
+/*
+ * 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.widget;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.media.AudioManager;
+import android.media.MediaPlayer;
+import android.media.MediaPlayer.OnCompletionListener;
+import android.media.MediaPlayer.OnErrorListener;
+import android.net.Uri;
+import android.os.PowerManager;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.View;
+import android.widget.MediaController.MediaPlayerControl;
+
+import java.io.IOException;
+
+/**
+ * Displays a video file. The VideoView class
+ * can load images from various sources (such as resources or content
+ * providers), takes care of computing its measurement from the video so that
+ * it can be used in any layout manager, and provides various display options
+ * such as scaling and tinting.
+ */
+public class VideoView extends SurfaceView implements MediaPlayerControl {
+ // settable by the client
+ private Uri mUri;
+
+ // All the stuff we need for playing and showing a video
+ private SurfaceHolder mSurfaceHolder = null;
+ private MediaPlayer mMediaPlayer = null;
+ private boolean mIsPrepared;
+ private int mVideoWidth;
+ private int mVideoHeight;
+ private int mSurfaceWidth;
+ private int mSurfaceHeight;
+ private MediaController mMediaController;
+ private OnCompletionListener mOnCompletionListener;
+ private MediaPlayer.OnPreparedListener mOnPreparedListener;
+ private int mCurrentBufferPercentage;
+ private OnErrorListener mOnErrorListener;
+ private boolean mStartWhenPrepared;
+ private int mSeekWhenPrepared;
+
+ public VideoView(Context context) {
+ super(context);
+ initVideoView();
+ }
+
+ public VideoView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ initVideoView();
+ }
+
+ public VideoView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ initVideoView();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ //Log.i("@@@@", "onMeasure");
+ int width = getDefaultSize(mVideoWidth, widthMeasureSpec);
+ int height = getDefaultSize(mVideoHeight, heightMeasureSpec);
+ if (mVideoWidth > 0 && mVideoHeight > 0) {
+ if ( mVideoWidth * height > width * mVideoHeight ) {
+ //Log.i("@@@", "image too tall, correcting");
+ height = width * mVideoHeight / mVideoWidth;
+ } else if ( mVideoWidth * height < width * mVideoHeight ) {
+ //Log.i("@@@", "image too wide, correcting");
+ width = height * mVideoWidth / mVideoHeight;
+ } else {
+ //Log.i("@@@", "aspect ratio is correct: " +
+ //width+"/"+height+"="+
+ //mVideoWidth+"/"+mVideoHeight);
+ }
+ }
+ //Log.i("@@@@@@@@@@", "setting size: " + width + 'x' + height);
+ setMeasuredDimension(width, height);
+ }
+
+ public int resolveAdjustedSize(int desiredSize, int measureSpec) {
+ int result = desiredSize;
+ int specMode = MeasureSpec.getMode(measureSpec);
+ int specSize = MeasureSpec.getSize(measureSpec);
+
+ switch (specMode) {
+ case MeasureSpec.UNSPECIFIED:
+ /* Parent says we can be as big as we want. Just don't be larger
+ * than max size imposed on ourselves.
+ */
+ result = desiredSize;
+ break;
+
+ case MeasureSpec.AT_MOST:
+ /* Parent says we can be as big as we want, up to specSize.
+ * Don't be larger than specSize, and don't be larger than
+ * the max size imposed on ourselves.
+ */
+ result = Math.min(desiredSize, specSize);
+ break;
+
+ case MeasureSpec.EXACTLY:
+ // No choice. Do what we are told.
+ result = specSize;
+ break;
+ }
+ return result;
+}
+
+ private void initVideoView() {
+ mVideoWidth = 0;
+ mVideoHeight = 0;
+ getHolder().addCallback(mSHCallback);
+ getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
+ setFocusable(true);
+ setFocusableInTouchMode(true);
+ requestFocus();
+ }
+
+ public void setVideoPath(String path) {
+ setVideoURI(Uri.parse(path));
+ }
+
+ public void setVideoURI(Uri uri) {
+ mUri = uri;
+ mStartWhenPrepared = false;
+ mSeekWhenPrepared = 0;
+ openVideo();
+ requestLayout();
+ invalidate();
+ }
+
+ public void stopPlayback() {
+ if (mMediaPlayer != null) {
+ mMediaPlayer.stop();
+ mMediaPlayer.release();
+ mMediaPlayer = null;
+ }
+ }
+
+ private void openVideo() {
+ if (mUri == null || mSurfaceHolder == null) {
+ // not ready for playback just yet, will try again later
+ return;
+ }
+ // Tell the music playback service to pause
+ // TODO: these constants need to be published somewhere in the framework.
+ Intent i = new Intent("com.android.music.musicservicecommand");
+ i.putExtra("command", "pause");
+ mContext.sendBroadcast(i);
+
+ if (mMediaPlayer != null) {
+ mMediaPlayer.reset();
+ mMediaPlayer.release();
+ mMediaPlayer = null;
+ }
+ try {
+ mMediaPlayer = new MediaPlayer();
+ mMediaPlayer.setOnPreparedListener(mPreparedListener);
+ mIsPrepared = false;
+ mMediaPlayer.setOnCompletionListener(mCompletionListener);
+ mMediaPlayer.setOnErrorListener(mErrorListener);
+ mMediaPlayer.setOnBufferingUpdateListener(mBufferingUpdateListener);
+ mCurrentBufferPercentage = 0;
+ mMediaPlayer.setDataSource(mContext, mUri);
+ mMediaPlayer.setDisplay(mSurfaceHolder);
+ mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
+ mMediaPlayer.setScreenOnWhilePlaying(true);
+ mMediaPlayer.prepareAsync();
+ attachMediaController();
+ } catch (IOException ex) {
+ Log.w("VideoView", "Unable to open content: " + mUri, ex);
+ return;
+ } catch (IllegalArgumentException ex) {
+ Log.w("VideoView", "Unable to open content: " + mUri, ex);
+ return;
+ }
+ }
+
+ public void setMediaController(MediaController controller) {
+ if (mMediaController != null) {
+ mMediaController.hide();
+ }
+ mMediaController = controller;
+ attachMediaController();
+ }
+
+ private void attachMediaController() {
+ if (mMediaPlayer != null && mMediaController != null) {
+ mMediaController.setMediaPlayer(this);
+ View anchorView = this.getParent() instanceof View ?
+ (View)this.getParent() : this;
+ mMediaController.setAnchorView(anchorView);
+ mMediaController.setEnabled(mIsPrepared);
+ }
+ }
+
+ MediaPlayer.OnPreparedListener mPreparedListener = new MediaPlayer.OnPreparedListener() {
+ public void onPrepared(MediaPlayer mp) {
+ // briefly show the mediacontroller
+ mIsPrepared = true;
+ if (mOnPreparedListener != null) {
+ mOnPreparedListener.onPrepared(mMediaPlayer);
+ }
+ if (mMediaController != null) {
+ mMediaController.setEnabled(true);
+ }
+ mVideoWidth = mp.getVideoWidth();
+ mVideoHeight = mp.getVideoHeight();
+ if (mVideoWidth != 0 && mVideoHeight != 0) {
+ //Log.i("@@@@", "video size: " + mVideoWidth +"/"+ mVideoHeight);
+ getHolder().setFixedSize(mVideoWidth, mVideoHeight);
+ if (mSurfaceWidth == mVideoWidth && mSurfaceHeight == mVideoHeight) {
+ // We didn't actually change the size (it was already at the size
+ // we need), so we won't get a "surface changed" callback, so
+ // start the video here instead of in the callback.
+ if (mSeekWhenPrepared != 0) {
+ mMediaPlayer.seekTo(mSeekWhenPrepared);
+ }
+ if (mStartWhenPrepared) {
+ mMediaPlayer.start();
+ if (mMediaController != null) {
+ mMediaController.show();
+ }
+ } else if (!isPlaying() && (mSeekWhenPrepared != 0 || getCurrentPosition() > 0)) {
+ if (mMediaController != null) {
+ mMediaController.show(0); // show the media controls when we're paused into a video and make 'em stick.
+ }
+ }
+ }
+ } else {
+ Log.d("VideoView", "Couldn't get video size after prepare(): " +
+ mVideoWidth + "/" + mVideoHeight);
+ // The file was probably truncated or corrupt. Start anyway, so
+ // that we play whatever short snippet is there and then get
+ // the "playback completed" event.
+ if (mStartWhenPrepared) {
+ mMediaPlayer.start();
+ }
+ }
+ }
+ };
+
+ private MediaPlayer.OnCompletionListener mCompletionListener =
+ new MediaPlayer.OnCompletionListener() {
+ public void onCompletion(MediaPlayer mp) {
+ if (mMediaController != null) {
+ mMediaController.hide();
+ }
+ if (mOnCompletionListener != null) {
+ mOnCompletionListener.onCompletion(mMediaPlayer);
+ }
+ }
+ };
+
+ private MediaPlayer.OnErrorListener mErrorListener =
+ new MediaPlayer.OnErrorListener() {
+ public boolean onError(MediaPlayer mp, int a, int b) {
+ Log.d("VideoView", "Error: " + a + "," + b);
+ if (mMediaController != null) {
+ mMediaController.hide();
+ }
+
+ /* If an error handler has been supplied, use it and finish. */
+ if (mOnErrorListener != null) {
+ if (mOnErrorListener.onError(mMediaPlayer, a, b)) {
+ return true;
+ }
+ }
+
+ /* Otherwise, pop up an error dialog so the user knows that
+ * something bad has happened. Only try and pop up the dialog
+ * if we're attached to a window. When we're going away and no
+ * longer have a window, don't bother showing the user an error.
+ */
+ if (getWindowToken() != null) {
+ Resources r = mContext.getResources();
+ new AlertDialog.Builder(mContext)
+ .setTitle(com.android.internal.R.string.VideoView_error_title)
+ .setMessage(com.android.internal.R.string.VideoView_error_text_unknown)
+ .setPositiveButton(com.android.internal.R.string.VideoView_error_button,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ /* If we get here, there is no onError listener, so
+ * at least inform them that the video is over.
+ */
+ if (mOnCompletionListener != null) {
+ mOnCompletionListener.onCompletion(mMediaPlayer);
+ }
+ }
+ })
+ .setCancelable(false)
+ .show();
+ }
+ return true;
+ }
+ };
+
+ private MediaPlayer.OnBufferingUpdateListener mBufferingUpdateListener =
+ new MediaPlayer.OnBufferingUpdateListener() {
+ public void onBufferingUpdate(MediaPlayer mp, int percent) {
+ mCurrentBufferPercentage = percent;
+ }
+ };
+
+ /**
+ * Register a callback to be invoked when the media file
+ * is loaded and ready to go.
+ *
+ * @param l The callback that will be run
+ */
+ public void setOnPreparedListener(MediaPlayer.OnPreparedListener l)
+ {
+ mOnPreparedListener = l;
+ }
+
+ /**
+ * Register a callback to be invoked when the end of a media file
+ * has been reached during playback.
+ *
+ * @param l The callback that will be run
+ */
+ public void setOnCompletionListener(OnCompletionListener l)
+ {
+ mOnCompletionListener = l;
+ }
+
+ /**
+ * Register a callback to be invoked when an error occurs
+ * during playback or setup. If no listener is specified,
+ * or if the listener returned false, VideoView will inform
+ * the user of any errors.
+ *
+ * @param l The callback that will be run
+ */
+ public void setOnErrorListener(OnErrorListener l)
+ {
+ mOnErrorListener = l;
+ }
+
+ SurfaceHolder.Callback mSHCallback = new SurfaceHolder.Callback()
+ {
+ public void surfaceChanged(SurfaceHolder holder, int format,
+ int w, int h)
+ {
+ mSurfaceWidth = w;
+ mSurfaceHeight = h;
+ if (mIsPrepared && mVideoWidth == w && mVideoHeight == h) {
+ if (mSeekWhenPrepared != 0) {
+ mMediaPlayer.seekTo(mSeekWhenPrepared);
+ }
+ mMediaPlayer.start();
+ if (mMediaController != null) {
+ mMediaController.show();
+ }
+ }
+ }
+
+ public void surfaceCreated(SurfaceHolder holder)
+ {
+ mSurfaceHolder = holder;
+ openVideo();
+ }
+
+ public void surfaceDestroyed(SurfaceHolder holder)
+ {
+ // after we return from this we can't use the surface any more
+ mSurfaceHolder = null;
+ if (mMediaController != null) mMediaController.hide();
+ if (mMediaPlayer != null) {
+ mMediaPlayer.reset();
+ mMediaPlayer.release();
+ mMediaPlayer = null;
+ }
+ }
+ };
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ if (mIsPrepared && mMediaPlayer != null && mMediaController != null) {
+ toggleMediaControlsVisiblity();
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onTrackballEvent(MotionEvent ev) {
+ if (mIsPrepared && mMediaPlayer != null && mMediaController != null) {
+ toggleMediaControlsVisiblity();
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event)
+ {
+ if (mIsPrepared &&
+ keyCode != KeyEvent.KEYCODE_BACK &&
+ keyCode != KeyEvent.KEYCODE_VOLUME_UP &&
+ keyCode != KeyEvent.KEYCODE_VOLUME_DOWN &&
+ keyCode != KeyEvent.KEYCODE_MENU &&
+ keyCode != KeyEvent.KEYCODE_CALL &&
+ keyCode != KeyEvent.KEYCODE_ENDCALL &&
+ mMediaPlayer != null &&
+ mMediaController != null) {
+ if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK) {
+ if (mMediaPlayer.isPlaying()) {
+ pause();
+ mMediaController.show();
+ } else {
+ start();
+ mMediaController.hide();
+ }
+ return true;
+ } else {
+ toggleMediaControlsVisiblity();
+ }
+ }
+
+ return super.onKeyDown(keyCode, event);
+ }
+
+ private void toggleMediaControlsVisiblity() {
+ if (mMediaController.isShowing()) {
+ mMediaController.hide();
+ } else {
+ mMediaController.show();
+ }
+ }
+
+ public void start() {
+ if (mMediaPlayer != null && mIsPrepared) {
+ mMediaPlayer.start();
+ mStartWhenPrepared = false;
+ } else {
+ mStartWhenPrepared = true;
+ }
+ }
+
+ public void pause() {
+ if (mMediaPlayer != null && mIsPrepared) {
+ if (mMediaPlayer.isPlaying()) {
+ mMediaPlayer.pause();
+ }
+ }
+ mStartWhenPrepared = false;
+ }
+
+ public int getDuration() {
+ if (mMediaPlayer != null && mIsPrepared) {
+ return mMediaPlayer.getDuration();
+ }
+ return -1;
+ }
+
+ public int getCurrentPosition() {
+ if (mMediaPlayer != null && mIsPrepared) {
+ return mMediaPlayer.getCurrentPosition();
+ }
+ return 0;
+ }
+
+ public void seekTo(int msec) {
+ if (mMediaPlayer != null && mIsPrepared) {
+ mMediaPlayer.seekTo(msec);
+ } else {
+ mSeekWhenPrepared = msec;
+ }
+ }
+
+ public boolean isPlaying() {
+ if (mMediaPlayer != null && mIsPrepared) {
+ return mMediaPlayer.isPlaying();
+ }
+ return false;
+ }
+
+ public int getBufferPercentage() {
+ if (mMediaPlayer != null) {
+ return mCurrentBufferPercentage;
+ }
+ return 0;
+ }
+}
diff --git a/core/java/android/widget/ViewAnimator.java b/core/java/android/widget/ViewAnimator.java
new file mode 100644
index 0000000..acc9c46
--- /dev/null
+++ b/core/java/android/widget/ViewAnimator.java
@@ -0,0 +1,247 @@
+/*
+ * 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.widget;
+
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+
+/**
+ * Base class for a {@link FrameLayout} container that will perform animations
+ * when switching between its views.
+ */
+public class ViewAnimator extends FrameLayout {
+
+ int mWhichChild = 0;
+ boolean mFirstTime = true;
+ boolean mAnimateFirstTime = true;
+
+ Animation mInAnimation;
+ Animation mOutAnimation;
+
+ public ViewAnimator(Context context) {
+ super(context);
+ initViewAnimator();
+ }
+
+ public ViewAnimator(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.ViewAnimator);
+ int resource = a.getResourceId(com.android.internal.R.styleable.ViewAnimator_inAnimation, 0);
+ if (resource > 0) {
+ setInAnimation(context, resource);
+ }
+
+ resource = a.getResourceId(com.android.internal.R.styleable.ViewAnimator_outAnimation, 0);
+ if (resource > 0) {
+ setOutAnimation(context, resource);
+ }
+ a.recycle();
+
+ initViewAnimator();
+ }
+
+ private void initViewAnimator() {
+ mMeasureAllChildren = true;
+ }
+
+ /**
+ * Sets which child view will be displayed.
+ *
+ * @param whichChild the index of the child view to display
+ */
+ public void setDisplayedChild(int whichChild) {
+ mWhichChild = whichChild;
+ if (whichChild >= getChildCount()) {
+ mWhichChild = 0;
+ } else if (whichChild < 0) {
+ mWhichChild = getChildCount() - 1;
+ }
+ boolean hasFocus = getFocusedChild() != null;
+ // This will clear old focus if we had it
+ showOnly(mWhichChild);
+ if (hasFocus) {
+ // Try to retake focus if we had it
+ requestFocus(FOCUS_FORWARD);
+ }
+ }
+
+ /**
+ * Returns the index of the currently displayed child view.
+ */
+ public int getDisplayedChild() {
+ return mWhichChild;
+ }
+
+ /**
+ * Manually shows the next child.
+ */
+ public void showNext() {
+ setDisplayedChild(mWhichChild + 1);
+ }
+
+ /**
+ * Manually shows the previous child.
+ */
+ public void showPrevious() {
+ setDisplayedChild(mWhichChild - 1);
+ }
+
+ /**
+ * Shows only the specified child. The other displays Views exit the screen
+ * with the {@link #getOutAnimation() out animation} and the specified child
+ * enters the screen with the {@link #getInAnimation() in animation}.
+ *
+ * @param childIndex The index of the child to be shown.
+ */
+ void showOnly(int childIndex) {
+ final int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (i == childIndex) {
+ if ((!mFirstTime || mAnimateFirstTime) && mInAnimation != null) {
+ child.startAnimation(mInAnimation);
+ }
+ child.setVisibility(View.VISIBLE);
+ mFirstTime = false;
+ } else {
+ if (mOutAnimation != null && child.getVisibility() == View.VISIBLE) {
+ child.startAnimation(mOutAnimation);
+ } else if (child.getAnimation() == mInAnimation)
+ child.clearAnimation();
+ child.setVisibility(View.GONE);
+ }
+ }
+ }
+
+ @Override
+ public void addView(View child, int index, ViewGroup.LayoutParams params) {
+ super.addView(child, index, params);
+ if (getChildCount() == 1) {
+ child.setVisibility(View.VISIBLE);
+ } else {
+ child.setVisibility(View.GONE);
+ }
+ }
+
+ /**
+ * Returns the View corresponding to the currently displayed child.
+ *
+ * @return The View currently displayed.
+ *
+ * @see #getDisplayedChild()
+ */
+ public View getCurrentView() {
+ return getChildAt(mWhichChild);
+ }
+
+ /**
+ * Returns the current animation used to animate a View that enters the screen.
+ *
+ * @return An Animation or null if none is set.
+ *
+ * @see #setInAnimation(android.view.animation.Animation)
+ * @see #setInAnimation(android.content.Context, int)
+ */
+ public Animation getInAnimation() {
+ return mInAnimation;
+ }
+
+ /**
+ * Specifies the animation used to animate a View that enters the screen.
+ *
+ * @param inAnimation The animation started when a View enters the screen.
+ *
+ * @see #getInAnimation()
+ * @see #setInAnimation(android.content.Context, int)
+ */
+ public void setInAnimation(Animation inAnimation) {
+ mInAnimation = inAnimation;
+ }
+
+ /**
+ * Returns the current animation used to animate a View that exits the screen.
+ *
+ * @return An Animation or null if none is set.
+ *
+ * @see #setOutAnimation(android.view.animation.Animation)
+ * @see #setOutAnimation(android.content.Context, int)
+ */
+ public Animation getOutAnimation() {
+ return mOutAnimation;
+ }
+
+ /**
+ * Specifies the animation used to animate a View that exit the screen.
+ *
+ * @param outAnimation The animation started when a View exit the screen.
+ *
+ * @see #getOutAnimation()
+ * @see #setOutAnimation(android.content.Context, int)
+ */
+ public void setOutAnimation(Animation outAnimation) {
+ mOutAnimation = outAnimation;
+ }
+
+ /**
+ * Specifies the animation used to animate a View that enters the screen.
+ *
+ * @param context The application's environment.
+ * @param resourceID The resource id of the animation.
+ *
+ * @see #getInAnimation()
+ * @see #setInAnimation(android.view.animation.Animation)
+ */
+ public void setInAnimation(Context context, int resourceID) {
+ setInAnimation(AnimationUtils.loadAnimation(context, resourceID));
+ }
+
+ /**
+ * Specifies the animation used to animate a View that exit the screen.
+ *
+ * @param context The application's environment.
+ * @param resourceID The resource id of the animation.
+ *
+ * @see #getOutAnimation()
+ * @see #setOutAnimation(android.view.animation.Animation)
+ */
+ public void setOutAnimation(Context context, int resourceID) {
+ setOutAnimation(AnimationUtils.loadAnimation(context, resourceID));
+ }
+
+ /**
+ * Indicates whether the current View should be animated the first time
+ * the ViewAnimation is displayed.
+ *
+ * @param animate True to animate the current View the first time it is displayed,
+ * false otherwise.
+ */
+ public void setAnimateFirstView(boolean animate) {
+ mAnimateFirstTime = animate;
+ }
+
+ @Override
+ public int getBaseline() {
+ return (getCurrentView() != null) ? getCurrentView().getBaseline() : super.getBaseline();
+ }
+}
diff --git a/core/java/android/widget/ViewFlipper.java b/core/java/android/widget/ViewFlipper.java
new file mode 100644
index 0000000..a3c15d9
--- /dev/null
+++ b/core/java/android/widget/ViewFlipper.java
@@ -0,0 +1,99 @@
+/*
+ * 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.widget;
+
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.os.Handler;
+import android.os.Message;
+import android.util.AttributeSet;
+
+/**
+ * Simple {@link ViewAnimator} that will animate between two or more views
+ * that have been added to it. Only one child is shown at a time. If
+ * requested, can automatically flip between each child at a regular interval.
+ */
+public class ViewFlipper extends ViewAnimator {
+ private int mFlipInterval = 3000;
+ private boolean mKeepFlipping = false;
+
+ public ViewFlipper(Context context) {
+ super(context);
+ }
+
+ public ViewFlipper(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ TypedArray a = context.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.ViewFlipper);
+ mFlipInterval = a.getInt(com.android.internal.R.styleable.ViewFlipper_flipInterval,
+ 3000);
+ a.recycle();
+ }
+
+ /**
+ * How long to wait before flipping to the next view
+ *
+ * @param milliseconds
+ * time in milliseconds
+ */
+ public void setFlipInterval(int milliseconds) {
+ mFlipInterval = milliseconds;
+ }
+
+ /**
+ * Start a timer to cycle through child views
+ */
+ public void startFlipping() {
+ if (!mKeepFlipping) {
+ mKeepFlipping = true;
+ showOnly(mWhichChild);
+ Message msg = mHandler.obtainMessage(FLIP_MSG);
+ mHandler.sendMessageDelayed(msg, mFlipInterval);
+ }
+ }
+
+ /**
+ * No more flips
+ */
+ public void stopFlipping() {
+ mKeepFlipping = false;
+ }
+
+ /**
+ * Returns true if the child views are flipping.
+ */
+ public boolean isFlipping() {
+ return mKeepFlipping;
+ }
+
+ private final int FLIP_MSG = 1;
+
+ private final Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.what == FLIP_MSG) {
+ if (mKeepFlipping) {
+ showNext();
+ msg = obtainMessage(FLIP_MSG);
+ sendMessageDelayed(msg, mFlipInterval);
+ }
+ }
+ }
+ };
+}
diff --git a/core/java/android/widget/ViewSwitcher.java b/core/java/android/widget/ViewSwitcher.java
new file mode 100644
index 0000000..f4f23a8
--- /dev/null
+++ b/core/java/android/widget/ViewSwitcher.java
@@ -0,0 +1,135 @@
+/*
+ * 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.widget;
+
+import java.util.Map;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * {@link ViewAnimator} that switches between two views, and has a factory
+ * from which these views are created. You can either use the factory to
+ * create the views, or add them yourself. A ViewSwitcher can only have two
+ * child views, of which only one is shown at a time.
+ */
+public class ViewSwitcher extends ViewAnimator {
+ /**
+ * The factory used to create the two children.
+ */
+ ViewFactory mFactory;
+
+ /**
+ * Creates a new empty ViewSwitcher.
+ *
+ * @param context the application's environment
+ */
+ public ViewSwitcher(Context context) {
+ super(context);
+ }
+
+ /**
+ * Creates a new empty ViewSwitcher for the given context and with the
+ * specified set attributes.
+ *
+ * @param context the application environment
+ * @param attrs a collection of attributes
+ */
+ public ViewSwitcher(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @throws IllegalStateException if this switcher already contains two children
+ */
+ @Override
+ public void addView(View child, int index, ViewGroup.LayoutParams params) {
+ if (getChildCount() >= 2) {
+ throw new IllegalStateException("Can't add more than 2 views to a ViewSwitcher");
+ }
+ super.addView(child, index, params);
+ }
+
+ /**
+ * Returns the next view to be displayed.
+ *
+ * @return the view that will be displayed after the next views flip.
+ */
+ public View getNextView() {
+ int which = mWhichChild == 0 ? 1 : 0;
+ return getChildAt(which);
+ }
+
+ private View obtainView() {
+ View child = mFactory.makeView();
+ LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (lp == null) {
+ lp = new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT);
+ }
+ addView(child, lp);
+ return child;
+ }
+
+ /**
+ * Sets the factory used to create the two views between which the
+ * ViewSwitcher will flip. Instead of using a factory, you can call
+ * {@link #addView(android.view.View, int, android.view.ViewGroup.LayoutParams)}
+ * twice.
+ *
+ * @param factory the view factory used to generate the switcher's content
+ */
+ public void setFactory(ViewFactory factory) {
+ mFactory = factory;
+ obtainView();
+ obtainView();
+ }
+
+ /**
+ * Reset the ViewSwitcher to hide all of the existing views and to make it
+ * think that the first time animation has not yet played.
+ */
+ public void reset() {
+ mFirstTime = true;
+ View v;
+ v = getChildAt(0);
+ if (v != null) {
+ v.setVisibility(View.GONE);
+ }
+ v = getChildAt(1);
+ if (v != null) {
+ v.setVisibility(View.GONE);
+ }
+ }
+
+ /**
+ * Creates views in a ViewSwitcher.
+ */
+ public interface ViewFactory {
+ /**
+ * Creates a new {@link android.view.View} to be added in a
+ * {@link android.widget.ViewSwitcher}.
+ *
+ * @return a {@link android.view.View}
+ */
+ View makeView();
+ }
+}
+
diff --git a/core/java/android/widget/WrapperListAdapter.java b/core/java/android/widget/WrapperListAdapter.java
new file mode 100644
index 0000000..7fe12ae
--- /dev/null
+++ b/core/java/android/widget/WrapperListAdapter.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+/**
+ * List adapter that wraps another list adapter. The wrapped adapter can be retrieved
+ * by calling {@link #getWrappedAdapter()}.
+ *
+ * @see ListView
+ */
+public interface WrapperListAdapter extends ListAdapter {
+ /**
+ * Returns the adapter wrapped by this list adapter.
+ *
+ * @return The {@link android.widget.ListAdapter} wrapped by this adapter.
+ */
+ public ListAdapter getWrappedAdapter();
+}
diff --git a/core/java/android/widget/ZoomButton.java b/core/java/android/widget/ZoomButton.java
new file mode 100644
index 0000000..5df8c8a
--- /dev/null
+++ b/core/java/android/widget/ZoomButton.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import android.content.Context;
+import android.os.Handler;
+import android.util.AttributeSet;
+import android.view.GestureDetector;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.GestureDetector.SimpleOnGestureListener;
+import android.view.View.OnLongClickListener;
+
+
+public class ZoomButton extends ImageButton implements OnLongClickListener {
+
+ private final Handler mHandler;
+ private final Runnable mRunnable = new Runnable() {
+ public void run() {
+ if ((mOnClickListener != null) && mIsInLongpress && isEnabled()) {
+ mOnClickListener.onClick(ZoomButton.this);
+ mHandler.postDelayed(this, mZoomSpeed);
+ }
+ }
+ };
+ private final GestureDetector mGestureDetector;
+
+ private long mZoomSpeed = 1000;
+ private boolean mIsInLongpress;
+
+ public ZoomButton(Context context) {
+ this(context, null);
+ }
+
+ public ZoomButton(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ZoomButton(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ mHandler = new Handler();
+ mGestureDetector = new GestureDetector(new SimpleOnGestureListener() {
+ @Override
+ public void onLongPress(MotionEvent e) {
+ onLongClick(ZoomButton.this);
+ }
+ });
+ setOnLongClickListener(this);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ mGestureDetector.onTouchEvent(event);
+ if ((event.getAction() == MotionEvent.ACTION_CANCEL)
+ || (event.getAction() == MotionEvent.ACTION_UP)) {
+ mIsInLongpress = false;
+ }
+ return super.onTouchEvent(event);
+ }
+
+ public void setZoomSpeed(long speed) {
+ mZoomSpeed = speed;
+ }
+
+ public boolean onLongClick(View v) {
+ mIsInLongpress = true;
+ mHandler.post(mRunnable);
+ return true;
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ mIsInLongpress = false;
+ return super.onKeyUp(keyCode, event);
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ if (!enabled) {
+
+ /* If we're being disabled reset the state back to unpressed
+ * as disabled views don't get events and therefore we won't
+ * get the up event to reset the state.
+ */
+ setPressed(false);
+ }
+ super.setEnabled(enabled);
+ }
+
+ @Override
+ public boolean dispatchUnhandledMove(View focused, int direction) {
+ clearFocus();
+ return super.dispatchUnhandledMove(focused, direction);
+ }
+}
diff --git a/core/java/android/widget/ZoomControls.java b/core/java/android/widget/ZoomControls.java
new file mode 100644
index 0000000..1fd662c
--- /dev/null
+++ b/core/java/android/widget/ZoomControls.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import android.annotation.Widget;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.animation.AlphaAnimation;
+
+import com.android.internal.R;
+
+
+/**
+ * The {@code ZoomControls} class displays a simple set of controls used for zooming and
+ * provides callbacks to register for events.
+ */
+@Widget
+public class ZoomControls extends LinearLayout {
+
+ private final ZoomButton mZoomIn;
+ private final ZoomButton mZoomOut;
+
+ public ZoomControls(Context context) {
+ this(context, null);
+ }
+
+ public ZoomControls(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ setFocusable(false);
+
+ LayoutInflater inflater = (LayoutInflater) context
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ inflater.inflate(R.layout.zoom_controls, this, // we are the parent
+ true);
+
+ mZoomIn = (ZoomButton) findViewById(R.id.zoomIn);
+ mZoomOut = (ZoomButton) findViewById(R.id.zoomOut);
+ }
+
+ public void setOnZoomInClickListener(OnClickListener listener) {
+ mZoomIn.setOnClickListener(listener);
+ }
+
+ public void setOnZoomOutClickListener(OnClickListener listener) {
+ mZoomOut.setOnClickListener(listener);
+ }
+
+ /*
+ * Sets how fast you get zoom events when the user holds down the
+ * zoom in/out buttons.
+ */
+ public void setZoomSpeed(long speed) {
+ mZoomIn.setZoomSpeed(speed);
+ mZoomOut.setZoomSpeed(speed);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+
+ /* Consume all touch events so they don't get dispatched to the view
+ * beneath this view.
+ */
+ return true;
+ }
+
+ public void show() {
+ fade(View.VISIBLE, 0.0f, 1.0f);
+ }
+
+ public void hide() {
+ fade(View.GONE, 1.0f, 0.0f);
+ }
+
+ private void fade(int visibility, float startAlpha, float endAlpha) {
+ AlphaAnimation anim = new AlphaAnimation(startAlpha, endAlpha);
+ anim.setDuration(500);
+ startAnimation(anim);
+ setVisibility(visibility);
+ }
+
+ public void setIsZoomInEnabled(boolean isEnabled) {
+ mZoomIn.setEnabled(isEnabled);
+ }
+
+ public void setIsZoomOutEnabled(boolean isEnabled) {
+ mZoomOut.setEnabled(isEnabled);
+ }
+
+ @Override
+ public boolean hasFocus() {
+ return mZoomIn.hasFocus() || mZoomOut.hasFocus();
+ }
+}
diff --git a/core/java/android/widget/package.html b/core/java/android/widget/package.html
new file mode 100644
index 0000000..7d94a4b
--- /dev/null
+++ b/core/java/android/widget/package.html
@@ -0,0 +1,32 @@
+<HTML>
+<BODY>
+The widget package contains (mostly visual) UI elements to use
+on your Application screen. You can design your own <p>
+To create your own widget, extend {@link android.view.View} or a subclass. To
+use your widget in layout XML, there are two additional files for you to
+create. Here is a list of files you'll need to create to implement a custom
+widget:
+<ul>
+<li><b>Java implementation file</b> - This is the file that implements the
+behavior of the widget. If you can instantiate the object from layout XML,
+you will also have to code a constructor that retrieves all the attribute
+values from the layout XML file.</li>
+<li><b>XML definition file</b> - An XML file in res/values/ that defines
+the XML element used to instantiate your widget, and the attributes that it
+supports. Other applications will use this element and attributes in their in
+another in their layout XML.</li>
+<li><b>Layout XML</b> [<em>optional</em>]- An optional XML file inside
+res/layout/ that describes the layout of your widget. You could also do
+this in code in your Java file.</li>
+</ul>
+ApiDemos sample application has an example of creating a custom layout XML
+tag, LabelView. See the following files that demonstrate implementing and using
+a custom widget:</p>
+<ul>
+ <li><strong>LabelView.java</strong> - The implentation file</li>
+ <li><strong>res/values/attrs.xml</strong> - Definition file</li>
+ <li><strong>res/layout/custom_view_1.xml</strong> - Layout
+file</li>
+</ul>
+</BODY>
+</HTML>