From 527a4f30a69aaa54fc9654539f0b6d839e37af3c Mon Sep 17 00:00:00 2001 From: Trevor Johns Date: Wed, 12 Nov 2014 11:39:30 -0800 Subject: [PATCH] Update browseable samples for lmp-docs Synced to commit df5e5013422b81b4fd05c0ac9fd964b13624847a. Includes new samples for Android Auto. Change-Id: I3fec46e2a6b3f196682a92f1afd91eb682dc2dc1 --- .../ActionBarCompat-Basic/AndroidManifest.xml | 4 +- .../ActionBarCompat-Basic/_index.jd | 19 +- .../res/values-v21/template-styles.xml | 22 + .../res/values/base-strings.xml | 3 - .../AndroidManifest.xml | 4 +- .../ActionBarCompat-ListPopupMenu/_index.jd | 16 +- .../res/values-v21/template-styles.xml | 22 + .../res/values/base-strings.xml | 3 - .../AndroidManifest.xml | 4 +- .../_index.jd | 16 +- .../res/values-v21/template-styles.xml | 22 + .../res/values/base-strings.xml | 3 - .../AndroidManifest.xml | 4 +- .../ActionBarCompat-Styled/_index.jd | 15 +- .../res/values-v21/template-styles.xml | 22 + .../res/values/base-strings.xml | 3 - .../AndroidManifest.xml | 2 +- .../ActivityInstrumentation/_index.jd | 15 +- .../res/values-v21/template-styles.xml | 22 + .../res/values/base-strings.xml | 3 - .../DetailActivity.java | 4 +- .../MainActivity.java | 4 +- .../AdapterTransition/AndroidManifest.xml | 4 +- .../browseable/AdapterTransition/_index.jd | 11 +- .../res/values-v21/template-styles.xml | 22 + .../res/values/base-strings.xml | 3 - .../AdapterTransition/res/values/strings.xml | 0 .../MainActivity.java | 13 +- .../AdvancedImmersiveMode/AndroidManifest.xml | 2 +- .../AdvancedImmersiveMode/_index.jd | 21 +- .../res/values-v21/template-styles.xml | 22 + .../res/values/base-strings.xml | 3 - .../MainActivity.java | 13 +- .../Application/AndroidManifest.xml | 6 +- .../res/values-v21/template-styles.xml | 22 + .../CalendarQueryService.java | 26 +- .../Constants.java | 2 +- .../MainActivity.java | 4 +- .../AgendaData/Shared/AndroidManifest.xml | 25 - .../AgendaData/Wearable/AndroidManifest.xml | 6 +- .../Constants.java | 2 +- .../DeleteService.java | 6 +- .../HomeListenerService.java | 18 +- .../AppRestrictions/AndroidManifest.xml | 2 +- samples/browseable/AppRestrictions/_index.jd | 3 - .../res/values-v21/template-styles.xml | 22 + .../res/values/base-strings.xml | 3 - .../BasicAccessibility/AndroidManifest.xml | 4 +- .../browseable/BasicAccessibility/_index.jd | 13 +- .../res/values-v21/template-styles.xml | 22 + .../res/values/base-strings.xml | 3 - .../BasicAndroidKeyStore/AndroidManifest.xml | 2 +- .../browseable/BasicAndroidKeyStore/_index.jd | 21 +- .../res/values-v21/template-styles.xml | 22 + .../res/values/base-strings.xml | 3 - .../MainActivity.java | 3 - .../BasicContactables/AndroidManifest.xml | 4 +- .../browseable/BasicContactables/_index.jd | 16 +- .../res/values-v21/template-styles.xml | 22 + .../res/values/base-strings.xml | 3 - .../BasicGestureDetect/AndroidManifest.xml | 2 +- .../browseable/BasicGestureDetect/_index.jd | 14 +- .../res/values-v21/template-styles.xml | 22 + .../res/values/base-strings.xml | 3 - .../BasicGestureDetect/res/values/strings.xml | 3 - .../MainActivity.java | 3 - .../BasicImmersiveMode/AndroidManifest.xml | 2 +- .../browseable/BasicImmersiveMode/_index.jd | 15 +- .../res/values-v21/template-styles.xml | 22 + .../res/values/base-strings.xml | 3 - .../BasicImmersiveMode/res/values/strings.xml | 3 - .../MainActivity.java | 3 - .../BasicMediaDecoder/AndroidManifest.xml | 3 +- .../browseable/BasicMediaDecoder/_index.jd | 11 +- .../res/values-v21/template-styles.xml | 22 + .../res/values/base-strings.xml | 3 - .../BasicMediaRouter/AndroidManifest.xml | 4 +- samples/browseable/BasicMediaRouter/_index.jd | 24 +- .../res/values-v21/template-styles.xml | 22 + .../res/values/base-strings.xml | 3 - .../BasicMultitouch/AndroidManifest.xml | 4 +- samples/browseable/BasicMultitouch/_index.jd | 16 +- .../res/values-v21/template-styles.xml | 22 + .../res/values/base-strings.xml | 3 - .../BasicNetworking/AndroidManifest.xml | 2 +- samples/browseable/BasicNetworking/_index.jd | 14 +- .../res/values-v21/template-styles.xml | 22 + .../res/values/base-strings.xml | 3 - .../BasicNotifications/AndroidManifest.xml | 4 +- .../browseable/BasicNotifications/_index.jd | 15 +- .../res/values-v21/template-styles.xml | 22 + .../res/values/base-strings.xml | 3 - .../BasicRenderScript/AndroidManifest.xml | 4 +- .../browseable/BasicRenderScript/_index.jd | 13 +- .../res/values-v21/template-styles.xml | 22 + .../res/values/base-strings.xml | 3 - .../MediaCodecWrapper.java | 3 +- .../BasicSyncAdapter/AndroidManifest.xml | 4 +- samples/browseable/BasicSyncAdapter/_index.jd | 20 +- .../res/values-v21/template-styles.xml | 22 + .../res/values/base-strings.xml | 3 - .../BasicTransition/AndroidManifest.xml | 2 +- samples/browseable/BasicTransition/_index.jd | 12 +- .../res/values-v21/template-styles.xml | 22 + .../res/values/base-strings.xml | 3 - .../BasicTransition/res/values/strings.xml | 0 .../MainActivity.java | 13 +- .../BatchStepSensor/AndroidManifest.xml | 2 +- samples/browseable/BatchStepSensor/_index.jd | 35 +- .../res/drawable-v21/card_action_bg.xml | 20 + .../drawable-v21/card_action_bg_negative.xml | 20 + .../drawable-v21/card_action_bg_positive.xml | 20 + .../res/layout/activity_main.xml | 3 - .../BatchStepSensor/res/layout/card.xml | 3 - .../res/layout/card_button_negative.xml | 3 - .../res/layout/card_button_neutral.xml | 3 - .../res/layout/card_button_positive.xml | 3 - .../BatchStepSensor/res/layout/cardstream.xml | 3 - .../res/values-v21/template-styles.xml | 22 + .../res/values/base-strings.xml | 3 - .../BatchStepSensor/res/values/color.xml | 5 +- .../MainActivity.java | 5 +- .../cardstream/Card.java | 3 - .../cardstream/CardActionButton.java | 33 +- .../cardstream/CardLayout.java | 3 - .../cardstream/CardStream.java | 3 - .../cardstream/CardStreamAnimator.java | 3 - .../cardstream/CardStreamFragment.java | 3 - .../cardstream/CardStreamLinearLayout.java | 3 - .../cardstream/CardStreamState.java | 3 - .../cardstream/DefaultCardStreamAnimator.java | 3 - .../cardstream/OnCardClickListener.java | 3 - .../cardstream/StreamRetentionFragment.java | 3 - .../BluetoothChat/AndroidManifest.xml | 53 + samples/browseable/BluetoothChat/_index.jd | 15 + ...tion_device_access_bluetooth_searching.png | Bin 0 -> 1355 bytes .../res/drawable-hdpi/ic_launcher.png | Bin 0 -> 4689 bytes .../res/drawable-hdpi/tile.9.png} | Bin ...tion_device_access_bluetooth_searching.png | Bin 0 -> 841 bytes .../res/drawable-mdpi/ic_launcher.png | Bin 0 -> 2834 bytes ...tion_device_access_bluetooth_searching.png | Bin 0 -> 1879 bytes .../res/drawable-xhdpi/ic_launcher.png | Bin 0 -> 6681 bytes ...tion_device_access_bluetooth_searching.png | Bin 0 -> 3083 bytes .../res/drawable-xxhdpi/ic_launcher.png | Bin 0 -> 12071 bytes .../res/layout-w720dp/activity_main.xml | 73 ++ .../res/layout/activity_device_list.xml | 66 ++ .../res/layout/activity_main.xml | 65 ++ .../res/layout/device_name.xml} | 9 +- .../res/layout/fragment_bluetooth_chat.xml | 49 + .../res/layout/message.xml} | 9 +- .../BluetoothChat/res/menu/bluetooth_chat.xml | 34 + .../BluetoothChat/res/menu/main.xml | 21 + .../res/values-sw600dp/template-dimens.xml | 24 + .../res/values-sw600dp/template-styles.xml | 25 + .../res/values-v11/template-styles.xml} | 9 +- .../res/values-v21/template-styles.xml | 22 + .../BluetoothChat/res/values/base-strings.xml | 34 + .../res/values/fragmentview_strings.xml} | 0 .../BluetoothChat/res/values/strings.xml | 41 + .../res/values/template-dimens.xml | 32 + .../res/values/template-styles.xml | 42 + .../BluetoothChatFragment.java | 402 ++++++++ .../BluetoothChatService.java | 519 ++++++++++ .../Constants.java | 35 + .../DeviceListActivity.java | 216 ++++ .../MainActivity.java | 109 ++ .../activities/SampleActivityBase.java | 0 .../logger/Log.java | 0 .../logger/LogFragment.java | 0 .../logger/LogNode.java | 0 .../logger/LogView.java | 0 .../logger/LogWrapper.java | 0 .../logger/MessageOnlyLogFilter.java | 0 .../BluetoothLeGatt/AndroidManifest.xml | 4 +- samples/browseable/BluetoothLeGatt/_index.jd | 14 +- .../res/values-v21/template-styles.xml | 22 + .../res/values/base-strings.xml | 3 - .../BorderlessButtons/AndroidManifest.xml | 2 +- .../browseable/BorderlessButtons/_index.jd | 11 +- .../res/values-v21/template-styles.xml | 22 + .../res/values/base-strings.xml | 3 - .../CardEmulation/AndroidManifest.xml | 2 +- samples/browseable/CardEmulation/_index.jd | 21 +- .../res/values-v21/template-styles.xml | 22 + .../CardEmulation/res/values/base-strings.xml | 3 - .../CardEmulation/res/values/strings.xml | 0 .../MainActivity.java | 13 +- .../browseable/CardReader/AndroidManifest.xml | 2 +- samples/browseable/CardReader/_index.jd | 22 +- .../res/values-v21/template-styles.xml | 22 + .../CardReader/res/values/base-strings.xml | 3 - .../CardReader/res/values/strings.xml | 0 .../MainActivity.java | 13 +- .../CustomChoiceList/AndroidManifest.xml | 2 +- samples/browseable/CustomChoiceList/_index.jd | 11 +- .../res/values-v21/template-styles.xml | 22 + .../res/values/base-strings.xml | 3 - .../CustomNotifications/AndroidManifest.xml | 4 +- .../browseable/CustomNotifications/_index.jd | 12 +- .../res/values-v21/template-styles.xml | 22 + .../res/values/base-strings.xml | 3 - .../CustomTransition/AndroidManifest.xml | 2 +- samples/browseable/CustomTransition/_index.jd | 9 +- .../res/values-v21/template-styles.xml | 22 + .../res/values/base-strings.xml | 3 - .../MainActivity.java | 13 +- .../DataLayer/Application/AndroidManifest.xml | 2 +- .../res/values-v21/template-styles.xml | 22 + .../MainActivity.java | 2 +- .../DataLayer/Wearable/AndroidManifest.xml | 4 +- .../DataLayerListenerService.java | 2 +- .../MainActivity.java | 4 +- .../Application/AndroidManifest.xml | 2 +- .../res/values-v21/template-styles.xml | 22 + .../activities/SampleActivityBase.java | 52 - .../logger/Log.java | 236 ----- .../logger/LogFragment.java | 109 -- .../logger/LogNode.java | 39 - .../logger/LogView.java | 145 --- .../logger/LogWrapper.java | 75 -- .../logger/MessageOnlyLogFilter.java | 60 -- .../MainActivity.java | 2 +- .../Wearable/AndroidManifest.xml | 2 +- .../MainActivity.java | 2 +- .../WearableMessageListenerService.java | 2 +- .../DisplayingBitmaps/AndroidManifest.xml | 2 +- .../browseable/DisplayingBitmaps/_index.jd | 26 +- .../res/values-v21/template-styles.xml | 22 + .../res/values/base-strings.xml | 3 - .../browseable/DoneBar/AndroidManifest.xml | 2 +- samples/browseable/DoneBar/_index.jd | 19 +- .../res/layout/sample_dashboard_item.xml | 42 +- .../res/values-v21/template-styles.xml | 22 + .../res/values/activitycards-colors.xml | 21 + .../res/values/activitycards-dimens.xml | 22 + .../res/values/activitycards-strings.xml | 3 - .../DoneBar/res/values/base-strings.xml | 3 - .../res/values/template-attrs.xml} | 7 +- .../DoneBar/res/values/template-styles.xml | 25 +- .../MainActivity.java | 3 - .../ElizaChat/Application/AndroidManifest.xml | 6 +- .../res/values-v21/template-styles.xml | 22 + .../activities/SampleActivityBase.java | 52 - .../logger/Log.java | 236 ----- .../logger/LogFragment.java | 109 -- .../logger/LogNode.java | 39 - .../logger/LogView.java | 145 --- .../logger/LogWrapper.java | 75 -- .../logger/MessageOnlyLogFilter.java | 60 -- .../ElizaResponder.java | 2 +- .../MainActivity.java | 6 +- .../ResponderService.java | 6 +- .../ElizaChat/Shared/AndroidManifest.xml | 2 +- .../Application/AndroidManifest.xml | 4 +- .../res/values-v21/template-styles.xml | 22 + .../activities/SampleActivityBase.java | 52 - .../logger/Log.java | 236 ----- .../logger/LogFragment.java | 109 -- .../logger/LogNode.java | 39 - .../logger/LogView.java | 145 --- .../logger/LogWrapper.java | 75 -- .../logger/MessageOnlyLogFilter.java | 60 -- .../PhoneActivity.java | 2 +- .../EmbeddedApp/Shared/AndroidManifest.xml | 25 - .../EmbeddedApp/Wearable/AndroidManifest.xml | 2 +- .../WearableActivity.java | 2 +- .../Application/AndroidManifest.xml | 4 +- .../res/values-v21/template-styles.xml | 22 + .../activities/SampleActivityBase.java | 52 - .../logger/Log.java | 236 ----- .../logger/LogFragment.java | 109 -- .../logger/LogNode.java | 39 - .../logger/LogView.java | 145 --- .../logger/LogWrapper.java | 75 -- .../logger/MessageOnlyLogFilter.java | 60 -- .../SoundAlarmListenerService.java | 2 +- .../FindMyPhone/Shared/res/values/strings.xml | 18 - .../FindMyPhone/Wearable/AndroidManifest.xml | 4 +- .../DisconnectListenerService.java | 2 +- .../FindPhoneActivity.java | 2 +- .../FindPhoneService.java | 2 +- .../Application/AndroidManifest.xml | 2 +- .../res/values-v21/template-styles.xml | 22 + .../Flashlight/Shared/AndroidManifest.xml | 25 - .../Flashlight/Shared/res/values/strings.xml | 18 - .../Flashlight/Wearable/AndroidManifest.xml | 2 +- .../MainActivity.java | 2 +- .../PartyLightView.java | 2 +- .../FragmentTransition/AndroidManifest.xml | 2 +- .../browseable/FragmentTransition/_index.jd | 9 +- .../res/values-v21/template-styles.xml | 22 + .../res/values/base-strings.xml | 3 - .../FragmentTransition/res/values/strings.xml | 0 .../MainActivity.java | 13 +- .../Application/AndroidManifest.xml | 2 +- .../res/values-v21/template-styles.xml | 22 + .../activities/SampleActivityBase.java | 52 - .../logger/Log.java | 236 ----- .../logger/LogFragment.java | 109 -- .../logger/LogNode.java | 39 - .../logger/LogView.java | 145 --- .../logger/LogWrapper.java | 75 -- .../logger/MessageOnlyLogFilter.java | 60 -- .../Constants.java | 2 +- .../GeofenceTransitionsIntentService.java | 12 +- .../MainActivity.java | 24 +- .../SimpleGeofence.java | 2 +- .../SimpleGeofenceStore.java | 20 +- .../Geofencing/Shared/AndroidManifest.xml | 25 - .../Geofencing/Shared/res/values/strings.xml | 18 - .../Geofencing/Wearable/AndroidManifest.xml | 4 +- .../CheckInAndDeleteDataItemsService.java | 12 +- .../Constants.java | 2 +- .../HomeListenerService.java | 16 +- .../Application/AndroidManifest.xml | 2 +- .../res/values-v21/template-styles.xml | 22 + .../activities/SampleActivityBase.java | 52 - .../logger/Log.java | 236 ----- .../logger/LogFragment.java | 109 -- .../logger/LogNode.java | 39 - .../logger/LogView.java | 145 --- .../logger/LogWrapper.java | 75 -- .../logger/MessageOnlyLogFilter.java | 60 -- .../GridViewPager/Shared/AndroidManifest.xml | 25 - .../Shared/res/values/strings.xml | 18 - .../Wearable/AndroidManifest.xml | 4 +- .../MainActivity.java | 2 +- .../SampleGridPagerAdapter.java | 2 +- .../HorizontalPaging/AndroidManifest.xml | 4 +- samples/browseable/HorizontalPaging/_index.jd | 12 +- .../res/values-v21/template-styles.xml | 22 + .../res/values/base-strings.xml | 3 - .../ImmersiveMode/AndroidManifest.xml | 2 +- samples/browseable/ImmersiveMode/_index.jd | 15 +- .../res/values-v21/template-styles.xml | 22 + .../ImmersiveMode/res/values/base-strings.xml | 3 - .../ImmersiveMode/res/values/strings.xml | 3 - .../MainActivity.java | 3 - .../MainActivity.java | 4 +- .../Application/AndroidManifest.xml | 2 +- .../res/values-v21/template-styles.xml | 22 + .../JumpingJack/Shared/AndroidManifest.xml | 25 - .../JumpingJack/Shared/res/values/strings.xml | 18 - .../JumpingJack/Wearable/AndroidManifest.xml | 2 +- .../MainActivity.java | 6 +- .../PagerAdapter.java | 2 +- .../Utils.java | 2 +- .../fragments/CounterFragment.java | 6 +- .../fragments/SettingsFragment.java | 6 +- samples/browseable/LNotifications/_index.jd | 2 +- .../MediaBrowserService/AndroidManifest.xml | 65 ++ .../browseable/MediaBrowserService/_index.jd | 11 + .../res/drawable-hdpi/ic_launcher.png | Bin 0 -> 4805 bytes .../res/drawable-hdpi/ic_notification.png | Bin 0 -> 4163 bytes .../res/drawable-hdpi/ic_pause_white_24dp.png | Bin 0 -> 188 bytes .../ic_play_arrow_white_24dp.png | Bin 0 -> 282 bytes .../drawable-hdpi/ic_shuffle_white_24dp.png | Bin 0 -> 458 bytes .../drawable-hdpi/ic_skip_next_white_24dp.png | Bin 0 -> 291 bytes .../ic_skip_previous_white_24dp.png | Bin 0 -> 306 bytes .../res/drawable-mdpi/ic_launcher.png | Bin 0 -> 2592 bytes .../res/drawable-night-xxhdpi/ic_star_off.png | Bin 0 -> 3201 bytes .../res/drawable-night-xxhdpi/ic_star_on.png | Bin 0 -> 4058 bytes .../ic_equalizer_white_24dp.png | Bin 0 -> 207 bytes .../res/drawable-xhdpi/ic_launcher.png | Bin 0 -> 5246 bytes .../drawable-xhdpi/ic_pause_white_24dp.png | Bin 0 -> 193 bytes .../ic_play_arrow_white_24dp.png | Bin 0 -> 318 bytes .../drawable-xhdpi/ic_shuffle_white_24dp.png | Bin 0 -> 481 bytes .../ic_skip_next_white_24dp.png | Bin 0 -> 326 bytes .../ic_skip_previous_white_24dp.png | Bin 0 -> 354 bytes .../res/drawable-xxhdpi/ic_by_genre.png | Bin 0 -> 1562 bytes .../res/drawable-xxhdpi/ic_default_art.png | Bin 0 -> 1593 bytes .../ic_equalizer_white_24dp.png | Bin 0 -> 265 bytes .../res/drawable-xxhdpi/ic_launcher.png | Bin 0 -> 14755 bytes .../drawable-xxhdpi/ic_pause_white_24dp.png | Bin 0 -> 215 bytes .../ic_play_arrow_white_24dp.png | Bin 0 -> 399 bytes .../drawable-xxhdpi/ic_shuffle_white_24dp.png | Bin 0 -> 830 bytes .../ic_skip_next_white_24dp.png | Bin 0 -> 408 bytes .../ic_skip_previous_white_24dp.png | Bin 0 -> 447 bytes .../res/drawable-xxhdpi/ic_star_off.png | Bin 0 -> 3305 bytes .../res/drawable-xxhdpi/ic_star_on.png | Bin 0 -> 4131 bytes .../res/layout/activity_player.xml | 22 + .../res/layout/fragment_list.xml | 60 ++ .../res/layout/media_list_item.xml | 55 + .../res/values-v21/styles.xml | 33 + .../MediaBrowserService/res/values/dimens.xml | 21 + .../res/values/strings.xml | 33 + .../res/values/strings_notifications.xml | 24 + .../MediaBrowserService/res/values/styles.xml | 26 + .../res/xml/automotive_app_desc.xml | 19 + .../BrowseFragment.java | 210 ++++ .../MediaNotification.java | 381 +++++++ .../MusicPlayerActivity.java | 61 ++ .../MusicService.java | 936 ++++++++++++++++++ .../QueueAdapter.java | 82 ++ .../QueueFragment.java | 295 ++++++ .../model/MusicProvider.java | 296 ++++++ .../utils/BitmapHelper.java | 77 ++ .../utils/LogHelper.java | 67 ++ .../utils/MediaIDHelper.java | 88 ++ .../utils/QueueHelper.java | 129 +++ .../MediaEffects/AndroidManifest.xml | 43 + samples/browseable/MediaEffects/_index.jd | 12 + .../res/drawable-hdpi/ic_launcher.png | Bin 0 -> 4781 bytes .../res/drawable-hdpi/tile.9.png} | Bin .../res/drawable-mdpi/ic_launcher.png | Bin 0 -> 2872 bytes .../MediaEffects/res/drawable-nodpi/puppy.jpg | Bin 0 -> 73836 bytes .../res/drawable-xhdpi/ic_launcher.png | Bin 0 -> 6655 bytes .../res/drawable-xxhdpi/ic_launcher.png | Bin 0 -> 11507 bytes .../res/layout-w720dp/activity_main.xml | 73 ++ .../MediaEffects/res/layout/activity_main.xml | 65 ++ .../res/layout/fragment_media_effects.xml | 26 + .../browseable/MediaEffects/res/menu/main.xml | 21 + .../MediaEffects/res/menu/media_effects.xml | 112 +++ .../res/values-sw600dp/template-dimens.xml | 24 + .../res/values-sw600dp/template-styles.xml | 25 + .../res/values-v11/template-styles.xml | 22 + .../res/values-v21/template-styles.xml | 22 + .../res/values/base-strings.xml} | 24 +- .../res/values/fragmentview_strings.xml} | 0 .../MediaEffects/res/values/strings.xml | 41 + .../res/values/template-dimens.xml | 32 + .../res/values/template-styles.xml | 42 + .../activities/SampleActivityBase.java | 0 .../logger/Log.java | 0 .../logger/LogFragment.java | 0 .../logger/LogNode.java | 0 .../logger/LogView.java | 0 .../logger/LogWrapper.java | 0 .../logger/MessageOnlyLogFilter.java | 0 .../GLToolbox.java | 86 ++ .../MainActivity.java | 109 ++ .../MediaEffectsFragment.java | 287 ++++++ .../TextureRenderer.java | 164 +++ .../MediaRecorder/AndroidManifest.xml | 4 +- samples/browseable/MediaRecorder/_index.jd | 10 +- .../res/values-v21/template-styles.xml | 22 + .../MediaRecorder/res/values/base-strings.xml | 3 - .../MediaCodecWrapper.java | 3 +- .../MediaRouter/AndroidManifest.xml | 5 +- samples/browseable/MediaRouter/_index.jd | 7 +- .../res/values-v21/template-styles.xml | 22 + .../MediaRouter/res/values/base-strings.xml | 3 - .../MessagingService/AndroidManifest.xml | 52 + samples/browseable/MessagingService/_index.jd | 12 + .../res/drawable-hdpi/android_contact.png | Bin 0 -> 1575 bytes .../res/drawable-hdpi/ic_launcher.png | Bin 0 -> 3964 bytes .../res/drawable-hdpi/notification_icon.png | Bin 0 -> 937 bytes .../res/drawable-mdpi/android_contact.png | Bin 0 -> 959 bytes .../res/drawable-mdpi/ic_launcher.png | Bin 0 -> 2327 bytes .../res/drawable-mdpi/notification_icon.png | Bin 0 -> 609 bytes .../res/drawable-xhdpi/android_contact.png | Bin 0 -> 2451 bytes .../res/drawable-xhdpi/ic_launcher.png | Bin 0 -> 5488 bytes .../res/drawable-xhdpi/notification_icon.png | Bin 0 -> 1233 bytes .../res/drawable-xxhdpi/android_contact.png | Bin 0 -> 3267 bytes .../res/drawable-xxhdpi/ic_launcher.png | Bin 0 -> 9578 bytes .../res/drawable-xxhdpi/notification_icon.png | Bin 0 -> 1973 bytes .../res/layout-land/fragment_message_me.xml | 67 ++ .../res/layout/activity_main.xml | 22 + .../res/layout/fragment_message_me.xml | 58 ++ .../res/values-v21/styles.xml | 22 + .../MessagingService/res/values/colors.xml | 20 + .../MessagingService/res/values/dimens.xml | 21 + .../MessagingService/res/values/strings.xml | 26 + .../MessagingService/res/values/styles.xml | 20 + .../res/xml/automotive_app_desc.xml | 19 + .../Conversations.java | 126 +++ .../MainActivity.java | 34 + .../MessageLogger.java | 57 ++ .../MessageReadReceiver.java | 42 + .../MessageReplyReceiver.java | 58 ++ .../MessagingFragment.java | 170 ++++ .../MessagingService.java | 174 ++++ .../res/values/template-attrs.xml} | 7 +- .../NetworkConnect/AndroidManifest.xml | 2 +- samples/browseable/NetworkConnect/_index.jd | 12 +- .../res/values-v21/template-styles.xml | 22 + .../res/values/base-strings.xml | 3 - .../Application/AndroidManifest.xml | 8 +- .../res/values-v21/template-styles.xml | 22 + .../activities/SampleActivityBase.java | 52 - .../logger/Log.java | 236 ----- .../logger/LogFragment.java | 109 -- .../logger/LogNode.java | 39 - .../logger/LogView.java | 145 --- .../logger/LogWrapper.java | 75 -- .../logger/MessageOnlyLogFilter.java | 60 -- .../ActionsPreset.java | 2 +- .../ActionsPresets.java | 2 +- .../BackgroundPickers.java | 2 +- .../MainActivity.java | 2 +- .../NamedPreset.java | 2 +- .../NotificationIntentReceiver.java | 8 +- .../NotificationPreset.java | 2 +- .../NotificationPresets.java | 2 +- .../NotificationUtil.java | 6 +- .../PriorityPreset.java | 2 +- .../PriorityPresets.java | 2 +- .../Notifications/Shared/AndroidManifest.xml | 25 - .../Shared/res/values/strings.xml | 18 - .../Wearable/AndroidManifest.xml | 2 +- .../AnimatedNotificationDisplayActivity.java | 2 +- .../BasicNotificationDisplayActivity.java | 2 +- .../MainActivity.java | 2 +- .../NotificationPreset.java | 2 +- .../NotificationPresets.java | 2 +- .../WearableListItemLayout.java | 2 +- .../AndroidManifest.xml | 19 +- samples/browseable/PdfRendererBasic/_index.jd | 9 + .../res/drawable-hdpi/ic_action_info.png | Bin 0 -> 1025 bytes .../res/drawable-hdpi/ic_launcher.png | Bin 0 -> 3976 bytes .../res/drawable-hdpi/tile.9.png | Bin 0 -> 196 bytes .../res/drawable-mdpi/ic_action_info.png | Bin 0 -> 665 bytes .../res/drawable-mdpi/ic_launcher.png | Bin 0 -> 2373 bytes .../res/drawable-xhdpi/ic_action_info.png | Bin 0 -> 1355 bytes .../res/drawable-xhdpi/ic_launcher.png | Bin 0 -> 5478 bytes .../res/drawable-xxhdpi/ic_action_info.png | Bin 0 -> 2265 bytes .../res/drawable-xxhdpi/ic_launcher.png | Bin 0 -> 9652 bytes .../res/layout/activity_main.xml | 36 + .../res/layout/activity_main_real.xml | 23 + .../layout/fragment_pdf_renderer_basic.xml | 56 ++ .../PdfRendererBasic/res/menu/main.xml | 25 + .../res/values-sw600dp/template-dimens.xml | 24 + .../res/values-sw600dp/template-styles.xml | 25 + .../res/values-v11/template-styles.xml | 22 + .../res/values-v21/template-styles.xml | 22 + .../res/values/base-strings.xml} | 21 +- .../PdfRendererBasic/res/values/strings.xml | 24 + .../res/values/template-dimens.xml | 32 + .../res/values/template-styles.xml | 42 + .../MainActivity.java | 59 ++ .../PdfRendererBasicFragment.java | 221 +++++ .../Quiz/Application/AndroidManifest.xml | 4 +- .../res/values-v21/template-styles.xml | 22 + .../activities/SampleActivityBase.java | 52 - .../logger/Log.java | 236 ----- .../logger/LogFragment.java | 109 -- .../logger/LogNode.java | 39 - .../logger/LogView.java | 145 --- .../logger/LogWrapper.java | 75 -- .../logger/MessageOnlyLogFilter.java | 60 -- .../Constants.java | 2 +- .../JsonUtils.java | 2 +- .../MainActivity.java | 28 +- .../Quiz/Shared/AndroidManifest.xml | 25 - .../Quiz/Shared/res/values/strings.xml | 18 - .../Quiz/Wearable/AndroidManifest.xml | 2 +- .../Constants.java | 2 +- .../DeleteQuestionService.java | 6 +- .../QuizListenerService.java | 26 +- .../QuizReportActionService.java | 8 +- .../UpdateQuestionService.java | 8 +- .../Application/AndroidManifest.xml | 2 +- .../res/values-v21/template-styles.xml | 22 + .../AssetUtils.java | 2 +- .../Constants.java | 4 +- .../MainActivity.java | 2 +- .../Recipe.java | 2 +- .../RecipeActivity.java | 2 +- .../RecipeListAdapter.java | 2 +- .../RecipeService.java | 2 +- .../Shared/AndroidManifest.xml | 2 +- .../RenderScriptIntrinsic/AndroidManifest.xml | 4 +- .../RenderScriptIntrinsic/_index.jd | 16 +- .../res/values-v21/template-styles.xml | 22 + .../res/values/base-strings.xml | 3 - .../RepeatingAlarm/AndroidManifest.xml | 2 +- samples/browseable/RepeatingAlarm/_index.jd | 13 +- .../res/values-v21/template-styles.xml | 22 + .../res/values/base-strings.xml | 3 - .../RepeatingAlarm/res/values/strings.xml | 3 - .../MainActivity.java | 3 - .../Application/AndroidManifest.xml | 2 +- .../res/values-v21/template-styles.xml | 22 + .../activities/SampleActivityBase.java | 52 - .../logger/Log.java | 236 ----- .../logger/LogFragment.java | 109 -- .../logger/LogNode.java | 39 - .../logger/LogView.java | 145 --- .../logger/LogWrapper.java | 75 -- .../logger/MessageOnlyLogFilter.java | 60 -- .../Shared/AndroidManifest.xml | 25 - .../Shared/res/values/strings.xml | 18 - .../Wearable/AndroidManifest.xml | 4 +- .../GridExampleActivity.java | 2 +- .../MainActivity.java | 2 +- .../SlidingTabsBasic/AndroidManifest.xml | 2 +- samples/browseable/SlidingTabsBasic/_index.jd | 14 +- .../res/values-v21/template-styles.xml | 22 + .../res/values/base-strings.xml | 3 - .../MainActivity.java | 13 +- .../SlidingTabsColors/AndroidManifest.xml | 2 +- .../browseable/SlidingTabsColors/_index.jd | 14 +- .../res/values-v21/template-styles.xml | 22 + .../res/values/base-strings.xml | 3 - .../SlidingTabsColors/res/values/strings.xml | 0 .../MainActivity.java | 13 +- .../res/values-v21/template-styles.xml | 22 + .../activities/SampleActivityBase.java | 52 - .../logger/Log.java | 236 ----- .../logger/LogFragment.java | 109 -- .../logger/LogNode.java | 39 - .../logger/LogView.java | 145 --- .../logger/LogWrapper.java | 75 -- .../logger/MessageOnlyLogFilter.java | 60 -- .../StorageClient/AndroidManifest.xml | 2 +- samples/browseable/StorageClient/_index.jd | 16 +- .../res/values-v21/template-styles.xml | 22 + .../StorageClient/res/values/base-strings.xml | 3 - .../StorageClient/res/values/strings.xml | 3 - .../MainActivity.java | 3 - .../StorageProvider/AndroidManifest.xml | 4 +- samples/browseable/StorageProvider/_index.jd | 11 +- .../res/values-v21/template-styles.xml | 22 + .../res/values/base-strings.xml | 3 - .../StorageProvider/res/values/strings.xml | 3 - .../MainActivity.java | 3 - .../AndroidManifest.xml | 2 +- .../SwipeRefreshLayoutBasic/_index.jd | 14 +- .../res/values-v21/template-styles.xml | 22 + .../res/values/base-strings.xml | 3 - .../res/values/strings.xml | 0 .../MainActivity.java | 13 +- .../AndroidManifest.xml | 2 +- .../SwipeRefreshListFragment/_index.jd | 16 +- .../res/values-v21/template-styles.xml | 22 + .../res/values/base-strings.xml | 3 - .../res/values/strings.xml | 0 .../MainActivity.java | 13 +- .../AndroidManifest.xml | 2 +- .../SwipeRefreshMultipleViews/_index.jd | 16 +- .../res/values-v21/template-styles.xml | 22 + .../res/values/base-strings.xml | 3 - .../res/values/strings.xml | 0 .../MainActivity.java | 13 +- .../Application/AndroidManifest.xml | 4 +- .../res/values-v21/template-styles.xml | 22 + .../DismissListener.java | 4 +- .../MainActivity.java | 2 +- .../SynchronizedNotificationsFragment.java | 4 +- .../Shared/AndroidManifest.xml | 2 +- .../Constants.java | 4 +- .../Wearable/AndroidManifest.xml | 4 +- .../Wearable/res/values/strings.xml | 2 +- .../NotificationUpdateService.java | 4 +- .../WearableActivity.java | 2 +- .../TextLinkify/AndroidManifest.xml | 4 +- samples/browseable/TextLinkify/_index.jd | 18 +- .../res/values-v21/template-styles.xml | 22 + .../TextLinkify/res/values/base-strings.xml | 3 - .../TextSwitcher/AndroidManifest.xml | 4 +- samples/browseable/TextSwitcher/_index.jd | 14 +- .../res/values-v21/template-styles.xml | 22 + .../TextSwitcher/res/values/base-strings.xml | 3 - .../Timer/Application/AndroidManifest.xml | 2 +- .../res/values-v21/template-styles.xml | 22 + .../activities/SampleActivityBase.java | 52 - .../logger/Log.java | 236 ----- .../logger/LogFragment.java | 109 -- .../logger/LogNode.java | 39 - .../logger/LogView.java | 145 --- .../logger/LogWrapper.java | 75 -- .../logger/MessageOnlyLogFilter.java | 60 -- .../Timer/Shared/AndroidManifest.xml | 25 - .../Timer/Shared/res/values/strings.xml | 18 - .../Timer/Wearable/AndroidManifest.xml | 2 +- .../SetTimerActivity.java | 6 +- .../TimerNotificationService.java | 4 +- .../WearableListItemLayout.java | 2 +- .../util/Constants.java | 2 +- .../util/TimerFormat.java | 2 +- .../util/TimerObj.java | 2 +- .../Application/AndroidManifest.xml | 2 +- .../res/values-v21/template-styles.xml | 22 + .../activities/SampleActivityBase.java | 52 - .../logger/Log.java | 236 ----- .../logger/LogFragment.java | 109 -- .../logger/LogNode.java | 39 - .../logger/LogView.java | 145 --- .../logger/LogWrapper.java | 75 -- .../logger/MessageOnlyLogFilter.java | 60 -- .../WatchViewStub/Shared/AndroidManifest.xml | 25 - .../Shared/res/values/strings.xml | 18 - .../Wearable/AndroidManifest.xml | 4 +- .../MainActivity.java | 2 +- 684 files changed, 10100 insertions(+), 10207 deletions(-) create mode 100644 samples/browseable/ActionBarCompat-Basic/res/values-v21/template-styles.xml create mode 100644 samples/browseable/ActionBarCompat-ListPopupMenu/res/values-v21/template-styles.xml create mode 100644 samples/browseable/ActionBarCompat-ShareActionProvider/res/values-v21/template-styles.xml create mode 100644 samples/browseable/ActionBarCompat-Styled/res/values-v21/template-styles.xml create mode 100644 samples/browseable/ActivityInstrumentation/res/values-v21/template-styles.xml create mode 100644 samples/browseable/AdapterTransition/res/values-v21/template-styles.xml mode change 100755 => 100644 samples/browseable/AdapterTransition/res/values/strings.xml create mode 100644 samples/browseable/AdvancedImmersiveMode/res/values-v21/template-styles.xml create mode 100644 samples/browseable/AgendaData/Application/res/values-v21/template-styles.xml rename samples/browseable/AgendaData/Application/src/{com.example.android.agendadata => com.example.android.wearable.agendadata}/CalendarQueryService.java (91%) rename samples/browseable/AgendaData/Application/src/{com.example.android.agendadata => com.example.android.wearable.agendadata}/Constants.java (96%) rename samples/browseable/AgendaData/Application/src/{com.example.android.agendadata => com.example.android.wearable.agendadata}/MainActivity.java (98%) delete mode 100644 samples/browseable/AgendaData/Shared/AndroidManifest.xml rename samples/browseable/AgendaData/Wearable/src/{com.example.android.agendadata => com.example.android.wearable.agendadata}/Constants.java (95%) rename samples/browseable/AgendaData/Wearable/src/{com.example.android.agendadata => com.example.android.wearable.agendadata}/DeleteService.java (95%) rename samples/browseable/AgendaData/Wearable/src/{com.example.android.agendadata => com.example.android.wearable.agendadata}/HomeListenerService.java (91%) create mode 100644 samples/browseable/AppRestrictions/res/values-v21/template-styles.xml create mode 100644 samples/browseable/BasicAccessibility/res/values-v21/template-styles.xml create mode 100644 samples/browseable/BasicAndroidKeyStore/res/values-v21/template-styles.xml create mode 100644 samples/browseable/BasicContactables/res/values-v21/template-styles.xml create mode 100644 samples/browseable/BasicGestureDetect/res/values-v21/template-styles.xml create mode 100644 samples/browseable/BasicImmersiveMode/res/values-v21/template-styles.xml create mode 100644 samples/browseable/BasicMediaDecoder/res/values-v21/template-styles.xml create mode 100644 samples/browseable/BasicMediaRouter/res/values-v21/template-styles.xml create mode 100644 samples/browseable/BasicMultitouch/res/values-v21/template-styles.xml create mode 100644 samples/browseable/BasicNetworking/res/values-v21/template-styles.xml create mode 100644 samples/browseable/BasicNotifications/res/values-v21/template-styles.xml create mode 100644 samples/browseable/BasicRenderScript/res/values-v21/template-styles.xml create mode 100644 samples/browseable/BasicSyncAdapter/res/values-v21/template-styles.xml create mode 100644 samples/browseable/BasicTransition/res/values-v21/template-styles.xml mode change 100755 => 100644 samples/browseable/BasicTransition/res/values/strings.xml create mode 100644 samples/browseable/BatchStepSensor/res/drawable-v21/card_action_bg.xml create mode 100644 samples/browseable/BatchStepSensor/res/drawable-v21/card_action_bg_negative.xml create mode 100644 samples/browseable/BatchStepSensor/res/drawable-v21/card_action_bg_positive.xml create mode 100644 samples/browseable/BatchStepSensor/res/values-v21/template-styles.xml create mode 100644 samples/browseable/BluetoothChat/AndroidManifest.xml create mode 100644 samples/browseable/BluetoothChat/_index.jd create mode 100755 samples/browseable/BluetoothChat/res/drawable-hdpi/ic_action_device_access_bluetooth_searching.png create mode 100644 samples/browseable/BluetoothChat/res/drawable-hdpi/ic_launcher.png rename samples/browseable/{DoneBar/res/drawable-xhdpi/sample_dashboard_item_background.9.png => BluetoothChat/res/drawable-hdpi/tile.9.png} (100%) create mode 100755 samples/browseable/BluetoothChat/res/drawable-mdpi/ic_action_device_access_bluetooth_searching.png create mode 100644 samples/browseable/BluetoothChat/res/drawable-mdpi/ic_launcher.png create mode 100755 samples/browseable/BluetoothChat/res/drawable-xhdpi/ic_action_device_access_bluetooth_searching.png create mode 100644 samples/browseable/BluetoothChat/res/drawable-xhdpi/ic_launcher.png create mode 100755 samples/browseable/BluetoothChat/res/drawable-xxhdpi/ic_action_device_access_bluetooth_searching.png create mode 100644 samples/browseable/BluetoothChat/res/drawable-xxhdpi/ic_launcher.png create mode 100755 samples/browseable/BluetoothChat/res/layout-w720dp/activity_main.xml create mode 100644 samples/browseable/BluetoothChat/res/layout/activity_device_list.xml create mode 100755 samples/browseable/BluetoothChat/res/layout/activity_main.xml rename samples/browseable/{EmbeddedApp/Shared/res/values/strings.xml => BluetoothChat/res/layout/device_name.xml} (74%) create mode 100644 samples/browseable/BluetoothChat/res/layout/fragment_bluetooth_chat.xml rename samples/browseable/{AgendaData/Shared/res/values/strings.xml => BluetoothChat/res/layout/message.xml} (74%) create mode 100644 samples/browseable/BluetoothChat/res/menu/bluetooth_chat.xml create mode 100644 samples/browseable/BluetoothChat/res/menu/main.xml create mode 100644 samples/browseable/BluetoothChat/res/values-sw600dp/template-dimens.xml create mode 100644 samples/browseable/BluetoothChat/res/values-sw600dp/template-styles.xml rename samples/browseable/{SlidingTabsBasic/res/values/strings.xml => BluetoothChat/res/values-v11/template-styles.xml} (84%) mode change 100755 => 100644 create mode 100644 samples/browseable/BluetoothChat/res/values-v21/template-styles.xml create mode 100644 samples/browseable/BluetoothChat/res/values/base-strings.xml rename samples/browseable/{AdvancedImmersiveMode/res/values/strings.xml => BluetoothChat/res/values/fragmentview_strings.xml} (100%) create mode 100644 samples/browseable/BluetoothChat/res/values/strings.xml create mode 100644 samples/browseable/BluetoothChat/res/values/template-dimens.xml create mode 100644 samples/browseable/BluetoothChat/res/values/template-styles.xml create mode 100644 samples/browseable/BluetoothChat/src/com.example.android.bluetoothchat/BluetoothChatFragment.java create mode 100644 samples/browseable/BluetoothChat/src/com.example.android.bluetoothchat/BluetoothChatService.java create mode 100644 samples/browseable/BluetoothChat/src/com.example.android.bluetoothchat/Constants.java create mode 100644 samples/browseable/BluetoothChat/src/com.example.android.bluetoothchat/DeviceListActivity.java create mode 100644 samples/browseable/BluetoothChat/src/com.example.android.bluetoothchat/MainActivity.java rename samples/browseable/{AgendaData/Application => BluetoothChat}/src/com.example.android.common/activities/SampleActivityBase.java (100%) rename samples/browseable/{AgendaData/Application => BluetoothChat}/src/com.example.android.common/logger/Log.java (100%) rename samples/browseable/{AgendaData/Application => BluetoothChat}/src/com.example.android.common/logger/LogFragment.java (100%) rename samples/browseable/{AgendaData/Application => BluetoothChat}/src/com.example.android.common/logger/LogNode.java (100%) rename samples/browseable/{AgendaData/Application => BluetoothChat}/src/com.example.android.common/logger/LogView.java (100%) rename samples/browseable/{AgendaData/Application => BluetoothChat}/src/com.example.android.common/logger/LogWrapper.java (100%) rename samples/browseable/{AgendaData/Application => BluetoothChat}/src/com.example.android.common/logger/MessageOnlyLogFilter.java (100%) create mode 100644 samples/browseable/BluetoothLeGatt/res/values-v21/template-styles.xml create mode 100644 samples/browseable/BorderlessButtons/res/values-v21/template-styles.xml create mode 100644 samples/browseable/CardEmulation/res/values-v21/template-styles.xml mode change 100755 => 100644 samples/browseable/CardEmulation/res/values/strings.xml create mode 100644 samples/browseable/CardReader/res/values-v21/template-styles.xml mode change 100755 => 100644 samples/browseable/CardReader/res/values/strings.xml create mode 100644 samples/browseable/CustomChoiceList/res/values-v21/template-styles.xml create mode 100644 samples/browseable/CustomNotifications/res/values-v21/template-styles.xml create mode 100644 samples/browseable/CustomTransition/res/values-v21/template-styles.xml create mode 100644 samples/browseable/DataLayer/Application/res/values-v21/template-styles.xml rename samples/browseable/DataLayer/Application/src/{com.example.android.datalayer => com.example.android.wearable.datalayer}/MainActivity.java (99%) rename samples/browseable/DataLayer/Wearable/src/{com.example.android.datalayer => com.example.android.wearable.datalayer}/DataLayerListenerService.java (98%) rename samples/browseable/DataLayer/Wearable/src/{com.example.android.datalayer => com.example.android.wearable.datalayer}/MainActivity.java (98%) create mode 100644 samples/browseable/DelayedConfirmation/Application/res/values-v21/template-styles.xml delete mode 100644 samples/browseable/DelayedConfirmation/Application/src/com.example.android.common/activities/SampleActivityBase.java delete mode 100644 samples/browseable/DelayedConfirmation/Application/src/com.example.android.common/logger/Log.java delete mode 100644 samples/browseable/DelayedConfirmation/Application/src/com.example.android.common/logger/LogFragment.java delete mode 100644 samples/browseable/DelayedConfirmation/Application/src/com.example.android.common/logger/LogNode.java delete mode 100644 samples/browseable/DelayedConfirmation/Application/src/com.example.android.common/logger/LogView.java delete mode 100644 samples/browseable/DelayedConfirmation/Application/src/com.example.android.common/logger/LogWrapper.java delete mode 100644 samples/browseable/DelayedConfirmation/Application/src/com.example.android.common/logger/MessageOnlyLogFilter.java rename samples/browseable/DelayedConfirmation/Application/src/{com.example.android.delayedconfirmation => com.example.android.wearable.delayedconfirmation}/MainActivity.java (98%) rename samples/browseable/DelayedConfirmation/Wearable/src/{com.example.android.delayedconfirmation => com.example.android.wearable.delayedconfirmation}/MainActivity.java (98%) rename samples/browseable/DelayedConfirmation/Wearable/src/{com.example.android.delayedconfirmation => com.example.android.wearable.delayedconfirmation}/WearableMessageListenerService.java (95%) create mode 100644 samples/browseable/DisplayingBitmaps/res/values-v21/template-styles.xml create mode 100644 samples/browseable/DoneBar/res/values-v21/template-styles.xml create mode 100644 samples/browseable/DoneBar/res/values/activitycards-colors.xml create mode 100644 samples/browseable/DoneBar/res/values/activitycards-dimens.xml rename samples/browseable/{DataLayer/Shared/res/values/strings.xml => DoneBar/res/values/template-attrs.xml} (92%) create mode 100644 samples/browseable/ElizaChat/Application/res/values-v21/template-styles.xml delete mode 100644 samples/browseable/ElizaChat/Application/src/com.example.android.common/activities/SampleActivityBase.java delete mode 100644 samples/browseable/ElizaChat/Application/src/com.example.android.common/logger/Log.java delete mode 100644 samples/browseable/ElizaChat/Application/src/com.example.android.common/logger/LogFragment.java delete mode 100644 samples/browseable/ElizaChat/Application/src/com.example.android.common/logger/LogNode.java delete mode 100644 samples/browseable/ElizaChat/Application/src/com.example.android.common/logger/LogView.java delete mode 100644 samples/browseable/ElizaChat/Application/src/com.example.android.common/logger/LogWrapper.java delete mode 100644 samples/browseable/ElizaChat/Application/src/com.example.android.common/logger/MessageOnlyLogFilter.java rename samples/browseable/ElizaChat/Application/src/{com.example.android.elizachat => com.example.android.wearable.elizachat}/ElizaResponder.java (99%) rename samples/browseable/ElizaChat/Application/src/{com.example.android.elizachat => com.example.android.wearable.elizachat}/MainActivity.java (95%) rename samples/browseable/ElizaChat/Application/src/{com.example.android.elizachat => com.example.android.wearable.elizachat}/ResponderService.java (97%) create mode 100644 samples/browseable/EmbeddedApp/Application/res/values-v21/template-styles.xml delete mode 100644 samples/browseable/EmbeddedApp/Application/src/com.example.android.common/activities/SampleActivityBase.java delete mode 100644 samples/browseable/EmbeddedApp/Application/src/com.example.android.common/logger/Log.java delete mode 100644 samples/browseable/EmbeddedApp/Application/src/com.example.android.common/logger/LogFragment.java delete mode 100644 samples/browseable/EmbeddedApp/Application/src/com.example.android.common/logger/LogNode.java delete mode 100644 samples/browseable/EmbeddedApp/Application/src/com.example.android.common/logger/LogView.java delete mode 100644 samples/browseable/EmbeddedApp/Application/src/com.example.android.common/logger/LogWrapper.java delete mode 100644 samples/browseable/EmbeddedApp/Application/src/com.example.android.common/logger/MessageOnlyLogFilter.java rename samples/browseable/EmbeddedApp/Application/src/{com.example.android.embeddedapp => com.example.android.wearable.embeddedapp}/PhoneActivity.java (94%) delete mode 100644 samples/browseable/EmbeddedApp/Shared/AndroidManifest.xml rename samples/browseable/EmbeddedApp/Wearable/src/{com.example.android.embeddedapp => com.example.android.wearable.embeddedapp}/WearableActivity.java (94%) create mode 100644 samples/browseable/FindMyPhone/Application/res/values-v21/template-styles.xml delete mode 100644 samples/browseable/FindMyPhone/Application/src/com.example.android.common/activities/SampleActivityBase.java delete mode 100644 samples/browseable/FindMyPhone/Application/src/com.example.android.common/logger/Log.java delete mode 100644 samples/browseable/FindMyPhone/Application/src/com.example.android.common/logger/LogFragment.java delete mode 100644 samples/browseable/FindMyPhone/Application/src/com.example.android.common/logger/LogNode.java delete mode 100644 samples/browseable/FindMyPhone/Application/src/com.example.android.common/logger/LogView.java delete mode 100644 samples/browseable/FindMyPhone/Application/src/com.example.android.common/logger/LogWrapper.java delete mode 100644 samples/browseable/FindMyPhone/Application/src/com.example.android.common/logger/MessageOnlyLogFilter.java rename samples/browseable/FindMyPhone/Application/src/{com.example.android.findphone => com.example.android.wearable.findphone}/SoundAlarmListenerService.java (98%) delete mode 100644 samples/browseable/FindMyPhone/Shared/res/values/strings.xml rename samples/browseable/FindMyPhone/Wearable/src/{com.example.android.findphone => com.example.android.wearable.findphone}/DisconnectListenerService.java (97%) rename samples/browseable/FindMyPhone/Wearable/src/{com.example.android.findphone => com.example.android.wearable.findphone}/FindPhoneActivity.java (98%) rename samples/browseable/FindMyPhone/Wearable/src/{com.example.android.findphone => com.example.android.wearable.findphone}/FindPhoneService.java (99%) create mode 100644 samples/browseable/Flashlight/Application/res/values-v21/template-styles.xml delete mode 100644 samples/browseable/Flashlight/Shared/AndroidManifest.xml delete mode 100644 samples/browseable/Flashlight/Shared/res/values/strings.xml rename samples/browseable/Flashlight/Wearable/src/{com.example.android.flashlight => com.example.android.wearable.flashlight}/MainActivity.java (98%) rename samples/browseable/Flashlight/Wearable/src/{com.example.android.flashlight => com.example.android.wearable.flashlight}/PartyLightView.java (98%) create mode 100644 samples/browseable/FragmentTransition/res/values-v21/template-styles.xml mode change 100755 => 100644 samples/browseable/FragmentTransition/res/values/strings.xml create mode 100644 samples/browseable/Geofencing/Application/res/values-v21/template-styles.xml delete mode 100644 samples/browseable/Geofencing/Application/src/com.example.android.common/activities/SampleActivityBase.java delete mode 100644 samples/browseable/Geofencing/Application/src/com.example.android.common/logger/Log.java delete mode 100644 samples/browseable/Geofencing/Application/src/com.example.android.common/logger/LogFragment.java delete mode 100644 samples/browseable/Geofencing/Application/src/com.example.android.common/logger/LogNode.java delete mode 100644 samples/browseable/Geofencing/Application/src/com.example.android.common/logger/LogView.java delete mode 100644 samples/browseable/Geofencing/Application/src/com.example.android.common/logger/LogWrapper.java delete mode 100644 samples/browseable/Geofencing/Application/src/com.example.android.common/logger/MessageOnlyLogFilter.java rename samples/browseable/Geofencing/Application/src/{com.example.android.geofencing => com.example.android.wearable.geofencing}/Constants.java (98%) rename samples/browseable/Geofencing/Application/src/{com.example.android.geofencing => com.example.android.wearable.geofencing}/GeofenceTransitionsIntentService.java (91%) rename samples/browseable/Geofencing/Application/src/{com.example.android.geofencing => com.example.android.wearable.geofencing}/MainActivity.java (89%) rename samples/browseable/Geofencing/Application/src/{com.example.android.geofencing => com.example.android.wearable.geofencing}/SimpleGeofence.java (98%) rename samples/browseable/Geofencing/Application/src/{com.example.android.geofencing => com.example.android.wearable.geofencing}/SimpleGeofenceStore.java (86%) delete mode 100644 samples/browseable/Geofencing/Shared/AndroidManifest.xml delete mode 100644 samples/browseable/Geofencing/Shared/res/values/strings.xml rename samples/browseable/Geofencing/Wearable/src/{com.example.android.geofencing => com.example.android.wearable.geofencing}/CheckInAndDeleteDataItemsService.java (91%) rename samples/browseable/Geofencing/Wearable/src/{com.example.android.geofencing => com.example.android.wearable.geofencing}/Constants.java (96%) rename samples/browseable/Geofencing/Wearable/src/{com.example.android.geofencing => com.example.android.wearable.geofencing}/HomeListenerService.java (91%) create mode 100644 samples/browseable/GridViewPager/Application/res/values-v21/template-styles.xml delete mode 100644 samples/browseable/GridViewPager/Application/src/com.example.android.common/activities/SampleActivityBase.java delete mode 100644 samples/browseable/GridViewPager/Application/src/com.example.android.common/logger/Log.java delete mode 100644 samples/browseable/GridViewPager/Application/src/com.example.android.common/logger/LogFragment.java delete mode 100644 samples/browseable/GridViewPager/Application/src/com.example.android.common/logger/LogNode.java delete mode 100644 samples/browseable/GridViewPager/Application/src/com.example.android.common/logger/LogView.java delete mode 100644 samples/browseable/GridViewPager/Application/src/com.example.android.common/logger/LogWrapper.java delete mode 100644 samples/browseable/GridViewPager/Application/src/com.example.android.common/logger/MessageOnlyLogFilter.java delete mode 100644 samples/browseable/GridViewPager/Shared/AndroidManifest.xml delete mode 100644 samples/browseable/GridViewPager/Shared/res/values/strings.xml rename samples/browseable/GridViewPager/Wearable/src/{com.example.android.gridviewpager => com.example.android.wearable.gridviewpager}/MainActivity.java (97%) rename samples/browseable/GridViewPager/Wearable/src/{com.example.android.gridviewpager => com.example.android.wearable.gridviewpager}/SampleGridPagerAdapter.java (98%) create mode 100644 samples/browseable/HorizontalPaging/res/values-v21/template-styles.xml create mode 100644 samples/browseable/ImmersiveMode/res/values-v21/template-styles.xml create mode 100644 samples/browseable/JumpingJack/Application/res/values-v21/template-styles.xml delete mode 100644 samples/browseable/JumpingJack/Shared/AndroidManifest.xml delete mode 100644 samples/browseable/JumpingJack/Shared/res/values/strings.xml rename samples/browseable/JumpingJack/Wearable/src/{com.example.android.jumpingjack => com.example.android.wearable.jumpingjack}/MainActivity.java (97%) rename samples/browseable/JumpingJack/Wearable/src/{com.example.android.jumpingjack => com.example.android.wearable.jumpingjack}/PagerAdapter.java (96%) rename samples/browseable/JumpingJack/Wearable/src/{com.example.android.jumpingjack => com.example.android.wearable.jumpingjack}/Utils.java (97%) rename samples/browseable/JumpingJack/Wearable/src/{com.example.android.jumpingjack => com.example.android.wearable.jumpingjack}/fragments/CounterFragment.java (94%) rename samples/browseable/JumpingJack/Wearable/src/{com.example.android.jumpingjack => com.example.android.wearable.jumpingjack}/fragments/SettingsFragment.java (90%) create mode 100644 samples/browseable/MediaBrowserService/AndroidManifest.xml create mode 100644 samples/browseable/MediaBrowserService/_index.jd create mode 100644 samples/browseable/MediaBrowserService/res/drawable-hdpi/ic_launcher.png create mode 100644 samples/browseable/MediaBrowserService/res/drawable-hdpi/ic_notification.png create mode 100644 samples/browseable/MediaBrowserService/res/drawable-hdpi/ic_pause_white_24dp.png create mode 100644 samples/browseable/MediaBrowserService/res/drawable-hdpi/ic_play_arrow_white_24dp.png create mode 100644 samples/browseable/MediaBrowserService/res/drawable-hdpi/ic_shuffle_white_24dp.png create mode 100644 samples/browseable/MediaBrowserService/res/drawable-hdpi/ic_skip_next_white_24dp.png create mode 100644 samples/browseable/MediaBrowserService/res/drawable-hdpi/ic_skip_previous_white_24dp.png create mode 100644 samples/browseable/MediaBrowserService/res/drawable-mdpi/ic_launcher.png create mode 100644 samples/browseable/MediaBrowserService/res/drawable-night-xxhdpi/ic_star_off.png create mode 100644 samples/browseable/MediaBrowserService/res/drawable-night-xxhdpi/ic_star_on.png create mode 100644 samples/browseable/MediaBrowserService/res/drawable-xhdpi/ic_equalizer_white_24dp.png create mode 100644 samples/browseable/MediaBrowserService/res/drawable-xhdpi/ic_launcher.png create mode 100644 samples/browseable/MediaBrowserService/res/drawable-xhdpi/ic_pause_white_24dp.png create mode 100644 samples/browseable/MediaBrowserService/res/drawable-xhdpi/ic_play_arrow_white_24dp.png create mode 100644 samples/browseable/MediaBrowserService/res/drawable-xhdpi/ic_shuffle_white_24dp.png create mode 100644 samples/browseable/MediaBrowserService/res/drawable-xhdpi/ic_skip_next_white_24dp.png create mode 100644 samples/browseable/MediaBrowserService/res/drawable-xhdpi/ic_skip_previous_white_24dp.png create mode 100644 samples/browseable/MediaBrowserService/res/drawable-xxhdpi/ic_by_genre.png create mode 100644 samples/browseable/MediaBrowserService/res/drawable-xxhdpi/ic_default_art.png create mode 100644 samples/browseable/MediaBrowserService/res/drawable-xxhdpi/ic_equalizer_white_24dp.png create mode 100644 samples/browseable/MediaBrowserService/res/drawable-xxhdpi/ic_launcher.png create mode 100644 samples/browseable/MediaBrowserService/res/drawable-xxhdpi/ic_pause_white_24dp.png create mode 100644 samples/browseable/MediaBrowserService/res/drawable-xxhdpi/ic_play_arrow_white_24dp.png create mode 100644 samples/browseable/MediaBrowserService/res/drawable-xxhdpi/ic_shuffle_white_24dp.png create mode 100644 samples/browseable/MediaBrowserService/res/drawable-xxhdpi/ic_skip_next_white_24dp.png create mode 100644 samples/browseable/MediaBrowserService/res/drawable-xxhdpi/ic_skip_previous_white_24dp.png create mode 100644 samples/browseable/MediaBrowserService/res/drawable-xxhdpi/ic_star_off.png create mode 100644 samples/browseable/MediaBrowserService/res/drawable-xxhdpi/ic_star_on.png create mode 100644 samples/browseable/MediaBrowserService/res/layout/activity_player.xml create mode 100644 samples/browseable/MediaBrowserService/res/layout/fragment_list.xml create mode 100644 samples/browseable/MediaBrowserService/res/layout/media_list_item.xml create mode 100644 samples/browseable/MediaBrowserService/res/values-v21/styles.xml create mode 100644 samples/browseable/MediaBrowserService/res/values/dimens.xml create mode 100644 samples/browseable/MediaBrowserService/res/values/strings.xml create mode 100644 samples/browseable/MediaBrowserService/res/values/strings_notifications.xml create mode 100644 samples/browseable/MediaBrowserService/res/values/styles.xml create mode 100644 samples/browseable/MediaBrowserService/res/xml/automotive_app_desc.xml create mode 100644 samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/BrowseFragment.java create mode 100644 samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/MediaNotification.java create mode 100644 samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/MusicPlayerActivity.java create mode 100644 samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/MusicService.java create mode 100644 samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/QueueAdapter.java create mode 100644 samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/QueueFragment.java create mode 100644 samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/model/MusicProvider.java create mode 100644 samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/utils/BitmapHelper.java create mode 100644 samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/utils/LogHelper.java create mode 100644 samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/utils/MediaIDHelper.java create mode 100644 samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/utils/QueueHelper.java create mode 100644 samples/browseable/MediaEffects/AndroidManifest.xml create mode 100644 samples/browseable/MediaEffects/_index.jd create mode 100644 samples/browseable/MediaEffects/res/drawable-hdpi/ic_launcher.png rename samples/browseable/{NavigationDrawer/res/drawable-xhdpi/sample_dashboard_item_background.9.png => MediaEffects/res/drawable-hdpi/tile.9.png} (100%) create mode 100644 samples/browseable/MediaEffects/res/drawable-mdpi/ic_launcher.png create mode 100644 samples/browseable/MediaEffects/res/drawable-nodpi/puppy.jpg create mode 100644 samples/browseable/MediaEffects/res/drawable-xhdpi/ic_launcher.png create mode 100644 samples/browseable/MediaEffects/res/drawable-xxhdpi/ic_launcher.png create mode 100755 samples/browseable/MediaEffects/res/layout-w720dp/activity_main.xml create mode 100755 samples/browseable/MediaEffects/res/layout/activity_main.xml create mode 100644 samples/browseable/MediaEffects/res/layout/fragment_media_effects.xml create mode 100644 samples/browseable/MediaEffects/res/menu/main.xml create mode 100644 samples/browseable/MediaEffects/res/menu/media_effects.xml create mode 100644 samples/browseable/MediaEffects/res/values-sw600dp/template-dimens.xml create mode 100644 samples/browseable/MediaEffects/res/values-sw600dp/template-styles.xml create mode 100644 samples/browseable/MediaEffects/res/values-v11/template-styles.xml create mode 100644 samples/browseable/MediaEffects/res/values-v21/template-styles.xml rename samples/browseable/{DelayedConfirmation/Shared/AndroidManifest.xml => MediaEffects/res/values/base-strings.xml} (54%) rename samples/browseable/{CustomTransition/res/values/strings.xml => MediaEffects/res/values/fragmentview_strings.xml} (100%) create mode 100644 samples/browseable/MediaEffects/res/values/strings.xml create mode 100644 samples/browseable/MediaEffects/res/values/template-dimens.xml create mode 100644 samples/browseable/MediaEffects/res/values/template-styles.xml rename samples/browseable/{DataLayer/Application => MediaEffects}/src/com.example.android.common/activities/SampleActivityBase.java (100%) rename samples/browseable/{DataLayer/Application => MediaEffects}/src/com.example.android.common/logger/Log.java (100%) rename samples/browseable/{DataLayer/Application => MediaEffects}/src/com.example.android.common/logger/LogFragment.java (100%) rename samples/browseable/{DataLayer/Application => MediaEffects}/src/com.example.android.common/logger/LogNode.java (100%) rename samples/browseable/{DataLayer/Application => MediaEffects}/src/com.example.android.common/logger/LogView.java (100%) rename samples/browseable/{DataLayer/Application => MediaEffects}/src/com.example.android.common/logger/LogWrapper.java (100%) rename samples/browseable/{DataLayer/Application => MediaEffects}/src/com.example.android.common/logger/MessageOnlyLogFilter.java (100%) create mode 100644 samples/browseable/MediaEffects/src/com.example.android.mediaeffects/GLToolbox.java create mode 100644 samples/browseable/MediaEffects/src/com.example.android.mediaeffects/MainActivity.java create mode 100644 samples/browseable/MediaEffects/src/com.example.android.mediaeffects/MediaEffectsFragment.java create mode 100644 samples/browseable/MediaEffects/src/com.example.android.mediaeffects/TextureRenderer.java create mode 100644 samples/browseable/MediaRecorder/res/values-v21/template-styles.xml create mode 100644 samples/browseable/MediaRouter/res/values-v21/template-styles.xml create mode 100644 samples/browseable/MessagingService/AndroidManifest.xml create mode 100644 samples/browseable/MessagingService/_index.jd create mode 100644 samples/browseable/MessagingService/res/drawable-hdpi/android_contact.png create mode 100644 samples/browseable/MessagingService/res/drawable-hdpi/ic_launcher.png create mode 100644 samples/browseable/MessagingService/res/drawable-hdpi/notification_icon.png create mode 100644 samples/browseable/MessagingService/res/drawable-mdpi/android_contact.png create mode 100644 samples/browseable/MessagingService/res/drawable-mdpi/ic_launcher.png create mode 100644 samples/browseable/MessagingService/res/drawable-mdpi/notification_icon.png create mode 100644 samples/browseable/MessagingService/res/drawable-xhdpi/android_contact.png create mode 100644 samples/browseable/MessagingService/res/drawable-xhdpi/ic_launcher.png create mode 100644 samples/browseable/MessagingService/res/drawable-xhdpi/notification_icon.png create mode 100644 samples/browseable/MessagingService/res/drawable-xxhdpi/android_contact.png create mode 100644 samples/browseable/MessagingService/res/drawable-xxhdpi/ic_launcher.png create mode 100644 samples/browseable/MessagingService/res/drawable-xxhdpi/notification_icon.png create mode 100644 samples/browseable/MessagingService/res/layout-land/fragment_message_me.xml create mode 100644 samples/browseable/MessagingService/res/layout/activity_main.xml create mode 100644 samples/browseable/MessagingService/res/layout/fragment_message_me.xml create mode 100644 samples/browseable/MessagingService/res/values-v21/styles.xml create mode 100644 samples/browseable/MessagingService/res/values/colors.xml create mode 100644 samples/browseable/MessagingService/res/values/dimens.xml create mode 100644 samples/browseable/MessagingService/res/values/strings.xml create mode 100644 samples/browseable/MessagingService/res/values/styles.xml create mode 100644 samples/browseable/MessagingService/res/xml/automotive_app_desc.xml create mode 100644 samples/browseable/MessagingService/src/com.example.android.messagingservice/Conversations.java create mode 100644 samples/browseable/MessagingService/src/com.example.android.messagingservice/MainActivity.java create mode 100644 samples/browseable/MessagingService/src/com.example.android.messagingservice/MessageLogger.java create mode 100644 samples/browseable/MessagingService/src/com.example.android.messagingservice/MessageReadReceiver.java create mode 100644 samples/browseable/MessagingService/src/com.example.android.messagingservice/MessageReplyReceiver.java create mode 100644 samples/browseable/MessagingService/src/com.example.android.messagingservice/MessagingFragment.java create mode 100644 samples/browseable/MessagingService/src/com.example.android.messagingservice/MessagingService.java rename samples/browseable/{DelayedConfirmation/Shared/res/values/strings.xml => NavigationDrawer/res/values/template-attrs.xml} (92%) create mode 100644 samples/browseable/NetworkConnect/res/values-v21/template-styles.xml create mode 100644 samples/browseable/Notifications/Application/res/values-v21/template-styles.xml delete mode 100644 samples/browseable/Notifications/Application/src/com.example.android.common/activities/SampleActivityBase.java delete mode 100644 samples/browseable/Notifications/Application/src/com.example.android.common/logger/Log.java delete mode 100644 samples/browseable/Notifications/Application/src/com.example.android.common/logger/LogFragment.java delete mode 100644 samples/browseable/Notifications/Application/src/com.example.android.common/logger/LogNode.java delete mode 100644 samples/browseable/Notifications/Application/src/com.example.android.common/logger/LogView.java delete mode 100644 samples/browseable/Notifications/Application/src/com.example.android.common/logger/LogWrapper.java delete mode 100644 samples/browseable/Notifications/Application/src/com.example.android.common/logger/MessageOnlyLogFilter.java rename samples/browseable/Notifications/Application/src/{com.example.android.notifications => com.example.android.support.wearable.notifications}/ActionsPreset.java (94%) rename samples/browseable/Notifications/Application/src/{com.example.android.notifications => com.example.android.support.wearable.notifications}/ActionsPresets.java (99%) rename samples/browseable/Notifications/Application/src/{com.example.android.notifications => com.example.android.support.wearable.notifications}/BackgroundPickers.java (98%) rename samples/browseable/Notifications/Application/src/{com.example.android.notifications => com.example.android.support.wearable.notifications}/MainActivity.java (99%) rename samples/browseable/Notifications/Application/src/{com.example.android.notifications => com.example.android.support.wearable.notifications}/NamedPreset.java (93%) rename samples/browseable/Notifications/Application/src/{com.example.android.notifications => com.example.android.support.wearable.notifications}/NotificationIntentReceiver.java (87%) rename samples/browseable/Notifications/Application/src/{com.example.android.notifications => com.example.android.support.wearable.notifications}/NotificationPreset.java (97%) rename samples/browseable/Notifications/Application/src/{com.example.android.notifications => com.example.android.support.wearable.notifications}/NotificationPresets.java (99%) rename samples/browseable/Notifications/Application/src/{com.example.android.notifications => com.example.android.support.wearable.notifications}/NotificationUtil.java (86%) rename samples/browseable/Notifications/Application/src/{com.example.android.notifications => com.example.android.support.wearable.notifications}/PriorityPreset.java (94%) rename samples/browseable/Notifications/Application/src/{com.example.android.notifications => com.example.android.support.wearable.notifications}/PriorityPresets.java (97%) delete mode 100644 samples/browseable/Notifications/Shared/AndroidManifest.xml delete mode 100644 samples/browseable/Notifications/Shared/res/values/strings.xml rename samples/browseable/Notifications/Wearable/src/{com.example.android.notifications => com.example.android.support.wearable.notifications}/AnimatedNotificationDisplayActivity.java (98%) rename samples/browseable/Notifications/Wearable/src/{com.example.android.notifications => com.example.android.support.wearable.notifications}/BasicNotificationDisplayActivity.java (95%) rename samples/browseable/Notifications/Wearable/src/{com.example.android.notifications => com.example.android.support.wearable.notifications}/MainActivity.java (98%) rename samples/browseable/Notifications/Wearable/src/{com.example.android.notifications => com.example.android.support.wearable.notifications}/NotificationPreset.java (94%) rename samples/browseable/Notifications/Wearable/src/{com.example.android.notifications => com.example.android.support.wearable.notifications}/NotificationPresets.java (99%) rename samples/browseable/Notifications/Wearable/src/{com.example.android.notifications => com.example.android.support.wearable.notifications}/WearableListItemLayout.java (97%) rename samples/browseable/{DataLayer/Shared => PdfRendererBasic}/AndroidManifest.xml (54%) create mode 100644 samples/browseable/PdfRendererBasic/_index.jd create mode 100644 samples/browseable/PdfRendererBasic/res/drawable-hdpi/ic_action_info.png create mode 100644 samples/browseable/PdfRendererBasic/res/drawable-hdpi/ic_launcher.png create mode 100644 samples/browseable/PdfRendererBasic/res/drawable-hdpi/tile.9.png create mode 100644 samples/browseable/PdfRendererBasic/res/drawable-mdpi/ic_action_info.png create mode 100644 samples/browseable/PdfRendererBasic/res/drawable-mdpi/ic_launcher.png create mode 100644 samples/browseable/PdfRendererBasic/res/drawable-xhdpi/ic_action_info.png create mode 100644 samples/browseable/PdfRendererBasic/res/drawable-xhdpi/ic_launcher.png create mode 100644 samples/browseable/PdfRendererBasic/res/drawable-xxhdpi/ic_action_info.png create mode 100644 samples/browseable/PdfRendererBasic/res/drawable-xxhdpi/ic_launcher.png create mode 100755 samples/browseable/PdfRendererBasic/res/layout/activity_main.xml create mode 100644 samples/browseable/PdfRendererBasic/res/layout/activity_main_real.xml create mode 100644 samples/browseable/PdfRendererBasic/res/layout/fragment_pdf_renderer_basic.xml create mode 100644 samples/browseable/PdfRendererBasic/res/menu/main.xml create mode 100644 samples/browseable/PdfRendererBasic/res/values-sw600dp/template-dimens.xml create mode 100644 samples/browseable/PdfRendererBasic/res/values-sw600dp/template-styles.xml create mode 100644 samples/browseable/PdfRendererBasic/res/values-v11/template-styles.xml create mode 100644 samples/browseable/PdfRendererBasic/res/values-v21/template-styles.xml rename samples/browseable/{FindMyPhone/Shared/AndroidManifest.xml => PdfRendererBasic/res/values/base-strings.xml} (68%) create mode 100644 samples/browseable/PdfRendererBasic/res/values/strings.xml create mode 100644 samples/browseable/PdfRendererBasic/res/values/template-dimens.xml create mode 100644 samples/browseable/PdfRendererBasic/res/values/template-styles.xml create mode 100644 samples/browseable/PdfRendererBasic/src/com.example.android.pdfrendererbasic/MainActivity.java create mode 100644 samples/browseable/PdfRendererBasic/src/com.example.android.pdfrendererbasic/PdfRendererBasicFragment.java create mode 100644 samples/browseable/Quiz/Application/res/values-v21/template-styles.xml delete mode 100644 samples/browseable/Quiz/Application/src/com.example.android.common/activities/SampleActivityBase.java delete mode 100644 samples/browseable/Quiz/Application/src/com.example.android.common/logger/Log.java delete mode 100644 samples/browseable/Quiz/Application/src/com.example.android.common/logger/LogFragment.java delete mode 100644 samples/browseable/Quiz/Application/src/com.example.android.common/logger/LogNode.java delete mode 100644 samples/browseable/Quiz/Application/src/com.example.android.common/logger/LogView.java delete mode 100644 samples/browseable/Quiz/Application/src/com.example.android.common/logger/LogWrapper.java delete mode 100644 samples/browseable/Quiz/Application/src/com.example.android.common/logger/MessageOnlyLogFilter.java rename samples/browseable/Quiz/Application/src/{com.example.android.quiz => com.example.android.wearable.quiz}/Constants.java (97%) rename samples/browseable/Quiz/Application/src/{com.example.android.quiz => com.example.android.wearable.quiz}/JsonUtils.java (97%) rename samples/browseable/Quiz/Application/src/{com.example.android.quiz => com.example.android.wearable.quiz}/MainActivity.java (96%) delete mode 100644 samples/browseable/Quiz/Shared/AndroidManifest.xml delete mode 100644 samples/browseable/Quiz/Shared/res/values/strings.xml rename samples/browseable/Quiz/Wearable/src/{com.example.android.quiz => com.example.android.wearable.quiz}/Constants.java (97%) rename samples/browseable/Quiz/Wearable/src/{com.example.android.quiz => com.example.android.wearable.quiz}/DeleteQuestionService.java (94%) rename samples/browseable/Quiz/Wearable/src/{com.example.android.quiz => com.example.android.wearable.quiz}/QuizListenerService.java (91%) rename samples/browseable/Quiz/Wearable/src/{com.example.android.quiz => com.example.android.wearable.quiz}/QuizReportActionService.java (91%) rename samples/browseable/Quiz/Wearable/src/{com.example.android.quiz => com.example.android.wearable.quiz}/UpdateQuestionService.java (93%) create mode 100644 samples/browseable/RecipeAssistant/Application/res/values-v21/template-styles.xml rename samples/browseable/RecipeAssistant/Application/src/{com.example.android.recipeassistant => com.example.android.wearable.recipeassistant}/AssetUtils.java (97%) rename samples/browseable/RecipeAssistant/Application/src/{com.example.android.recipeassistant => com.example.android.wearable.recipeassistant}/Constants.java (93%) rename samples/browseable/RecipeAssistant/Application/src/{com.example.android.recipeassistant => com.example.android.wearable.recipeassistant}/MainActivity.java (96%) rename samples/browseable/RecipeAssistant/Application/src/{com.example.android.recipeassistant => com.example.android.wearable.recipeassistant}/Recipe.java (98%) rename samples/browseable/RecipeAssistant/Application/src/{com.example.android.recipeassistant => com.example.android.wearable.recipeassistant}/RecipeActivity.java (98%) rename samples/browseable/RecipeAssistant/Application/src/{com.example.android.recipeassistant => com.example.android.wearable.recipeassistant}/RecipeListAdapter.java (98%) rename samples/browseable/RecipeAssistant/Application/src/{com.example.android.recipeassistant => com.example.android.wearable.recipeassistant}/RecipeService.java (98%) create mode 100644 samples/browseable/RenderScriptIntrinsic/res/values-v21/template-styles.xml create mode 100644 samples/browseable/RepeatingAlarm/res/values-v21/template-styles.xml create mode 100644 samples/browseable/SkeletonWearableApp/Application/res/values-v21/template-styles.xml delete mode 100644 samples/browseable/SkeletonWearableApp/Application/src/com.example.android.common/activities/SampleActivityBase.java delete mode 100644 samples/browseable/SkeletonWearableApp/Application/src/com.example.android.common/logger/Log.java delete mode 100644 samples/browseable/SkeletonWearableApp/Application/src/com.example.android.common/logger/LogFragment.java delete mode 100644 samples/browseable/SkeletonWearableApp/Application/src/com.example.android.common/logger/LogNode.java delete mode 100644 samples/browseable/SkeletonWearableApp/Application/src/com.example.android.common/logger/LogView.java delete mode 100644 samples/browseable/SkeletonWearableApp/Application/src/com.example.android.common/logger/LogWrapper.java delete mode 100644 samples/browseable/SkeletonWearableApp/Application/src/com.example.android.common/logger/MessageOnlyLogFilter.java delete mode 100644 samples/browseable/SkeletonWearableApp/Shared/AndroidManifest.xml delete mode 100644 samples/browseable/SkeletonWearableApp/Shared/res/values/strings.xml rename samples/browseable/SkeletonWearableApp/Wearable/src/{com.example.android.skeletonwearableapp => com.example.android.google.wearable.app}/GridExampleActivity.java (98%) rename samples/browseable/SkeletonWearableApp/Wearable/src/{com.example.android.skeletonwearableapp => com.example.android.google.wearable.app}/MainActivity.java (98%) create mode 100644 samples/browseable/SlidingTabsBasic/res/values-v21/template-styles.xml create mode 100644 samples/browseable/SlidingTabsColors/res/values-v21/template-styles.xml mode change 100755 => 100644 samples/browseable/SlidingTabsColors/res/values/strings.xml create mode 100644 samples/browseable/SpeedTracker/Application/res/values-v21/template-styles.xml delete mode 100644 samples/browseable/SpeedTracker/Application/src/com.example.android.common/activities/SampleActivityBase.java delete mode 100644 samples/browseable/SpeedTracker/Application/src/com.example.android.common/logger/Log.java delete mode 100644 samples/browseable/SpeedTracker/Application/src/com.example.android.common/logger/LogFragment.java delete mode 100644 samples/browseable/SpeedTracker/Application/src/com.example.android.common/logger/LogNode.java delete mode 100644 samples/browseable/SpeedTracker/Application/src/com.example.android.common/logger/LogView.java delete mode 100644 samples/browseable/SpeedTracker/Application/src/com.example.android.common/logger/LogWrapper.java delete mode 100644 samples/browseable/SpeedTracker/Application/src/com.example.android.common/logger/MessageOnlyLogFilter.java create mode 100644 samples/browseable/StorageClient/res/values-v21/template-styles.xml create mode 100644 samples/browseable/StorageProvider/res/values-v21/template-styles.xml create mode 100644 samples/browseable/SwipeRefreshLayoutBasic/res/values-v21/template-styles.xml mode change 100755 => 100644 samples/browseable/SwipeRefreshLayoutBasic/res/values/strings.xml create mode 100644 samples/browseable/SwipeRefreshListFragment/res/values-v21/template-styles.xml mode change 100755 => 100644 samples/browseable/SwipeRefreshListFragment/res/values/strings.xml create mode 100644 samples/browseable/SwipeRefreshMultipleViews/res/values-v21/template-styles.xml mode change 100755 => 100644 samples/browseable/SwipeRefreshMultipleViews/res/values/strings.xml create mode 100644 samples/browseable/SynchronizedNotifications/Application/res/values-v21/template-styles.xml rename samples/browseable/SynchronizedNotifications/Application/src/{com.example.android.synchronizednotifications => com.example.android.wearable.synchronizednotifications}/DismissListener.java (97%) rename samples/browseable/SynchronizedNotifications/Application/src/{com.example.android.synchronizednotifications => com.example.android.wearable.synchronizednotifications}/MainActivity.java (97%) rename samples/browseable/SynchronizedNotifications/Application/src/{com.example.android.synchronizednotifications => com.example.android.wearable.synchronizednotifications}/SynchronizedNotificationsFragment.java (98%) rename samples/browseable/SynchronizedNotifications/Shared/src/{com.example.android.synchronizednotifications.common => com.example.android.wearable.synchronizednotifications.common}/Constants.java (89%) rename samples/browseable/SynchronizedNotifications/Wearable/src/{com.example.android.synchronizednotifications => com.example.android.wearable.synchronizednotifications}/NotificationUpdateService.java (97%) rename samples/browseable/SynchronizedNotifications/Wearable/src/{com.example.android.synchronizednotifications => com.example.android.wearable.synchronizednotifications}/WearableActivity.java (93%) create mode 100644 samples/browseable/TextLinkify/res/values-v21/template-styles.xml create mode 100644 samples/browseable/TextSwitcher/res/values-v21/template-styles.xml create mode 100644 samples/browseable/Timer/Application/res/values-v21/template-styles.xml delete mode 100644 samples/browseable/Timer/Application/src/com.example.android.common/activities/SampleActivityBase.java delete mode 100644 samples/browseable/Timer/Application/src/com.example.android.common/logger/Log.java delete mode 100644 samples/browseable/Timer/Application/src/com.example.android.common/logger/LogFragment.java delete mode 100644 samples/browseable/Timer/Application/src/com.example.android.common/logger/LogNode.java delete mode 100644 samples/browseable/Timer/Application/src/com.example.android.common/logger/LogView.java delete mode 100644 samples/browseable/Timer/Application/src/com.example.android.common/logger/LogWrapper.java delete mode 100644 samples/browseable/Timer/Application/src/com.example.android.common/logger/MessageOnlyLogFilter.java delete mode 100644 samples/browseable/Timer/Shared/AndroidManifest.xml delete mode 100644 samples/browseable/Timer/Shared/res/values/strings.xml rename samples/browseable/Timer/Wearable/src/{com.example.android.timer => com.example.android.wearable.timer}/SetTimerActivity.java (97%) rename samples/browseable/Timer/Wearable/src/{com.example.android.timer => com.example.android.wearable.timer}/TimerNotificationService.java (97%) rename samples/browseable/Timer/Wearable/src/{com.example.android.timer => com.example.android.wearable.timer}/WearableListItemLayout.java (98%) rename samples/browseable/Timer/Wearable/src/{com.example.android.timer => com.example.android.wearable.timer}/util/Constants.java (96%) rename samples/browseable/Timer/Wearable/src/{com.example.android.timer => com.example.android.wearable.timer}/util/TimerFormat.java (98%) rename samples/browseable/Timer/Wearable/src/{com.example.android.timer => com.example.android.wearable.timer}/util/TimerObj.java (96%) create mode 100644 samples/browseable/WatchViewStub/Application/res/values-v21/template-styles.xml delete mode 100644 samples/browseable/WatchViewStub/Application/src/com.example.android.common/activities/SampleActivityBase.java delete mode 100644 samples/browseable/WatchViewStub/Application/src/com.example.android.common/logger/Log.java delete mode 100644 samples/browseable/WatchViewStub/Application/src/com.example.android.common/logger/LogFragment.java delete mode 100644 samples/browseable/WatchViewStub/Application/src/com.example.android.common/logger/LogNode.java delete mode 100644 samples/browseable/WatchViewStub/Application/src/com.example.android.common/logger/LogView.java delete mode 100644 samples/browseable/WatchViewStub/Application/src/com.example.android.common/logger/LogWrapper.java delete mode 100644 samples/browseable/WatchViewStub/Application/src/com.example.android.common/logger/MessageOnlyLogFilter.java delete mode 100644 samples/browseable/WatchViewStub/Shared/AndroidManifest.xml delete mode 100644 samples/browseable/WatchViewStub/Shared/res/values/strings.xml rename samples/browseable/WatchViewStub/Wearable/src/{com.example.android.watchviewstub => com.example.android.google.wearable.watchviewstub}/MainActivity.java (98%) diff --git a/samples/browseable/ActionBarCompat-Basic/AndroidManifest.xml b/samples/browseable/ActionBarCompat-Basic/AndroidManifest.xml index 332c055be..2e19220d2 100644 --- a/samples/browseable/ActionBarCompat-Basic/AndroidManifest.xml +++ b/samples/browseable/ActionBarCompat-Basic/AndroidManifest.xml @@ -21,9 +21,7 @@ android:versionName="1.0"> - + This sample demonstrates how to create a basic action bar that displays -action items. The sample shows how to inflate items from a menu resource, and -how to add items programatically. To reduce clutter, rarely used actions are -displayed in an action bar overflow.

-

The activity in this sample extends from -{@link android.support.v7.app.ActionBarActivity}, which provides the -functionality necessary to display a compatible action bar on devices -running Android 2.1 and higher.

+

+ + This sample shows you how to use ActionBarCompat to create a basic Activity which + displays action items. It covers inflating items from a menu resource, as well as adding + an item in code. Items that are not shown as action items on the Action Bar are + displayed in the action bar overflow. + +

diff --git a/samples/browseable/ActionBarCompat-Basic/res/values-v21/template-styles.xml b/samples/browseable/ActionBarCompat-Basic/res/values-v21/template-styles.xml new file mode 100644 index 000000000..134fcd9d3 --- /dev/null +++ b/samples/browseable/ActionBarCompat-Basic/res/values-v21/template-styles.xml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/samples/browseable/SlidingTabsBasic/res/values/strings.xml b/samples/browseable/BluetoothChat/res/values-v11/template-styles.xml old mode 100755 new mode 100644 similarity index 84% rename from samples/browseable/SlidingTabsBasic/res/values/strings.xml rename to samples/browseable/BluetoothChat/res/values-v11/template-styles.xml index 7b9d9ec4f..8c1ea66f2 --- a/samples/browseable/SlidingTabsBasic/res/values/strings.xml +++ b/samples/browseable/BluetoothChat/res/values-v11/template-styles.xml @@ -12,8 +12,11 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ---> + --> + - Show Log - Hide Log + + + + + + + diff --git a/samples/browseable/BluetoothChat/src/com.example.android.bluetoothchat/BluetoothChatFragment.java b/samples/browseable/BluetoothChat/src/com.example.android.bluetoothchat/BluetoothChatFragment.java new file mode 100644 index 000000000..8ee906246 --- /dev/null +++ b/samples/browseable/BluetoothChat/src/com.example.android.bluetoothchat/BluetoothChatFragment.java @@ -0,0 +1,402 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.bluetoothchat; + +import android.app.ActionBar; +import android.app.Activity; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; + +import com.example.android.common.logger.Log; + +/** + * This fragment controls Bluetooth to communicate with other devices. + */ +public class BluetoothChatFragment extends Fragment { + + private static final String TAG = "BluetoothChatFragment"; + + // Intent request codes + private static final int REQUEST_CONNECT_DEVICE_SECURE = 1; + private static final int REQUEST_CONNECT_DEVICE_INSECURE = 2; + private static final int REQUEST_ENABLE_BT = 3; + + // Layout Views + private ListView mConversationView; + private EditText mOutEditText; + private Button mSendButton; + + /** + * Name of the connected device + */ + private String mConnectedDeviceName = null; + + /** + * Array adapter for the conversation thread + */ + private ArrayAdapter mConversationArrayAdapter; + + /** + * String buffer for outgoing messages + */ + private StringBuffer mOutStringBuffer; + + /** + * Local Bluetooth adapter + */ + private BluetoothAdapter mBluetoothAdapter = null; + + /** + * Member object for the chat services + */ + private BluetoothChatService mChatService = null; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + // Get local Bluetooth adapter + mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + + // If the adapter is null, then Bluetooth is not supported + if (mBluetoothAdapter == null) { + FragmentActivity activity = getActivity(); + Toast.makeText(activity, "Bluetooth is not available", Toast.LENGTH_LONG).show(); + activity.finish(); + } + } + + + @Override + public void onStart() { + super.onStart(); + // If BT is not on, request that it be enabled. + // setupChat() will then be called during onActivityResult + if (!mBluetoothAdapter.isEnabled()) { + Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); + startActivityForResult(enableIntent, REQUEST_ENABLE_BT); + // Otherwise, setup the chat session + } else if (mChatService == null) { + setupChat(); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (mChatService != null) { + mChatService.stop(); + } + } + + @Override + public void onResume() { + super.onResume(); + + // Performing this check in onResume() covers the case in which BT was + // not enabled during onStart(), so we were paused to enable it... + // onResume() will be called when ACTION_REQUEST_ENABLE activity returns. + if (mChatService != null) { + // Only if the state is STATE_NONE, do we know that we haven't started already + if (mChatService.getState() == BluetoothChatService.STATE_NONE) { + // Start the Bluetooth chat services + mChatService.start(); + } + } + } + + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_bluetooth_chat, container, false); + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + mConversationView = (ListView) view.findViewById(R.id.in); + mOutEditText = (EditText) view.findViewById(R.id.edit_text_out); + mSendButton = (Button) view.findViewById(R.id.button_send); + } + + /** + * Set up the UI and background operations for chat. + */ + private void setupChat() { + Log.d(TAG, "setupChat()"); + + // Initialize the array adapter for the conversation thread + mConversationArrayAdapter = new ArrayAdapter(getActivity(), R.layout.message); + + mConversationView.setAdapter(mConversationArrayAdapter); + + // Initialize the compose field with a listener for the return key + mOutEditText.setOnEditorActionListener(mWriteListener); + + // Initialize the send button with a listener that for click events + mSendButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + // Send a message using content of the edit text widget + View view = getView(); + if (null != view) { + TextView textView = (TextView) view.findViewById(R.id.edit_text_out); + String message = textView.getText().toString(); + sendMessage(message); + } + } + }); + + // Initialize the BluetoothChatService to perform bluetooth connections + mChatService = new BluetoothChatService(getActivity(), mHandler); + + // Initialize the buffer for outgoing messages + mOutStringBuffer = new StringBuffer(""); + } + + /** + * Makes this device discoverable. + */ + private void ensureDiscoverable() { + if (mBluetoothAdapter.getScanMode() != + BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) { + Intent discoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE); + discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300); + startActivity(discoverableIntent); + } + } + + /** + * Sends a message. + * + * @param message A string of text to send. + */ + private void sendMessage(String message) { + // Check that we're actually connected before trying anything + if (mChatService.getState() != BluetoothChatService.STATE_CONNECTED) { + Toast.makeText(getActivity(), R.string.not_connected, Toast.LENGTH_SHORT).show(); + return; + } + + // Check that there's actually something to send + if (message.length() > 0) { + // Get the message bytes and tell the BluetoothChatService to write + byte[] send = message.getBytes(); + mChatService.write(send); + + // Reset out string buffer to zero and clear the edit text field + mOutStringBuffer.setLength(0); + mOutEditText.setText(mOutStringBuffer); + } + } + + /** + * The action listener for the EditText widget, to listen for the return key + */ + private TextView.OnEditorActionListener mWriteListener + = new TextView.OnEditorActionListener() { + public boolean onEditorAction(TextView view, int actionId, KeyEvent event) { + // If the action is a key-up event on the return key, send the message + if (actionId == EditorInfo.IME_NULL && event.getAction() == KeyEvent.ACTION_UP) { + String message = view.getText().toString(); + sendMessage(message); + } + return true; + } + }; + + /** + * Updates the status on the action bar. + * + * @param resId a string resource ID + */ + private void setStatus(int resId) { + FragmentActivity activity = getActivity(); + if (null == activity) { + return; + } + final ActionBar actionBar = activity.getActionBar(); + if (null == actionBar) { + return; + } + actionBar.setSubtitle(resId); + } + + /** + * Updates the status on the action bar. + * + * @param subTitle status + */ + private void setStatus(CharSequence subTitle) { + FragmentActivity activity = getActivity(); + if (null == activity) { + return; + } + final ActionBar actionBar = activity.getActionBar(); + if (null == actionBar) { + return; + } + actionBar.setSubtitle(subTitle); + } + + /** + * The Handler that gets information back from the BluetoothChatService + */ + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + FragmentActivity activity = getActivity(); + switch (msg.what) { + case Constants.MESSAGE_STATE_CHANGE: + switch (msg.arg1) { + case BluetoothChatService.STATE_CONNECTED: + setStatus(getString(R.string.title_connected_to, mConnectedDeviceName)); + mConversationArrayAdapter.clear(); + break; + case BluetoothChatService.STATE_CONNECTING: + setStatus(R.string.title_connecting); + break; + case BluetoothChatService.STATE_LISTEN: + case BluetoothChatService.STATE_NONE: + setStatus(R.string.title_not_connected); + break; + } + break; + case Constants.MESSAGE_WRITE: + byte[] writeBuf = (byte[]) msg.obj; + // construct a string from the buffer + String writeMessage = new String(writeBuf); + mConversationArrayAdapter.add("Me: " + writeMessage); + break; + case Constants.MESSAGE_READ: + byte[] readBuf = (byte[]) msg.obj; + // construct a string from the valid bytes in the buffer + String readMessage = new String(readBuf, 0, msg.arg1); + mConversationArrayAdapter.add(mConnectedDeviceName + ": " + readMessage); + break; + case Constants.MESSAGE_DEVICE_NAME: + // save the connected device's name + mConnectedDeviceName = msg.getData().getString(Constants.DEVICE_NAME); + if (null != activity) { + Toast.makeText(activity, "Connected to " + + mConnectedDeviceName, Toast.LENGTH_SHORT).show(); + } + break; + case Constants.MESSAGE_TOAST: + if (null != activity) { + Toast.makeText(activity, msg.getData().getString(Constants.TOAST), + Toast.LENGTH_SHORT).show(); + } + break; + } + } + }; + + public void onActivityResult(int requestCode, int resultCode, Intent data) { + switch (requestCode) { + case REQUEST_CONNECT_DEVICE_SECURE: + // When DeviceListActivity returns with a device to connect + if (resultCode == Activity.RESULT_OK) { + connectDevice(data, true); + } + break; + case REQUEST_CONNECT_DEVICE_INSECURE: + // When DeviceListActivity returns with a device to connect + if (resultCode == Activity.RESULT_OK) { + connectDevice(data, false); + } + break; + case REQUEST_ENABLE_BT: + // When the request to enable Bluetooth returns + if (resultCode == Activity.RESULT_OK) { + // Bluetooth is now enabled, so set up a chat session + setupChat(); + } else { + // User did not enable Bluetooth or an error occurred + Log.d(TAG, "BT not enabled"); + Toast.makeText(getActivity(), R.string.bt_not_enabled_leaving, + Toast.LENGTH_SHORT).show(); + getActivity().finish(); + } + } + } + + /** + * Establish connection with other divice + * + * @param data An {@link Intent} with {@link DeviceListActivity#EXTRA_DEVICE_ADDRESS} extra. + * @param secure Socket Security type - Secure (true) , Insecure (false) + */ + private void connectDevice(Intent data, boolean secure) { + // Get the device MAC address + String address = data.getExtras() + .getString(DeviceListActivity.EXTRA_DEVICE_ADDRESS); + // Get the BluetoothDevice object + BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address); + // Attempt to connect to the device + mChatService.connect(device, secure); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.bluetooth_chat, menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.secure_connect_scan: { + // Launch the DeviceListActivity to see devices and do scan + Intent serverIntent = new Intent(getActivity(), DeviceListActivity.class); + startActivityForResult(serverIntent, REQUEST_CONNECT_DEVICE_SECURE); + return true; + } + case R.id.insecure_connect_scan: { + // Launch the DeviceListActivity to see devices and do scan + Intent serverIntent = new Intent(getActivity(), DeviceListActivity.class); + startActivityForResult(serverIntent, REQUEST_CONNECT_DEVICE_INSECURE); + return true; + } + case R.id.discoverable: { + // Ensure this device is discoverable by others + ensureDiscoverable(); + return true; + } + } + return false; + } + +} diff --git a/samples/browseable/BluetoothChat/src/com.example.android.bluetoothchat/BluetoothChatService.java b/samples/browseable/BluetoothChat/src/com.example.android.bluetoothchat/BluetoothChatService.java new file mode 100644 index 000000000..b88b160d2 --- /dev/null +++ b/samples/browseable/BluetoothChat/src/com.example.android.bluetoothchat/BluetoothChatService.java @@ -0,0 +1,519 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.bluetoothchat; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothServerSocket; +import android.bluetooth.BluetoothSocket; +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; + +import com.example.android.common.logger.Log; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.UUID; + +/** + * This class does all the work for setting up and managing Bluetooth + * connections with other devices. It has a thread that listens for + * incoming connections, a thread for connecting with a device, and a + * thread for performing data transmissions when connected. + */ +public class BluetoothChatService { + // Debugging + private static final String TAG = "BluetoothChatService"; + + // Name for the SDP record when creating server socket + private static final String NAME_SECURE = "BluetoothChatSecure"; + private static final String NAME_INSECURE = "BluetoothChatInsecure"; + + // Unique UUID for this application + private static final UUID MY_UUID_SECURE = + UUID.fromString("fa87c0d0-afac-11de-8a39-0800200c9a66"); + private static final UUID MY_UUID_INSECURE = + UUID.fromString("8ce255c0-200a-11e0-ac64-0800200c9a66"); + + // Member fields + private final BluetoothAdapter mAdapter; + private final Handler mHandler; + private AcceptThread mSecureAcceptThread; + private AcceptThread mInsecureAcceptThread; + private ConnectThread mConnectThread; + private ConnectedThread mConnectedThread; + private int mState; + + // Constants that indicate the current connection state + public static final int STATE_NONE = 0; // we're doing nothing + public static final int STATE_LISTEN = 1; // now listening for incoming connections + public static final int STATE_CONNECTING = 2; // now initiating an outgoing connection + public static final int STATE_CONNECTED = 3; // now connected to a remote device + + /** + * Constructor. Prepares a new BluetoothChat session. + * + * @param context The UI Activity Context + * @param handler A Handler to send messages back to the UI Activity + */ + public BluetoothChatService(Context context, Handler handler) { + mAdapter = BluetoothAdapter.getDefaultAdapter(); + mState = STATE_NONE; + mHandler = handler; + } + + /** + * Set the current state of the chat connection + * + * @param state An integer defining the current connection state + */ + private synchronized void setState(int state) { + Log.d(TAG, "setState() " + mState + " -> " + state); + mState = state; + + // Give the new state to the Handler so the UI Activity can update + mHandler.obtainMessage(Constants.MESSAGE_STATE_CHANGE, state, -1).sendToTarget(); + } + + /** + * Return the current connection state. + */ + public synchronized int getState() { + return mState; + } + + /** + * Start the chat service. Specifically start AcceptThread to begin a + * session in listening (server) mode. Called by the Activity onResume() + */ + public synchronized void start() { + Log.d(TAG, "start"); + + // Cancel any thread attempting to make a connection + if (mConnectThread != null) { + mConnectThread.cancel(); + mConnectThread = null; + } + + // Cancel any thread currently running a connection + if (mConnectedThread != null) { + mConnectedThread.cancel(); + mConnectedThread = null; + } + + setState(STATE_LISTEN); + + // Start the thread to listen on a BluetoothServerSocket + if (mSecureAcceptThread == null) { + mSecureAcceptThread = new AcceptThread(true); + mSecureAcceptThread.start(); + } + if (mInsecureAcceptThread == null) { + mInsecureAcceptThread = new AcceptThread(false); + mInsecureAcceptThread.start(); + } + } + + /** + * Start the ConnectThread to initiate a connection to a remote device. + * + * @param device The BluetoothDevice to connect + * @param secure Socket Security type - Secure (true) , Insecure (false) + */ + public synchronized void connect(BluetoothDevice device, boolean secure) { + Log.d(TAG, "connect to: " + device); + + // Cancel any thread attempting to make a connection + if (mState == STATE_CONNECTING) { + if (mConnectThread != null) { + mConnectThread.cancel(); + mConnectThread = null; + } + } + + // Cancel any thread currently running a connection + if (mConnectedThread != null) { + mConnectedThread.cancel(); + mConnectedThread = null; + } + + // Start the thread to connect with the given device + mConnectThread = new ConnectThread(device, secure); + mConnectThread.start(); + setState(STATE_CONNECTING); + } + + /** + * Start the ConnectedThread to begin managing a Bluetooth connection + * + * @param socket The BluetoothSocket on which the connection was made + * @param device The BluetoothDevice that has been connected + */ + public synchronized void connected(BluetoothSocket socket, BluetoothDevice + device, final String socketType) { + Log.d(TAG, "connected, Socket Type:" + socketType); + + // Cancel the thread that completed the connection + if (mConnectThread != null) { + mConnectThread.cancel(); + mConnectThread = null; + } + + // Cancel any thread currently running a connection + if (mConnectedThread != null) { + mConnectedThread.cancel(); + mConnectedThread = null; + } + + // Cancel the accept thread because we only want to connect to one device + if (mSecureAcceptThread != null) { + mSecureAcceptThread.cancel(); + mSecureAcceptThread = null; + } + if (mInsecureAcceptThread != null) { + mInsecureAcceptThread.cancel(); + mInsecureAcceptThread = null; + } + + // Start the thread to manage the connection and perform transmissions + mConnectedThread = new ConnectedThread(socket, socketType); + mConnectedThread.start(); + + // Send the name of the connected device back to the UI Activity + Message msg = mHandler.obtainMessage(Constants.MESSAGE_DEVICE_NAME); + Bundle bundle = new Bundle(); + bundle.putString(Constants.DEVICE_NAME, device.getName()); + msg.setData(bundle); + mHandler.sendMessage(msg); + + setState(STATE_CONNECTED); + } + + /** + * Stop all threads + */ + public synchronized void stop() { + Log.d(TAG, "stop"); + + if (mConnectThread != null) { + mConnectThread.cancel(); + mConnectThread = null; + } + + if (mConnectedThread != null) { + mConnectedThread.cancel(); + mConnectedThread = null; + } + + if (mSecureAcceptThread != null) { + mSecureAcceptThread.cancel(); + mSecureAcceptThread = null; + } + + if (mInsecureAcceptThread != null) { + mInsecureAcceptThread.cancel(); + mInsecureAcceptThread = null; + } + setState(STATE_NONE); + } + + /** + * Write to the ConnectedThread in an unsynchronized manner + * + * @param out The bytes to write + * @see ConnectedThread#write(byte[]) + */ + public void write(byte[] out) { + // Create temporary object + ConnectedThread r; + // Synchronize a copy of the ConnectedThread + synchronized (this) { + if (mState != STATE_CONNECTED) return; + r = mConnectedThread; + } + // Perform the write unsynchronized + r.write(out); + } + + /** + * Indicate that the connection attempt failed and notify the UI Activity. + */ + private void connectionFailed() { + // Send a failure message back to the Activity + Message msg = mHandler.obtainMessage(Constants.MESSAGE_TOAST); + Bundle bundle = new Bundle(); + bundle.putString(Constants.TOAST, "Unable to connect device"); + msg.setData(bundle); + mHandler.sendMessage(msg); + + // Start the service over to restart listening mode + BluetoothChatService.this.start(); + } + + /** + * Indicate that the connection was lost and notify the UI Activity. + */ + private void connectionLost() { + // Send a failure message back to the Activity + Message msg = mHandler.obtainMessage(Constants.MESSAGE_TOAST); + Bundle bundle = new Bundle(); + bundle.putString(Constants.TOAST, "Device connection was lost"); + msg.setData(bundle); + mHandler.sendMessage(msg); + + // Start the service over to restart listening mode + BluetoothChatService.this.start(); + } + + /** + * This thread runs while listening for incoming connections. It behaves + * like a server-side client. It runs until a connection is accepted + * (or until cancelled). + */ + private class AcceptThread extends Thread { + // The local server socket + private final BluetoothServerSocket mmServerSocket; + private String mSocketType; + + public AcceptThread(boolean secure) { + BluetoothServerSocket tmp = null; + mSocketType = secure ? "Secure" : "Insecure"; + + // Create a new listening server socket + try { + if (secure) { + tmp = mAdapter.listenUsingRfcommWithServiceRecord(NAME_SECURE, + MY_UUID_SECURE); + } else { + tmp = mAdapter.listenUsingInsecureRfcommWithServiceRecord( + NAME_INSECURE, MY_UUID_INSECURE); + } + } catch (IOException e) { + Log.e(TAG, "Socket Type: " + mSocketType + "listen() failed", e); + } + mmServerSocket = tmp; + } + + public void run() { + Log.d(TAG, "Socket Type: " + mSocketType + + "BEGIN mAcceptThread" + this); + setName("AcceptThread" + mSocketType); + + BluetoothSocket socket = null; + + // Listen to the server socket if we're not connected + while (mState != STATE_CONNECTED) { + try { + // This is a blocking call and will only return on a + // successful connection or an exception + socket = mmServerSocket.accept(); + } catch (IOException e) { + Log.e(TAG, "Socket Type: " + mSocketType + "accept() failed", e); + break; + } + + // If a connection was accepted + if (socket != null) { + synchronized (BluetoothChatService.this) { + switch (mState) { + case STATE_LISTEN: + case STATE_CONNECTING: + // Situation normal. Start the connected thread. + connected(socket, socket.getRemoteDevice(), + mSocketType); + break; + case STATE_NONE: + case STATE_CONNECTED: + // Either not ready or already connected. Terminate new socket. + try { + socket.close(); + } catch (IOException e) { + Log.e(TAG, "Could not close unwanted socket", e); + } + break; + } + } + } + } + Log.i(TAG, "END mAcceptThread, socket Type: " + mSocketType); + + } + + public void cancel() { + Log.d(TAG, "Socket Type" + mSocketType + "cancel " + this); + try { + mmServerSocket.close(); + } catch (IOException e) { + Log.e(TAG, "Socket Type" + mSocketType + "close() of server failed", e); + } + } + } + + + /** + * This thread runs while attempting to make an outgoing connection + * with a device. It runs straight through; the connection either + * succeeds or fails. + */ + private class ConnectThread extends Thread { + private final BluetoothSocket mmSocket; + private final BluetoothDevice mmDevice; + private String mSocketType; + + public ConnectThread(BluetoothDevice device, boolean secure) { + mmDevice = device; + BluetoothSocket tmp = null; + mSocketType = secure ? "Secure" : "Insecure"; + + // Get a BluetoothSocket for a connection with the + // given BluetoothDevice + try { + if (secure) { + tmp = device.createRfcommSocketToServiceRecord( + MY_UUID_SECURE); + } else { + tmp = device.createInsecureRfcommSocketToServiceRecord( + MY_UUID_INSECURE); + } + } catch (IOException e) { + Log.e(TAG, "Socket Type: " + mSocketType + "create() failed", e); + } + mmSocket = tmp; + } + + public void run() { + Log.i(TAG, "BEGIN mConnectThread SocketType:" + mSocketType); + setName("ConnectThread" + mSocketType); + + // Always cancel discovery because it will slow down a connection + mAdapter.cancelDiscovery(); + + // Make a connection to the BluetoothSocket + try { + // This is a blocking call and will only return on a + // successful connection or an exception + mmSocket.connect(); + } catch (IOException e) { + // Close the socket + try { + mmSocket.close(); + } catch (IOException e2) { + Log.e(TAG, "unable to close() " + mSocketType + + " socket during connection failure", e2); + } + connectionFailed(); + return; + } + + // Reset the ConnectThread because we're done + synchronized (BluetoothChatService.this) { + mConnectThread = null; + } + + // Start the connected thread + connected(mmSocket, mmDevice, mSocketType); + } + + public void cancel() { + try { + mmSocket.close(); + } catch (IOException e) { + Log.e(TAG, "close() of connect " + mSocketType + " socket failed", e); + } + } + } + + /** + * This thread runs during a connection with a remote device. + * It handles all incoming and outgoing transmissions. + */ + private class ConnectedThread extends Thread { + private final BluetoothSocket mmSocket; + private final InputStream mmInStream; + private final OutputStream mmOutStream; + + public ConnectedThread(BluetoothSocket socket, String socketType) { + Log.d(TAG, "create ConnectedThread: " + socketType); + mmSocket = socket; + InputStream tmpIn = null; + OutputStream tmpOut = null; + + // Get the BluetoothSocket input and output streams + try { + tmpIn = socket.getInputStream(); + tmpOut = socket.getOutputStream(); + } catch (IOException e) { + Log.e(TAG, "temp sockets not created", e); + } + + mmInStream = tmpIn; + mmOutStream = tmpOut; + } + + public void run() { + Log.i(TAG, "BEGIN mConnectedThread"); + byte[] buffer = new byte[1024]; + int bytes; + + // Keep listening to the InputStream while connected + while (true) { + try { + // Read from the InputStream + bytes = mmInStream.read(buffer); + + // Send the obtained bytes to the UI Activity + mHandler.obtainMessage(Constants.MESSAGE_READ, bytes, -1, buffer) + .sendToTarget(); + } catch (IOException e) { + Log.e(TAG, "disconnected", e); + connectionLost(); + // Start the service over to restart listening mode + BluetoothChatService.this.start(); + break; + } + } + } + + /** + * Write to the connected OutStream. + * + * @param buffer The bytes to write + */ + public void write(byte[] buffer) { + try { + mmOutStream.write(buffer); + + // Share the sent message back to the UI Activity + mHandler.obtainMessage(Constants.MESSAGE_WRITE, -1, -1, buffer) + .sendToTarget(); + } catch (IOException e) { + Log.e(TAG, "Exception during write", e); + } + } + + public void cancel() { + try { + mmSocket.close(); + } catch (IOException e) { + Log.e(TAG, "close() of connect socket failed", e); + } + } + } +} diff --git a/samples/browseable/BluetoothChat/src/com.example.android.bluetoothchat/Constants.java b/samples/browseable/BluetoothChat/src/com.example.android.bluetoothchat/Constants.java new file mode 100644 index 000000000..3500e8e70 --- /dev/null +++ b/samples/browseable/BluetoothChat/src/com.example.android.bluetoothchat/Constants.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.bluetoothchat; + +/** + * Defines several constants used between {@link BluetoothChatService} and the UI. + */ +public interface Constants { + + // Message types sent from the BluetoothChatService Handler + public static final int MESSAGE_STATE_CHANGE = 1; + public static final int MESSAGE_READ = 2; + public static final int MESSAGE_WRITE = 3; + public static final int MESSAGE_DEVICE_NAME = 4; + public static final int MESSAGE_TOAST = 5; + + // Key names received from the BluetoothChatService Handler + public static final String DEVICE_NAME = "device_name"; + public static final String TOAST = "toast"; + +} diff --git a/samples/browseable/BluetoothChat/src/com.example.android.bluetoothchat/DeviceListActivity.java b/samples/browseable/BluetoothChat/src/com.example.android.bluetoothchat/DeviceListActivity.java new file mode 100644 index 000000000..8b70adc4c --- /dev/null +++ b/samples/browseable/BluetoothChat/src/com.example.android.bluetoothchat/DeviceListActivity.java @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.bluetoothchat; + +import android.app.Activity; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; +import android.view.View; +import android.view.Window; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.ListView; +import android.widget.TextView; + +import com.example.android.common.logger.Log; + +import java.util.Set; + +/** + * This Activity appears as a dialog. It lists any paired devices and + * devices detected in the area after discovery. When a device is chosen + * by the user, the MAC address of the device is sent back to the parent + * Activity in the result Intent. + */ +public class DeviceListActivity extends Activity { + + /** + * Tag for Log + */ + private static final String TAG = "DeviceListActivity"; + + /** + * Return Intent extra + */ + public static String EXTRA_DEVICE_ADDRESS = "device_address"; + + /** + * Member fields + */ + private BluetoothAdapter mBtAdapter; + + /** + * Newly discovered devices + */ + private ArrayAdapter mNewDevicesArrayAdapter; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Setup the window + requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + setContentView(R.layout.activity_device_list); + + // Set result CANCELED in case the user backs out + setResult(Activity.RESULT_CANCELED); + + // Initialize the button to perform device discovery + Button scanButton = (Button) findViewById(R.id.button_scan); + scanButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + doDiscovery(); + v.setVisibility(View.GONE); + } + }); + + // Initialize array adapters. One for already paired devices and + // one for newly discovered devices + ArrayAdapter pairedDevicesArrayAdapter = + new ArrayAdapter(this, R.layout.device_name); + mNewDevicesArrayAdapter = new ArrayAdapter(this, R.layout.device_name); + + // Find and set up the ListView for paired devices + ListView pairedListView = (ListView) findViewById(R.id.paired_devices); + pairedListView.setAdapter(pairedDevicesArrayAdapter); + pairedListView.setOnItemClickListener(mDeviceClickListener); + + // Find and set up the ListView for newly discovered devices + ListView newDevicesListView = (ListView) findViewById(R.id.new_devices); + newDevicesListView.setAdapter(mNewDevicesArrayAdapter); + newDevicesListView.setOnItemClickListener(mDeviceClickListener); + + // Register for broadcasts when a device is discovered + IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND); + this.registerReceiver(mReceiver, filter); + + // Register for broadcasts when discovery has finished + filter = new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED); + this.registerReceiver(mReceiver, filter); + + // Get the local Bluetooth adapter + mBtAdapter = BluetoothAdapter.getDefaultAdapter(); + + // Get a set of currently paired devices + Set pairedDevices = mBtAdapter.getBondedDevices(); + + // If there are paired devices, add each one to the ArrayAdapter + if (pairedDevices.size() > 0) { + findViewById(R.id.title_paired_devices).setVisibility(View.VISIBLE); + for (BluetoothDevice device : pairedDevices) { + pairedDevicesArrayAdapter.add(device.getName() + "\n" + device.getAddress()); + } + } else { + String noDevices = getResources().getText(R.string.none_paired).toString(); + pairedDevicesArrayAdapter.add(noDevices); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + + // Make sure we're not doing discovery anymore + if (mBtAdapter != null) { + mBtAdapter.cancelDiscovery(); + } + + // Unregister broadcast listeners + this.unregisterReceiver(mReceiver); + } + + /** + * Start device discover with the BluetoothAdapter + */ + private void doDiscovery() { + Log.d(TAG, "doDiscovery()"); + + // Indicate scanning in the title + setProgressBarIndeterminateVisibility(true); + setTitle(R.string.scanning); + + // Turn on sub-title for new devices + findViewById(R.id.title_new_devices).setVisibility(View.VISIBLE); + + // If we're already discovering, stop it + if (mBtAdapter.isDiscovering()) { + mBtAdapter.cancelDiscovery(); + } + + // Request discover from BluetoothAdapter + mBtAdapter.startDiscovery(); + } + + /** + * The on-click listener for all devices in the ListViews + */ + private AdapterView.OnItemClickListener mDeviceClickListener + = new AdapterView.OnItemClickListener() { + public void onItemClick(AdapterView av, View v, int arg2, long arg3) { + // Cancel discovery because it's costly and we're about to connect + mBtAdapter.cancelDiscovery(); + + // Get the device MAC address, which is the last 17 chars in the View + String info = ((TextView) v).getText().toString(); + String address = info.substring(info.length() - 17); + + // Create the result Intent and include the MAC address + Intent intent = new Intent(); + intent.putExtra(EXTRA_DEVICE_ADDRESS, address); + + // Set result and finish this Activity + setResult(Activity.RESULT_OK, intent); + finish(); + } + }; + + /** + * The BroadcastReceiver that listens for discovered devices and changes the title when + * discovery is finished + */ + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + + // When discovery finds a device + if (BluetoothDevice.ACTION_FOUND.equals(action)) { + // Get the BluetoothDevice object from the Intent + BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + // If it's already paired, skip it, because it's been listed already + if (device.getBondState() != BluetoothDevice.BOND_BONDED) { + mNewDevicesArrayAdapter.add(device.getName() + "\n" + device.getAddress()); + } + // When discovery is finished, change the Activity title + } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) { + setProgressBarIndeterminateVisibility(false); + setTitle(R.string.select_device); + if (mNewDevicesArrayAdapter.getCount() == 0) { + String noDevices = getResources().getText(R.string.none_found).toString(); + mNewDevicesArrayAdapter.add(noDevices); + } + } + } + }; + +} diff --git a/samples/browseable/BluetoothChat/src/com.example.android.bluetoothchat/MainActivity.java b/samples/browseable/BluetoothChat/src/com.example.android.bluetoothchat/MainActivity.java new file mode 100644 index 000000000..cf4ec47e3 --- /dev/null +++ b/samples/browseable/BluetoothChat/src/com.example.android.bluetoothchat/MainActivity.java @@ -0,0 +1,109 @@ +/* +* Copyright 2013 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +package com.example.android.bluetoothchat; + +import android.os.Bundle; +import android.support.v4.app.FragmentTransaction; +import android.view.Menu; +import android.view.MenuItem; +import android.widget.ViewAnimator; + +import com.example.android.common.activities.SampleActivityBase; +import com.example.android.common.logger.Log; +import com.example.android.common.logger.LogFragment; +import com.example.android.common.logger.LogWrapper; +import com.example.android.common.logger.MessageOnlyLogFilter; + +/** + * A simple launcher activity containing a summary sample description, sample log and a custom + * {@link android.support.v4.app.Fragment} which can display a view. + *

+ * For devices with displays with a width of 720dp or greater, the sample log is always visible, + * on other devices it's visibility is controlled by an item on the Action Bar. + */ +public class MainActivity extends SampleActivityBase { + + public static final String TAG = "MainActivity"; + + // Whether the Log Fragment is currently shown + private boolean mLogShown; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + if (savedInstanceState == null) { + FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); + BluetoothChatFragment fragment = new BluetoothChatFragment(); + transaction.replace(R.id.sample_content_fragment, fragment); + transaction.commit(); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.main, menu); + return true; + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + MenuItem logToggle = menu.findItem(R.id.menu_toggle_log); + logToggle.setVisible(findViewById(R.id.sample_output) instanceof ViewAnimator); + logToggle.setTitle(mLogShown ? R.string.sample_hide_log : R.string.sample_show_log); + + return super.onPrepareOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch(item.getItemId()) { + case R.id.menu_toggle_log: + mLogShown = !mLogShown; + ViewAnimator output = (ViewAnimator) findViewById(R.id.sample_output); + if (mLogShown) { + output.setDisplayedChild(1); + } else { + output.setDisplayedChild(0); + } + supportInvalidateOptionsMenu(); + return true; + } + return super.onOptionsItemSelected(item); + } + + /** Create a chain of targets that will receive log data */ + @Override + public void initializeLogging() { + // Wraps Android's native log framework. + LogWrapper logWrapper = new LogWrapper(); + // Using Log, front-end to the logging chain, emulates android.util.log method signatures. + Log.setLogNode(logWrapper); + + // Filter strips out everything except the message text. + MessageOnlyLogFilter msgFilter = new MessageOnlyLogFilter(); + logWrapper.setNext(msgFilter); + + // On screen logging via a fragment with a TextView. + LogFragment logFragment = (LogFragment) getSupportFragmentManager() + .findFragmentById(R.id.log_fragment); + msgFilter.setNext(logFragment.getLogView()); + + Log.i(TAG, "Ready"); + } +} diff --git a/samples/browseable/AgendaData/Application/src/com.example.android.common/activities/SampleActivityBase.java b/samples/browseable/BluetoothChat/src/com.example.android.common/activities/SampleActivityBase.java similarity index 100% rename from samples/browseable/AgendaData/Application/src/com.example.android.common/activities/SampleActivityBase.java rename to samples/browseable/BluetoothChat/src/com.example.android.common/activities/SampleActivityBase.java diff --git a/samples/browseable/AgendaData/Application/src/com.example.android.common/logger/Log.java b/samples/browseable/BluetoothChat/src/com.example.android.common/logger/Log.java similarity index 100% rename from samples/browseable/AgendaData/Application/src/com.example.android.common/logger/Log.java rename to samples/browseable/BluetoothChat/src/com.example.android.common/logger/Log.java diff --git a/samples/browseable/AgendaData/Application/src/com.example.android.common/logger/LogFragment.java b/samples/browseable/BluetoothChat/src/com.example.android.common/logger/LogFragment.java similarity index 100% rename from samples/browseable/AgendaData/Application/src/com.example.android.common/logger/LogFragment.java rename to samples/browseable/BluetoothChat/src/com.example.android.common/logger/LogFragment.java diff --git a/samples/browseable/AgendaData/Application/src/com.example.android.common/logger/LogNode.java b/samples/browseable/BluetoothChat/src/com.example.android.common/logger/LogNode.java similarity index 100% rename from samples/browseable/AgendaData/Application/src/com.example.android.common/logger/LogNode.java rename to samples/browseable/BluetoothChat/src/com.example.android.common/logger/LogNode.java diff --git a/samples/browseable/AgendaData/Application/src/com.example.android.common/logger/LogView.java b/samples/browseable/BluetoothChat/src/com.example.android.common/logger/LogView.java similarity index 100% rename from samples/browseable/AgendaData/Application/src/com.example.android.common/logger/LogView.java rename to samples/browseable/BluetoothChat/src/com.example.android.common/logger/LogView.java diff --git a/samples/browseable/AgendaData/Application/src/com.example.android.common/logger/LogWrapper.java b/samples/browseable/BluetoothChat/src/com.example.android.common/logger/LogWrapper.java similarity index 100% rename from samples/browseable/AgendaData/Application/src/com.example.android.common/logger/LogWrapper.java rename to samples/browseable/BluetoothChat/src/com.example.android.common/logger/LogWrapper.java diff --git a/samples/browseable/AgendaData/Application/src/com.example.android.common/logger/MessageOnlyLogFilter.java b/samples/browseable/BluetoothChat/src/com.example.android.common/logger/MessageOnlyLogFilter.java similarity index 100% rename from samples/browseable/AgendaData/Application/src/com.example.android.common/logger/MessageOnlyLogFilter.java rename to samples/browseable/BluetoothChat/src/com.example.android.common/logger/MessageOnlyLogFilter.java diff --git a/samples/browseable/BluetoothLeGatt/AndroidManifest.xml b/samples/browseable/BluetoothLeGatt/AndroidManifest.xml index babd6df2d..d3cf25757 100644 --- a/samples/browseable/BluetoothLeGatt/AndroidManifest.xml +++ b/samples/browseable/BluetoothLeGatt/AndroidManifest.xml @@ -22,8 +22,8 @@ android:versionCode="1" android:versionName="1.0"> - + + + + + + + - + + diff --git a/samples/browseable/DoneBar/src/com.example.android.donebar/MainActivity.java b/samples/browseable/DoneBar/src/com.example.android.donebar/MainActivity.java index 8b1e8a469..c51996c4c 100644 --- a/samples/browseable/DoneBar/src/com.example.android.donebar/MainActivity.java +++ b/samples/browseable/DoneBar/src/com.example.android.donebar/MainActivity.java @@ -14,9 +14,6 @@ * limitations under the License. */ - - - package com.example.android.donebar; import android.app.Activity; diff --git a/samples/browseable/ElizaChat/Application/AndroidManifest.xml b/samples/browseable/ElizaChat/Application/AndroidManifest.xml index e653fa9d6..14e982383 100644 --- a/samples/browseable/ElizaChat/Application/AndroidManifest.xml +++ b/samples/browseable/ElizaChat/Application/AndroidManifest.xml @@ -15,7 +15,7 @@ --> + package="com.example.android.wearable.elizachat" > @@ -36,8 +36,8 @@ - - + + diff --git a/samples/browseable/ElizaChat/Application/res/values-v21/template-styles.xml b/samples/browseable/ElizaChat/Application/res/values-v21/template-styles.xml new file mode 100644 index 000000000..134fcd9d3 --- /dev/null +++ b/samples/browseable/ElizaChat/Application/res/values-v21/template-styles.xml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/samples/browseable/MediaBrowserService/res/values/dimens.xml b/samples/browseable/MediaBrowserService/res/values/dimens.xml new file mode 100644 index 000000000..e57a8c919 --- /dev/null +++ b/samples/browseable/MediaBrowserService/res/values/dimens.xml @@ -0,0 +1,21 @@ + + + + 16dp + 4dp + 6dp + diff --git a/samples/browseable/MediaBrowserService/res/values/strings.xml b/samples/browseable/MediaBrowserService/res/values/strings.xml new file mode 100644 index 000000000..7a012e870 --- /dev/null +++ b/samples/browseable/MediaBrowserService/res/values/strings.xml @@ -0,0 +1,33 @@ + + + + + Auto Music Demo + Favorite + Unable to retrieve metadata. + Genres + Songs by genre + %1$s songs + Random music + Cannot skip + Error Loading Media + Play item + Skip to previous + play or pause + Skip to next + + diff --git a/samples/browseable/MediaBrowserService/res/values/strings_notifications.xml b/samples/browseable/MediaBrowserService/res/values/strings_notifications.xml new file mode 100644 index 000000000..f406ba667 --- /dev/null +++ b/samples/browseable/MediaBrowserService/res/values/strings_notifications.xml @@ -0,0 +1,24 @@ + + + + + Pause + Play + Previous + Next + Empty metadata! + diff --git a/samples/browseable/MediaBrowserService/res/values/styles.xml b/samples/browseable/MediaBrowserService/res/values/styles.xml new file mode 100644 index 000000000..3be59c152 --- /dev/null +++ b/samples/browseable/MediaBrowserService/res/values/styles.xml @@ -0,0 +1,26 @@ + + + + + + + + + + \ No newline at end of file diff --git a/samples/browseable/MediaBrowserService/res/xml/automotive_app_desc.xml b/samples/browseable/MediaBrowserService/res/xml/automotive_app_desc.xml new file mode 100644 index 000000000..a84750b04 --- /dev/null +++ b/samples/browseable/MediaBrowserService/res/xml/automotive_app_desc.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/BrowseFragment.java b/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/BrowseFragment.java new file mode 100644 index 000000000..726ae15b6 --- /dev/null +++ b/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/BrowseFragment.java @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.mediabrowserservice; + +import android.app.Fragment; +import android.content.ComponentName; +import android.content.Context; +import android.media.browse.MediaBrowser; +import android.media.session.MediaController; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; + +import com.example.android.mediabrowserservice.utils.LogHelper; + +import java.util.ArrayList; +import java.util.List; + +/** + * A Fragment that lists all the various browsable queues available + * from a {@link android.service.media.MediaBrowserService}. + *

+ * It uses a {@link MediaBrowser} to connect to the {@link MusicService}. Once connected, + * the fragment subscribes to get all the children. All {@link MediaBrowser.MediaItem}'s + * that can be browsed are shown in a ListView. + */ +public class BrowseFragment extends Fragment { + + private static final String TAG = BrowseFragment.class.getSimpleName(); + + public static final String ARG_MEDIA_ID = "media_id"; + + public static interface FragmentDataHelper { + void onMediaItemSelected(MediaBrowser.MediaItem item); + } + + // The mediaId to be used for subscribing for children using the MediaBrowser. + private String mMediaId; + + private MediaBrowser mMediaBrowser; + private BrowseAdapter mBrowserAdapter; + + private MediaBrowser.SubscriptionCallback mSubscriptionCallback = new MediaBrowser.SubscriptionCallback() { + + @Override + public void onChildrenLoaded(String parentId, List children) { + mBrowserAdapter.clear(); + mBrowserAdapter.notifyDataSetInvalidated(); + for (MediaBrowser.MediaItem item : children) { + mBrowserAdapter.add(item); + } + mBrowserAdapter.notifyDataSetChanged(); + } + + @Override + public void onError(String id) { + Toast.makeText(getActivity(), R.string.error_loading_media, + Toast.LENGTH_LONG).show(); + } + }; + + private MediaBrowser.ConnectionCallback mConnectionCallback = + new MediaBrowser.ConnectionCallback() { + @Override + public void onConnected() { + LogHelper.d(TAG, "onConnected: session token " + mMediaBrowser.getSessionToken()); + + if (mMediaId == null) { + mMediaId = mMediaBrowser.getRoot(); + } + mMediaBrowser.subscribe(mMediaId, mSubscriptionCallback); + if (mMediaBrowser.getSessionToken() == null) { + throw new IllegalArgumentException("No Session token"); + } + MediaController mediaController = new MediaController(getActivity(), + mMediaBrowser.getSessionToken()); + getActivity().setMediaController(mediaController); + } + + @Override + public void onConnectionFailed() { + LogHelper.d(TAG, "onConnectionFailed"); + } + + @Override + public void onConnectionSuspended() { + LogHelper.d(TAG, "onConnectionSuspended"); + getActivity().setMediaController(null); + } + }; + + public static BrowseFragment newInstance(String mediaId) { + Bundle args = new Bundle(); + args.putString(ARG_MEDIA_ID, mediaId); + BrowseFragment fragment = new BrowseFragment(); + fragment.setArguments(args); + return fragment; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.fragment_list, container, false); + + mBrowserAdapter = new BrowseAdapter(getActivity()); + + View controls = rootView.findViewById(R.id.controls); + controls.setVisibility(View.GONE); + + ListView listView = (ListView) rootView.findViewById(R.id.list_view); + listView.setAdapter(mBrowserAdapter); + listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + MediaBrowser.MediaItem item = mBrowserAdapter.getItem(position); + try { + FragmentDataHelper listener = (FragmentDataHelper) getActivity(); + listener.onMediaItemSelected(item); + } catch (ClassCastException ex) { + Log.e(TAG, "Exception trying to cast to FragmentDataHelper", ex); + } + } + }); + + Bundle args = getArguments(); + mMediaId = args.getString(ARG_MEDIA_ID, null); + + mMediaBrowser = new MediaBrowser(getActivity(), + new ComponentName(getActivity(), MusicService.class), + mConnectionCallback, null); + + return rootView; + } + + @Override + public void onStart() { + super.onStart(); + mMediaBrowser.connect(); + } + + @Override + public void onStop() { + super.onStop(); + mMediaBrowser.disconnect(); + } + + // An adapter for showing the list of browsed MediaItem's + private static class BrowseAdapter extends ArrayAdapter { + + public BrowseAdapter(Context context) { + super(context, R.layout.media_list_item, new ArrayList()); + } + + static class ViewHolder { + ImageView mImageView; + TextView mTitleView; + TextView mDescriptionView; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + + ViewHolder holder; + + if (convertView == null) { + convertView = LayoutInflater.from(getContext()) + .inflate(R.layout.media_list_item, parent, false); + holder = new ViewHolder(); + holder.mImageView = (ImageView) convertView.findViewById(R.id.play_eq); + holder.mImageView.setVisibility(View.GONE); + holder.mTitleView = (TextView) convertView.findViewById(R.id.title); + holder.mDescriptionView = (TextView) convertView.findViewById(R.id.description); + convertView.setTag(holder); + } else { + holder = (ViewHolder) convertView.getTag(); + } + + MediaBrowser.MediaItem item = getItem(position); + holder.mTitleView.setText(item.getDescription().getTitle()); + holder.mDescriptionView.setText(item.getDescription().getDescription()); + if (item.isPlayable()) { + holder.mImageView.setImageDrawable( + getContext().getDrawable(R.drawable.ic_play_arrow_white_24dp)); + holder.mImageView.setVisibility(View.VISIBLE); + } + return convertView; + } + } +} diff --git a/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/MediaNotification.java b/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/MediaNotification.java new file mode 100644 index 000000000..7b8631a45 --- /dev/null +++ b/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/MediaNotification.java @@ -0,0 +1,381 @@ +/* + * Copyright (C) 2014 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.mediabrowserservice; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Color; +import android.media.MediaDescription; +import android.media.MediaMetadata; +import android.media.session.MediaController; +import android.media.session.MediaSession; +import android.media.session.PlaybackState; +import android.os.AsyncTask; +import android.util.LruCache; +import android.util.SparseArray; + +import com.example.android.mediabrowserservice.utils.BitmapHelper; +import com.example.android.mediabrowserservice.utils.LogHelper; + +import java.io.IOException; + +/** + * Keeps track of a notification and updates it automatically for a given + * MediaSession. Maintaining a visible notification (usually) guarantees that the music service + * won't be killed during playback. + */ +public class MediaNotification extends BroadcastReceiver { + private static final String TAG = "MediaNotification"; + + private static final int NOTIFICATION_ID = 412; + + public static final String ACTION_PAUSE = "com.example.android.mediabrowserservice.pause"; + public static final String ACTION_PLAY = "com.example.android.mediabrowserservice.play"; + public static final String ACTION_PREV = "com.example.android.mediabrowserservice.prev"; + public static final String ACTION_NEXT = "com.example.android.mediabrowserservice.next"; + + private static final int MAX_ALBUM_ART_CACHE_SIZE = 1024*1024; + + private final MusicService mService; + private MediaSession.Token mSessionToken; + private MediaController mController; + private MediaController.TransportControls mTransportControls; + private final SparseArray mIntents = new SparseArray(); + private final LruCache mAlbumArtCache; + + private PlaybackState mPlaybackState; + private MediaMetadata mMetadata; + + private Notification.Builder mNotificationBuilder; + private NotificationManager mNotificationManager; + private Notification.Action mPlayPauseAction; + + private String mCurrentAlbumArt; + private int mNotificationColor; + + private boolean mStarted = false; + + public MediaNotification(MusicService service) { + mService = service; + updateSessionToken(); + + // simple album art cache that holds no more than + // MAX_ALBUM_ART_CACHE_SIZE bytes: + mAlbumArtCache = new LruCache(MAX_ALBUM_ART_CACHE_SIZE) { + @Override + protected int sizeOf(String key, Bitmap value) { + return value.getByteCount(); + } + }; + + mNotificationColor = getNotificationColor(); + + mNotificationManager = (NotificationManager) mService + .getSystemService(Context.NOTIFICATION_SERVICE); + + String pkg = mService.getPackageName(); + mIntents.put(R.drawable.ic_pause_white_24dp, PendingIntent.getBroadcast(mService, 100, + new Intent(ACTION_PAUSE).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT)); + mIntents.put(R.drawable.ic_play_arrow_white_24dp, PendingIntent.getBroadcast(mService, 100, + new Intent(ACTION_PLAY).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT)); + mIntents.put(R.drawable.ic_skip_previous_white_24dp, PendingIntent.getBroadcast(mService, 100, + new Intent(ACTION_PREV).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT)); + mIntents.put(R.drawable.ic_skip_next_white_24dp, PendingIntent.getBroadcast(mService, 100, + new Intent(ACTION_NEXT).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT)); + } + + protected int getNotificationColor() { + int notificationColor = 0; + String packageName = mService.getPackageName(); + try { + Context packageContext = mService.createPackageContext(packageName, 0); + ApplicationInfo applicationInfo = + mService.getPackageManager().getApplicationInfo(packageName, 0); + packageContext.setTheme(applicationInfo.theme); + Resources.Theme theme = packageContext.getTheme(); + TypedArray ta = theme.obtainStyledAttributes( + new int[] {android.R.attr.colorPrimary}); + notificationColor = ta.getColor(0, Color.DKGRAY); + ta.recycle(); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + } + return notificationColor; + } + + /** + * Posts the notification and starts tracking the session to keep it + * updated. The notification will automatically be removed if the session is + * destroyed before {@link #stopNotification} is called. + */ + public void startNotification() { + if (!mStarted) { + mController.registerCallback(mCb); + IntentFilter filter = new IntentFilter(); + filter.addAction(ACTION_NEXT); + filter.addAction(ACTION_PAUSE); + filter.addAction(ACTION_PLAY); + filter.addAction(ACTION_PREV); + mService.registerReceiver(this, filter); + + mMetadata = mController.getMetadata(); + mPlaybackState = mController.getPlaybackState(); + + mStarted = true; + // The notification must be updated after setting started to true + updateNotificationMetadata(); + } + } + + /** + * Removes the notification and stops tracking the session. If the session + * was destroyed this has no effect. + */ + public void stopNotification() { + mStarted = false; + mController.unregisterCallback(mCb); + try { + mNotificationManager.cancel(NOTIFICATION_ID); + mService.unregisterReceiver(this); + } catch (IllegalArgumentException ex) { + // ignore if the receiver is not registered. + } + mService.stopForeground(true); + } + + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + LogHelper.d(TAG, "Received intent with action " + action); + if (ACTION_PAUSE.equals(action)) { + mTransportControls.pause(); + } else if (ACTION_PLAY.equals(action)) { + mTransportControls.play(); + } else if (ACTION_NEXT.equals(action)) { + mTransportControls.skipToNext(); + } else if (ACTION_PREV.equals(action)) { + mTransportControls.skipToPrevious(); + } + } + + /** + * Update the state based on a change on the session token. Called either when + * we are running for the first time or when the media session owner has destroyed the session + * (see {@link android.media.session.MediaController.Callback#onSessionDestroyed()}) + */ + private void updateSessionToken() { + MediaSession.Token freshToken = mService.getSessionToken(); + if (mSessionToken == null || !mSessionToken.equals(freshToken)) { + if (mController != null) { + mController.unregisterCallback(mCb); + } + mSessionToken = freshToken; + mController = new MediaController(mService, mSessionToken); + mTransportControls = mController.getTransportControls(); + if (mStarted) { + mController.registerCallback(mCb); + } + } + } + + private final MediaController.Callback mCb = new MediaController.Callback() { + @Override + public void onPlaybackStateChanged(PlaybackState state) { + mPlaybackState = state; + LogHelper.d(TAG, "Received new playback state", state); + updateNotificationPlaybackState(); + } + + @Override + public void onMetadataChanged(MediaMetadata metadata) { + mMetadata = metadata; + LogHelper.d(TAG, "Received new metadata ", metadata); + updateNotificationMetadata(); + } + + @Override + public void onSessionDestroyed() { + super.onSessionDestroyed(); + LogHelper.d(TAG, "Session was destroyed, resetting to the new session token"); + updateSessionToken(); + } + }; + + private void updateNotificationMetadata() { + LogHelper.d(TAG, "updateNotificationMetadata. mMetadata=" + mMetadata); + if (mMetadata == null || mPlaybackState == null) { + return; + } + + updatePlayPauseAction(); + + mNotificationBuilder = new Notification.Builder(mService); + int playPauseActionIndex = 0; + + // If skip to previous action is enabled + if ((mPlaybackState.getActions() & PlaybackState.ACTION_SKIP_TO_PREVIOUS) != 0) { + mNotificationBuilder + .addAction(R.drawable.ic_skip_previous_white_24dp, + mService.getString(R.string.label_previous), + mIntents.get(R.drawable.ic_skip_previous_white_24dp)); + playPauseActionIndex = 1; + } + + mNotificationBuilder.addAction(mPlayPauseAction); + + // If skip to next action is enabled + if ((mPlaybackState.getActions() & PlaybackState.ACTION_SKIP_TO_NEXT) != 0) { + mNotificationBuilder.addAction(R.drawable.ic_skip_next_white_24dp, + mService.getString(R.string.label_next), + mIntents.get(R.drawable.ic_skip_next_white_24dp)); + } + + MediaDescription description = mMetadata.getDescription(); + + String fetchArtUrl = null; + Bitmap art = description.getIconBitmap(); + if (art == null && description.getIconUri() != null) { + // This sample assumes the iconUri will be a valid URL formatted String, but + // it can actually be any valid Android Uri formatted String. + // async fetch the album art icon + String artUrl = description.getIconUri().toString(); + art = mAlbumArtCache.get(artUrl); + if (art == null) { + fetchArtUrl = artUrl; + // use a placeholder art while the remote art is being downloaded + art = BitmapFactory.decodeResource(mService.getResources(), R.drawable.ic_default_art); + } + } + + mNotificationBuilder + .setStyle(new Notification.MediaStyle() + .setShowActionsInCompactView(playPauseActionIndex) // only show play/pause in compact view + .setMediaSession(mSessionToken)) + .setColor(mNotificationColor) + .setSmallIcon(R.drawable.ic_notification) + .setVisibility(Notification.VISIBILITY_PUBLIC) + .setUsesChronometer(true) + .setContentTitle(description.getTitle()) + .setContentText(description.getSubtitle()) + .setLargeIcon(art); + + updateNotificationPlaybackState(); + + mService.startForeground(NOTIFICATION_ID, mNotificationBuilder.build()); + if (fetchArtUrl != null) { + fetchBitmapFromURLAsync(fetchArtUrl); + } + } + + private void updatePlayPauseAction() { + LogHelper.d(TAG, "updatePlayPauseAction"); + String playPauseLabel = ""; + int playPauseIcon; + if (mPlaybackState.getState() == PlaybackState.STATE_PLAYING) { + playPauseLabel = mService.getString(R.string.label_pause); + playPauseIcon = R.drawable.ic_pause_white_24dp; + } else { + playPauseLabel = mService.getString(R.string.label_play); + playPauseIcon = R.drawable.ic_play_arrow_white_24dp; + } + if (mPlayPauseAction == null) { + mPlayPauseAction = new Notification.Action(playPauseIcon, playPauseLabel, + mIntents.get(playPauseIcon)); + } else { + mPlayPauseAction.icon = playPauseIcon; + mPlayPauseAction.title = playPauseLabel; + mPlayPauseAction.actionIntent = mIntents.get(playPauseIcon); + } + } + + private void updateNotificationPlaybackState() { + LogHelper.d(TAG, "updateNotificationPlaybackState. mPlaybackState=" + mPlaybackState); + if (mPlaybackState == null || !mStarted) { + LogHelper.d(TAG, "updateNotificationPlaybackState. cancelling notification!"); + mService.stopForeground(true); + return; + } + if (mNotificationBuilder == null) { + LogHelper.d(TAG, "updateNotificationPlaybackState. there is no notificationBuilder. Ignoring request to update state!"); + return; + } + if (mPlaybackState.getPosition() >= 0) { + LogHelper.d(TAG, "updateNotificationPlaybackState. updating playback position to ", + (System.currentTimeMillis() - mPlaybackState.getPosition()) / 1000, " seconds"); + mNotificationBuilder + .setWhen(System.currentTimeMillis() - mPlaybackState.getPosition()) + .setShowWhen(true) + .setUsesChronometer(true); + mNotificationBuilder.setShowWhen(true); + } else { + LogHelper.d(TAG, "updateNotificationPlaybackState. hiding playback position"); + mNotificationBuilder + .setWhen(0) + .setShowWhen(false) + .setUsesChronometer(false); + } + + updatePlayPauseAction(); + + // Make sure that the notification can be dismissed by the user when we are not playing: + mNotificationBuilder.setOngoing(mPlaybackState.getState() == PlaybackState.STATE_PLAYING); + + mNotificationManager.notify(NOTIFICATION_ID, mNotificationBuilder.build()); + } + + public void fetchBitmapFromURLAsync(final String source) { + LogHelper.d(TAG, "getBitmapFromURLAsync: starting asynctask to fetch ", source); + new AsyncTask() { + @Override + protected Bitmap doInBackground(Void[] objects) { + Bitmap bitmap = null; + try { + bitmap = BitmapHelper.fetchAndRescaleBitmap(source, + BitmapHelper.MEDIA_ART_BIG_WIDTH, BitmapHelper.MEDIA_ART_BIG_HEIGHT); + mAlbumArtCache.put(source, bitmap); + } catch (IOException e) { + LogHelper.e(TAG, e, "getBitmapFromURLAsync: " + source); + } + return bitmap; + } + + @Override + protected void onPostExecute(Bitmap bitmap) { + if (bitmap != null && mMetadata != null && + mNotificationBuilder != null && mMetadata.getDescription() != null && + !source.equals(mMetadata.getDescription().getIconUri())) { + // If the media is still the same, update the notification: + LogHelper.d(TAG, "getBitmapFromURLAsync: set bitmap to ", source); + mNotificationBuilder.setLargeIcon(bitmap); + mNotificationManager.notify(NOTIFICATION_ID, mNotificationBuilder.build()); + } + } + }.execute(); + } + +} diff --git a/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/MusicPlayerActivity.java b/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/MusicPlayerActivity.java new file mode 100644 index 000000000..648d26896 --- /dev/null +++ b/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/MusicPlayerActivity.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.mediabrowserservice; + +import android.app.Activity; +import android.media.browse.MediaBrowser; +import android.media.session.MediaController; +import android.os.Bundle; + +/** + * Main activity for the music player. + */ +public class MusicPlayerActivity extends Activity + implements BrowseFragment.FragmentDataHelper { + + private static final String TAG = MusicPlayerActivity.class.getSimpleName(); + + private MediaBrowser mMediaBrowser; + private MediaController mMediaController; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_player); + if (savedInstanceState == null) { + getFragmentManager().beginTransaction() + .add(R.id.container, BrowseFragment.newInstance(null)) + .commit(); + } + } + + @Override + public void onMediaItemSelected(MediaBrowser.MediaItem item) { + if (item.isPlayable()) { + getMediaController().getTransportControls().playFromMediaId(item.getMediaId(), null); + QueueFragment queueFragment = QueueFragment.newInstance(); + getFragmentManager().beginTransaction() + .replace(R.id.container, queueFragment) + .addToBackStack(null) + .commit(); + } else if (item.isBrowsable()) { + getFragmentManager().beginTransaction() + .replace(R.id.container, BrowseFragment.newInstance(item.getMediaId())) + .addToBackStack(null) + .commit(); + } + } +} diff --git a/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/MusicService.java b/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/MusicService.java new file mode 100644 index 000000000..a7a9ae2e7 --- /dev/null +++ b/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/MusicService.java @@ -0,0 +1,936 @@ +/* + * Copyright (C) 2014 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.mediabrowserservice; + +import android.content.Context; +import android.content.Intent; +import android.media.AudioManager; +import android.media.MediaDescription; +import android.media.MediaMetadata; +import android.media.MediaPlayer; +import android.media.MediaPlayer.OnCompletionListener; +import android.media.MediaPlayer.OnErrorListener; +import android.media.MediaPlayer.OnPreparedListener; +import android.media.browse.MediaBrowser; +import android.media.browse.MediaBrowser.MediaItem; +import android.media.session.MediaSession; +import android.media.session.PlaybackState; +import android.net.Uri; +import android.net.wifi.WifiManager; +import android.net.wifi.WifiManager.WifiLock; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.PowerManager; +import android.os.SystemClock; +import android.service.media.MediaBrowserService; + +import com.example.android.mediabrowserservice.model.MusicProvider; +import com.example.android.mediabrowserservice.utils.LogHelper; +import com.example.android.mediabrowserservice.utils.MediaIDHelper; +import com.example.android.mediabrowserservice.utils.QueueHelper; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import static com.example.android.mediabrowserservice.utils.MediaIDHelper.MEDIA_ID_MUSICS_BY_GENRE; +import static com.example.android.mediabrowserservice.utils.MediaIDHelper.MEDIA_ID_ROOT; +import static com.example.android.mediabrowserservice.utils.MediaIDHelper.createBrowseCategoryMediaID; +import static com.example.android.mediabrowserservice.utils.MediaIDHelper.extractBrowseCategoryFromMediaID; + +/** + * This class provides a MediaBrowser through a service. It exposes the media library to a browsing + * client, through the onGetRoot and onLoadChildren methods. It also creates a MediaSession and + * exposes it through its MediaSession.Token, which allows the client to create a MediaController + * that connects to and send control commands to the MediaSession remotely. This is useful for + * user interfaces that need to interact with your media session, like Android Auto. You can + * (should) also use the same service from your app's UI, which gives a seamless playback + * experience to the user. + * + * To implement a MediaBrowserService, you need to: + * + *

    + * + *
  • Extend {@link android.service.media.MediaBrowserService}, implementing the media browsing + * related methods {@link android.service.media.MediaBrowserService#onGetRoot} and + * {@link android.service.media.MediaBrowserService#onLoadChildren}; + *
  • In onCreate, start a new {@link android.media.session.MediaSession} and notify its parent + * with the session's token {@link android.service.media.MediaBrowserService#setSessionToken}; + * + *
  • Set a callback on the + * {@link android.media.session.MediaSession#setCallback(android.media.session.MediaSession.Callback)}. + * The callback will receive all the user's actions, like play, pause, etc; + * + *
  • Handle all the actual music playing using any method your app prefers (for example, + * {@link android.media.MediaPlayer}) + * + *
  • Update playbackState, "now playing" metadata and queue, using MediaSession proper methods + * {@link android.media.session.MediaSession#setPlaybackState(android.media.session.PlaybackState)} + * {@link android.media.session.MediaSession#setMetadata(android.media.MediaMetadata)} and + * {@link android.media.session.MediaSession#setQueue(java.util.List)}) + * + *
  • Declare and export the service in AndroidManifest with an intent receiver for the action + * android.media.browse.MediaBrowserService + * + *
+ * + * To make your app compatible with Android Auto, you also need to: + * + *
    + * + *
  • Declare a meta-data tag in AndroidManifest.xml linking to a xml resource + * with a <automotiveApp> root element. For a media app, this must include + * an <uses name="media"/> element as a child. + * For example, in AndroidManifest.xml: + * <meta-data android:name="com.google.android.gms.car.application" + * android:resource="@xml/automotive_app_desc"/> + * And in res/values/automotive_app_desc.xml: + * <automotiveApp> + * <uses name="media"/> + * </automotiveApp> + * + *
+ + * @see README.md for more details. + * + */ + +public class MusicService extends MediaBrowserService implements OnPreparedListener, + OnCompletionListener, OnErrorListener, AudioManager.OnAudioFocusChangeListener { + + private static final String TAG = "MusicService"; + + // Action to thumbs up a media item + private static final String CUSTOM_ACTION_THUMBS_UP = "thumbs_up"; + // Delay stopSelf by using a handler. + private static final int STOP_DELAY = 30000; + + // The volume we set the media player to when we lose audio focus, but are + // allowed to reduce the volume instead of stopping playback. + public static final float VOLUME_DUCK = 0.2f; + + // The volume we set the media player when we have audio focus. + public static final float VOLUME_NORMAL = 1.0f; + public static final String ANDROID_AUTO_PACKAGE_NAME = "com.google.android.projection.gearhead"; + public static final String ANDROID_AUTO_EMULATOR_PACKAGE_NAME = "com.google.android.mediasimulator"; + + // Music catalog manager + private MusicProvider mMusicProvider; + + private MediaSession mSession; + private MediaPlayer mMediaPlayer; + + // "Now playing" queue: + private List mPlayingQueue; + private int mCurrentIndexOnQueue; + + // Current local media player state + private int mState = PlaybackState.STATE_NONE; + + // Wifi lock that we hold when streaming files from the internet, in order + // to prevent the device from shutting off the Wifi radio + private WifiLock mWifiLock; + + private MediaNotification mMediaNotification; + + // Indicates whether the service was started. + private boolean mServiceStarted; + + enum AudioFocus { + NoFocusNoDuck, // we don't have audio focus, and can't duck + NoFocusCanDuck, // we don't have focus, but can play at a low volume + // ("ducking") + Focused // we have full audio focus + } + + // Type of audio focus we have: + private AudioFocus mAudioFocus = AudioFocus.NoFocusNoDuck; + private AudioManager mAudioManager; + + // Indicates if we should start playing immediately after we gain focus. + private boolean mPlayOnFocusGain; + + private Handler mDelayedStopHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + if ((mMediaPlayer != null && mMediaPlayer.isPlaying()) || + mPlayOnFocusGain) { + LogHelper.d(TAG, "Ignoring delayed stop since the media player is in use."); + return; + } + LogHelper.d(TAG, "Stopping service with delay handler."); + stopSelf(); + mServiceStarted = false; + } + }; + + /* + * (non-Javadoc) + * @see android.app.Service#onCreate() + */ + @Override + public void onCreate() { + super.onCreate(); + LogHelper.d(TAG, "onCreate"); + + mPlayingQueue = new ArrayList<>(); + + // Create the Wifi lock (this does not acquire the lock, this just creates it) + mWifiLock = ((WifiManager) getSystemService(Context.WIFI_SERVICE)) + .createWifiLock(WifiManager.WIFI_MODE_FULL, "MusicDemo_lock"); + + + // Create the music catalog metadata provider + mMusicProvider = new MusicProvider(); + mMusicProvider.retrieveMedia(new MusicProvider.Callback() { + @Override + public void onMusicCatalogReady(boolean success) { + mState = success ? PlaybackState.STATE_NONE : PlaybackState.STATE_ERROR; + } + }); + + mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + + // Start a new MediaSession + mSession = new MediaSession(this, "MusicService"); + setSessionToken(mSession.getSessionToken()); + mSession.setCallback(new MediaSessionCallback()); + mSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS | + MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS); + + // Use these extras to reserve space for the corresponding actions, even when they are disabled + // in the playbackstate, so the custom actions don't reflow. + Bundle extras = new Bundle(); + extras.putBoolean( + "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_NEXT", + true); + extras.putBoolean( + "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_PREVIOUS", + true); + // If you want to reserve the Queue slot when there is no queue + // (mSession.setQueue(emptylist)), uncomment the lines below: + // extras.putBoolean( + // "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_QUEUE", + // true); + mSession.setExtras(extras); + + updatePlaybackState(null); + + mMediaNotification = new MediaNotification(this); + } + + /* + * (non-Javadoc) + * @see android.app.Service#onDestroy() + */ + @Override + public void onDestroy() { + LogHelper.d(TAG, "onDestroy"); + + // Service is being killed, so make sure we release our resources + handleStopRequest(null); + + mDelayedStopHandler.removeCallbacksAndMessages(null); + // In particular, always release the MediaSession to clean up resources + // and notify associated MediaController(s). + mSession.release(); + } + + + @Override + public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) { + LogHelper.d(TAG, "OnGetRoot: clientPackageName=" + clientPackageName, + "; clientUid=" + clientUid + " ; rootHints=", rootHints); + // To ensure you are not allowing any arbitrary app to browse your app's contents, you + // need to check the origin: + if (!ANDROID_AUTO_PACKAGE_NAME.equals(clientPackageName) && + !ANDROID_AUTO_EMULATOR_PACKAGE_NAME.equals(clientPackageName) && + !getApplication().getPackageName().equals(clientPackageName)) { + // If the request comes from an untrusted package, return null. No further calls will + // be made to other media browsing methods. + LogHelper.w(TAG, "OnGetRoot: IGNORING request from untrusted package " + clientPackageName); + return null; + } + if (ANDROID_AUTO_PACKAGE_NAME.equals(clientPackageName)) { + // Optional: if your app needs to adapt ads, music library or anything else that + // needs to run differently when connected to the car, this is where you should handle + // it. + } + return new BrowserRoot(MEDIA_ID_ROOT, null); + } + + @Override + public void onLoadChildren(final String parentMediaId, final Result> result) { + if (!mMusicProvider.isInitialized()) { + // Use result.detach to allow calling result.sendResult from another thread: + result.detach(); + + mMusicProvider.retrieveMedia(new MusicProvider.Callback() { + @Override + public void onMusicCatalogReady(boolean success) { + if (success) { + loadChildrenImpl(parentMediaId, result); + } else { + updatePlaybackState(getString(R.string.error_no_metadata)); + result.sendResult(new ArrayList()); + } + } + }); + + } else { + // If our music catalog is already loaded/cached, load them into result immediately + loadChildrenImpl(parentMediaId, result); + } + } + + /** + * Actual implementation of onLoadChildren that assumes that MusicProvider is already + * initialized. + */ + private void loadChildrenImpl(final String parentMediaId, + final Result> result) { + LogHelper.d(TAG, "OnLoadChildren: parentMediaId=", parentMediaId); + + List mediaItems = new ArrayList<>(); + + if (MEDIA_ID_ROOT.equals(parentMediaId)) { + LogHelper.d(TAG, "OnLoadChildren.ROOT"); + mediaItems.add(new MediaBrowser.MediaItem( + new MediaDescription.Builder() + .setMediaId(MEDIA_ID_MUSICS_BY_GENRE) + .setTitle(getString(R.string.browse_genres)) + .setIconUri(Uri.parse("android.resource://" + + "com.example.android.mediabrowserservice/drawable/ic_by_genre")) + .setSubtitle(getString(R.string.browse_genre_subtitle)) + .build(), MediaBrowser.MediaItem.FLAG_BROWSABLE + )); + + } else if (MEDIA_ID_MUSICS_BY_GENRE.equals(parentMediaId)) { + LogHelper.d(TAG, "OnLoadChildren.GENRES"); + for (String genre: mMusicProvider.getGenres()) { + MediaBrowser.MediaItem item = new MediaBrowser.MediaItem( + new MediaDescription.Builder() + .setMediaId(createBrowseCategoryMediaID(MEDIA_ID_MUSICS_BY_GENRE, genre)) + .setTitle(genre) + .setSubtitle(getString(R.string.browse_musics_by_genre_subtitle, genre)) + .build(), MediaBrowser.MediaItem.FLAG_BROWSABLE + ); + mediaItems.add(item); + } + + } else if (parentMediaId.startsWith(MEDIA_ID_MUSICS_BY_GENRE)) { + String genre = extractBrowseCategoryFromMediaID(parentMediaId)[1]; + LogHelper.d(TAG, "OnLoadChildren.SONGS_BY_GENRE genre=", genre); + for (MediaMetadata track: mMusicProvider.getMusicsByGenre(genre)) { + // Since mediaMetadata fields are immutable, we need to create a copy, so we + // can set a hierarchy-aware mediaID. We will need to know the media hierarchy + // when we get a onPlayFromMusicID call, so we can create the proper queue based + // on where the music was selected from (by artist, by genre, random, etc) + String hierarchyAwareMediaID = MediaIDHelper.createTrackMediaID( + MEDIA_ID_MUSICS_BY_GENRE, genre, track); + MediaMetadata trackCopy = new MediaMetadata.Builder(track) + .putString(MediaMetadata.METADATA_KEY_MEDIA_ID, hierarchyAwareMediaID) + .build(); + MediaBrowser.MediaItem bItem = new MediaBrowser.MediaItem( + trackCopy.getDescription(), MediaItem.FLAG_PLAYABLE); + mediaItems.add(bItem); + } + } else { + LogHelper.w(TAG, "Skipping unmatched parentMediaId: ", parentMediaId); + } + result.sendResult(mediaItems); + } + + + + private final class MediaSessionCallback extends MediaSession.Callback { + @Override + public void onPlay() { + LogHelper.d(TAG, "play"); + + if (mPlayingQueue == null || mPlayingQueue.isEmpty()) { + mPlayingQueue = QueueHelper.getRandomQueue(mMusicProvider); + mSession.setQueue(mPlayingQueue); + mSession.setQueueTitle(getString(R.string.random_queue_title)); + // start playing from the beginning of the queue + mCurrentIndexOnQueue = 0; + } + + if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) { + handlePlayRequest(); + } + } + + @Override + public void onSkipToQueueItem(long queueId) { + LogHelper.d(TAG, "OnSkipToQueueItem:" + queueId); + if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) { + + // set the current index on queue from the music Id: + mCurrentIndexOnQueue = QueueHelper.getMusicIndexOnQueue(mPlayingQueue, queueId); + + // play the music + handlePlayRequest(); + } + } + + @Override + public void onPlayFromMediaId(String mediaId, Bundle extras) { + LogHelper.d(TAG, "playFromMediaId mediaId:", mediaId, " extras=", extras); + + // The mediaId used here is not the unique musicId. This one comes from the + // MediaBrowser, and is actually a "hierarchy-aware mediaID": a concatenation of + // the hierarchy in MediaBrowser and the actual unique musicID. This is necessary + // so we can build the correct playing queue, based on where the track was + // selected from. + mPlayingQueue = QueueHelper.getPlayingQueue(mediaId, mMusicProvider); + mSession.setQueue(mPlayingQueue); + String queueTitle = getString(R.string.browse_musics_by_genre_subtitle, + MediaIDHelper.extractBrowseCategoryValueFromMediaID(mediaId)); + mSession.setQueueTitle(queueTitle); + + if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) { + String uniqueMusicID = MediaIDHelper.extractMusicIDFromMediaID(mediaId); + // set the current index on queue from the music Id: + mCurrentIndexOnQueue = QueueHelper.getMusicIndexOnQueue( + mPlayingQueue, uniqueMusicID); + + // play the music + handlePlayRequest(); + } + } + + @Override + public void onPause() { + LogHelper.d(TAG, "pause. current state=" + mState); + handlePauseRequest(); + } + + @Override + public void onStop() { + LogHelper.d(TAG, "stop. current state=" + mState); + handleStopRequest(null); + } + + @Override + public void onSkipToNext() { + LogHelper.d(TAG, "skipToNext"); + mCurrentIndexOnQueue++; + if (mPlayingQueue != null && mCurrentIndexOnQueue >= mPlayingQueue.size()) { + mCurrentIndexOnQueue = 0; + } + if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) { + mState = PlaybackState.STATE_PLAYING; + handlePlayRequest(); + } else { + LogHelper.e(TAG, "skipToNext: cannot skip to next. next Index=" + + mCurrentIndexOnQueue + " queue length=" + + (mPlayingQueue == null ? "null" : mPlayingQueue.size())); + handleStopRequest("Cannot skip"); + } + } + + @Override + public void onSkipToPrevious() { + LogHelper.d(TAG, "skipToPrevious"); + + mCurrentIndexOnQueue--; + if (mPlayingQueue != null && mCurrentIndexOnQueue < 0) { + // This sample's behavior: skipping to previous when in first song restarts the + // first song. + mCurrentIndexOnQueue = 0; + } + if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) { + mState = PlaybackState.STATE_PLAYING; + handlePlayRequest(); + } else { + LogHelper.e(TAG, "skipToPrevious: cannot skip to previous. previous Index=" + + mCurrentIndexOnQueue + " queue length=" + + (mPlayingQueue == null ? "null" : mPlayingQueue.size())); + handleStopRequest("Cannot skip"); + } + } + + @Override + public void onCustomAction(String action, Bundle extras) { + if (CUSTOM_ACTION_THUMBS_UP.equals(action)) { + LogHelper.i(TAG, "onCustomAction: favorite for current track"); + MediaMetadata track = getCurrentPlayingMusic(); + if (track != null) { + String mediaId = track.getString(MediaMetadata.METADATA_KEY_MEDIA_ID); + mMusicProvider.setFavorite(mediaId, !mMusicProvider.isFavorite(mediaId)); + } + updatePlaybackState(null); + } else { + LogHelper.e(TAG, "Unsupported action: ", action); + } + + } + + @Override + public void onPlayFromSearch(String query, Bundle extras) { + LogHelper.d(TAG, "playFromSearch query=", query); + + mPlayingQueue = QueueHelper.getPlayingQueueFromSearch(query, mMusicProvider); + LogHelper.d(TAG, "playFromSearch playqueue.length=" + mPlayingQueue.size()); + mSession.setQueue(mPlayingQueue); + + if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) { + + // start playing from the beginning of the queue + mCurrentIndexOnQueue = 0; + + handlePlayRequest(); + } + } + } + + + + /* + * Called when media player is done playing current song. + * @see android.media.MediaPlayer.OnCompletionListener + */ + @Override + public void onCompletion(MediaPlayer player) { + LogHelper.d(TAG, "onCompletion from MediaPlayer"); + // The media player finished playing the current song, so we go ahead + // and start the next. + if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) { + // In this sample, we restart the playing queue when it gets to the end: + mCurrentIndexOnQueue++; + if (mCurrentIndexOnQueue >= mPlayingQueue.size()) { + mCurrentIndexOnQueue = 0; + } + handlePlayRequest(); + } else { + // If there is nothing to play, we stop and release the resources: + handleStopRequest(null); + } + } + + /* + * Called when media player is done preparing. + * @see android.media.MediaPlayer.OnPreparedListener + */ + @Override + public void onPrepared(MediaPlayer player) { + LogHelper.d(TAG, "onPrepared from MediaPlayer"); + // The media player is done preparing. That means we can start playing if we + // have audio focus. + configMediaPlayerState(); + } + + /** + * Called when there's an error playing media. When this happens, the media + * player goes to the Error state. We warn the user about the error and + * reset the media player. + * + * @see android.media.MediaPlayer.OnErrorListener + */ + @Override + public boolean onError(MediaPlayer mp, int what, int extra) { + LogHelper.e(TAG, "Media player error: what=" + what + ", extra=" + extra); + handleStopRequest("MediaPlayer error " + what + " (" + extra + ")"); + return true; // true indicates we handled the error + } + + + + + /** + * Called by AudioManager on audio focus changes. + */ + @Override + public void onAudioFocusChange(int focusChange) { + LogHelper.d(TAG, "onAudioFocusChange. focusChange=" + focusChange); + if (focusChange == AudioManager.AUDIOFOCUS_GAIN) { + // We have gained focus: + mAudioFocus = AudioFocus.Focused; + + } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS || + focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT || + focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) { + // We have lost focus. If we can duck (low playback volume), we can keep playing. + // Otherwise, we need to pause the playback. + boolean canDuck = focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK; + mAudioFocus = canDuck ? AudioFocus.NoFocusCanDuck : AudioFocus.NoFocusNoDuck; + + // If we are playing, we need to reset media player by calling configMediaPlayerState + // with mAudioFocus properly set. + if (mState == PlaybackState.STATE_PLAYING && !canDuck) { + // If we don't have audio focus and can't duck, we save the information that + // we were playing, so that we can resume playback once we get the focus back. + mPlayOnFocusGain = true; + } + } else { + LogHelper.e(TAG, "onAudioFocusChange: Ignoring unsupported focusChange: " + focusChange); + } + + configMediaPlayerState(); + } + + + + /** + * Handle a request to play music + */ + private void handlePlayRequest() { + LogHelper.d(TAG, "handlePlayRequest: mState=" + mState); + + mDelayedStopHandler.removeCallbacksAndMessages(null); + if (!mServiceStarted) { + LogHelper.v(TAG, "Starting service"); + // The MusicService needs to keep running even after the calling MediaBrowser + // is disconnected. Call startService(Intent) and then stopSelf(..) when we no longer + // need to play media. + startService(new Intent(getApplicationContext(), MusicService.class)); + mServiceStarted = true; + } + + mPlayOnFocusGain = true; + tryToGetAudioFocus(); + + if (!mSession.isActive()) { + mSession.setActive(true); + } + + // actually play the song + if (mState == PlaybackState.STATE_PAUSED) { + // If we're paused, just continue playback and restore the + // 'foreground service' state. + configMediaPlayerState(); + } else { + // If we're stopped or playing a song, + // just go ahead to the new song and (re)start playing + playCurrentSong(); + } + } + + + /** + * Handle a request to pause music + */ + private void handlePauseRequest() { + LogHelper.d(TAG, "handlePauseRequest: mState=" + mState); + + if (mState == PlaybackState.STATE_PLAYING) { + // Pause media player and cancel the 'foreground service' state. + mState = PlaybackState.STATE_PAUSED; + if (mMediaPlayer.isPlaying()) { + mMediaPlayer.pause(); + } + // while paused, retain the MediaPlayer but give up audio focus + relaxResources(false); + giveUpAudioFocus(); + } + updatePlaybackState(null); + } + + /** + * Handle a request to stop music + */ + private void handleStopRequest(String withError) { + LogHelper.d(TAG, "handleStopRequest: mState=" + mState + " error=", withError); + mState = PlaybackState.STATE_STOPPED; + + // let go of all resources... + relaxResources(true); + giveUpAudioFocus(); + updatePlaybackState(withError); + + mMediaNotification.stopNotification(); + + // service is no longer necessary. Will be started again if needed. + stopSelf(); + mServiceStarted = false; + } + + /** + * Releases resources used by the service for playback. This includes the + * "foreground service" status, the wake locks and possibly the MediaPlayer. + * + * @param releaseMediaPlayer Indicates whether the Media Player should also + * be released or not + */ + private void relaxResources(boolean releaseMediaPlayer) { + LogHelper.d(TAG, "relaxResources. releaseMediaPlayer=" + releaseMediaPlayer); + // stop being a foreground service + stopForeground(true); + + // reset the delayed stop handler. + mDelayedStopHandler.removeCallbacksAndMessages(null); + mDelayedStopHandler.sendEmptyMessageDelayed(0, STOP_DELAY); + + // stop and release the Media Player, if it's available + if (releaseMediaPlayer && mMediaPlayer != null) { + mMediaPlayer.reset(); + mMediaPlayer.release(); + mMediaPlayer = null; + } + + // we can also release the Wifi lock, if we're holding it + if (mWifiLock.isHeld()) { + mWifiLock.release(); + } + } + + /** + * Reconfigures MediaPlayer according to audio focus settings and + * starts/restarts it. This method starts/restarts the MediaPlayer + * respecting the current audio focus state. So if we have focus, it will + * play normally; if we don't have focus, it will either leave the + * MediaPlayer paused or set it to a low volume, depending on what is + * allowed by the current focus settings. This method assumes mPlayer != + * null, so if you are calling it, you have to do so from a context where + * you are sure this is the case. + */ + private void configMediaPlayerState() { + LogHelper.d(TAG, "configAndStartMediaPlayer. mAudioFocus=" + mAudioFocus); + if (mAudioFocus == AudioFocus.NoFocusNoDuck) { + // If we don't have audio focus and can't duck, we have to pause, + if (mState == PlaybackState.STATE_PLAYING) { + handlePauseRequest(); + } + } else { // we have audio focus: + if (mAudioFocus == AudioFocus.NoFocusCanDuck) { + mMediaPlayer.setVolume(VOLUME_DUCK, VOLUME_DUCK); // we'll be relatively quiet + } else { + mMediaPlayer.setVolume(VOLUME_NORMAL, VOLUME_NORMAL); // we can be loud again + } + // If we were playing when we lost focus, we need to resume playing. + if (mPlayOnFocusGain) { + if (!mMediaPlayer.isPlaying()) { + LogHelper.d(TAG, "configAndStartMediaPlayer startMediaPlayer."); + mMediaPlayer.start(); + } + mPlayOnFocusGain = false; + mState = PlaybackState.STATE_PLAYING; + } + } + updatePlaybackState(null); + } + + /** + * Makes sure the media player exists and has been reset. This will create + * the media player if needed, or reset the existing media player if one + * already exists. + */ + private void createMediaPlayerIfNeeded() { + LogHelper.d(TAG, "createMediaPlayerIfNeeded. needed? " + (mMediaPlayer==null)); + if (mMediaPlayer == null) { + mMediaPlayer = new MediaPlayer(); + + // Make sure the media player will acquire a wake-lock while + // playing. If we don't do that, the CPU might go to sleep while the + // song is playing, causing playback to stop. + mMediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK); + + // we want the media player to notify us when it's ready preparing, + // and when it's done playing: + mMediaPlayer.setOnPreparedListener(this); + mMediaPlayer.setOnCompletionListener(this); + mMediaPlayer.setOnErrorListener(this); + } else { + mMediaPlayer.reset(); + } + } + + /** + * Starts playing the current song in the playing queue. + */ + void playCurrentSong() { + MediaMetadata track = getCurrentPlayingMusic(); + if (track == null) { + LogHelper.e(TAG, "playSong: ignoring request to play next song, because cannot" + + " find it." + + " currentIndex=" + mCurrentIndexOnQueue + + " playQueue.size=" + (mPlayingQueue==null?"null": mPlayingQueue.size())); + return; + } + String source = track.getString(MusicProvider.CUSTOM_METADATA_TRACK_SOURCE); + LogHelper.d(TAG, "playSong: current (" + mCurrentIndexOnQueue + ") in playingQueue. " + + " musicId=" + track.getString(MediaMetadata.METADATA_KEY_MEDIA_ID) + + " source=" + source); + + mState = PlaybackState.STATE_STOPPED; + relaxResources(false); // release everything except MediaPlayer + + try { + createMediaPlayerIfNeeded(); + + mState = PlaybackState.STATE_BUFFERING; + + mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); + mMediaPlayer.setDataSource(source); + + // Starts preparing the media player in the background. When + // it's done, it will call our OnPreparedListener (that is, + // the onPrepared() method on this class, since we set the + // listener to 'this'). Until the media player is prepared, + // we *cannot* call start() on it! + mMediaPlayer.prepareAsync(); + + // If we are streaming from the internet, we want to hold a + // Wifi lock, which prevents the Wifi radio from going to + // sleep while the song is playing. + mWifiLock.acquire(); + + updatePlaybackState(null); + updateMetadata(); + + } catch (IOException ex) { + LogHelper.e(TAG, ex, "IOException playing song"); + updatePlaybackState(ex.getMessage()); + } + } + + + + private void updateMetadata() { + if (!QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) { + LogHelper.e(TAG, "Can't retrieve current metadata."); + mState = PlaybackState.STATE_ERROR; + updatePlaybackState(getResources().getString(R.string.error_no_metadata)); + return; + } + MediaSession.QueueItem queueItem = mPlayingQueue.get(mCurrentIndexOnQueue); + String mediaId = queueItem.getDescription().getMediaId(); + MediaMetadata track = mMusicProvider.getMusic(mediaId); + String trackId = track.getString(MediaMetadata.METADATA_KEY_MEDIA_ID); + if (!mediaId.equals(trackId)) { + throw new IllegalStateException("track ID (" + trackId + ") " + + "should match mediaId (" + mediaId + ")"); + } + LogHelper.d(TAG, "Updating metadata for MusicID= " + mediaId); + mSession.setMetadata(track); + } + + + /** + * Update the current media player state, optionally showing an error message. + * + * @param error if not null, error message to present to the user. + * + */ + private void updatePlaybackState(String error) { + + LogHelper.d(TAG, "updatePlaybackState, setting session playback state to " + mState); + long position = PlaybackState.PLAYBACK_POSITION_UNKNOWN; + if (mMediaPlayer != null && mMediaPlayer.isPlaying()) { + position = mMediaPlayer.getCurrentPosition(); + } + PlaybackState.Builder stateBuilder = new PlaybackState.Builder() + .setActions(getAvailableActions()); + + setCustomAction(stateBuilder); + + // If there is an error message, send it to the playback state: + if (error != null) { + // Error states are really only supposed to be used for errors that cause playback to + // stop unexpectedly and persist until the user takes action to fix it. + stateBuilder.setErrorMessage(error); + mState = PlaybackState.STATE_ERROR; + } + stateBuilder.setState(mState, position, 1.0f, SystemClock.elapsedRealtime()); + + // Set the activeQueueItemId if the current index is valid. + if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) { + MediaSession.QueueItem item = mPlayingQueue.get(mCurrentIndexOnQueue); + stateBuilder.setActiveQueueItemId(item.getQueueId()); + } + + mSession.setPlaybackState(stateBuilder.build()); + + if (mState == PlaybackState.STATE_PLAYING || mState == PlaybackState.STATE_PAUSED) { + mMediaNotification.startNotification(); + } + } + + private void setCustomAction(PlaybackState.Builder stateBuilder) { + MediaMetadata currentMusic = getCurrentPlayingMusic(); + if (currentMusic != null) { + // Set appropriate "Favorite" icon on Custom action: + String mediaId = currentMusic.getString(MediaMetadata.METADATA_KEY_MEDIA_ID); + int favoriteIcon = R.drawable.ic_star_off; + if (mMusicProvider.isFavorite(mediaId)) { + favoriteIcon = R.drawable.ic_star_on; + } + LogHelper.d(TAG, "updatePlaybackState, setting Favorite custom action of music ", + mediaId, " current favorite=", mMusicProvider.isFavorite(mediaId)); + stateBuilder.addCustomAction(CUSTOM_ACTION_THUMBS_UP, getString(R.string.favorite), + favoriteIcon); + } + } + + private long getAvailableActions() { + long actions = PlaybackState.ACTION_PLAY | PlaybackState.ACTION_PLAY_FROM_MEDIA_ID | + PlaybackState.ACTION_PLAY_FROM_SEARCH; + if (mPlayingQueue == null || mPlayingQueue.isEmpty()) { + return actions; + } + if (mState == PlaybackState.STATE_PLAYING) { + actions |= PlaybackState.ACTION_PAUSE; + } + if (mCurrentIndexOnQueue > 0) { + actions |= PlaybackState.ACTION_SKIP_TO_PREVIOUS; + } + if (mCurrentIndexOnQueue < mPlayingQueue.size() - 1) { + actions |= PlaybackState.ACTION_SKIP_TO_NEXT; + } + return actions; + } + + private MediaMetadata getCurrentPlayingMusic() { + if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) { + MediaSession.QueueItem item = mPlayingQueue.get(mCurrentIndexOnQueue); + if (item != null) { + LogHelper.d(TAG, "getCurrentPlayingMusic for musicId=", + item.getDescription().getMediaId()); + return mMusicProvider.getMusic(item.getDescription().getMediaId()); + } + } + return null; + } + + /** + * Try to get the system audio focus. + */ + void tryToGetAudioFocus() { + LogHelper.d(TAG, "tryToGetAudioFocus"); + if (mAudioFocus != AudioFocus.Focused) { + int result = mAudioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, + AudioManager.AUDIOFOCUS_GAIN); + if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { + mAudioFocus = AudioFocus.Focused; + } + } + } + + /** + * Give up the audio focus. + */ + void giveUpAudioFocus() { + LogHelper.d(TAG, "giveUpAudioFocus"); + if (mAudioFocus == AudioFocus.Focused) { + if (mAudioManager.abandonAudioFocus(this) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { + mAudioFocus = AudioFocus.NoFocusNoDuck; + } + } + } +} diff --git a/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/QueueAdapter.java b/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/QueueAdapter.java new file mode 100644 index 000000000..4f24e9944 --- /dev/null +++ b/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/QueueAdapter.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.mediabrowserservice; + +import android.app.Activity; +import android.media.session.MediaSession; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.TextView; + +import java.util.ArrayList; + +/** + * A list adapter for items in a queue + */ +public class QueueAdapter extends ArrayAdapter { + + // The currently selected/active queue item Id. + private long mActiveQueueItemId = MediaSession.QueueItem.UNKNOWN_ID; + + public QueueAdapter(Activity context) { + super(context, R.layout.media_list_item, new ArrayList()); + } + + public void setActiveQueueItemId(long id) { + this.mActiveQueueItemId = id; + } + + private static class ViewHolder { + ImageView mImageView; + TextView mTitleView; + TextView mDescriptionView; + } + + public View getView(int position, View convertView, ViewGroup parent) { + ViewHolder holder; + + if (convertView == null) { + convertView = LayoutInflater.from(getContext()) + .inflate(R.layout.media_list_item, parent, false); + holder = new ViewHolder(); + holder.mImageView = (ImageView) convertView.findViewById(R.id.play_eq); + holder.mTitleView = (TextView) convertView.findViewById(R.id.title); + holder.mDescriptionView = (TextView) convertView.findViewById(R.id.description); + convertView.setTag(holder); + } else { + holder = (ViewHolder) convertView.getTag(); + } + + MediaSession.QueueItem item = getItem(position); + holder.mTitleView.setText(item.getDescription().getTitle()); + if (item.getDescription().getDescription() != null) { + holder.mDescriptionView.setText(item.getDescription().getDescription()); + } + + // If the itemId matches the active Id then use a different icon + if (mActiveQueueItemId == item.getQueueId()) { + holder.mImageView.setImageDrawable( + getContext().getDrawable(R.drawable.ic_equalizer_white_24dp)); + } else { + holder.mImageView.setImageDrawable( + getContext().getDrawable(R.drawable.ic_play_arrow_white_24dp)); + } + return convertView; + } +} diff --git a/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/QueueFragment.java b/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/QueueFragment.java new file mode 100644 index 000000000..f6076bc88 --- /dev/null +++ b/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/QueueFragment.java @@ -0,0 +1,295 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.mediabrowserservice; + +import android.app.Fragment; +import android.content.ComponentName; +import android.media.browse.MediaBrowser; +import android.media.session.MediaController; +import android.media.session.MediaSession; +import android.media.session.PlaybackState; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ImageButton; +import android.widget.ListView; + +import com.example.android.mediabrowserservice.utils.LogHelper; + +import java.util.List; + +/** + * A class that shows the Media Queue to the user. + */ +public class QueueFragment extends Fragment { + + private static final String TAG = QueueFragment.class.getSimpleName(); + + private ImageButton mSkipNext; + private ImageButton mSkipPrevious; + private ImageButton mPlayPause; + + private MediaBrowser mMediaBrowser; + private MediaController.TransportControls mTransportControls; + private MediaController mMediaController; + private PlaybackState mPlaybackState; + + private QueueAdapter mQueueAdapter; + + private MediaBrowser.ConnectionCallback mConnectionCallback = + new MediaBrowser.ConnectionCallback() { + @Override + public void onConnected() { + LogHelper.d(TAG, "onConnected: session token ", mMediaBrowser.getSessionToken()); + + if (mMediaBrowser.getSessionToken() == null) { + throw new IllegalArgumentException("No Session token"); + } + + mMediaController = new MediaController(getActivity(), + mMediaBrowser.getSessionToken()); + mTransportControls = mMediaController.getTransportControls(); + mMediaController.registerCallback(mSessionCallback); + + getActivity().setMediaController(mMediaController); + mPlaybackState = mMediaController.getPlaybackState(); + + List queue = mMediaController.getQueue(); + if (queue != null) { + mQueueAdapter.clear(); + mQueueAdapter.notifyDataSetInvalidated(); + mQueueAdapter.addAll(queue); + mQueueAdapter.notifyDataSetChanged(); + } + onPlaybackStateChanged(mPlaybackState); + } + + @Override + public void onConnectionFailed() { + LogHelper.d(TAG, "onConnectionFailed"); + } + + @Override + public void onConnectionSuspended() { + LogHelper.d(TAG, "onConnectionSuspended"); + mMediaController.unregisterCallback(mSessionCallback); + mTransportControls = null; + mMediaController = null; + getActivity().setMediaController(null); + } + }; + + // Receive callbacks from the MediaController. Here we update our state such as which queue + // is being shown, the current title and description and the PlaybackState. + private MediaController.Callback mSessionCallback = new MediaController.Callback() { + + @Override + public void onSessionDestroyed() { + LogHelper.d(TAG, "Session destroyed. Need to fetch a new Media Session"); + } + + @Override + public void onPlaybackStateChanged(PlaybackState state) { + if (state == null) { + return; + } + LogHelper.d(TAG, "Received playback state change to state ", state.getState()); + mPlaybackState = state; + QueueFragment.this.onPlaybackStateChanged(state); + } + + @Override + public void onQueueChanged(List queue) { + LogHelper.d(TAG, "onQueueChanged ", queue); + if (queue != null) { + mQueueAdapter.clear(); + mQueueAdapter.notifyDataSetInvalidated(); + mQueueAdapter.addAll(queue); + mQueueAdapter.notifyDataSetChanged(); + } + } + }; + + public static QueueFragment newInstance() { + return new QueueFragment(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.fragment_list, container, false); + + mSkipPrevious = (ImageButton) rootView.findViewById(R.id.skip_previous); + mSkipPrevious.setEnabled(false); + mSkipPrevious.setOnClickListener(mButtonListener); + + mSkipNext = (ImageButton) rootView.findViewById(R.id.skip_next); + mSkipNext.setEnabled(false); + mSkipNext.setOnClickListener(mButtonListener); + + mPlayPause = (ImageButton) rootView.findViewById(R.id.play_pause); + mPlayPause.setEnabled(true); + mPlayPause.setOnClickListener(mButtonListener); + + mQueueAdapter = new QueueAdapter(getActivity()); + + ListView mListView = (ListView) rootView.findViewById(R.id.list_view); + mListView.setAdapter(mQueueAdapter); + mListView.setFocusable(true); + mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + MediaSession.QueueItem item = mQueueAdapter.getItem(position); + mTransportControls.skipToQueueItem(item.getQueueId()); + } + }); + + mMediaBrowser = new MediaBrowser(getActivity(), + new ComponentName(getActivity(), MusicService.class), + mConnectionCallback, null); + + return rootView; + } + + @Override + public void onResume() { + super.onResume(); + if (mMediaBrowser != null) { + mMediaBrowser.connect(); + } + } + + @Override + public void onPause() { + super.onPause(); + if (mMediaController != null) { + mMediaController.unregisterCallback(mSessionCallback); + } + if (mMediaBrowser != null) { + mMediaBrowser.disconnect(); + } + } + + + private void onPlaybackStateChanged(PlaybackState state) { + LogHelper.d(TAG, "onPlaybackStateChanged ", state); + if (state == null) { + return; + } + mQueueAdapter.setActiveQueueItemId(state.getActiveQueueItemId()); + mQueueAdapter.notifyDataSetChanged(); + boolean enablePlay = false; + StringBuilder statusBuilder = new StringBuilder(); + switch (state.getState()) { + case PlaybackState.STATE_PLAYING: + statusBuilder.append("playing"); + enablePlay = false; + break; + case PlaybackState.STATE_PAUSED: + statusBuilder.append("paused"); + enablePlay = true; + break; + case PlaybackState.STATE_STOPPED: + statusBuilder.append("ended"); + enablePlay = true; + break; + case PlaybackState.STATE_ERROR: + statusBuilder.append("error: ").append(state.getErrorMessage()); + break; + case PlaybackState.STATE_BUFFERING: + statusBuilder.append("buffering"); + break; + case PlaybackState.STATE_NONE: + statusBuilder.append("none"); + enablePlay = false; + break; + case PlaybackState.STATE_CONNECTING: + statusBuilder.append("connecting"); + break; + default: + statusBuilder.append(mPlaybackState); + } + statusBuilder.append(" -- At position: ").append(state.getPosition()); + LogHelper.d(TAG, statusBuilder.toString()); + + if (enablePlay) { + mPlayPause.setImageDrawable( + getActivity().getDrawable(R.drawable.ic_play_arrow_white_24dp)); + } else { + mPlayPause.setImageDrawable(getActivity().getDrawable(R.drawable.ic_pause_white_24dp)); + } + + mSkipPrevious.setEnabled((state.getActions() & PlaybackState.ACTION_SKIP_TO_PREVIOUS) != 0); + mSkipNext.setEnabled((state.getActions() & PlaybackState.ACTION_SKIP_TO_NEXT) != 0); + + LogHelper.d(TAG, "Queue From MediaController *** Title " + + mMediaController.getQueueTitle() + "\n: Queue: " + mMediaController.getQueue() + + "\n Metadata " + mMediaController.getMetadata()); + } + + private View.OnClickListener mButtonListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + final int state = mPlaybackState == null ? + PlaybackState.STATE_NONE : mPlaybackState.getState(); + switch (v.getId()) { + case R.id.play_pause: + LogHelper.d(TAG, "Play button pressed, in state " + state); + if (state == PlaybackState.STATE_PAUSED || + state == PlaybackState.STATE_STOPPED || + state == PlaybackState.STATE_NONE) { + playMedia(); + } else if (state == PlaybackState.STATE_PLAYING) { + pauseMedia(); + } + break; + case R.id.skip_previous: + LogHelper.d(TAG, "Start button pressed, in state " + state); + skipToPrevious(); + break; + case R.id.skip_next: + skipToNext(); + break; + } + } + }; + + private void playMedia() { + if (mTransportControls != null) { + mTransportControls.play(); + } + } + + private void pauseMedia() { + if (mTransportControls != null) { + mTransportControls.pause(); + } + } + + private void skipToPrevious() { + if (mTransportControls != null) { + mTransportControls.skipToPrevious(); + } + } + + private void skipToNext() { + if (mTransportControls != null) { + mTransportControls.skipToNext(); + } + } +} diff --git a/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/model/MusicProvider.java b/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/model/MusicProvider.java new file mode 100644 index 000000000..ae90fb092 --- /dev/null +++ b/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/model/MusicProvider.java @@ -0,0 +1,296 @@ +/* + * Copyright (C) 2014 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.mediabrowserservice.model; + +import android.media.MediaMetadata; +import android.os.AsyncTask; + +import com.example.android.mediabrowserservice.utils.LogHelper; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URLConnection; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Utility class to get a list of MusicTrack's based on a server-side JSON + * configuration. + */ +public class MusicProvider { + + private static final String TAG = "MusicProvider"; + + private static final String CATALOG_URL = "http://storage.googleapis.com/automotive-media/music.json"; + + public static final String CUSTOM_METADATA_TRACK_SOURCE = "__SOURCE__"; + + private static String JSON_MUSIC = "music"; + private static String JSON_TITLE = "title"; + private static String JSON_ALBUM = "album"; + private static String JSON_ARTIST = "artist"; + private static String JSON_GENRE = "genre"; + private static String JSON_SOURCE = "source"; + private static String JSON_IMAGE = "image"; + private static String JSON_TRACK_NUMBER = "trackNumber"; + private static String JSON_TOTAL_TRACK_COUNT = "totalTrackCount"; + private static String JSON_DURATION = "duration"; + + private final ReentrantLock initializationLock = new ReentrantLock(); + + // Categorized caches for music track data: + private final HashMap> mMusicListByGenre; + private final HashMap mMusicListById; + + private final HashSet mFavoriteTracks; + + enum State { + NON_INITIALIZED, INITIALIZING, INITIALIZED; + } + + private State mCurrentState = State.NON_INITIALIZED; + + + public interface Callback { + void onMusicCatalogReady(boolean success); + } + + public MusicProvider() { + mMusicListByGenre = new HashMap<>(); + mMusicListById = new HashMap<>(); + mFavoriteTracks = new HashSet<>(); + } + + /** + * Get an iterator over the list of genres + * + * @return + */ + public Iterable getGenres() { + if (mCurrentState != State.INITIALIZED) { + return new ArrayList(0); + } + return mMusicListByGenre.keySet(); + } + + /** + * Get music tracks of the given genre + * + * @return + */ + public Iterable getMusicsByGenre(String genre) { + if (mCurrentState != State.INITIALIZED || !mMusicListByGenre.containsKey(genre)) { + return new ArrayList(); + } + return mMusicListByGenre.get(genre); + } + + /** + * Very basic implementation of a search that filter music tracks which title containing + * the given query. + * + * @return + */ + public Iterable searchMusics(String titleQuery) { + ArrayList result = new ArrayList<>(); + if (mCurrentState != State.INITIALIZED) { + return result; + } + titleQuery = titleQuery.toLowerCase(); + for (MediaMetadata track: mMusicListById.values()) { + if (track.getString(MediaMetadata.METADATA_KEY_TITLE).toLowerCase() + .contains(titleQuery)) { + result.add(track); + } + } + return result; + } + + public MediaMetadata getMusic(String mediaId) { + return mMusicListById.get(mediaId); + } + + public void setFavorite(String mediaId, boolean favorite) { + if (favorite) { + mFavoriteTracks.add(mediaId); + } else { + mFavoriteTracks.remove(mediaId); + } + } + + public boolean isFavorite(String musicId) { + return mFavoriteTracks.contains(musicId); + } + + public boolean isInitialized() { + return mCurrentState == State.INITIALIZED; + } + + /** + * Get the list of music tracks from a server and caches the track information + * for future reference, keying tracks by mediaId and grouping by genre. + * + * @return + */ + public void retrieveMedia(final Callback callback) { + + if (mCurrentState == State.INITIALIZED) { + // Nothing to do, execute callback immediately + callback.onMusicCatalogReady(true); + return; + } + + // Asynchronously load the music catalog in a separate thread + new AsyncTask() { + @Override + protected Object doInBackground(Object[] objects) { + retrieveMediaAsync(callback); + return null; + } + }.execute(); + } + + private void retrieveMediaAsync(Callback callback) { + initializationLock.lock(); + + try { + if (mCurrentState == State.NON_INITIALIZED) { + mCurrentState = State.INITIALIZING; + + int slashPos = CATALOG_URL.lastIndexOf('/'); + String path = CATALOG_URL.substring(0, slashPos + 1); + JSONObject jsonObj = parseUrl(CATALOG_URL); + + JSONArray tracks = jsonObj.getJSONArray(JSON_MUSIC); + if (tracks != null) { + for (int j = 0; j < tracks.length(); j++) { + MediaMetadata item = buildFromJSON(tracks.getJSONObject(j), path); + String genre = item.getString(MediaMetadata.METADATA_KEY_GENRE); + List list = mMusicListByGenre.get(genre); + if (list == null) { + list = new ArrayList<>(); + } + list.add(item); + mMusicListByGenre.put(genre, list); + mMusicListById.put(item.getString(MediaMetadata.METADATA_KEY_MEDIA_ID), + item); + } + } + mCurrentState = State.INITIALIZED; + } + } catch (RuntimeException | JSONException e) { + LogHelper.e(TAG, e, "Could not retrieve music list"); + } finally { + if (mCurrentState != State.INITIALIZED) { + // Something bad happened, so we reset state to NON_INITIALIZED to allow + // retries (eg if the network connection is temporary unavailable) + mCurrentState = State.NON_INITIALIZED; + } + initializationLock.unlock(); + if (callback != null) { + callback.onMusicCatalogReady(mCurrentState == State.INITIALIZED); + } + } + } + + private MediaMetadata buildFromJSON(JSONObject json, String basePath) throws JSONException { + String title = json.getString(JSON_TITLE); + String album = json.getString(JSON_ALBUM); + String artist = json.getString(JSON_ARTIST); + String genre = json.getString(JSON_GENRE); + String source = json.getString(JSON_SOURCE); + String iconUrl = json.getString(JSON_IMAGE); + int trackNumber = json.getInt(JSON_TRACK_NUMBER); + int totalTrackCount = json.getInt(JSON_TOTAL_TRACK_COUNT); + int duration = json.getInt(JSON_DURATION) * 1000; // ms + + LogHelper.d(TAG, "Found music track: ", json); + + // Media is stored relative to JSON file + if (!source.startsWith("http")) { + source = basePath + source; + } + if (!iconUrl.startsWith("http")) { + iconUrl = basePath + iconUrl; + } + // Since we don't have a unique ID in the server, we fake one using the hashcode of + // the music source. In a real world app, this could come from the server. + String id = String.valueOf(source.hashCode()); + + // Adding the music source to the MediaMetadata (and consequently using it in the + // mediaSession.setMetadata) is not a good idea for a real world music app, because + // the session metadata can be accessed by notification listeners. This is done in this + // sample for convenience only. + return new MediaMetadata.Builder() + .putString(MediaMetadata.METADATA_KEY_MEDIA_ID, id) + .putString(CUSTOM_METADATA_TRACK_SOURCE, source) + .putString(MediaMetadata.METADATA_KEY_ALBUM, album) + .putString(MediaMetadata.METADATA_KEY_ARTIST, artist) + .putLong(MediaMetadata.METADATA_KEY_DURATION, duration) + .putString(MediaMetadata.METADATA_KEY_GENRE, genre) + .putString(MediaMetadata.METADATA_KEY_ALBUM_ART_URI, iconUrl) + .putString(MediaMetadata.METADATA_KEY_TITLE, title) + .putLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER, trackNumber) + .putLong(MediaMetadata.METADATA_KEY_NUM_TRACKS, totalTrackCount) + .build(); + } + + /** + * Download a JSON file from a server, parse the content and return the JSON + * object. + * + * @param urlString + * @return + */ + private JSONObject parseUrl(String urlString) { + InputStream is = null; + try { + java.net.URL url = new java.net.URL(urlString); + URLConnection urlConnection = url.openConnection(); + is = new BufferedInputStream(urlConnection.getInputStream()); + BufferedReader reader = new BufferedReader(new InputStreamReader( + urlConnection.getInputStream(), "iso-8859-1")); + StringBuilder sb = new StringBuilder(); + String line = null; + while ((line = reader.readLine()) != null) { + sb.append(line); + } + return new JSONObject(sb.toString()); + } catch (Exception e) { + LogHelper.e(TAG, "Failed to parse the json for media list", e); + return null; + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + // ignore + } + } + } + } +} \ No newline at end of file diff --git a/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/utils/BitmapHelper.java b/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/utils/BitmapHelper.java new file mode 100644 index 000000000..5f0e76754 --- /dev/null +++ b/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/utils/BitmapHelper.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.mediabrowserservice.utils; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; + +public class BitmapHelper { + + // Bitmap size for album art in media notifications when there are more than 3 playback actions + public static final int MEDIA_ART_SMALL_WIDTH=64; + public static final int MEDIA_ART_SMALL_HEIGHT=64; + + // Bitmap size for album art in media notifications when there are no more than 3 playback actions + public static final int MEDIA_ART_BIG_WIDTH=128; + public static final int MEDIA_ART_BIG_HEIGHT=128; + + public static final Bitmap scaleBitmap(int scaleFactor, InputStream is) { + // Get the dimensions of the bitmap + BitmapFactory.Options bmOptions = new BitmapFactory.Options(); + + // Decode the image file into a Bitmap sized to fill the View + bmOptions.inJustDecodeBounds = false; + bmOptions.inSampleSize = scaleFactor; + + Bitmap bitmap = BitmapFactory.decodeStream(is, null, bmOptions); + return bitmap; + } + + public static final int findScaleFactor(int targetW, int targetH, InputStream is) { + // Get the dimensions of the bitmap + BitmapFactory.Options bmOptions = new BitmapFactory.Options(); + bmOptions.inJustDecodeBounds = true; + BitmapFactory.decodeStream(is, null, bmOptions); + int actualW = bmOptions.outWidth; + int actualH = bmOptions.outHeight; + + // Determine how much to scale down the image + return Math.min(actualW/targetW, actualH/targetH); + } + + public static final Bitmap fetchAndRescaleBitmap(String uri, int width, int height) + throws IOException { + URL url = new URL(uri); + HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection(); + httpConnection.setDoInput(true); + httpConnection.connect(); + InputStream inputStream = httpConnection.getInputStream(); + int scaleFactor = findScaleFactor(width, height, inputStream); + + httpConnection = (HttpURLConnection) url.openConnection(); + httpConnection.setDoInput(true); + httpConnection.connect(); + inputStream = httpConnection.getInputStream(); + Bitmap bitmap = scaleBitmap(scaleFactor, inputStream); + return bitmap; + } + +} diff --git a/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/utils/LogHelper.java b/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/utils/LogHelper.java new file mode 100644 index 000000000..92b2e099e --- /dev/null +++ b/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/utils/LogHelper.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.mediabrowserservice.utils; + +import android.util.Log; + +public class LogHelper { + public static void v(String tag, Object... messages) { + log(tag, Log.VERBOSE, null, messages); + } + + public static void d(String tag, Object... messages) { + log(tag, Log.DEBUG, null, messages); + } + + public static void i(String tag, Object... messages) { + log(tag, Log.INFO, null, messages); + } + + public static void w(String tag, Object... messages) { + log(tag, Log.WARN, null, messages); + } + + public static void w(String tag, Throwable t, Object... messages) { + log(tag, Log.WARN, t, messages); + } + + public static void e(String tag, Object... messages) { + log(tag, Log.ERROR, null, messages); + } + + public static void e(String tag, Throwable t, Object... messages) { + log(tag, Log.ERROR, t, messages); + } + + public static void log(String tag, int level, Throwable t, Object... messages) { + if (messages != null && Log.isLoggable(tag, level)) { + String message; + if (messages.length == 1) { + message = messages[0] == null ? null : messages[0].toString(); + } else { + StringBuilder sb = new StringBuilder(); + for (Object m: messages) { + sb.append(m); + } + if (t != null) { + sb.append("\n").append(Log.getStackTraceString(t)); + } + message = sb.toString(); + } + Log.println(level, tag, message); + } + } +} diff --git a/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/utils/MediaIDHelper.java b/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/utils/MediaIDHelper.java new file mode 100644 index 000000000..68e6db9e3 --- /dev/null +++ b/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/utils/MediaIDHelper.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2014 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.mediabrowserservice.utils; + +import android.media.MediaMetadata; + +/** + * Utility class to help on queue related tasks. + */ +public class MediaIDHelper { + + private static final String TAG = "MediaIDHelper"; + + // Media IDs used on browseable items of MediaBrowser + public static final String MEDIA_ID_ROOT = "__ROOT__"; + public static final String MEDIA_ID_MUSICS_BY_GENRE = "__BY_GENRE__"; + + public static final String createTrackMediaID(String categoryType, String categoryValue, + MediaMetadata track) { + // MediaIDs are of the form /|, to make it easy to + // find the category (like genre) that a music was selected from, so we + // can correctly build the playing queue. This is specially useful when + // one music can appear in more than one list, like "by genre -> genre_1" + // and "by artist -> artist_1". + return categoryType + "/" + categoryValue + "|" + + track.getString(MediaMetadata.METADATA_KEY_MEDIA_ID); + } + + public static final String createBrowseCategoryMediaID(String categoryType, String categoryValue) { + return categoryType + "/" + categoryValue; + } + + /** + * Extracts unique musicID from the mediaID. mediaID is, by this sample's convention, a + * concatenation of category (eg "by_genre"), categoryValue (eg "Classical") and unique + * musicID. This is necessary so we know where the user selected the music from, when the music + * exists in more than one music list, and thus we are able to correctly build the playing queue. + * + * @param musicID + * @return + */ + public static final String extractMusicIDFromMediaID(String musicID) { + String[] segments = musicID.split("\\|", 2); + return segments.length == 2 ? segments[1] : null; + } + + /** + * Extracts category and categoryValue from the mediaID. mediaID is, by this sample's + * convention, a concatenation of category (eg "by_genre"), categoryValue (eg "Classical") and + * mediaID. This is necessary so we know where the user selected the music from, when the music + * exists in more than one music list, and thus we are able to correctly build the playing queue. + * + * @param mediaID + * @return + */ + public static final String[] extractBrowseCategoryFromMediaID(String mediaID) { + if (mediaID.indexOf('|') >= 0) { + mediaID = mediaID.split("\\|")[0]; + } + if (mediaID.indexOf('/') == 0) { + return new String[]{mediaID, null}; + } else { + return mediaID.split("/", 2); + } + } + + public static final String extractBrowseCategoryValueFromMediaID(String mediaID) { + String[] categoryAndValue = extractBrowseCategoryFromMediaID(mediaID); + if (categoryAndValue != null && categoryAndValue.length == 2) { + return categoryAndValue[1]; + } + return null; + } +} \ No newline at end of file diff --git a/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/utils/QueueHelper.java b/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/utils/QueueHelper.java new file mode 100644 index 000000000..abe3d34cd --- /dev/null +++ b/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/utils/QueueHelper.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2014 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.mediabrowserservice.utils; + +import android.media.MediaMetadata; +import android.media.session.MediaSession; + +import com.example.android.mediabrowserservice.model.MusicProvider; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import static com.example.android.mediabrowserservice.utils.MediaIDHelper.MEDIA_ID_MUSICS_BY_GENRE; + +/** + * Utility class to help on queue related tasks. + */ +public class QueueHelper { + + private static final String TAG = "QueueHelper"; + + public static final List getPlayingQueue(String mediaId, + MusicProvider musicProvider) { + + // extract the category and unique music ID from the media ID: + String[] category = MediaIDHelper.extractBrowseCategoryFromMediaID(mediaId); + + // This sample only supports genre category. + if (!category[0].equals(MEDIA_ID_MUSICS_BY_GENRE) || category.length != 2) { + LogHelper.e(TAG, "Could not build a playing queue for this mediaId: ", mediaId); + return null; + } + + String categoryValue = category[1]; + LogHelper.e(TAG, "Creating playing queue for musics of genre ", categoryValue); + + List queue = convertToQueue( + musicProvider.getMusicsByGenre(categoryValue)); + + return queue; + } + + public static final List getPlayingQueueFromSearch(String query, + MusicProvider musicProvider) { + + LogHelper.e(TAG, "Creating playing queue for musics from search ", query); + + return convertToQueue(musicProvider.searchMusics(query)); + } + + + public static final int getMusicIndexOnQueue(Iterable queue, + String mediaId) { + int index = 0; + for (MediaSession.QueueItem item: queue) { + if (mediaId.equals(item.getDescription().getMediaId())) { + return index; + } + index++; + } + return -1; + } + + public static final int getMusicIndexOnQueue(Iterable queue, + long queueId) { + int index = 0; + for (MediaSession.QueueItem item: queue) { + if (queueId == item.getQueueId()) { + return index; + } + index++; + } + return -1; + } + + private static final List convertToQueue( + Iterable tracks) { + List queue = new ArrayList<>(); + int count = 0; + for (MediaMetadata track : tracks) { + // We don't expect queues to change after created, so we use the item index as the + // queueId. Any other number unique in the queue would work. + MediaSession.QueueItem item = new MediaSession.QueueItem( + track.getDescription(), count++); + queue.add(item); + } + return queue; + + } + + /** + * Create a random queue. For simplicity sake, instead of a random queue, we create a + * queue using the first genre, + * + * @param musicProvider + * @return + */ + public static final List getRandomQueue(MusicProvider musicProvider) { + Iterator genres = musicProvider.getGenres().iterator(); + if (!genres.hasNext()) { + return new ArrayList<>(); + } + String genre = genres.next(); + Iterable tracks = musicProvider.getMusicsByGenre(genre); + + return convertToQueue(tracks); + } + + + + public static final boolean isIndexPlayable(int index, List queue) { + return (queue != null && index >= 0 && index < queue.size()); + } +} diff --git a/samples/browseable/MediaEffects/AndroidManifest.xml b/samples/browseable/MediaEffects/AndroidManifest.xml new file mode 100644 index 000000000..556f3c196 --- /dev/null +++ b/samples/browseable/MediaEffects/AndroidManifest.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + diff --git a/samples/browseable/MediaEffects/_index.jd b/samples/browseable/MediaEffects/_index.jd new file mode 100644 index 000000000..76448c36d --- /dev/null +++ b/samples/browseable/MediaEffects/_index.jd @@ -0,0 +1,12 @@ +page.tags="MediaEffects" +sample.group=Media +@jd:body + +

+ + This sample shows how to use the Media Effects APIs that were introduced in Android 4.0. + These APIs let you apply effects to image frames represented as OpenGL ES 2.0 textures. + Image frames can be images loaded from disk, frames from the device\'s camera, or other + video streams. + +

diff --git a/samples/browseable/MediaEffects/res/drawable-hdpi/ic_launcher.png b/samples/browseable/MediaEffects/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..960dc8eb18e7af511dc0d62b57d5120808f00265 GIT binary patch literal 4781 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 Hu0mjfL=zEu literal 0 HcmV?d00001 diff --git a/samples/browseable/NavigationDrawer/res/drawable-xhdpi/sample_dashboard_item_background.9.png b/samples/browseable/MediaEffects/res/drawable-hdpi/tile.9.png similarity index 100% rename from samples/browseable/NavigationDrawer/res/drawable-xhdpi/sample_dashboard_item_background.9.png rename to samples/browseable/MediaEffects/res/drawable-hdpi/tile.9.png diff --git a/samples/browseable/MediaEffects/res/drawable-mdpi/ic_launcher.png b/samples/browseable/MediaEffects/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..9356f268e7eb695518f99cd34c0879553dd6b341 GIT binary patch literal 2872 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 literal 0 HcmV?d00001 diff --git a/samples/browseable/MediaEffects/res/drawable-xhdpi/ic_launcher.png b/samples/browseable/MediaEffects/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..230d5584d5b9d427494e166876f1e35467f5c8c9 GIT binary patch literal 6655 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 literal 0 HcmV?d00001 diff --git a/samples/browseable/MediaEffects/res/layout-w720dp/activity_main.xml b/samples/browseable/MediaEffects/res/layout-w720dp/activity_main.xml new file mode 100755 index 000000000..c9a52f621 --- /dev/null +++ b/samples/browseable/MediaEffects/res/layout-w720dp/activity_main.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/browseable/MediaEffects/res/layout/activity_main.xml b/samples/browseable/MediaEffects/res/layout/activity_main.xml new file mode 100755 index 000000000..1ae4f981e --- /dev/null +++ b/samples/browseable/MediaEffects/res/layout/activity_main.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/samples/browseable/MediaEffects/res/layout/fragment_media_effects.xml b/samples/browseable/MediaEffects/res/layout/fragment_media_effects.xml new file mode 100644 index 000000000..4fb1ce1bc --- /dev/null +++ b/samples/browseable/MediaEffects/res/layout/fragment_media_effects.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/samples/browseable/MediaEffects/res/menu/main.xml b/samples/browseable/MediaEffects/res/menu/main.xml new file mode 100644 index 000000000..b49c2c526 --- /dev/null +++ b/samples/browseable/MediaEffects/res/menu/main.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/samples/browseable/MediaEffects/res/menu/media_effects.xml b/samples/browseable/MediaEffects/res/menu/media_effects.xml new file mode 100644 index 000000000..c37a9ac6b --- /dev/null +++ b/samples/browseable/MediaEffects/res/menu/media_effects.xml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/browseable/MediaEffects/res/values-sw600dp/template-dimens.xml b/samples/browseable/MediaEffects/res/values-sw600dp/template-dimens.xml new file mode 100644 index 000000000..22074a2bd --- /dev/null +++ b/samples/browseable/MediaEffects/res/values-sw600dp/template-dimens.xml @@ -0,0 +1,24 @@ + + + + + + + @dimen/margin_huge + @dimen/margin_medium + + diff --git a/samples/browseable/MediaEffects/res/values-sw600dp/template-styles.xml b/samples/browseable/MediaEffects/res/values-sw600dp/template-styles.xml new file mode 100644 index 000000000..03d197418 --- /dev/null +++ b/samples/browseable/MediaEffects/res/values-sw600dp/template-styles.xml @@ -0,0 +1,25 @@ + + + + + + + diff --git a/samples/browseable/MediaEffects/res/values-v11/template-styles.xml b/samples/browseable/MediaEffects/res/values-v11/template-styles.xml new file mode 100644 index 000000000..8c1ea66f2 --- /dev/null +++ b/samples/browseable/MediaEffects/res/values-v11/template-styles.xml @@ -0,0 +1,22 @@ + + + + + + + + + + diff --git a/samples/browseable/DataLayer/Application/src/com.example.android.common/activities/SampleActivityBase.java b/samples/browseable/MediaEffects/src/com.example.android.common/activities/SampleActivityBase.java similarity index 100% rename from samples/browseable/DataLayer/Application/src/com.example.android.common/activities/SampleActivityBase.java rename to samples/browseable/MediaEffects/src/com.example.android.common/activities/SampleActivityBase.java diff --git a/samples/browseable/DataLayer/Application/src/com.example.android.common/logger/Log.java b/samples/browseable/MediaEffects/src/com.example.android.common/logger/Log.java similarity index 100% rename from samples/browseable/DataLayer/Application/src/com.example.android.common/logger/Log.java rename to samples/browseable/MediaEffects/src/com.example.android.common/logger/Log.java diff --git a/samples/browseable/DataLayer/Application/src/com.example.android.common/logger/LogFragment.java b/samples/browseable/MediaEffects/src/com.example.android.common/logger/LogFragment.java similarity index 100% rename from samples/browseable/DataLayer/Application/src/com.example.android.common/logger/LogFragment.java rename to samples/browseable/MediaEffects/src/com.example.android.common/logger/LogFragment.java diff --git a/samples/browseable/DataLayer/Application/src/com.example.android.common/logger/LogNode.java b/samples/browseable/MediaEffects/src/com.example.android.common/logger/LogNode.java similarity index 100% rename from samples/browseable/DataLayer/Application/src/com.example.android.common/logger/LogNode.java rename to samples/browseable/MediaEffects/src/com.example.android.common/logger/LogNode.java diff --git a/samples/browseable/DataLayer/Application/src/com.example.android.common/logger/LogView.java b/samples/browseable/MediaEffects/src/com.example.android.common/logger/LogView.java similarity index 100% rename from samples/browseable/DataLayer/Application/src/com.example.android.common/logger/LogView.java rename to samples/browseable/MediaEffects/src/com.example.android.common/logger/LogView.java diff --git a/samples/browseable/DataLayer/Application/src/com.example.android.common/logger/LogWrapper.java b/samples/browseable/MediaEffects/src/com.example.android.common/logger/LogWrapper.java similarity index 100% rename from samples/browseable/DataLayer/Application/src/com.example.android.common/logger/LogWrapper.java rename to samples/browseable/MediaEffects/src/com.example.android.common/logger/LogWrapper.java diff --git a/samples/browseable/DataLayer/Application/src/com.example.android.common/logger/MessageOnlyLogFilter.java b/samples/browseable/MediaEffects/src/com.example.android.common/logger/MessageOnlyLogFilter.java similarity index 100% rename from samples/browseable/DataLayer/Application/src/com.example.android.common/logger/MessageOnlyLogFilter.java rename to samples/browseable/MediaEffects/src/com.example.android.common/logger/MessageOnlyLogFilter.java diff --git a/samples/browseable/MediaEffects/src/com.example.android.mediaeffects/GLToolbox.java b/samples/browseable/MediaEffects/src/com.example.android.mediaeffects/GLToolbox.java new file mode 100644 index 000000000..02a8c590d --- /dev/null +++ b/samples/browseable/MediaEffects/src/com.example.android.mediaeffects/GLToolbox.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.mediaeffects; + +import android.opengl.GLES20; + +public class GLToolbox { + + public static int loadShader(int shaderType, String source) { + int shader = GLES20.glCreateShader(shaderType); + if (shader != 0) { + GLES20.glShaderSource(shader, source); + GLES20.glCompileShader(shader); + int[] compiled = new int[1]; + GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0); + if (compiled[0] == 0) { + String info = GLES20.glGetShaderInfoLog(shader); + GLES20.glDeleteShader(shader); + throw new RuntimeException("Could not compile shader " + shaderType + ":" + info); + } + } + return shader; + } + + public static int createProgram(String vertexSource, String fragmentSource) { + int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource); + if (vertexShader == 0) { + return 0; + } + int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource); + if (pixelShader == 0) { + return 0; + } + + int program = GLES20.glCreateProgram(); + if (program != 0) { + GLES20.glAttachShader(program, vertexShader); + checkGlError("glAttachShader"); + GLES20.glAttachShader(program, pixelShader); + checkGlError("glAttachShader"); + GLES20.glLinkProgram(program); + int[] linkStatus = new int[1]; + GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, + 0); + if (linkStatus[0] != GLES20.GL_TRUE) { + String info = GLES20.glGetProgramInfoLog(program); + GLES20.glDeleteProgram(program); + throw new RuntimeException("Could not link program: " + info); + } + } + return program; + } + + public static void checkGlError(String op) { + int error; + while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) { + throw new RuntimeException(op + ": glError " + error); + } + } + + public static void initTexParams() { + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, + GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, + GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, + GLES20.GL_CLAMP_TO_EDGE); + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, + GLES20.GL_CLAMP_TO_EDGE); + } + +} diff --git a/samples/browseable/MediaEffects/src/com.example.android.mediaeffects/MainActivity.java b/samples/browseable/MediaEffects/src/com.example.android.mediaeffects/MainActivity.java new file mode 100644 index 000000000..be6224310 --- /dev/null +++ b/samples/browseable/MediaEffects/src/com.example.android.mediaeffects/MainActivity.java @@ -0,0 +1,109 @@ +/* +* Copyright 2013 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +package com.example.android.mediaeffects; + +import android.os.Bundle; +import android.support.v4.app.FragmentTransaction; +import android.view.Menu; +import android.view.MenuItem; +import android.widget.ViewAnimator; + +import com.example.android.common.activities.SampleActivityBase; +import com.example.android.common.logger.Log; +import com.example.android.common.logger.LogFragment; +import com.example.android.common.logger.LogWrapper; +import com.example.android.common.logger.MessageOnlyLogFilter; + +/** + * A simple launcher activity containing a summary sample description, sample log and a custom + * {@link android.support.v4.app.Fragment} which can display a view. + *

+ * For devices with displays with a width of 720dp or greater, the sample log is always visible, + * on other devices it's visibility is controlled by an item on the Action Bar. + */ +public class MainActivity extends SampleActivityBase { + + public static final String TAG = "MainActivity"; + + // Whether the Log Fragment is currently shown + private boolean mLogShown; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + if (savedInstanceState == null) { + FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); + MediaEffectsFragment fragment = new MediaEffectsFragment(); + transaction.replace(R.id.sample_content_fragment, fragment); + transaction.commit(); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.main, menu); + return true; + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + MenuItem logToggle = menu.findItem(R.id.menu_toggle_log); + logToggle.setVisible(findViewById(R.id.sample_output) instanceof ViewAnimator); + logToggle.setTitle(mLogShown ? R.string.sample_hide_log : R.string.sample_show_log); + + return super.onPrepareOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch(item.getItemId()) { + case R.id.menu_toggle_log: + mLogShown = !mLogShown; + ViewAnimator output = (ViewAnimator) findViewById(R.id.sample_output); + if (mLogShown) { + output.setDisplayedChild(1); + } else { + output.setDisplayedChild(0); + } + supportInvalidateOptionsMenu(); + return true; + } + return super.onOptionsItemSelected(item); + } + + /** Create a chain of targets that will receive log data */ + @Override + public void initializeLogging() { + // Wraps Android's native log framework. + LogWrapper logWrapper = new LogWrapper(); + // Using Log, front-end to the logging chain, emulates android.util.log method signatures. + Log.setLogNode(logWrapper); + + // Filter strips out everything except the message text. + MessageOnlyLogFilter msgFilter = new MessageOnlyLogFilter(); + logWrapper.setNext(msgFilter); + + // On screen logging via a fragment with a TextView. + LogFragment logFragment = (LogFragment) getSupportFragmentManager() + .findFragmentById(R.id.log_fragment); + msgFilter.setNext(logFragment.getLogView()); + + Log.i(TAG, "Ready"); + } +} diff --git a/samples/browseable/MediaEffects/src/com.example.android.mediaeffects/MediaEffectsFragment.java b/samples/browseable/MediaEffects/src/com.example.android.mediaeffects/MediaEffectsFragment.java new file mode 100644 index 000000000..5af16845f --- /dev/null +++ b/samples/browseable/MediaEffects/src/com.example.android.mediaeffects/MediaEffectsFragment.java @@ -0,0 +1,287 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.mediaeffects; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Color; +import android.media.effect.Effect; +import android.media.effect.EffectContext; +import android.media.effect.EffectFactory; +import android.opengl.GLES20; +import android.opengl.GLSurfaceView; +import android.opengl.GLUtils; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; + +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; + +public class MediaEffectsFragment extends Fragment implements GLSurfaceView.Renderer { + + private static final String STATE_CURRENT_EFFECT = "current_effect"; + + private GLSurfaceView mEffectView; + private int[] mTextures = new int[2]; + private EffectContext mEffectContext; + private Effect mEffect; + private TextureRenderer mTexRenderer = new TextureRenderer(); + private int mImageWidth; + private int mImageHeight; + private boolean mInitialized = false; + private int mCurrentEffect; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_media_effects, container, false); + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + mEffectView = (GLSurfaceView) view.findViewById(R.id.effectsview); + mEffectView.setEGLContextClientVersion(2); + mEffectView.setRenderer(this); + mEffectView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); + if (null != savedInstanceState && savedInstanceState.containsKey(STATE_CURRENT_EFFECT)) { + setCurrentEffect(savedInstanceState.getInt(STATE_CURRENT_EFFECT)); + } else { + setCurrentEffect(R.id.none); + } + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.media_effects, menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + setCurrentEffect(item.getItemId()); + mEffectView.requestRender(); + return true; + } + + @Override + public void onSaveInstanceState(Bundle outState) { + outState.putInt(STATE_CURRENT_EFFECT, mCurrentEffect); + } + + @Override + public void onSurfaceCreated(GL10 gl, EGLConfig eglConfig) { + // Nothing to do here + } + + @Override + public void onSurfaceChanged(GL10 gl, int width, int height) { + if (mTexRenderer != null) { + mTexRenderer.updateViewSize(width, height); + } + } + + @Override + public void onDrawFrame(GL10 gl) { + if (!mInitialized) { + //Only need to do this once + mEffectContext = EffectContext.createWithCurrentGlContext(); + mTexRenderer.init(); + loadTextures(); + mInitialized = true; + } + if (mCurrentEffect != R.id.none) { + //if an effect is chosen initialize it and apply it to the texture + initEffect(); + applyEffect(); + } + renderResult(); + } + + private void setCurrentEffect(int effect) { + mCurrentEffect = effect; + } + + private void loadTextures() { + // Generate textures + GLES20.glGenTextures(2, mTextures, 0); + + // Load input bitmap + Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.puppy); + mImageWidth = bitmap.getWidth(); + mImageHeight = bitmap.getHeight(); + mTexRenderer.updateTextureSize(mImageWidth, mImageHeight); + + // Upload to texture + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextures[0]); + GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0); + + // Set texture parameters + GLToolbox.initTexParams(); + } + + private void initEffect() { + EffectFactory effectFactory = mEffectContext.getFactory(); + if (mEffect != null) { + mEffect.release(); + } + // Initialize the correct effect based on the selected menu/action item + switch (mCurrentEffect) { + + case R.id.none: + break; + + case R.id.autofix: + mEffect = effectFactory.createEffect(EffectFactory.EFFECT_AUTOFIX); + mEffect.setParameter("scale", 0.5f); + break; + + case R.id.bw: + mEffect = effectFactory.createEffect(EffectFactory.EFFECT_BLACKWHITE); + mEffect.setParameter("black", .1f); + mEffect.setParameter("white", .7f); + break; + + case R.id.brightness: + mEffect = effectFactory.createEffect(EffectFactory.EFFECT_BRIGHTNESS); + mEffect.setParameter("brightness", 2.0f); + break; + + case R.id.contrast: + mEffect = effectFactory.createEffect(EffectFactory.EFFECT_CONTRAST); + mEffect.setParameter("contrast", 1.4f); + break; + + case R.id.crossprocess: + mEffect = effectFactory.createEffect(EffectFactory.EFFECT_CROSSPROCESS); + break; + + case R.id.documentary: + mEffect = effectFactory.createEffect(EffectFactory.EFFECT_DOCUMENTARY); + break; + + case R.id.duotone: + mEffect = effectFactory.createEffect(EffectFactory.EFFECT_DUOTONE); + mEffect.setParameter("first_color", Color.YELLOW); + mEffect.setParameter("second_color", Color.DKGRAY); + break; + + case R.id.filllight: + mEffect = effectFactory.createEffect(EffectFactory.EFFECT_FILLLIGHT); + mEffect.setParameter("strength", .8f); + break; + + case R.id.fisheye: + mEffect = effectFactory.createEffect(EffectFactory.EFFECT_FISHEYE); + mEffect.setParameter("scale", .5f); + break; + + case R.id.flipvert: + mEffect = effectFactory.createEffect(EffectFactory.EFFECT_FLIP); + mEffect.setParameter("vertical", true); + break; + + case R.id.fliphor: + mEffect = effectFactory.createEffect(EffectFactory.EFFECT_FLIP); + mEffect.setParameter("horizontal", true); + break; + + case R.id.grain: + mEffect = effectFactory.createEffect(EffectFactory.EFFECT_GRAIN); + mEffect.setParameter("strength", 1.0f); + break; + + case R.id.grayscale: + mEffect = effectFactory.createEffect(EffectFactory.EFFECT_GRAYSCALE); + break; + + case R.id.lomoish: + mEffect = effectFactory.createEffect(EffectFactory.EFFECT_LOMOISH); + break; + + case R.id.negative: + mEffect = effectFactory.createEffect(EffectFactory.EFFECT_NEGATIVE); + break; + + case R.id.posterize: + mEffect = effectFactory.createEffect(EffectFactory.EFFECT_POSTERIZE); + break; + + case R.id.rotate: + mEffect = effectFactory.createEffect(EffectFactory.EFFECT_ROTATE); + mEffect.setParameter("angle", 180); + break; + + case R.id.saturate: + mEffect = effectFactory.createEffect(EffectFactory.EFFECT_SATURATE); + mEffect.setParameter("scale", .5f); + break; + + case R.id.sepia: + mEffect = effectFactory.createEffect(EffectFactory.EFFECT_SEPIA); + break; + + case R.id.sharpen: + mEffect = effectFactory.createEffect(EffectFactory.EFFECT_SHARPEN); + break; + + case R.id.temperature: + mEffect = effectFactory.createEffect(EffectFactory.EFFECT_TEMPERATURE); + mEffect.setParameter("scale", .9f); + break; + + case R.id.tint: + mEffect = effectFactory.createEffect(EffectFactory.EFFECT_TINT); + mEffect.setParameter("tint", Color.MAGENTA); + break; + + case R.id.vignette: + mEffect = effectFactory.createEffect(EffectFactory.EFFECT_VIGNETTE); + mEffect.setParameter("scale", .5f); + break; + + default: + break; + } + } + + private void applyEffect() { + mEffect.apply(mTextures[0], mImageWidth, mImageHeight, mTextures[1]); + } + + private void renderResult() { + if (mCurrentEffect != R.id.none) { + // if no effect is chosen, just render the original bitmap + mTexRenderer.renderTexture(mTextures[1]); + } else { + // render the result of applyEffect() + mTexRenderer.renderTexture(mTextures[0]); + } + } + +} diff --git a/samples/browseable/MediaEffects/src/com.example.android.mediaeffects/TextureRenderer.java b/samples/browseable/MediaEffects/src/com.example.android.mediaeffects/TextureRenderer.java new file mode 100644 index 000000000..9c77927d2 --- /dev/null +++ b/samples/browseable/MediaEffects/src/com.example.android.mediaeffects/TextureRenderer.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.mediaeffects; + +import android.opengl.GLES20; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; + +public class TextureRenderer { + + private int mProgram; + private int mTexSamplerHandle; + private int mTexCoordHandle; + private int mPosCoordHandle; + + private FloatBuffer mTexVertices; + private FloatBuffer mPosVertices; + + private int mViewWidth; + private int mViewHeight; + + private int mTexWidth; + private int mTexHeight; + + private static final String VERTEX_SHADER = + "attribute vec4 a_position;\n" + + "attribute vec2 a_texcoord;\n" + + "varying vec2 v_texcoord;\n" + + "void main() {\n" + + " gl_Position = a_position;\n" + + " v_texcoord = a_texcoord;\n" + + "}\n"; + + private static final String FRAGMENT_SHADER = + "precision mediump float;\n" + + "uniform sampler2D tex_sampler;\n" + + "varying vec2 v_texcoord;\n" + + "void main() {\n" + + " gl_FragColor = texture2D(tex_sampler, v_texcoord);\n" + + "}\n"; + + private static final float[] TEX_VERTICES = { + 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f + }; + + private static final float[] POS_VERTICES = { + -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f + }; + + private static final int FLOAT_SIZE_BYTES = 4; + + public void init() { + // Create program + mProgram = GLToolbox.createProgram(VERTEX_SHADER, FRAGMENT_SHADER); + + // Bind attributes and uniforms + mTexSamplerHandle = GLES20.glGetUniformLocation(mProgram, + "tex_sampler"); + mTexCoordHandle = GLES20.glGetAttribLocation(mProgram, "a_texcoord"); + mPosCoordHandle = GLES20.glGetAttribLocation(mProgram, "a_position"); + + // Setup coordinate buffers + mTexVertices = ByteBuffer.allocateDirect( + TEX_VERTICES.length * FLOAT_SIZE_BYTES) + .order(ByteOrder.nativeOrder()).asFloatBuffer(); + mTexVertices.put(TEX_VERTICES).position(0); + mPosVertices = ByteBuffer.allocateDirect( + POS_VERTICES.length * FLOAT_SIZE_BYTES) + .order(ByteOrder.nativeOrder()).asFloatBuffer(); + mPosVertices.put(POS_VERTICES).position(0); + } + + public void tearDown() { + GLES20.glDeleteProgram(mProgram); + } + + public void updateTextureSize(int texWidth, int texHeight) { + mTexWidth = texWidth; + mTexHeight = texHeight; + computeOutputVertices(); + } + + public void updateViewSize(int viewWidth, int viewHeight) { + mViewWidth = viewWidth; + mViewHeight = viewHeight; + computeOutputVertices(); + } + + public void renderTexture(int texId) { + // Bind default FBO + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0); + + // Use our shader program + GLES20.glUseProgram(mProgram); + GLToolbox.checkGlError("glUseProgram"); + + // Set viewport + GLES20.glViewport(0, 0, mViewWidth, mViewHeight); + GLToolbox.checkGlError("glViewport"); + + // Disable blending + GLES20.glDisable(GLES20.GL_BLEND); + + // Set the vertex attributes + GLES20.glVertexAttribPointer(mTexCoordHandle, 2, GLES20.GL_FLOAT, false, + 0, mTexVertices); + GLES20.glEnableVertexAttribArray(mTexCoordHandle); + GLES20.glVertexAttribPointer(mPosCoordHandle, 2, GLES20.GL_FLOAT, false, + 0, mPosVertices); + GLES20.glEnableVertexAttribArray(mPosCoordHandle); + GLToolbox.checkGlError("vertex attribute setup"); + + // Set the input texture + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + GLToolbox.checkGlError("glActiveTexture"); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texId); + GLToolbox.checkGlError("glBindTexture"); + GLES20.glUniform1i(mTexSamplerHandle, 0); + + // Draw + GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + } + + private void computeOutputVertices() { + if (mPosVertices != null) { + float imgAspectRatio = mTexWidth / (float)mTexHeight; + float viewAspectRatio = mViewWidth / (float)mViewHeight; + float relativeAspectRatio = viewAspectRatio / imgAspectRatio; + float x0, y0, x1, y1; + if (relativeAspectRatio > 1.0f) { + x0 = -1.0f / relativeAspectRatio; + y0 = -1.0f; + x1 = 1.0f / relativeAspectRatio; + y1 = 1.0f; + } else { + x0 = -1.0f; + y0 = -relativeAspectRatio; + x1 = 1.0f; + y1 = relativeAspectRatio; + } + float[] coords = new float[] { x0, y0, x1, y0, x0, y1, x1, y1 }; + mPosVertices.put(coords).position(0); + } + } + +} diff --git a/samples/browseable/MediaRecorder/AndroidManifest.xml b/samples/browseable/MediaRecorder/AndroidManifest.xml index 32f88f64f..539dc2c3d 100644 --- a/samples/browseable/MediaRecorder/AndroidManifest.xml +++ b/samples/browseable/MediaRecorder/AndroidManifest.xml @@ -22,9 +22,7 @@ android:versionCode="1" android:versionName="1.0"> - + diff --git a/samples/browseable/MediaRecorder/_index.jd b/samples/browseable/MediaRecorder/_index.jd index dac835aa8..28c55901f 100644 --- a/samples/browseable/MediaRecorder/_index.jd +++ b/samples/browseable/MediaRecorder/_index.jd @@ -2,6 +2,10 @@ page.tags="MediaRecorder" sample.group=Media @jd:body -

This sample demonstrates how to use the {@link android.media.MediaRecorder} -API to record video from a camera or camcorder, and display a preview of the -recording.

+

+ + This sample uses the camera/camcorder as the A/V source for the MediaRecorder API. + A TextureView is used as the camera preview which limits the code to API 14+. This + can be easily replaced with a SurfaceView to run on older devices. + +

diff --git a/samples/browseable/MediaRecorder/res/values-v21/template-styles.xml b/samples/browseable/MediaRecorder/res/values-v21/template-styles.xml new file mode 100644 index 000000000..134fcd9d3 --- /dev/null +++ b/samples/browseable/MediaRecorder/res/values-v21/template-styles.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/samples/browseable/MessagingService/res/values/colors.xml b/samples/browseable/MessagingService/res/values/colors.xml new file mode 100644 index 000000000..0e6825b0e --- /dev/null +++ b/samples/browseable/MessagingService/res/values/colors.xml @@ -0,0 +1,20 @@ + + + + #ff4092c3 + #ff241c99 + diff --git a/samples/browseable/MessagingService/res/values/dimens.xml b/samples/browseable/MessagingService/res/values/dimens.xml new file mode 100644 index 000000000..574a35d20 --- /dev/null +++ b/samples/browseable/MessagingService/res/values/dimens.xml @@ -0,0 +1,21 @@ + + + + + 16dp + 16dp + diff --git a/samples/browseable/MessagingService/res/values/strings.xml b/samples/browseable/MessagingService/res/values/strings.xml new file mode 100644 index 000000000..001b10eed --- /dev/null +++ b/samples/browseable/MessagingService/res/values/strings.xml @@ -0,0 +1,26 @@ + + + + Messaging Sample + Settings + Messaging Sample + Reply by Voice + Send 2 conversations with 1 message + Send 1 conversation with 1 message + Send 1 conversation with 3 messages + Clear Log + diff --git a/samples/browseable/MessagingService/res/values/styles.xml b/samples/browseable/MessagingService/res/values/styles.xml new file mode 100644 index 000000000..3f1a6af88 --- /dev/null +++ b/samples/browseable/MessagingService/res/values/styles.xml @@ -0,0 +1,20 @@ + + + + + diff --git a/samples/browseable/MessagingService/res/xml/automotive_app_desc.xml b/samples/browseable/MessagingService/res/xml/automotive_app_desc.xml new file mode 100644 index 000000000..9e9f1741f --- /dev/null +++ b/samples/browseable/MessagingService/res/xml/automotive_app_desc.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/samples/browseable/MessagingService/src/com.example.android.messagingservice/Conversations.java b/samples/browseable/MessagingService/src/com.example.android.messagingservice/Conversations.java new file mode 100644 index 000000000..210e061f0 --- /dev/null +++ b/samples/browseable/MessagingService/src/com.example.android.messagingservice/Conversations.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.messagingservice; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +/** + * A simple class that denotes unread conversations and messages. In a real world application, + * this would be replaced by a content provider that actually gets the unread messages to be + * shown to the user. + */ +public class Conversations { + + /** + * Set of strings used as messages by the sample. + */ + private static final String[] MESSAGES = new String[]{ + "Are you at home?", + "Can you give me a call?", + "Hey yt?", + "Don't forget to get some milk on your way back home", + "Is that project done?", + "Did you finish the Messaging app yet?" + }; + + /** + * Senders of the said messages. + */ + private static final String[] PARTICIPANTS = new String[]{ + "John Rambo", + "Han Solo", + "Rocky Balboa", + "Lara Croft" + }; + + static class Conversation { + + private final int conversationId; + + private final String participantName; + + /** + * A given conversation can have a single or multiple messages. + * Note that the messages are sorted from *newest* to *oldest* + */ + private final List messages; + + private final long timestamp; + + public Conversation(int conversationId, String participantName, + List messages) { + this.conversationId = conversationId; + this.participantName = participantName; + this.messages = messages == null ? Collections.emptyList() : messages; + this.timestamp = System.currentTimeMillis(); + } + + public int getConversationId() { + return conversationId; + } + + public String getParticipantName() { + return participantName; + } + + public List getMessages() { + return messages; + } + + public long getTimestamp() { + return timestamp; + } + + public String toString() { + return "[Conversation: conversationId=" + conversationId + + ", participantName=" + participantName + + ", messages=" + messages + + ", timestamp=" + timestamp + "]"; + } + } + + private Conversations() { + } + + public static Conversation[] getUnreadConversations(int howManyConversations, + int messagesPerConversation) { + Conversation[] conversations = new Conversation[howManyConversations]; + for (int i = 0; i < howManyConversations; i++) { + conversations[i] = new Conversation( + ThreadLocalRandom.current().nextInt(), + name(), makeMessages(messagesPerConversation)); + } + return conversations; + } + + private static List makeMessages(int messagesPerConversation) { + int maxLen = MESSAGES.length; + List messages = new ArrayList<>(messagesPerConversation); + for (int i = 0; i < messagesPerConversation; i++) { + messages.add(MESSAGES[ThreadLocalRandom.current().nextInt(0, maxLen)]); + } + return messages; + } + + private static String name() { + return PARTICIPANTS[ThreadLocalRandom.current().nextInt(0, PARTICIPANTS.length)]; + } +} diff --git a/samples/browseable/MessagingService/src/com.example.android.messagingservice/MainActivity.java b/samples/browseable/MessagingService/src/com.example.android.messagingservice/MainActivity.java new file mode 100644 index 000000000..e558a64a8 --- /dev/null +++ b/samples/browseable/MessagingService/src/com.example.android.messagingservice/MainActivity.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.messagingservice; + +import android.app.Activity; +import android.os.Bundle; + +public class MainActivity extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + if (savedInstanceState == null) { + getFragmentManager().beginTransaction() + .add(R.id.container, new MessagingFragment()) + .commit(); + } + } +} diff --git a/samples/browseable/MessagingService/src/com.example.android.messagingservice/MessageLogger.java b/samples/browseable/MessagingService/src/com.example.android.messagingservice/MessageLogger.java new file mode 100644 index 000000000..d1007b5ad --- /dev/null +++ b/samples/browseable/MessagingService/src/com.example.android.messagingservice/MessageLogger.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.messagingservice; + +import android.content.Context; +import android.content.SharedPreferences; + +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * A simple logger that uses shared preferences to log messages, their reads + * and replies. Don't use this in a real world application. This logger is only + * used for displaying the messages in the text view. + */ +public class MessageLogger { + + private static final String PREF_MESSAGE = "MESSAGE_LOGGER"; + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + public static final String LOG_KEY = "message_data"; + public static final String LINE_BREAKS = "\n\n"; + + public static void logMessage(Context context, String message) { + SharedPreferences prefs = getPrefs(context); + message = DATE_FORMAT.format(new Date(System.currentTimeMillis())) + ": " + message; + prefs.edit() + .putString(LOG_KEY, prefs.getString(LOG_KEY, "") + LINE_BREAKS + message) + .apply(); + } + + public static SharedPreferences getPrefs(Context context) { + return context.getSharedPreferences(PREF_MESSAGE, Context.MODE_PRIVATE); + } + + public static String getAllMessages(Context context) { + return getPrefs(context).getString(LOG_KEY, ""); + } + + public static void clear(Context context) { + getPrefs(context).edit().remove(LOG_KEY).apply(); + } +} diff --git a/samples/browseable/MessagingService/src/com.example.android.messagingservice/MessageReadReceiver.java b/samples/browseable/MessagingService/src/com.example.android.messagingservice/MessageReadReceiver.java new file mode 100644 index 000000000..f28a3a778 --- /dev/null +++ b/samples/browseable/MessagingService/src/com.example.android.messagingservice/MessageReadReceiver.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.messagingservice; + +import android.app.NotificationManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.support.v4.app.NotificationManagerCompat; +import android.util.Log; + +public class MessageReadReceiver extends BroadcastReceiver { + private static final String TAG = MessageReadReceiver.class.getSimpleName(); + + private static final String CONVERSATION_ID = "conversation_id"; + + @Override + public void onReceive(Context context, Intent intent) { + Log.d(TAG, "onReceive"); + int conversationId = intent.getIntExtra(CONVERSATION_ID, -1); + if (conversationId != -1) { + Log.d(TAG, "Conversation " + conversationId + " was read"); + MessageLogger.logMessage(context, "Conversation " + conversationId + " was read."); + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); + notificationManager.cancel(conversationId); + } + } +} diff --git a/samples/browseable/MessagingService/src/com.example.android.messagingservice/MessageReplyReceiver.java b/samples/browseable/MessagingService/src/com.example.android.messagingservice/MessageReplyReceiver.java new file mode 100644 index 000000000..0a3eba692 --- /dev/null +++ b/samples/browseable/MessagingService/src/com.example.android.messagingservice/MessageReplyReceiver.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.messagingservice; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.app.RemoteInput; +import android.util.Log; + +/** + * A receiver that gets called when a reply is sent to a given conversationId + */ +public class MessageReplyReceiver extends BroadcastReceiver { + + private static final String TAG = MessageReplyReceiver.class.getSimpleName(); + + @Override + public void onReceive(Context context, Intent intent) { + if (MessagingService.REPLY_ACTION.equals(intent.getAction())) { + int conversationId = intent.getIntExtra(MessagingService.CONVERSATION_ID, -1); + CharSequence reply = getMessageText(intent); + if (conversationId != -1) { + Log.d(TAG, "Got reply (" + reply + ") for ConversationId " + conversationId); + MessageLogger.logMessage(context, "ConversationId: " + conversationId + + " received a reply: [" + reply + "]"); + } + } + } + + /** + * Get the message text from the intent. + * Note that you should call {@code RemoteInput#getResultsFromIntent(intent)} to process + * the RemoteInput. + */ + private CharSequence getMessageText(Intent intent) { + Bundle remoteInput = RemoteInput.getResultsFromIntent(intent); + if (remoteInput != null) { + return remoteInput.getCharSequence(MessagingService.EXTRA_VOICE_REPLY); + } + return null; + } +} diff --git a/samples/browseable/MessagingService/src/com.example.android.messagingservice/MessagingFragment.java b/samples/browseable/MessagingService/src/com.example.android.messagingservice/MessagingFragment.java new file mode 100644 index 000000000..f8efcc0c7 --- /dev/null +++ b/samples/browseable/MessagingService/src/com.example.android.messagingservice/MessagingFragment.java @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.messagingservice; + +import android.app.Fragment; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.os.IBinder; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; +import android.text.method.ScrollingMovementMethod; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TextView; + +/** + * The main fragment that shows the buttons and the text view containing the log. + */ +public class MessagingFragment extends Fragment implements View.OnClickListener { + + private static final String TAG = MessagingFragment.class.getSimpleName(); + + private Button mSendSingleConversation; + private Button mSendTwoConversations; + private Button mSendConversationWithThreeMessages; + private TextView mDataPortView; + private Button mClearLogButton; + + private Messenger mService; + private boolean mBound; + + private ServiceConnection mConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName componentName, IBinder service) { + mService = new Messenger(service); + mBound = true; + setButtonsState(true); + } + + @Override + public void onServiceDisconnected(ComponentName componentName) { + mService = null; + mBound = false; + setButtonsState(false); + } + }; + + private SharedPreferences.OnSharedPreferenceChangeListener listener = + new SharedPreferences.OnSharedPreferenceChangeListener() { + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if (MessageLogger.LOG_KEY.equals(key)) { + mDataPortView.setText(MessageLogger.getAllMessages(getActivity())); + } + } + }; + + public MessagingFragment() { + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.fragment_message_me, container, false); + + mSendSingleConversation = (Button) rootView.findViewById(R.id.send_1_conversation); + mSendSingleConversation.setOnClickListener(this); + + mSendTwoConversations = (Button) rootView.findViewById(R.id.send_2_conversations); + mSendTwoConversations.setOnClickListener(this); + + mSendConversationWithThreeMessages = + (Button) rootView.findViewById(R.id.send_1_conversation_3_messages); + mSendConversationWithThreeMessages.setOnClickListener(this); + + mDataPortView = (TextView) rootView.findViewById(R.id.data_port); + mDataPortView.setMovementMethod(new ScrollingMovementMethod()); + + mClearLogButton = (Button) rootView.findViewById(R.id.clear); + mClearLogButton.setOnClickListener(this); + + setButtonsState(false); + + return rootView; + } + + @Override + public void onClick(View view) { + if (view == mSendSingleConversation) { + sendMsg(1, 1); + } else if (view == mSendTwoConversations) { + sendMsg(2, 1); + } else if (view == mSendConversationWithThreeMessages) { + sendMsg(1, 3); + } else if (view == mClearLogButton) { + MessageLogger.clear(getActivity()); + mDataPortView.setText(MessageLogger.getAllMessages(getActivity())); + } + } + + @Override + public void onStart() { + super.onStart(); + getActivity().bindService(new Intent(getActivity(), MessagingService.class), mConnection, + Context.BIND_AUTO_CREATE); + } + + @Override + public void onPause() { + super.onPause(); + MessageLogger.getPrefs(getActivity()).unregisterOnSharedPreferenceChangeListener(listener); + } + + @Override + public void onResume() { + super.onResume(); + mDataPortView.setText(MessageLogger.getAllMessages(getActivity())); + MessageLogger.getPrefs(getActivity()).registerOnSharedPreferenceChangeListener(listener); + } + + @Override + public void onStop() { + super.onStop(); + if (mBound) { + getActivity().unbindService(mConnection); + mBound = false; + } + } + + private void sendMsg(int howManyConversations, int messagesPerConversation) { + if (mBound) { + Message msg = Message.obtain(null, MessagingService.MSG_SEND_NOTIFICATION, + howManyConversations, messagesPerConversation); + try { + mService.send(msg); + } catch (RemoteException e) { + Log.e(TAG, "Error sending a message", e); + MessageLogger.logMessage(getActivity(), "Error occurred while sending a message."); + } + } + } + + private void setButtonsState(boolean enable) { + mSendSingleConversation.setEnabled(enable); + mSendTwoConversations.setEnabled(enable); + mSendConversationWithThreeMessages.setEnabled(enable); + } +} diff --git a/samples/browseable/MessagingService/src/com.example.android.messagingservice/MessagingService.java b/samples/browseable/MessagingService/src/com.example.android.messagingservice/MessagingService.java new file mode 100644 index 000000000..f980375d5 --- /dev/null +++ b/samples/browseable/MessagingService/src/com.example.android.messagingservice/MessagingService.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.messagingservice; + +import android.app.PendingIntent; +import android.app.Service; +import android.content.Intent; +import android.graphics.BitmapFactory; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.Messenger; +import android.preview.support.v4.app.NotificationCompat.CarExtender; +import android.preview.support.v4.app.NotificationCompat.CarExtender.UnreadConversation; +import android.support.v4.app.NotificationCompat; +import android.support.v4.app.NotificationManagerCompat; +import android.support.v4.app.RemoteInput; +import android.util.Log; + +import java.util.Iterator; + +public class MessagingService extends Service { + private static final String TAG = MessagingService.class.getSimpleName(); + + public static final String READ_ACTION = + "com.example.android.messagingservice.ACTION_MESSAGE_READ"; + public static final String REPLY_ACTION = + "com.example.android.messagingservice.ACTION_MESSAGE_REPLY"; + public static final String CONVERSATION_ID = "conversation_id"; + public static final String EXTRA_VOICE_REPLY = "extra_voice_reply"; + public static final int MSG_SEND_NOTIFICATION = 1; + public static final String EOL = "\n"; + + private NotificationManagerCompat mNotificationManager; + + private final Messenger mMessenger = new Messenger(new IncomingHandler()); + + /** + * Handler of incoming messages from clients. + */ + class IncomingHandler extends Handler { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_SEND_NOTIFICATION: + int howManyConversations = msg.arg1 <= 0 ? 1 : msg.arg1; + int messagesPerConv = msg.arg2 <= 0 ? 1 : msg.arg2; + sendNotification(howManyConversations, messagesPerConv); + break; + default: + super.handleMessage(msg); + } + } + } + + @Override + public void onCreate() { + Log.d(TAG, "onCreate"); + mNotificationManager = NotificationManagerCompat.from(getApplicationContext()); + } + + @Override + public IBinder onBind(Intent intent) { + Log.d(TAG, "onBind"); + return mMessenger.getBinder(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Log.d(TAG, "onStartCommand"); + return START_STICKY; + } + + @Override + public void onDestroy() { + super.onDestroy(); + Log.d(TAG, "onDestroy"); + } + + // Creates an intent that will be triggered when a message is marked as read. + private Intent getMessageReadIntent(int id) { + return new Intent() + .addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES) + .setAction(READ_ACTION) + .putExtra(CONVERSATION_ID, id); + } + + // Creates an Intent that will be triggered when a voice reply is received. + private Intent getMessageReplyIntent(int conversationId) { + return new Intent() + .addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES) + .setAction(REPLY_ACTION) + .putExtra(CONVERSATION_ID, conversationId); + } + + private void sendNotification(int howManyConversations, int messagesPerConversation) { + Conversations.Conversation[] conversations = Conversations.getUnreadConversations( + howManyConversations, messagesPerConversation); + for (Conversations.Conversation conv : conversations) { + sendNotificationForConversation(conv); + } + } + + private void sendNotificationForConversation(Conversations.Conversation conversation) { + // A pending Intent for reads + PendingIntent readPendingIntent = PendingIntent.getBroadcast(getApplicationContext(), + conversation.getConversationId(), + getMessageReadIntent(conversation.getConversationId()), + PendingIntent.FLAG_UPDATE_CURRENT); + + // Build a RemoteInput for receiving voice input in a Car Notification + RemoteInput remoteInput = new RemoteInput.Builder(EXTRA_VOICE_REPLY) + .setLabel(getApplicationContext().getString(R.string.notification_reply)) + .build(); + + // Building a Pending Intent for the reply action to trigger + PendingIntent replyIntent = PendingIntent.getBroadcast(getApplicationContext(), + conversation.getConversationId(), + getMessageReplyIntent(conversation.getConversationId()), + PendingIntent.FLAG_UPDATE_CURRENT); + + // Create the UnreadConversation and populate it with the participant name, + // read and reply intents. + UnreadConversation.Builder unreadConvBuilder = + new UnreadConversation.Builder(conversation.getParticipantName()) + .setLatestTimestamp(conversation.getTimestamp()) + .setReadPendingIntent(readPendingIntent) + .setReplyAction(replyIntent, remoteInput); + + // Note: Add messages from oldest to newest to the UnreadConversation.Builder + StringBuilder messageForNotification = new StringBuilder(); + for (Iterator messages = conversation.getMessages().iterator(); + messages.hasNext(); ) { + String message = messages.next(); + unreadConvBuilder.addMessage(message); + messageForNotification.append(message); + if (messages.hasNext()) { + messageForNotification.append(EOL); + } + } + + NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext()) + .setSmallIcon(R.drawable.notification_icon) + .setLargeIcon(BitmapFactory.decodeResource( + getApplicationContext().getResources(), R.drawable.android_contact)) + .setContentText(messageForNotification.toString()) + .setWhen(conversation.getTimestamp()) + .setContentTitle(conversation.getParticipantName()) + .setContentIntent(readPendingIntent) + .extend(new CarExtender() + .setUnreadConversation(unreadConvBuilder.build()) + .setColor(getApplicationContext() + .getResources().getColor(R.color.default_color_light))); + + MessageLogger.logMessage(getApplicationContext(), "Sending notification " + + conversation.getConversationId() + " conversation: " + conversation); + + mNotificationManager.notify(conversation.getConversationId(), builder.build()); + } +} diff --git a/samples/browseable/DelayedConfirmation/Shared/res/values/strings.xml b/samples/browseable/NavigationDrawer/res/values/template-attrs.xml similarity index 92% rename from samples/browseable/DelayedConfirmation/Shared/res/values/strings.xml rename to samples/browseable/NavigationDrawer/res/values/template-attrs.xml index 0f2bb9075..442ed7781 100644 --- a/samples/browseable/DelayedConfirmation/Shared/res/values/strings.xml +++ b/samples/browseable/NavigationDrawer/res/values/template-attrs.xml @@ -1,18 +1,15 @@ + - Shared - + \ No newline at end of file diff --git a/samples/browseable/NetworkConnect/AndroidManifest.xml b/samples/browseable/NetworkConnect/AndroidManifest.xml index 1ae29df9a..00ce7f3d0 100644 --- a/samples/browseable/NetworkConnect/AndroidManifest.xml +++ b/samples/browseable/NetworkConnect/AndroidManifest.xml @@ -23,7 +23,7 @@ android:versionCode="1" android:versionName="1.0"> - + diff --git a/samples/browseable/NetworkConnect/_index.jd b/samples/browseable/NetworkConnect/_index.jd index eaac88496..7d67dd3dd 100644 --- a/samples/browseable/NetworkConnect/_index.jd +++ b/samples/browseable/NetworkConnect/_index.jd @@ -1,10 +1,10 @@ - - - page.tags="NetworkConnect" sample.group=Connectivity @jd:body -

This sample demonstrates how to connect to the network and fetch raw HTML. -The sample uses {@link android.os.AsyncTask} to perform the fetch on a -background thread.

+

+ + This sample demonstrates how to connect to the network and fetch raw HTML using + HttpURLConnection. AsyncTask is used to perform the fetch on a background thread. + +

diff --git a/samples/browseable/NetworkConnect/res/values-v21/template-styles.xml b/samples/browseable/NetworkConnect/res/values-v21/template-styles.xml new file mode 100644 index 000000000..134fcd9d3 --- /dev/null +++ b/samples/browseable/NetworkConnect/res/values-v21/template-styles.xml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/samples/browseable/PdfRendererBasic/res/values-v11/template-styles.xml b/samples/browseable/PdfRendererBasic/res/values-v11/template-styles.xml new file mode 100644 index 000000000..8c1ea66f2 --- /dev/null +++ b/samples/browseable/PdfRendererBasic/res/values-v11/template-styles.xml @@ -0,0 +1,22 @@ + + + + + + + + + + diff --git a/samples/browseable/PdfRendererBasic/src/com.example.android.pdfrendererbasic/MainActivity.java b/samples/browseable/PdfRendererBasic/src/com.example.android.pdfrendererbasic/MainActivity.java new file mode 100644 index 000000000..6b7e8b454 --- /dev/null +++ b/samples/browseable/PdfRendererBasic/src/com.example.android.pdfrendererbasic/MainActivity.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.pdfrendererbasic; + +import android.app.Activity; +import android.app.AlertDialog; +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuItem; + +public class MainActivity extends Activity { + + public static final String FRAGMENT_PDF_RENDERER_BASIC = "pdf_renderer_basic"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main_real); + if (savedInstanceState == null) { + getFragmentManager().beginTransaction() + .add(R.id.container, new PdfRendererBasicFragment(), + FRAGMENT_PDF_RENDERER_BASIC) + .commit(); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.main, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_info: + new AlertDialog.Builder(this) + .setMessage(R.string.intro_message) + .setPositiveButton(android.R.string.ok, null) + .show(); + return true; + } + return super.onOptionsItemSelected(item); + } +} diff --git a/samples/browseable/PdfRendererBasic/src/com.example.android.pdfrendererbasic/PdfRendererBasicFragment.java b/samples/browseable/PdfRendererBasic/src/com.example.android.pdfrendererbasic/PdfRendererBasicFragment.java new file mode 100644 index 000000000..e413c6599 --- /dev/null +++ b/samples/browseable/PdfRendererBasic/src/com.example.android.pdfrendererbasic/PdfRendererBasicFragment.java @@ -0,0 +1,221 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.pdfrendererbasic; + +import android.app.Activity; +import android.app.Fragment; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.pdf.PdfRenderer; +import android.os.Bundle; +import android.os.ParcelFileDescriptor; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.Toast; + +import java.io.IOException; + +/** + * This fragment has a big {@ImageView} that shows PDF pages, and 2 {@link android.widget.Button}s to move between + * pages. We use a {@link android.graphics.pdf.PdfRenderer} to render PDF pages as {@link android.graphics.Bitmap}s. + */ +public class PdfRendererBasicFragment extends Fragment implements View.OnClickListener { + + /** + * Key string for saving the state of current page index. + */ + private static final String STATE_CURRENT_PAGE_INDEX = "current_page_index"; + + /** + * File descriptor of the PDF. + */ + private ParcelFileDescriptor mFileDescriptor; + + /** + * {@link android.graphics.pdf.PdfRenderer} to render the PDF. + */ + private PdfRenderer mPdfRenderer; + + /** + * Page that is currently shown on the screen. + */ + private PdfRenderer.Page mCurrentPage; + + /** + * {@link android.widget.ImageView} that shows a PDF page as a {@link android.graphics.Bitmap} + */ + private ImageView mImageView; + + /** + * {@link android.widget.Button} to move to the previous page. + */ + private Button mButtonPrevious; + + /** + * {@link android.widget.Button} to move to the next page. + */ + private Button mButtonNext; + + public PdfRendererBasicFragment() { + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_pdf_renderer_basic, container, false); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + // Retain view references. + mImageView = (ImageView) view.findViewById(R.id.image); + mButtonPrevious = (Button) view.findViewById(R.id.previous); + mButtonNext = (Button) view.findViewById(R.id.next); + // Bind events. + mButtonPrevious.setOnClickListener(this); + mButtonNext.setOnClickListener(this); + // Show the first page by default. + int index = 0; + // If there is a savedInstanceState (screen orientations, etc.), we restore the page index. + if (null != savedInstanceState) { + index = savedInstanceState.getInt(STATE_CURRENT_PAGE_INDEX, 0); + } + showPage(index); + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + try { + openRenderer(activity); + } catch (IOException e) { + e.printStackTrace(); + Toast.makeText(activity, "Error! " + e.getMessage(), Toast.LENGTH_SHORT).show(); + activity.finish(); + } + } + + @Override + public void onDetach() { + try { + closeRenderer(); + } catch (IOException e) { + e.printStackTrace(); + } + super.onDetach(); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + if (null != mCurrentPage) { + outState.putInt(STATE_CURRENT_PAGE_INDEX, mCurrentPage.getIndex()); + } + } + + /** + * Sets up a {@link android.graphics.pdf.PdfRenderer} and related resources. + */ + private void openRenderer(Context context) throws IOException { + // In this sample, we read a PDF from the assets directory. + mFileDescriptor = context.getAssets().openFd("sample.pdf").getParcelFileDescriptor(); + // This is the PdfRenderer we use to render the PDF. + mPdfRenderer = new PdfRenderer(mFileDescriptor); + } + + /** + * Closes the {@link android.graphics.pdf.PdfRenderer} and related resources. + * + * @throws java.io.IOException When the PDF file cannot be closed. + */ + private void closeRenderer() throws IOException { + if (null != mCurrentPage) { + mCurrentPage.close(); + } + mPdfRenderer.close(); + mFileDescriptor.close(); + } + + /** + * Shows the specified page of PDF to the screen. + * + * @param index The page index. + */ + private void showPage(int index) { + if (mPdfRenderer.getPageCount() <= index) { + return; + } + // Make sure to close the current page before opening another one. + if (null != mCurrentPage) { + mCurrentPage.close(); + } + // Use `openPage` to open a specific page in PDF. + mCurrentPage = mPdfRenderer.openPage(index); + // Important: the destination bitmap must be ARGB (not RGB). + Bitmap bitmap = Bitmap.createBitmap(mCurrentPage.getWidth(), mCurrentPage.getHeight(), + Bitmap.Config.ARGB_8888); + // Here, we render the page onto the Bitmap. + // To render a portion of the page, use the second and third parameter. Pass nulls to get + // the default result. + // Pass either RENDER_MODE_FOR_DISPLAY or RENDER_MODE_FOR_PRINT for the last parameter. + mCurrentPage.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY); + // We are ready to show the Bitmap to user. + mImageView.setImageBitmap(bitmap); + updateUi(); + } + + /** + * Updates the state of 2 control buttons in response to the current page index. + */ + private void updateUi() { + int index = mCurrentPage.getIndex(); + int pageCount = mPdfRenderer.getPageCount(); + mButtonPrevious.setEnabled(0 != index); + mButtonNext.setEnabled(index + 1 < pageCount); + getActivity().setTitle(getString(R.string.app_name_with_index, index + 1, pageCount)); + } + + /** + * Gets the number of pages in the PDF. This method is marked as public for testing. + * + * @return The number of pages. + */ + public int getPageCount() { + return mPdfRenderer.getPageCount(); + } + + @Override + public void onClick(View view) { + switch (view.getId()) { + case R.id.previous: { + // Move to the previous page + showPage(mCurrentPage.getIndex() - 1); + break; + } + case R.id.next: { + // Move to the next page + showPage(mCurrentPage.getIndex() + 1); + break; + } + } + } + +} diff --git a/samples/browseable/Quiz/Application/AndroidManifest.xml b/samples/browseable/Quiz/Application/AndroidManifest.xml index 40e36027e..ce7213542 100644 --- a/samples/browseable/Quiz/Application/AndroidManifest.xml +++ b/samples/browseable/Quiz/Application/AndroidManifest.xml @@ -15,7 +15,7 @@ --> + package="com.example.android.wearable.quiz" > @@ -29,7 +29,7 @@ android:value="@integer/google_play_services_version" /> diff --git a/samples/browseable/Quiz/Application/res/values-v21/template-styles.xml b/samples/browseable/Quiz/Application/res/values-v21/template-styles.xml new file mode 100644 index 000000000..134fcd9d3 --- /dev/null +++ b/samples/browseable/Quiz/Application/res/values-v21/template-styles.xml @@ -0,0 +1,22 @@ + + + + + +