From 9972271b9c7ca26db3fb96be4e890e87bd458e66 Mon Sep 17 00:00:00 2001 From: Synced Synapse Date: Wed, 14 Jan 2015 11:12:47 +0000 Subject: [PATCH] First dump --- .gitignore | 25 + LICENSE.txt | 202 + app/.gitignore | 1 + app/build.gradle | 65 + app/proguard-rules.pro | 43 + .../syncedsynapse/kore2/ApplicationTest.java | 13 + app/src/main/AndroidManifest.xml | 48 + .../vending/billing/IInAppBillingService.aidl | 144 + .../com/syncedsynapse/kore2/Settings.java | 136 + .../syncedsynapse/kore2/billing/Base64.java | 570 ++ .../kore2/billing/Base64DecoderException.java | 32 + .../kore2/billing/IabException.java | 43 + .../kore2/billing/IabHelper.java | 1017 ++ .../kore2/billing/IabResult.java | 45 + .../kore2/billing/Inventory.java | 91 + .../syncedsynapse/kore2/billing/Purchase.java | 63 + .../syncedsynapse/kore2/billing/Security.java | 121 + .../kore2/billing/SkuDetails.java | 58 + .../kore2/host/HostConnectionObserver.java | 603 ++ .../syncedsynapse/kore2/host/HostInfo.java | 224 + .../syncedsynapse/kore2/host/HostManager.java | 396 + .../kore2/jsonrpc/ApiCallback.java | 43 + .../kore2/jsonrpc/ApiException.java | 119 + .../kore2/jsonrpc/ApiMethod.java | 282 + .../kore2/jsonrpc/ApiNotification.java | 71 + .../kore2/jsonrpc/HostConnection.java | 814 ++ .../kore2/jsonrpc/event/MediaSyncEvent.java | 54 + .../kore2/jsonrpc/method/Addons.java | 168 + .../kore2/jsonrpc/method/Application.java | 131 + .../kore2/jsonrpc/method/AudioLibrary.java | 256 + .../kore2/jsonrpc/method/Files.java | 51 + .../kore2/jsonrpc/method/GUI.java | 184 + .../kore2/jsonrpc/method/Input.java | 395 + .../kore2/jsonrpc/method/JSONRPC.java | 45 + .../kore2/jsonrpc/method/Player.java | 469 + .../kore2/jsonrpc/method/Playlist.java | 173 + .../kore2/jsonrpc/method/System.java | 75 + .../kore2/jsonrpc/method/VideoLibrary.java | 439 + .../kore2/jsonrpc/notification/Input.java | 50 + .../kore2/jsonrpc/notification/Player.java | 194 + .../kore2/jsonrpc/notification/System.java | 67 + .../kore2/jsonrpc/type/AddonType.java | 143 + .../kore2/jsonrpc/type/ApiParameter.java | 25 + .../kore2/jsonrpc/type/ApplicationType.java | 79 + .../kore2/jsonrpc/type/AudioType.java | 333 + .../kore2/jsonrpc/type/FilesType.java | 48 + .../kore2/jsonrpc/type/GlobalType.java | 57 + .../kore2/jsonrpc/type/ItemType.java | 40 + .../kore2/jsonrpc/type/LibraryType.java | 62 + .../kore2/jsonrpc/type/ListType.java | 405 + .../kore2/jsonrpc/type/MediaType.java | 87 + .../kore2/jsonrpc/type/PlayerType.java | 331 + .../kore2/jsonrpc/type/PlaylistType.java | 113 + .../kore2/jsonrpc/type/VideoType.java | 673 ++ .../kore2/provider/MediaContract.java | 808 ++ .../kore2/provider/MediaDatabase.java | 458 + .../kore2/provider/MediaProvider.java | 785 ++ .../kore2/service/LibrarySyncService.java | 1173 +++ .../kore2/service/SyncUtils.java | 404 + .../kore2/ui/AboutDialogFragment.java | 62 + .../kore2/ui/AddonDetailsFragment.java | 300 + .../kore2/ui/AddonListFragment.java | 278 + .../kore2/ui/AddonsActivity.java | 197 + .../kore2/ui/AlbumDetailsFragment.java | 678 ++ .../kore2/ui/AlbumListFragment.java | 419 + .../kore2/ui/ArtistListFragment.java | 376 + .../kore2/ui/AudioGenresListFragment.java | 359 + .../syncedsynapse/kore2/ui/BaseActivity.java | 61 + .../kore2/ui/GenericSelectDialog.java | 175 + .../kore2/ui/HostConnectionActivity.java | 47 + .../kore2/ui/MovieDetailsFragment.java | 680 ++ .../kore2/ui/MovieListFragment.java | 432 + .../kore2/ui/MoviesActivity.java | 196 + .../syncedsynapse/kore2/ui/MusicActivity.java | 315 + .../kore2/ui/MusicListFragment.java | 85 + .../kore2/ui/MusicVideoDetailsFragment.java | 525 + .../kore2/ui/MusicVideoListFragment.java | 379 + .../kore2/ui/NavigationDrawerFragment.java | 451 + .../kore2/ui/NowPlayingFragment.java | 922 ++ .../kore2/ui/PlaylistFragment.java | 670 ++ .../kore2/ui/RemoteActivity.java | 329 + .../kore2/ui/RemoteFragment.java | 412 + .../kore2/ui/SendTextDialogFragment.java | 150 + .../kore2/ui/SettingsActivity.java | 91 + .../kore2/ui/SettingsFragment.java | 288 + .../kore2/ui/TVShowDetailsFragment.java | 110 + .../ui/TVShowEpisodeDetailsFragment.java | 636 ++ .../kore2/ui/TVShowEpisodeListFragment.java | 583 ++ .../kore2/ui/TVShowListFragment.java | 421 + .../kore2/ui/TVShowOverviewFragment.java | 461 + .../kore2/ui/TVShowsActivity.java | 246 + .../kore2/ui/hosts/AddHostActivity.java | 165 + .../kore2/ui/hosts/AddHostFragmentFinish.java | 104 + .../ui/hosts/AddHostFragmentWelcome.java | 93 + .../ui/hosts/AddHostFragmentZeroconf.java | 307 + .../kore2/ui/hosts/EditHostActivity.java | 138 + .../HostFragmentManualConfiguration.java | 345 + .../kore2/ui/hosts/HostListFragment.java | 389 + .../kore2/ui/hosts/HostManagerActivity.java | 83 + .../kore2/ui/views/CirclePageIndicator.java | 570 ++ .../kore2/ui/views/PageIndicator.java | 62 + .../utils/BasicAuthPicassoDownloader.java | 57 + .../kore2/utils/CharacterDrawable.java | 99 + .../kore2/utils/FileDownloadHelper.java | 397 + .../syncedsynapse/kore2/utils/JsonUtils.java | 106 + .../syncedsynapse/kore2/utils/LogUtils.java | 121 + .../syncedsynapse/kore2/utils/NetUtils.java | 165 + .../kore2/utils/RepeatListener.java | 134 + .../kore2/utils/SelectionBuilder.java | 173 + .../kore2/utils/TabsAdapter.java | 85 + .../syncedsynapse/kore2/utils/UIUtils.java | 359 + .../com/syncedsynapse/kore2/utils/Utils.java | 134 + app/src/main/res/anim/activity_in.xml | 42 + app/src/main/res/anim/activity_out.xml | 42 + app/src/main/res/anim/button_in.xml | 33 + app/src/main/res/anim/button_out.xml | 33 + .../main/res/anim/fragment_details_enter.xml | 30 + .../main/res/anim/fragment_list_popenter.xml | 25 + .../remote_back_black.png | Bin 0 -> 4242 bytes .../remote_back_white.png | Bin 0 -> 3673 bytes .../remote_codec_black.png | Bin 0 -> 5153 bytes .../remote_codec_white.png | Bin 0 -> 4061 bytes .../remote_down_black.png | Bin 0 -> 2409 bytes .../remote_down_white.png | Bin 0 -> 2735 bytes .../remote_info_black.png | Bin 0 -> 3109 bytes .../remote_info_white.png | Bin 0 -> 2571 bytes .../remote_left_black.png | Bin 0 -> 2548 bytes .../remote_left_white.png | Bin 0 -> 2969 bytes .../remote_menu_black.png | Bin 0 -> 3850 bytes .../remote_menu_white.png | Bin 0 -> 2906 bytes .../remote_right_black.png | Bin 0 -> 2557 bytes .../remote_right_white.png | Bin 0 -> 2658 bytes .../remote_select_black.png | Bin 0 -> 2013 bytes .../remote_select_white.png | Bin 0 -> 2098 bytes .../remote_up_black.png | Bin 0 -> 2515 bytes .../remote_up_white.png | Bin 0 -> 2751 bytes .../main/res/drawable-xhdpi/drawer_image.png | Bin 0 -> 437872 bytes .../res/drawable-xhdpi/drawer_shadow.9.png | Bin 0 -> 174 bytes .../drawable-xhdpi/ic_add_box_black_24dp.png | Bin 0 -> 290 bytes .../drawable-xhdpi/ic_add_box_white_24dp.png | Bin 0 -> 257 bytes .../ic_chevron_left_black_24dp.png | Bin 0 -> 297 bytes .../ic_chevron_left_white_24dp.png | Bin 0 -> 311 bytes .../ic_chevron_right_black_24dp.png | Bin 0 -> 291 bytes .../ic_chevron_right_white_24dp.png | Bin 0 -> 303 bytes .../ic_closed_caption_black_24dp.png | Bin 0 -> 351 bytes .../ic_closed_caption_white_24dp.png | Bin 0 -> 302 bytes .../drawable-xhdpi/ic_delete_black_24dp.png | Bin 0 -> 246 bytes .../drawable-xhdpi/ic_delete_white_24dp.png | Bin 0 -> 270 bytes .../res/drawable-xhdpi/ic_done_black_24dp.png | Bin 0 -> 349 bytes .../res/drawable-xhdpi/ic_done_white_24dp.png | Bin 0 -> 363 bytes .../res/drawable-xhdpi/ic_edit_black_24dp.png | Bin 0 -> 407 bytes .../res/drawable-xhdpi/ic_edit_white_24dp.png | Bin 0 -> 378 bytes .../ic_expand_less_black_24dp.png | Bin 0 -> 268 bytes .../ic_expand_less_white_24dp.png | Bin 0 -> 294 bytes .../ic_expand_more_black_24dp.png | Bin 0 -> 297 bytes .../ic_expand_more_white_24dp.png | Bin 0 -> 324 bytes .../ic_extension_black_24dp.png | Bin 0 -> 510 bytes .../ic_extension_white_24dp.png | Bin 0 -> 474 bytes .../ic_fast_forward_black_24dp.png | Bin 0 -> 392 bytes .../ic_fast_forward_white_24dp.png | Bin 0 -> 386 bytes .../ic_fast_rewind_black_24dp.png | Bin 0 -> 407 bytes .../ic_fast_rewind_white_24dp.png | Bin 0 -> 399 bytes .../drawable-xhdpi/ic_games_black_24dp.png | Bin 0 -> 302 bytes .../drawable-xhdpi/ic_games_white_24dp.png | Bin 0 -> 273 bytes .../drawable-xhdpi/ic_get_app_black_24dp.png | Bin 0 -> 264 bytes .../drawable-xhdpi/ic_get_app_white_24dp.png | Bin 0 -> 282 bytes .../drawable-xhdpi/ic_headset_black_24dp.png | Bin 0 -> 574 bytes .../drawable-xhdpi/ic_headset_white_24dp.png | Bin 0 -> 580 bytes .../res/drawable-xhdpi/ic_home_black_24dp.png | Bin 0 -> 380 bytes .../res/drawable-xhdpi/ic_home_white_24dp.png | Bin 0 -> 345 bytes .../drawable-xhdpi/ic_image_black_24dp.png | Bin 0 -> 430 bytes .../drawable-xhdpi/ic_image_white_24dp.png | Bin 0 -> 390 bytes .../drawable-xhdpi/ic_keyboard_black_24dp.png | Bin 0 -> 314 bytes .../drawable-xhdpi/ic_keyboard_white_24dp.png | Bin 0 -> 333 bytes .../main/res/drawable-xhdpi/ic_launcher.png | Bin 0 -> 2781 bytes .../res/drawable-xhdpi/ic_menu_white_24dp.png | Bin 0 -> 192 bytes .../ic_more_vert_black_24dp.png | Bin 0 -> 246 bytes .../ic_more_vert_white_24dp.png | Bin 0 -> 269 bytes .../drawable-xhdpi/ic_movie_black_24dp.png | Bin 0 -> 315 bytes .../drawable-xhdpi/ic_movie_white_24dp.png | Bin 0 -> 318 bytes .../ic_open_in_new_black_24dp.png | Bin 0 -> 399 bytes .../ic_open_in_new_white_24dp.png | Bin 0 -> 368 bytes .../drawable-xhdpi/ic_pause_black_24dp.png | Bin 0 -> 170 bytes .../drawable-xhdpi/ic_pause_white_24dp.png | Bin 0 -> 193 bytes .../ic_phonelink_black_24dp.png | Bin 0 -> 262 bytes .../ic_phonelink_white_24dp.png | Bin 0 -> 287 bytes .../ic_play_arrow_black_24dp.png | Bin 0 -> 342 bytes .../ic_play_arrow_white_24dp.png | Bin 0 -> 318 bytes .../drawable-xhdpi/ic_queue_black_24dp.png | Bin 0 -> 339 bytes .../drawable-xhdpi/ic_queue_white_24dp.png | Bin 0 -> 288 bytes .../drawable-xhdpi/ic_repeat_black_24dp.png | Bin 0 -> 298 bytes .../ic_repeat_one_black_24dp.png | Bin 0 -> 326 bytes .../ic_repeat_one_white_24dp.png | Bin 0 -> 333 bytes .../drawable-xhdpi/ic_repeat_white_24dp.png | Bin 0 -> 314 bytes .../drawable-xhdpi/ic_search_black_24dp.png | Bin 0 -> 681 bytes .../drawable-xhdpi/ic_search_white_24dp.png | Bin 0 -> 591 bytes .../drawable-xhdpi/ic_settings_black_24dp.png | Bin 0 -> 785 bytes .../ic_settings_power_black_24dp.png | Bin 0 -> 663 bytes .../ic_settings_power_white_24dp.png | Bin 0 -> 628 bytes .../drawable-xhdpi/ic_settings_white_24dp.png | Bin 0 -> 737 bytes .../drawable-xhdpi/ic_shuffle_black_24dp.png | Bin 0 -> 491 bytes .../drawable-xhdpi/ic_shuffle_white_24dp.png | Bin 0 -> 424 bytes .../ic_skip_next_black_24dp.png | Bin 0 -> 346 bytes .../ic_skip_next_white_24dp.png | Bin 0 -> 326 bytes .../ic_skip_previous_black_24dp.png | Bin 0 -> 363 bytes .../ic_skip_previous_white_24dp.png | Bin 0 -> 354 bytes .../res/drawable-xhdpi/ic_stop_black_24dp.png | Bin 0 -> 166 bytes .../res/drawable-xhdpi/ic_stop_white_24dp.png | Bin 0 -> 190 bytes .../ic_subtitles_black_24dp.png | Bin 0 -> 298 bytes .../ic_subtitles_white_24dp.png | Bin 0 -> 306 bytes .../res/drawable-xhdpi/ic_tv_black_24dp.png | Bin 0 -> 289 bytes .../res/drawable-xhdpi/ic_tv_white_24dp.png | Bin 0 -> 296 bytes .../ic_volume_down_black_24dp.png | Bin 0 -> 338 bytes .../ic_volume_down_white_24dp.png | Bin 0 -> 338 bytes .../ic_volume_off_black_24dp.png | Bin 0 -> 779 bytes .../ic_volume_off_white_24dp.png | Bin 0 -> 650 bytes .../ic_volume_up_black_24dp.png | Bin 0 -> 637 bytes .../ic_volume_up_white_24dp.png | Bin 0 -> 545 bytes .../res/drawable-xxhdpi/drawer_shadow.9.png | Bin 0 -> 208 bytes .../drawable-xxhdpi/ic_add_box_black_24dp.png | Bin 0 -> 362 bytes .../drawable-xxhdpi/ic_add_box_white_24dp.png | Bin 0 -> 324 bytes .../ic_chevron_left_black_24dp.png | Bin 0 -> 372 bytes .../ic_chevron_left_white_24dp.png | Bin 0 -> 365 bytes .../ic_chevron_right_black_24dp.png | Bin 0 -> 364 bytes .../ic_chevron_right_white_24dp.png | Bin 0 -> 380 bytes .../ic_closed_caption_black_24dp.png | Bin 0 -> 494 bytes .../ic_closed_caption_white_24dp.png | Bin 0 -> 415 bytes .../drawable-xxhdpi/ic_delete_black_24dp.png | Bin 0 -> 305 bytes .../drawable-xxhdpi/ic_delete_white_24dp.png | Bin 0 -> 338 bytes .../drawable-xxhdpi/ic_done_black_24dp.png | Bin 0 -> 464 bytes .../drawable-xxhdpi/ic_done_white_24dp.png | Bin 0 -> 476 bytes .../drawable-xxhdpi/ic_edit_black_24dp.png | Bin 0 -> 526 bytes .../drawable-xxhdpi/ic_edit_white_24dp.png | Bin 0 -> 490 bytes .../ic_expand_less_black_24dp.png | Bin 0 -> 337 bytes .../ic_expand_less_white_24dp.png | Bin 0 -> 371 bytes .../ic_expand_more_black_24dp.png | Bin 0 -> 361 bytes .../ic_expand_more_white_24dp.png | Bin 0 -> 406 bytes .../ic_extension_black_24dp.png | Bin 0 -> 711 bytes .../ic_extension_white_24dp.png | Bin 0 -> 629 bytes .../ic_fast_forward_black_24dp.png | Bin 0 -> 498 bytes .../ic_fast_forward_white_24dp.png | Bin 0 -> 496 bytes .../ic_fast_rewind_black_24dp.png | Bin 0 -> 508 bytes .../ic_fast_rewind_white_24dp.png | Bin 0 -> 511 bytes .../drawable-xxhdpi/ic_games_black_24dp.png | Bin 0 -> 398 bytes .../drawable-xxhdpi/ic_games_white_24dp.png | Bin 0 -> 350 bytes .../drawable-xxhdpi/ic_get_app_black_24dp.png | Bin 0 -> 323 bytes .../drawable-xxhdpi/ic_get_app_white_24dp.png | Bin 0 -> 351 bytes .../drawable-xxhdpi/ic_headset_black_24dp.png | Bin 0 -> 814 bytes .../drawable-xxhdpi/ic_headset_white_24dp.png | Bin 0 -> 797 bytes .../drawable-xxhdpi/ic_home_black_24dp.png | Bin 0 -> 464 bytes .../drawable-xxhdpi/ic_home_white_24dp.png | Bin 0 -> 423 bytes .../drawable-xxhdpi/ic_image_black_24dp.png | Bin 0 -> 590 bytes .../drawable-xxhdpi/ic_image_white_24dp.png | Bin 0 -> 614 bytes .../ic_keyboard_black_24dp.png | Bin 0 -> 379 bytes .../ic_keyboard_white_24dp.png | Bin 0 -> 420 bytes .../main/res/drawable-xxhdpi/ic_launcher.png | Bin 0 -> 4448 bytes .../drawable-xxhdpi/ic_menu_white_24dp.png | Bin 0 -> 226 bytes .../ic_more_vert_black_24dp.png | Bin 0 -> 307 bytes .../ic_more_vert_white_24dp.png | Bin 0 -> 313 bytes .../drawable-xxhdpi/ic_movie_black_24dp.png | Bin 0 -> 391 bytes .../drawable-xxhdpi/ic_movie_white_24dp.png | Bin 0 -> 426 bytes .../ic_open_in_new_black_24dp.png | Bin 0 -> 491 bytes .../ic_open_in_new_white_24dp.png | Bin 0 -> 533 bytes .../drawable-xxhdpi/ic_pause_black_24dp.png | Bin 0 -> 186 bytes .../drawable-xxhdpi/ic_pause_white_24dp.png | Bin 0 -> 215 bytes .../ic_phonelink_black_24dp.png | Bin 0 -> 321 bytes .../ic_phonelink_white_24dp.png | Bin 0 -> 351 bytes .../ic_play_arrow_black_24dp.png | Bin 0 -> 424 bytes .../ic_play_arrow_white_24dp.png | Bin 0 -> 399 bytes .../drawable-xxhdpi/ic_queue_black_24dp.png | Bin 0 -> 421 bytes .../drawable-xxhdpi/ic_queue_white_24dp.png | Bin 0 -> 432 bytes .../drawable-xxhdpi/ic_repeat_black_24dp.png | Bin 0 -> 372 bytes .../ic_repeat_one_black_24dp.png | Bin 0 -> 406 bytes .../ic_repeat_one_white_24dp.png | Bin 0 -> 422 bytes .../drawable-xxhdpi/ic_repeat_white_24dp.png | Bin 0 -> 397 bytes .../drawable-xxhdpi/ic_search_black_24dp.png | Bin 0 -> 1008 bytes .../drawable-xxhdpi/ic_search_white_24dp.png | Bin 0 -> 871 bytes .../ic_settings_black_24dp.png | Bin 0 -> 1149 bytes .../ic_settings_power_black_24dp.png | Bin 0 -> 981 bytes .../ic_settings_power_white_24dp.png | Bin 0 -> 922 bytes .../ic_settings_white_24dp.png | Bin 0 -> 974 bytes .../drawable-xxhdpi/ic_shuffle_black_24dp.png | Bin 0 -> 733 bytes .../drawable-xxhdpi/ic_shuffle_white_24dp.png | Bin 0 -> 627 bytes .../ic_skip_next_black_24dp.png | Bin 0 -> 427 bytes .../ic_skip_next_white_24dp.png | Bin 0 -> 408 bytes .../ic_skip_previous_black_24dp.png | Bin 0 -> 447 bytes .../ic_skip_previous_white_24dp.png | Bin 0 -> 447 bytes .../drawable-xxhdpi/ic_stop_black_24dp.png | Bin 0 -> 182 bytes .../drawable-xxhdpi/ic_stop_white_24dp.png | Bin 0 -> 211 bytes .../ic_subtitles_black_24dp.png | Bin 0 -> 365 bytes .../ic_subtitles_white_24dp.png | Bin 0 -> 398 bytes .../res/drawable-xxhdpi/ic_tv_black_24dp.png | Bin 0 -> 362 bytes .../res/drawable-xxhdpi/ic_tv_white_24dp.png | Bin 0 -> 397 bytes .../ic_volume_down_black_24dp.png | Bin 0 -> 445 bytes .../ic_volume_down_white_24dp.png | Bin 0 -> 447 bytes .../ic_volume_off_black_24dp.png | Bin 0 -> 1114 bytes .../ic_volume_off_white_24dp.png | Bin 0 -> 911 bytes .../ic_volume_up_black_24dp.png | Bin 0 -> 894 bytes .../ic_volume_up_white_24dp.png | Bin 0 -> 836 bytes .../res/drawable-xxhdpi/remote_back_black.png | Bin 0 -> 2779 bytes .../res/drawable-xxhdpi/remote_back_white.png | Bin 0 -> 2280 bytes .../drawable-xxhdpi/remote_codec_black.png | Bin 0 -> 3372 bytes .../drawable-xxhdpi/remote_codec_white.png | Bin 0 -> 2568 bytes .../res/drawable-xxhdpi/remote_down_black.png | Bin 0 -> 1655 bytes .../res/drawable-xxhdpi/remote_down_white.png | Bin 0 -> 1840 bytes .../res/drawable-xxhdpi/remote_info_black.png | Bin 0 -> 2095 bytes .../res/drawable-xxhdpi/remote_info_white.png | Bin 0 -> 1698 bytes .../res/drawable-xxhdpi/remote_left_black.png | Bin 0 -> 1692 bytes .../res/drawable-xxhdpi/remote_left_white.png | Bin 0 -> 1945 bytes .../res/drawable-xxhdpi/remote_menu_black.png | Bin 0 -> 2541 bytes .../res/drawable-xxhdpi/remote_menu_white.png | Bin 0 -> 1855 bytes .../drawable-xxhdpi/remote_right_black.png | Bin 0 -> 1655 bytes .../drawable-xxhdpi/remote_right_white.png | Bin 0 -> 1741 bytes .../drawable-xxhdpi/remote_select_black.png | Bin 0 -> 1269 bytes .../drawable-xxhdpi/remote_select_white.png | Bin 0 -> 1250 bytes .../res/drawable-xxhdpi/remote_up_black.png | Bin 0 -> 1695 bytes .../res/drawable-xxhdpi/remote_up_white.png | Bin 0 -> 1841 bytes .../res/drawable/host_status_indicator.xml | 22 + .../main/res/layout-land/fragment_remote.xml | 246 + .../res/layout/activity_generic_media.xml | 43 + .../main/res/layout/activity_host_manager.xml | 44 + app/src/main/res/layout/activity_remote.xml | 64 + app/src/main/res/layout/activity_settings.xml | 31 + app/src/main/res/layout/dialog_send_text.xml | 43 + app/src/main/res/layout/empty_view.xml | 27 + app/src/main/res/layout/fragment_about.xml | 58 + .../res/layout/fragment_add_host_finish.xml | 57 + ...fragment_add_host_manual_configuration.xml | 166 + .../res/layout/fragment_add_host_welcome.xml | 55 + .../res/layout/fragment_add_host_zeroconf.xml | 71 + .../res/layout/fragment_addon_details.xml | 180 + .../res/layout/fragment_album_details.xml | 224 + .../res/layout/fragment_episode_details.xml | 258 + .../layout/fragment_generic_media_list.xml | 45 + .../main/res/layout/fragment_host_list.xml | 62 + .../res/layout/fragment_movie_details.xml | 288 + .../main/res/layout/fragment_music_list.xml | 35 + .../layout/fragment_music_video_details.xml | 181 + .../res/layout/fragment_navigation_drawer.xml | 29 + .../main/res/layout/fragment_now_playing.xml | 336 + app/src/main/res/layout/fragment_playlist.xml | 42 + app/src/main/res/layout/fragment_remote.xml | 228 + .../res/layout/fragment_tvshow_details.xml | 35 + .../layout/fragment_tvshow_episodes_list.xml | 45 + .../res/layout/fragment_tvshow_overview.xml | 208 + app/src/main/res/layout/grid_item_addon.xml | 56 + app/src/main/res/layout/grid_item_album.xml | 62 + app/src/main/res/layout/grid_item_artist.xml | 57 + .../main/res/layout/grid_item_audio_genre.xml | 44 + app/src/main/res/layout/grid_item_cast.xml | 57 + app/src/main/res/layout/grid_item_host.xml | 71 + app/src/main/res/layout/grid_item_movie.xml | 77 + .../main/res/layout/grid_item_music_video.xml | 61 + .../main/res/layout/grid_item_playlist.xml | 74 + app/src/main/res/layout/grid_item_tvshow.xml | 63 + app/src/main/res/layout/list_item_episode.xml | 71 + .../layout/list_item_navigation_drawer.xml | 44 + .../list_item_navigation_drawer_divider.xml | 28 + .../list_item_navigation_drawer_host.xml | 61 + app/src/main/res/layout/list_item_season.xml | 58 + app/src/main/res/layout/list_item_song.xml | 60 + app/src/main/res/layout/remote_info_panel.xml | 39 + app/src/main/res/layout/toolbar_default.xml | 26 + app/src/main/res/layout/wizard_button_bar.xml | 54 + app/src/main/res/layout/wizard_title.xml | 28 + app/src/main/res/menu/host_manager.xml | 24 + app/src/main/res/menu/hostlist_item.xml | 31 + app/src/main/res/menu/media_info.xml | 24 + app/src/main/res/menu/media_search.xml | 26 + app/src/main/res/menu/movie_list.xml | 35 + app/src/main/res/menu/playlist.xml | 23 + app/src/main/res/menu/playlist_item.xml | 23 + app/src/main/res/menu/remote.xml | 39 + app/src/main/res/menu/song_item.xml | 31 + app/src/main/res/menu/tvshow_episode_list.xml | 25 + app/src/main/res/menu/tvshow_list.xml | 31 + app/src/main/res/menu/video_overflow.xml | 22 + .../main/res/transition-v21/media_details.xml | 35 + app/src/main/res/values-land/integers.xml | 19 + .../main/res/values-sw600dp-land/integers.xml | 19 + app/src/main/res/values-sw600dp/dimens.xml | 74 + app/src/main/res/values-sw600dp/integers.xml | 19 + app/src/main/res/values-v19/themes.xml | 23 + app/src/main/res/values-v21/dimens.xml | 21 + app/src/main/res/values-w820dp/dimens.xml | 22 + app/src/main/res/values/arrays.xml | 44 + app/src/main/res/values/attr.xml | 100 + app/src/main/res/values/colors.xml | 79 + app/src/main/res/values/dimens.xml | 117 + app/src/main/res/values/integers.xml | 26 + app/src/main/res/values/material_colors.xml | 311 + app/src/main/res/values/strings.xml | 281 + app/src/main/res/values/styles.xml | 286 + app/src/main/res/values/themes.xml | 303 + app/src/main/res/values/vpi__attrs.xml | 51 + app/src/main/res/values/vpi__defaults.xml | 27 + app/src/main/res/xml/preferences.xml | 36 + .../IMG_20141204_003903_20141205113958890.jpg | Bin 0 -> 7726175 bytes art/drawer/drawer_remote.png | Bin 0 -> 437872 bytes art/launcher/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2061 bytes art/launcher/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1484 bytes art/launcher/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 2781 bytes .../res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 4448 bytes .../res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 5752 bytes art/launcher/web_hi_res_512.png | Bin 0 -> 18326 bytes art/remote/arrow_master.svg | 80 + art/remote/remote_back.svg | 86 + art/remote/remote_codec.svg | 87 + art/remote/remote_info.svg | 87 + art/remote/remote_menu.svg | 87 + art/remote/remote_select.svg | 109 + art/remote/remote_select_outline.svg | 109 + art/remote/tests/arrow2.svg | 86 + art/remote/tests/arrow3.svg | 80 + art/remote/tests/arrow4.svg | 86 + art/remote/tests/arrow5.svg | 80 + art/remote/tests/arrow_dots.svg | 110 + art/remote/tests/arrow_rect.svg | 87 + art/remote/tests/arrow_stroke.svg | 86 + art/remote/tests/circle.svg | 76 + art/remote/tests/circle2.svg | 148 + art/remote/tests/circle_stroke.svg | 76 + art/remote/tests/remote_button.svg | 76 + art/remote/tests/remote_button_back.png | Bin 0 -> 6732 bytes art/remote/tests/remote_button_back.svg | 84 + .../tests/remote_button_back_square.png | Bin 0 -> 4063 bytes .../tests/remote_button_back_square.svg | 93 + art/remote/tests/remote_button_codec.png | Bin 0 -> 7148 bytes art/remote/tests/remote_button_codec.svg | 84 + .../tests/remote_button_codec_square.png | Bin 0 -> 3430 bytes .../tests/remote_button_codec_square.svg | 93 + art/remote/tests/remote_button_info.png | Bin 0 -> 5365 bytes art/remote/tests/remote_button_info.svg | 84 + .../tests/remote_button_info_square.png | Bin 0 -> 2594 bytes .../tests/remote_button_info_square.svg | 93 + art/remote/tests/remote_button_menu.png | Bin 0 -> 6088 bytes art/remote/tests/remote_button_menu.svg | 84 + .../tests/remote_button_menu_square.png | Bin 0 -> 2888 bytes .../tests/remote_button_menu_square.svg | 93 + art/remote/tests/remote_down.png | Bin 0 -> 5110 bytes art/remote/tests/remote_down_dots.png | Bin 0 -> 2993 bytes art/remote/tests/remote_down_simple.png | Bin 0 -> 2336 bytes .../tests/remote_down_simple_stroke.png | Bin 0 -> 5551 bytes art/remote/tests/remote_down_stroke.png | Bin 0 -> 11207 bytes art/remote/tests/remote_left.png | Bin 0 -> 5248 bytes art/remote/tests/remote_left_dots.png | Bin 0 -> 3148 bytes art/remote/tests/remote_left_simple.png | Bin 0 -> 2351 bytes .../tests/remote_left_simple_stroke.png | Bin 0 -> 5165 bytes art/remote/tests/remote_left_stroke.png | Bin 0 -> 11437 bytes art/remote/tests/remote_right.png | Bin 0 -> 5121 bytes art/remote/tests/remote_right_dots.png | Bin 0 -> 3104 bytes art/remote/tests/remote_right_simple.png | Bin 0 -> 2326 bytes .../tests/remote_right_simple_stroke.png | Bin 0 -> 5002 bytes art/remote/tests/remote_right_stroke.png | Bin 0 -> 11317 bytes art/remote/tests/remote_select.png | Bin 0 -> 4341 bytes art/remote/tests/remote_select_dots.png | Bin 0 -> 10468 bytes art/remote/tests/remote_select_stroke.png | Bin 0 -> 7351 bytes art/remote/tests/remote_triangle_back.png | Bin 0 -> 3736 bytes art/remote/tests/remote_triangle_back.svg | 93 + art/remote/tests/remote_triangle_codec.png | Bin 0 -> 3777 bytes art/remote/tests/remote_triangle_codec.svg | 92 + art/remote/tests/remote_triangle_info.png | Bin 0 -> 2608 bytes art/remote/tests/remote_triangle_info.svg | 92 + art/remote/tests/remote_triangle_menu.png | Bin 0 -> 2914 bytes art/remote/tests/remote_triangle_menu.svg | 92 + art/remote/tests/remote_up.png | Bin 0 -> 4898 bytes art/remote/tests/remote_up_dots.png | Bin 0 -> 2976 bytes art/remote/tests/remote_up_simple.png | Bin 0 -> 2299 bytes art/remote/tests/remote_up_simple_stroke.png | Bin 0 -> 5584 bytes art/remote/tests/remote_up_stroke.png | Bin 0 -> 10747 bytes .../xxhdpi-sw600dp/remote_back_black.png | Bin 0 -> 4242 bytes .../xxhdpi-sw600dp/remote_back_white.png | Bin 0 -> 3673 bytes .../xxhdpi-sw600dp/remote_codec_black.png | Bin 0 -> 5153 bytes .../xxhdpi-sw600dp/remote_codec_white.png | Bin 0 -> 4061 bytes .../xxhdpi-sw600dp/remote_down_black.png | Bin 0 -> 2409 bytes .../xxhdpi-sw600dp/remote_down_white.png | Bin 0 -> 2735 bytes .../xxhdpi-sw600dp/remote_info_black.png | Bin 0 -> 3109 bytes .../xxhdpi-sw600dp/remote_info_white.png | Bin 0 -> 2571 bytes .../xxhdpi-sw600dp/remote_left_black.png | Bin 0 -> 2548 bytes .../xxhdpi-sw600dp/remote_left_white.png | Bin 0 -> 2969 bytes .../xxhdpi-sw600dp/remote_menu_black.png | Bin 0 -> 3850 bytes .../xxhdpi-sw600dp/remote_menu_white.png | Bin 0 -> 2906 bytes .../xxhdpi-sw600dp/remote_right_black.png | Bin 0 -> 2557 bytes .../xxhdpi-sw600dp/remote_right_white.png | Bin 0 -> 2658 bytes .../xxhdpi-sw600dp/remote_select_black.png | Bin 0 -> 2013 bytes .../xxhdpi-sw600dp/remote_select_white.png | Bin 0 -> 2098 bytes art/remote/xxhdpi-sw600dp/remote_up_black.png | Bin 0 -> 2515 bytes art/remote/xxhdpi-sw600dp/remote_up_white.png | Bin 0 -> 2751 bytes art/remote/xxhdpi/remote_back_black.png | Bin 0 -> 2779 bytes art/remote/xxhdpi/remote_back_white.png | Bin 0 -> 2280 bytes art/remote/xxhdpi/remote_codec_black.png | Bin 0 -> 3372 bytes art/remote/xxhdpi/remote_codec_white.png | Bin 0 -> 2568 bytes art/remote/xxhdpi/remote_down_black.png | Bin 0 -> 1655 bytes art/remote/xxhdpi/remote_down_white.png | Bin 0 -> 1840 bytes art/remote/xxhdpi/remote_info_black.png | Bin 0 -> 2095 bytes art/remote/xxhdpi/remote_info_white.png | Bin 0 -> 1698 bytes art/remote/xxhdpi/remote_left_black.png | Bin 0 -> 1692 bytes art/remote/xxhdpi/remote_left_white.png | Bin 0 -> 1945 bytes art/remote/xxhdpi/remote_menu_black.png | Bin 0 -> 2541 bytes art/remote/xxhdpi/remote_menu_white.png | Bin 0 -> 1855 bytes art/remote/xxhdpi/remote_right_black.png | Bin 0 -> 1655 bytes art/remote/xxhdpi/remote_right_white.png | Bin 0 -> 1741 bytes art/remote/xxhdpi/remote_select.png | Bin 0 -> 1275 bytes art/remote/xxhdpi/remote_select_black.png | Bin 0 -> 1269 bytes art/remote/xxhdpi/remote_select_outline.png | Bin 0 -> 1206 bytes art/remote/xxhdpi/remote_select_white.png | Bin 0 -> 1250 bytes art/remote/xxhdpi/remote_up_black.png | Bin 0 -> 1695 bytes art/remote/xxhdpi/remote_up_white.png | Bin 0 -> 1841 bytes art/screenshots/feature_graphic.png | Bin 0 -> 24972 bytes art/screenshots/feature_graphic.xcf | Bin 0 -> 125167 bytes .../v0.9/device-2015-01-13-112107.png | Bin 0 -> 690690 bytes .../v0.9/device-2015-01-13-112118.png | Bin 0 -> 1184059 bytes .../v0.9/device-2015-01-13-112128.png | Bin 0 -> 520828 bytes .../v0.9/device-2015-01-13-112149.png | Bin 0 -> 834639 bytes .../v0.9/device-2015-01-13-112223.png | Bin 0 -> 1015730 bytes .../v0.9/device-2015-01-13-112258.png | Bin 0 -> 725430 bytes .../v0.9/device-2015-01-13-112308.png | Bin 0 -> 1017497 bytes .../v0.9/device-2015-01-13-112343.png | Bin 0 -> 1019370 bytes .../v0.9/device-2015-01-13-112354.png | Bin 0 -> 727678 bytes .../v0.9/device-2015-01-13-112408.png | Bin 0 -> 693071 bytes .../v0.9/device-2015-01-13-112416.png | Bin 0 -> 1182021 bytes .../v0.9/device-2015-01-13-112438.png | Bin 0 -> 330791 bytes .../v0.9/device-2015-01-13-112529.png | Bin 0 -> 729083 bytes .../v0.9/device-2015-01-13-112540.png | Bin 0 -> 1021043 bytes .../v0.9/device-2015-01-13-112551.png | Bin 0 -> 839967 bytes .../v0.9/device-2015-01-13-112930.png | Bin 0 -> 223587 bytes .../v0.9/device-2015-01-13-112945.png | Bin 0 -> 224820 bytes .../v0.9/device-2015-01-13-120444.png | Bin 0 -> 361413 bytes art/screenshots/v0.9/now_playing_nexus5.png | Bin 0 -> 2948003 bytes art/screenshots/v0.9/now_playing_nexus6.png | Bin 0 -> 4140596 bytes build.gradle | 19 + doc/json_responses/Audio.Details.Album.json | 327 + doc/json_responses/Audio.Details.Artist.json | 376 + doc/json_responses/Audio.Details.Song.json | 1720 ++++ doc/json_responses/Introspect-Frodo.json | 6127 ++++++++++++ doc/json_responses/Introspect-Gotham.json | 7490 +++++++++++++++ doc/json_responses/List.Item.All-Movies.json | 168 + doc/json_responses/List.Item.All-Music.json | 55 + doc/json_responses/List.Item.All-Series.json | 416 + doc/json_responses/Video.Details.Episode.json | 1585 +++ doc/json_responses/Video.Details.Movie.json | 1306 +++ .../Video.Details.MusicVideo.json | 165 + doc/json_responses/Video.Details.Season.json | 44 + doc/json_responses/Video.Details.TVShows.json | 630 ++ doc/json_responses/introspect.json | 8560 +++++++++++++++++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 49896 bytes gradlew | 164 + gradlew.bat | 90 + settings.gradle | 1 + 549 files changed, 68066 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE.txt create mode 100644 app/.gitignore create mode 100644 app/build.gradle create mode 100644 app/proguard-rules.pro create mode 100644 app/src/androidTest/java/com/syncedsynapse/kore2/ApplicationTest.java create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/aidl/com/android/vending/billing/IInAppBillingService.aidl create mode 100644 app/src/main/java/com/syncedsynapse/kore2/Settings.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/billing/Base64.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/billing/Base64DecoderException.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/billing/IabException.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/billing/IabHelper.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/billing/IabResult.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/billing/Inventory.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/billing/Purchase.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/billing/Security.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/billing/SkuDetails.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/host/HostConnectionObserver.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/host/HostInfo.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/host/HostManager.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/jsonrpc/ApiCallback.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/jsonrpc/ApiException.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/jsonrpc/ApiMethod.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/jsonrpc/ApiNotification.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/jsonrpc/HostConnection.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/jsonrpc/event/MediaSyncEvent.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/Addons.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/Application.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/AudioLibrary.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/Files.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/GUI.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/Input.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/JSONRPC.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/Player.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/Playlist.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/System.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/VideoLibrary.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/jsonrpc/notification/Input.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/jsonrpc/notification/Player.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/jsonrpc/notification/System.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/AddonType.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/ApiParameter.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/ApplicationType.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/AudioType.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/FilesType.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/GlobalType.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/ItemType.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/LibraryType.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/ListType.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/MediaType.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/PlayerType.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/PlaylistType.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/VideoType.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/provider/MediaContract.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/provider/MediaDatabase.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/provider/MediaProvider.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/service/LibrarySyncService.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/service/SyncUtils.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/ui/AboutDialogFragment.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/ui/AddonDetailsFragment.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/ui/AddonListFragment.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/ui/AddonsActivity.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/ui/AlbumDetailsFragment.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/ui/AlbumListFragment.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/ui/ArtistListFragment.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/ui/AudioGenresListFragment.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/ui/BaseActivity.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/ui/GenericSelectDialog.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/ui/HostConnectionActivity.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/ui/MovieDetailsFragment.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/ui/MovieListFragment.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/ui/MoviesActivity.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/ui/MusicActivity.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/ui/MusicListFragment.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/ui/MusicVideoDetailsFragment.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/ui/MusicVideoListFragment.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/ui/NavigationDrawerFragment.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/ui/NowPlayingFragment.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/ui/PlaylistFragment.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/ui/RemoteActivity.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/ui/RemoteFragment.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/ui/SendTextDialogFragment.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/ui/SettingsActivity.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/ui/SettingsFragment.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/ui/TVShowDetailsFragment.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/ui/TVShowEpisodeDetailsFragment.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/ui/TVShowEpisodeListFragment.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/ui/TVShowListFragment.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/ui/TVShowOverviewFragment.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/ui/TVShowsActivity.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/ui/hosts/AddHostActivity.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/ui/hosts/AddHostFragmentFinish.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/ui/hosts/AddHostFragmentWelcome.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/ui/hosts/AddHostFragmentZeroconf.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/ui/hosts/EditHostActivity.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/ui/hosts/HostFragmentManualConfiguration.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/ui/hosts/HostListFragment.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/ui/hosts/HostManagerActivity.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/ui/views/CirclePageIndicator.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/ui/views/PageIndicator.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/utils/BasicAuthPicassoDownloader.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/utils/CharacterDrawable.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/utils/FileDownloadHelper.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/utils/JsonUtils.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/utils/LogUtils.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/utils/NetUtils.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/utils/RepeatListener.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/utils/SelectionBuilder.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/utils/TabsAdapter.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/utils/UIUtils.java create mode 100644 app/src/main/java/com/syncedsynapse/kore2/utils/Utils.java create mode 100644 app/src/main/res/anim/activity_in.xml create mode 100644 app/src/main/res/anim/activity_out.xml create mode 100644 app/src/main/res/anim/button_in.xml create mode 100644 app/src/main/res/anim/button_out.xml create mode 100644 app/src/main/res/anim/fragment_details_enter.xml create mode 100644 app/src/main/res/anim/fragment_list_popenter.xml create mode 100644 app/src/main/res/drawable-sw600dp-xxhdpi/remote_back_black.png create mode 100644 app/src/main/res/drawable-sw600dp-xxhdpi/remote_back_white.png create mode 100644 app/src/main/res/drawable-sw600dp-xxhdpi/remote_codec_black.png create mode 100644 app/src/main/res/drawable-sw600dp-xxhdpi/remote_codec_white.png create mode 100644 app/src/main/res/drawable-sw600dp-xxhdpi/remote_down_black.png create mode 100644 app/src/main/res/drawable-sw600dp-xxhdpi/remote_down_white.png create mode 100644 app/src/main/res/drawable-sw600dp-xxhdpi/remote_info_black.png create mode 100644 app/src/main/res/drawable-sw600dp-xxhdpi/remote_info_white.png create mode 100644 app/src/main/res/drawable-sw600dp-xxhdpi/remote_left_black.png create mode 100644 app/src/main/res/drawable-sw600dp-xxhdpi/remote_left_white.png create mode 100644 app/src/main/res/drawable-sw600dp-xxhdpi/remote_menu_black.png create mode 100644 app/src/main/res/drawable-sw600dp-xxhdpi/remote_menu_white.png create mode 100644 app/src/main/res/drawable-sw600dp-xxhdpi/remote_right_black.png create mode 100644 app/src/main/res/drawable-sw600dp-xxhdpi/remote_right_white.png create mode 100644 app/src/main/res/drawable-sw600dp-xxhdpi/remote_select_black.png create mode 100644 app/src/main/res/drawable-sw600dp-xxhdpi/remote_select_white.png create mode 100644 app/src/main/res/drawable-sw600dp-xxhdpi/remote_up_black.png create mode 100644 app/src/main/res/drawable-sw600dp-xxhdpi/remote_up_white.png create mode 100644 app/src/main/res/drawable-xhdpi/drawer_image.png create mode 100644 app/src/main/res/drawable-xhdpi/drawer_shadow.9.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_add_box_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_add_box_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_chevron_left_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_chevron_left_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_chevron_right_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_chevron_right_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_closed_caption_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_closed_caption_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_delete_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_delete_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_done_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_done_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_edit_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_edit_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_expand_less_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_expand_less_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_expand_more_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_expand_more_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_extension_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_extension_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_fast_forward_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_fast_forward_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_fast_rewind_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_fast_rewind_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_games_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_games_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_get_app_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_get_app_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_headset_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_headset_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_home_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_home_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_image_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_image_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_keyboard_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_keyboard_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_launcher.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_menu_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_more_vert_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_more_vert_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_movie_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_movie_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_open_in_new_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_open_in_new_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_pause_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_pause_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_phonelink_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_phonelink_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_play_arrow_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_play_arrow_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_queue_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_queue_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_repeat_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_repeat_one_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_repeat_one_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_repeat_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_search_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_search_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_settings_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_settings_power_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_settings_power_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_settings_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_shuffle_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_shuffle_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_skip_next_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_skip_next_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_skip_previous_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_skip_previous_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_stop_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_stop_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_subtitles_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_subtitles_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_tv_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_tv_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_volume_down_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_volume_down_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_volume_off_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_volume_off_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_volume_up_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_volume_up_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/drawer_shadow.9.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_add_box_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_add_box_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_chevron_left_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_chevron_left_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_chevron_right_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_chevron_right_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_closed_caption_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_closed_caption_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_delete_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_delete_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_done_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_done_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_edit_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_edit_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_expand_less_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_expand_less_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_expand_more_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_expand_more_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_extension_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_extension_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_fast_forward_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_fast_forward_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_fast_rewind_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_fast_rewind_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_games_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_games_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_get_app_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_get_app_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_headset_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_headset_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_home_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_home_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_image_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_image_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_keyboard_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_keyboard_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_launcher.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_menu_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_more_vert_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_more_vert_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_movie_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_movie_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_open_in_new_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_open_in_new_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_pause_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_pause_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_phonelink_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_phonelink_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_play_arrow_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_play_arrow_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_queue_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_queue_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_repeat_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_repeat_one_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_repeat_one_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_repeat_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_search_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_search_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_settings_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_settings_power_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_settings_power_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_settings_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_shuffle_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_shuffle_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_skip_next_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_skip_next_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_skip_previous_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_skip_previous_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_stop_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_stop_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_subtitles_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_subtitles_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_tv_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_tv_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_volume_down_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_volume_down_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_volume_off_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_volume_off_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_volume_up_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_volume_up_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/remote_back_black.png create mode 100644 app/src/main/res/drawable-xxhdpi/remote_back_white.png create mode 100644 app/src/main/res/drawable-xxhdpi/remote_codec_black.png create mode 100644 app/src/main/res/drawable-xxhdpi/remote_codec_white.png create mode 100644 app/src/main/res/drawable-xxhdpi/remote_down_black.png create mode 100644 app/src/main/res/drawable-xxhdpi/remote_down_white.png create mode 100644 app/src/main/res/drawable-xxhdpi/remote_info_black.png create mode 100644 app/src/main/res/drawable-xxhdpi/remote_info_white.png create mode 100644 app/src/main/res/drawable-xxhdpi/remote_left_black.png create mode 100644 app/src/main/res/drawable-xxhdpi/remote_left_white.png create mode 100644 app/src/main/res/drawable-xxhdpi/remote_menu_black.png create mode 100644 app/src/main/res/drawable-xxhdpi/remote_menu_white.png create mode 100644 app/src/main/res/drawable-xxhdpi/remote_right_black.png create mode 100644 app/src/main/res/drawable-xxhdpi/remote_right_white.png create mode 100644 app/src/main/res/drawable-xxhdpi/remote_select_black.png create mode 100644 app/src/main/res/drawable-xxhdpi/remote_select_white.png create mode 100644 app/src/main/res/drawable-xxhdpi/remote_up_black.png create mode 100644 app/src/main/res/drawable-xxhdpi/remote_up_white.png create mode 100644 app/src/main/res/drawable/host_status_indicator.xml create mode 100644 app/src/main/res/layout-land/fragment_remote.xml create mode 100644 app/src/main/res/layout/activity_generic_media.xml create mode 100644 app/src/main/res/layout/activity_host_manager.xml create mode 100644 app/src/main/res/layout/activity_remote.xml create mode 100644 app/src/main/res/layout/activity_settings.xml create mode 100644 app/src/main/res/layout/dialog_send_text.xml create mode 100644 app/src/main/res/layout/empty_view.xml create mode 100644 app/src/main/res/layout/fragment_about.xml create mode 100644 app/src/main/res/layout/fragment_add_host_finish.xml create mode 100644 app/src/main/res/layout/fragment_add_host_manual_configuration.xml create mode 100644 app/src/main/res/layout/fragment_add_host_welcome.xml create mode 100644 app/src/main/res/layout/fragment_add_host_zeroconf.xml create mode 100644 app/src/main/res/layout/fragment_addon_details.xml create mode 100644 app/src/main/res/layout/fragment_album_details.xml create mode 100644 app/src/main/res/layout/fragment_episode_details.xml create mode 100644 app/src/main/res/layout/fragment_generic_media_list.xml create mode 100644 app/src/main/res/layout/fragment_host_list.xml create mode 100644 app/src/main/res/layout/fragment_movie_details.xml create mode 100644 app/src/main/res/layout/fragment_music_list.xml create mode 100644 app/src/main/res/layout/fragment_music_video_details.xml create mode 100644 app/src/main/res/layout/fragment_navigation_drawer.xml create mode 100644 app/src/main/res/layout/fragment_now_playing.xml create mode 100644 app/src/main/res/layout/fragment_playlist.xml create mode 100644 app/src/main/res/layout/fragment_remote.xml create mode 100644 app/src/main/res/layout/fragment_tvshow_details.xml create mode 100644 app/src/main/res/layout/fragment_tvshow_episodes_list.xml create mode 100644 app/src/main/res/layout/fragment_tvshow_overview.xml create mode 100644 app/src/main/res/layout/grid_item_addon.xml create mode 100644 app/src/main/res/layout/grid_item_album.xml create mode 100644 app/src/main/res/layout/grid_item_artist.xml create mode 100644 app/src/main/res/layout/grid_item_audio_genre.xml create mode 100644 app/src/main/res/layout/grid_item_cast.xml create mode 100644 app/src/main/res/layout/grid_item_host.xml create mode 100644 app/src/main/res/layout/grid_item_movie.xml create mode 100644 app/src/main/res/layout/grid_item_music_video.xml create mode 100644 app/src/main/res/layout/grid_item_playlist.xml create mode 100644 app/src/main/res/layout/grid_item_tvshow.xml create mode 100644 app/src/main/res/layout/list_item_episode.xml create mode 100644 app/src/main/res/layout/list_item_navigation_drawer.xml create mode 100644 app/src/main/res/layout/list_item_navigation_drawer_divider.xml create mode 100644 app/src/main/res/layout/list_item_navigation_drawer_host.xml create mode 100644 app/src/main/res/layout/list_item_season.xml create mode 100644 app/src/main/res/layout/list_item_song.xml create mode 100644 app/src/main/res/layout/remote_info_panel.xml create mode 100644 app/src/main/res/layout/toolbar_default.xml create mode 100644 app/src/main/res/layout/wizard_button_bar.xml create mode 100644 app/src/main/res/layout/wizard_title.xml create mode 100644 app/src/main/res/menu/host_manager.xml create mode 100644 app/src/main/res/menu/hostlist_item.xml create mode 100644 app/src/main/res/menu/media_info.xml create mode 100644 app/src/main/res/menu/media_search.xml create mode 100644 app/src/main/res/menu/movie_list.xml create mode 100644 app/src/main/res/menu/playlist.xml create mode 100644 app/src/main/res/menu/playlist_item.xml create mode 100644 app/src/main/res/menu/remote.xml create mode 100644 app/src/main/res/menu/song_item.xml create mode 100644 app/src/main/res/menu/tvshow_episode_list.xml create mode 100644 app/src/main/res/menu/tvshow_list.xml create mode 100644 app/src/main/res/menu/video_overflow.xml create mode 100644 app/src/main/res/transition-v21/media_details.xml create mode 100644 app/src/main/res/values-land/integers.xml create mode 100644 app/src/main/res/values-sw600dp-land/integers.xml create mode 100644 app/src/main/res/values-sw600dp/dimens.xml create mode 100644 app/src/main/res/values-sw600dp/integers.xml create mode 100644 app/src/main/res/values-v19/themes.xml create mode 100644 app/src/main/res/values-v21/dimens.xml create mode 100644 app/src/main/res/values-w820dp/dimens.xml create mode 100644 app/src/main/res/values/arrays.xml create mode 100644 app/src/main/res/values/attr.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/dimens.xml create mode 100644 app/src/main/res/values/integers.xml create mode 100644 app/src/main/res/values/material_colors.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/styles.xml create mode 100644 app/src/main/res/values/themes.xml create mode 100644 app/src/main/res/values/vpi__attrs.xml create mode 100644 app/src/main/res/values/vpi__defaults.xml create mode 100644 app/src/main/res/xml/preferences.xml create mode 100755 art/drawer/IMG_20141204_003903_20141205113958890.jpg create mode 100644 art/drawer/drawer_remote.png create mode 100644 art/launcher/res/mipmap-hdpi/ic_launcher.png create mode 100644 art/launcher/res/mipmap-mdpi/ic_launcher.png create mode 100644 art/launcher/res/mipmap-xhdpi/ic_launcher.png create mode 100644 art/launcher/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 art/launcher/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 art/launcher/web_hi_res_512.png create mode 100644 art/remote/arrow_master.svg create mode 100644 art/remote/remote_back.svg create mode 100644 art/remote/remote_codec.svg create mode 100644 art/remote/remote_info.svg create mode 100644 art/remote/remote_menu.svg create mode 100644 art/remote/remote_select.svg create mode 100644 art/remote/remote_select_outline.svg create mode 100644 art/remote/tests/arrow2.svg create mode 100644 art/remote/tests/arrow3.svg create mode 100644 art/remote/tests/arrow4.svg create mode 100644 art/remote/tests/arrow5.svg create mode 100644 art/remote/tests/arrow_dots.svg create mode 100644 art/remote/tests/arrow_rect.svg create mode 100644 art/remote/tests/arrow_stroke.svg create mode 100644 art/remote/tests/circle.svg create mode 100644 art/remote/tests/circle2.svg create mode 100644 art/remote/tests/circle_stroke.svg create mode 100644 art/remote/tests/remote_button.svg create mode 100644 art/remote/tests/remote_button_back.png create mode 100644 art/remote/tests/remote_button_back.svg create mode 100644 art/remote/tests/remote_button_back_square.png create mode 100644 art/remote/tests/remote_button_back_square.svg create mode 100644 art/remote/tests/remote_button_codec.png create mode 100644 art/remote/tests/remote_button_codec.svg create mode 100644 art/remote/tests/remote_button_codec_square.png create mode 100644 art/remote/tests/remote_button_codec_square.svg create mode 100644 art/remote/tests/remote_button_info.png create mode 100644 art/remote/tests/remote_button_info.svg create mode 100644 art/remote/tests/remote_button_info_square.png create mode 100644 art/remote/tests/remote_button_info_square.svg create mode 100644 art/remote/tests/remote_button_menu.png create mode 100644 art/remote/tests/remote_button_menu.svg create mode 100644 art/remote/tests/remote_button_menu_square.png create mode 100644 art/remote/tests/remote_button_menu_square.svg create mode 100644 art/remote/tests/remote_down.png create mode 100644 art/remote/tests/remote_down_dots.png create mode 100644 art/remote/tests/remote_down_simple.png create mode 100644 art/remote/tests/remote_down_simple_stroke.png create mode 100644 art/remote/tests/remote_down_stroke.png create mode 100644 art/remote/tests/remote_left.png create mode 100644 art/remote/tests/remote_left_dots.png create mode 100644 art/remote/tests/remote_left_simple.png create mode 100644 art/remote/tests/remote_left_simple_stroke.png create mode 100644 art/remote/tests/remote_left_stroke.png create mode 100644 art/remote/tests/remote_right.png create mode 100644 art/remote/tests/remote_right_dots.png create mode 100644 art/remote/tests/remote_right_simple.png create mode 100644 art/remote/tests/remote_right_simple_stroke.png create mode 100644 art/remote/tests/remote_right_stroke.png create mode 100644 art/remote/tests/remote_select.png create mode 100644 art/remote/tests/remote_select_dots.png create mode 100644 art/remote/tests/remote_select_stroke.png create mode 100644 art/remote/tests/remote_triangle_back.png create mode 100644 art/remote/tests/remote_triangle_back.svg create mode 100644 art/remote/tests/remote_triangle_codec.png create mode 100644 art/remote/tests/remote_triangle_codec.svg create mode 100644 art/remote/tests/remote_triangle_info.png create mode 100644 art/remote/tests/remote_triangle_info.svg create mode 100644 art/remote/tests/remote_triangle_menu.png create mode 100644 art/remote/tests/remote_triangle_menu.svg create mode 100644 art/remote/tests/remote_up.png create mode 100644 art/remote/tests/remote_up_dots.png create mode 100644 art/remote/tests/remote_up_simple.png create mode 100644 art/remote/tests/remote_up_simple_stroke.png create mode 100644 art/remote/tests/remote_up_stroke.png create mode 100644 art/remote/xxhdpi-sw600dp/remote_back_black.png create mode 100644 art/remote/xxhdpi-sw600dp/remote_back_white.png create mode 100644 art/remote/xxhdpi-sw600dp/remote_codec_black.png create mode 100644 art/remote/xxhdpi-sw600dp/remote_codec_white.png create mode 100644 art/remote/xxhdpi-sw600dp/remote_down_black.png create mode 100644 art/remote/xxhdpi-sw600dp/remote_down_white.png create mode 100644 art/remote/xxhdpi-sw600dp/remote_info_black.png create mode 100644 art/remote/xxhdpi-sw600dp/remote_info_white.png create mode 100644 art/remote/xxhdpi-sw600dp/remote_left_black.png create mode 100644 art/remote/xxhdpi-sw600dp/remote_left_white.png create mode 100644 art/remote/xxhdpi-sw600dp/remote_menu_black.png create mode 100644 art/remote/xxhdpi-sw600dp/remote_menu_white.png create mode 100644 art/remote/xxhdpi-sw600dp/remote_right_black.png create mode 100644 art/remote/xxhdpi-sw600dp/remote_right_white.png create mode 100644 art/remote/xxhdpi-sw600dp/remote_select_black.png create mode 100644 art/remote/xxhdpi-sw600dp/remote_select_white.png create mode 100644 art/remote/xxhdpi-sw600dp/remote_up_black.png create mode 100644 art/remote/xxhdpi-sw600dp/remote_up_white.png create mode 100644 art/remote/xxhdpi/remote_back_black.png create mode 100644 art/remote/xxhdpi/remote_back_white.png create mode 100644 art/remote/xxhdpi/remote_codec_black.png create mode 100644 art/remote/xxhdpi/remote_codec_white.png create mode 100644 art/remote/xxhdpi/remote_down_black.png create mode 100644 art/remote/xxhdpi/remote_down_white.png create mode 100644 art/remote/xxhdpi/remote_info_black.png create mode 100644 art/remote/xxhdpi/remote_info_white.png create mode 100644 art/remote/xxhdpi/remote_left_black.png create mode 100644 art/remote/xxhdpi/remote_left_white.png create mode 100644 art/remote/xxhdpi/remote_menu_black.png create mode 100644 art/remote/xxhdpi/remote_menu_white.png create mode 100644 art/remote/xxhdpi/remote_right_black.png create mode 100644 art/remote/xxhdpi/remote_right_white.png create mode 100644 art/remote/xxhdpi/remote_select.png create mode 100644 art/remote/xxhdpi/remote_select_black.png create mode 100644 art/remote/xxhdpi/remote_select_outline.png create mode 100644 art/remote/xxhdpi/remote_select_white.png create mode 100644 art/remote/xxhdpi/remote_up_black.png create mode 100644 art/remote/xxhdpi/remote_up_white.png create mode 100755 art/screenshots/feature_graphic.png create mode 100755 art/screenshots/feature_graphic.xcf create mode 100644 art/screenshots/v0.9/device-2015-01-13-112107.png create mode 100644 art/screenshots/v0.9/device-2015-01-13-112118.png create mode 100644 art/screenshots/v0.9/device-2015-01-13-112128.png create mode 100644 art/screenshots/v0.9/device-2015-01-13-112149.png create mode 100644 art/screenshots/v0.9/device-2015-01-13-112223.png create mode 100644 art/screenshots/v0.9/device-2015-01-13-112258.png create mode 100644 art/screenshots/v0.9/device-2015-01-13-112308.png create mode 100644 art/screenshots/v0.9/device-2015-01-13-112343.png create mode 100644 art/screenshots/v0.9/device-2015-01-13-112354.png create mode 100644 art/screenshots/v0.9/device-2015-01-13-112408.png create mode 100644 art/screenshots/v0.9/device-2015-01-13-112416.png create mode 100644 art/screenshots/v0.9/device-2015-01-13-112438.png create mode 100644 art/screenshots/v0.9/device-2015-01-13-112529.png create mode 100644 art/screenshots/v0.9/device-2015-01-13-112540.png create mode 100644 art/screenshots/v0.9/device-2015-01-13-112551.png create mode 100644 art/screenshots/v0.9/device-2015-01-13-112930.png create mode 100644 art/screenshots/v0.9/device-2015-01-13-112945.png create mode 100644 art/screenshots/v0.9/device-2015-01-13-120444.png create mode 100644 art/screenshots/v0.9/now_playing_nexus5.png create mode 100644 art/screenshots/v0.9/now_playing_nexus6.png create mode 100644 build.gradle create mode 100644 doc/json_responses/Audio.Details.Album.json create mode 100644 doc/json_responses/Audio.Details.Artist.json create mode 100644 doc/json_responses/Audio.Details.Song.json create mode 100644 doc/json_responses/Introspect-Frodo.json create mode 100644 doc/json_responses/Introspect-Gotham.json create mode 100644 doc/json_responses/List.Item.All-Movies.json create mode 100644 doc/json_responses/List.Item.All-Music.json create mode 100644 doc/json_responses/List.Item.All-Series.json create mode 100644 doc/json_responses/Video.Details.Episode.json create mode 100644 doc/json_responses/Video.Details.Movie.json create mode 100644 doc/json_responses/Video.Details.MusicVideo.json create mode 100644 doc/json_responses/Video.Details.Season.json create mode 100644 doc/json_responses/Video.Details.TVShows.json create mode 100644 doc/json_responses/introspect.json create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7b58b4d --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Gradle +.gradle + +# Generated files +/build + +# IntelliJ +*.iml +*.ipr +*.iws +.idea/ +#/.idea/workspace.xml +#/.idea/libraries + +# Local configuration file (sdk path, etc) +*.properties + +# Keystores +*.keystore + +# Windows +.DS_Store + +# Linux +*~ diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..f7408b7 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,65 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 21 + buildToolsVersion "21.1.2" + + defaultConfig { + applicationId "com.syncedsynapse.kore2" + minSdkVersion 15 + targetSdkVersion 21 + versionCode 2 + versionName "0.9.0" + + buildConfigField("String", "IAP_KEY", "\"${rootProject.property("IAP_KEY")}\"") + } + + signingConfigs { + release { + def Properties keyProps = new Properties() + keyProps.load(new FileInputStream(file('keystore.properties'))) + + storeFile file(keyProps["store"]) + keyAlias keyProps["alias"] + storePassword keyProps["storePass"] + keyPassword keyProps["pass"] + } + } + + buildTypes { +// debug { +// minifyEnabled true +// proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' +// } + + release { + signingConfig signingConfigs.release + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + packagingOptions { + exclude 'META-INF/DEPENDENCIES' + exclude 'META-INF/NOTICE' + exclude 'META-INF/LICENSE' + exclude 'META-INF/LICENSE.txt' + exclude 'META-INF/NOTICE.txt' + } +} + +dependencies { + compile 'com.android.support:support-v4:21.0.3' + compile 'com.android.support:appcompat-v7:21.0.3' + compile 'com.android.support:cardview-v7:21.0.0' + + compile 'com.fasterxml.jackson.core:jackson-databind:2.4.2' + compile 'com.jakewharton:butterknife:5.1.2' + compile 'com.squareup.picasso:picasso:2.4.0' + compile 'de.greenrobot:eventbus:2.2.1' + compile 'javax.jmdns:jmdns:3.4.1' + compile 'com.astuetz:pagerslidingtabstrip:1.0.1' + compile 'com.melnykov:floatingactionbutton:1.1.0' + + compile fileTree(dir: 'libs', include: ['*.jar']) +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..0c8dbe1 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,43 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /opt/android-sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# Don't obfuscate for now. Obfuscation decreases apk size by about 300k +-dontobfuscate + +# Picasso +-dontwarn com.squareup.okhttp.** + +# Butterknife +-dontwarn butterknife.internal.** +-keep class **$$ViewInjector { *; } +-keepnames class * { @butterknife.InjectView *;} + +# Jackson +-dontskipnonpubliclibraryclassmembers +-keepattributes EnclosingMethod, Signature +#-keep class org.codehaus.** { *; } +-keepnames class com.fasterxml.jackson.** { *; } +-dontwarn com.fasterxml.jackson.databind.** + +# EventBus +-keepclassmembers class ** { + public void onEvent*(**); +} + +# SearchView +-keep class android.support.v7.widget.SearchView { *; } + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/app/src/androidTest/java/com/syncedsynapse/kore2/ApplicationTest.java b/app/src/androidTest/java/com/syncedsynapse/kore2/ApplicationTest.java new file mode 100644 index 0000000..3cca216 --- /dev/null +++ b/app/src/androidTest/java/com/syncedsynapse/kore2/ApplicationTest.java @@ -0,0 +1,13 @@ +package com.syncedsynapse.kore2; + +import android.app.Application; +import android.test.ApplicationTestCase; + +/** + * Testing Fundamentals + */ +public class ApplicationTest extends ApplicationTestCase { + public ApplicationTest() { + super(Application.class); + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..4fe8569 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/aidl/com/android/vending/billing/IInAppBillingService.aidl b/app/src/main/aidl/com/android/vending/billing/IInAppBillingService.aidl new file mode 100644 index 0000000..2a492f7 --- /dev/null +++ b/app/src/main/aidl/com/android/vending/billing/IInAppBillingService.aidl @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.vending.billing; + +import android.os.Bundle; + +/** + * InAppBillingService is the service that provides in-app billing version 3 and beyond. + * This service provides the following features: + * 1. Provides a new API to get details of in-app items published for the app including + * price, type, title and description. + * 2. The purchase flow is synchronous and purchase information is available immediately + * after it completes. + * 3. Purchase information of in-app purchases is maintained within the Google Play system + * till the purchase is consumed. + * 4. An API to consume a purchase of an inapp item. All purchases of one-time + * in-app items are consumable and thereafter can be purchased again. + * 5. An API to get current purchases of the user immediately. This will not contain any + * consumed purchases. + * + * All calls will give a response code with the following possible values + * RESULT_OK = 0 - success + * RESULT_USER_CANCELED = 1 - user pressed back or canceled a dialog + * RESULT_BILLING_UNAVAILABLE = 3 - this billing API version is not supported for the type requested + * RESULT_ITEM_UNAVAILABLE = 4 - requested SKU is not available for purchase + * RESULT_DEVELOPER_ERROR = 5 - invalid arguments provided to the API + * RESULT_ERROR = 6 - Fatal error during the API action + * RESULT_ITEM_ALREADY_OWNED = 7 - Failure to purchase since item is already owned + * RESULT_ITEM_NOT_OWNED = 8 - Failure to consume since item is not owned + */ +interface IInAppBillingService { + /** + * Checks support for the requested billing API version, package and in-app type. + * Minimum API version supported by this interface is 3. + * @param apiVersion the billing version which the app is using + * @param packageName the package name of the calling app + * @param type type of the in-app item being purchased "inapp" for one-time purchases + * and "subs" for subscription. + * @return RESULT_OK(0) on success, corresponding result code on failures + */ + int isBillingSupported(int apiVersion, String packageName, String type); + + /** + * Provides details of a list of SKUs + * Given a list of SKUs of a valid type in the skusBundle, this returns a bundle + * with a list JSON strings containing the productId, price, title and description. + * This API can be called with a maximum of 20 SKUs. + * @param apiVersion billing API version that the Third-party is using + * @param packageName the package name of the calling app + * @param skusBundle bundle containing a StringArrayList of SKUs with key "ITEM_ID_LIST" + * @return Bundle containing the following key-value pairs + * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on + * failure as listed above. + * "DETAILS_LIST" with a StringArrayList containing purchase information + * in JSON format similar to: + * '{ "productId" : "exampleSku", "type" : "inapp", "price" : "$5.00", + * "title : "Example Title", "description" : "This is an example description" }' + */ + Bundle getSkuDetails(int apiVersion, String packageName, String type, in Bundle skusBundle); + + /** + * Returns a pending intent to launch the purchase flow for an in-app item by providing a SKU, + * the type, a unique purchase token and an optional developer payload. + * @param apiVersion billing API version that the app is using + * @param packageName package name of the calling app + * @param sku the SKU of the in-app item as published in the developer console + * @param type the type of the in-app item ("inapp" for one-time purchases + * and "subs" for subscription). + * @param developerPayload optional argument to be sent back with the purchase information + * @return Bundle containing the following key-value pairs + * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on + * failure as listed above. + * "BUY_INTENT" - PendingIntent to start the purchase flow + * + * The Pending intent should be launched with startIntentSenderForResult. When purchase flow + * has completed, the onActivityResult() will give a resultCode of OK or CANCELED. + * If the purchase is successful, the result data will contain the following key-value pairs + * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on + * failure as listed above. + * "INAPP_PURCHASE_DATA" - String in JSON format similar to + * '{"orderId":"12999763169054705758.1371079406387615", + * "packageName":"com.example.app", + * "productId":"exampleSku", + * "purchaseTime":1345678900000, + * "purchaseToken" : "122333444455555", + * "developerPayload":"example developer payload" }' + * "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that + * was signed with the private key of the developer + * TODO: change this to app-specific keys. + */ + Bundle getBuyIntent(int apiVersion, String packageName, String sku, String type, + String developerPayload); + + /** + * Returns the current SKUs owned by the user of the type and package name specified along with + * purchase information and a signature of the data to be validated. + * This will return all SKUs that have been purchased in V3 and managed items purchased using + * V1 and V2 that have not been consumed. + * @param apiVersion billing API version that the app is using + * @param packageName package name of the calling app + * @param type the type of the in-app items being requested + * ("inapp" for one-time purchases and "subs" for subscription). + * @param continuationToken to be set as null for the first call, if the number of owned + * skus are too many, a continuationToken is returned in the response bundle. + * This method can be called again with the continuation token to get the next set of + * owned skus. + * @return Bundle containing the following key-value pairs + * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on + * failure as listed above. + * "INAPP_PURCHASE_ITEM_LIST" - StringArrayList containing the list of SKUs + * "INAPP_PURCHASE_DATA_LIST" - StringArrayList containing the purchase information + * "INAPP_DATA_SIGNATURE_LIST"- StringArrayList containing the signatures + * of the purchase information + * "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the + * next set of in-app purchases. Only set if the + * user has more owned skus than the current list. + */ + Bundle getPurchases(int apiVersion, String packageName, String type, String continuationToken); + + /** + * Consume the last purchase of the given SKU. This will result in this item being removed + * from all subsequent responses to getPurchases() and allow re-purchase of this item. + * @param apiVersion billing API version that the app is using + * @param packageName package name of the calling app + * @param purchaseToken token in the purchase information JSON that identifies the purchase + * to be consumed + * @return 0 if consumption succeeded. Appropriate error values for failures. + */ + int consumePurchase(int apiVersion, String packageName, String purchaseToken); +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/Settings.java b/app/src/main/java/com/syncedsynapse/kore2/Settings.java new file mode 100644 index 0000000..d91d226 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/Settings.java @@ -0,0 +1,136 @@ +/* + * Copyright 2015 Synced Synapse. 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.syncedsynapse.kore2; + +import android.content.Context; +import android.content.SharedPreferences; +import android.text.format.DateUtils; + +import com.syncedsynapse.kore2.utils.LogUtils; + +/** + * Singleton that holds the settings of the app. + * + * Interfaces with {@link android.content.SharedPreferences} to load/store these preferences. + */ +public class Settings { + private static final String TAG = LogUtils.makeLogTag(Settings.class); + + /** + * The update interval for the records in the DB. If the last update is older than this value + * a refresh will be triggered. Aplicable to TV Shows and Movies. + */ + public static final long DB_UPDATE_INTERVAL = 12 * DateUtils.HOUR_IN_MILLIS; +// public static final long DB_UPDATE_INTERVAL = DateUtils.MINUTE_IN_MILLIS; + + // Constants for Shared Preferences + private static final String SETTINGS_KEY = "SETTINGS_SHARED_PREFS"; + + // Tags to save the values + private static final String CURRENT_HOST_ID = "CURRENT_HOST_ID"; + private static final String MAX_CAST_PICTURES = "MAX_CAST_PICTURES"; + private static final String MOVIES_FILTER_HIDE_WATCHED = "MOVIES_FILTER_HIDE_WATCHED"; + private static final String TVSHOWS_FILTER_HIDE_WATCHED = "TVSHOWS_FILTER_HIDE_WATCHED"; + private static final String TVSHOW_EPISODES_FILTER_HIDE_WATCHED = "TVSHOW_EPISODES_FILTER_HIDE_WATCHED"; + + private static final String SHOW_THANKS_FOR_COFFEE_MESSAGE = "SHOW_THANKS_FOR_COFFEE_MESSAGE"; + private static final String HAS_BOUGHT_COFFEE = "HAS_BOUGHT_COFFEE"; + + // Default values + private static final int DEFAULT_MAX_CAST_PICTURES = 12; + + // Singleton instance + private static Settings instance = null; + private Context context; + + /** + * Current saved host id + */ + public int currentHostId; + /** + * Maximum pictures to show on cast list (-1 to show all) + */ + public int maxCastPictures; + /** + * Filter watched movies on movie list + */ + public boolean moviesFilterHideWatched; + /** + * Filter watched tv shows on list (all episodes) + */ + public boolean tvshowsFilterHideWatched; + /** + * Filter watched episodes of a tv shows on list + */ + public boolean tvshowEpisodesFilterHideWatched; + + /** + * Show the thanks for coffee message + */ + public boolean showThanksForCofeeMessage; + + /** + * Local variable to save the last state of the coffe purchase + */ + public boolean hasBoughtCoffee; + + /** + * Protected singleton constructor. Loads all the preferences + * @param context App context + */ + protected Settings(Context context) { + this.context = context.getApplicationContext(); + + SharedPreferences preferences = context.getSharedPreferences(SETTINGS_KEY, Context.MODE_PRIVATE); + + currentHostId = preferences.getInt(CURRENT_HOST_ID, -1); + maxCastPictures = preferences.getInt(MAX_CAST_PICTURES, DEFAULT_MAX_CAST_PICTURES); +// maxCastPictures = 12; + moviesFilterHideWatched = preferences.getBoolean(MOVIES_FILTER_HIDE_WATCHED, false); + tvshowsFilterHideWatched = preferences.getBoolean(TVSHOWS_FILTER_HIDE_WATCHED, false); + tvshowEpisodesFilterHideWatched = preferences.getBoolean(TVSHOW_EPISODES_FILTER_HIDE_WATCHED, false); + showThanksForCofeeMessage = preferences.getBoolean(SHOW_THANKS_FOR_COFFEE_MESSAGE, true); + hasBoughtCoffee = preferences.getBoolean(HAS_BOUGHT_COFFEE, false); + } + + /** + * Returns the singleton {@link com.syncedsynapse.kore2.Settings} object. + * @param context App context + * @return Singleton instance + */ + public static Settings getInstance(Context context) { + if (instance == null) + instance = new Settings(context); + return instance; + } + + /** + * Save the current values in {@link android.content.SharedPreferences} + */ + public void save() { + SharedPreferences preferences = context.getSharedPreferences(SETTINGS_KEY, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = preferences.edit(); + + editor.putInt(CURRENT_HOST_ID, currentHostId); + editor.putInt(MAX_CAST_PICTURES, maxCastPictures); + editor.putBoolean(MOVIES_FILTER_HIDE_WATCHED, moviesFilterHideWatched); + editor.putBoolean(TVSHOWS_FILTER_HIDE_WATCHED, tvshowsFilterHideWatched); + editor.putBoolean(TVSHOW_EPISODES_FILTER_HIDE_WATCHED, tvshowEpisodesFilterHideWatched); + editor.putBoolean(SHOW_THANKS_FOR_COFFEE_MESSAGE, showThanksForCofeeMessage); + editor.putBoolean(HAS_BOUGHT_COFFEE, hasBoughtCoffee); + editor.apply(); + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/billing/Base64.java b/app/src/main/java/com/syncedsynapse/kore2/billing/Base64.java new file mode 100644 index 0000000..4dc9cb2 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/billing/Base64.java @@ -0,0 +1,570 @@ +// Portions copyright 2002, Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.syncedsynapse.kore2.billing; + +// This code was converted from code at http://iharder.sourceforge.net/base64/ +// Lots of extraneous features were removed. +/* The original code said: + *

+ * I am placing this code in the Public Domain. Do with it as you will. + * This software comes with no guarantees or warranties but with + * plenty of well-wishing instead! + * Please visit + * http://iharder.net/xmlizable + * periodically to check for updates or to contribute improvements. + *

+ * + * @author Robert Harder + * @author rharder@usa.net + * @version 1.3 + */ + +/** + * Base64 converter class. This code is not a complete MIME encoder; + * it simply converts binary data to base64 data and back. + * + *

Note {@link CharBase64} is a GWT-compatible implementation of this + * class. + */ +public class Base64 { + /** Specify encoding (value is {@code true}). */ + public final static boolean ENCODE = true; + + /** Specify decoding (value is {@code false}). */ + public final static boolean DECODE = false; + + /** The equals sign (=) as a byte. */ + private final static byte EQUALS_SIGN = (byte) '='; + + /** The new line character (\n) as a byte. */ + private final static byte NEW_LINE = (byte) '\n'; + + /** + * The 64 valid Base64 values. + */ + private final static byte[] ALPHABET = + {(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', + (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', + (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P', + (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', + (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', + (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', + (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', + (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o', + (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', + (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', + (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3', + (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8', + (byte) '9', (byte) '+', (byte) '/'}; + + /** + * The 64 valid web safe Base64 values. + */ + private final static byte[] WEBSAFE_ALPHABET = + {(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', + (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', + (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P', + (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', + (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', + (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', + (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', + (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o', + (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', + (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', + (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3', + (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8', + (byte) '9', (byte) '-', (byte) '_'}; + + /** + * Translates a Base64 value to either its 6-bit reconstruction value + * or a negative number indicating some other meaning. + **/ + private final static byte[] DECODABET = {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8 + -5, -5, // Whitespace: Tab and Linefeed + -9, -9, // Decimal 11 - 12 + -5, // Whitespace: Carriage Return + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 + -9, -9, -9, -9, -9, // Decimal 27 - 31 + -5, // Whitespace: Space + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42 + 62, // Plus sign at decimal 43 + -9, -9, -9, // Decimal 44 - 46 + 63, // Slash at decimal 47 + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine + -9, -9, -9, // Decimal 58 - 60 + -1, // Equals sign at decimal 61 + -9, -9, -9, // Decimal 62 - 64 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N' + 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z' + -9, -9, -9, -9, -9, -9, // Decimal 91 - 96 + 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm' + 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z' + -9, -9, -9, -9, -9 // Decimal 123 - 127 + /* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */ + }; + + /** The web safe decodabet */ + private final static byte[] WEBSAFE_DECODABET = + {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8 + -5, -5, // Whitespace: Tab and Linefeed + -9, -9, // Decimal 11 - 12 + -5, // Whitespace: Carriage Return + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 + -9, -9, -9, -9, -9, // Decimal 27 - 31 + -5, // Whitespace: Space + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 44 + 62, // Dash '-' sign at decimal 45 + -9, -9, // Decimal 46-47 + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine + -9, -9, -9, // Decimal 58 - 60 + -1, // Equals sign at decimal 61 + -9, -9, -9, // Decimal 62 - 64 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N' + 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z' + -9, -9, -9, -9, // Decimal 91-94 + 63, // Underscore '_' at decimal 95 + -9, // Decimal 96 + 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm' + 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z' + -9, -9, -9, -9, -9 // Decimal 123 - 127 + /* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */ + }; + + // Indicates white space in encoding + private final static byte WHITE_SPACE_ENC = -5; + // Indicates equals sign in encoding + private final static byte EQUALS_SIGN_ENC = -1; + + /** Defeats instantiation. */ + private Base64() { + } + + /* ******** E N C O D I N G M E T H O D S ******** */ + + /** + * Encodes up to three bytes of the array source + * and writes the resulting four Base64 bytes to destination. + * The source and destination arrays can be manipulated + * anywhere along their length by specifying + * srcOffset and destOffset. + * This method does not check to make sure your arrays + * are large enough to accommodate srcOffset + 3 for + * the source array or destOffset + 4 for + * the destination array. + * The actual number of significant bytes in your array is + * given by numSigBytes. + * + * @param source the array to convert + * @param srcOffset the index where conversion begins + * @param numSigBytes the number of significant bytes in your array + * @param destination the array to hold the conversion + * @param destOffset the index where output will be put + * @param alphabet is the encoding alphabet + * @return the destination array + * @since 1.3 + */ + private static byte[] encode3to4(byte[] source, int srcOffset, + int numSigBytes, byte[] destination, int destOffset, byte[] alphabet) { + // 1 2 3 + // 01234567890123456789012345678901 Bit position + // --------000000001111111122222222 Array position from threeBytes + // --------| || || || | Six bit groups to index alphabet + // >>18 >>12 >> 6 >> 0 Right shift necessary + // 0x3f 0x3f 0x3f Additional AND + + // Create buffer with zero-padding if there are only one or two + // significant bytes passed in the array. + // We have to shift left 24 in order to flush out the 1's that appear + // when Java treats a value as negative that is cast from a byte to an int. + int inBuff = + (numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0) + | (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0) + | (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0); + + switch (numSigBytes) { + case 3: + destination[destOffset] = alphabet[(inBuff >>> 18)]; + destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; + destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f]; + destination[destOffset + 3] = alphabet[(inBuff) & 0x3f]; + return destination; + case 2: + destination[destOffset] = alphabet[(inBuff >>> 18)]; + destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; + destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f]; + destination[destOffset + 3] = EQUALS_SIGN; + return destination; + case 1: + destination[destOffset] = alphabet[(inBuff >>> 18)]; + destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; + destination[destOffset + 2] = EQUALS_SIGN; + destination[destOffset + 3] = EQUALS_SIGN; + return destination; + default: + return destination; + } // end switch + } // end encode3to4 + + /** + * Encodes a byte array into Base64 notation. + * Equivalent to calling + * {@code encodeBytes(source, 0, source.length)} + * + * @param source The data to convert + * @since 1.4 + */ + public static String encode(byte[] source) { + return encode(source, 0, source.length, ALPHABET, true); + } + + /** + * Encodes a byte array into web safe Base64 notation. + * + * @param source The data to convert + * @param doPadding is {@code true} to pad result with '=' chars + * if it does not fall on 3 byte boundaries + */ + public static String encodeWebSafe(byte[] source, boolean doPadding) { + return encode(source, 0, source.length, WEBSAFE_ALPHABET, doPadding); + } + + /** + * Encodes a byte array into Base64 notation. + * + * @param source the data to convert + * @param off offset in array where conversion should begin + * @param len length of data to convert + * @param alphabet the encoding alphabet + * @param doPadding is {@code true} to pad result with '=' chars + * if it does not fall on 3 byte boundaries + * @since 1.4 + */ + public static String encode(byte[] source, int off, int len, byte[] alphabet, + boolean doPadding) { + byte[] outBuff = encode(source, off, len, alphabet, Integer.MAX_VALUE); + int outLen = outBuff.length; + + // If doPadding is false, set length to truncate '=' + // padding characters + while (doPadding == false && outLen > 0) { + if (outBuff[outLen - 1] != '=') { + break; + } + outLen -= 1; + } + + return new String(outBuff, 0, outLen); + } + + /** + * Encodes a byte array into Base64 notation. + * + * @param source the data to convert + * @param off offset in array where conversion should begin + * @param len length of data to convert + * @param alphabet is the encoding alphabet + * @param maxLineLength maximum length of one line. + * @return the BASE64-encoded byte array + */ + public static byte[] encode(byte[] source, int off, int len, byte[] alphabet, + int maxLineLength) { + int lenDiv3 = (len + 2) / 3; // ceil(len / 3) + int len43 = lenDiv3 * 4; + byte[] outBuff = new byte[len43 // Main 4:3 + + (len43 / maxLineLength)]; // New lines + + int d = 0; + int e = 0; + int len2 = len - 2; + int lineLength = 0; + for (; d < len2; d += 3, e += 4) { + + // The following block of code is the same as + // encode3to4( source, d + off, 3, outBuff, e, alphabet ); + // but inlined for faster encoding (~20% improvement) + int inBuff = + ((source[d + off] << 24) >>> 8) + | ((source[d + 1 + off] << 24) >>> 16) + | ((source[d + 2 + off] << 24) >>> 24); + outBuff[e] = alphabet[(inBuff >>> 18)]; + outBuff[e + 1] = alphabet[(inBuff >>> 12) & 0x3f]; + outBuff[e + 2] = alphabet[(inBuff >>> 6) & 0x3f]; + outBuff[e + 3] = alphabet[(inBuff) & 0x3f]; + + lineLength += 4; + if (lineLength == maxLineLength) { + outBuff[e + 4] = NEW_LINE; + e++; + lineLength = 0; + } // end if: end of line + } // end for: each piece of array + + if (d < len) { + encode3to4(source, d + off, len - d, outBuff, e, alphabet); + + lineLength += 4; + if (lineLength == maxLineLength) { + // Add a last newline + outBuff[e + 4] = NEW_LINE; + e++; + } + e += 4; + } + + assert (e == outBuff.length); + return outBuff; + } + + + /* ******** D E C O D I N G M E T H O D S ******** */ + + + /** + * Decodes four bytes from array source + * and writes the resulting bytes (up to three of them) + * to destination. + * The source and destination arrays can be manipulated + * anywhere along their length by specifying + * srcOffset and destOffset. + * This method does not check to make sure your arrays + * are large enough to accommodate srcOffset + 4 for + * the source array or destOffset + 3 for + * the destination array. + * This method returns the actual number of bytes that + * were converted from the Base64 encoding. + * + * + * @param source the array to convert + * @param srcOffset the index where conversion begins + * @param destination the array to hold the conversion + * @param destOffset the index where output will be put + * @param decodabet the decodabet for decoding Base64 content + * @return the number of decoded bytes converted + * @since 1.3 + */ + private static int decode4to3(byte[] source, int srcOffset, + byte[] destination, int destOffset, byte[] decodabet) { + // Example: Dk== + if (source[srcOffset + 2] == EQUALS_SIGN) { + int outBuff = + ((decodabet[source[srcOffset]] << 24) >>> 6) + | ((decodabet[source[srcOffset + 1]] << 24) >>> 12); + + destination[destOffset] = (byte) (outBuff >>> 16); + return 1; + } else if (source[srcOffset + 3] == EQUALS_SIGN) { + // Example: DkL= + int outBuff = + ((decodabet[source[srcOffset]] << 24) >>> 6) + | ((decodabet[source[srcOffset + 1]] << 24) >>> 12) + | ((decodabet[source[srcOffset + 2]] << 24) >>> 18); + + destination[destOffset] = (byte) (outBuff >>> 16); + destination[destOffset + 1] = (byte) (outBuff >>> 8); + return 2; + } else { + // Example: DkLE + int outBuff = + ((decodabet[source[srcOffset]] << 24) >>> 6) + | ((decodabet[source[srcOffset + 1]] << 24) >>> 12) + | ((decodabet[source[srcOffset + 2]] << 24) >>> 18) + | ((decodabet[source[srcOffset + 3]] << 24) >>> 24); + + destination[destOffset] = (byte) (outBuff >> 16); + destination[destOffset + 1] = (byte) (outBuff >> 8); + destination[destOffset + 2] = (byte) (outBuff); + return 3; + } + } // end decodeToBytes + + + /** + * Decodes data from Base64 notation. + * + * @param s the string to decode (decoded in default encoding) + * @return the decoded data + * @since 1.4 + */ + public static byte[] decode(String s) throws Base64DecoderException { + byte[] bytes = s.getBytes(); + return decode(bytes, 0, bytes.length); + } + + /** + * Decodes data from web safe Base64 notation. + * Web safe encoding uses '-' instead of '+', '_' instead of '/' + * + * @param s the string to decode (decoded in default encoding) + * @return the decoded data + */ + public static byte[] decodeWebSafe(String s) throws Base64DecoderException { + byte[] bytes = s.getBytes(); + return decodeWebSafe(bytes, 0, bytes.length); + } + + /** + * Decodes Base64 content in byte array format and returns + * the decoded byte array. + * + * @param source The Base64 encoded data + * @return decoded data + * @since 1.3 + * @throws Base64DecoderException + */ + public static byte[] decode(byte[] source) throws Base64DecoderException { + return decode(source, 0, source.length); + } + + /** + * Decodes web safe Base64 content in byte array format and returns + * the decoded data. + * Web safe encoding uses '-' instead of '+', '_' instead of '/' + * + * @param source the string to decode (decoded in default encoding) + * @return the decoded data + */ + public static byte[] decodeWebSafe(byte[] source) + throws Base64DecoderException { + return decodeWebSafe(source, 0, source.length); + } + + /** + * Decodes Base64 content in byte array format and returns + * the decoded byte array. + * + * @param source the Base64 encoded data + * @param off the offset of where to begin decoding + * @param len the length of characters to decode + * @return decoded data + * @since 1.3 + * @throws Base64DecoderException + */ + public static byte[] decode(byte[] source, int off, int len) + throws Base64DecoderException { + return decode(source, off, len, DECODABET); + } + + /** + * Decodes web safe Base64 content in byte array format and returns + * the decoded byte array. + * Web safe encoding uses '-' instead of '+', '_' instead of '/' + * + * @param source the Base64 encoded data + * @param off the offset of where to begin decoding + * @param len the length of characters to decode + * @return decoded data + */ + public static byte[] decodeWebSafe(byte[] source, int off, int len) + throws Base64DecoderException { + return decode(source, off, len, WEBSAFE_DECODABET); + } + + /** + * Decodes Base64 content using the supplied decodabet and returns + * the decoded byte array. + * + * @param source the Base64 encoded data + * @param off the offset of where to begin decoding + * @param len the length of characters to decode + * @param decodabet the decodabet for decoding Base64 content + * @return decoded data + */ + public static byte[] decode(byte[] source, int off, int len, byte[] decodabet) + throws Base64DecoderException { + int len34 = len * 3 / 4; + byte[] outBuff = new byte[2 + len34]; // Upper limit on size of output + int outBuffPosn = 0; + + byte[] b4 = new byte[4]; + int b4Posn = 0; + int i = 0; + byte sbiCrop = 0; + byte sbiDecode = 0; + for (i = 0; i < len; i++) { + sbiCrop = (byte) (source[i + off] & 0x7f); // Only the low seven bits + sbiDecode = decodabet[sbiCrop]; + + if (sbiDecode >= WHITE_SPACE_ENC) { // White space Equals sign or better + if (sbiDecode >= EQUALS_SIGN_ENC) { + // An equals sign (for padding) must not occur at position 0 or 1 + // and must be the last byte[s] in the encoded value + if (sbiCrop == EQUALS_SIGN) { + int bytesLeft = len - i; + byte lastByte = (byte) (source[len - 1 + off] & 0x7f); + if (b4Posn == 0 || b4Posn == 1) { + throw new Base64DecoderException( + "invalid padding byte '=' at byte offset " + i); + } else if ((b4Posn == 3 && bytesLeft > 2) + || (b4Posn == 4 && bytesLeft > 1)) { + throw new Base64DecoderException( + "padding byte '=' falsely signals end of encoded value " + + "at offset " + i); + } else if (lastByte != EQUALS_SIGN && lastByte != NEW_LINE) { + throw new Base64DecoderException( + "encoded value has invalid trailing byte"); + } + break; + } + + b4[b4Posn++] = sbiCrop; + if (b4Posn == 4) { + outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet); + b4Posn = 0; + } + } + } else { + throw new Base64DecoderException("Bad Base64 input character at " + i + + ": " + source[i + off] + "(decimal)"); + } + } + + // Because web safe encoding allows non padding base64 encodes, we + // need to pad the rest of the b4 buffer with equal signs when + // b4Posn != 0. There can be at most 2 equal signs at the end of + // four characters, so the b4 buffer must have two or three + // characters. This also catches the case where the input is + // padded with EQUALS_SIGN + if (b4Posn != 0) { + if (b4Posn == 1) { + throw new Base64DecoderException("single trailing character at offset " + + (len - 1)); + } + b4[b4Posn++] = EQUALS_SIGN; + outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet); + } + + byte[] out = new byte[outBuffPosn]; + System.arraycopy(outBuff, 0, out, 0, outBuffPosn); + return out; + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/billing/Base64DecoderException.java b/app/src/main/java/com/syncedsynapse/kore2/billing/Base64DecoderException.java new file mode 100644 index 0000000..b1265d6 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/billing/Base64DecoderException.java @@ -0,0 +1,32 @@ +// Copyright 2002, Google, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.syncedsynapse.kore2.billing; + +/** + * Exception thrown when encountering an invalid Base64 input character. + * + * @author nelson + */ +public class Base64DecoderException extends Exception { + public Base64DecoderException() { + super(); + } + + public Base64DecoderException(String s) { + super(s); + } + + private static final long serialVersionUID = 1L; +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/billing/IabException.java b/app/src/main/java/com/syncedsynapse/kore2/billing/IabException.java new file mode 100644 index 0000000..df53a0c --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/billing/IabException.java @@ -0,0 +1,43 @@ +/* Copyright (c) 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.syncedsynapse.kore2.billing; + +/** + * Exception thrown when something went wrong with in-app billing. + * An IabException has an associated IabResult (an error). + * To get the IAB result that caused this exception to be thrown, + * call {@link #getResult()}. + */ +public class IabException extends Exception { + IabResult mResult; + + public IabException(IabResult r) { + this(r, null); + } + public IabException(int response, String message) { + this(new IabResult(response, message)); + } + public IabException(IabResult r, Exception cause) { + super(r.getMessage(), cause); + mResult = r; + } + public IabException(int response, String message, Exception cause) { + this(new IabResult(response, message), cause); + } + + /** Returns the IAB result (error) that this exception signals. */ + public IabResult getResult() { return mResult; } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/billing/IabHelper.java b/app/src/main/java/com/syncedsynapse/kore2/billing/IabHelper.java new file mode 100644 index 0000000..df5add5 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/billing/IabHelper.java @@ -0,0 +1,1017 @@ +/* Copyright (c) 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.syncedsynapse.kore2.billing; + +import android.app.Activity; +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentSender.SendIntentException; +import android.content.ServiceConnection; +import android.content.pm.ResolveInfo; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.RemoteException; +import android.text.TextUtils; +import android.util.Log; + +import com.android.vending.billing.IInAppBillingService; + +import org.json.JSONException; + +import java.util.ArrayList; +import java.util.List; + + +/** + * Provides convenience methods for in-app billing. You can create one instance of this + * class for your application and use it to process in-app billing operations. + * It provides synchronous (blocking) and asynchronous (non-blocking) methods for + * many common in-app billing operations, as well as automatic signature + * verification. + * + * After instantiating, you must perform setup in order to start using the object. + * To perform setup, call the {@link #startSetup} method and provide a listener; + * that listener will be notified when setup is complete, after which (and not before) + * you may call other methods. + * + * After setup is complete, you will typically want to request an inventory of owned + * items and subscriptions. See {@link #queryInventory}, {@link #queryInventoryAsync} + * and related methods. + * + * When you are done with this object, don't forget to call {@link #dispose} + * to ensure proper cleanup. This object holds a binding to the in-app billing + * service, which will leak unless you dispose of it correctly. If you created + * the object on an Activity's onCreate method, then the recommended + * place to dispose of it is the Activity's onDestroy method. + * + * A note about threading: When using this object from a background thread, you may + * call the blocking versions of methods; when using from a UI thread, call + * only the asynchronous versions and handle the results via callbacks. + * Also, notice that you can only call one asynchronous operation at a time; + * attempting to start a second asynchronous operation while the first one + * has not yet completed will result in an exception being thrown. + * + * @author Bruno Oliveira (Google) + * + */ +public class IabHelper { + // Is debug logging enabled? + boolean mDebugLog = false; + String mDebugTag = "IabHelper"; + + // Is setup done? + boolean mSetupDone = false; + + // Has this object been disposed of? (If so, we should ignore callbacks, etc) + boolean mDisposed = false; + + // Are subscriptions supported? + boolean mSubscriptionsSupported = false; + + // Is an asynchronous operation in progress? + // (only one at a time can be in progress) + boolean mAsyncInProgress = false; + + // (for logging/debugging) + // if mAsyncInProgress == true, what asynchronous operation is in progress? + String mAsyncOperation = ""; + + // Context we were passed during initialization + Context mContext; + + // Connection to the service + IInAppBillingService mService; + ServiceConnection mServiceConn; + + // The request code used to launch purchase flow + int mRequestCode; + + // The item type of the current purchase flow + String mPurchasingItemType; + + // Public key for verifying signature, in base64 encoding + String mSignatureBase64 = null; + + // Billing response codes + public static final int BILLING_RESPONSE_RESULT_OK = 0; + public static final int BILLING_RESPONSE_RESULT_USER_CANCELED = 1; + public static final int BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE = 3; + public static final int BILLING_RESPONSE_RESULT_ITEM_UNAVAILABLE = 4; + public static final int BILLING_RESPONSE_RESULT_DEVELOPER_ERROR = 5; + public static final int BILLING_RESPONSE_RESULT_ERROR = 6; + public static final int BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED = 7; + public static final int BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED = 8; + + // IAB Helper error codes + public static final int IABHELPER_ERROR_BASE = -1000; + public static final int IABHELPER_REMOTE_EXCEPTION = -1001; + public static final int IABHELPER_BAD_RESPONSE = -1002; + public static final int IABHELPER_VERIFICATION_FAILED = -1003; + public static final int IABHELPER_SEND_INTENT_FAILED = -1004; + public static final int IABHELPER_USER_CANCELLED = -1005; + public static final int IABHELPER_UNKNOWN_PURCHASE_RESPONSE = -1006; + public static final int IABHELPER_MISSING_TOKEN = -1007; + public static final int IABHELPER_UNKNOWN_ERROR = -1008; + public static final int IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE = -1009; + public static final int IABHELPER_INVALID_CONSUMPTION = -1010; + + // Keys for the responses from InAppBillingService + public static final String RESPONSE_CODE = "RESPONSE_CODE"; + public static final String RESPONSE_GET_SKU_DETAILS_LIST = "DETAILS_LIST"; + public static final String RESPONSE_BUY_INTENT = "BUY_INTENT"; + public static final String RESPONSE_INAPP_PURCHASE_DATA = "INAPP_PURCHASE_DATA"; + public static final String RESPONSE_INAPP_SIGNATURE = "INAPP_DATA_SIGNATURE"; + public static final String RESPONSE_INAPP_ITEM_LIST = "INAPP_PURCHASE_ITEM_LIST"; + public static final String RESPONSE_INAPP_PURCHASE_DATA_LIST = "INAPP_PURCHASE_DATA_LIST"; + public static final String RESPONSE_INAPP_SIGNATURE_LIST = "INAPP_DATA_SIGNATURE_LIST"; + public static final String INAPP_CONTINUATION_TOKEN = "INAPP_CONTINUATION_TOKEN"; + + // Item types + public static final String ITEM_TYPE_INAPP = "inapp"; + public static final String ITEM_TYPE_SUBS = "subs"; + + // some fields on the getSkuDetails response bundle + public static final String GET_SKU_DETAILS_ITEM_LIST = "ITEM_ID_LIST"; + public static final String GET_SKU_DETAILS_ITEM_TYPE_LIST = "ITEM_TYPE_LIST"; + + /** + * Creates an instance. After creation, it will not yet be ready to use. You must perform + * setup by calling {@link #startSetup} and wait for setup to complete. This constructor does not + * block and is safe to call from a UI thread. + * + * @param ctx Your application or Activity context. Needed to bind to the in-app billing service. + * @param base64PublicKey Your application's public key, encoded in base64. + * This is used for verification of purchase signatures. You can find your app's base64-encoded + * public key in your application's page on Google Play Developer Console. Note that this + * is NOT your "developer public key". + */ + public IabHelper(Context ctx, String base64PublicKey) { + mContext = ctx.getApplicationContext(); + mSignatureBase64 = base64PublicKey; + logDebug("IAB helper created."); + } + + /** + * Enables or disable debug logging through LogCat. + */ + public void enableDebugLogging(boolean enable, String tag) { + checkNotDisposed(); + mDebugLog = enable; + mDebugTag = tag; + } + + public void enableDebugLogging(boolean enable) { + checkNotDisposed(); + mDebugLog = enable; + } + + /** + * Callback for setup process. This listener's {@link #onIabSetupFinished} method is called + * when the setup process is complete. + */ + public interface OnIabSetupFinishedListener { + /** + * Called to notify that setup is complete. + * + * @param result The result of the setup process. + */ + public void onIabSetupFinished(IabResult result); + } + + /** + * Starts the setup process. This will start up the setup process asynchronously. + * You will be notified through the listener when the setup process is complete. + * This method is safe to call from a UI thread. + * + * @param listener The listener to notify when the setup process is complete. + */ + public void startSetup(final OnIabSetupFinishedListener listener) { + // If already set up, can't do it again. + checkNotDisposed(); + if (mSetupDone) throw new IllegalStateException("IAB helper is already set up."); + + // Connection to IAB service + logDebug("Starting in-app billing setup."); + mServiceConn = new ServiceConnection() { + @Override + public void onServiceDisconnected(ComponentName name) { + logDebug("Billing service disconnected."); + mService = null; + } + + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + if (mDisposed) return; + logDebug("Billing service connected."); + mService = IInAppBillingService.Stub.asInterface(service); + String packageName = mContext.getPackageName(); + try { + logDebug("Checking for in-app billing 3 support."); + + // check for in-app billing v3 support + int response = mService.isBillingSupported(3, packageName, ITEM_TYPE_INAPP); + if (response != BILLING_RESPONSE_RESULT_OK) { + if (listener != null) listener.onIabSetupFinished(new IabResult(response, + "Error checking for billing v3 support.")); + + // if in-app purchases aren't supported, neither are subscriptions. + mSubscriptionsSupported = false; + return; + } + logDebug("In-app billing version 3 supported for " + packageName); + + // check for v3 subscriptions support + response = mService.isBillingSupported(3, packageName, ITEM_TYPE_SUBS); + if (response == BILLING_RESPONSE_RESULT_OK) { + logDebug("Subscriptions AVAILABLE."); + mSubscriptionsSupported = true; + } + else { + logDebug("Subscriptions NOT AVAILABLE. Response: " + response); + } + + mSetupDone = true; + } + catch (RemoteException e) { + if (listener != null) { + listener.onIabSetupFinished(new IabResult(IABHELPER_REMOTE_EXCEPTION, + "RemoteException while setting up in-app billing.")); + } + e.printStackTrace(); + return; + } + + if (listener != null) { + listener.onIabSetupFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Setup successful.")); + } + } + }; + + Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND"); + serviceIntent.setPackage("com.android.vending"); + + List services; + try { + services = mContext.getPackageManager().queryIntentServices(serviceIntent, 0); + } catch (Exception exc) { + services = null; + } + + if ((services == null) || (services.isEmpty())) { + // no service available to handle that Intent + if (listener != null) { + mServiceConn = null; + listener.onIabSetupFinished( + new IabResult(BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE, + "Billing service unavailable on device.")); + } + } + else { + // service available to handle that Intent + mContext.bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE); + } + } + + /** + * Dispose of object, releasing resources. It's very important to call this + * method when you are done with this object. It will release any resources + * used by it such as service connections. Naturally, once the object is + * disposed of, it can't be used again. + */ + public void dispose() { + logDebug("Disposing."); + mSetupDone = false; + if (mServiceConn != null) { + logDebug("Unbinding from service."); + if (mContext != null) mContext.unbindService(mServiceConn); + } + mDisposed = true; + mContext = null; + mServiceConn = null; + mService = null; + mPurchaseListener = null; + } + + private void checkNotDisposed() { + if (mDisposed) throw new IllegalStateException("IabHelper was disposed of, so it cannot be used."); + } + + /** Returns whether subscriptions are supported. */ + public boolean subscriptionsSupported() { + checkNotDisposed(); + return mSubscriptionsSupported; + } + + + /** + * Callback that notifies when a purchase is finished. + */ + public interface OnIabPurchaseFinishedListener { + /** + * Called to notify that an in-app purchase finished. If the purchase was successful, + * then the sku parameter specifies which item was purchased. If the purchase failed, + * the sku and extraData parameters may or may not be null, depending on how far the purchase + * process went. + * + * @param result The result of the purchase. + * @param info The purchase information (null if purchase failed) + */ + public void onIabPurchaseFinished(IabResult result, Purchase info); + } + + // The listener registered on launchPurchaseFlow, which we have to call back when + // the purchase finishes + OnIabPurchaseFinishedListener mPurchaseListener; + + public void launchPurchaseFlow(Activity act, String sku, int requestCode, OnIabPurchaseFinishedListener listener) { + launchPurchaseFlow(act, sku, requestCode, listener, ""); + } + + public void launchPurchaseFlow(Activity act, String sku, int requestCode, + OnIabPurchaseFinishedListener listener, String extraData) { + launchPurchaseFlow(act, sku, ITEM_TYPE_INAPP, requestCode, listener, extraData); + } + + public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode, + OnIabPurchaseFinishedListener listener) { + launchSubscriptionPurchaseFlow(act, sku, requestCode, listener, ""); + } + + public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode, + OnIabPurchaseFinishedListener listener, String extraData) { + launchPurchaseFlow(act, sku, ITEM_TYPE_SUBS, requestCode, listener, extraData); + } + + /** + * Initiate the UI flow for an in-app purchase. Call this method to initiate an in-app purchase, + * which will involve bringing up the Google Play screen. The calling activity will be paused while + * the user interacts with Google Play, and the result will be delivered via the activity's + * {@link android.app.Activity#onActivityResult} method, at which point you must call + * this object's {@link #handleActivityResult} method to continue the purchase flow. This method + * MUST be called from the UI thread of the Activity. + * + * @param act The calling activity. + * @param sku The sku of the item to purchase. + * @param itemType indicates if it's a product or a subscription (ITEM_TYPE_INAPP or ITEM_TYPE_SUBS) + * @param requestCode A request code (to differentiate from other responses -- + * as in {@link android.app.Activity#startActivityForResult}). + * @param listener The listener to notify when the purchase process finishes + * @param extraData Extra data (developer payload), which will be returned with the purchase data + * when the purchase completes. This extra data will be permanently bound to that purchase + * and will always be returned when the purchase is queried. + */ + public void launchPurchaseFlow(Activity act, String sku, String itemType, int requestCode, + OnIabPurchaseFinishedListener listener, String extraData) { + checkNotDisposed(); + checkSetupDone("launchPurchaseFlow"); + flagStartAsync("launchPurchaseFlow"); + IabResult result; + + if (itemType.equals(ITEM_TYPE_SUBS) && !mSubscriptionsSupported) { + IabResult r = new IabResult(IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE, + "Subscriptions are not available."); + flagEndAsync(); + if (listener != null) listener.onIabPurchaseFinished(r, null); + return; + } + + try { + logDebug("Constructing buy intent for " + sku + ", item type: " + itemType); + Bundle buyIntentBundle = mService.getBuyIntent(3, mContext.getPackageName(), sku, itemType, extraData); + int response = getResponseCodeFromBundle(buyIntentBundle); + if (response != BILLING_RESPONSE_RESULT_OK) { + logError("Unable to buy item, Error response: " + getResponseDesc(response)); + flagEndAsync(); + result = new IabResult(response, "Unable to buy item"); + if (listener != null) listener.onIabPurchaseFinished(result, null); + return; + } + + PendingIntent pendingIntent = buyIntentBundle.getParcelable(RESPONSE_BUY_INTENT); + logDebug("Launching buy intent for " + sku + ". Request code: " + requestCode); + mRequestCode = requestCode; + mPurchaseListener = listener; + mPurchasingItemType = itemType; + act.startIntentSenderForResult(pendingIntent.getIntentSender(), + requestCode, new Intent(), + Integer.valueOf(0), Integer.valueOf(0), + Integer.valueOf(0)); + } + catch (SendIntentException e) { + logError("SendIntentException while launching purchase flow for sku " + sku); + e.printStackTrace(); + flagEndAsync(); + + result = new IabResult(IABHELPER_SEND_INTENT_FAILED, "Failed to send intent."); + if (listener != null) listener.onIabPurchaseFinished(result, null); + } + catch (RemoteException e) { + logError("RemoteException while launching purchase flow for sku " + sku); + e.printStackTrace(); + flagEndAsync(); + + result = new IabResult(IABHELPER_REMOTE_EXCEPTION, "Remote exception while starting purchase flow"); + if (listener != null) listener.onIabPurchaseFinished(result, null); + } + } + + /** + * Handles an activity result that's part of the purchase flow in in-app billing. If you + * are calling {@link #launchPurchaseFlow}, then you must call this method from your + * Activity's {@link android.app.Activity@onActivityResult} method. This method + * MUST be called from the UI thread of the Activity. + * + * @param requestCode The requestCode as you received it. + * @param resultCode The resultCode as you received it. + * @param data The data (Intent) as you received it. + * @return Returns true if the result was related to a purchase flow and was handled; + * false if the result was not related to a purchase, in which case you should + * handle it normally. + */ + public boolean handleActivityResult(int requestCode, int resultCode, Intent data) { + IabResult result; + if (requestCode != mRequestCode) return false; + + checkNotDisposed(); + checkSetupDone("handleActivityResult"); + + // end of async purchase operation that started on launchPurchaseFlow + flagEndAsync(); + + if (data == null) { + logError("Null data in IAB activity result."); + result = new IabResult(IABHELPER_BAD_RESPONSE, "Null data in IAB result"); + if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null); + return true; + } + + int responseCode = getResponseCodeFromIntent(data); + String purchaseData = data.getStringExtra(RESPONSE_INAPP_PURCHASE_DATA); + String dataSignature = data.getStringExtra(RESPONSE_INAPP_SIGNATURE); + + if (resultCode == Activity.RESULT_OK && responseCode == BILLING_RESPONSE_RESULT_OK) { + logDebug("Successful resultcode from purchase activity."); + logDebug("Purchase data: " + purchaseData); + logDebug("Data signature: " + dataSignature); + logDebug("Extras: " + data.getExtras()); + logDebug("Expected item type: " + mPurchasingItemType); + + if (purchaseData == null || dataSignature == null) { + logError("BUG: either purchaseData or dataSignature is null."); + logDebug("Extras: " + data.getExtras().toString()); + result = new IabResult(IABHELPER_UNKNOWN_ERROR, "IAB returned null purchaseData or dataSignature"); + if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null); + return true; + } + + Purchase purchase = null; + try { + purchase = new Purchase(mPurchasingItemType, purchaseData, dataSignature); + String sku = purchase.getSku(); + + // Verify signature + if (!Security.verifyPurchase(mSignatureBase64, purchaseData, dataSignature)) { + logError("Purchase signature verification FAILED for sku " + sku); + result = new IabResult(IABHELPER_VERIFICATION_FAILED, "Signature verification failed for sku " + sku); + if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, purchase); + return true; + } + logDebug("Purchase signature successfully verified."); + } + catch (JSONException e) { + logError("Failed to parse purchase data."); + e.printStackTrace(); + result = new IabResult(IABHELPER_BAD_RESPONSE, "Failed to parse purchase data."); + if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null); + return true; + } + + if (mPurchaseListener != null) { + mPurchaseListener.onIabPurchaseFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Success"), purchase); + } + } + else if (resultCode == Activity.RESULT_OK) { + // result code was OK, but in-app billing response was not OK. + logDebug("Result code was OK but in-app billing response was not OK: " + getResponseDesc(responseCode)); + if (mPurchaseListener != null) { + result = new IabResult(responseCode, "Problem purchashing item."); + mPurchaseListener.onIabPurchaseFinished(result, null); + } + } + else if (resultCode == Activity.RESULT_CANCELED) { + logDebug("Purchase canceled - Response: " + getResponseDesc(responseCode)); + result = new IabResult(IABHELPER_USER_CANCELLED, "User canceled."); + if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null); + } + else { + logError("Purchase failed. Result code: " + Integer.toString(resultCode) + + ". Response: " + getResponseDesc(responseCode)); + result = new IabResult(IABHELPER_UNKNOWN_PURCHASE_RESPONSE, "Unknown purchase response."); + if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null); + } + return true; + } + + public Inventory queryInventory(boolean querySkuDetails, List moreSkus) throws IabException { + return queryInventory(querySkuDetails, moreSkus, null); + } + + /** + * Queries the inventory. This will query all owned items from the server, as well as + * information on additional skus, if specified. This method may block or take long to execute. + * Do not call from a UI thread. For that, use the non-blocking version {@link #refreshInventoryAsync}. + * + * @param querySkuDetails if true, SKU details (price, description, etc) will be queried as well + * as purchase information. + * @param moreItemSkus additional PRODUCT skus to query information on, regardless of ownership. + * Ignored if null or if querySkuDetails is false. + * @param moreSubsSkus additional SUBSCRIPTIONS skus to query information on, regardless of ownership. + * Ignored if null or if querySkuDetails is false. + * @throws IabException if a problem occurs while refreshing the inventory. + */ + public Inventory queryInventory(boolean querySkuDetails, List moreItemSkus, + List moreSubsSkus) throws IabException { + checkNotDisposed(); + checkSetupDone("queryInventory"); + try { + Inventory inv = new Inventory(); + int r = queryPurchases(inv, ITEM_TYPE_INAPP); + if (r != BILLING_RESPONSE_RESULT_OK) { + throw new IabException(r, "Error refreshing inventory (querying owned items)."); + } + + if (querySkuDetails) { + r = querySkuDetails(ITEM_TYPE_INAPP, inv, moreItemSkus); + if (r != BILLING_RESPONSE_RESULT_OK) { + throw new IabException(r, "Error refreshing inventory (querying prices of items)."); + } + } + + // if subscriptions are supported, then also query for subscriptions + if (mSubscriptionsSupported) { + r = queryPurchases(inv, ITEM_TYPE_SUBS); + if (r != BILLING_RESPONSE_RESULT_OK) { + throw new IabException(r, "Error refreshing inventory (querying owned subscriptions)."); + } + + if (querySkuDetails) { + r = querySkuDetails(ITEM_TYPE_SUBS, inv, moreItemSkus); + if (r != BILLING_RESPONSE_RESULT_OK) { + throw new IabException(r, "Error refreshing inventory (querying prices of subscriptions)."); + } + } + } + + return inv; + } + catch (RemoteException e) { + throw new IabException(IABHELPER_REMOTE_EXCEPTION, "Remote exception while refreshing inventory.", e); + } + catch (JSONException e) { + throw new IabException(IABHELPER_BAD_RESPONSE, "Error parsing JSON response while refreshing inventory.", e); + } + catch (NullPointerException e) { + throw new IabException(IABHELPER_UNKNOWN_ERROR, "NullPointer while refreshing inventory.", e); + } + } + + /** + * Listener that notifies when an inventory query operation completes. + */ + public interface QueryInventoryFinishedListener { + /** + * Called to notify that an inventory query operation completed. + * + * @param result The result of the operation. + * @param inv The inventory. + */ + public void onQueryInventoryFinished(IabResult result, Inventory inv); + } + + + /** + * Asynchronous wrapper for inventory query. This will perform an inventory + * query as described in {@link #queryInventory}, but will do so asynchronously + * and call back the specified listener upon completion. This method is safe to + * call from a UI thread. + * + * @param querySkuDetails as in {@link #queryInventory} + * @param moreSkus as in {@link #queryInventory} + * @param listener The listener to notify when the refresh operation completes. + */ + public void queryInventoryAsync(final boolean querySkuDetails, + final List moreSkus, + final QueryInventoryFinishedListener listener) { + final Handler handler = new Handler(); + checkNotDisposed(); + checkSetupDone("queryInventory"); + flagStartAsync("refresh inventory"); + (new Thread(new Runnable() { + public void run() { + IabResult result = new IabResult(BILLING_RESPONSE_RESULT_OK, "Inventory refresh successful."); + Inventory inv = null; + try { + inv = queryInventory(querySkuDetails, moreSkus); + } + catch (IabException ex) { + result = ex.getResult(); + } + + flagEndAsync(); + + final IabResult result_f = result; + final Inventory inv_f = inv; + if (!mDisposed && listener != null) { + handler.post(new Runnable() { + public void run() { + listener.onQueryInventoryFinished(result_f, inv_f); + } + }); + } + } + })).start(); + } + + public void queryInventoryAsync(QueryInventoryFinishedListener listener) { + queryInventoryAsync(true, null, listener); + } + + public void queryInventoryAsync(boolean querySkuDetails, QueryInventoryFinishedListener listener) { + queryInventoryAsync(querySkuDetails, null, listener); + } + + + /** + * Consumes a given in-app product. Consuming can only be done on an item + * that's owned, and as a result of consumption, the user will no longer own it. + * This method may block or take long to return. Do not call from the UI thread. + * For that, see {@link #consumeAsync}. + * + * @param itemInfo The PurchaseInfo that represents the item to consume. + * @throws IabException if there is a problem during consumption. + */ + void consume(Purchase itemInfo) throws IabException { + checkNotDisposed(); + checkSetupDone("consume"); + + if (!itemInfo.mItemType.equals(ITEM_TYPE_INAPP)) { + throw new IabException(IABHELPER_INVALID_CONSUMPTION, + "Items of type '" + itemInfo.mItemType + "' can't be consumed."); + } + + try { + String token = itemInfo.getToken(); + String sku = itemInfo.getSku(); + if (token == null || token.equals("")) { + logError("Can't consume "+ sku + ". No token."); + throw new IabException(IABHELPER_MISSING_TOKEN, "PurchaseInfo is missing token for sku: " + + sku + " " + itemInfo); + } + + logDebug("Consuming sku: " + sku + ", token: " + token); + int response = mService.consumePurchase(3, mContext.getPackageName(), token); + if (response == BILLING_RESPONSE_RESULT_OK) { + logDebug("Successfully consumed sku: " + sku); + } + else { + logDebug("Error consuming consuming sku " + sku + ". " + getResponseDesc(response)); + throw new IabException(response, "Error consuming sku " + sku); + } + } + catch (RemoteException e) { + throw new IabException(IABHELPER_REMOTE_EXCEPTION, "Remote exception while consuming. PurchaseInfo: " + itemInfo, e); + } + } + + /** + * Callback that notifies when a consumption operation finishes. + */ + public interface OnConsumeFinishedListener { + /** + * Called to notify that a consumption has finished. + * + * @param purchase The purchase that was (or was to be) consumed. + * @param result The result of the consumption operation. + */ + public void onConsumeFinished(Purchase purchase, IabResult result); + } + + /** + * Callback that notifies when a multi-item consumption operation finishes. + */ + public interface OnConsumeMultiFinishedListener { + /** + * Called to notify that a consumption of multiple items has finished. + * + * @param purchases The purchases that were (or were to be) consumed. + * @param results The results of each consumption operation, corresponding to each + * sku. + */ + public void onConsumeMultiFinished(List purchases, List results); + } + + /** + * Asynchronous wrapper to item consumption. Works like {@link #consume}, but + * performs the consumption in the background and notifies completion through + * the provided listener. This method is safe to call from a UI thread. + * + * @param purchase The purchase to be consumed. + * @param listener The listener to notify when the consumption operation finishes. + */ + public void consumeAsync(Purchase purchase, OnConsumeFinishedListener listener) { + checkNotDisposed(); + checkSetupDone("consume"); + List purchases = new ArrayList(); + purchases.add(purchase); + consumeAsyncInternal(purchases, listener, null); + } + + /** + * Same as {@link consumeAsync}, but for multiple items at once. + * @param purchases The list of PurchaseInfo objects representing the purchases to consume. + * @param listener The listener to notify when the consumption operation finishes. + */ + public void consumeAsync(List purchases, OnConsumeMultiFinishedListener listener) { + checkNotDisposed(); + checkSetupDone("consume"); + consumeAsyncInternal(purchases, null, listener); + } + + /** + * Returns a human-readable description for the given response code. + * + * @param code The response code + * @return A human-readable string explaining the result code. + * It also includes the result code numerically. + */ + public static String getResponseDesc(int code) { + String[] iab_msgs = ("0:OK/1:User Canceled/2:Unknown/" + + "3:Billing Unavailable/4:Item unavailable/" + + "5:Developer Error/6:Error/7:Item Already Owned/" + + "8:Item not owned").split("/"); + String[] iabhelper_msgs = ("0:OK/-1001:Remote exception during initialization/" + + "-1002:Bad response received/" + + "-1003:Purchase signature verification failed/" + + "-1004:Send intent failed/" + + "-1005:User cancelled/" + + "-1006:Unknown purchase response/" + + "-1007:Missing token/" + + "-1008:Unknown error/" + + "-1009:Subscriptions not available/" + + "-1010:Invalid consumption attempt").split("/"); + + if (code <= IABHELPER_ERROR_BASE) { + int index = IABHELPER_ERROR_BASE - code; + if (index >= 0 && index < iabhelper_msgs.length) return iabhelper_msgs[index]; + else return String.valueOf(code) + ":Unknown IAB Helper Error"; + } + else if (code < 0 || code >= iab_msgs.length) + return String.valueOf(code) + ":Unknown"; + else + return iab_msgs[code]; + } + + + // Checks that setup was done; if not, throws an exception. + void checkSetupDone(String operation) { + if (!mSetupDone) { + logError("Illegal state for operation (" + operation + "): IAB helper is not set up."); + throw new IllegalStateException("IAB helper is not set up. Can't perform operation: " + operation); + } + } + + // Workaround to bug where sometimes response codes come as Long instead of Integer + int getResponseCodeFromBundle(Bundle b) { + Object o = b.get(RESPONSE_CODE); + if (o == null) { + logDebug("Bundle with null response code, assuming OK (known issue)"); + return BILLING_RESPONSE_RESULT_OK; + } + else if (o instanceof Integer) return ((Integer)o).intValue(); + else if (o instanceof Long) return (int)((Long)o).longValue(); + else { + logError("Unexpected type for bundle response code."); + logError(o.getClass().getName()); + throw new RuntimeException("Unexpected type for bundle response code: " + o.getClass().getName()); + } + } + + // Workaround to bug where sometimes response codes come as Long instead of Integer + int getResponseCodeFromIntent(Intent i) { + Object o = i.getExtras().get(RESPONSE_CODE); + if (o == null) { + logError("Intent with no response code, assuming OK (known issue)"); + return BILLING_RESPONSE_RESULT_OK; + } + else if (o instanceof Integer) return ((Integer)o).intValue(); + else if (o instanceof Long) return (int)((Long)o).longValue(); + else { + logError("Unexpected type for intent response code."); + logError(o.getClass().getName()); + throw new RuntimeException("Unexpected type for intent response code: " + o.getClass().getName()); + } + } + + void flagStartAsync(String operation) { + // HACK: This check was causing problems because somehow the billing infrastructure wasn't + // calling onActivityResult if there's an error, so it never reseted mAsyncInProgress + // Comment this for now +// if (mAsyncInProgress) throw new IllegalStateException("Can't start async operation (" + +// operation + ") because another async operation(" + mAsyncOperation + ") is in progress."); + mAsyncOperation = operation; + mAsyncInProgress = true; + logDebug("Starting async operation: " + operation); + } + + void flagEndAsync() { + logDebug("Ending async operation: " + mAsyncOperation); + mAsyncOperation = ""; + mAsyncInProgress = false; + } + + int queryPurchases(Inventory inv, String itemType) throws JSONException, RemoteException { + // Query purchases + logDebug("Querying owned items, item type: " + itemType); + logDebug("Package name: " + mContext.getPackageName()); + boolean verificationFailed = false; + String continueToken = null; + + do { + logDebug("Calling getPurchases with continuation token: " + continueToken); + Bundle ownedItems = mService.getPurchases(3, mContext.getPackageName(), + itemType, continueToken); + + int response = getResponseCodeFromBundle(ownedItems); + logDebug("Owned items response: " + String.valueOf(response)); + if (response != BILLING_RESPONSE_RESULT_OK) { + logDebug("getPurchases() failed: " + getResponseDesc(response)); + return response; + } + if (!ownedItems.containsKey(RESPONSE_INAPP_ITEM_LIST) + || !ownedItems.containsKey(RESPONSE_INAPP_PURCHASE_DATA_LIST) + || !ownedItems.containsKey(RESPONSE_INAPP_SIGNATURE_LIST)) { + logError("Bundle returned from getPurchases() doesn't contain required fields."); + return IABHELPER_BAD_RESPONSE; + } + + ArrayList ownedSkus = ownedItems.getStringArrayList( + RESPONSE_INAPP_ITEM_LIST); + ArrayList purchaseDataList = ownedItems.getStringArrayList( + RESPONSE_INAPP_PURCHASE_DATA_LIST); + ArrayList signatureList = ownedItems.getStringArrayList( + RESPONSE_INAPP_SIGNATURE_LIST); + + for (int i = 0; i < purchaseDataList.size(); ++i) { + String purchaseData = purchaseDataList.get(i); + String signature = signatureList.get(i); + String sku = ownedSkus.get(i); + + //logWarn("SKU " + sku); + //if (sku.equals("android.test.purchased")) { + // logWarn("HERE!!!!!"); + // try { + // consume(new Purchase(itemType, purchaseData, signature)); + // } catch (IabException exc) { + // return IABHELPER_BAD_RESPONSE; + // } + //} + + if (Security.verifyPurchase(mSignatureBase64, purchaseData, signature)) { + logDebug("Sku is owned: " + sku); + Purchase purchase = new Purchase(itemType, purchaseData, signature); + + if (TextUtils.isEmpty(purchase.getToken())) { + logWarn("BUG: empty/null token!"); + logDebug("Purchase data: " + purchaseData); + } + + // Record ownership and token + inv.addPurchase(purchase); + } + else { + logWarn("Purchase signature verification **FAILED**. Not adding item."); + logDebug(" Purchase data: " + purchaseData); + logDebug(" Signature: " + signature); + verificationFailed = true; + } + } + + continueToken = ownedItems.getString(INAPP_CONTINUATION_TOKEN); + logDebug("Continuation token: " + continueToken); + } while (!TextUtils.isEmpty(continueToken)); + + return verificationFailed ? IABHELPER_VERIFICATION_FAILED : BILLING_RESPONSE_RESULT_OK; + } + + int querySkuDetails(String itemType, Inventory inv, List moreSkus) + throws RemoteException, JSONException { + logDebug("Querying SKU details."); + ArrayList skuList = new ArrayList(); + skuList.addAll(inv.getAllOwnedSkus(itemType)); + if (moreSkus != null) { + for (String sku : moreSkus) { + if (!skuList.contains(sku)) { + skuList.add(sku); + } + } + } + + if (skuList.size() == 0) { + logDebug("queryPrices: nothing to do because there are no SKUs."); + return BILLING_RESPONSE_RESULT_OK; + } + + Bundle querySkus = new Bundle(); + querySkus.putStringArrayList(GET_SKU_DETAILS_ITEM_LIST, skuList); + Bundle skuDetails = mService.getSkuDetails(3, mContext.getPackageName(), + itemType, querySkus); + + if (!skuDetails.containsKey(RESPONSE_GET_SKU_DETAILS_LIST)) { + int response = getResponseCodeFromBundle(skuDetails); + if (response != BILLING_RESPONSE_RESULT_OK) { + logDebug("getSkuDetails() failed: " + getResponseDesc(response)); + return response; + } + else { + logError("getSkuDetails() returned a bundle with neither an error nor a detail list."); + return IABHELPER_BAD_RESPONSE; + } + } + + ArrayList responseList = skuDetails.getStringArrayList( + RESPONSE_GET_SKU_DETAILS_LIST); + + for (String thisResponse : responseList) { + SkuDetails d = new SkuDetails(itemType, thisResponse); + logDebug("Got sku details: " + d); + inv.addSkuDetails(d); + } + return BILLING_RESPONSE_RESULT_OK; + } + + + void consumeAsyncInternal(final List purchases, + final OnConsumeFinishedListener singleListener, + final OnConsumeMultiFinishedListener multiListener) { + final Handler handler = new Handler(); + flagStartAsync("consume"); + (new Thread(new Runnable() { + public void run() { + final List results = new ArrayList(); + for (Purchase purchase : purchases) { + try { + consume(purchase); + results.add(new IabResult(BILLING_RESPONSE_RESULT_OK, "Successful consume of sku " + purchase.getSku())); + } + catch (IabException ex) { + results.add(ex.getResult()); + } + } + + flagEndAsync(); + if (!mDisposed && singleListener != null) { + handler.post(new Runnable() { + public void run() { + singleListener.onConsumeFinished(purchases.get(0), results.get(0)); + } + }); + } + if (!mDisposed && multiListener != null) { + handler.post(new Runnable() { + public void run() { + multiListener.onConsumeMultiFinished(purchases, results); + } + }); + } + } + })).start(); + } + + void logDebug(String msg) { + if (mDebugLog) Log.d(mDebugTag, msg); + } + + void logError(String msg) { + Log.e(mDebugTag, "In-app billing error: " + msg); + } + + void logWarn(String msg) { + Log.w(mDebugTag, "In-app billing warning: " + msg); + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/billing/IabResult.java b/app/src/main/java/com/syncedsynapse/kore2/billing/IabResult.java new file mode 100644 index 0000000..12bb4d5 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/billing/IabResult.java @@ -0,0 +1,45 @@ +/* Copyright (c) 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.syncedsynapse.kore2.billing; + +/** + * Represents the result of an in-app billing operation. + * A result is composed of a response code (an integer) and possibly a + * message (String). You can get those by calling + * {@link #getResponse} and {@link #getMessage()}, respectively. You + * can also inquire whether a result is a success or a failure by + * calling {@link #isSuccess()} and {@link #isFailure()}. + */ +public class IabResult { + int mResponse; + String mMessage; + + public IabResult(int response, String message) { + mResponse = response; + if (message == null || message.trim().length() == 0) { + mMessage = IabHelper.getResponseDesc(response); + } + else { + mMessage = message + " (response: " + IabHelper.getResponseDesc(response) + ")"; + } + } + public int getResponse() { return mResponse; } + public String getMessage() { return mMessage; } + public boolean isSuccess() { return mResponse == IabHelper.BILLING_RESPONSE_RESULT_OK; } + public boolean isFailure() { return !isSuccess(); } + public String toString() { return "IabResult: " + getMessage(); } +} + diff --git a/app/src/main/java/com/syncedsynapse/kore2/billing/Inventory.java b/app/src/main/java/com/syncedsynapse/kore2/billing/Inventory.java new file mode 100644 index 0000000..ddc1be9 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/billing/Inventory.java @@ -0,0 +1,91 @@ +/* Copyright (c) 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.syncedsynapse.kore2.billing; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Represents a block of information about in-app items. + * An Inventory is returned by such methods as {@link IabHelper#queryInventory}. + */ +public class Inventory { + Map mSkuMap = new HashMap(); + Map mPurchaseMap = new HashMap(); + + Inventory() { } + + /** Returns the listing details for an in-app product. */ + public SkuDetails getSkuDetails(String sku) { + return mSkuMap.get(sku); + } + + /** Returns purchase information for a given product, or null if there is no purchase. */ + public Purchase getPurchase(String sku) { + return mPurchaseMap.get(sku); + } + + /** Returns whether or not there exists a purchase of the given product. */ + public boolean hasPurchase(String sku) { + return mPurchaseMap.containsKey(sku); + } + + /** Return whether or not details about the given product are available. */ + public boolean hasDetails(String sku) { + return mSkuMap.containsKey(sku); + } + + /** + * Erase a purchase (locally) from the inventory, given its product ID. This just + * modifies the Inventory object locally and has no effect on the server! This is + * useful when you have an existing Inventory object which you know to be up to date, + * and you have just consumed an item successfully, which means that erasing its + * purchase data from the Inventory you already have is quicker than querying for + * a new Inventory. + */ + public void erasePurchase(String sku) { + if (mPurchaseMap.containsKey(sku)) mPurchaseMap.remove(sku); + } + + /** Returns a list of all owned product IDs. */ + List getAllOwnedSkus() { + return new ArrayList(mPurchaseMap.keySet()); + } + + /** Returns a list of all owned product IDs of a given type */ + List getAllOwnedSkus(String itemType) { + List result = new ArrayList(); + for (Purchase p : mPurchaseMap.values()) { + if (p.getItemType().equals(itemType)) result.add(p.getSku()); + } + return result; + } + + /** Returns a list of all purchases. */ + List getAllPurchases() { + return new ArrayList(mPurchaseMap.values()); + } + + void addSkuDetails(SkuDetails d) { + mSkuMap.put(d.getSku(), d); + } + + void addPurchase(Purchase p) { + mPurchaseMap.put(p.getSku(), p); + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/billing/Purchase.java b/app/src/main/java/com/syncedsynapse/kore2/billing/Purchase.java new file mode 100644 index 0000000..7264039 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/billing/Purchase.java @@ -0,0 +1,63 @@ +/* Copyright (c) 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.syncedsynapse.kore2.billing; + +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Represents an in-app billing purchase. + */ +public class Purchase { + String mItemType; // ITEM_TYPE_INAPP or ITEM_TYPE_SUBS + String mOrderId; + String mPackageName; + String mSku; + long mPurchaseTime; + int mPurchaseState; + String mDeveloperPayload; + String mToken; + String mOriginalJson; + String mSignature; + + public Purchase(String itemType, String jsonPurchaseInfo, String signature) throws JSONException { + mItemType = itemType; + mOriginalJson = jsonPurchaseInfo; + JSONObject o = new JSONObject(mOriginalJson); + mOrderId = o.optString("orderId"); + mPackageName = o.optString("packageName"); + mSku = o.optString("productId"); + mPurchaseTime = o.optLong("purchaseTime"); + mPurchaseState = o.optInt("purchaseState"); + mDeveloperPayload = o.optString("developerPayload"); + mToken = o.optString("token", o.optString("purchaseToken")); + mSignature = signature; + } + + public String getItemType() { return mItemType; } + public String getOrderId() { return mOrderId; } + public String getPackageName() { return mPackageName; } + public String getSku() { return mSku; } + public long getPurchaseTime() { return mPurchaseTime; } + public int getPurchaseState() { return mPurchaseState; } + public String getDeveloperPayload() { return mDeveloperPayload; } + public String getToken() { return mToken; } + public String getOriginalJson() { return mOriginalJson; } + public String getSignature() { return mSignature; } + + @Override + public String toString() { return "PurchaseInfo(type:" + mItemType + "):" + mOriginalJson; } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/billing/Security.java b/app/src/main/java/com/syncedsynapse/kore2/billing/Security.java new file mode 100644 index 0000000..72b570c --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/billing/Security.java @@ -0,0 +1,121 @@ +/* Copyright (c) 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.syncedsynapse.kore2.billing; + +import android.text.TextUtils; +import android.util.Log; + +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; + +/** + * Security-related methods. For a secure implementation, all of this code + * should be implemented on a server that communicates with the + * application on the device. For the sake of simplicity and clarity of this + * example, this code is included here and is executed on the device. If you + * must verify the purchases on the phone, you should obfuscate this code to + * make it harder for an attacker to replace the code with stubs that treat all + * purchases as verified. + */ +public class Security { + private static final String TAG = "IABUtil/Security"; + + private static final String KEY_FACTORY_ALGORITHM = "RSA"; + private static final String SIGNATURE_ALGORITHM = "SHA1withRSA"; + + /** + * Verifies that the data was signed with the given signature, and returns + * the verified purchase. The data is in JSON format and signed + * with a private key. The data also contains the {@link PurchaseState} + * and product ID of the purchase. + * @param base64PublicKey the base64-encoded public key to use for verifying. + * @param signedData the signed JSON string (signed, not encrypted) + * @param signature the signature for the data, signed with the private key + */ + public static boolean verifyPurchase(String base64PublicKey, String signedData, String signature) { + if (TextUtils.isEmpty(signedData) || TextUtils.isEmpty(base64PublicKey) || + TextUtils.isEmpty(signature)) { + Log.e(TAG, "Purchase verification failed: missing data."); + return false; + } + + PublicKey key = Security.generatePublicKey(base64PublicKey); + return Security.verify(key, signedData, signature); + // TODO: Uncomment this + //return true; + } + + /** + * Generates a PublicKey instance from a string containing the + * Base64-encoded public key. + * + * @param encodedPublicKey Base64-encoded public key + * @throws IllegalArgumentException if encodedPublicKey is invalid + */ + public static PublicKey generatePublicKey(String encodedPublicKey) { + try { + byte[] decodedKey = Base64.decode(encodedPublicKey); + KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM); + return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey)); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } catch (InvalidKeySpecException e) { + Log.e(TAG, "Invalid key specification."); + throw new IllegalArgumentException(e); + } catch (Base64DecoderException e) { + Log.e(TAG, "Base64 decoding failed."); + throw new IllegalArgumentException(e); + } + } + + /** + * Verifies that the signature from the server matches the computed + * signature on the data. Returns true if the data is correctly signed. + * + * @param publicKey public key associated with the developer account + * @param signedData signed data from server + * @param signature server signature + * @return true if the data and signature match + */ + public static boolean verify(PublicKey publicKey, String signedData, String signature) { + Signature sig; + try { + sig = Signature.getInstance(SIGNATURE_ALGORITHM); + sig.initVerify(publicKey); + sig.update(signedData.getBytes()); + if (!sig.verify(Base64.decode(signature))) { + Log.e(TAG, "Signature verification failed."); + return false; + } + return true; + } catch (NoSuchAlgorithmException e) { + Log.e(TAG, "NoSuchAlgorithmException."); + } catch (InvalidKeyException e) { + Log.e(TAG, "Invalid key specification."); + } catch (SignatureException e) { + Log.e(TAG, "Signature exception."); + } catch (Base64DecoderException e) { + Log.e(TAG, "Base64 decoding failed."); + } + return false; + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/billing/SkuDetails.java b/app/src/main/java/com/syncedsynapse/kore2/billing/SkuDetails.java new file mode 100644 index 0000000..c6fffc5 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/billing/SkuDetails.java @@ -0,0 +1,58 @@ +/* Copyright (c) 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.syncedsynapse.kore2.billing; + +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Represents an in-app product's listing details. + */ +public class SkuDetails { + String mItemType; + String mSku; + String mType; + String mPrice; + String mTitle; + String mDescription; + String mJson; + + public SkuDetails(String jsonSkuDetails) throws JSONException { + this(IabHelper.ITEM_TYPE_INAPP, jsonSkuDetails); + } + + public SkuDetails(String itemType, String jsonSkuDetails) throws JSONException { + mItemType = itemType; + mJson = jsonSkuDetails; + JSONObject o = new JSONObject(mJson); + mSku = o.optString("productId"); + mType = o.optString("type"); + mPrice = o.optString("price"); + mTitle = o.optString("title"); + mDescription = o.optString("description"); + } + + public String getSku() { return mSku; } + public String getType() { return mType; } + public String getPrice() { return mPrice; } + public String getTitle() { return mTitle; } + public String getDescription() { return mDescription; } + + @Override + public String toString() { + return "SkuDetails:" + mJson; + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/host/HostConnectionObserver.java b/app/src/main/java/com/syncedsynapse/kore2/host/HostConnectionObserver.java new file mode 100644 index 0000000..f6862e1 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/host/HostConnectionObserver.java @@ -0,0 +1,603 @@ +/* + * Copyright 2015 Synced Synapse. 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.syncedsynapse.kore2.host; + +import android.os.Handler; + +import com.syncedsynapse.kore2.jsonrpc.ApiCallback; +import com.syncedsynapse.kore2.jsonrpc.HostConnection; +import com.syncedsynapse.kore2.jsonrpc.method.JSONRPC; +import com.syncedsynapse.kore2.jsonrpc.method.Player; +import com.syncedsynapse.kore2.jsonrpc.notification.Input; +import com.syncedsynapse.kore2.jsonrpc.notification.System; +import com.syncedsynapse.kore2.jsonrpc.type.ListType; +import com.syncedsynapse.kore2.jsonrpc.type.PlayerType; +import com.syncedsynapse.kore2.utils.LogUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * Object that listens to a connection and notifies observers about changes in its state + * This class serves as an adpater to the {@link HostConnection.PlayerNotificationsObserver}, + * to enable to get notifications not only through TCP but also through HTTP. + * Depending on the connection protocol this class registers itself as an observer for + * {@link HostConnection.PlayerNotificationsObserver} and forwards the notifications it gets, + * or, if through HTTP, starts a periodic polling of XBMC, and tries to discern when a change in + * the player has occurred, notifying the listeners + * + * NOTE: An object of this class should always be called from the same thread. + */ +public class HostConnectionObserver + implements HostConnection.PlayerNotificationsObserver, + HostConnection.SystemNotificationsObserver, + HostConnection.InputNotificationsObserver { + public static final String TAG = LogUtils.makeLogTag(HostConnectionObserver.class); + + /** + * Interface that an observer has to implement to receive player events + */ + public interface PlayerEventsObserver { + /** + * Constants for possible events. Useful to save the last event and compare with the + * current one to check for differences + */ + public static final int PLAYER_NO_RESULT = 0, + PLAYER_CONNECTION_ERROR = 1, + PLAYER_IS_PLAYING = 2, + PLAYER_IS_PAUSED = 3, + PLAYER_IS_STOPPED = 4; + + /** + * Notifies that something is playing + * @param getActivePlayerResult Active player obtained by a call to {@link com.syncedsynapse.kore2.jsonrpc.method.Player.GetActivePlayers} + * @param getPropertiesResult Properties obtained by a call to {@link com.syncedsynapse.kore2.jsonrpc.method.Player.GetProperties} + * @param getItemResult Currently playing item, obtained by a call to {@link com.syncedsynapse.kore2.jsonrpc.method.Player.GetItem} + */ + public void playerOnPlay(PlayerType.GetActivePlayersReturnType getActivePlayerResult, + PlayerType.PropertyValue getPropertiesResult, + ListType.ItemsAll getItemResult); + + /** + * Notifies that something is paused + * @param getActivePlayerResult Active player obtained by a call to {@link com.syncedsynapse.kore2.jsonrpc.method.Player.GetActivePlayers} + * @param getPropertiesResult Properties obtained by a call to {@link com.syncedsynapse.kore2.jsonrpc.method.Player.GetProperties} + * @param getItemResult Currently paused item, obtained by a call to {@link com.syncedsynapse.kore2.jsonrpc.method.Player.GetItem} + */ + public void playerOnPause(PlayerType.GetActivePlayersReturnType getActivePlayerResult, + PlayerType.PropertyValue getPropertiesResult, + ListType.ItemsAll getItemResult); + + /** + * Notifies that media is stopped/nothing is playing + */ + public void playerOnStop(); + + /** + * Called when we get a connection error + * @param errorCode + * @param description + */ + public void playerOnConnectionError(int errorCode, String description); + + /** + * Notifies that we don't have a result yet + */ + public void playerNoResultsYet(); + + /** + * Notifies that XBMC has quit/shutdown/sleep + */ + public void systemOnQuit(); + + /** + * Notifies that XBMC has requested input + */ + public void inputOnInputRequested(String title, String type, String value); + } + + /** + * The connection on which to listen + */ + private HostConnection connection; + + /** + * The list of observers + */ + private List playerEventsObservers = new ArrayList(); + +// /** +// * Handlers for which observer, on which to notify them +// */ +// private Map observerHandlerMap = new HashMap(); + + private Handler checkerHandler = new Handler(); + private Runnable httpCheckerRunnable = new Runnable() { + @Override + public void run() { + final int HTTP_NOTIFICATION_CHECK_INTERVAL = 3000; + // If no one is listening to this, just exit + if (playerEventsObservers.size() == 0) return; + + // Check whats playing + checkWhatsPlaying(); + + // Keep checking + checkerHandler.postDelayed(this, HTTP_NOTIFICATION_CHECK_INTERVAL); + } + }; + + private Runnable tcpCheckerRunnable = new Runnable() { + @Override + public void run() { + final int PING_AFTER_ERROR_CHECK_INTERVAL = 2000, + PING_AFTER_SUCCESS_CHECK_INTERVAL = 10000; + // If no one is listening to this, just exit + if (playerEventsObservers.size() == 0) return; + + JSONRPC.Ping ping = new JSONRPC.Ping(); + ping.execute(connection, new ApiCallback() { + @Override + public void onSucess(String result) { + // Ok, we've got a ping, if we were in a error or uninitialized state, update + if ((lastCallResult == PlayerEventsObserver.PLAYER_NO_RESULT) || + (lastCallResult == PlayerEventsObserver.PLAYER_CONNECTION_ERROR)) { + checkWhatsPlaying(); + } + checkerHandler.postDelayed(tcpCheckerRunnable, PING_AFTER_SUCCESS_CHECK_INTERVAL); + } + + @Override + public void onError(int errorCode, String description) { + // Notify a connection error + notifyConnectionError(errorCode, description, playerEventsObservers); + checkerHandler.postDelayed(tcpCheckerRunnable, PING_AFTER_ERROR_CHECK_INTERVAL); + } + }, checkerHandler); +// if ((lastCallResult == PlayerEventsObserver.PLAYER_NO_RESULT) || +// (lastCallResult == PlayerEventsObserver.PLAYER_CONNECTION_ERROR)) { +// checkerHandler.postDelayed(tcpCheckerRunnable, PING_AFTER_ERROR_CHECK_INTERVAL); +// } else { +// checkerHandler.postDelayed(tcpCheckerRunnable, PING_AFTER_SUCCESS_CHECK_INTERVAL); +// } + } + }; + + private int lastCallResult = PlayerEventsObserver.PLAYER_NO_RESULT; + private PlayerType.GetActivePlayersReturnType lastGetActivePlayerResult = null; + private PlayerType.PropertyValue lastGetPropertiesResult = null; + private ListType.ItemsAll lastGetItemResult = null; + private int lastErrorCode; + private String lastErrorDescription; + + public HostConnectionObserver(HostConnection connection) { + this.connection = connection; + } + + /** + * Registers a new observer that will be notified about player events + * @param observer Observer + */ + public synchronized void registerPlayerObserver(PlayerEventsObserver observer) { + if (this.connection == null) + return; + + // Save this observer and a new handle to notify him + playerEventsObservers.add(observer); +// observerHandlerMap.put(observer, new Handler()); + + if (playerEventsObservers.size() == 1) { + // If this is the first observer, start checking through HTTP or register us + // as a connection observer, which we will pass to the "real" observer + if (connection.getProtocol() == HostConnection.PROTOCOL_TCP) { + connection.registerPlayerNotificationsObserver(this, checkerHandler); + connection.registerSystemNotificationsObserver(this, checkerHandler); + connection.registerInputNotificationsObserver(this, checkerHandler); + // Start the ping checker + checkerHandler.post(tcpCheckerRunnable); + } else { + checkerHandler.post(httpCheckerRunnable); + } + } + } + + /** + * Unregisters a previously registered observer + * @param observer Observer to unregister + */ + public synchronized void unregisterPlayerObserver(PlayerEventsObserver observer) { + // Remove this observer and its associated handler + playerEventsObservers.remove(observer); +// observerHandlerMap.remove(observer); + + LogUtils.LOGD(TAG, "Unregistering observer. Still got " + playerEventsObservers.size() + + " observers."); + + if (playerEventsObservers.size() == 0) { + // No more observers, so unregister us from the host connection, or stop + // the http checker thread + if (connection.getProtocol() == HostConnection.PROTOCOL_TCP) { + connection.unregisterPlayerNotificationsObserver(this); + connection.unregisterSystemNotificationsObserver(this); + connection.unregisterInputNotificationsObserver(this); + checkerHandler.removeCallbacks(tcpCheckerRunnable); + } else { + checkerHandler.removeCallbacks(httpCheckerRunnable); + } + lastCallResult = PlayerEventsObserver.PLAYER_NO_RESULT; + } + } + + /** + * Unregisters all observers + */ + public void unregisterAllObservers() { +// observerHandlerMap.clear(); + playerEventsObservers.clear(); + + if (connection.getProtocol() == HostConnection.PROTOCOL_TCP) { + connection.unregisterPlayerNotificationsObserver(this); + connection.unregisterSystemNotificationsObserver(this); + connection.unregisterInputNotificationsObserver(this); + checkerHandler.removeCallbacks(tcpCheckerRunnable); + } else { + checkerHandler.removeCallbacks(httpCheckerRunnable); + } + lastCallResult = PlayerEventsObserver.PLAYER_NO_RESULT; + } + + /** + * The {@link HostConnection.PlayerNotificationsObserver} interface methods + */ + public void onPlay(com.syncedsynapse.kore2.jsonrpc.notification.Player.OnPlay notification) { + // Just start our chain calls + chainCallGetActivePlayers(); + } + + public void onPause(com.syncedsynapse.kore2.jsonrpc.notification.Player.OnPause + notification) { + // Just start our chain calls + chainCallGetActivePlayers(); + } + + public void onSpeedChanged(com.syncedsynapse.kore2.jsonrpc.notification.Player + .OnSpeedChanged notification) { + // Just start our chain calls + chainCallGetActivePlayers(); + } + + public void onSeek(com.syncedsynapse.kore2.jsonrpc.notification.Player.OnSeek notification) { + // Just start our chain calls + chainCallGetActivePlayers(); + } + + public void onStop(com.syncedsynapse.kore2.jsonrpc.notification.Player.OnStop notification) { + // Just start our chain calls + notifyNothingIsPlaying(playerEventsObservers); + } + + /** + * The {@link HostConnection.SystemNotificationsObserver} interface methods + */ + public void onQuit(System.OnQuit notification) { + for (final PlayerEventsObserver observer : playerEventsObservers) { + observer.systemOnQuit(); + } + } + + public void onRestart(System.OnRestart notification) { + for (final PlayerEventsObserver observer : playerEventsObservers) { + observer.systemOnQuit(); + } + } + + public void onSleep(System.OnSleep notification) { + for (final PlayerEventsObserver observer : playerEventsObservers) { + observer.systemOnQuit(); + } + } + + public void onInputRequested(Input.OnInputRequested notification) { + for (final PlayerEventsObserver observer : playerEventsObservers) { + observer.inputOnInputRequested(notification.title, notification.type, notification.value); + } + } + + /** + * Checks whats playing and notifies observers + */ + private void checkWhatsPlaying() { + LogUtils.LOGD(TAG, "Checking whats playing"); + + // Start the calls: Player.GetActivePlayers -> Player.GetProperties -> Player.GetItem + chainCallGetActivePlayers(); + } + + /** + * Calls Player.GetActivePlayers + * On success chains execution to chainCallGetProperties + */ + private void chainCallGetActivePlayers() { + Player.GetActivePlayers getActivePlayers = new Player.GetActivePlayers(); + getActivePlayers.execute(connection, new ApiCallback>() { + @Override + public void onSucess(ArrayList result) { + if (result.isEmpty()) { + LogUtils.LOGD(TAG, "Nothing is playing"); + notifyNothingIsPlaying(playerEventsObservers); + return; + } + chainCallGetProperties(result.get(0)); + } + + @Override + public void onError(int errorCode, String description) { + LogUtils.LOGD(TAG, "Notifying error"); + notifyConnectionError(errorCode, description, playerEventsObservers); + } + }, checkerHandler); + } + + /** + * Calls Player.GetProperties + * On success chains execution to chainCallGetItem + */ + private void chainCallGetProperties(final PlayerType.GetActivePlayersReturnType getActivePlayersResult) { + String propertiesToGet[] = new String[] { + // Check is something more is needed + PlayerType.PropertyName.SPEED, + PlayerType.PropertyName.PERCENTAGE, + PlayerType.PropertyName.POSITION, + PlayerType.PropertyName.TIME, + PlayerType.PropertyName.TOTALTIME, + PlayerType.PropertyName.REPEAT, + PlayerType.PropertyName.SHUFFLED, + PlayerType.PropertyName.CURRENTAUDIOSTREAM, + PlayerType.PropertyName.CURRENTSUBTITLE, + PlayerType.PropertyName.AUDIOSTREAMS, + PlayerType.PropertyName.SUBTITLES, + }; + + Player.GetProperties getProperties = new Player.GetProperties(getActivePlayersResult.playerid, propertiesToGet); + getProperties.execute(connection, new ApiCallback() { + @Override + public void onSucess(PlayerType.PropertyValue result) { + chainCallGetItem(getActivePlayersResult, result); + } + + @Override + public void onError(int errorCode, String description) { + notifyConnectionError(errorCode, description, playerEventsObservers); + } + }, checkerHandler); + } + + /** + * Calls Player.GetItem + * On success notifies observers + */ + private void chainCallGetItem(final PlayerType.GetActivePlayersReturnType getActivePlayersResult, + final PlayerType.PropertyValue getPropertiesResult) { +// COMMENT, LYRICS, MUSICBRAINZTRACKID, MUSICBRAINZARTISTID, MUSICBRAINZALBUMID, +// MUSICBRAINZALBUMARTISTID, TRAILER, ORIGINALTITLE, LASTPLAYED, MPAA, COUNTRY, +// PRODUCTIONCODE, SET, SHOWLINK, FILE, +// ARTISTID, ALBUMID, TVSHOW_ID, SETID, WATCHEDEPISODES, DISC, TAG, GENREID, +// ALBUMARTISTID, DESCRIPTION, THEME, MOOD, STYLE, ALBUMLABEL, SORTTITLE, UNIQUEID, +// DATEADDED, CHANNEL, CHANNELTYPE, HIDDEN, LOCKED, CHANNELNUMBER, STARTTIME, ENDTIME, +// EPISODEGUIDE, ORIGINALTITLE, PLAYCOUNT, PLOTOUTLINE, SET, + String[] propertiesToGet = new String[] { + ListType.FieldsAll.ART, + ListType.FieldsAll.ARTIST, + ListType.FieldsAll.ALBUMARTIST, + ListType.FieldsAll.ALBUM, + ListType.FieldsAll.CAST, + ListType.FieldsAll.DIRECTOR, + ListType.FieldsAll.DISPLAYARTIST, + ListType.FieldsAll.DURATION, + ListType.FieldsAll.EPISODE, + ListType.FieldsAll.FANART, + ListType.FieldsAll.FILE, + ListType.FieldsAll.FIRSTAIRED, + ListType.FieldsAll.GENRE, + ListType.FieldsAll.IMDBNUMBER, + ListType.FieldsAll.PLOT, + ListType.FieldsAll.PREMIERED, + ListType.FieldsAll.RATING, + ListType.FieldsAll.RESUME, + ListType.FieldsAll.RUNTIME, + ListType.FieldsAll.SEASON, + ListType.FieldsAll.SHOWTITLE, + ListType.FieldsAll.STREAMDETAILS, + ListType.FieldsAll.STUDIO, + ListType.FieldsAll.TAGLINE, + ListType.FieldsAll.THUMBNAIL, + ListType.FieldsAll.TITLE, + ListType.FieldsAll.TOP250, + ListType.FieldsAll.TRACK, + ListType.FieldsAll.VOTES, + ListType.FieldsAll.WRITER, + ListType.FieldsAll.YEAR, + ListType.FieldsAll.DESCRIPTION, + }; +// propertiesToGet = ListType.FieldsAll.allValues; + Player.GetItem getItem = new Player.GetItem(getActivePlayersResult.playerid, propertiesToGet); + getItem.execute(connection, new ApiCallback() { + @Override + public void onSucess(ListType.ItemsAll result) { + // Ok, now we got a result + notifySomethingIsPlaying(getActivePlayersResult, getPropertiesResult, result, playerEventsObservers); + } + + @Override + public void onError(int errorCode, String description) { + notifyConnectionError(errorCode, description, playerEventsObservers); + } + }, checkerHandler); + } + + // Whether to foorce a reply or if the results are equal to the last one, don't reply + private boolean forceReply = false; + + /** + * Notifies a list of observers of a connection error + * Only notifies them if the result is different from the last one + * @param errorCode Error code to report + * @param description Description to report + * @param observers List of observers + */ + private void notifyConnectionError(final int errorCode, final String description, List observers) { + // Reply if different from last result + if (forceReply || + (lastCallResult != PlayerEventsObserver.PLAYER_CONNECTION_ERROR) || + (lastErrorCode != errorCode)) { + lastCallResult = PlayerEventsObserver.PLAYER_CONNECTION_ERROR; + lastErrorCode = errorCode; + lastErrorDescription = description; + forceReply = false; + for (final PlayerEventsObserver observer : observers) { + notifyConnectionError(errorCode, description, observer); + } + } + } + + /** + * Notifies a specific observer of a connection error + * Always notifies the observer, and doesn't save results in last call + * @param errorCode Error code to report + * @param description Description to report + * @param observer Observers + */ + private void notifyConnectionError(final int errorCode, final String description, PlayerEventsObserver observer) { + observer.playerOnConnectionError(errorCode, description); +// Handler observerHandler = observerHandlerMap.get(observer); +// observerHandler.post(new Runnable() { +// @Override +// public void run() { +// observer.playerOnConnectionError(errorCode, description); +// } +// }); + } + + + /** + * Nothing is playing, notify observers calling playerOnStop + * Only notifies them if the result is different from the last one + * @param observers List of observers + */ + private void notifyNothingIsPlaying(List observers) { + // Reply if forced or different from last result + if (forceReply || + (lastCallResult != PlayerEventsObserver.PLAYER_IS_STOPPED)) { + lastCallResult = PlayerEventsObserver.PLAYER_IS_STOPPED; + forceReply = false; + for (final PlayerEventsObserver observer : observers) { + notifyNothingIsPlaying(observer); + } + } + } + + /** + * Notifies a specific observer + * Always notifies the observer, and doesn't save results in last call + * @param observer Observer + */ + private void notifyNothingIsPlaying(PlayerEventsObserver observer) { + observer.playerOnStop(); + } + + /** + * Something is playing or paused, notify observers + * Only notifies them if the result is different from the last one + * @param getActivePlayersResult + * @param getPropertiesResult + * @param getItemResult + * @param observers List of observers + */ + private void notifySomethingIsPlaying(final PlayerType.GetActivePlayersReturnType getActivePlayersResult, + final PlayerType.PropertyValue getPropertiesResult, + final ListType.ItemsAll getItemResult, + List observers) { + int currentCallResult = (getPropertiesResult.speed == 0) ? + PlayerEventsObserver.PLAYER_IS_PAUSED : PlayerEventsObserver.PLAYER_IS_PLAYING; + if (forceReply || + (lastCallResult != currentCallResult) || + (lastGetPropertiesResult.speed != getPropertiesResult.speed) || + (lastGetPropertiesResult.shuffled != getPropertiesResult.shuffled) || + (!lastGetPropertiesResult.repeat.equals(getPropertiesResult.repeat)) || + (lastGetItemResult.id != getItemResult.id)) { + lastCallResult = currentCallResult; + lastGetActivePlayerResult = getActivePlayersResult; + lastGetPropertiesResult = getPropertiesResult; + lastGetItemResult = getItemResult; + forceReply = false; + for (final PlayerEventsObserver observer : observers) { + notifySomethingIsPlaying(getActivePlayersResult, getPropertiesResult, getItemResult, observer); + } + } + } + + /** + * Something is playing or paused, notify a specific observer + * Always notifies the observer, and doesn't save results in last call + * @param getActivePlayersResult + * @param getPropertiesResult + * @param getItemResult + * @param observer Specific observer + */ + private void notifySomethingIsPlaying(final PlayerType.GetActivePlayersReturnType getActivePlayersResult, + final PlayerType.PropertyValue getPropertiesResult, + final ListType.ItemsAll getItemResult, + PlayerEventsObserver observer) { + if (getPropertiesResult.speed == 0) { + // Paused + observer.playerOnPause(getActivePlayersResult, getPropertiesResult, getItemResult); + } else { + // Playing + observer.playerOnPlay(getActivePlayersResult, getPropertiesResult, getItemResult); + } + } + + /** + * Replies to the observer with the last result we got. + * If we have no result, nothing will be called on the observer interface. + * @param observer Obserser to call with last result + */ + public void replyWithLastResult(PlayerEventsObserver observer) { + switch (lastCallResult) { + case PlayerEventsObserver.PLAYER_CONNECTION_ERROR: + notifyConnectionError(lastErrorCode, lastErrorDescription, observer); + break; + case PlayerEventsObserver.PLAYER_IS_STOPPED: + notifyNothingIsPlaying(observer); + break; + case PlayerEventsObserver.PLAYER_IS_PAUSED: + case PlayerEventsObserver.PLAYER_IS_PLAYING: + notifySomethingIsPlaying(lastGetActivePlayerResult, lastGetPropertiesResult, lastGetItemResult, observer); + break; + case PlayerEventsObserver.PLAYER_NO_RESULT: + observer.playerNoResultsYet(); + break; + } + } + + /** + * Forces a refresh of the current cached results + */ + public void forceRefreshResults() { + forceReply = true; + chainCallGetActivePlayers(); + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/host/HostInfo.java b/app/src/main/java/com/syncedsynapse/kore2/host/HostInfo.java new file mode 100644 index 0000000..e00ea31 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/host/HostInfo.java @@ -0,0 +1,224 @@ +/* + * Copyright 2015 Synced Synapse. 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.syncedsynapse.kore2.host; + +import com.syncedsynapse.kore2.jsonrpc.HostConnection; +import com.syncedsynapse.kore2.utils.LogUtils; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; + +/** + * XBMC Host information container. + */ +public class HostInfo { + private static final String TAG = LogUtils.makeLogTag(HostInfo.class); + + private static final String JSON_RPC_ENDPOINT = "/jsonrpc"; + + /** + * Default HTTP port for XBMC (80 on Windows, 8080 on others) + */ + public static final int DEFAULT_HTTP_PORT = 8080; + + /** + * Default TCP port for XBMC + */ + public static final int DEFAULT_TCP_PORT = 9090; + + /** + * Default WoL port + */ + public static final int DEFAULT_WOL_PORT = 9; + + /** + * Internal id of the host + */ + private int id; + + /** + * Friendly name of the host + */ + private String name; + + /** + * Connection information + */ + private String address; + private int httpPort; + private int tcpPort; + + /** + * Authentication information + */ + private String username; + private String password; + + /** + * Mac address and Wake On Lan port + */ + private String macAddress; + private int wolPort; + + /** + * Prefered protocol to communicate with this host + */ + private int protocol; + + private String auxImageHttpAddress; + + /** + * Full constructor. This constructor should be used when instantiating from the database + * + * @param name Friendly name of the host + * @param id ID + * @param address URL + * @param protocol Protocol + * @param httpPort HTTP Port + * @param tcpPort TCP Port + * @param username Username for basic auth + * @param password Password for basic auth + */ + public HostInfo(int id, String name, String address, int protocol, int httpPort, int tcpPort, + String username, String password, String macAddress, int wolPort) { + this.id = id; + this.name = name; + this.address = address; + if (!HostConnection.isValidProtocol(protocol)) { + throw new IllegalArgumentException("Invalid protocol specified."); + } + this.protocol = protocol; + this.httpPort = httpPort; + this.tcpPort = tcpPort; + this.username = username; + this.password = password; + this.macAddress = macAddress; + this.wolPort = wolPort; + + // For performance reasons + this.auxImageHttpAddress = getHttpURL() + "/image/"; + } + + /** + * Auxiliary constructor for HTTP protocol. + * This constructoor should only be used to test connections. It doesn't represent an + * instance of the host in the database. + * + * @param name Friendly name of the host + * @param address URL + * @param httpPort HTTP Port + * @param username Username for basic auth + * @param password Password for basic auth + */ + public HostInfo(String name, String address, int httpPort, int tcpPort, String username, String password) { + this(-1, name, address, HostConnection.PROTOCOL_TCP, httpPort, tcpPort, username, + password, null, DEFAULT_WOL_PORT); + } + + public int getId() { + return id; + } + + public String getName() { + return name; + } + + public String getAddress() { + return address; + } + + public int getHttpPort() { + return httpPort; + } + + public int getTcpPort() { + return tcpPort; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + + public String getMacAddress() { + return macAddress; + } + + public void setMacAddress(String macAddress) { + this.macAddress = macAddress; + } + + public int getWolPort() { + return wolPort; + } + + public void setWolPort(int wolPort) { + this.wolPort = wolPort; + } + + public int getProtocol() { + return protocol; + } + + /** + * Overrides the protocol for this host info + * @param protocol Protocol + */ + public void setProtocol(int protocol) { + if (!HostConnection.isValidProtocol(protocol)) { + throw new IllegalArgumentException("Invalid protocol specified."); + } + this.protocol = protocol; + } + + /** + * Returns the URL of the host + * @return HTTP URL eg. http://192.168.1.1:8080 + */ + public String getHttpURL() { + return "http://" + address + ":" + httpPort; + } + + /** + * Returns the JSON RPC endpoint URL of the host + * @return HTTP URL eg. http://192.168.1.1:8080/jsonrpc + */ + public String getJsonRpcHttpEndpoint() { + return "http://" + address + ":" + httpPort + JSON_RPC_ENDPOINT; + } + + /** + * Get the URL of an image, given the image identifier returned by XBMC + * @param image image identifier stored in XBMC + * @return URL on the XBMC host on which the image can be fetched + */ + public String getImageUrl(String image) { + if (image == null) { + return null; + } + + try { +// return getHttpURL() + "/image/" + URLEncoder.encode(image, "UTF-8"); + return auxImageHttpAddress + URLEncoder.encode(image, "UTF-8"); + } catch (UnsupportedEncodingException e) { + // Ignore for now... + return null; + } + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/host/HostManager.java b/app/src/main/java/com/syncedsynapse/kore2/host/HostManager.java new file mode 100644 index 0000000..b8c9d69 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/host/HostManager.java @@ -0,0 +1,396 @@ +/* + * Copyright 2015 Synced Synapse. 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.syncedsynapse.kore2.host; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; + +import com.squareup.picasso.Picasso; +import com.syncedsynapse.kore2.Settings; +import com.syncedsynapse.kore2.provider.MediaContract; +import com.syncedsynapse.kore2.jsonrpc.HostConnection; +import com.syncedsynapse.kore2.utils.BasicAuthPicassoDownloader; +import com.syncedsynapse.kore2.utils.LogUtils; + +import java.util.ArrayList; + +/** + * Manages XBMC Hosts + * Singleton that loads the list of registered hosts, keeps a + * {@link HostConnection} to the active host + * and allows for creation and removal of hosts + */ +public class HostManager { + private static final String TAG = LogUtils.makeLogTag(HostManager.class); + + // Singleton instance + private static volatile HostManager instance = null; + + private Context context; + + /** + * Arraylist that will hold all the hosts in the database + */ + private ArrayList hosts = new ArrayList(); + + /** + * Current host + */ + private HostInfo currentHostInfo = null; + /** + * Current host connection + */ + private HostConnection currentHostConnection = null; + + /** + * Picasso to download images from current XBMC + */ + private Picasso currentPicasso = null; + + /** + * Current connection observer + */ + private HostConnectionObserver currentHostConnectionObserver = null; + + /** + * Singleton constructor + * @param context Context (can pass Activity context, will get App Context) + */ + protected HostManager(Context context) { + this.context = context.getApplicationContext(); + } + + /** + * Singleton access method + * @param context Android app context + * @return HostManager singleton + */ + public static HostManager getInstance(Context context) { + if (instance == null) { + synchronized (HostManager.class) { + if (instance == null) { + instance = new HostManager(context); + } + } + } + return instance; + } + + /** + * Returns the current host list + * @return Host list + */ + public ArrayList getHosts() { + return getHosts(false); + } + + /** + * Returns the current host list, maybe forcing a reload from the database + * @param forcedReload Whether to force a reload from the database + * @return Host list + */ + public ArrayList getHosts(boolean forcedReload) { + if (forcedReload || (hosts.size() == 0)) { + hosts.clear(); + + Cursor cursor = context.getContentResolver() + .query(MediaContract.Hosts.CONTENT_URI, + MediaContract.Hosts.ALL_COLUMNS, + null, null, null); + + if (cursor.getCount() > 0) { + while (cursor.moveToNext()) { + int idx = 0; + int id = cursor.getInt(idx++); + long updated = cursor.getLong(idx++); + String name = cursor.getString(idx++); + String address = cursor.getString(idx++); + int protocol = cursor.getInt(idx++); + int httpPort = cursor.getInt(idx++); + int tcpPort = cursor.getInt(idx++); + String username = cursor.getString(idx++); + String password = cursor.getString(idx++); + String macAddress = cursor.getString(idx++); + int wolPort = cursor.getInt(idx++); + + hosts.add(new HostInfo(id, name, address, protocol, httpPort, tcpPort, + username, password, macAddress, wolPort)); + } + } + cursor.close(); + } + return hosts; + } + + /** + * Returns the current active host info + * @return Active host info + */ + public HostInfo getHostInfo() { + if (currentHostInfo == null) { + Settings settings = Settings.getInstance(context); + ArrayList hosts = getHosts(); + + // No host selected. Check if there are hosts configured and default to the first one + if (settings.currentHostId == -1) { + if (hosts.size() > 0) { + currentHostInfo = hosts.get(0); + settings.currentHostId = currentHostInfo.getId(); + settings.save(); + } + } else { + for (HostInfo host : hosts) { + if (host.getId() == settings.currentHostId) { + currentHostInfo = host; + break; + } + } + } + } + return currentHostInfo; + } + + /** + * Returns the current active host connection + * @return Active host connection + */ + public HostConnection getConnection() { + if (currentHostConnection == null) { + currentHostInfo = getHostInfo(); + + if (currentHostInfo != null) { + currentHostConnection = new HostConnection(currentHostInfo); + } + } + return currentHostConnection; + } + + /** + * Returns the current host {@link Picasso} image downloader + * @return {@link Picasso} instance suitable to download images from the current xbmc + */ + public Picasso getPicasso() { + if (currentPicasso == null) { + currentHostInfo = getHostInfo(); + if (currentHostInfo != null) { + currentPicasso = new Picasso.Builder(context) + .downloader(new BasicAuthPicassoDownloader(context, + currentHostInfo.getUsername(), currentHostInfo.getPassword())) + .build(); + } + } + + return currentPicasso; + } + + /** + * Returns the current {@link HostConnectionObserver} for the current connection + * @return The {@link HostConnectionObserver} for the current connection + */ + public HostConnectionObserver getHostConnectionObserver() { + if (currentHostConnectionObserver == null) { + currentHostConnection = getConnection(); + if (currentHostConnection != null) { + currentHostConnectionObserver = new HostConnectionObserver(currentHostConnection); + } + } + return currentHostConnectionObserver; + } + + /** + * Sets the current host. + * @param hostInfo Host info + */ + public void switchHost(HostInfo hostInfo) { + releaseCurrentHost(); + + currentHostInfo = hostInfo; + if (currentHostInfo != null) { + Settings settings = Settings.getInstance(context); + settings.currentHostId = currentHostInfo.getId(); + settings.save(); + } + } + +// /** +// * Sets the current host. +// * Throws {@link java.lang.IllegalArgumentException} if the host doesn't exist +// * @param hostId Host id +// */ +// public void switchHost(int hostId) { +// ArrayList hosts = getHosts(); +// HostInfo newHostInfo = null; +// +// for (HostInfo host : hosts) { +// if (host.getId() == hostId) { +// newHostInfo = host; +// break; +// } +// } +// +// if (newHostInfo == null) { +// throw new IllegalArgumentException("Host doesn't exist!"); +// } +// switchHost(newHostInfo); +// } + + /** + * Adds a new XBMC host to the database + * @param hostInfo Host to add + * @return Newly created {@link com.syncedsynapse.kore2.host.HostInfo} + */ + public HostInfo addHost(HostInfo hostInfo) { + return addHost(hostInfo.getName(), hostInfo.getAddress(), hostInfo.getProtocol(), + hostInfo.getHttpPort(), hostInfo.getTcpPort(), + hostInfo.getUsername(), hostInfo.getPassword(), + hostInfo.getMacAddress(), hostInfo.getWolPort()); + } + + + /** + * Adds a new XBMC host to the database + * @param name Name of this instance + * @param address Hostname or IP Address + * @param protocol Protocol to use + * @param httpPort HTTP port + * @param tcpPort TCP port + * @param username Username for HTTP + * @param password Password for HTTP + * @return Newly created {@link com.syncedsynapse.kore2.host.HostInfo} + */ + public HostInfo addHost(String name, String address, int protocol, int httpPort, int tcpPort, + String username, String password, String macAddress, int wolPort) { + + ContentValues values = new ContentValues(); + values.put(MediaContract.HostsColumns.NAME, name); + values.put(MediaContract.HostsColumns.ADDRESS, address); + values.put(MediaContract.HostsColumns.PROTOCOL, protocol); + values.put(MediaContract.HostsColumns.HTTP_PORT, httpPort); + values.put(MediaContract.HostsColumns.TCP_PORT, tcpPort); + values.put(MediaContract.HostsColumns.USERNAME, username); + values.put(MediaContract.HostsColumns.PASSWORD, password); + values.put(MediaContract.HostsColumns.MAC_ADDRESS, macAddress); + values.put(MediaContract.HostsColumns.WOL_PORT, wolPort); + + Uri newUri = context.getContentResolver() + .insert(MediaContract.Hosts.CONTENT_URI, values); + long newId = Long.valueOf(MediaContract.Hosts.getHostId(newUri)); + + // Refresh the list and return the created host + hosts = getHosts(true); + HostInfo newHost = null; + for (HostInfo host : hosts) { + if (host.getId() == newId) { + newHost = host; + break; + } + } + return newHost; + } + + /** + * Edits a host on the database + * @param hostId Id of the host to edit + * @param newHostInfo New values to update + * @return New {@link HostInfo} object + */ + public HostInfo editHost(int hostId, HostInfo newHostInfo) { + ContentValues values = new ContentValues(); + values.put(MediaContract.HostsColumns.NAME, newHostInfo.getName()); + values.put(MediaContract.HostsColumns.ADDRESS, newHostInfo.getAddress()); + values.put(MediaContract.HostsColumns.PROTOCOL, newHostInfo.getProtocol()); + values.put(MediaContract.HostsColumns.HTTP_PORT, newHostInfo.getHttpPort()); + values.put(MediaContract.HostsColumns.TCP_PORT, newHostInfo.getTcpPort()); + values.put(MediaContract.HostsColumns.USERNAME, newHostInfo.getUsername()); + values.put(MediaContract.HostsColumns.PASSWORD, newHostInfo.getPassword()); + values.put(MediaContract.HostsColumns.MAC_ADDRESS, newHostInfo.getMacAddress()); + values.put(MediaContract.HostsColumns.WOL_PORT, newHostInfo.getWolPort()); + + context.getContentResolver() + .update(MediaContract.Hosts.buildHostUri(hostId), values, null, null); + + // Refresh the list and return the created host + hosts = getHosts(true); + HostInfo newHost = null; + for (HostInfo host : hosts) { + if (host.getId() == hostId) { + newHost = host; + break; + } + } + return newHost; + } + + /** + * Deletes a host from the database. + * If the delete host is the current one, we will try too change the current one to another + * or set it to null if there's no other + * @param hostId Id of the host to delete + */ + public void deleteHost(final int hostId) { + // Async call delete. The triggers to delete all host information can take some time + new Thread(new Runnable() { + @Override + public void run() { + context.getContentResolver() + .delete(MediaContract.Hosts.buildHostUri(hostId), null, null); + } + }).start(); + + // Refresh information + int index = -1; + for (int i = 0; i < hosts.size(); i++) { + if (hosts.get(i).getId() == hostId) { + index = i; + break; + } + } + if (index != -1) + hosts.remove(index); + // If we just deleted the current connection, switch to another + if ((currentHostInfo != null) && (currentHostInfo.getId() == hostId)) { + releaseCurrentHost(); + if (hosts.size() > 0) + switchHost(hosts.get(0)); + } + } + + /** + * Releases all state related to the current connection + */ + private void releaseCurrentHost() { + if (currentHostConnectionObserver != null) { + currentHostConnectionObserver.unregisterAllObservers(); + currentHostConnectionObserver = null; + } + + if (currentHostConnection != null) { + currentHostConnection.disconnect(); + currentHostConnection = null; + } + + if (currentPicasso != null) { + // Calling shutdown here causes a picasso error: + // Handler (com.squareup.picasso.Stats$StatsHandler) {41b13d40} sending message to a Handler on a dead thread + // Check: https://github.com/square/picasso/issues/445 + // So, for now, just let it be... +// currentPicasso.shutdown(); + currentPicasso = null; + } + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/ApiCallback.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/ApiCallback.java new file mode 100644 index 0000000..a7d1cbe --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/ApiCallback.java @@ -0,0 +1,43 @@ +/* + * Copyright 2015 Synced Synapse. 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.syncedsynapse.kore2.jsonrpc; + +/** + * Callback from a JSON RPC method execution. + * When executing a method in JSON RPC, through + * {@link HostConnection#execute(ApiMethod, ApiCallback, android.os.Handler)}, + * an object implementing this interface should be provided, to call after receiving the response + * from XBMC. Depending on the response {@link ApiCallback#onSucess(Object)} or {@link + * ApiCallback#onError(int, String)} will be called. + * * @param Result type + */ +public interface ApiCallback { + + /** + * Callback that will be called after a sucessfull reponse from the XBMC JSON RPC method + * @param result The result that was obtained and sucessfully parsed from XBMC + */ + public abstract void onSucess(T result); + + /** + * Calllback that will be called when an error occurs executing the method on XBMC. + * This can be a general error (like a connection error), or an error reported by XBMC (like + * an incorrect call) + * @param errorCode Error code. Check {@link ApiException} for detailed error codes + * @param description Error description + */ + public abstract void onError(int errorCode, String description); +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/ApiException.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/ApiException.java new file mode 100644 index 0000000..bf02ab9 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/ApiException.java @@ -0,0 +1,119 @@ +/* + * Copyright 2015 Synced Synapse. 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.syncedsynapse.kore2.jsonrpc; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.syncedsynapse.kore2.utils.JsonUtils; + +/** + * Exception class for errors on JSON API. + * Some communication exceptions are catched and casted to this type. + * Response error from the JSON API are also returned as an instance of this exception. + */ +public class ApiException extends Exception { + + /** + * We got an invalid JSON response + */ + public static final int INVALID_JSON_RESPONSE_FROM_HOST = 0; + + /** + * IO Exception while connecting + */ + public static final int IO_EXCEPTION_WHILE_CONNECTING = 1; + + /** + * IO Exception while sending + */ + public static final int IO_EXCEPTION_WHILE_SENDING_REQUEST = 2; + + /** + * IO Exception while sending + */ + public static final int IO_EXCEPTION_WHILE_READING_RESPONSE = 3; + + /** + * HTTP response code unknown/unhandled + */ + public static final int HTTP_RESPONSE_CODE_UNKNOWN = 4; + + /** + * HTTP response code unknown/unhandled + */ + public static final int HTTP_RESPONSE_CODE_UNAUTHORIZED = 5; + + /** + * HTTP response code unknown/unhandled + */ + public static final int HTTP_RESPONSE_CODE_NOT_FOUND = 6; + + /** + * API returned an error + */ + public static int API_ERROR = 100; + + /** + * Attempted to send a method while not connected to host + */ + public static int API_NO_CONNECTION = 101; + + /** + * Attempted to execute a method with the same id of another already running + */ + public static int API_METHOD_WITH_SAME_ID_ALREADY_EXECUTING = 102; + + private int code; + + /** + * Constructor + * @param code Exception code + * @param message Message + */ + public ApiException(int code, String message) { + super(message); + this.code = code; + } + + /** + * Construct exception from other exception + * @param code Exception code + * @param originalException Original exception + */ + public ApiException(int code, Exception originalException) { + super(originalException); + this.code = code; + } + + /** + * Construct exception from JSON response + * @param code Exception code + * @param jsonResponse Json response, with an Error node + */ + public ApiException(int code, ObjectNode jsonResponse) { + super((jsonResponse.get(ApiMethod.ERROR_NODE) != null) ? + JsonUtils.stringFromJsonNode(jsonResponse.get(ApiMethod.ERROR_NODE), "message") : + "No message returned"); + this.code = code; + } + + /** + * Internal code of the exception + * @return Code of the exception + */ + public int getCode() { + return code; + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/ApiMethod.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/ApiMethod.java new file mode 100644 index 0000000..3b8b50c --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/ApiMethod.java @@ -0,0 +1,282 @@ +/* + * Copyright 2015 Synced Synapse. 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.syncedsynapse.kore2.jsonrpc; + + +import android.os.Handler; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.syncedsynapse.kore2.jsonrpc.type.ApiParameter; +import com.syncedsynapse.kore2.utils.LogUtils; + +import java.io.IOException; + +/** + * Abstract class base of all the JSON RPC API calls + * + * Every subclass represents a method on the JSON RPC API. + * + * Each subclass should implement constructors to represent each of the API call variations, and + * call this class {@link #execute(HostConnection, ApiCallback, android.os.Handler) execute()} to send + * the call to the server. + * + * This class is a template which should be typed with the return type of specific the method call. + */ +public abstract class ApiMethod { + private static final String TAG = LogUtils.makeLogTag(ApiMethod.class); + + public static final String RESULT_NODE = "result"; + public static final String ERROR_NODE = "error"; + public static final String ID_NODE = "id"; + public static final String METHOD_NODE = "method"; + public static final String PARAMS_NODE = "params"; + + /** + * Id of the method call. Autoincremented for each method call + */ + private static int lastId = 0; + protected final int id; + + protected static final ObjectMapper objectMapper = new ObjectMapper(); + /** + * Json object that will be used to generate the json representation of the current method call + */ + protected final ObjectNode jsonRequest; + + /** + * Constructor, sets up the necessary items to make the call later + */ + public ApiMethod() { + synchronized (this) { + this.id = (++lastId % 10000); + } + + // Create the rpc request object with the common fields according to JSON RPC spec + jsonRequest = objectMapper.createObjectNode(); + jsonRequest.put("jsonrpc", "2.0"); + jsonRequest.put(METHOD_NODE, getMethodName()); + jsonRequest.put(ID_NODE, id); + } + + /** + * Returns the parameters node of the json request object + * Creates one if necessary + * @return Parameters node + */ + protected ObjectNode getParametersNode() { + ObjectNode params; + if (jsonRequest.has(PARAMS_NODE)) { + params = (ObjectNode)jsonRequest.get(PARAMS_NODE); + } else { + params = objectMapper.createObjectNode(); + jsonRequest.put(PARAMS_NODE, params); + } + + return params; + } + + /** + * Adds a parameter to the request + * @param parameter Parameter name + * @param value Value to add + */ + protected void addParameterToRequest(String parameter, int value) { + getParametersNode().put(parameter, value); + } + + /** + * Adds a parameter to the request + * @param parameter Parameter name + * @param value Value to add + */ + protected void addParameterToRequest(String parameter, String value) { + if (value != null) + getParametersNode().put(parameter, value); + } + + + /** + * Adds a parameter to the request + * @param parameter Parameter name + * @param value Value to add + */ + protected void addParameterToRequest(String parameter, Integer value) { + if (value != null) + getParametersNode().put(parameter, value); + } + + /** + * Adds a parameter to the request + * @param parameter Parameter name + * @param value Value to add + */ + protected void addParameterToRequest(String parameter, Double value) { + if (value != null) + getParametersNode().put(parameter, value); + } + + /** + * Adds a parameter to the request + * @param parameter Parameter name + * @param value Value to add + */ + protected void addParameterToRequest(String parameter, boolean value) { + getParametersNode().put(parameter, value); + } + + /** + * Adds a parameter to the request + * @param parameter Parameter name + * @param values Values to add + */ + protected void addParameterToRequest(String parameter, String[] values) { + if (values != null) { + final ArrayNode arrayNode = objectMapper.createArrayNode(); + for (int i = 0; i < values.length; i++) { + arrayNode.add(values[i]); + } + getParametersNode().put(parameter, arrayNode); + } + } + + /** + * Adds a parameter to the request + * @param parameter Parameter name + * @param value Value to add + */ + protected void addParameterToRequest(String parameter, ApiParameter value) { + if (value != null) + getParametersNode().put(parameter, value.toJsonNode()); + } + + /** + * Adds a parameter to the request + * @param parameter Parameter name + * @param value Value to add + */ + protected void addParameterToRequest(String parameter, JsonNode value) { + if (value != null) + getParametersNode().put(parameter, value); + } + + /** + * Returns the id to identify the current method call. + * An id is generated for each object that is created. + * @return Method call id + */ + public int getId() { + return id; + } + + /** + * Returns the string json representation of the current method. + * @return Json string representation of the current method + */ + public String toJsonString() { return jsonRequest.toString(); } + + /** + * Returns the json object representation of the current method. + * @return JsonObject representation of the current method + */ + public ObjectNode toJsonObject() { return jsonRequest; } + +// /** +// * Calls the method represented by this object on the server. +// * This call is always asynchronous. The results will be posted, through the callback parameter, +// * on the same thread that is calling this method. +// * Note: The current thread must have a Looper prepared, otherwise this will fail because we +// * try to get handler on the thread. +// * +// * @param hostConnection Host connection on which to call the method +// * @param callback Callbacks to post the response to +// */ +// public void execute(HostConnection hostConnection, ApiCallback callback) { +// execute(hostConnection, callback, new Handler(Looper.myLooper())); +// } + + /** + * Calls the method represented by this object on the server. + * This call is always asynchronous. The results will be posted, through the callback parameter, + * on the specified handler. + * + * @param hostConnection Host connection on which to call the method + * @param callback Callbacks to post the response to + * @param handler Handler to invoke callbacks on + */ + public void execute(HostConnection hostConnection, ApiCallback callback, Handler handler) { + if (hostConnection != null) { + hostConnection.execute(this, callback, handler); + } else { + callback.onError(ApiException.API_NO_CONNECTION, "No connection specified."); + } + } + + /** + * Returns the current method name + * @return Current method name + */ + public abstract String getMethodName(); + + /** + * Constructs an object of this method's return type from a json response. + * This method must be implemented by each subcall to parse the json reponse and create + * an return object of the appropriate type for this api method. + * + * @param jsonResult Json response obtained from a call + * @return Result object of the appropriate type for this api method + */ + public T resultFromJson(String jsonResult) throws ApiException{ + try { + return resultFromJson((ObjectNode)objectMapper.readTree(jsonResult)); + } catch (JsonProcessingException e) { + throw new ApiException(ApiException.INVALID_JSON_RESPONSE_FROM_HOST, e); + } catch (IOException e) { + throw new ApiException(ApiException.INVALID_JSON_RESPONSE_FROM_HOST, e); + } + } + + /** + * Constructs an object of this method's return type from a json response. + * This method must be implemented by each subcall to parse the json reponse and create + * an return object of the appropriate type for this api method. + * + * @param jsonObject Json response obtained from a call + * @return Result object of the appropriate type for this api method + */ + public abstract T resultFromJson(ObjectNode jsonObject) throws ApiException; + + /** + * Default callback for methods which the result doesnt matter + */ + public static ApiCallback getDefaultActionCallback() { + + return new ApiCallback() { + @Override + public void onSucess(T result) { + } + + @Override + public void onError(int errorCode, String description) { + LogUtils.LOGD(TAG, "Got an error calling a method. Error code: " + errorCode + ", description: " + description); + } + }; + } + +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/ApiNotification.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/ApiNotification.java new file mode 100644 index 0000000..f7316cf --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/ApiNotification.java @@ -0,0 +1,71 @@ +/* + * Copyright 2015 Synced Synapse. 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.syncedsynapse.kore2.jsonrpc; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.syncedsynapse.kore2.jsonrpc.notification.Player; + +/** + * Abstract class, based of all the JSON RPC notifications + * + * Each specific notification should be a subclass of this. + */ +public abstract class ApiNotification { + protected static final String METHOD_NODE = "method"; + protected static final String PARAMS_NODE = "params"; + + public final String sender; + + /** + * Constructor from a notification node (starting on "params" node) + * @param node node + */ + public ApiNotification(ObjectNode node) { + sender = node.get("sender").textValue(); + } + + /** + * Returns this notification name + */ + public abstract String getNotificationName(); + + /** + * Returns a specific notification present in the Json Node + * + * @param node Json node with notification + * @return Specific notification object + */ + public static ApiNotification notificationFromJsonNode(JsonNode node) { + String method = node.get(METHOD_NODE).asText(); + ObjectNode params = (ObjectNode)node.get(PARAMS_NODE); + + ApiNotification result = null; + if (method.equals(Player.OnPause.NOTIFICATION_NAME)) { + result = new Player.OnPause(params); + } else if (method.equals(Player.OnPlay.NOTIFICATION_NAME)) { + result = new Player.OnPlay(params); + } else if (method.equals(Player.OnSeek.NOTIFICATION_NAME)) { + result = new Player.OnSeek(params); + } else if (method.equals(Player.OnSpeedChanged.NOTIFICATION_NAME)) { + result = new Player.OnSpeedChanged(params); + } else if (method.equals(Player.OnStop.NOTIFICATION_NAME)) { + result = new Player.OnStop(params); + } + + return result; + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/HostConnection.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/HostConnection.java new file mode 100644 index 0000000..a7b0d57 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/HostConnection.java @@ -0,0 +1,814 @@ +/* + * Copyright 2015 Synced Synapse. 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.syncedsynapse.kore2.jsonrpc; + +import android.os.*; +import android.os.Process; +import android.util.Base64; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.syncedsynapse.kore2.host.HostInfo; +import com.syncedsynapse.kore2.jsonrpc.notification.*; +import com.syncedsynapse.kore2.jsonrpc.notification.System; +import com.syncedsynapse.kore2.utils.LogUtils; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.net.HttpURLConnection; +import java.net.InetSocketAddress; +import java.net.ProtocolException; +import java.net.Socket; +import java.net.SocketException; +import java.net.URL; +import java.util.HashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * Class responsible for communicating with the host. + */ +public class HostConnection { + public static final String TAG = LogUtils.makeLogTag(HostConnection.class); + + private static final int TIMEOUT = 5000; // ns + + /** + * Communicate via TCP + */ + public static final int PROTOCOL_TCP = 0; + /** + * Communicate via HTTP + */ + public static final int PROTOCOL_HTTP = 1; + + /** + * Interface that an observer must implement to be notified of player notifications + */ + public interface PlayerNotificationsObserver { + public void onPlay(Player.OnPlay notification); + public void onPause(Player.OnPause notification); + public void onSpeedChanged(Player.OnSpeedChanged notification); + public void onSeek(Player.OnSeek notification); + public void onStop(Player.OnStop notification); + } + + /** + * Interface that an observer must implement to be notified of System notifications + */ + public interface SystemNotificationsObserver { + public void onQuit(System.OnQuit notification); + public void onRestart(System.OnRestart notification); + public void onSleep(System.OnSleep notification); + } + + /** + * Interface that an observer must implement to be notified of Input notifications + */ + public interface InputNotificationsObserver { + public void onInputRequested(Input.OnInputRequested notification); + } + + /** + * Host to connect too + */ + private final HostInfo hostInfo; + + /** + * The protocol to use: {@link #PROTOCOL_HTTP} or {@link #PROTOCOL_TCP} + * This is initially obtained from the {@link HostInfo}, but can be later changed through + * {@link #setProtocol(int)} + */ + private int protocol; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + /** + * Socket used to communicate through TCP + */ + private Socket socket = null; + /** + * Listener {@link Thread} that will be listening on the TCP socket + */ + private Thread listenerThread = null; + + /** + * {@link java.util.HashMap} that will hold the {@link MethodCallInfo} with the information + * necessary to respond to clients (TCP only) + */ + private final HashMap> clientCallbacks = new HashMap>(); + + /** + * The observers that will be notified of player notifications + */ + private final HashMap playerNotificationsObservers = + new HashMap(); + + /** + * The observers that will be notified of system notifications + */ + private final HashMap systemNotificationsObservers = + new HashMap(); + + /** + * The observers that will be notified of input notifications + */ + private final HashMap inputNotificationsObservers = + new HashMap(); + + private ExecutorService executorService; + + public HostConnection(final HostInfo hostInfo) { + this.hostInfo = hostInfo; + // Start with the default host protocol + this.protocol = hostInfo.getProtocol(); + // Create a single threaded executor + this.executorService = Executors.newSingleThreadExecutor(); + } + + /** + * Returns this connection protocol + * @return {@link #PROTOCOL_HTTP} or {@link #PROTOCOL_TCP} + */ + public int getProtocol() { + return protocol; + } + + /** + * Overrides the protocol for this connection + * @param protocol {@link #PROTOCOL_HTTP} or {@link #PROTOCOL_TCP} + */ + public void setProtocol(int protocol) { + if (!isValidProtocol(protocol)) { + throw new IllegalArgumentException("Invalid protocol specified."); + } + this.protocol = protocol; + } + + public static boolean isValidProtocol(int protocol) { + return ((protocol == PROTOCOL_TCP) || (protocol == PROTOCOL_HTTP)); + } + + /** + * Registers an observer for player notifications + * @param observer The {@link PlayerNotificationsObserver} + */ + public void registerPlayerNotificationsObserver(PlayerNotificationsObserver observer, + Handler handler) { + playerNotificationsObservers.put(observer, handler); + } + + /** + * Unregisters and observer from the player notifications + * @param observer The {@link PlayerNotificationsObserver} to unregister + */ + public void unregisterPlayerNotificationsObserver(PlayerNotificationsObserver observer) { + playerNotificationsObservers.remove(observer); + } + + /** + * Registers an observer for system notifications + * @param observer The {@link SystemNotificationsObserver} + */ + public void registerSystemNotificationsObserver(SystemNotificationsObserver observer, + Handler handler) { + systemNotificationsObservers.put(observer, handler); + } + + /** + * Unregisters and observer from the system notifications + * @param observer The {@link SystemNotificationsObserver} + */ + public void unregisterSystemNotificationsObserver(SystemNotificationsObserver observer) { + systemNotificationsObservers.remove(observer); + } + + /** + * Registers an observer for input notifications + * @param observer The {@link InputNotificationsObserver} + */ + public void registerInputNotificationsObserver(InputNotificationsObserver observer, + Handler handler) { + inputNotificationsObservers.put(observer, handler); + } + + /** + * Unregisters and observer from the input notifications + * @param observer The {@link InputNotificationsObserver} + */ + public void unregisterInputNotificationsObserver(InputNotificationsObserver observer) { + inputNotificationsObservers.remove(observer); + } + + /** + * Calls the a method on the server + * This call is always asynchronous. The results will be posted, through the + * {@link ApiCallback callback} parameter, on the specified {@link android.os.Handler}. + * + * @param method Method object that represents the methood too call + * @param callback {@link ApiCallback} to post the response to + * @param handler {@link Handler} to invoke callbacks on + * @param Method return type + */ + public void execute(final ApiMethod method, final ApiCallback callback, + final Handler handler) { + LogUtils.LOGD(TAG, "Starting method execute. Method: " + method.getMethodName() + + " on host: " + hostInfo.getJsonRpcHttpEndpoint()); + + // Launch background thread + Runnable command = new Runnable() { + @Override + public void run() { + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + if (protocol == PROTOCOL_HTTP) { + executeThroughHTTP(method, callback, handler); + } else { + executeThroughTcp(method, callback, handler); + } + } + }; + + executorService.execute(command); + //new Thread(command).start(); + } + + /** + * Sends the JSON RPC request through HTTP + */ + private void executeThroughHTTP(final ApiMethod method, final ApiCallback callback, + final Handler handler) { + String jsonRequest = method.toJsonString(); + try { + HttpURLConnection connection = openHttpConnection(hostInfo); + sendHttpRequest(connection, jsonRequest); + // Read response and convert it + final T result = method.resultFromJson(parseJsonResponse(readHttpResponse(connection))); + + if ((handler != null) && (callback != null)) { + handler.post(new Runnable() { + @Override + public void run() { + callback.onSucess(result); + } + }); + } + } catch (final ApiException e) { + // Got an error, call error handler + + if ((handler != null) && (callback != null)) { + handler.post(new Runnable() { + @Override + public void run() { + callback.onError(e.getCode(), e.getMessage()); + } + }); + } + } + } + + /** + * Sends the JSON RPC request through HTTP, and calls the callback with the raw response, + * not parsed into the internal representation. + * Useful for sync methods that don't want to incur the overhead of constructing the + * internal objects. + * + * @param method Method object that represents the method too call + * @param callback {@link ApiCallback} to post the response to. This will be the raw + * {@link ObjectNode} received + * @param handler {@link Handler} to invoke callbacks on + * @param Method return type + */ + public void executeRaw(final ApiMethod method, final ApiCallback callback, + final Handler handler) { + String jsonRequest = method.toJsonString(); + try { + HttpURLConnection connection = openHttpConnection(hostInfo); + sendHttpRequest(connection, jsonRequest); + // Read response and convert it + final ObjectNode result = parseJsonResponse(readHttpResponse(connection)); + + if ((handler != null) && (callback != null)) { + handler.post(new Runnable() { + @Override + public void run() { + callback.onSucess(result); + } + }); + } + } catch (final ApiException e) { + // Got an error, call error handler + if ((handler != null) && (callback != null)) { + handler.post(new Runnable() { + @Override + public void run() { + callback.onError(e.getCode(), e.getMessage()); + } + }); + } + } + } + + + /** + * Auxiliary method to open a HTTP connection. + * This method calls connect() so that any errors are cathced + * @param hostInfo Host info + * @return Connection set up + * @throws ApiException + */ + private HttpURLConnection openHttpConnection(HostInfo hostInfo) throws ApiException { + try { +// LogUtils.LOGD(TAG, "Opening HTTP connection."); + HttpURLConnection connection = (HttpURLConnection) new URL(hostInfo.getJsonRpcHttpEndpoint()).openConnection(); + connection.setRequestMethod("POST"); + connection.setConnectTimeout(TIMEOUT); + connection.setReadTimeout(TIMEOUT); + connection.setRequestProperty("Content-Type", "application/json"); + connection.setDoOutput(true); + + // http basic authorization + if ((hostInfo.getUsername() != null) && !hostInfo.getUsername().isEmpty() && + (hostInfo.getPassword() != null) && !hostInfo.getPassword().isEmpty()) { + final String token = Base64.encodeToString((hostInfo.getUsername() + ":" + + hostInfo.getPassword()).getBytes(), Base64.DEFAULT); + connection.setRequestProperty("Authorization", "Basic " + token); + } + + // Check the connection + connection.connect(); + return connection; + } catch (ProtocolException e) { + // Won't try to catch this + LogUtils.LOGE(TAG, "Got protocol exception while opening HTTP connection.", e); + throw new RuntimeException(e); + } catch (IOException e) { + LogUtils.LOGW(TAG, "Failed to open HTTP connection.", e); + throw new ApiException(ApiException.IO_EXCEPTION_WHILE_CONNECTING, e); + } + } + + /** + * Send an HTTP POST request + * @param connection Open connection + * @param request Request to send + * @throws ApiException + */ + private void sendHttpRequest(HttpURLConnection connection, String request) throws ApiException { + try { + LogUtils.LOGD(TAG, "Sending request via HTTP: " + request); + OutputStreamWriter out = new OutputStreamWriter(connection.getOutputStream()); + out.write(request); + out.flush(); + out.close(); + } catch (IOException e) { + LogUtils.LOGW(TAG, "Failed to send HTTP request.", e); + throw new ApiException(ApiException.IO_EXCEPTION_WHILE_SENDING_REQUEST, e); + } + + } + + /** + * Reads the response from the server + * @param connection Connection + * @return Response read + * @throws ApiException + */ + private String readHttpResponse(HttpURLConnection connection) throws ApiException { + try { +// LogUtils.LOGD(TAG, "Reading HTTP response."); + int responseCode = connection.getResponseCode(); + + switch (responseCode) { + case 200: + // All ok, read response + BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream())); + StringBuilder response = new StringBuilder(); + String inputLine; + while ((inputLine = in.readLine()) != null) + response.append(inputLine); + in.close(); + LogUtils.LOGD(TAG, "HTTP response: " + response.toString()); + return response.toString(); + case 401: + LogUtils.LOGD(TAG, "HTTP response read error. Got a 401."); + throw new ApiException(ApiException.HTTP_RESPONSE_CODE_UNAUTHORIZED, + "Server returned response code: " + responseCode); + case 404: + LogUtils.LOGD(TAG, "HTTP response read error. Got a 404."); + throw new ApiException(ApiException.HTTP_RESPONSE_CODE_NOT_FOUND, + "Server returned response code: " + responseCode); + default: + LogUtils.LOGD(TAG, "HTTP response read error. Got: " + responseCode); + throw new ApiException(ApiException.HTTP_RESPONSE_CODE_UNKNOWN, + "Server returned response code: " + responseCode); + } + } catch (IOException e) { + LogUtils.LOGW(TAG, "Failed to read HTTP response.", e); + throw new ApiException(ApiException.IO_EXCEPTION_WHILE_READING_RESPONSE, e); + } + } + + /** + * Parses the JSON response from the server. + * If it is a valid result returns the JSON {@link com.fasterxml.jackson.databind.node.ObjectNode} that represents it. + * If it is an error (contains the error tag), returns an {@link ApiException} with the info. + * @param response JSON response + * @return {@link com.fasterxml.jackson.databind.node.ObjectNode} constructed + * @throws ApiException + */ + private ObjectNode parseJsonResponse(String response) throws ApiException { +// LogUtils.LOGD(TAG, "Parsing JSON response"); + try { + ObjectNode jsonResponse = (ObjectNode) objectMapper.readTree(response); + + if (jsonResponse.has(ApiMethod.ERROR_NODE)) { + throw new ApiException(ApiException.API_ERROR, jsonResponse); + } + + if (!jsonResponse.has(ApiMethod.RESULT_NODE)) { + // Something strange is going on + throw new ApiException(ApiException.INVALID_JSON_RESPONSE_FROM_HOST, + "Result doesn't contain a result node."); + } + + return jsonResponse; + } catch (JsonProcessingException e) { + LogUtils.LOGW(TAG, "Got an exception while parsing JSON response.", e); + throw new ApiException(ApiException.INVALID_JSON_RESPONSE_FROM_HOST, e); + } catch (IOException e) { + LogUtils.LOGW(TAG, "Got an exception while parsing JSON response.", e); + throw new ApiException(ApiException.INVALID_JSON_RESPONSE_FROM_HOST, e); + } + } + + /** + * Sends the JSON RPC request through TCP + * Keeps a background thread running, listening on a socket + */ + private void executeThroughTcp(final ApiMethod method, final ApiCallback callback, + final Handler handler) { + + // TODO: We're going to create a background listener thread. + // Also create a thread that periodically checks if the connection should be shutdown + // based on not having activity. Use android timer or Thread.sleep + String methodId = String.valueOf(method.getId()); + try { + // Save this method/callback for later response + // Check if a method with this id is already running and raise an error if so + synchronized (clientCallbacks) { + if (clientCallbacks.containsKey(methodId)) { + if ((handler != null) && (callback != null)) { + handler.post(new Runnable() { + @Override + public void run() { + callback.onError(ApiException.API_METHOD_WITH_SAME_ID_ALREADY_EXECUTING, + "A method with the same Id is already executing"); + } + }); + } + return; + } + clientCallbacks.put(methodId, new MethodCallInfo(method, callback, handler)); + } + + // TODO: Validate if this shouldn't be enclosed by a synchronized. + if (socket == null) { + // Open connection to the server and setup reader thread + socket = openTcpConnection(hostInfo); + listenerThread = newListenerThread(socket); + listenerThread.start(); + } + + // Write request + sendTcpRequest(socket, method.toJsonString()); + } catch (final ApiException e) { + callErrorCallback(methodId, e); + } + } + + /** + * Auxiliary method to open the TCP {@link Socket}. + * This method calls connect() so that any errors are cathced + * @param hostInfo Host info + * @return Connection set up + * @throws ApiException + */ + private Socket openTcpConnection(HostInfo hostInfo) throws ApiException { + try { + LogUtils.LOGD(TAG, "Opening TCP connection on host: " + hostInfo.getAddress()); + + Socket socket = new Socket(); + final InetSocketAddress address = new InetSocketAddress(hostInfo.getAddress(), hostInfo.getTcpPort()); + socket.setSoTimeout(0); // No read timeout. Read should block + socket.connect(address, TIMEOUT); + + return socket; + } catch (SocketException e) { + LogUtils.LOGW(TAG, "Failed to open TCP connection to host: " + hostInfo.getAddress()); + throw new ApiException(ApiException.IO_EXCEPTION_WHILE_CONNECTING, e); + } catch (IOException e) { + LogUtils.LOGW(TAG, "Failed to open TCP connection to host: " + hostInfo.getAddress()); + throw new ApiException(ApiException.IO_EXCEPTION_WHILE_CONNECTING, e); + } + } + + + /** + * Send a TCP request + * @param socket Socket to write to + * @param request Request to send + * @throws ApiException + */ + private void sendTcpRequest(Socket socket, String request) throws ApiException { + try { + LogUtils.LOGD(TAG, "Sending request via TCP: " + request); + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); + writer.write(request); + writer.flush(); + } catch (Exception e) { + LogUtils.LOGW(TAG, "Failed to send TCP request.", e); + disconnect(); + throw new ApiException(ApiException.IO_EXCEPTION_WHILE_SENDING_REQUEST, e); + } + } + + private Thread newListenerThread(final Socket socket) { + // Launch a new thread to read from the socket + return new Thread(new Runnable() { + @Override + public void run() { + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + try { + LogUtils.LOGD(TAG, "Starting socket listener thread..."); + // We're going to read from the socket. This will be a blocking call and + // it will keep on going until disconnect() is called on this object. + // Note: Mind the objects used here: we use createParser because it doesn't + // close the socket after ObjectMapper.readTree. + JsonParser jsonParser = objectMapper.getFactory().createParser(socket.getInputStream()); + ObjectNode jsonResponse; + while ((jsonResponse = objectMapper.readTree(jsonParser)) != null) { + LogUtils.LOGD(TAG, "Read from socket: " + jsonResponse.toString()); +// LogUtils.LOGD_FULL(TAG, "Read from socket: " + jsonResponse.toString()); + handleTcpResponse(jsonResponse); + } + } catch (JsonProcessingException e) { + LogUtils.LOGW(TAG, "Got an exception while parsing JSON response.", e); + callErrorCallback(null, new ApiException(ApiException.INVALID_JSON_RESPONSE_FROM_HOST, e)); + } catch (IOException e) { + LogUtils.LOGW(TAG, "Error reading from socket.", e); + disconnect(); + callErrorCallback(null, new ApiException(ApiException.IO_EXCEPTION_WHILE_READING_RESPONSE, e)); + } + } + }); + } + + private void handleTcpResponse(ObjectNode jsonResponse) { + + if (!jsonResponse.has(ApiMethod.ID_NODE)) { + // It's a notification, notify observers + String notificationName = jsonResponse.get(ApiNotification.METHOD_NODE).asText(); + ObjectNode params = (ObjectNode)jsonResponse.get(ApiNotification.PARAMS_NODE); + + if (notificationName.equals(Player.OnPause.NOTIFICATION_NAME)) { + final Player.OnPause apiNotification = new Player.OnPause(params); + for (final PlayerNotificationsObserver observer : + playerNotificationsObservers.keySet()) { + Handler handler = playerNotificationsObservers.get(observer); + handler.post(new Runnable() { + @Override + public void run() { + observer.onPause(apiNotification); + } + }); + } + } else if (notificationName.equals(Player.OnPlay.NOTIFICATION_NAME)) { + final Player.OnPlay apiNotification = new Player.OnPlay(params); + for (final PlayerNotificationsObserver observer : + playerNotificationsObservers.keySet()) { + Handler handler = playerNotificationsObservers.get(observer); + handler.post(new Runnable() { + @Override + public void run() { + observer.onPlay(apiNotification); + } + }); + } + } else if (notificationName.equals(Player.OnSeek.NOTIFICATION_NAME)) { + final Player.OnSeek apiNotification = new Player.OnSeek(params); + for (final PlayerNotificationsObserver observer : + playerNotificationsObservers.keySet()) { + Handler handler = playerNotificationsObservers.get(observer); + handler.post(new Runnable() { + @Override + public void run() { + observer.onSeek(apiNotification); + } + }); + } + } else if (notificationName.equals(Player.OnSpeedChanged.NOTIFICATION_NAME)) { + final Player.OnSpeedChanged apiNotification = new Player.OnSpeedChanged(params); + for (final PlayerNotificationsObserver observer : + playerNotificationsObservers.keySet()) { + Handler handler = playerNotificationsObservers.get(observer); + handler.post(new Runnable() { + @Override + public void run() { + observer.onSpeedChanged(apiNotification); + } + }); + } + } else if (notificationName.equals(Player.OnStop.NOTIFICATION_NAME)) { + final Player.OnStop apiNotification = new Player.OnStop(params); + for (final PlayerNotificationsObserver observer : + playerNotificationsObservers.keySet()) { + Handler handler = playerNotificationsObservers.get(observer); + handler.post(new Runnable() { + @Override + public void run() { + observer.onStop(apiNotification); + } + }); + } + } else if (notificationName.equals(System.OnQuit.NOTIFICATION_NAME)) { + final System.OnQuit apiNotification = new System.OnQuit(params); + for (final SystemNotificationsObserver observer : + systemNotificationsObservers.keySet()) { + Handler handler = systemNotificationsObservers.get(observer); + handler.post(new Runnable() { + @Override + public void run() { + observer.onQuit(apiNotification); + } + }); + } + } else if (notificationName.equals(System.OnRestart.NOTIFICATION_NAME)) { + final System.OnRestart apiNotification = new System.OnRestart(params); + for (final SystemNotificationsObserver observer : + systemNotificationsObservers.keySet()) { + Handler handler = systemNotificationsObservers.get(observer); + handler.post(new Runnable() { + @Override + public void run() { + observer.onRestart(apiNotification); + } + }); + } + } else if (notificationName.equals(System.OnSleep.NOTIFICATION_NAME)) { + final System.OnSleep apiNotification = new System.OnSleep(params); + for (final SystemNotificationsObserver observer : + systemNotificationsObservers.keySet()) { + Handler handler = systemNotificationsObservers.get(observer); + handler.post(new Runnable() { + @Override + public void run() { + observer.onSleep(apiNotification); + } + }); + } + } else if (notificationName.equals(Input.OnInputRequested.NOTIFICATION_NAME)) { + final Input.OnInputRequested apiNotification = new Input.OnInputRequested(params); + for (final InputNotificationsObserver observer : + inputNotificationsObservers.keySet()) { + Handler handler = inputNotificationsObservers.get(observer); + handler.post(new Runnable() { + @Override + public void run() { + observer.onInputRequested(apiNotification); + } + }); + } + } + + LogUtils.LOGD(TAG, "Got a notification: " + jsonResponse.get("method").textValue()); + } else { + String methodId = jsonResponse.get(ApiMethod.ID_NODE).asText(); + + if (jsonResponse.has(ApiMethod.ERROR_NODE)) { + // Error response + callErrorCallback(methodId, new ApiException(ApiException.API_ERROR, jsonResponse)); + } else { + // Sucess response + final MethodCallInfo methodCallInfo = clientCallbacks.get(methodId); +// LogUtils.LOGD(TAG, "Sending response to method: " + methodCallInfo.method.getMethodName()); + + if (methodCallInfo != null) { + try { + final T result = (T) methodCallInfo.method.resultFromJson(jsonResponse); + final ApiCallback callback = (ApiCallback) methodCallInfo.callback; + + if ((methodCallInfo.handler != null) && (callback != null)) { + methodCallInfo.handler.post(new Runnable() { + @Override + public void run() { + callback.onSucess(result); + } + }); + } + + // We've replied, remove the client from the list + synchronized (clientCallbacks) { + clientCallbacks.remove(methodId); + } + } catch (ApiException e) { + callErrorCallback(methodId, e); + } + } + } + } + } + + private void callErrorCallback(String methodId, final ApiException error) { + synchronized (clientCallbacks) { + if (methodId != null) { + // Send error back to client + final MethodCallInfo methodCallInfo = clientCallbacks.get(methodId); + if (methodCallInfo != null) { + final ApiCallback callback = (ApiCallback) methodCallInfo.callback; + + if ((methodCallInfo.handler != null) && (callback != null)) { + methodCallInfo.handler.post(new Runnable() { + @Override + public void run() { + callback.onError(error.getCode(), error.getMessage()); + } + }); + } + } + clientCallbacks.remove(methodId); + } else { + // Notify all pending clients, it might be an error for them + for (String id : clientCallbacks.keySet()) { + final MethodCallInfo methodCallInfo = clientCallbacks.get(id); + final ApiCallback callback = (ApiCallback)methodCallInfo.callback; + + if ((methodCallInfo.handler != null) && (callback != null)) { + methodCallInfo.handler.post(new Runnable() { + @Override + public void run() { + callback.onError(error.getCode(), error.getMessage()); + } + }); + } + } + clientCallbacks.clear(); + } + } + } + + /** + * Cleans up used resources. + * This method should always be called if the protocoll used is TCP, so we can shutdown gracefully + */ + public void disconnect() { + if (protocol == PROTOCOL_HTTP) + return; + + try { + if (socket != null) { + // Remove pending calls + if (!socket.isClosed()) { + socket.close(); + } + } + } catch (IOException e) { + LogUtils.LOGE(TAG, "Error while closing socket", e); + } finally { + socket = null; + } + } + + /** + * Helper class to aggregate a method, callback and handler + * @param + */ + private static class MethodCallInfo { + public final ApiMethod method; + public final ApiCallback callback; + public final Handler handler; + + public MethodCallInfo(ApiMethod method, ApiCallback callback, Handler handler) { + this.method = method; + this.callback = callback; + this.handler = handler; + } + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/event/MediaSyncEvent.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/event/MediaSyncEvent.java new file mode 100644 index 0000000..f2df9fb --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/event/MediaSyncEvent.java @@ -0,0 +1,54 @@ +/* + * Copyright 2015 Synced Synapse. 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.syncedsynapse.kore2.jsonrpc.event; + +import android.os.Bundle; + +/** + * Event to post on {@link de.greenrobot.event.EventBus} that notifies of a sync + */ +public class MediaSyncEvent { + public static final int STATUS_FAIL = 0; + public static final int STATUS_SUCCESS = 1; + + public final String syncType; + public final int status; + public final int errorCode; + public final String errorMessage; + public final Bundle syncExtras; + + /** + * Creates a new sync event + * + * @param syncType One of the constants in {@link com.syncedsynapse.kore2.service.LibrarySyncService} + */ + public MediaSyncEvent(String syncType, Bundle syncExtras, int status) { + this(syncType, syncExtras, status, -1, null); + // Assert that status is success + if (status != STATUS_SUCCESS) + throw new IllegalArgumentException("This MediaSyncEvent constructor should only be " + + "called with a successful status."); + } + + public MediaSyncEvent(String syncType, Bundle syncExtras, + int status, int errorCode, String errorMessage) { + this.syncType = syncType; + this.syncExtras = syncExtras; + this.status = status; + this.errorCode = errorCode; + this.errorMessage = errorMessage; + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/Addons.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/Addons.java new file mode 100644 index 0000000..1c447f1 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/Addons.java @@ -0,0 +1,168 @@ +/* + * Copyright 2015 Synced Synapse. 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.syncedsynapse.kore2.jsonrpc.method; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.syncedsynapse.kore2.jsonrpc.ApiException; +import com.syncedsynapse.kore2.jsonrpc.ApiMethod; +import com.syncedsynapse.kore2.jsonrpc.type.AddonType; + +import java.util.ArrayList; +import java.util.List; + +/** + * JSON RPC methods in Addons.* + */ +public class Addons { + + /** + * Executes the given addon with the given parameters (if possible) + */ + public static final class ExecuteAddon extends ApiMethod { + public final static String METHOD_NAME = "Addons.ExecuteAddon"; + + /** + * Known addon ids + */ + public final static String ADDON_SUBTITLES = "script.xbmc.subtitles"; + + /** + * Executes the given addon with the given parameters (if possible) + */ + public ExecuteAddon(String addonId) { + super(); + addParameterToRequest("addonid", addonId); + } + + @Override + public String getMethodName() { + return METHOD_NAME; + } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + + /** + * Gets all available addons + */ + public static final class GetAddons extends ApiMethod> { + public final static String METHOD_NAME = "Addons.GetAddons"; + + private final static String LIST_NODE = "addons"; + + /** + * Gets all available addons + * @param enabled Whether to get enabled addons + * @param properties Properties to retrieve. See {AddonType.Fields} + */ + public GetAddons(boolean enabled, String... properties) { + super(); + addParameterToRequest("enabled", enabled); + addParameterToRequest("properties", properties); + } + + /** + * Gets all available addons + * @param properties Properties to retrieve. See {AddonType.Fields} + */ + public GetAddons(String... properties) { + super(); + addParameterToRequest("properties", properties); + } + + @Override + public String getMethodName() { + return METHOD_NAME; + } + + @Override + public List resultFromJson(ObjectNode jsonObject) throws ApiException { + JsonNode resultNode = jsonObject.get(RESULT_NODE); + ArrayNode items = resultNode.has(LIST_NODE) ? + (ArrayNode)resultNode.get(LIST_NODE) : null; + if (items == null) { + return new ArrayList(0); + } + ArrayList result = new ArrayList(items.size()); + + for (JsonNode item : items) { + result.add(new AddonType.Details(item)); + } + + return result; + } + } + + /** + * Gets the details of a specific addon + */ + public static final class GetAddonDetails extends ApiMethod { + public final static String METHOD_NAME = "Addons.GetAddonDetails"; + + /** + * Gets the details of a specific addon + * @param addonid Addon id + * @param properties Properties to retrieve. See {AddonType.Fields} + */ + public GetAddonDetails(String addonid, String... properties) { + super(); + addParameterToRequest("addonid", addonid); + addParameterToRequest("properties", properties); + } + + @Override + public String getMethodName() { + return METHOD_NAME; + } + + @Override + public AddonType.Details resultFromJson(ObjectNode jsonObject) throws ApiException { + return new AddonType.Details(jsonObject.get(RESULT_NODE).get("addon")); + } + } + + /** + * Enables/Disables a specific addon + */ + public static final class SetAddonEnabled extends ApiMethod { + public final static String METHOD_NAME = "Addons.SetAddonEnabled"; + + /** + * Enables/Disables a specific addon + */ + public SetAddonEnabled(String addonId, boolean enabled) { + super(); + addParameterToRequest("addonid", addonId); + addParameterToRequest("enabled", enabled); + } + + @Override + public String getMethodName() { + return METHOD_NAME; + } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/Application.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/Application.java new file mode 100644 index 0000000..19dfc2c --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/Application.java @@ -0,0 +1,131 @@ +/* + * Copyright 2015 Synced Synapse. 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.syncedsynapse.kore2.jsonrpc.method; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.syncedsynapse.kore2.jsonrpc.ApiException; +import com.syncedsynapse.kore2.jsonrpc.ApiMethod; +import com.syncedsynapse.kore2.jsonrpc.type.ApplicationType; +import com.syncedsynapse.kore2.utils.JsonUtils; + +/** + * All JSON RPC methods in Application.* + */ +public class Application { + + /** + * Quit application + */ + public static final class Quit extends ApiMethod { + public final static String METHOD_NAME = "Application.Quit"; + + /** + * Quit application + */ + public Quit() { + super(); + } + + @Override + public String getMethodName() { + return METHOD_NAME; + } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + + /** + * Set the current volume + */ + public static final class SetVolume extends ApiMethod { + public final static String METHOD_NAME = "Application.SetVolume"; + + /** + * Set the current volume + * @param volume String enum in {@link com.syncedsynapse.kore2.jsonrpc.type.GlobalType.IncrementDecrement} + */ + public SetVolume(String volume) { + super(); + addParameterToRequest("volume", volume); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public Integer resultFromJson(ObjectNode jsonObject) throws ApiException { + return JsonUtils.intFromJsonNode(jsonObject, RESULT_NODE); + } + } + + /** + * Toggle mute/unmute + */ + public static final class SetMute extends ApiMethod { + public final static String METHOD_NAME = "Application.SetMute"; + + /** + * Toggle mute/unmute + */ + public SetMute() { + super(); + addParameterToRequest("mute", "toggle"); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public Boolean resultFromJson(ObjectNode jsonObject) throws ApiException { + return JsonUtils.booleanFromJsonNode(jsonObject, RESULT_NODE); + } + } + + /** + * Retrieves the values of the given properties. + */ + public static class GetProperties extends ApiMethod { + public final static String METHOD_NAME = "Application.GetProperties"; + + /** + * Properties + */ + public final static String VOLUME = "volume"; + public final static String MUTED = "muted"; + public final static String NAME = "name"; + public final static String VERSION = "version"; + + /** + * Retrieves the values of the given properties. + * @param properties See this class constants. + */ + public GetProperties(String... properties) { + super(); + addParameterToRequest("properties", properties); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public ApplicationType.PropertyValue resultFromJson(ObjectNode jsonObject) throws ApiException { + return new ApplicationType.PropertyValue(jsonObject.get(RESULT_NODE)); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/AudioLibrary.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/AudioLibrary.java new file mode 100644 index 0000000..3e8749e --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/AudioLibrary.java @@ -0,0 +1,256 @@ +/* + * Copyright 2015 Synced Synapse. 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.syncedsynapse.kore2.jsonrpc.method; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.syncedsynapse.kore2.jsonrpc.ApiException; +import com.syncedsynapse.kore2.jsonrpc.ApiMethod; +import com.syncedsynapse.kore2.jsonrpc.type.AudioType; +import com.syncedsynapse.kore2.jsonrpc.type.LibraryType; + +import java.util.ArrayList; +import java.util.List; + +/** + * JSON RPC methods in AudioLibrary.* + */ +public class AudioLibrary { + + /** + * Cleans the audio library from non-existent items. + */ + public static class Clean extends ApiMethod { + public final static String METHOD_NAME = "AudioLibrary.Clean"; + + /** + * Cleans the video library from non-existent items. + */ + public Clean() { + super(); + } + + @Override + public String getMethodName() { + return METHOD_NAME; + } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + + /** + * Scans the audio sources for new library items. + */ + public static class Scan extends ApiMethod { + public final static String METHOD_NAME = "AudioLibrary.Scan"; + + /** + * Scans the audio sources for new library items. + */ + public Scan() { + super(); + } + + @Override + public String getMethodName() { + return METHOD_NAME; + } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + + /** + * Retrieve all artists + */ + public static class GetArtists extends ApiMethod> { + public final static String METHOD_NAME = "AudioLibrary.GetArtists"; + + private final static String LIST_NODE = "artists"; + + /** + * Retrieve all artists + * + * @param albumartistsonly Whether or not to include artists only appearing in + * compilations. If the parameter is not passed or is passed as + * null the GUI setting will be used + * @param properties Properties to retrieve. See {@link AudioType.FieldsArtists} for a + * list of accepted values + */ + public GetArtists(boolean albumartistsonly, String... properties) { + super(); + addParameterToRequest("albumartistsonly", albumartistsonly); + addParameterToRequest("properties", properties); + } + + @Override + public String getMethodName() { + return METHOD_NAME; + } + + @Override + public List resultFromJson(ObjectNode jsonObject) + throws ApiException { + JsonNode resultNode = jsonObject.get(RESULT_NODE); + ArrayNode items = resultNode.has(LIST_NODE) ? + (ArrayNode)resultNode.get(LIST_NODE) : null; + if (items == null) { + return new ArrayList(0); + } + ArrayList result = new ArrayList(items.size()); + + for (JsonNode item : items) { + result.add(new AudioType.DetailsArtist(item)); + } + + return result; + } + } + + /** + * Retrieve all albums from specified artist or genre + */ + public static class GetAlbums extends ApiMethod> { + public final static String METHOD_NAME = "AudioLibrary.GetAlbums"; + + private final static String LIST_NODE = "albums"; + + /** + * Retrieve all albums from specified artist or genre + * + * @param properties Properties to retrieve. See {@link AudioType.FieldsAlbum} for a + * list of accepted values + */ + public GetAlbums(String... properties) { + super(); + addParameterToRequest("properties", properties); + } + + @Override + public String getMethodName() { + return METHOD_NAME; + } + + @Override + public List resultFromJson(ObjectNode jsonObject) + throws ApiException { + JsonNode resultNode = jsonObject.get(RESULT_NODE); + ArrayNode items = resultNode.has(LIST_NODE) ? + (ArrayNode)resultNode.get(LIST_NODE) : null; + if (items == null) { + return new ArrayList(0); + } + ArrayList result = new ArrayList(items.size()); + for (JsonNode item : items) { + result.add(new AudioType.DetailsAlbum(item)); + } + + return result; + } + } + + /** + * Retrieve all genres + */ + public static class GetGenres extends ApiMethod> { + public final static String METHOD_NAME = "AudioLibrary.GetGenres"; + + private final static String LIST_NODE = "genres"; + + /** + * Retrieve all genres + * + * @param properties Properties to retrieve. See {@link LibraryType.FieldsGenre} for a + * list of accepted values + */ + public GetGenres(String... properties) { + super(); + addParameterToRequest("properties", properties); + } + + @Override + public String getMethodName() { + return METHOD_NAME; + } + + @Override + public List resultFromJson(ObjectNode jsonObject) + throws ApiException { + JsonNode resultNode = jsonObject.get(RESULT_NODE); + ArrayNode items = resultNode.has(LIST_NODE) ? + (ArrayNode)resultNode.get(LIST_NODE) : null; + if (items == null) { + return new ArrayList(0); + } + ArrayList result = new ArrayList(items.size()); + for (JsonNode item : items) { + result.add(new LibraryType.DetailsGenre(item)); + } + + return result; + } + } + + /** + * Retrieve all songs from specified album, artist or genre + */ + public static class GetSongs extends ApiMethod> { + public final static String METHOD_NAME = "AudioLibrary.GetSongs"; + + private final static String LIST_NODE = "songs"; + + /** + * Retrieve all songs from specified album, artist or genre + * + * @param properties Properties to retrieve. See {@link AudioType.FieldsSong} for a + * list of accepted values + */ + public GetSongs(String... properties) { + super(); + addParameterToRequest("properties", properties); + } + + @Override + public String getMethodName() { + return METHOD_NAME; + } + + @Override + public List resultFromJson(ObjectNode jsonObject) + throws ApiException { + JsonNode resultNode = jsonObject.get(RESULT_NODE); + + ArrayNode items = resultNode.has(LIST_NODE) ? + (ArrayNode)resultNode.get(LIST_NODE) : null; + if (items == null) { + return new ArrayList(0); + } + ArrayList result = new ArrayList(items.size()); + for (JsonNode item : items) { + result.add(new AudioType.DetailsSong(item)); + } + + return result; + } + } + +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/Files.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/Files.java new file mode 100644 index 0000000..1f83fb1 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/Files.java @@ -0,0 +1,51 @@ +/* + * Copyright 2015 Synced Synapse. 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.syncedsynapse.kore2.jsonrpc.method; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.syncedsynapse.kore2.jsonrpc.ApiException; +import com.syncedsynapse.kore2.jsonrpc.ApiMethod; +import com.syncedsynapse.kore2.jsonrpc.type.FilesType; + +/** + * All JSON RPC methods in Files.* + */ +public class Files { + + /** + * Prepare Download + * Provides a way to download a given file (e.g. providing an URL to the real file location) + */ + public static final class PrepareDownload extends ApiMethod { + public final static String METHOD_NAME = "Files.PrepareDownload"; + + /** + * Provides a way to download a given file (e.g. providing an URL to the real file location) + */ + public PrepareDownload(String path) { + super(); + addParameterToRequest("path", path); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public FilesType.PrepareDownloadReturnType resultFromJson(ObjectNode jsonObject) throws ApiException { + return new FilesType.PrepareDownloadReturnType(jsonObject.get(RESULT_NODE)); + } + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/GUI.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/GUI.java new file mode 100644 index 0000000..fd21c83 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/GUI.java @@ -0,0 +1,184 @@ +/* + * Copyright 2015 Synced Synapse. 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.syncedsynapse.kore2.jsonrpc.method; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.syncedsynapse.kore2.jsonrpc.ApiException; +import com.syncedsynapse.kore2.jsonrpc.ApiMethod; + +/** + * All JSON RPC methods in GUI.* + */ +public class GUI { + + public static final class ActivateWindow extends ApiMethod { + public final static String METHOD_NAME = "GUI.ActivateWindow"; + + /** All windows that we can navigate to */ + public final static String HOME = "home"; + public final static String PROGRAMS = "programs"; + public final static String PICTURES = "pictures"; + public final static String FILEMANAGER = "filemanager"; + public final static String FILES = "files"; + public final static String SETTINGS = "settings"; + public final static String MUSIC = "music"; + public final static String VIDEO = "video"; + public final static String VIDEOS = "videos"; + public final static String TV = "tv"; + public final static String PVR = "pvr"; + public final static String PVRGUIDEINFO = "pvrguideinfo"; + public final static String PVRRECORDINGINFO = "pvrrecordinginfo"; + public final static String PVRTIMERSETTING = "pvrtimersetting"; + public final static String PVRGROUPMANAGER = "pvrgroupmanager"; + public final static String PVRCHANNELMANAGER = "pvrchannelmanager"; + public final static String PVRGUIDESEARCH = "pvrguidesearch"; + public final static String PVRCHANNELSCAN = "pvrchannelscan"; + public final static String PVRUPDATEPROGRESS = "pvrupdateprogress"; + public final static String PVROSDCHANNELS = "pvrosdchannels"; + public final static String PVROSDGUIDE = "pvrosdguide"; + public final static String PVROSDDIRECTOR = "pvrosddirector"; + public final static String PVROSDCUTTER = "pvrosdcutter"; + public final static String PVROSDTELETEXT = "pvrosdteletext"; + public final static String SYSTEMINFO = "systeminfo"; + public final static String TESTPATTERN = "testpattern"; + public final static String SCREENCALIBRATION = "screencalibration"; + public final static String GUICALIBRATION = "guicalibration"; + public final static String PICTURESSETTINGS = "picturessettings"; + public final static String PROGRAMSSETTINGS = "programssettings"; + public final static String WEATHERSETTINGS = "weathersettings"; + public final static String MUSICSETTINGS = "musicsettings"; + public final static String SYSTEMSETTINGS = "systemsettings"; + public final static String VIDEOSSETTINGS = "videossettings"; + public final static String NETWORKSETTINGS = "networksettings"; + public final static String SERVICESETTINGS = "servicesettings"; + public final static String APPEARANCESETTINGS = "appearancesettings"; + public final static String PVRSETTINGS = "pvrsettings"; + public final static String TVSETTINGS = "tvsettings"; + public final static String SCRIPTS = "scripts"; + public final static String VIDEOFILES = "videofiles"; + public final static String VIDEOLIBRARY = "videolibrary"; + public final static String VIDEOPLAYLIST = "videoplaylist"; + public final static String LOGINSCREEN = "loginscreen"; + public final static String PROFILES = "profiles"; + public final static String SKINSETTINGS = "skinsettings"; + public final static String ADDONBROWSER = "addonbrowser"; + public final static String YESNODIALOG = "yesnodialog"; + public final static String PROGRESSDIALOG = "progressdialog"; + public final static String VIRTUALKEYBOARD = "virtualkeyboard"; + public final static String VOLUMEBAR = "volumebar"; + public final static String SUBMENU = "submenu"; + public final static String FAVOURITES = "favourites"; + public final static String CONTEXTMENU = "contextmenu"; + public final static String INFODIALOG = "infodialog"; + public final static String NUMERICINPUT = "numericinput"; + public final static String GAMEPADINPUT = "gamepadinput"; + public final static String SHUTDOWNMENU = "shutdownmenu"; + public final static String MUTEBUG = "mutebug"; + public final static String PLAYERCONTROLS = "playercontrols"; + public final static String SEEKBAR = "seekbar"; + public final static String MUSICOSD = "musicosd"; + public final static String ADDONSETTINGS = "addonsettings"; + public final static String VISUALISATIONSETTINGS = "visualisationsettings"; + public final static String VISUALISATIONPRESETLIST = "visualisationpresetlist"; + public final static String OSDVIDEOSETTINGS = "osdvideosettings"; + public final static String OSDAUDIOSETTINGS = "osdaudiosettings"; + public final static String VIDEOBOOKMARKS = "videobookmarks"; + public final static String FILEBROWSER = "filebrowser"; + public final static String NETWORKSETUP = "networksetup"; + public final static String MEDIASOURCE = "mediasource"; + public final static String PROFILESETTINGS = "profilesettings"; + public final static String LOCKSETTINGS = "locksettings"; + public final static String CONTENTSETTINGS = "contentsettings"; + public final static String SONGINFORMATION = "songinformation"; + public final static String SMARTPLAYLISTEDITOR = "smartplaylisteditor"; + public final static String SMARTPLAYLISTRULE = "smartplaylistrule"; + public final static String BUSYDIALOG = "busydialog"; + public final static String PICTUREINFO = "pictureinfo"; + public final static String ACCESSPOINTS = "accesspoints"; + public final static String FULLSCREENINFO = "fullscreeninfo"; + public final static String KARAOKESELECTOR = "karaokeselector"; + public final static String KARAOKELARGESELECTOR = "karaokelargeselector"; + public final static String SLIDERDIALOG = "sliderdialog"; + public final static String ADDONINFORMATION = "addoninformation"; + public final static String MUSICPLAYLIST = "musicplaylist"; + public final static String MUSICFILES = "musicfiles"; + public final static String MUSICLIBRARY = "musiclibrary"; + public final static String MUSICPLAYLISTEDITOR = "musicplaylisteditor"; + public final static String TELETEXT = "teletext"; + public final static String SELECTDIALOG = "selectdialog"; + public final static String MUSICINFORMATION = "musicinformation"; + public final static String OKDIALOG = "okdialog"; + public final static String MOVIEINFORMATION = "movieinformation"; + public final static String TEXTVIEWER = "textviewer"; + public final static String FULLSCREENVIDEO = "fullscreenvideo"; + public final static String FULLSCREENLIVETV = "fullscreenlivetv"; + public final static String VISUALISATION = "visualisation"; + public final static String SLIDESHOW = "slideshow"; + public final static String FILESTACKINGDIALOG = "filestackingdialog"; + public final static String KARAOKE = "karaoke"; + public final static String WEATHER = "weather"; + public final static String SCREENSAVER = "screensaver"; + public final static String VIDEOOSD = "videoosd"; + public final static String VIDEOMENU = "videomenu"; + public final static String VIDEOTIMESEEK = "videotimeseek"; + public final static String MUSICOVERLAY = "musicoverlay"; + public final static String VIDEOOVERLAY = "videooverlay"; + public final static String STARTWINDOW = "startwindow"; + public final static String STARTUP = "startup"; + public final static String PERIPHERALS = "peripherals"; + public final static String PERIPHERALSETTINGS = "peripheralsettings"; + public final static String EXTENDEDPROGRESSDIALOG = "extendedprogressdialog"; + public final static String MEDIAFILTER = "mediafilter"; + + // Only on Gotham + public final static String SUBTITLESEARCH = "subtitlesearch"; + + /** + * For use in params, to go directly to Movies + */ + public final static String PARAM_MOVIE_TITLES = "MovieTitles"; + /** + * For use in params, to go directly to TV shows + */ + public final static String PARAM_TV_SHOWS_TITLES = "TvShowTitles"; + + /** + * Activates a window in XBMC. See class constants to check which windows are allowed. + */ + public ActivateWindow(String window) { + super(); + addParameterToRequest("window", window); + } + + /** + * Activates a window in XBMC. See class constants to check which windows are allowed. + */ + public ActivateWindow(String window, String... parameters) { + super(); + addParameterToRequest("window", window); + addParameterToRequest("parameters", parameters); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/Input.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/Input.java new file mode 100644 index 0000000..f441edb --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/Input.java @@ -0,0 +1,395 @@ +/* + * Copyright 2015 Synced Synapse. 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.syncedsynapse.kore2.jsonrpc.method; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.syncedsynapse.kore2.jsonrpc.ApiException; +import com.syncedsynapse.kore2.jsonrpc.ApiMethod; + +/** + * All JSON RPC methods in Input.* + */ +public class Input { + + /** + * Execute action + * Executes general actions on XBMC. See class constants for available actions. + */ + public static final class ExecuteAction extends ApiMethod { + public final static String METHOD_NAME = "Input.ExecuteAction"; + + /** Available actions */ + public final static String LEFT = "left"; + public final static String RIGHT = "right"; + public final static String UP = "up"; + public final static String DOWN = "down"; + public final static String PAGEUP = "pageup"; + public final static String PAGEDOWN = "pagedown"; + public final static String SELECT = "select"; + public final static String HIGHLIGHT = "highlight"; + public final static String PARENTDIR = "parentdir"; + public final static String PARENTFOLDER = "parentfolder"; + public final static String BACK = "back"; + public final static String PREVIOUSMENU = "previousmenu"; + public final static String INFO = "info"; + public final static String PAUSE = "pause"; + public final static String STOP = "stop"; + public final static String SKIPNEXT = "skipnext"; + public final static String SKIPPREVIOUS = "skipprevious"; + public final static String FULLSCREEN = "fullscreen"; + public final static String ASPECTRATIO = "aspectratio"; + public final static String STEPFORWARD = "stepforward"; + public final static String STEPBACK = "stepback"; + public final static String BIGSTEPFORWARD = "bigstepforward"; + public final static String BIGSTEPBACK = "bigstepback"; + public final static String OSD = "osd"; + public final static String SHOWSUBTITLES = "showsubtitles"; + public final static String NEXTSUBTITLE = "nextsubtitle"; + public final static String CODECINFO = "codecinfo"; + public final static String NEXTPICTURE = "nextpicture"; + public final static String PREVIOUSPICTURE = "previouspicture"; + public final static String ZOOMOUT = "zoomout"; + public final static String ZOOMIN = "zoomin"; + public final static String PLAYLIST = "playlist"; + public final static String QUEUE = "queue"; + public final static String ZOOMNORMAL = "zoomnormal"; + public final static String ZOOMLEVEL1 = "zoomlevel1"; + public final static String ZOOMLEVEL2 = "zoomlevel2"; + public final static String ZOOMLEVEL3 = "zoomlevel3"; + public final static String ZOOMLEVEL4 = "zoomlevel4"; + public final static String ZOOMLEVEL5 = "zoomlevel5"; + public final static String ZOOMLEVEL6 = "zoomlevel6"; + public final static String ZOOMLEVEL7 = "zoomlevel7"; + public final static String ZOOMLEVEL8 = "zoomlevel8"; + public final static String ZOOMLEVEL9 = "zoomlevel9"; + public final static String NEXTCALIBRATION = "nextcalibration"; + public final static String RESETCALIBRATION = "resetcalibration"; + public final static String ANALOGMOVE = "analogmove"; + public final static String ROTATE = "rotate"; + public final static String ROTATECCW = "rotateccw"; + public final static String CLOSE = "close"; + public final static String SUBTITLEDELAYMINUS = "subtitledelayminus"; + public final static String SUBTITLEDELAY = "subtitledelay"; + public final static String SUBTITLEDELAYPLUS = "subtitledelayplus"; + public final static String AUDIODELAYMINUS = "audiodelayminus"; + public final static String AUDIODELAY = "audiodelay"; + public final static String AUDIODELAYPLUS = "audiodelayplus"; + public final static String SUBTITLESHIFTUP = "subtitleshiftup"; + public final static String SUBTITLESHIFTDOWN = "subtitleshiftdown"; + public final static String SUBTITLEALIGN = "subtitlealign"; + public final static String AUDIONEXTLANGUAGE = "audionextlanguage"; + public final static String VERTICALSHIFTUP = "verticalshiftup"; + public final static String VERTICALSHIFTDOWN = "verticalshiftdown"; + public final static String NEXTRESOLUTION = "nextresolution"; + public final static String AUDIOTOGGLEDIGITAL = "audiotoggledigital"; + public final static String NUMBER0 = "number0"; + public final static String NUMBER1 = "number1"; + public final static String NUMBER2 = "number2"; + public final static String NUMBER3 = "number3"; + public final static String NUMBER4 = "number4"; + public final static String NUMBER5 = "number5"; + public final static String NUMBER6 = "number6"; + public final static String NUMBER7 = "number7"; + public final static String NUMBER8 = "number8"; + public final static String NUMBER9 = "number9"; + public final static String OSDLEFT = "osdleft"; + public final static String OSDRIGHT = "osdright"; + public final static String OSDUP = "osdup"; + public final static String OSDDOWN = "osddown"; + public final static String OSDSELECT = "osdselect"; + public final static String OSDVALUEPLUS = "osdvalueplus"; + public final static String OSDVALUEMINUS = "osdvalueminus"; + public final static String SMALLSTEPBACK = "smallstepback"; + public final static String FASTFORWARD = "fastforward"; + public final static String REWIND = "rewind"; + public final static String PLAY = "play"; + public final static String PLAYPAUSE = "playpause"; + public final static String DELETE = "delete"; + public final static String COPY = "copy"; + public final static String MOVE = "move"; + public final static String MPLAYEROSD = "mplayerosd"; + public final static String HIDESUBMENU = "hidesubmenu"; + public final static String SCREENSHOT = "screenshot"; + public final static String RENAME = "rename"; + public final static String TOGGLEWATCHED = "togglewatched"; + public final static String SCANITEM = "scanitem"; + public final static String RELOADKEYMAPS = "reloadkeymaps"; + public final static String VOLUMEUP = "volumeup"; + public final static String VOLUMEDOWN = "volumedown"; + public final static String MUTE = "mute"; + public final static String BACKSPACE = "backspace"; + public final static String SCROLLUP = "scrollup"; + public final static String SCROLLDOWN = "scrolldown"; + public final static String ANALOGFASTFORWARD = "analogfastforward"; + public final static String ANALOGREWIND = "analogrewind"; + public final static String MOVEITEMUP = "moveitemup"; + public final static String MOVEITEMDOWN = "moveitemdown"; + public final static String CONTEXTMENU = "contextmenu"; + public final static String SHIFT = "shift"; + public final static String SYMBOLS = "symbols"; + public final static String CURSORLEFT = "cursorleft"; + public final static String CURSORRIGHT = "cursorright"; + public final static String SHOWTIME = "showtime"; + public final static String ANALOGSEEKFORWARD = "analogseekforward"; + public final static String ANALOGSEEKBACK = "analogseekback"; + public final static String SHOWPRESET = "showpreset"; + public final static String PRESETLIST = "presetlist"; + public final static String NEXTPRESET = "nextpreset"; + public final static String PREVIOUSPRESET = "previouspreset"; + public final static String LOCKPRESET = "lockpreset"; + public final static String RANDOMPRESET = "randompreset"; + public final static String INCREASEVISRATING = "increasevisrating"; + public final static String DECREASEVISRATING = "decreasevisrating"; + public final static String SHOWVIDEOMENU = "showvideomenu"; + public final static String ENTER = "enter"; + public final static String INCREASERATING = "increaserating"; + public final static String DECREASERATING = "decreaserating"; + public final static String TOGGLEFULLSCREEN = "togglefullscreen"; + public final static String NEXTSCENE = "nextscene"; + public final static String PREVIOUSSCENE = "previousscene"; + public final static String NEXTLETTER = "nextletter"; + public final static String PREVLETTER = "prevletter"; + public final static String JUMPSMS2 = "jumpsms2"; + public final static String JUMPSMS3 = "jumpsms3"; + public final static String JUMPSMS4 = "jumpsms4"; + public final static String JUMPSMS5 = "jumpsms5"; + public final static String JUMPSMS6 = "jumpsms6"; + public final static String JUMPSMS7 = "jumpsms7"; + public final static String JUMPSMS8 = "jumpsms8"; + public final static String JUMPSMS9 = "jumpsms9"; + public final static String FILTER = "filter"; + public final static String FILTERCLEAR = "filterclear"; + public final static String FILTERSMS2 = "filtersms2"; + public final static String FILTERSMS3 = "filtersms3"; + public final static String FILTERSMS4 = "filtersms4"; + public final static String FILTERSMS5 = "filtersms5"; + public final static String FILTERSMS6 = "filtersms6"; + public final static String FILTERSMS7 = "filtersms7"; + public final static String FILTERSMS8 = "filtersms8"; + public final static String FILTERSMS9 = "filtersms9"; + public final static String FIRSTPAGE = "firstpage"; + public final static String LASTPAGE = "lastpage"; + public final static String GUIPROFILE = "guiprofile"; + public final static String RED = "red"; + public final static String GREEN = "green"; + public final static String YELLOW = "yellow"; + public final static String BLUE = "blue"; + public final static String INCREASEPAR = "increasepar"; + public final static String DECREASEPAR = "decreasepar"; + public final static String VOLAMPUP = "volampup"; + public final static String VOLAMPDOWN = "volampdown"; + public final static String CHANNELUP = "channelup"; + public final static String CHANNELDOWN = "channeldown"; + public final static String PREVIOUSCHANNELGROUP = "previouschannelgroup"; + public final static String NEXTCHANNELGROUP = "nextchannelgroup"; + public final static String LEFTCLICK = "leftclick"; + public final static String RIGHTCLICK = "rightclick"; + public final static String MIDDLECLICK = "middleclick"; + public final static String DOUBLECLICK = "doubleclick"; + public final static String WHEELUP = "wheelup"; + public final static String WHEELDOWN = "wheeldown"; + public final static String MOUSEDRAG = "mousedrag"; + public final static String MOUSEMOVE = "mousemove"; + public final static String NOOP = "noop"; + + /** + * Executes general actions on XBMC. See class constants for available actions. + */ + public ExecuteAction(String action) { + super(); + addParameterToRequest("action", action); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + + /** + * Goes to home window in GUI + */ + public static final class Home extends ApiMethod { + public final static String METHOD_NAME = "Input.Home"; + /** + * Goes to home window in GUI + */ + public Home() { + super(); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + + /** + * Navigate left in GUI + */ + public static final class Left extends ApiMethod { + public final static String METHOD_NAME = "Input.Left"; + /** + * Navigate left in GUI + */ + public Left() { + super(); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + + /** + * Navigate right in GUI + */ + public static final class Right extends ApiMethod { + public final static String METHOD_NAME = "Input.Right"; + /** + * Navigate right in GUI + */ + public Right() { + super(); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + + /** + * Navigate up in GUI + */ + public static final class Up extends ApiMethod { + public final static String METHOD_NAME = "Input.Up"; + /** + * Navigate up in GUI + */ + public Up() { + super(); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + + /** + * Navigate down in GUI + */ + public static final class Down extends ApiMethod { + public final static String METHOD_NAME = "Input.Down"; + /** + * Navigate down in GUI + */ + public Down() { + super(); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + + /** + * Navigate back in GUI + */ + public static final class Back extends ApiMethod { + public final static String METHOD_NAME = "Input.Back"; + /** + * Navigate down in GUI + */ + public Back() { + super(); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + + /** + * Select in GUI + */ + public static final class Select extends ApiMethod { + public final static String METHOD_NAME = "Input.Select"; + /** + * Select in GUI + */ + public Select() { + super(); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + + /** + * Send a generic (unicode) text + */ + public static final class SendText extends ApiMethod { + public final static String METHOD_NAME = "Input.SendText"; + /** + * Send a generic (unicode) text + */ + public SendText(String text, boolean done) { + super(); + addParameterToRequest("text", text); + addParameterToRequest("done", done); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/JSONRPC.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/JSONRPC.java new file mode 100644 index 0000000..d059c19 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/JSONRPC.java @@ -0,0 +1,45 @@ +/* + * Copyright 2015 Synced Synapse. 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.syncedsynapse.kore2.jsonrpc.method; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.syncedsynapse.kore2.jsonrpc.ApiException; +import com.syncedsynapse.kore2.jsonrpc.ApiMethod; + +public class JSONRPC { + + /** + * Ping responder + */ + public static final class Ping extends ApiMethod { + public final static String METHOD_NAME = "JSONRPC.Ping"; + + /** + * Ping responder + */ + public Ping() { + super(); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/Player.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/Player.java new file mode 100644 index 0000000..2511765 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/Player.java @@ -0,0 +1,469 @@ +/* + * Copyright 2015 Synced Synapse. 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.syncedsynapse.kore2.jsonrpc.method; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.syncedsynapse.kore2.jsonrpc.ApiException; +import com.syncedsynapse.kore2.jsonrpc.ApiMethod; +import com.syncedsynapse.kore2.jsonrpc.type.ListType; +import com.syncedsynapse.kore2.jsonrpc.type.PlayerType; +import com.syncedsynapse.kore2.jsonrpc.type.PlayerType.GetActivePlayersReturnType; +import com.syncedsynapse.kore2.jsonrpc.type.PlaylistType; +import com.syncedsynapse.kore2.utils.JsonUtils; + +import java.util.ArrayList; + +/** + * All JSON RPC methods in Playyer.* + */ +public class Player { + + /** + * Returns all active players. + */ + public static final class GetActivePlayers extends ApiMethod> { + public final static String METHOD_NAME = "Player.GetActivePlayers"; + + /** + * Returns all active players. + */ + public GetActivePlayers() { + super(); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public ArrayList resultFromJson(ObjectNode jsonObject) throws ApiException { + ArrayNode resultNode = (ArrayNode)jsonObject.get(RESULT_NODE); + ArrayList res = new ArrayList(); + if (resultNode != null) { + for (JsonNode node : resultNode) { + res.add(new GetActivePlayersReturnType(node)); + } + } + return res; + } + } + + /** + * Retrieves the currently played item + */ + public static final class GetItem extends ApiMethod { + public final static String METHOD_NAME = "Player.GetItem"; + + /** + * Retrieves the currently played item + * @param playerId Player id for which to retrieve the item + * @param properties Properties to retrieve. + * See {@link ListType.FieldsAll} for a list of accepted values + */ + public GetItem(int playerId, String... properties) { + super(); + addParameterToRequest("playerid", playerId); + addParameterToRequest("properties", properties); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public ListType.ItemsAll resultFromJson(ObjectNode jsonObject) throws ApiException { + return new ListType.ItemsAll(jsonObject.get(RESULT_NODE).get("item")); + } + } + + /** + * Retrieves the values of the given properties + */ + public static final class GetProperties extends ApiMethod { + public final static String METHOD_NAME = "Player.GetProperties"; + + /** + * Retrieves the values of the given properties + * @param playerId Player id for which to retrieve the item + * @param properties Properties to retrieve. + * See {@link PlayerType.PropertyName} constants for a list of accepted values + */ + public GetProperties(int playerId, String... properties) { + super(); + addParameterToRequest("playerid", playerId); + addParameterToRequest("properties", properties); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public PlayerType.PropertyValue resultFromJson(ObjectNode jsonObject) throws ApiException { + return new PlayerType.PropertyValue(jsonObject.get(RESULT_NODE)); + } + } + + + /** + * Pauses or unpause playback and returns the new state + */ + public static final class PlayPause extends ApiMethod { + public final static String METHOD_NAME = "Player.PlayPause"; + + /** + * Pauses or unpause playback and returns the new state + * @param playerId Player id for which to toggle the state + */ + public PlayPause(int playerId) { + super(); + addParameterToRequest("playerid", playerId); + addParameterToRequest("play", "toggle"); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public Integer resultFromJson(ObjectNode jsonObject) throws ApiException { + return JsonUtils.intFromJsonNode(jsonObject.get(RESULT_NODE), "speed"); + } + } + + /** + * Set the speed of the current playback + */ + public static final class SetSpeed extends ApiMethod { + public final static String METHOD_NAME = "Player.SetSpeed"; + + /** + * Set the speed of the current playback + * @param playerId Player id for which to toggle the state + * @param speed String enum in {@link com.syncedsynapse.kore2.jsonrpc.type.GlobalType.IncrementDecrement} + */ + public SetSpeed(int playerId, String speed) { + super(); + addParameterToRequest("playerid", playerId); + addParameterToRequest("speed", speed); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public Integer resultFromJson(ObjectNode jsonObject) throws ApiException { + return JsonUtils.intFromJsonNode(jsonObject.get(RESULT_NODE), "speed"); + } + } + + /** + * Stops playback + */ + public static final class Stop extends ApiMethod { + public final static String METHOD_NAME = "Player.Stop"; + + /** + * Stops playback + * @param playerId Player id for which to stop playback + */ + public Stop(int playerId) { + super(); + addParameterToRequest("playerid", playerId); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + + /** + * Go to previous/next/specific item in the playlist. + */ + public static final class GoTo extends ApiMethod { + public final static String METHOD_NAME = "Player.GoTo"; + + /** + * Go to constants + */ + public static final String PREVIOUS = "previous"; + public static final String NEXT = "next"; + + /** + * Go to previous/next/specific item in the playlist. + * @param playerId Player id for which to stop playback + * @param to Where to go. See this class constants for values + */ + public GoTo(int playerId, String to) { + super(); + addParameterToRequest("playerid", playerId); + addParameterToRequest("to", to); + } + + /** + * Go to previous/next/specific item in the playlist. + * @param playerId Player id for which to stop playback + * @param to position in playlist + */ + public GoTo(int playerId, int to) { + super(); + addParameterToRequest("playerid", playerId); + addParameterToRequest("to", to); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + + /** + * Seek through the playing item + */ + public static final class Seek extends ApiMethod { + public final static String METHOD_NAME = "Player.Seek"; + + /** + * Seek through the playing item (by time) + * @param playerId Player id for which to stop playback + * @param value Where to seek + */ + public Seek(int playerId, PlayerType.PositionTime value) { + super(); + addParameterToRequest("playerid", playerId); + addParameterToRequest("value", value); + } + + /** + * Seek through the playing item (by percentage) + * @param playerId Player id for which to stop playback + * @param value Percentage + */ + public Seek(int playerId, int value) { + super(); + addParameterToRequest("playerid", playerId); + addParameterToRequest("value", value); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public PlayerType.SeekReturnType resultFromJson(ObjectNode jsonObject) throws ApiException { + return new PlayerType.SeekReturnType(jsonObject.get(RESULT_NODE)); + } + } + + /** + * Set the subtitle displayed by the player + */ + public static final class SetSubtitle extends ApiMethod { + public final static String METHOD_NAME = "Player.SetSubtitle"; + + /** + * SetSubtitle constants + */ + public static final String PREVIOUS = "previous"; + public static final String NEXT = "next"; + public static final String OFF = "off"; + public static final String ON = "on"; + + /** + * Set the subtitle displayed by the player + * @param playerId Player id for which to stop playback + * @param subtitle One of the constanstants of this class + * @param enable Whether to enable subtitles to be displayed after setting the new subtitle + */ + public SetSubtitle(int playerId, String subtitle, boolean enable) { + super(); + addParameterToRequest("playerid", playerId); + addParameterToRequest("subtitle", subtitle); + addParameterToRequest("enable", enable); + } + + /** + * Set the subtitle displayed by the player + * @param playerId Player id for which to stop playback + * @param subtitle Index of the subtitle to display + * @param enable Whether to enable subtitles to be displayed after setting the new subtitle + */ + public SetSubtitle(int playerId, int subtitle, boolean enable) { + super(); + addParameterToRequest("playerid", playerId); + addParameterToRequest("subtitle", subtitle); + addParameterToRequest("enable", enable); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + + /** + * Set the audio stream played by the player + */ + public static final class SetAudioStream extends ApiMethod { + public final static String METHOD_NAME = "Player.SetAudioStream"; + + /** + * SetAudioStream constants + */ + public static final String PREVIOUS = "previous"; + public static final String NEXT = "next"; + + /** + * Set the audio stream played by the player + * @param playerId Player id for which to stop playback + * @param stream One of the constanstants of this class + */ + public SetAudioStream(int playerId, String stream) { + super(); + addParameterToRequest("playerid", playerId); + addParameterToRequest("stream", stream); + } + + /** + * Set the audio stream played by the player + * @param playerId Player id for which to stop playback + * @param stream Index of the audio stream to play + */ + public SetAudioStream(int playerId, int stream) { + super(); + addParameterToRequest("playerid", playerId); + addParameterToRequest("stream", stream); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + + /** + * Set the repeat mode of the player + */ + public static final class SetRepeat extends ApiMethod { + public final static String METHOD_NAME = "Player.SetRepeat"; + + /** + * Set the repeat mode of the player + * @param playerId Player id for which to stop playback + * @param repeat Repeat mode, see {@link com.syncedsynapse.kore2.jsonrpc.type.PlayerType.Repeat} + */ + public SetRepeat(int playerId, String repeat) { + super(); + addParameterToRequest("playerid", playerId); + addParameterToRequest("repeat", repeat); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + + /** + * Shuffle/Unshuffle items in the player + */ + public static final class SetShuffle extends ApiMethod { + public final static String METHOD_NAME = "Player.SetShuffle"; + + /** + * Shuffle/Unshuffle items in the player + * @param playerId Player id for which to shuffle + * @param shuffle True/false + */ + public SetShuffle(int playerId, boolean shuffle) { + super(); + addParameterToRequest("playerid", playerId); + addParameterToRequest("shuffle", shuffle); + } + + /** + * Shuffle/Unshuffle items in the player + * @param playerId Player id for which to shuffle + */ + public SetShuffle(int playerId) { + super(); + addParameterToRequest("playerid", playerId); + addParameterToRequest("shuffle", "toggle"); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + + /** + * Start playback of either the playlist with the given ID, a slideshow with the pictures + * from the given directory or a single file or an item from the database. + */ + public static final class Open extends ApiMethod { + public final static String METHOD_NAME = "Player.Open"; + + /** + * Start playback of either the playlist with the given ID, a slideshow with the pictures + * from the given directory or a single file or an item from the database. + * @param playlistId + * @param position + */ + public Open(int playlistId, int position) { + super(); + final ObjectNode item = objectMapper.createObjectNode(); + item.put("playlistid", playlistId); + item.put("position", position); + addParameterToRequest("item", item); + } + + /** + * Start playback of either the playlist with the given ID, a slideshow with the pictures + * from the given directory or a single file or an item from the database. + * @param playlistItem Item to play + */ + public Open(PlaylistType.Item playlistItem) { + super(); + addParameterToRequest("item", playlistItem.toJsonNode()); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/Playlist.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/Playlist.java new file mode 100644 index 0000000..c4542f0 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/Playlist.java @@ -0,0 +1,173 @@ +/* + * Copyright 2015 Synced Synapse. 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.syncedsynapse.kore2.jsonrpc.method; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.syncedsynapse.kore2.jsonrpc.ApiException; +import com.syncedsynapse.kore2.jsonrpc.ApiMethod; +import com.syncedsynapse.kore2.jsonrpc.type.ListType; +import com.syncedsynapse.kore2.jsonrpc.type.PlaylistType; +import com.syncedsynapse.kore2.jsonrpc.type.PlaylistType.GetPlaylistsReturnType; + +import java.util.ArrayList; +import java.util.List; + +/** + * JSON RPC methods in Playlist.* + */ +public class Playlist { + + /** + * Returns all existing playlists + */ + public static final class GetPlaylists extends ApiMethod> { + public final static String METHOD_NAME = "Playlist.GetPlaylists"; + + /** + * Returns all existing playlists + */ + public GetPlaylists() { + super(); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public ArrayList resultFromJson(ObjectNode jsonObject) throws ApiException { + ArrayNode resultNode = (ArrayNode)jsonObject.get(RESULT_NODE); + ArrayList res = new ArrayList(); + if (resultNode != null) { + for (JsonNode node : resultNode) { + res.add(new GetPlaylistsReturnType(node)); + } + } + return res; + } + } + + /** + * Get all items from playlist + */ + public static final class GetItems extends ApiMethod> { + public final static String METHOD_NAME = "Playlist.GetItems"; + + /** + * Get all items from playlist + * @param playlistId Playlist id for which to get the items + * @param properties Properties to retrieve. + * See {@link ListType.FieldsAll} for a list of accepted values + */ + public GetItems(int playlistId, String... properties) { + super(); + addParameterToRequest("playlistid", playlistId); + addParameterToRequest("properties", properties); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public List resultFromJson(ObjectNode jsonObject) throws ApiException { + JsonNode resultNode = jsonObject.get(RESULT_NODE); + if (!resultNode.has("items") || (!resultNode.get("items").isArray()) || + ((resultNode.get("items")).size() == 0)) { + return new ArrayList(0); + } + ArrayNode items = (ArrayNode)resultNode.get("items"); + ArrayList result = new ArrayList(items.size()); + + for (JsonNode item : items) { + result.add(new ListType.ItemsAll(item)); + } + + return result; + } + } + + /** + * Clear playlist + */ + public static final class Clear extends ApiMethod { + public final static String METHOD_NAME = "Playlist.Clear"; + + /** + * Clear playlist + */ + public Clear(int playlistId) { + super(); + addParameterToRequest("playlistid", playlistId); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + + /** + * Remove item from playlist. Does not work for picture playlists (aka slideshows). + */ + public static final class Remove extends ApiMethod { + public final static String METHOD_NAME = "Playlist.Remove"; + + /** + * Remove item from playlist. Does not work for picture playlists (aka slideshows). + */ + public Remove(int playlistId, int position) { + super(); + addParameterToRequest("playlistid", playlistId); + addParameterToRequest("position", position); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + + /** + * Add item(s) to playlist + */ + public static final class Add extends ApiMethod { + public final static String METHOD_NAME = "Playlist.Add"; + + /** + * Add item(s) to playlist + */ + public Add(int playlistId, PlaylistType.Item item) { + super(); + addParameterToRequest("playlistid", playlistId); + addParameterToRequest("item", item); + } + + @Override + public String getMethodName() { return METHOD_NAME; } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/System.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/System.java new file mode 100644 index 0000000..ce162ab --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/System.java @@ -0,0 +1,75 @@ +/* + * Copyright 2015 Synced Synapse. 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.syncedsynapse.kore2.jsonrpc.method; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.syncedsynapse.kore2.jsonrpc.ApiException; +import com.syncedsynapse.kore2.jsonrpc.ApiMethod; + +/** + * All JSON RPC methods in System.* + */ +public class System { + + /** + * Shuts the system running XBMC down + */ + public static final class Shutdown extends ApiMethod { + public final static String METHOD_NAME = "System.Shutdown"; + + /** + * Shuts the system running XBMC down + */ + public Shutdown() { + super(); + } + + @Override + public String getMethodName() { + return METHOD_NAME; + } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + + /** + * Suspends the system running XBMC + */ + public static final class Suspend extends ApiMethod { + public final static String METHOD_NAME = "System.Suspend"; + + /** + * Suspends the system running XBMC + */ + public Suspend() { + super(); + } + + @Override + public String getMethodName() { + return METHOD_NAME; + } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/VideoLibrary.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/VideoLibrary.java new file mode 100644 index 0000000..4b975d1 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/method/VideoLibrary.java @@ -0,0 +1,439 @@ +/* + * Copyright 2015 Synced Synapse. 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.syncedsynapse.kore2.jsonrpc.method; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.syncedsynapse.kore2.jsonrpc.ApiException; +import com.syncedsynapse.kore2.jsonrpc.ApiMethod; +import com.syncedsynapse.kore2.jsonrpc.type.VideoType; + +import java.util.ArrayList; +import java.util.List; + +/** + * JSON RPC methods in VideoLibrary.* + */ +public class VideoLibrary { + + /** + * Cleans the video library from non-existent items. + */ + public static class Clean extends ApiMethod { + public final static String METHOD_NAME = "VideoLibrary.Clean"; + + /** + * Cleans the video library from non-existent items. + */ + public Clean() { + super(); + } + + @Override + public String getMethodName() { + return METHOD_NAME; + } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + + /** + * Scans the video sources for new library items. + */ + public static class Scan extends ApiMethod { + public final static String METHOD_NAME = "VideoLibrary.Scan"; + + /** + * Scans the video sources for new library items. + */ + public Scan() { + super(); + } + + @Override + public String getMethodName() { + return METHOD_NAME; + } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + + /** + * Retrieve all movies + */ + public static class GetMovies extends ApiMethod> { + public final static String METHOD_NAME = "VideoLibrary.GetMovies"; + + private final static String LIST_NODE = "movies"; + + /** + * Retrieve all movies + * + * @param properties Properties to retrieve. See {@link VideoType.FieldsMovie} for a list of + * accepted values + */ + public GetMovies(String... properties) { + super(); + addParameterToRequest("properties", properties); + } + + @Override + public String getMethodName() { + return METHOD_NAME; + } + + @Override + public List resultFromJson(ObjectNode jsonObject) + throws ApiException { + JsonNode resultNode = jsonObject.get(RESULT_NODE); + ArrayNode items = resultNode.has(LIST_NODE) ? + (ArrayNode)resultNode.get(LIST_NODE) : null; + if (items == null) { + return new ArrayList(0); + } + ArrayList result = new ArrayList(items.size()); + + for (JsonNode item : items) { + result.add(new VideoType.DetailsMovie(item)); + } + + return result; + } + } + + /** + * Retrieve details about a specific movie + */ + public static class GetMovieDetails extends ApiMethod { + public final static String METHOD_NAME = "VideoLibrary.GetMovieDetails"; + + /** + * Retrieve details about a specific movie + * + * @param movieId Movie id + * @param properties Properties to retrieve. See {@link VideoType.FieldsMovie} for a list of + * accepted values + */ + public GetMovieDetails(int movieId, String... properties) { + super(); + addParameterToRequest("movieid", movieId); + addParameterToRequest("properties", properties); + } + + @Override + public String getMethodName() { + return METHOD_NAME; + } + + @Override + public VideoType.DetailsMovie resultFromJson(ObjectNode jsonObject) + throws ApiException { + return new VideoType.DetailsMovie(jsonObject.get(RESULT_NODE).get("moviedetails")); + } + } + + /** + * Update the given movie with the given details + * Just the parameters we can change in the gui + */ + public static class SetMovieDetails extends ApiMethod { + public final static String METHOD_NAME = "VideoLibrary.SetMovieDetails"; + + /** + * Update the given movie with the given details + * + * @param movieid Movie id + */ + public SetMovieDetails(int movieid, Integer playcount, Double rating) { + super(); + addParameterToRequest("movieid", movieid); + if (playcount != null) addParameterToRequest("playcount", playcount); + if (rating != null) addParameterToRequest("rating", rating); + } + + @Override + public String getMethodName() { + return METHOD_NAME; + } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + + /** + * Retrieve all TV Shows + */ + public static class GetTVShows extends ApiMethod> { + public final static String METHOD_NAME = "VideoLibrary.GetTVShows"; + + private final static String LIST_NODE = "tvshows"; + + /** + * Retrieve all tv shows + * + * @param properties Properties to retrieve. See {@link VideoType.FieldsTVShow} for a + * list of accepted values + */ + public GetTVShows(String... properties) { + super(); + addParameterToRequest("properties", properties); + } + + @Override + public String getMethodName() { + return METHOD_NAME; + } + + @Override + public List resultFromJson(ObjectNode jsonObject) + throws ApiException { + JsonNode resultNode = jsonObject.get(RESULT_NODE); + ArrayNode items = resultNode.has(LIST_NODE) ? + (ArrayNode)resultNode.get(LIST_NODE) : null; + if (items == null) { + return new ArrayList(0); + } + ArrayList result = new ArrayList(items.size()); + + for (JsonNode item : items) { + result.add(new VideoType.DetailsTVShow(item)); + } + + return result; + } + } + + /** + * Retrieve details about a specific tv show + */ + public static class GetTVShowDetails extends ApiMethod { + public final static String METHOD_NAME = "VideoLibrary.GetTVShowDetails"; + + /** + * Retrieve details about a specific tv show + * + * @param tvshowId Show id + * @param properties Properties to retrieve. See {@link VideoType.FieldsTVShow} for a + * list of accepted values + */ + public GetTVShowDetails(int tvshowId, String... properties) { + super(); + addParameterToRequest("tvshowid", tvshowId); + addParameterToRequest("properties", properties); + } + + @Override + public String getMethodName() { + return METHOD_NAME; + } + + @Override + public VideoType.DetailsTVShow resultFromJson(ObjectNode jsonObject) + throws ApiException { + return new VideoType.DetailsTVShow(jsonObject.get(RESULT_NODE).get("tvshowdetails")); + } + } + + /** + * Update the given episode with the given details + * Just the parameters we can change in the gui + */ + public static class SetEpisodeDetails extends ApiMethod { + public final static String METHOD_NAME = "VideoLibrary.SetEpisodeDetails"; + + /** + * Update the given episode with the given details + * + * @param episodeid Episode id + */ + public SetEpisodeDetails(int episodeid, Integer playcount, Double rating) { + super(); + addParameterToRequest("episodeid", episodeid); + if (playcount != null) addParameterToRequest("playcount", playcount); + if (rating != null) addParameterToRequest("rating", rating); + } + + @Override + public String getMethodName() { + return METHOD_NAME; + } + + @Override + public String resultFromJson(ObjectNode jsonObject) throws ApiException { + return jsonObject.get(RESULT_NODE).textValue(); + } + } + + /** + * Retrieve all tv seasons + */ + public static class GetSeasons extends ApiMethod> { + public final static String METHOD_NAME = "VideoLibrary.GetSeasons"; + + private final static String LIST_NODE = "seasons"; + + /** + * Retrieve all tv seasons + * + * @param tvshowid TV Show id + * @param properties Properties to retrieve. See {@link VideoType.FieldsSeason} for a + * list of accepted values + */ + public GetSeasons(int tvshowid, String... properties) { + super(); + addParameterToRequest("tvshowid", tvshowid); + addParameterToRequest("properties", properties); + } + + @Override + public String getMethodName() { + return METHOD_NAME; + } + + @Override + public List resultFromJson(ObjectNode jsonObject) + throws ApiException { + JsonNode resultNode = jsonObject.get(RESULT_NODE); + ArrayNode items = resultNode.has(LIST_NODE) ? + (ArrayNode)resultNode.get(LIST_NODE) : null; + if (items == null) { + return new ArrayList(0); + } + ArrayList result = new ArrayList(items + .size()); + + for (JsonNode item : items) { + result.add(new VideoType.DetailsSeason(item)); + } + + return result; + } + } + + /** + * Retrieve all tv show episodes + */ + public static class GetEpisodes extends ApiMethod> { + public final static String METHOD_NAME = "VideoLibrary.GetEpisodes"; + + private final static String LIST_NODE = "episodes"; + + /** + * Retrieve all tv show episodes + * + * @param tvshowid TV Show id + * @param properties Properties to retrieve. See {@link VideoType.FieldsEpisode} for a + * list of accepted values + */ + public GetEpisodes(int tvshowid, String... properties) { + super(); + addParameterToRequest("tvshowid", tvshowid); + addParameterToRequest("properties", properties); + } + + /** + * Retrieve all tv show episodes + * + * @param tvshowid TV Show id + * @param season Season + * @param properties Properties to retrieve. See {@link VideoType.FieldsEpisode} for a + * list of accepted values + */ + public GetEpisodes(int tvshowid, int season, String... properties) { + super(); + addParameterToRequest("tvshowid", tvshowid); + addParameterToRequest("season", season); + addParameterToRequest("properties", properties); + } + + @Override + public String getMethodName() { + return METHOD_NAME; + } + + @Override + public List resultFromJson(ObjectNode jsonObject) + throws ApiException { + JsonNode resultNode = jsonObject.get(RESULT_NODE); + ArrayNode items = resultNode.has(LIST_NODE) ? + (ArrayNode)resultNode.get(LIST_NODE) : null; + if (items == null) { + return new ArrayList(0); + } + ArrayList result = new ArrayList(items + .size()); + + for (JsonNode item : items) { + result.add(new VideoType.DetailsEpisode(item)); + } + + return result; + } + } + + /** + * Retrieve all music videos + */ + public static class GetMusicVideos extends ApiMethod> { + public final static String METHOD_NAME = "VideoLibrary.GetMusicVideos"; + + private final static String LIST_NODE = "musicvideos"; + + /** + * Retrieve all music videos + * + * @param properties Properties to retrieve. See {@link VideoType.FieldsMusicVideo} for a + * list of accepted values + */ + public GetMusicVideos(String... properties) { + super(); + addParameterToRequest("properties", properties); + } + + @Override + public String getMethodName() { + return METHOD_NAME; + } + + @Override + public List resultFromJson(ObjectNode jsonObject) + throws ApiException { + JsonNode resultNode = jsonObject.get(RESULT_NODE); + ArrayNode items = resultNode.has(LIST_NODE) ? + (ArrayNode)resultNode.get(LIST_NODE) : null; + if (items == null) { + return new ArrayList(0); + } + ArrayList result = + new ArrayList(items.size()); + + for (JsonNode item : items) { + result.add(new VideoType.DetailsMusicVideo(item)); + } + + return result; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/notification/Input.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/notification/Input.java new file mode 100644 index 0000000..bcf3c48 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/notification/Input.java @@ -0,0 +1,50 @@ +/* + * Copyright 2015 Synced Synapse. 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.syncedsynapse.kore2.jsonrpc.notification; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.syncedsynapse.kore2.jsonrpc.ApiNotification; +import com.syncedsynapse.kore2.utils.JsonUtils; + +/** + * Input.* notifications + */ +public class Input { + + /** + * Input.OnInputRequested + * The user is requested to provide some information + */ + public static class OnInputRequested extends ApiNotification { + public static final String NOTIFICATION_NAME = "Input.OnInputRequested"; + + public static final String DATA_NODE = "data"; + + public final String title; + public final String type; + public final String value; + + public OnInputRequested(ObjectNode node) { + super(node); + ObjectNode dataNode = (ObjectNode)node.get(DATA_NODE); + title = JsonUtils.stringFromJsonNode(dataNode, "title"); + type = JsonUtils.stringFromJsonNode(dataNode, "type"); + value = JsonUtils.stringFromJsonNode(dataNode, "value"); + } + + public String getNotificationName() { return NOTIFICATION_NAME; } + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/notification/Player.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/notification/Player.java new file mode 100644 index 0000000..520ffbd --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/notification/Player.java @@ -0,0 +1,194 @@ +/* + * Copyright 2015 Synced Synapse. 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.syncedsynapse.kore2.jsonrpc.notification; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.syncedsynapse.kore2.jsonrpc.ApiNotification; +import com.syncedsynapse.kore2.jsonrpc.type.GlobalType; +import com.syncedsynapse.kore2.utils.JsonUtils; + +/** + * All Player.* notifications + */ +public class Player { + + /** + * Player.OnPause notification + * Playback of a media item has been paused. If there is no ID available extra information will be provided. + */ + public static class OnPause extends ApiNotification { + public static final String NOTIFICATION_NAME = "Player.OnPause"; + + public final NotificationsData data; + + public OnPause(ObjectNode node) { + super(node); + data = new NotificationsData(node.get(NotificationsData.DATA_NODE)); + } + + public String getNotificationName() { return NOTIFICATION_NAME; } + } + + /** + * Player.OnPlay notification + * Playback of a media item has been started or the playback speed has changed. If there is no + * ID available extra information will be provided. + */ + public static class OnPlay extends ApiNotification { + public static final String NOTIFICATION_NAME = "Player.OnPlay"; + + public final NotificationsData data; + + public OnPlay(ObjectNode node) { + super(node); + data = new NotificationsData(node.get(NotificationsData.DATA_NODE)); + } + + public String getNotificationName() { return NOTIFICATION_NAME; } + } + + /** + * Player.OnSeek notification + * The playback position has been changed. If there is no ID available extra information will + * be provided. + */ + public static class OnSeek extends ApiNotification { + public static final String NOTIFICATION_NAME = "Player.OnSeek"; + + public final NotificationsItem item; + public final GlobalType.Time time; + public final GlobalType.Time seekoffset; + + public OnSeek(ObjectNode node) { + super(node); + ObjectNode dataNode = (ObjectNode)node.get("data"); + item = new NotificationsItem(dataNode.get(NotificationsItem.ITEM_NODE)); + ObjectNode playerNode = (ObjectNode)dataNode.get("player"); + time = new GlobalType.Time(playerNode.get("time")); + seekoffset = new GlobalType.Time(playerNode.get("seekoffset")); + } + + public String getNotificationName() { return NOTIFICATION_NAME; } + } + + /** + * Player.OnSpeedChanged notification + * Speed of the playback of a media item has been changed. If there is no ID available extra information will be provided. + * be provided. + */ + public static class OnSpeedChanged extends ApiNotification { + public static final String NOTIFICATION_NAME = "Player.OnSpeedChanged"; + + public final NotificationsData data; + + public OnSpeedChanged(ObjectNode node) { + super(node); + data = new NotificationsData(node.get(NotificationsData.DATA_NODE)); + } + + public String getNotificationName() { return NOTIFICATION_NAME; } + } + + /** + * Player.OnStop notification + * Playback of a media item has been stopped. If there is no ID available extra information will be provided. + */ + public static class OnStop extends ApiNotification { + public static final String NOTIFICATION_NAME = "Player.OnStop"; + + public final boolean end; + public final NotificationsItem item; + + public OnStop(ObjectNode node) { + super(node); + ObjectNode dataNode = (ObjectNode)node.get("data"); + end = JsonUtils.booleanFromJsonNode(dataNode, "end"); + item = new NotificationsItem(dataNode.get(NotificationsItem.ITEM_NODE)); + } + + public String getNotificationName() { return NOTIFICATION_NAME; } + } + + /** + * Notification data for Player + */ + public static class NotificationsPlayer { + public static final String PLAYER_NODE = "player"; + + public final int playerId; + public final int speed; + + public NotificationsPlayer(JsonNode node) { + playerId = JsonUtils.intFromJsonNode(node, "playerid"); + speed = JsonUtils.intFromJsonNode(node, "speed", 0); + } + } + + /** + * General notification data + */ + public static class NotificationsItem { + public static final String ITEM_NODE = "item"; + /** + * The item types + */ + public static final String TYPE_UNKNOWN = "unknown", + TYPE_MOVIE = "movie", + TYPE_EPISODE = "episode", + TYPE_MUSIC_VIDEO = "musicvideo", + TYPE_SONG = "song", + TYPE_PICTURE = "picture", + TYPE_CHANNEL = "channel"; + + public final String type; + public final int id; + public final String title; + public final int year; + public final int episode; + public final int season; + public final String showtitle; + public final String album; + public final String artist; + public final int track; + + public NotificationsItem(JsonNode node) { + type = JsonUtils.stringFromJsonNode(node, "type", TYPE_UNKNOWN); + id = JsonUtils.intFromJsonNode(node, "speed"); + title = JsonUtils.stringFromJsonNode(node, "title"); + year = JsonUtils.intFromJsonNode(node, "year", 0); + episode = JsonUtils.intFromJsonNode(node, "episode", 0); + season = JsonUtils.intFromJsonNode(node, "season", 0); + showtitle = JsonUtils.stringFromJsonNode(node, "showtitle"); + album = JsonUtils.stringFromJsonNode(node, "album"); + artist = JsonUtils.stringFromJsonNode(node, "artist"); + track = JsonUtils.intFromJsonNode(node, "track", 0); + } + } + + public static class NotificationsData { + public static final String DATA_NODE = "data"; + + public final NotificationsPlayer player; + public final NotificationsItem item; + + public NotificationsData(JsonNode node) { + item = new NotificationsItem((ObjectNode)node.get(NotificationsItem.ITEM_NODE)); + player = new NotificationsPlayer((ObjectNode)node.get(NotificationsPlayer.PLAYER_NODE)); + } + } + +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/notification/System.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/notification/System.java new file mode 100644 index 0000000..18c3de6 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/notification/System.java @@ -0,0 +1,67 @@ +/* + * Copyright 2015 Synced Synapse. 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.syncedsynapse.kore2.jsonrpc.notification; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.syncedsynapse.kore2.jsonrpc.ApiNotification; + +/** + * System.* notifications + */ +public class System { + + /** + * System.OnQuit notification + * XBMC will be closed + */ + public static class OnQuit extends ApiNotification { + public static final String NOTIFICATION_NAME = "System.OnQuit"; + + public OnQuit(ObjectNode node) { + super(node); + } + + public String getNotificationName() { return NOTIFICATION_NAME; } + } + + /** + * System.OnRestart notification + * The system will be restarted. + */ + public static class OnRestart extends ApiNotification { + public static final String NOTIFICATION_NAME = "System.OnRestart"; + + public OnRestart(ObjectNode node) { + super(node); + } + + public String getNotificationName() { return NOTIFICATION_NAME; } + } + + /** + * System.OnSleep notification + * The system will be suspended. + */ + public static class OnSleep extends ApiNotification { + public static final String NOTIFICATION_NAME = "System.OnSleep"; + + public OnSleep(ObjectNode node) { + super(node); + } + + public String getNotificationName() { return NOTIFICATION_NAME; } + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/AddonType.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/AddonType.java new file mode 100644 index 0000000..476e5cc --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/AddonType.java @@ -0,0 +1,143 @@ +/* + * Copyright 2015 Synced Synapse. 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.syncedsynapse.kore2.jsonrpc.type; + +import com.fasterxml.jackson.databind.JsonNode; +import com.syncedsynapse.kore2.utils.JsonUtils; + +/** + * Types in Addon.* + */ +public class AddonType { + /** + * Enums for Addon.Fields + */ + public interface Fields { + public final String NAME = "name"; + public final String VERSION = "version"; + public final String SUMMARY = "summary"; + public final String DESCRIPTION = "description"; + public final String PATH = "path"; + public final String AUTHOR = "author"; + public final String THUMBNAIL = "thumbnail"; + public final String DISCLAIMER = "disclaimer"; + public final String FANART = "fanart"; + public final String DEPENDENCIES = "dependencies"; + public final String BROKEN = "broken"; + public final String EXTRAINFO = "extrainfo"; + public final String RATING = "rating"; + public final String ENABLED = "enabled"; + + public final static String[] allValues = new String[] { + NAME, VERSION, SUMMARY, DESCRIPTION, PATH, AUTHOR, THUMBNAIL, DISCLAIMER, + FANART, DEPENDENCIES, BROKEN, EXTRAINFO, RATING, ENABLED + }; + } + + /** + * Enums for Addon.Types + */ + public interface Types { + public final String UNKNOWN = "unknown"; + public final String XBMC_METADATA_SCRAPER_ALBUMS = "xbmc.metadata.scraper.albums"; + public final String XBMC_METADATA_SCRAPER_ARTISTS = "xbmc.metadata.scraper.artists"; + public final String XBMC_METADATA_SCRAPER_MOVIES = "xbmc.metadata.scraper.movies"; + public final String XBMC_METADATA_SCRAPER_MUSICVIDEOS = "xbmc.metadata.scraper.musicvideos"; + public final String XBMC_METADATA_SCRAPER_TVSHOWS = "xbmc.metadata.scraper.tvshows"; + public final String XBMC_UI_SCREENSAVER = "xbmc.ui.screensaver"; + public final String XBMC_PLAYER_MUSICVIZ = "xbmc.player.musicviz"; + public final String XBMC_PYTHON_PLUGINSOURCE = "xbmc.python.pluginsource"; + public final String XBMC_PYTHON_SCRIPT = "xbmc.python.script"; + public final String XBMC_PYTHON_WEATHER = "xbmc.python.weather"; + public final String XBMC_PYTHON_SUBTITLES = "xbmc.python.subtitles"; + public final String XBMC_PYTHON_LYRICS = "xbmc.python.lyrics"; + public final String XBMC_GUI_SKIN = "xbmc.gui.skin"; + public final String XBMC_GUI_WEBINTERFACE = "xbmc.gui.webinterface"; + public final String XBMC_PVRCLIENT = "xbmc.pvrclient"; + public final String XBMC_ADDON_VIDEO = "xbmc.addon.video"; + public final String XBMC_ADDON_AUDIO = "xbmc.addon.audio"; + public final String XBMC_ADDON_IMAGE = "xbmc.addon.image"; + public final String XBMC_ADDON_EXECUTABLE = "xbmc.addon.executable"; + public final String XBMC_SERVICE = "xbmc.service"; + + public final static String[] allValues = new String[]{ + UNKNOWN, XBMC_METADATA_SCRAPER_ALBUMS, XBMC_METADATA_SCRAPER_ARTISTS, + XBMC_METADATA_SCRAPER_MOVIES, XBMC_METADATA_SCRAPER_MUSICVIDEOS, + XBMC_METADATA_SCRAPER_TVSHOWS, XBMC_UI_SCREENSAVER, XBMC_PLAYER_MUSICVIZ, + XBMC_PYTHON_PLUGINSOURCE, XBMC_PYTHON_SCRIPT, XBMC_PYTHON_WEATHER, + XBMC_PYTHON_SUBTITLES, XBMC_PYTHON_LYRICS, XBMC_GUI_SKIN, XBMC_GUI_WEBINTERFACE, + XBMC_PVRCLIENT, XBMC_ADDON_VIDEO, XBMC_ADDON_AUDIO, XBMC_ADDON_IMAGE, + XBMC_ADDON_EXECUTABLE, XBMC_SERVICE + }; + } + + public static class Details extends ItemType.DetailsBase { + public static final String ADDONID = "addonid"; + public static final String AUTHOR = "author"; + public static final String BROKEN = "broken"; +// public static final String DEPENDENCIES = "dependencies"; + public static final String DESCRIPTION = "description"; + public static final String DISCLAIMER = "disclaimer"; + public static final String ENABLED = "enabled"; +// public static final String EXTRAINFO = "extrainfo"; + public static final String FANART = "fanart"; + public static final String NAME = "name"; + public static final String PATH = "path"; + public static final String RATING = "rating"; + public static final String SUMMARY = "summary"; + public static final String THUMBNAIL = "thumbnail"; + public static final String TYPE = "type"; + public static final String VERSION = "version"; + + public final String addonid; + public final String author; + public final boolean broken; + public final String description; + public final String disclaimer; + public final Boolean enabled; + public final String fanart; + public final String name; + public final String path; + public final int rating; + public final String summary; + public final String thumbnail; + public final String type; + public final String version; + + /** + * Constructor + * @param node JSON object representing a Detail object + */ + public Details(JsonNode node) { + super(node); + addonid = JsonUtils.stringFromJsonNode(node, ADDONID); + author = JsonUtils.stringFromJsonNode(node, AUTHOR); + broken = JsonUtils.booleanFromJsonNode(node, BROKEN, false); + description = JsonUtils.stringFromJsonNode(node, DESCRIPTION); + disclaimer = JsonUtils.stringFromJsonNode(node, DISCLAIMER); + enabled = JsonUtils.booleanFromJsonNode(node, ENABLED, false); + fanart = JsonUtils.stringFromJsonNode(node, FANART); + name = JsonUtils.stringFromJsonNode(node, NAME); + path = JsonUtils.stringFromJsonNode(node, PATH); + rating = JsonUtils.intFromJsonNode(node, RATING, 0); + summary = JsonUtils.stringFromJsonNode(node, SUMMARY); + thumbnail = JsonUtils.stringFromJsonNode(node, THUMBNAIL); + type = JsonUtils.stringFromJsonNode(node, TYPE); + version = JsonUtils.stringFromJsonNode(node, VERSION); + } + + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/ApiParameter.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/ApiParameter.java new file mode 100644 index 0000000..6413790 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/ApiParameter.java @@ -0,0 +1,25 @@ +/* + * Copyright 2015 Synced Synapse. 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.syncedsynapse.kore2.jsonrpc.type; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * Interface that should be implemented by all API types that can be parameters to methods + */ +public interface ApiParameter { + public JsonNode toJsonNode(); +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/ApplicationType.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/ApplicationType.java new file mode 100644 index 0000000..bad23f5 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/ApplicationType.java @@ -0,0 +1,79 @@ +/* + * Copyright 2015 Synced Synapse. 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.syncedsynapse.kore2.jsonrpc.type; + +import com.fasterxml.jackson.databind.JsonNode; +import com.syncedsynapse.kore2.utils.JsonUtils; + +/** + * Types defined in Application.* + */ +public class ApplicationType { + + /** + * Application.Property.Value + */ + public static class PropertyValue { + public static final String MUTED = "muted"; + public static final String NAME = "name"; + public static final String VERSION = "version"; + public static final String VOLUME = "volume"; + + // class members + public final Boolean muted; + public final String name; + public final Version version; + public final Integer volume; + + /** + * Contructor + * @param node JSON object representing a PropertyValue + */ + public PropertyValue(JsonNode node) { + muted = JsonUtils.booleanFromJsonNode(node, MUTED, false); + name = JsonUtils.stringFromJsonNode(node, NAME); + version = new Version(node.get(VERSION)); + volume = JsonUtils.intFromJsonNode(node, VOLUME, 0); + } + + /** + * Version + */ + public static class Version { + public static final String MAJOR = "major"; + public static final String MINOR = "minor"; + public static final String REVISION = "revision"; + public static final String TAG = "tag"; + + public final Integer major; + public final Integer minor; + public final String revision; + public final String tag; + + /** + * Constructor + * @param node JSON object representing a Version + */ + public Version(JsonNode node) { + major = JsonUtils.intFromJsonNode(node, MAJOR, 0); + minor = JsonUtils.intFromJsonNode(node, MINOR, 0); + revision = JsonUtils.stringFromJsonNode(node, REVISION); + tag = JsonUtils.stringFromJsonNode(node, TAG); + } + + } + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/AudioType.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/AudioType.java new file mode 100644 index 0000000..769e350 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/AudioType.java @@ -0,0 +1,333 @@ +/* + * Copyright 2015 Synced Synapse. 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.syncedsynapse.kore2.jsonrpc.type; + +import com.fasterxml.jackson.databind.JsonNode; +import com.syncedsynapse.kore2.utils.JsonUtils; + +import java.util.List; + +/** + * Types from Audio.* + */ +public class AudioType { + + /** + * Enums for Video.Fields.Artists + */ + public interface FieldsArtists { + public final String INSTRUMENT = "instrument"; + public final String STYLE = "style"; + public final String MOOD = "mood"; + public final String BORN = "born"; + public final String FORMED = "formed"; + public final String DESCRIPTION = "description"; + public final String GENRE = "genre"; + public final String DIED = "died"; + public final String DISBANDED = "disbanded"; + public final String YEARSACTIVE = "yearsactive"; + public final String MUSICBRAINZARTISTID = "musicbrainzartistid"; + public final String FANART = "fanart"; + public final String THUMBNAIL = "thumbnail"; + public final String COMPILATIONARTIST = "compilationartist"; + + public final static String[] allValues = new String[]{ + INSTRUMENT, STYLE, MOOD, BORN, FORMED, DESCRIPTION, GENRE, DIED, DISBANDED, + YEARSACTIVE, MUSICBRAINZARTISTID, FANART, THUMBNAIL, COMPILATIONARTIST + }; + } + + /** + * Audio.Details.Base + */ + public static class DetailsBase extends MediaType.DetailsBase { + public static final String GENRE = "genre"; + + public final List genre; + + /** + * Constructor + * @param node Json node + */ + public DetailsBase(JsonNode node) { + super(node); + genre = JsonUtils.stringListFromJsonNode(node, GENRE); + } + } + + /** + * Audio.Details.Media + */ + public static class DetailsMedia extends DetailsBase { + public static final String ARTIST = "artist"; + public static final String ARTISTID = "artistid"; + public static final String DISPLAYARTIST = "displayartist"; + public static final String GENREID = "genreid"; + public static final String MUSICBRAINZALBUMARTISTID = "musicbrainzalbumartistid"; + public static final String MUSICBRAINZALBUMID = "musicbrainzalbumid"; + public static final String RATING = "rating"; + public static final String TITLE = "title"; + public static final String YEAR = "year"; + + // class members + public final List artist; + public final List artistid; + public final String displayartist; + public final List genreid; + public final String musicbrainzalbumartistid; + public final String musicbrainzalbumid; + public final int rating; + public final String title; + public final int year; + + public DetailsMedia(JsonNode node) { + super(node); + artist = JsonUtils.stringListFromJsonNode(node, ARTIST); + artistid = JsonUtils.integerListFromJsonNode(node, ARTISTID); + displayartist = JsonUtils.stringFromJsonNode(node, DISPLAYARTIST); + genreid = JsonUtils.integerListFromJsonNode(node, GENREID); + musicbrainzalbumartistid = JsonUtils.stringFromJsonNode(node, MUSICBRAINZALBUMARTISTID); + musicbrainzalbumid = JsonUtils.stringFromJsonNode(node, MUSICBRAINZALBUMID); + rating = JsonUtils.intFromJsonNode(node, RATING); + title = JsonUtils.stringFromJsonNode(node, TITLE); + year = JsonUtils.intFromJsonNode(node, YEAR); + } + } + + /** + * Audio.Details.Artist + */ + public static class DetailsArtist extends DetailsBase { + public static final String ARTIST = "artist"; + public static final String ARTISTID = "artistid"; + public static final String BORN = "born"; + public static final String COMPILATIONARTIST = "compilationartist"; + public static final String DESCRIPTION = "description"; + public static final String DIED = "died"; + public static final String DISBANDED = "disbanded"; + public static final String FORMED = "formed"; + public static final String INSTRUMENT = "instrument"; + public static final String MOOD = "mood"; + public static final String MUSICBRAINZARTISTID = "musicbrainzartistid"; + public static final String STYLE = "style"; + public static final String YEARSACTIVE = "yearsactive"; + + public final String artist; + public final int artistid; + public final String born; + public final boolean compilationartist; + public final String description; + public final String died; + public final String disbanded; + public final String formed; + public final List instrument; + public final List mood; + public final String musicbrainzartistid; + public final List style; + public final List yearsactive; + + + /** + * Constructor + * @param node Json node + */ + public DetailsArtist(JsonNode node) { + super(node); + artist = JsonUtils.stringFromJsonNode(node, ARTIST); + artistid = JsonUtils.intFromJsonNode(node, ARTISTID); + born = JsonUtils.stringFromJsonNode(node, BORN); + compilationartist = JsonUtils.booleanFromJsonNode(node, COMPILATIONARTIST, false); + description = JsonUtils.stringFromJsonNode(node, DESCRIPTION); + died = JsonUtils.stringFromJsonNode(node, DIED); + disbanded = JsonUtils.stringFromJsonNode(node, DISBANDED); + formed = JsonUtils.stringFromJsonNode(node, FORMED); + instrument = JsonUtils.stringListFromJsonNode(node, INSTRUMENT); + mood = JsonUtils.stringListFromJsonNode(node, MOOD); + musicbrainzartistid = JsonUtils.stringFromJsonNode(node, MUSICBRAINZARTISTID); + style = JsonUtils.stringListFromJsonNode(node, STYLE); + yearsactive = JsonUtils.stringListFromJsonNode(node, YEARSACTIVE); + } + } + + /** + * Enums for Audio.Fields.Album + */ + public interface FieldsAlbum { + public final String TITLE = "title"; + public final String DESCRIPTION = "description"; + public final String ARTIST = "artist"; + public final String GENRE = "genre"; + public final String THEME = "theme"; + public final String MOOD = "mood"; + public final String STYLE = "style"; + public final String TYPE = "type"; + public final String ALBUMLABEL = "albumlabel"; + public final String RATING = "rating"; + public final String YEAR = "year"; + public final String MUSICBRAINZALBUMID = "musicbrainzalbumid"; + public final String MUSICBRAINZALBUMARTISTID = "musicbrainzalbumartistid"; + public final String FANART = "fanart"; + public final String THUMBNAIL = "thumbnail"; + public final String PLAYCOUNT = "playcount"; + public final String GENREID = "genreid"; + public final String ARTISTID = "artistid"; + public final String DISPLAYARTIST = "displayartist"; + + public final static String[] allValues = new String[]{ + TITLE, DESCRIPTION, ARTIST, GENRE, THEME, MOOD, STYLE, TYPE, ALBUMLABEL, RATING, + YEAR, MUSICBRAINZALBUMID, MUSICBRAINZALBUMARTISTID, FANART, THUMBNAIL, + PLAYCOUNT, GENREID, ARTISTID, DISPLAYARTIST + }; + } + + /** + * Audio.Details.Album + */ + public static class DetailsAlbum extends DetailsMedia { + public static final String ALBUMID = "albumid"; + public static final String ALBUMLABEL = "albumlabel"; + public static final String DESCRIPTION = "description"; + public static final String MOOD = "mood"; + public static final String PLAYCOUNT = "playcount"; + public static final String STYLE = "style"; + public static final String THEME = "theme"; + public static final String TYPE = "type"; + + public final int albumid; + public final String albumlabel; + public final String description; + public final List mood; + public final int playcount; + public final List style; + public final List theme; + public final String type; + + /** + * Constructor + * @param node Json node + */ + public DetailsAlbum(JsonNode node) { + super(node); + albumid = JsonUtils.intFromJsonNode(node, ALBUMID); + albumlabel = JsonUtils.stringFromJsonNode(node, ALBUMLABEL); + description = JsonUtils.stringFromJsonNode(node, DESCRIPTION); + mood = JsonUtils.stringListFromJsonNode(node, MOOD); + playcount = JsonUtils.intFromJsonNode(node, PLAYCOUNT); + style = JsonUtils.stringListFromJsonNode(node, STYLE); + theme = JsonUtils.stringListFromJsonNode(node, THEME); + type = JsonUtils.stringFromJsonNode(node, TYPE); + } + } + + /** + * Enums for Audio.Fields.Song + */ + public interface FieldsSong { + public final String TITLE = "title"; + public final String ARTIST = "artist"; + public final String ALBUMARTIST = "albumartist"; + public final String GENRE = "genre"; + public final String YEAR = "year"; + public final String RATING = "rating"; + public final String ALBUM = "album"; + public final String TRACK = "track"; + public final String DURATION = "duration"; + public final String COMMENT = "comment"; + public final String LYRICS = "lyrics"; + public final String MUSICBRAINZTRACKID = "musicbrainztrackid"; + public final String MUSICBRAINZARTISTID = "musicbrainzartistid"; + public final String MUSICBRAINZALBUMID = "musicbrainzalbumid"; + public final String MUSICBRAINZALBUMARTISTID = "musicbrainzalbumartistid"; + public final String PLAYCOUNT = "playcount"; + public final String FANART = "fanart"; + public final String THUMBNAIL = "thumbnail"; + public final String FILE = "file"; + public final String ALBUMID = "albumid"; + public final String LASTPLAYED = "lastplayed"; + public final String DISC = "disc"; + public final String GENREID = "genreid"; + public final String ARTISTID = "artistid"; + public final String DISPLAYARTIST = "displayartist"; + public final String ALBUMARTISTID = "albumartistid"; + + public final static String[] allValues = new String[]{ + TITLE, ARTIST, ALBUMARTIST, GENRE, YEAR, RATING, ALBUM, TRACK, DURATION, + COMMENT, LYRICS, MUSICBRAINZTRACKID, MUSICBRAINZARTISTID, MUSICBRAINZALBUMID, + MUSICBRAINZALBUMARTISTID, PLAYCOUNT, FANART, THUMBNAIL, FILE, ALBUMID, + LASTPLAYED, DISC, GENREID, ARTISTID, DISPLAYARTIST, ALBUMARTISTID + }; + } + + /** + * Audio.Details.Song + */ + public static class DetailsSong extends DetailsMedia { + public static final String ALBUM = "album"; + public static final String ALBUMARTIST = "albumartist"; + public static final String ALBUMARTISTID = "albumartistid"; + public static final String ALBUMID = "albumid"; + public static final String COMMENT = "comment"; + public static final String DISC = "disc"; + public static final String DURATION = "duration"; + public static final String FILE = "file"; + public static final String LASTPLAYED = "lastplayed"; + public static final String LYRICS = "lyrics"; + public static final String MUSICBRAINZARTISTID = "musicbrainzartistid"; + public static final String MUSICBRAINZTRACKID = "musicbrainztrackid"; + public static final String PLAYCOUNT = "playcount"; + public static final String SONGID = "songid"; + public static final String TRACK = "track"; + + public final String album; + public final List albumartist; + public final List albumartistid; + public final int albumid; + public final String comment; + public final int disc; + public final int duration; + public final String file; + public final String lastplayed; + public final String lyrics; + public final String musicbrainzartistid; + public final String musicbrainztrackid; + public final int playcount; + public final int songid; + public final int track; + + /** + * Constructor + * @param node Json node + */ + public DetailsSong(JsonNode node) { + super(node); + album = JsonUtils.stringFromJsonNode(node, ALBUM); + albumid = JsonUtils.intFromJsonNode(node, ALBUMID); + albumartist = JsonUtils.stringListFromJsonNode(node, ALBUMARTIST); + albumartistid = JsonUtils.integerListFromJsonNode(node, ALBUMARTISTID); + comment = JsonUtils.stringFromJsonNode(node, COMMENT); + disc = JsonUtils.intFromJsonNode(node, DISC); + duration = JsonUtils.intFromJsonNode(node, DURATION); + file = JsonUtils.stringFromJsonNode(node, FILE); + lastplayed = JsonUtils.stringFromJsonNode(node, LASTPLAYED); + lyrics= JsonUtils.stringFromJsonNode(node, LYRICS); + musicbrainzartistid = JsonUtils.stringFromJsonNode(node, MUSICBRAINZARTISTID); + musicbrainztrackid = JsonUtils.stringFromJsonNode(node, MUSICBRAINZTRACKID); + playcount = JsonUtils.intFromJsonNode(node, PLAYCOUNT); + songid = JsonUtils.intFromJsonNode(node, SONGID); + track = JsonUtils.intFromJsonNode(node, TRACK); + } + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/FilesType.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/FilesType.java new file mode 100644 index 0000000..6aa43fc --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/FilesType.java @@ -0,0 +1,48 @@ +/* + * Copyright 2015 Synced Synapse. 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.syncedsynapse.kore2.jsonrpc.type; + +import com.fasterxml.jackson.databind.JsonNode; +import com.syncedsynapse.kore2.utils.JsonUtils; + +/** + * Return types for methods in Files.* + */ +public class FilesType { + /** + * GetActivePlayers return type + */ + public static final class PrepareDownloadReturnType { + public final static String DETAILS = "details"; + public final static String MODE = "mode"; + public final static String PROTOCOL = "protocol"; + public final static String PATH = "path"; + + // Returned info +// public final String details; + public final String mode; + public final String protocol; + public final String path; + + public PrepareDownloadReturnType(JsonNode node) { + mode = JsonUtils.stringFromJsonNode(node, MODE); + protocol = JsonUtils.stringFromJsonNode(node, PROTOCOL); + + JsonNode details = node.get(DETAILS); + path = JsonUtils.stringFromJsonNode(details, PATH); + } + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/GlobalType.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/GlobalType.java new file mode 100644 index 0000000..f739781 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/GlobalType.java @@ -0,0 +1,57 @@ +/* + * Copyright 2015 Synced Synapse. 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.syncedsynapse.kore2.jsonrpc.type; + +import com.fasterxml.jackson.databind.JsonNode; +import com.syncedsynapse.kore2.utils.JsonUtils; + +/** + * Types from Global.* + */ +public class GlobalType { + + /** + * Global.Time + */ + public static class Time { + public static final String HOURS = "hours"; + public static final String MILLISECONDS = "milliseconds"; + public static final String MINUTES = "minutes"; + public static final String SECONDS = "seconds"; + + public final int hours; + public final int milliseconds; + public final int minutes; + public final int seconds; + + public Time(JsonNode node) { + hours = JsonUtils.intFromJsonNode(node, HOURS, 0); + milliseconds = JsonUtils.intFromJsonNode(node, MILLISECONDS, 0); + minutes = JsonUtils.intFromJsonNode(node, MINUTES, 0); + seconds = JsonUtils.intFromJsonNode(node, SECONDS, 0); + } + } + + /** + * Global.IncrementDecrement + */ + public interface IncrementDecrement { + public final String INCREMENT = "increment"; + public final String DECREMENT = "decrement"; + } + +} + diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/ItemType.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/ItemType.java new file mode 100644 index 0000000..ffafa9b --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/ItemType.java @@ -0,0 +1,40 @@ +/* + * Copyright 2015 Synced Synapse. 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.syncedsynapse.kore2.jsonrpc.type; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * Types from Item.* + */ +public class ItemType { + /** + * Item.Details.Base + */ + public static class DetailsBase { + public static final String LABEL = "label"; + + public final String label; + + public DetailsBase(JsonNode node) { + JsonNode labelNode = node.get(LABEL); + if (labelNode != null) + label = labelNode.asText(); + else + label = null; + } + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/LibraryType.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/LibraryType.java new file mode 100644 index 0000000..5666c16 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/LibraryType.java @@ -0,0 +1,62 @@ +/* + * Copyright 2015 Synced Synapse. 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.syncedsynapse.kore2.jsonrpc.type; + +import com.fasterxml.jackson.databind.JsonNode; +import com.syncedsynapse.kore2.utils.JsonUtils; + +/** + * Types from Library.* + */ +public class LibraryType { + /** + * Enums for Library.Fields.Genre + */ + public interface FieldsGenre { + public final String TITLE = "title"; + public final String THUMBNAIL = "thumbnail"; + + public final static String[] allValues = new String[]{ + TITLE, THUMBNAIL + }; + } + + /** + * Library.Details.Genre + */ + public static class DetailsGenre extends ItemType.DetailsBase { + public static final String GENREID = "genreid"; + public static final String THUMBNAIL = "thumbnail"; + public static final String TITLE = "title"; + + // class members + public final Integer genreid; + public final String thumbnail; + public final String title; + + /** + * Constructor + * @param node Json node + */ + public DetailsGenre(JsonNode node) { + super(node); + genreid = JsonUtils.intFromJsonNode(node, GENREID); + thumbnail = JsonUtils.stringFromJsonNode(node, THUMBNAIL); + title = JsonUtils.stringFromJsonNode(node, TITLE); + } + } + +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/ListType.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/ListType.java new file mode 100644 index 0000000..5c474be --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/ListType.java @@ -0,0 +1,405 @@ +/* + * Copyright 2015 Synced Synapse. 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.syncedsynapse.kore2.jsonrpc.type; + +import com.fasterxml.jackson.databind.JsonNode; +import com.syncedsynapse.kore2.utils.JsonUtils; + +import java.util.List; + +/** + * Types defined in List.* + */ +public class ListType { + + /** + * List.Item.Base type + */ + public static class ItemBase { + public static final String TYPE_MOVIE = "movie"; + public static final String TYPE_EPISODE = "episode"; + public static final String TYPE_SONG = "song"; + public static final String TYPE_MUSIC_VIDEO = "musicvideo"; + + // From List.Item.Base + public static final String ALBUM = "album"; + public static final String ALBUMARTIST = "albumartist"; + public static final String ALBUMARTISTID = "albumartistid"; + public static final String ALBUMID = "albumid"; + public static final String ALBUMLABEL = "albumlabel"; + public static final String CAST = "cast"; + public static final String COMMENT = "comment"; + public static final String COUNTRY = "country"; + public static final String DESCRIPTION = "description"; + public static final String DISC = "disc"; + public static final String DURATION = "duration"; + public static final String EPISODE = "episode"; + public static final String EPISODEGUIDE = "episodeguide"; + public static final String FIRSTAIRED = "firstaired"; + public static final String ID = "id"; + public static final String IMDBNUMBER = "imdbnumber"; + public static final String LYRICS = "lyrics"; + public static final String MOOD = "mood"; + public static final String MPAA = "mpaa"; + public static final String MUSICBRAINZARTISTID = "musicbrainzartistid"; + public static final String MUSICBRAINZTRACKID = "musicbrainztrackid"; + public static final String ORIGINALTITLE = "originaltitle"; + public static final String PLOTOUTLINE = "plotoutline"; + public static final String PREMIERED = "premiered"; + public static final String PRODUCTIONCODE = "productioncode"; + public static final String SEASON = "season"; + public static final String SET = "set"; + public static final String SETID = "setid"; + public static final String SHOWLINK = "showlink"; + public static final String SHOWTITLE = "showtitle"; + public static final String SORTTITLE = "sorttitle"; + public static final String STUDIO = "studio"; + public static final String STYLE = "style"; + public static final String TAG = "tag"; + public static final String TAGLINE = "tagline"; + public static final String THEME = "theme"; + public static final String TOP250 = "top250"; + public static final String TRACK = "track"; + public static final String TRAILER = "trailer"; + public static final String TVSHOWID = "tvshowid"; + public static final String TYPE = "type"; + public static final String UNIQUEID = "uniqueid"; + public static final String VOTES = "votes"; + public static final String WATCHEDEPISODES = "watchedepisodes"; + public static final String WRITER = "writer"; + + public final String album; + public final List albumartist; + public final List albumartistid; + public final int albumid; + public final String albumlabel; + public final List cast; + public final String comment; + public final List country; + public final String description; + public final int disc; + public final int duration; + public final int episode; + public final String episodeguide; + public final String firstaired; + public final int id; + public final String imdbnumber; + public final String lyrics; + public final List mood; + public final String mpaa; + public final String musicbrainzartistid; + public final String musicbrainztrackid; + public final String originaltitle; + public final String plotoutline; + public final String premiered; + public final String productioncode; + public final int season; + public final String set; + public final int setid; + public final List showlink; + public final String showtitle; + public final String sorttitle; + public final List studio; + public final List style; + public final List tag; + public final String tagline; + public final List theme; + public final int top250; + public final int track; + public final String trailer; + public final int tvshowid; + public final String type; + // public final HashMap uniqueid; + public final String votes; + public final int watchedepisodes; + public final List writer; + + + // From Video.Details.Base + public static final String ART = "art"; + public static final String PLAYCOUNT = "playcount"; + + public MediaType.Artwork art; + public int playcount; + + + // From Audio.Details.Media + public static final String ARTIST = "artist"; + public static final String ARTISTID = "artistid"; + public static final String DISPLAYARTIST = "displayartist"; + public static final String GENREID = "genreid"; + public static final String MUSICBRAINZALBUMARTISTID = "musicbrainzalbumartistid"; + public static final String MUSICBRAINZALBUMID = "musicbrainzalbumid"; + public static final String RATING = "rating"; + public static final String TITLE = "title"; + public static final String YEAR = "year"; + + public final List artist; + public final List artistid; + public final String displayartist; + public final List genreid; + public final String musicbrainzalbumartistid; + public final String musicbrainzalbumid; + public final double rating; + public final String title; + public final int year; + + + // From Video.Details.Item + public static final String DATEADDED = "dateadded"; + public static final String FILE = "file"; + public static final String LASTPLAYED = "lastplayed"; + public static final String PLOT = "plot"; + + public final String dateadded; + public final String file; + public final String lastplayed; + public final String plot; + + + // From Video.Details.File + public static final String DIRECTOR = "director"; + public static final String RESUME = "resume"; + public static final String RUNTIME = "runtime"; + public static final String STREAMDETAILS = "streamdetails"; + + public final List director; + public final VideoType.Resume resume; + public final int runtime; + public final VideoType.Streams streamdetails; + + + // From Media.Details.Base + public static final String FANART = "fanart"; + public static final String THUMBNAIL = "thumbnail"; + + public final String fanart; + public final String thumbnail; + + + // From Audio.Details.Base + public static final String GENRE = "genre"; + + public final List genre; + + + // From Item.Details.Base. + public static final String LABEL = "label"; + + public final String label; + + + /** + * Constructor. + * + * @param node JSON object + */ + public ItemBase(JsonNode node) { + album = JsonUtils.stringFromJsonNode(node, ALBUM, null); + albumartist = JsonUtils.stringListFromJsonNode(node, ALBUMARTIST); + albumartistid = JsonUtils.integerListFromJsonNode(node, ALBUMARTISTID); + albumid = JsonUtils.intFromJsonNode(node, ALBUMID, -1); + albumlabel = JsonUtils.stringFromJsonNode(node, ALBUMLABEL, null); + art = new MediaType.Artwork(node.get(ART)); + artist = JsonUtils.stringListFromJsonNode(node, ARTIST); + artistid = JsonUtils.integerListFromJsonNode(node, ARTISTID); + cast = VideoType.Cast.castListFromJsonNode(node, CAST); + comment = JsonUtils.stringFromJsonNode(node, COMMENT, null); + country = JsonUtils.stringListFromJsonNode(node, COUNTRY); + dateadded = JsonUtils.stringFromJsonNode(node, DATEADDED, null); + description = JsonUtils.stringFromJsonNode(node, DESCRIPTION, null); + director = JsonUtils.stringListFromJsonNode(node, DIRECTOR); + disc = JsonUtils.intFromJsonNode(node, DISC, 0); + displayartist = JsonUtils.stringFromJsonNode(node, DISPLAYARTIST, null); + duration = JsonUtils.intFromJsonNode(node, DURATION, 0); + episode = JsonUtils.intFromJsonNode(node, EPISODE, 0); + episodeguide = JsonUtils.stringFromJsonNode(node, EPISODEGUIDE, null); + fanart = JsonUtils.stringFromJsonNode(node, FANART, null); + file = JsonUtils.stringFromJsonNode(node, FILE, null); + firstaired = JsonUtils.stringFromJsonNode(node, FIRSTAIRED, null); + genre = JsonUtils.stringListFromJsonNode(node, GENRE); + genreid = JsonUtils.integerListFromJsonNode(node, GENREID); + id = JsonUtils.intFromJsonNode(node, ID, -1); + imdbnumber = JsonUtils.stringFromJsonNode(node, IMDBNUMBER, null); + label = JsonUtils.stringFromJsonNode(node, LABEL, null); + lastplayed = JsonUtils.stringFromJsonNode(node, LASTPLAYED, null); + lyrics = JsonUtils.stringFromJsonNode(node, LYRICS, null); + mood = JsonUtils.stringListFromJsonNode(node, MOOD); + mpaa = JsonUtils.stringFromJsonNode(node, MPAA, null); + musicbrainzalbumartistid = JsonUtils.stringFromJsonNode(node, MUSICBRAINZALBUMARTISTID, null); + musicbrainzalbumid = JsonUtils.stringFromJsonNode(node, MUSICBRAINZALBUMID, null); + musicbrainzartistid = JsonUtils.stringFromJsonNode(node, MUSICBRAINZARTISTID, null); + musicbrainztrackid = JsonUtils.stringFromJsonNode(node, MUSICBRAINZTRACKID, null); + originaltitle = JsonUtils.stringFromJsonNode(node, ORIGINALTITLE, null); + playcount = JsonUtils.intFromJsonNode(node, PLAYCOUNT, 0); + plot = JsonUtils.stringFromJsonNode(node, PLOT, null); + plotoutline = JsonUtils.stringFromJsonNode(node, PLOTOUTLINE, null); + premiered = JsonUtils.stringFromJsonNode(node, PREMIERED, null); + productioncode = JsonUtils.stringFromJsonNode(node, PRODUCTIONCODE, null); + rating = JsonUtils.doubleFromJsonNode(node, RATING, 0); + resume = node.has(RESUME) ? new VideoType.Resume(node.get(RESUME)) : null; + runtime = JsonUtils.intFromJsonNode(node, RUNTIME, -1); + season = JsonUtils.intFromJsonNode(node, SEASON, 0); + set = JsonUtils.stringFromJsonNode(node, SET, null); + setid = JsonUtils.intFromJsonNode(node, SETID, -1); + showlink = JsonUtils.stringListFromJsonNode(node, SHOWLINK); + showtitle = JsonUtils.stringFromJsonNode(node, SHOWTITLE, null); + sorttitle = JsonUtils.stringFromJsonNode(node, SORTTITLE, null); + streamdetails = node.has(STREAMDETAILS) ? new VideoType.Streams(node.get(STREAMDETAILS)) : null; + studio = JsonUtils.stringListFromJsonNode(node, STUDIO); + style = JsonUtils.stringListFromJsonNode(node, STYLE); + tag = JsonUtils.stringListFromJsonNode(node, TAG); + tagline = JsonUtils.stringFromJsonNode(node, TAGLINE, null); + theme = JsonUtils.stringListFromJsonNode(node, THEME); + thumbnail = JsonUtils.stringFromJsonNode(node, THUMBNAIL, null); + title = JsonUtils.stringFromJsonNode(node, TITLE, null); + top250 = JsonUtils.intFromJsonNode(node, TOP250, 0); + track = JsonUtils.intFromJsonNode(node, TRACK, 0); + trailer = JsonUtils.stringFromJsonNode(node, TRAILER, null); + tvshowid = JsonUtils.intFromJsonNode(node, TVSHOWID, -1); + type = JsonUtils.stringFromJsonNode(node, TYPE, null); +// uniqueid = getStringMap(node, UNIQUEID); + votes = JsonUtils.stringFromJsonNode(node, VOTES, null); + watchedepisodes = JsonUtils.intFromJsonNode(node, WATCHEDEPISODES, -1); + writer = JsonUtils.stringListFromJsonNode(node, WRITER); + year = JsonUtils.intFromJsonNode(node, YEAR, -1); + } + } + + public static class ItemsAll extends ItemBase { + public static final String CHANNEL = "channel"; + public static final String CHANNELNUMBER = "channelnumber"; + public static final String CHANNELTYPE = "channeltype"; + public static final String ENDTIME = "endtime"; + public static final String HIDDEN = "hidden"; + public static final String LOCKED = "locked"; + public static final String STARTTIME = "starttime"; + + // class members + public final String channel; + public final int channelnumber; + public final String channeltype; + public final String endtime; + public final boolean hidden; + public final boolean locked; + public final String starttime; + + public ItemsAll(JsonNode node) { + super(node); + + channel = JsonUtils.stringFromJsonNode(node, CHANNEL, null); + channelnumber = JsonUtils.intFromJsonNode(node, CHANNELNUMBER, 0); + channeltype = JsonUtils.stringFromJsonNode(node, CHANNELTYPE, "tv"); + endtime = JsonUtils.stringFromJsonNode(node, ENDTIME, null); + hidden = JsonUtils.booleanFromJsonNode(node, HIDDEN, false); + locked = JsonUtils.booleanFromJsonNode(node, LOCKED, false); + starttime = JsonUtils.stringFromJsonNode(node, STARTTIME, null); + } + } + + /** + * Enums for List.Fields.All + */ + public interface FieldsAll { + // We are ignoring Item.Fields.Base as it seems to have nothing useful + public final String TITLE = "title"; + public final String ARTIST = "artist"; + public final String ALBUMARTIST = "albumartist"; + public final String GENRE = "genre"; + public final String YEAR = "year"; + public final String RATING = "rating"; + public final String ALBUM = "album"; + public final String TRACK = "track"; + public final String DURATION = "duration"; + public final String COMMENT = "comment"; + public final String LYRICS = "lyrics"; + public final String MUSICBRAINZTRACKID = "musicbrainztrackid"; + public final String MUSICBRAINZARTISTID = "musicbrainzartistid"; + public final String MUSICBRAINZALBUMID = "musicbrainzalbumid"; + public final String MUSICBRAINZALBUMARTISTID = "musicbrainzalbumartistid"; + public final String PLAYCOUNT = "playcount"; + public final String FANART = "fanart"; + public final String DIRECTOR = "director"; + public final String TRAILER = "trailer"; + public final String TAGLINE = "tagline"; + public final String PLOT = "plot"; + public final String PLOTOUTLINE = "plotoutline"; + public final String ORIGINALTITLE = "originaltitle"; + public final String LASTPLAYED = "lastplayed"; + public final String WRITER = "writer"; + public final String STUDIO = "studio"; + public final String MPAA = "mpaa"; + public final String CAST = "cast"; + public final String COUNTRY = "country"; + public final String IMDBNUMBER = "imdbnumber"; + public final String PREMIERED = "premiered"; + public final String PRODUCTIONCODE = "productioncode"; + public final String RUNTIME = "runtime"; + public final String SET = "set"; + public final String SHOWLINK = "showlink"; + public final String STREAMDETAILS = "streamdetails"; + public final String TOP250 = "top250"; + public final String VOTES = "votes"; + public final String FIRSTAIRED = "firstaired"; + public final String SEASON = "season"; + public final String EPISODE = "episode"; + public final String SHOWTITLE = "showtitle"; + public final String THUMBNAIL = "thumbnail"; + public final String FILE = "file"; + public final String RESUME = "resume"; + public final String ARTISTID = "artistid"; + public final String ALBUMID = "albumid"; + public final String TVSHOWID = "tvshowid"; + public final String SETID = "setid"; + public final String WATCHEDEPISODES = "watchedepisodes"; + public final String DISC = "disc"; + public final String TAG = "tag"; + public final String ART = "art"; + public final String GENREID = "genreid"; + public final String DISPLAYARTIST = "displayartist"; + public final String ALBUMARTISTID = "albumartistid"; + public final String DESCRIPTION = "description"; + public final String THEME = "theme"; + public final String MOOD = "mood"; + public final String STYLE = "style"; + public final String ALBUMLABEL = "albumlabel"; + public final String SORTTITLE = "sorttitle"; + public final String EPISODEGUIDE = "episodeguide"; + public final String UNIQUEID = "uniqueid"; + public final String DATEADDED = "dateadded"; + public final String CHANNEL = "channel"; + public final String CHANNELTYPE = "channeltype"; + public final String HIDDEN = "hidden"; + public final String LOCKED = "locked"; + public final String CHANNELNUMBER = "channelnumber"; + public final String STARTTIME = "starttime"; + public final String ENDTIME = "endtime"; + + public final String[] allValues = new String[] { + TITLE, ARTIST, ALBUMARTIST, GENRE, YEAR, RATING, ALBUM, TRACK, DURATION, COMMENT, + LYRICS, MUSICBRAINZTRACKID, MUSICBRAINZARTISTID, MUSICBRAINZALBUMID, + MUSICBRAINZALBUMARTISTID, PLAYCOUNT, FANART, DIRECTOR, TRAILER, TAGLINE, PLOT, + PLOTOUTLINE, ORIGINALTITLE, LASTPLAYED, WRITER, STUDIO, MPAA, CAST, COUNTRY, + IMDBNUMBER, PREMIERED, PRODUCTIONCODE, RUNTIME, SET, SHOWLINK, STREAMDETAILS, + TOP250, VOTES, FIRSTAIRED, SEASON, EPISODE, SHOWTITLE, THUMBNAIL, FILE, RESUME, + ARTISTID, ALBUMID, TVSHOWID, SETID, WATCHEDEPISODES, DISC, TAG, ART, GENREID, + DISPLAYARTIST, ALBUMARTISTID, DESCRIPTION, THEME, MOOD, STYLE, ALBUMLABEL, + SORTTITLE, EPISODEGUIDE, UNIQUEID, DATEADDED, CHANNEL, CHANNELTYPE, HIDDEN, + LOCKED, CHANNELNUMBER, STARTTIME, ENDTIME + }; + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/MediaType.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/MediaType.java new file mode 100644 index 0000000..b8eca47 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/MediaType.java @@ -0,0 +1,87 @@ +/* + * Copyright 2015 Synced Synapse. 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.syncedsynapse.kore2.jsonrpc.type; + +import com.fasterxml.jackson.databind.JsonNode; +import com.syncedsynapse.kore2.utils.JsonUtils; + +/** + * Types from Media.* + */ +public class MediaType { + + + /** + * Media.Artwork + */ + public static class Artwork { + public static final String BANNER = "banner"; + public static final String TV_SHOW_BANNER = "tvshow.banner"; + public static final String FANART = "fanart"; + public static final String TV_SHOW_FANART = "tvshow.fanart"; + public static final String POSTER = "poster"; + public static final String TV_SHOW_POSTER = "tvshow.poster"; + public static final String THUMB = "thumb"; + public static final String ALBUM_THUMB = "album.thumb"; + + public String banner; + public String fanart; + public String poster; + public String thumb; + + public Artwork(JsonNode node) { + if (node == null) { + return; + } + + banner = JsonUtils.stringFromJsonNode(node, BANNER, null); + if (banner == null) + banner = JsonUtils.stringFromJsonNode(node, TV_SHOW_BANNER, null); + fanart = JsonUtils.stringFromJsonNode(node, FANART, null); + if (fanart == null) + poster = JsonUtils.stringFromJsonNode(node, TV_SHOW_FANART, null); + poster = JsonUtils.stringFromJsonNode(node, POSTER, null); + if (poster == null) + poster = JsonUtils.stringFromJsonNode(node, TV_SHOW_POSTER, null); + thumb = JsonUtils.stringFromJsonNode(node, THUMB, null); + if (thumb == null) + thumb = JsonUtils.stringFromJsonNode(node, ALBUM_THUMB, null); + } + } + + /** + * Media.Details.Base + */ + public static class DetailsBase extends ItemType.DetailsBase { + public static final String FANART = "fanart"; + public static final String THUMBNAIL = "thumbnail"; + + public final String fanart; + public final String thumbnail; + + /** + * Constructor from Json node + * @param node Json node + */ + public DetailsBase(JsonNode node) { + super(node); + fanart = JsonUtils.stringFromJsonNode(node, FANART, null); + thumbnail = JsonUtils.stringFromJsonNode(node, THUMBNAIL, null); + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/PlayerType.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/PlayerType.java new file mode 100644 index 0000000..60d20a0 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/PlayerType.java @@ -0,0 +1,331 @@ +/* + * Copyright 2015 Synced Synapse. 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.syncedsynapse.kore2.jsonrpc.type; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.syncedsynapse.kore2.utils.JsonUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * Return types for methods in Player.* + */ +public class PlayerType { + + /** + * GetActivePlayers return type + */ + public static final class GetActivePlayersReturnType { + private final static String PLAYERID = "playerid"; + private final static String TYPE = "type"; + + public final static String VIDEO = "video"; + public final static String AUDIO = "audio"; + public final static String PICTURE = "picture"; + + /** + * Player id currently active + */ + public final int playerid; + /** + * Type of player. See this class public constants + */ + public final String type; + + public GetActivePlayersReturnType(JsonNode node) { + playerid = node.has(PLAYERID) ? node.get(PLAYERID).asInt(-1) : -1; + type = node.has(TYPE) ? node.get(TYPE).textValue() : null; + } + } + + /** + * Enums for Player.Property.Name + */ + public interface PropertyName { + public final String TYPE = "type"; + public final String PARTYMODE = "partymode"; + public final String SPEED = "speed"; + public final String TIME = "time"; + public final String PERCENTAGE = "percentage"; + public final String TOTALTIME = "totaltime"; + public final String PLAYLISTID = "playlistid"; + public final String POSITION = "position"; + public final String REPEAT = "repeat"; + public final String SHUFFLED = "shuffled"; + public final String CANSEEK = "canseek"; + public final String CANCHANGESPEED = "canchangespeed"; + public final String CANMOVE = "canmove"; + public final String CANZOOM = "canzoom"; + public final String CANROTATE = "canrotate"; + public final String CANSHUFFLE = "canshuffle"; + public final String CANREPEAT = "canrepeat"; + public final String CURRENTAUDIOSTREAM = "currentaudiostream"; + public final String AUDIOSTREAMS = "audiostreams"; + public final String SUBTITLEENABLED = "subtitleenabled"; + public final String CURRENTSUBTITLE = "currentsubtitle"; + public final String SUBTITLES = "subtitles"; + public final String LIVE = "live"; + + public final String[] allValues = new String[]{ + TYPE, PARTYMODE, SPEED, TIME, PERCENTAGE, TOTALTIME, PLAYLISTID, POSITION, REPEAT, + SHUFFLED, CANSEEK, CANCHANGESPEED, CANMOVE, CANZOOM, CANROTATE, CANSHUFFLE, + CANREPEAT, CURRENTAUDIOSTREAM, AUDIOSTREAMS, SUBTITLEENABLED, CURRENTSUBTITLE, + SUBTITLES, LIVE + }; + } + + /** + * Player.Property.Value + */ + public static class PropertyValue { + /** + * Player.Type + */ + public static final String TYPE_VIDEO = "video"; + public static final String TYPE_AUDIO = "audio"; + public static final String TYPE_PICTURE = "picture"; + + /** + * Properties + */ + public static final String AUDIOSTREAMS = "audiostreams"; + public static final String CANCHANGESPEED = "canchangespeed"; + public static final String CANMOVE = "canmove"; + public static final String CANREPEAT = "canrepeat"; + public static final String CANROTATE = "canrotate"; + public static final String CANSEEK = "canseek"; + public static final String CANSHUFFLE = "canshuffle"; + public static final String CANZOOM = "canzoom"; + public static final String CURRENTAUDIOSTREAM = "currentaudiostream"; + public static final String CURRENTSUBTITLE = "currentsubtitle"; + public static final String LIVE = "live"; + public static final String PARTYMODE = "partymode"; + public static final String PERCENTAGE = "percentage"; + public static final String PLAYLISTID = "playlistid"; + public static final String POSITION = "position"; + public static final String REPEAT = "repeat"; + public static final String SHUFFLED = "shuffled"; + public static final String SPEED = "speed"; + public static final String SUBTITLEENABLED = "subtitleenabled"; + public static final String SUBTITLES = "subtitles"; + public static final String TIME = "time"; + public static final String TOTALTIME = "totaltime"; + public static final String TYPE = "type"; + + public final List audiostreams; + public final boolean canchangespeed; + public final boolean canmove; + public final boolean canrepeat; + public final boolean canrotate; + public final boolean canseek; + public final boolean canshuffle; + public final boolean canzoom; + public final AudioStreamExtended currentaudiostream; + public final Subtitle currentsubtitle; + public final boolean live; + public final boolean partymode; + public final double percentage; + public final int playlistid; + public final int position; + public final String repeat; + public final boolean shuffled; + public final int speed; + public final boolean subtitleenabled; + public final List subtitles; + public final GlobalType.Time time; + public final GlobalType.Time totaltime; + public final String type; + + + public PropertyValue(JsonNode node) { + audiostreams = node.has(AUDIOSTREAMS) ? AudioStream.getListAudioStream(node.get(AUDIOSTREAMS)) : null; + canchangespeed = JsonUtils.booleanFromJsonNode(node, CANCHANGESPEED, false); + canmove = JsonUtils.booleanFromJsonNode(node, CANMOVE, false); + canrepeat = JsonUtils.booleanFromJsonNode(node, CANREPEAT, false); + canrotate = JsonUtils.booleanFromJsonNode(node, CANROTATE, false); + canseek = JsonUtils.booleanFromJsonNode(node, CANSEEK, false); + canshuffle = JsonUtils.booleanFromJsonNode(node, CANSHUFFLE, false); + canzoom = JsonUtils.booleanFromJsonNode(node, CANZOOM, false); + currentaudiostream = node.has(CURRENTAUDIOSTREAM) ? new AudioStreamExtended(node.get(CURRENTAUDIOSTREAM)) : null; + currentsubtitle = node.has(CURRENTSUBTITLE) ? new Subtitle(node.get(CURRENTSUBTITLE)) : null; + live = JsonUtils.booleanFromJsonNode(node, LIVE, false); + partymode = JsonUtils.booleanFromJsonNode(node, PARTYMODE, false); + percentage = JsonUtils.doubleFromJsonNode(node, PERCENTAGE, 0); + playlistid = JsonUtils.intFromJsonNode(node, PLAYLISTID, -1); + position = JsonUtils.intFromJsonNode(node, POSITION, -1); + repeat = JsonUtils.stringFromJsonNode(node, REPEAT, "off"); + shuffled = JsonUtils.booleanFromJsonNode(node, SHUFFLED, false); + speed = JsonUtils.intFromJsonNode(node, SPEED, 0); + subtitleenabled = JsonUtils.booleanFromJsonNode(node, SUBTITLEENABLED, false); + subtitles = node.has(SUBTITLES) ? Subtitle.getListSubtitle(node.get(SUBTITLES)) : null; + time = node.has(TIME) ? new GlobalType.Time(node.get(TIME)) : null; + totaltime = node.has(TOTALTIME) ? new GlobalType.Time(node.get(TOTALTIME)) : null; + type = JsonUtils.stringFromJsonNode(node, TYPE, "video"); + } + } + + /** + * Player.Audio.Stream + */ + public static class AudioStream { + public static final String INDEX = "index"; + public static final String LANGUAGE = "language"; + public static final String NAME = "name"; + + public final int index; + public final String language; + public final String name; + + public AudioStream(JsonNode node) { + index = JsonUtils.intFromJsonNode(node, INDEX); + language = JsonUtils.stringFromJsonNode(node, LANGUAGE); + name = JsonUtils.stringFromJsonNode(node, NAME); + } + + public static List getListAudioStream(JsonNode node) { + final ArrayNode arrayNode = (ArrayNode)node; + final List result = new ArrayList(node.size()); + + for (JsonNode audioStreamNode : arrayNode) { + result.add(new AudioStream(audioStreamNode)); + } + return result; + } + } + + /** + * Player.Audio.Stream.Extended + */ + public static class AudioStreamExtended extends AudioStream { + public static final String BITRATE = "bitrate"; + public static final String CHANNELS = "channels"; + public static final String CODEC = "codec"; + + public final int bitrate; + public final int channels; + public final String codec; + + public AudioStreamExtended(JsonNode node) { + super(node); + bitrate = JsonUtils.intFromJsonNode(node, BITRATE); + channels = JsonUtils.intFromJsonNode(node, CHANNELS); + codec = JsonUtils.stringFromJsonNode(node, CODEC); + } + } + + /** + * Player.Subtitle + */ + public static class Subtitle { + public static final String INDEX = "index"; + public static final String LANGUAGE = "language"; + public static final String NAME = "name"; + + public final int index; + public final String language; + public final String name; + + public Subtitle(JsonNode node) { + index = JsonUtils.intFromJsonNode(node, INDEX); + language = JsonUtils.stringFromJsonNode(node, LANGUAGE); + name = JsonUtils.stringFromJsonNode(node, NAME); + } + + public static List getListSubtitle(JsonNode node) { + final ArrayNode arrayNode = (ArrayNode)node; + final List result = new ArrayList(node.size()); + + for (JsonNode subtitleNode : arrayNode) { + result.add(new Subtitle(subtitleNode)); + } + return result; + } + } + + /** + * Player.Position.Time + */ + public static class PositionTime + implements ApiParameter { + public static final String HOURS = "hours"; + public static final String MILLISECONDS = "milliseconds"; + public static final String MINUTES = "minutes"; + public static final String SECONDS = "seconds"; + + protected static final ObjectMapper objectMapper = new ObjectMapper(); + + public final int hours; + public final int milliseconds; + public final int minutes; + public final int seconds; + + public PositionTime(int hours, int minutes, int seconds, int milliseconds) { + this.hours = hours; + this.minutes = minutes; + this.seconds = seconds; + this.milliseconds = milliseconds; + } + + public JsonNode toJsonNode() { + final ObjectNode node = objectMapper.createObjectNode(); + node.put(HOURS, hours); + node.put(MILLISECONDS, milliseconds); + node.put(MINUTES, minutes); + node.put(SECONDS, seconds); + return node; + } + } + + /** + * Player.Seek return type + */ + public static final class SeekReturnType { + private final static String PERCENTAGE = "percentage"; + private final static String TIME = "time"; + private final static String TOTAL_TIME = "totaltime"; + + /** + * Percentage + */ + public final int percentage; + /** + * Time and Total time + */ + public final GlobalType.Time time; + public final GlobalType.Time totalTime; + + public SeekReturnType(JsonNode node) { + percentage = JsonUtils.intFromJsonNode(node, PERCENTAGE); + time = new GlobalType.Time(node.get(TIME)); + totalTime = new GlobalType.Time(node.get(TOTAL_TIME)); + } + } + + /** + * Player.Repeat constants + */ + public interface Repeat { + public final String OFF = "off"; + public final String ONE = "one"; + public final String ALL = "all"; + + public final String CYCLE = "cycle"; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/PlaylistType.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/PlaylistType.java new file mode 100644 index 0000000..4dc8de3 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/PlaylistType.java @@ -0,0 +1,113 @@ +/* + * Copyright 2015 Synced Synapse. 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.syncedsynapse.kore2.jsonrpc.type; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.syncedsynapse.kore2.utils.JsonUtils; + +/** + * Return types for methods in Playlist.* + */ +public class PlaylistType { + + /** + * GetPlaylists return type + */ + public static final class GetPlaylistsReturnType { + private final static String PLAYLISTID = "playlistid"; + private final static String TYPE = "type"; + + public final static String UNKNOWN = "unknown"; + public final static String VIDEO = "video"; + public final static String AUDIO = "audio"; + public final static String PICTURE = "picture"; + public final static String MIXED = "mixed"; + + /** + * Playlist id + */ + public final int playlistid; + /** + * Type of playlist. See this class public constants + */ + public final String type; + + public GetPlaylistsReturnType(JsonNode node) { + playlistid = node.get(PLAYLISTID).asInt(-1); + type = JsonUtils.stringFromJsonNode(node, TYPE, UNKNOWN); + } + } + + /** + * Playlist.Item + */ + public static class Item implements ApiParameter { + + protected static final ObjectMapper objectMapper = new ObjectMapper(); + + // class members + public int albumid = -1; + public int artistid = -1; + public String directory = null; + public int episodeid = -1; + public String file = null; + public int genreid = -1; + public int movieid = -1; + public int musicvideoid = -1; + public int songid = -1; + + /** + * Constructors + */ + public Item() { + } + + @Override + public JsonNode toJsonNode() { + final ObjectNode node = objectMapper.createObjectNode(); + if (albumid != -1) { + node.put("albumid", albumid); + } + if (artistid != -1) { + node.put("artistid", artistid); + } + if (directory != null) { + node.put("directory", directory); + } + if (episodeid != -1) { + node.put("episodeid", episodeid); + } + if (file != null) { + node.put("file", file); + } + if (genreid != -1) { + node.put("genreid", genreid); + } + if (movieid != -1) { + node.put("movieid", movieid); + } + if (musicvideoid != -1) { + node.put("musicvideoid", musicvideoid); + } + if (songid != -1) { + node.put("songid", songid); + } + return node; + } + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/VideoType.java b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/VideoType.java new file mode 100644 index 0000000..0f6ea74 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/jsonrpc/type/VideoType.java @@ -0,0 +1,673 @@ +/* + * Copyright 2015 Synced Synapse. 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.syncedsynapse.kore2.jsonrpc.type; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.syncedsynapse.kore2.utils.JsonUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * Types from Video.* + */ +public class VideoType { + + public static class Cast { + public static final String NAME = "name"; + public static final String ORDER = "order"; + public static final String ROLE = "role"; + public static final String THUMBNAIL = "thumbnail"; + + public final String name; + public final int order; + public final String role; + public final String thumbnail; + + public Cast(JsonNode node) { + name = JsonUtils.stringFromJsonNode(node, NAME); + order = JsonUtils.intFromJsonNode(node, ORDER, 0); + role = JsonUtils.stringFromJsonNode(node, ROLE); + thumbnail = JsonUtils.stringFromJsonNode(node, THUMBNAIL); + } + + public Cast(String name, int order, String role, String thumbnail) { + this.name = name; + this.order = order; + this.role = role; + this.thumbnail = thumbnail; + } + + public static List castListFromJsonNode(JsonNode node, String key) { + if ((node == null) || (!node.has(key))) { + return new ArrayList(0); + } + + ArrayNode arrayNode = (ArrayNode) node.get(key); + ArrayList castList = new ArrayList(arrayNode.size()); + for (JsonNode innerNode : arrayNode) { + castList.add(new Cast(innerNode)); + } + return castList; + } + } + + public static class Resume { + public static final String POSITION = "position"; + public static final String TOTAL = "total"; + + public final double position; + public final double total; + + public Resume(JsonNode node) { + position = JsonUtils.doubleFromJsonNode(node, POSITION, 0); + total = JsonUtils.doubleFromJsonNode(node, TOTAL, 0); + } + } + + public static class Streams { + + public static class Audio { + public static final String CHANNELS = "channels"; + public static final String CODEC = "codec"; + public static final String LANGUAGE = "language"; + + public final int channels; + public final String codec; + public final String language; + + public Audio(JsonNode node) { + channels = JsonUtils.intFromJsonNode(node, CHANNELS, 0); + codec = JsonUtils.stringFromJsonNode(node, CODEC); + language = JsonUtils.stringFromJsonNode(node, LANGUAGE); + } + } + + public static class Subtitle { + public static final String LANGUAGE = "language"; + + public final String language; + + public Subtitle(JsonNode node) { + language = JsonUtils.stringFromJsonNode(node, LANGUAGE, null); + } + } + + public static class Video { + public static final String ASPECT = "aspect"; + public static final String CODEC = "codec"; + public static final String DURATION = "duration"; + public static final String HEIGHT = "height"; + public static final String WIDTH = "width"; + + public final double aspect; + public final String codec; + public final int duration; + public final int height; + public final int width; + + public Video(JsonNode node) { + aspect = JsonUtils.doubleFromJsonNode(node, ASPECT, 0); + codec = JsonUtils.stringFromJsonNode(node, CODEC, null); + duration = JsonUtils.intFromJsonNode(node, DURATION, -1); + height = JsonUtils.intFromJsonNode(node, HEIGHT, -1); + width = JsonUtils.intFromJsonNode(node, WIDTH, -1); + } + } + + public static final String AUDIO = "audio"; + public static final String SUBTITLE = "subtitle"; + public static final String VIDEO = "video"; + + // class members + public final List

Set the current page of both the ViewPager and indicator.

+ *

+ *

This must be used if you need to set the page before the views are drawn + * on screen (e.g., default start page).

+ * + * @param item + */ + void setCurrentItem(int item); + + /** + * Set a page change listener which will receive forwarded events. + * + * @param listener + */ + void setOnPageChangeListener(ViewPager.OnPageChangeListener listener); + + /** + * Notify the indicator that the fragment list has changed. + */ + void notifyDataSetChanged(); +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/utils/BasicAuthPicassoDownloader.java b/app/src/main/java/com/syncedsynapse/kore2/utils/BasicAuthPicassoDownloader.java new file mode 100644 index 0000000..99549d2 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/utils/BasicAuthPicassoDownloader.java @@ -0,0 +1,57 @@ +/* + * Copyright 2015 Synced Synapse. 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.syncedsynapse.kore2.utils; + +import android.text.TextUtils; +import android.util.Base64; + +import com.squareup.picasso.UrlConnectionDownloader; + +import java.net.HttpURLConnection; + +/** + * Picasso Downloader that sets basic authentication in the headers + */ +public class BasicAuthPicassoDownloader extends UrlConnectionDownloader { + + protected final String username; + protected final String password; + + public BasicAuthPicassoDownloader(android.content.Context context) { + super(context); + this.username = null; + this.password = null; + } + + public BasicAuthPicassoDownloader(android.content.Context context, String username, String password) { + super(context); + this.username = username; + this.password = password; + } + + @Override + protected HttpURLConnection openConnection(android.net.Uri uri) + throws java.io.IOException { + HttpURLConnection urlConnection = super.openConnection(uri); + + if (!TextUtils.isEmpty(username) && !TextUtils.isEmpty(password)) { + String creds = username + ":" + password; + urlConnection.setRequestProperty("Authorization", "Basic " + + Base64.encodeToString(creds.getBytes(), Base64.NO_WRAP)); + } + return urlConnection; + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/utils/CharacterDrawable.java b/app/src/main/java/com/syncedsynapse/kore2/utils/CharacterDrawable.java new file mode 100644 index 0000000..b70e341 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/utils/CharacterDrawable.java @@ -0,0 +1,99 @@ +/* + * Copyright 2015 Synced Synapse. 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.syncedsynapse.kore2.utils; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Typeface; +import android.graphics.drawable.ColorDrawable; + +public class CharacterDrawable extends ColorDrawable { + private final char character; + private final Paint textPaint; +// private final Paint borderPaint; +// private static final int STROKE_WIDTH = 10; +// private static final float SHADE_FACTOR = 0.9f; + + private static final Typeface typeface; + static { + if (Utils.isJellybeanMR1OrLater()) { + typeface = Typeface.create("sans-serif-thin", Typeface.NORMAL); + } else if (Utils.isJellybeanOrLater()) { + typeface = Typeface.create("sans-serif-light", Typeface.NORMAL); + } else { + typeface = Typeface.create("sans-serif", Typeface.NORMAL); + } + } + + public CharacterDrawable(char character, int color) { + super(color); + this.character = character; + this.textPaint = new Paint(); +// this.borderPaint = new Paint(); + + // text paint settings + textPaint.setColor(Color.WHITE); + textPaint.setAntiAlias(true); + textPaint.setFakeBoldText(false); + + textPaint.setStyle(Paint.Style.FILL); + textPaint.setTextAlign(Paint.Align.CENTER); + textPaint.setTypeface(typeface); + + // border paint settings +// borderPaint.setColor(getDarkerShade(color)); +// borderPaint.setStyle(Paint.Style.STROKE); +// borderPaint.setStrokeWidth(STROKE_WIDTH); + } + +// private int getDarkerShade(int color) { +// return Color.rgb((int)(SHADE_FACTOR * Color.red(color)), +// (int)(SHADE_FACTOR * Color.green(color)), +// (int)(SHADE_FACTOR * Color.blue(color))); +// } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + + // draw border +// canvas.drawRect(getBounds(), borderPaint); + + // draw text + int width = canvas.getWidth(); + int height = canvas.getHeight(); + textPaint.setTextSize(height / 2); + canvas.drawText(String.valueOf(character), width/2, height/2 - ((textPaint.descent() + textPaint.ascent()) / 2) , textPaint); + } + + @Override + public void setAlpha(int alpha) { + textPaint.setAlpha(alpha); + } + + @Override + public void setColorFilter(ColorFilter cf) { + textPaint.setColorFilter(cf); + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/syncedsynapse/kore2/utils/FileDownloadHelper.java b/app/src/main/java/com/syncedsynapse/kore2/utils/FileDownloadHelper.java new file mode 100644 index 0000000..9e5ab7f --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/utils/FileDownloadHelper.java @@ -0,0 +1,397 @@ +/* + * Copyright 2015 Synced Synapse. 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.syncedsynapse.kore2.utils; + +import android.app.DownloadManager; +import android.content.Context; +import android.net.Uri; +import android.os.Environment; +import android.os.Handler; +import android.text.TextUtils; +import android.util.Base64; +import android.widget.Toast; + +import com.syncedsynapse.kore2.R; +import com.syncedsynapse.kore2.host.HostInfo; +import com.syncedsynapse.kore2.jsonrpc.ApiCallback; +import com.syncedsynapse.kore2.jsonrpc.HostConnection; +import com.syncedsynapse.kore2.jsonrpc.method.Files; +import com.syncedsynapse.kore2.jsonrpc.method.JSONRPC; +import com.syncedsynapse.kore2.jsonrpc.type.FilesType; + +import java.io.File; +import java.util.List; + +/** + * Various methods to help with file downloading + */ +public class FileDownloadHelper { + private static final String TAG = LogUtils.makeLogTag(FileDownloadHelper.class); + + public static final int OVERWRITE_FILES = 0, + DOWNLOAD_WITH_NEW_NAME = 1; + + public static abstract class MediaInfo { + public final String fileName; + + public MediaInfo(final String fileName) { + this.fileName = fileName; + } + + /** + * Check whether the directory on which to load the file exists + * @return Whether the directory exists + */ + public boolean downloadDirectoryExists() { + File file = new File(getAbsoluteDirectoryPath()); +// LogUtils.LOGD(TAG, "Checking directory: " + file.getPath()); +// LogUtils.LOGD(TAG, "Exists: " + file.exists()); + return file.exists(); + } + + /** + * Check whether the file to download already exists + * @return Whether the file exists + */ + public boolean downloadFileExists() { + if (!downloadDirectoryExists()) + return false; + File file = new File(getAbsoluteFilePath()); + return file.exists(); + } + + public String getAbsoluteDirectoryPath() { + File externalFilesDir = Environment.getExternalStoragePublicDirectory(getExternalPublicDirType()); + return externalFilesDir.getPath() + "/" + getRelativeDirectoryPath(); + + } + + public String getAbsoluteFilePath() { + return getAbsoluteDirectoryPath() + "/" + getDownloadFileName(); + } + + public String getRelativeFilePath() { + return getRelativeDirectoryPath() + "/" + getDownloadFileName(); + } + + public abstract String getExternalPublicDirType(); + public abstract String getRelativeDirectoryPath(); + public abstract String getDownloadFileName(); + + public abstract String getDownloadTitle(Context context); + public String getDownloadDescrition(Context context) { + return context.getString(R.string.download_file_description); + } + } + + /** + * Info for downloading songs + */ + public static class SongInfo extends MediaInfo { + public final String artist; + public final String album; + public final int songId; + public final int track; + public final String title; + + public SongInfo(final String artist, final String album, + final int songId, final int track, final String title, + final String fileName) { + super(fileName); + this.artist = artist; + this.album = album; + this.songId = songId; + this.track = track; + this.title = title; + } + + public String getRelativeDirectoryPath() { + return (TextUtils.isEmpty(album) || TextUtils.isEmpty(artist)) ? + null : artist + "/" + album; + } + + public String getDownloadFileName() { + String ext = getFilenameExtension(fileName); + return (ext != null) ? + String.valueOf(track) + " - " + title + ext : + null; + } + + public String getExternalPublicDirType() { + return Environment.DIRECTORY_MUSIC; + } + + public String getDownloadTitle(Context context) { + return title; + } + } + + /** + * Info for downloading movies + */ + public static class MovieInfo extends MediaInfo { + public final String title; + + public MovieInfo(final String title, final String fileName) { + super(fileName); + this.title = title; + } + + public String getRelativeDirectoryPath() { + return (TextUtils.isEmpty(title)) ? + null : title; + } + + public String getDownloadFileName() { + String ext = getFilenameExtension(fileName); + return (ext != null) ? + title + ext : null; + } + + public String getExternalPublicDirType() { + return Environment.DIRECTORY_MOVIES; + } + + public String getDownloadTitle(Context context) { + return title; + } + } + + /** + * Info for downloading TVShows + */ + public static class TVShowInfo extends MediaInfo { + public final String tvshowTitle; + public final int season; + public final int episodeNumber; + public final String title; + + public TVShowInfo(final String tvshowTitle, final int season, + final int episodeNumber, final String title, + final String fileName) { + super(fileName); + this.tvshowTitle = tvshowTitle; + this.season = season; + this.episodeNumber = episodeNumber; + this.title = title; + } + + public String getRelativeDirectoryPath() { + if (season > 0) { + return (TextUtils.isEmpty(tvshowTitle)) ? + null : tvshowTitle + "/Season" + String.valueOf(season); + } else { + return (TextUtils.isEmpty(tvshowTitle)) ? + null : tvshowTitle; + } + } + + public String getDownloadFileName() { + String ext = getFilenameExtension(fileName); + return (ext != null) ? + String.valueOf(episodeNumber) + " - " + title + ext : null; + } + + public String getExternalPublicDirType() { + return Environment.DIRECTORY_MOVIES; + } + + public String getDownloadTitle(Context context) { + return title; + } + } + + /** + * Info for downloading music videos + */ + public static class MusicVideoInfo extends MediaInfo { + public final String title; + + private static final String SUBDIRECTORY = "Music Videos"; + + public MusicVideoInfo(final String title, final String fileName) { + super(fileName); + this.title = title; + } + + public String getRelativeDirectoryPath() { + return (TextUtils.isEmpty(title)) ? + null : SUBDIRECTORY + "/" + title; + } + + public String getDownloadFileName() { + String ext = getFilenameExtension(fileName); + return (ext != null) ? + title + ext : null; + } + + public String getExternalPublicDirType() { + return Environment.DIRECTORY_MUSIC; + } + + public String getDownloadTitle(Context context) { + return title; + } + } + + /** + * Auxiliary method to get a filename extension, assuming the filename ends with .ext + * @param filename File name + * @return Extension if present, or null + */ + public static String getFilenameExtension(String filename) { + int idx = filename.lastIndexOf("."); + return (idx > 0) ? filename.substring(idx) : null; + } + + public static void downloadFiles(final Context context, final HostInfo hostInfo, + final MediaInfo mediaInfo, + final int fileHandlingMode, + final Handler callbackHandler) { + if (mediaInfo == null) + return; + + if (!checkDownloadDir(context, mediaInfo.getAbsoluteDirectoryPath())) + return; + + // Check if we are connected to the host + final HostConnection httpHostConnection = new HostConnection(hostInfo); + httpHostConnection.setProtocol(HostConnection.PROTOCOL_HTTP); + + JSONRPC.Ping action = new JSONRPC.Ping(); + action.execute(httpHostConnection, new ApiCallback() { + @Override + public void onSucess(String result) { + // Ok, continue, iterate through the song list and launch a download for each + final DownloadManager downloadManager = (DownloadManager)context.getSystemService(Context.DOWNLOAD_SERVICE); + + downloadSingleFile(context, httpHostConnection, hostInfo, + mediaInfo, fileHandlingMode, downloadManager, callbackHandler); + } + + @Override + public void onError(int errorCode, String description) { + Toast.makeText(context, R.string.unable_to_connect_to_xbmc, Toast.LENGTH_SHORT) + .show(); + } + }, callbackHandler); + } + + public static void downloadFiles(final Context context, final HostInfo hostInfo, + final List mediaInfoList, + final int fileHandlingMode, + final Handler callbackHandler) { + if ((mediaInfoList == null) || (mediaInfoList.size() == 0)) + return; + + if (!checkDownloadDir(context, mediaInfoList.get(0).getAbsoluteDirectoryPath())) + return; + + // Check if we are connected to the host + final HostConnection httpHostConnection = new HostConnection(hostInfo); + httpHostConnection.setProtocol(HostConnection.PROTOCOL_HTTP); + + JSONRPC.Ping action = new JSONRPC.Ping(); + action.execute(httpHostConnection, new ApiCallback() { + @Override + public void onSucess(String result) { + // Ok, continue, iterate through the song list and launch a download for each + final DownloadManager downloadManager = (DownloadManager)context.getSystemService(Context.DOWNLOAD_SERVICE); + + for (final MediaInfo mediaInfo : mediaInfoList) { + downloadSingleFile(context, httpHostConnection, hostInfo, + mediaInfo, fileHandlingMode, downloadManager, callbackHandler); + } + } + + @Override + public void onError(int errorCode, String description) { + Toast.makeText(context, R.string.unable_to_connect_to_xbmc, Toast.LENGTH_SHORT) + .show(); + } + }, callbackHandler); + } + + private static boolean checkDownloadDir(Context context, String downloadDirPath) { + File downloadDir = new File(downloadDirPath); + if ((downloadDir.exists() && !downloadDir.isDirectory())) { + Toast.makeText(context, + "Download directory already exists and is not a directory.", + Toast.LENGTH_SHORT) + .show(); + return false; + } + if (!downloadDir.isDirectory() && !downloadDir.mkdirs()) { + Toast.makeText(context, + "Couldn't create download directory: " + downloadDir.getPath(), + Toast.LENGTH_SHORT) + .show(); + return false; + } + return true; + } + + private static void downloadSingleFile(final Context context, + final HostConnection httpHostConnection, + final HostInfo hostInfo, + final MediaInfo mediaInfo, + final int fileHandlingMode, + final DownloadManager downloadManager, + final Handler callbackHandler) { + Files.PrepareDownload action = new Files.PrepareDownload(mediaInfo.fileName); + action.execute(httpHostConnection, new ApiCallback() { + @Override + public void onSucess(FilesType.PrepareDownloadReturnType result) { + // If the file exists and it's to be overwritten, delete it, + // as the DownloadManager always creates a new name + if (fileHandlingMode == OVERWRITE_FILES) { + File file = new File(mediaInfo.getAbsoluteFilePath()); + if (file.exists()) { + file.delete(); + } + } + + // Ok, we got the path, invoke downloader + Uri uri = Uri.parse(hostInfo.getHttpURL() + "/" + result.path); + DownloadManager.Request request = new DownloadManager.Request(uri); + // http basic authorization + if ((hostInfo.getUsername() != null) && !hostInfo.getUsername().isEmpty() && + (hostInfo.getPassword() != null) && !hostInfo.getPassword().isEmpty()) { + final String token = Base64.encodeToString((hostInfo.getUsername() + ":" + + hostInfo.getPassword()).getBytes(), Base64.DEFAULT); + request.addRequestHeader("Authorization", "Basic " + token); + } + request.allowScanningByMediaScanner(); + request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI); + request.setTitle(mediaInfo.getDownloadTitle(context)); + request.setDescription(mediaInfo.getDownloadDescrition(context)); + + request.setDestinationInExternalPublicDir(mediaInfo.getExternalPublicDirType(), + mediaInfo.getRelativeFilePath()); + downloadManager.enqueue(request); + } + + @Override + public void onError(int errorCode, String description) { + Toast.makeText(context, + String.format(context.getString(R.string.error_getting_file_information), + mediaInfo.getDownloadTitle(context)), + Toast.LENGTH_SHORT) + .show(); + } + }, callbackHandler); + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/utils/JsonUtils.java b/app/src/main/java/com/syncedsynapse/kore2/utils/JsonUtils.java new file mode 100644 index 0000000..03acd28 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/utils/JsonUtils.java @@ -0,0 +1,106 @@ +/* + * Copyright 2015 Synced Synapse. 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.syncedsynapse.kore2.utils; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; + +import java.util.ArrayList; +import java.util.List; + +/** + * Misc util methods for use with JSON + */ +public class JsonUtils { + + public static String stringFromJsonNode(JsonNode node, String key) { + // Duplicate code for performance resons + if (node == null) return null; + JsonNode value = node.get(key); + if (value == null) return null; + return value.textValue(); + } + + public static String stringFromJsonNode(JsonNode node, String key, String defaultValue) { + if (node == null) return defaultValue; + JsonNode value = node.get(key); + if (value == null) return defaultValue; + return value.textValue(); + } + + public static double doubleFromJsonNode(JsonNode node, String key) { + return doubleFromJsonNode(node, key, 0); + } + + public static double doubleFromJsonNode(JsonNode node, String key, double defaultValue) { + if (node == null) return defaultValue; + JsonNode value = node.get(key); + if (value == null) return defaultValue; + return value.asDouble(); + } + + public static int intFromJsonNode(JsonNode node, String key) { + // Duplicate code for performance resons + if (node == null) return 0; + JsonNode value = node.get(key); + if (value == null) return 0; + return value.asInt(); + } + + public static int intFromJsonNode(JsonNode node, String key, int defaultValue) { + if (node == null) return defaultValue; + JsonNode value = node.get(key); + if (value == null) return defaultValue; + return value.asInt(); + } + + public static boolean booleanFromJsonNode(JsonNode node, String key) { + return booleanFromJsonNode(node, key, false); + } + + public static boolean booleanFromJsonNode(JsonNode node, String key, boolean defaultValue) { + if (node == null) return defaultValue; + JsonNode value = node.get(key); + if (value == null) return defaultValue; + return value.asBoolean(); + } + + public static List stringListFromJsonNode(JsonNode node, String key) { + if (node == null) return new ArrayList(0); + JsonNode value = node.get(key); + if (value == null) return new ArrayList(0); + + ArrayNode arrayNode = (ArrayNode)value; + ArrayList result = new ArrayList(arrayNode.size()); + for (JsonNode innerNode : arrayNode) { + result.add(innerNode.textValue()); + } + return result; + } + + public static List integerListFromJsonNode(JsonNode node, String key) { + if (node == null) return new ArrayList(0); + JsonNode value = node.get(key); + if (value == null) return new ArrayList(0); + + ArrayNode arrayNode = (ArrayNode)value; + ArrayList result = new ArrayList(arrayNode.size()); + for (JsonNode innerNode : arrayNode) { + result.add(innerNode.asInt()); + } + return result; + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/utils/LogUtils.java b/app/src/main/java/com/syncedsynapse/kore2/utils/LogUtils.java new file mode 100644 index 0000000..22e3409 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/utils/LogUtils.java @@ -0,0 +1,121 @@ +/* + * Copyright 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.syncedsynapse.kore2.utils; + +import com.syncedsynapse.kore2.BuildConfig; +import com.syncedsynapse.kore2.host.HostConnectionObserver; +import com.syncedsynapse.kore2.jsonrpc.HostConnection; + +import android.util.Log; + +import java.util.Arrays; +import java.util.List; + +/** + * Log utils shamelessly ripped from Google's iosched app... + */ +public class LogUtils { + private static final int MAX_LOG_TAG_LENGTH = 23; + + // TODO: Remove this later + private static final List doNotLogTags = Arrays.asList( + HostConnection.TAG, + HostConnectionObserver.TAG + ); + + public static String makeLogTag(String str) { + if (str.length() > MAX_LOG_TAG_LENGTH) { + return str.substring(0, MAX_LOG_TAG_LENGTH - 1); + } + return str; + } + + /** + * Don't use this when obfuscating class names! + */ + public static String makeLogTag(Class cls) { + return makeLogTag(cls.getSimpleName()); + } + + public static void LOGD(final String tag, String message) { + //noinspection PointlessBooleanExpression,ConstantConditions + if ((BuildConfig.DEBUG && !doNotLogTags.contains(tag)) || + Log.isLoggable(tag, Log.DEBUG)) { + Log.d(tag, message); + } + //Log.d(tag, message); + } + + public static void LOGD(final String tag, String message, Throwable cause) { + //noinspection PointlessBooleanExpression,ConstantConditions + if (BuildConfig.DEBUG || Log.isLoggable(tag, Log.DEBUG)) { + Log.d(tag, message, cause); + } + } + + public static void LOGD_FULL(final String tag, String message) { + if (BuildConfig.DEBUG || Log.isLoggable(tag, Log.DEBUG)) { + for (int i = 0; i < message.length(); i += 1024) { + if (i + 1024 < message.length()) + LOGD(tag, message.substring(i, i + 1024)); + else + LOGD(tag, message.substring(i, message.length())); + } + } + } + + public static void LOGV(final String tag, String message) { + //noinspection PointlessBooleanExpression,ConstantConditions + if (BuildConfig.DEBUG && Log.isLoggable(tag, Log.VERBOSE)) { + Log.v(tag, message); + } + } + + public static void LOGV(final String tag, String message, Throwable cause) { + //noinspection PointlessBooleanExpression,ConstantConditions + if (BuildConfig.DEBUG && Log.isLoggable(tag, Log.VERBOSE)) { + Log.v(tag, message, cause); + } + } + + public static void LOGI(final String tag, String message) { + Log.i(tag, message); + } + + public static void LOGI(final String tag, String message, Throwable cause) { + Log.i(tag, message, cause); + } + + public static void LOGW(final String tag, String message) { + Log.w(tag, message); + } + + public static void LOGW(final String tag, String message, Throwable cause) { + Log.w(tag, message, cause); + } + + public static void LOGE(final String tag, String message) { + Log.e(tag, message); + } + + public static void LOGE(final String tag, String message, Throwable cause) { + Log.e(tag, message, cause); + } + + private LogUtils() { + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/utils/NetUtils.java b/app/src/main/java/com/syncedsynapse/kore2/utils/NetUtils.java new file mode 100644 index 0000000..e5e73b7 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/utils/NetUtils.java @@ -0,0 +1,165 @@ +/* + * Copyright 2015 Synced Synapse. 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.syncedsynapse.kore2.utils; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** + * Various utilities related to networking + */ +public class NetUtils { + private static final String TAG = LogUtils.makeLogTag(NetUtils.class); + + /** + * Convert a IPv4 address from an integer to an InetAddress. + * @param hostAddress an int corresponding to the IPv4 address in network byte order + */ + public static InetAddress intToInetAddress(int hostAddress) { + if (hostAddress == 0) + return null; + + byte[] addressBytes = { (byte)(0xff & hostAddress), + (byte)(0xff & (hostAddress >> 8)), + (byte)(0xff & (hostAddress >> 16)), + (byte)(0xff & (hostAddress >> 24)) }; + + try { + return InetAddress.getByAddress(addressBytes); + } catch (UnknownHostException e) { + throw new AssertionError(); + } + } + + /** + * Tries to return the MAC address of a host on the same subnet by looking at the ARP cache.. + * Note: This is a synchronous call, so it should only be called on a background thread + * + * @param hostAddress Hostname or IP address + * @return MAC address if found or null + */ + public static String getMacAddress(String hostAddress) { + String ipHostAddress; + LogUtils.LOGD(TAG, "Starting get Mac Address for: " + hostAddress); + try { + InetAddress inet = InetAddress.getByName(hostAddress); + + // Send some traffic, with a timeout + boolean reachable = inet.isReachable(1000); + + ipHostAddress = inet.getHostAddress(); + } catch (UnknownHostException e) { + LogUtils.LOGD(TAG, "Got an UnknownHostException for host: " + hostAddress, e); + return null; + } catch (IOException e) { + LogUtils.LOGD(TAG, "Couldn't check reachability of host: " + hostAddress, e); + return null; + } + + try { + // Read the arp cache + BufferedReader br = new BufferedReader(new FileReader("/proc/net/arp")); + + String arpLine; + while ((arpLine = br.readLine()) != null) { + if (arpLine.startsWith(ipHostAddress)) { + // Ok, this is the line, get the MAC Address + br.close(); + return arpLine.split("\\s+")[3].toUpperCase(); // 4th element + } + } + br.close(); + } catch (IOException e) { + LogUtils.LOGD(TAG, "Couldn check ARP cache.", e); + } + return null; + } + + /** + * Sends a Wake On Lan magic packet to a host + * Note: This is a synchronous call, so it should only be called on a background thread + * + * @param macAddress MAC address + * @param hostAddress Hostname or IP address + * @param port Port for Wake On Lan + * @return Whether the packet was successfully sent + */ + public static boolean sendWolMagicPacket(String macAddress, String hostAddress, int port) { + if (macAddress == null) { + return false; + } + + // Get MAC adress bytes + byte[] macAddressBytes = new byte[6]; + String[] hex = macAddress.split("(\\:|\\-)"); + if (hex.length != 6) { + LogUtils.LOGD(TAG, "Send magic packet: got an invalid MAC address: " + macAddress); + return false; + } + + try { + for (int i = 0; i < 6; i++) { + macAddressBytes[i] = (byte)Integer.parseInt(hex[i], 16); + } + } + catch (NumberFormatException e) { + LogUtils.LOGD(TAG, "Send magic packet: got an invalid MAC address: " + macAddress); + return false; + } + + byte[] bytes = new byte[6 + 16 * macAddressBytes.length]; + for (int i = 0; i < 6; i++) { + bytes[i] = (byte)0xff; + } + for (int i = 6; i < bytes.length; i += macAddressBytes.length) { + System.arraycopy(macAddressBytes, 0, bytes, i, macAddressBytes.length); + } + + try { + InetAddress address = InetAddress.getByName(hostAddress); + DatagramPacket packet = new DatagramPacket(bytes, bytes.length, address, port); + DatagramSocket socket = new DatagramSocket(); + socket.send(packet); + socket.close(); + } catch (IOException e) { + LogUtils.LOGD(TAG, "Exception while sending magic packet.", e); + return false; + } + return true; + } + + private static byte[] getMacBytes(String macStr) throws IllegalArgumentException { + byte[] bytes = new byte[6]; + String[] hex = macStr.split("(\\:|\\-)"); + if (hex.length != 6) { + throw new IllegalArgumentException("Invalid MAC address."); + } + try { + for (int i = 0; i < 6; i++) { + bytes[i] = (byte) Integer.parseInt(hex[i], 16); + } + } + catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid hex digit in MAC address."); + } + return bytes; + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/utils/RepeatListener.java b/app/src/main/java/com/syncedsynapse/kore2/utils/RepeatListener.java new file mode 100644 index 0000000..ae3278f --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/utils/RepeatListener.java @@ -0,0 +1,134 @@ +/* + * Copyright 2015 Synced Synapse. 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.syncedsynapse.kore2.utils; + +import android.os.Handler; +import android.view.MotionEvent; +import android.view.SoundEffectConstants; +import android.view.View; +import android.view.animation.Animation; +import android.widget.Button; +import android.widget.ImageButton; + +/** + * A class, that can be used as a TouchListener on any view (e.g. a Button). + * It cyclically runs a clickListener, emulating keyboard-like behaviour. First + * click is fired immediately, next after initialInterval, and subsequent after + * repeatInterval. + * + *

Interval is scheduled after the onClick completes, so it has to run fast. + * If it runs slow, it does not generate skipped onClicks. + */ +public class RepeatListener implements View.OnTouchListener { + private static final String TAG = LogUtils.makeLogTag(RepeatListener.class); + + private static Handler repeatHandler = new Handler(); + + private int initialInterval; + private final int repeatInterval; + private final View.OnClickListener clickListener; + + private Runnable handlerRunnable = new Runnable() { + @Override + public void run() { + if (downView.isShown()) { + if (repeatInterval >= 0) { + repeatHandler.postDelayed(this, repeatInterval); + } + clickListener.onClick(downView); + } + } + }; + + /** + * Animations for down/up + */ + private Animation animDown; + private Animation animUp; + + private View downView; + + /** + * Constructor for a repeat listener + * + * @param initialInterval The interval after first click event + * @param repeatInterval The interval after second and subsequent click events + * @param clickListener The OnClickListener, that will be called periodically + */ + public RepeatListener(int initialInterval, int repeatInterval, View.OnClickListener clickListener) { + this(initialInterval, repeatInterval, clickListener, null, null); + } + + /** + * Constructor for a repeat listener, with animation + * + * @param initialInterval The interval after first click event. If negative, no repeat will occur + * @param repeatInterval The interval after second and subsequent click events. If negative, no repeat will occur + * @param clickListener The OnClickListener, that will be called periodically + * @param animDown Animation to play on touch + * @param animUp Animation to play on release + */ + public RepeatListener(int initialInterval, int repeatInterval, View.OnClickListener clickListener, + Animation animDown, Animation animUp) { + this.initialInterval = initialInterval; + this.repeatInterval = repeatInterval; + this.clickListener = clickListener; + + this.animDown = animDown; + this.animUp = animUp; + } + + /** + * Handle touch events. + * + * Note: For buttons, this event Handler returns false, so that the other event handlers + * of buttons get called. For other views this event Handler consumes the event + * @param view + * @param motionEvent + * @return + */ + public boolean onTouch(View view, MotionEvent motionEvent) { + switch (motionEvent.getAction()) { + case MotionEvent.ACTION_DOWN: + repeatHandler.removeCallbacks(handlerRunnable); + if (initialInterval >= 0) { + repeatHandler.postDelayed(handlerRunnable, initialInterval); + } + downView = view; + + if (animDown != null) { + animDown.setFillAfter(true); + view.startAnimation(animDown); + } + break; + case MotionEvent.ACTION_UP: + clickListener.onClick(view); + view.playSoundEffect(SoundEffectConstants.CLICK); + // Fallthrough + case MotionEvent.ACTION_CANCEL: + repeatHandler.removeCallbacks(handlerRunnable); + downView = null; + + if (animUp != null) { + view.startAnimation(animUp); + } + break; + } + // Consume the event for views other than buttons + return !((view instanceof Button) || (view instanceof ImageButton)); + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/syncedsynapse/kore2/utils/SelectionBuilder.java b/app/src/main/java/com/syncedsynapse/kore2/utils/SelectionBuilder.java new file mode 100644 index 0000000..50334ec --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/utils/SelectionBuilder.java @@ -0,0 +1,173 @@ +/* + * Copyright 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * Modifications: + * -Imported from AOSP frameworks/base/core/java/com/android/internal/content + * -Changed package name + */ + +package com.syncedsynapse.kore2.utils; + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.text.TextUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Helper for building selection clauses for {@link android.database.sqlite.SQLiteDatabase}. Each + * appended clause is combined using {@code AND}. This class is not + * thread safe. + */ +public class SelectionBuilder { + private static final String TAG = LogUtils.makeLogTag(SelectionBuilder.class); + + private String mTable = null; + private Map mProjectionMap = new HashMap(); + private StringBuilder mSelection = new StringBuilder(); + private ArrayList mSelectionArgs = new ArrayList(); + + /** + * Reset any internal state, allowing this builder to be recycled. + */ + public SelectionBuilder reset() { + mTable = null; + mSelection.setLength(0); + mSelectionArgs.clear(); + return this; + } + + /** + * Append the given selection clause to the internal state. Each clause is + * surrounded with parenthesis and combined using {@code AND}. + */ + public SelectionBuilder where(String selection, String... selectionArgs) { + if (TextUtils.isEmpty(selection)) { + if (selectionArgs != null && selectionArgs.length > 0) { + throw new IllegalArgumentException( + "Valid selection required when including arguments="); + } + + // Shortcut when clause is empty + return this; + } + + if (mSelection.length() > 0) { + mSelection.append(" AND "); + } + + mSelection.append("(").append(selection).append(")"); + if (selectionArgs != null) { + Collections.addAll(mSelectionArgs, selectionArgs); + } + + return this; + } + + public SelectionBuilder table(String table) { + mTable = table; + return this; + } + + private void assertTable() { + if (mTable == null) { + throw new IllegalStateException("Table not specified"); + } + } + + public SelectionBuilder mapToTable(String column, String table) { + mProjectionMap.put(column, table + "." + column); + return this; + } + + public SelectionBuilder map(String fromColumn, String toClause) { + mProjectionMap.put(fromColumn, toClause + " AS " + fromColumn); + return this; + } + + /** + * Return selection string for current internal state. + * + * @see #getSelectionArgs() + */ + public String getSelection() { + return mSelection.toString(); + } + + /** + * Return selection arguments for current internal state. + * + * @see #getSelection() + */ + public String[] getSelectionArgs() { + return mSelectionArgs.toArray(new String[mSelectionArgs.size()]); + } + + private void mapColumns(String[] columns) { + for (int i = 0; i < columns.length; i++) { + final String target = mProjectionMap.get(columns[i]); + if (target != null) { + columns[i] = target; + } + } + } + + @Override + public String toString() { + return "SelectionBuilder[table=" + mTable + ", selection=" + getSelection() + + ", selectionArgs=" + Arrays.toString(getSelectionArgs()) + "]"; + } + + /** + * Execute query using the current internal state as {@code WHERE} clause. + */ + public Cursor query(SQLiteDatabase db, String[] columns, String orderBy) { + return query(db, columns, null, null, orderBy, null); + } + + /** + * Execute query using the current internal state as {@code WHERE} clause. + */ + public Cursor query(SQLiteDatabase db, String[] columns, String groupBy, + String having, String orderBy, String limit) { + assertTable(); + if (columns != null) mapColumns(columns); + return db.query(mTable, columns, getSelection(), getSelectionArgs(), groupBy, having, + orderBy, limit); + } + + /** + * Execute update using the current internal state as {@code WHERE} clause. + */ + public int update(SQLiteDatabase db, ContentValues values) { + assertTable(); + return db.update(mTable, values, getSelection(), getSelectionArgs()); + } + + /** + * Execute delete using the current internal state as {@code WHERE} clause. + */ + public int delete(SQLiteDatabase db) { + assertTable(); + return db.delete(mTable, getSelection(), getSelectionArgs()); + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/utils/TabsAdapter.java b/app/src/main/java/com/syncedsynapse/kore2/utils/TabsAdapter.java new file mode 100644 index 0000000..5ba1415 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/utils/TabsAdapter.java @@ -0,0 +1,85 @@ +/* + * Copyright 2015 Synced Synapse. 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.syncedsynapse.kore2.utils; + +import android.content.Context; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentPagerAdapter; + +import java.util.ArrayList; + +/** + * This is a helper class that implements the management of tabs and all + * details of connecting a ViewPager with associated TabHost. + */ +public class TabsAdapter extends FragmentPagerAdapter { + private final Context context; + private final ArrayList tabInfos; + + public static final class TabInfo { + private final Class fragmentClass; + private final Bundle args; + private final int titleRes; + private final long fragmentId; + + TabInfo(Class fragmentClass, Bundle args, int titleRes, long fragmentId) { + this.fragmentClass = fragmentClass; + this.args = args; + this.titleRes = titleRes; + this.fragmentId = fragmentId; + } + } + + public TabsAdapter(Context context, FragmentManager fragmentManager) { + super(fragmentManager); + this.context = context; + this.tabInfos = new ArrayList(); + } + + public TabsAdapter addTab(Class fragmentClass, Bundle args, int titleRes, long fragmentId) { + TabInfo info = new TabInfo(fragmentClass, args, titleRes, fragmentId); + tabInfos.add(info); + return this; + } + + @Override + public int getCount() { + return tabInfos.size(); + } + + @Override + public Fragment getItem(int position) { + TabInfo info = tabInfos.get(position); + return Fragment.instantiate(context, info.fragmentClass.getName(), info.args); + } + + @Override + public long getItemId(int position) { + return tabInfos.get(position).fragmentId; + } + + @Override + public CharSequence getPageTitle(int position) { + TabInfo tabInfo = tabInfos.get(position); + if (tabInfo != null) { +// return context.getString(tabInfo.titleRes).toUpperCase(Locale.getDefault()); + return context.getString(tabInfo.titleRes); + } + return null; + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/utils/UIUtils.java b/app/src/main/java/com/syncedsynapse/kore2/utils/UIUtils.java new file mode 100644 index 0000000..f19e685 --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/utils/UIUtils.java @@ -0,0 +1,359 @@ +/* + * Copyright 2015 Synced Synapse. 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.syncedsynapse.kore2.utils; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.text.TextUtils; +import android.util.DisplayMetrics; +import android.view.LayoutInflater; +import android.view.View; +import android.view.WindowManager; +import android.widget.GridLayout; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; + +import com.syncedsynapse.kore2.R; +import com.syncedsynapse.kore2.Settings; +import com.syncedsynapse.kore2.host.HostInfo; +import com.syncedsynapse.kore2.host.HostManager; +import com.syncedsynapse.kore2.jsonrpc.type.GlobalType; +import com.syncedsynapse.kore2.jsonrpc.type.VideoType; + +import java.util.ArrayList; +import java.util.List; + +/** + * General UI Utils + */ +public class UIUtils { + + public static final float IMAGE_RESIZE_FACTOR = 1.0f; + + public static final int initialButtonRepeatInterval = 400; // ms + public static final int buttonRepeatInterval = 80; // ms + + /** + * Formats time based on seconds + * @param seconds seconds + * @return Formated string + */ + public static String formatTime(int seconds) { + return formatTime(seconds / 3600, (seconds % 3600) / 60, (seconds % 3600) % 60); + } + + /** + * Formats time + */ + public static String formatTime(GlobalType.Time time) { + return formatTime(time.hours, time.minutes, time.seconds); + } + + /** + * Formats time + */ + public static String formatTime(int hours, int minutes, int seconds) { + if (hours > 0) { + return String.format("%d:%02d:%02d", hours, minutes, seconds); + } else { + return String.format("%1d:%02d",minutes, seconds); + } + } + + /** + * Loads an image into an imageview + * @param hostManager Hostmanager connected to the host + * @param imageUrl XBMC url of the image to load + * @param imageView Image view to load into + * @param imageWidth Width of the image, for caching purposes + * @param imageHeight Height of the image, for caching purposes + */ + public static void loadImageIntoImageview(HostManager hostManager, + String imageUrl, ImageView imageView, + int imageWidth, int imageHeight) { +// if (TextUtils.isEmpty(imageUrl)) { +// imageView.setImageResource(R.drawable.delete_ic_action_picture); +// return; +// } + + if ((imageWidth) > 0 && (imageHeight > 0)) { + hostManager.getPicasso() + .load(hostManager.getHostInfo().getImageUrl(imageUrl)) + .resize(imageWidth, imageHeight) + .centerCrop() + .into(imageView); + } else { + hostManager.getPicasso() + .load(hostManager.getHostInfo().getImageUrl(imageUrl)) + .fit() + .centerCrop() + .into(imageView); + } + } + + private static TypedArray characterAvatarColors = null; + private static int avatarColorsIdx = 0; +// private static Random randomGenerator = new Random(); + + /** + * Loads an image into an imageview, presenting an alternate charater avatar if empty + * @param hostManager Hostmanager connected to the host + * @param imageUrl XBMC url of the image to load + * @param stringAvatar Character avatar too present if image is null + * @param imageView Image view to load into + * @param imageWidth Width of the image, for caching purposes + * @param imageHeight Height of the image, for caching purposes + */ + public static void loadImageWithCharacterAvatar( + Context context, HostManager hostManager, + String imageUrl, String stringAvatar, + ImageView imageView, + int imageWidth, int imageHeight) { + // Load character avatar + if (characterAvatarColors == null) { + characterAvatarColors = context.getResources() + .obtainTypedArray(R.array.character_avatar_colors); + } + + char charAvatar = TextUtils.isEmpty(stringAvatar) ? + ' ' : stringAvatar.charAt(0); + avatarColorsIdx = Math.max( + Character.getNumericValue(stringAvatar.charAt(0)) + + Character.getNumericValue(stringAvatar.charAt(stringAvatar.length() - 1)) + + stringAvatar.length(), 0) % + characterAvatarColors.length(); + int color = characterAvatarColors.getColor(avatarColorsIdx, 0xff000000); + CharacterDrawable avatarDrawable = new CharacterDrawable(charAvatar, color); + +// avatarColorsIdx = randomGenerator.nextInt(characterAvatarColors.length()); + if (TextUtils.isEmpty(imageUrl)) { + imageView.setImageDrawable(avatarDrawable); + return; + } + + if ((imageWidth) > 0 && (imageHeight > 0)) { + hostManager.getPicasso() + .load(hostManager.getHostInfo().getImageUrl(imageUrl)) + .placeholder(avatarDrawable) + .resize(imageWidth, imageHeight) + .centerCrop() + .into(imageView); + } else { + hostManager.getPicasso() + .load(hostManager.getHostInfo().getImageUrl(imageUrl)) + .fit() + .centerCrop() + .into(imageView); + } + } + + /** + * Sets play/pause button icon on a ImageView, based on speed + * @param context Activity + * @param view ImageView/ImageButton + * @param speed Current player speed + */ + public static void setPlayPauseButtonIcon(Context context, ImageView view, int speed) { + int resAttrId = (speed == 1) ? R.attr.iconPause : R.attr.iconPlay; + int defaultResourceId = (speed == 1) ? + R.drawable.ic_pause_white_24dp : + R.drawable.ic_play_arrow_white_24dp; + + TypedArray styledAttributes = context.obtainStyledAttributes(new int[]{resAttrId}); + view.setImageResource(styledAttributes.getResourceId(0, defaultResourceId)); + styledAttributes.recycle(); + } + + /** + * Fills the standard cast info list, consisting of a {@link android.widget.GridLayout} + * with actor images and a Textview with the name and the role of the additional cast. + * The number of actor presented on the {@link android.widget.GridLayout} is controlled + * through the global setting, and only actors with images are presented. + * The rest are presented in the additionalCastView TextView + * + * @param context Activity + * @param castList Cast list + * @param castListView GridLayout on which too show actors that have images + * @param additionalCastTitleView View with additional cast title + * @param additionalCastView Additional cast + */ + public static void setupCastInfo(final Context context, + List castList, GridLayout castListView, + TextView additionalCastTitleView, TextView additionalCastView) { + HostManager hostManager = HostManager.getInstance(context); + Resources resources = context.getResources(); + DisplayMetrics displayMetrics = new DisplayMetrics(); + WindowManager windowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); + windowManager.getDefaultDisplay().getMetrics(displayMetrics); + + View.OnClickListener castListClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + Utils.openImdbForPerson(context, (String)v.getTag()); + } + }; + + + castListView.removeAllViews(); + int numColumns = castListView.getColumnCount(); + + int layoutMarginPx = 2 * resources.getDimensionPixelSize(R.dimen.remote_content_hmargin); + int imageMarginPx = 2 * resources.getDimensionPixelSize(R.dimen.image_grid_margin); + int imageWidth = (displayMetrics.widthPixels - layoutMarginPx - numColumns * imageMarginPx) / numColumns; + int imageHeight = (int)(imageWidth * 1.2); + + List noPicturesCastList = new ArrayList(); + int maxCastPictures = Settings.getInstance(context).maxCastPictures; + int currentPictureNumber = 0; + for (int i = 0; i < castList.size(); i++) { + VideoType.Cast actor = castList.get(i); + + if (((maxCastPictures == -1) || (currentPictureNumber < maxCastPictures)) && + (actor.thumbnail != null)) { + // Present the picture + currentPictureNumber++; + View castView = LayoutInflater.from(context).inflate(R.layout.grid_item_cast, castListView, false); + ImageView castPicture = (ImageView) castView.findViewById(R.id.picture); + TextView castName = (TextView) castView.findViewById(R.id.name); + TextView castRole = (TextView) castView.findViewById(R.id.role); + + castView.getLayoutParams().width = imageWidth; + castView.getLayoutParams().height = (int) (imageHeight * 1.2); + castView.setTag(actor.name); + castView.setOnClickListener(castListClickListener); + + castName.setText(actor.name); + castRole.setText(actor.role); + UIUtils.loadImageWithCharacterAvatar(context, hostManager, + actor.thumbnail, actor.name, + castPicture, imageWidth, imageHeight); + castListView.addView(castView); + } else { + noPicturesCastList.add(actor); + } + } + + // Additional cast + if (noPicturesCastList.size() > 0) { + additionalCastTitleView.setVisibility(View.VISIBLE); + additionalCastView.setVisibility(View.VISIBLE); + StringBuilder castListText = new StringBuilder(); + boolean first = true; + for (VideoType.Cast cast : noPicturesCastList) { + if (!first) castListText.append("\n"); + first = false; + if (!TextUtils.isEmpty(cast.role)) { + castListText.append(String.format(context.getString(R.string.cast_list_text), + cast.name, cast.role)); + } else { + castListText.append(cast.name); + } + } + additionalCastView.setText(castListText); + } else { + additionalCastTitleView.setVisibility(View.GONE); + additionalCastView.setVisibility(View.GONE); + } + } + + /** + * Simple wrapper to {@link NetUtils#sendWolMagicPacket(String, String, int)} + * that sends a WoL magic packet in a new thread + * + * @param context Context + * @param hostInfo Host to send WoL + */ + public static void sendWolAsync(Context context, final HostInfo hostInfo) { + if (hostInfo == null) + return; + + // Send WoL magic packet on a new thread + new Thread(new Runnable() { + @Override + public void run() { + NetUtils.sendWolMagicPacket(hostInfo.getMacAddress(), + hostInfo.getAddress(), hostInfo.getWolPort()); + } + }).start(); + Toast.makeText(context, R.string.wol_sent, Toast.LENGTH_SHORT).show(); + } + +// /** +// * Sets the default {@link android.support.v4.widget.SwipeRefreshLayout} color scheme +// * @param swipeRefreshLayout layout +// */ +// public static void setSwipeRefreshLayoutColorScheme(SwipeRefreshLayout swipeRefreshLayout) { +// Resources.Theme theme = swipeRefreshLayout.getContext().getTheme(); +// TypedArray styledAttributes = theme.obtainStyledAttributes(new int[] { +// R.attr.refreshColor1, +// R.attr.refreshColor2, +// R.attr.refreshColor3, +// R.attr.refreshColor4, +// }); +// +// swipeRefreshLayout.setColorScheme(styledAttributes.getResourceId(0, android.R.color.holo_blue_dark), +// styledAttributes.getResourceId(1, android.R.color.holo_purple), +// styledAttributes.getResourceId(2, android.R.color.holo_red_dark), +// styledAttributes.getResourceId(3, android.R.color.holo_green_dark)); +// styledAttributes.recycle(); +// } + +// /** +// * Sets a views padding top/bottom to account for the system bars +// * (Top status and action bar, bottom nav bar, right nav bar if in ladscape mode) +// * +// * @param context Context +// * @param view View to pad +// * @param padTop Whether to set views paddingTop +// * @param padRight Whether to set views paddingRight (for nav bar in landscape mode) +// * @param padBottom Whether to set views paddingBottom +// */ +// public static void setPaddingForSystemBars(Activity context, View view, +// boolean padTop, boolean padRight, boolean padBottom) { +// //if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return; +// SystemBarTintManager tintManager = new SystemBarTintManager(context); +// SystemBarTintManager.SystemBarConfig config = tintManager.getConfig(); +// +// view.setPadding(view.getPaddingLeft(), +// padTop ? config.getPixelInsetTop(true) : view.getPaddingTop(), +// padRight? config.getPixelInsetRight() : view.getPaddingRight(), +// padBottom ? config.getPixelInsetBottom() : view.getPaddingBottom()); +// } + + /** + * Returns a theme resource Id given the value stored in Shared Preferences + * @param prefThemeValue Shared Preferences value for the theme + * @return Android resource id of the theme + */ + public static int getThemeResourceId(String prefThemeValue) { + switch (Integer.valueOf(prefThemeValue)) { + case 0: + return R.style.NightTheme; + case 1: + return R.style.DayTheme; + case 2: + return R.style.MistTheme; + case 3: + return R.style.SolarizedLightTheme; + case 4: + return R.style.SolarizedDarkTheme; + default: + return R.style.NightTheme; + } + } +} diff --git a/app/src/main/java/com/syncedsynapse/kore2/utils/Utils.java b/app/src/main/java/com/syncedsynapse/kore2/utils/Utils.java new file mode 100644 index 0000000..ff2977d --- /dev/null +++ b/app/src/main/java/com/syncedsynapse/kore2/utils/Utils.java @@ -0,0 +1,134 @@ +/* + * Copyright 2015 Synced Synapse. 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.syncedsynapse.kore2.utils; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.text.TextUtils; + +import java.util.List; + +/** + * Because every project needs one of these + * */ +public class Utils { + + /** + * Returns whether the SDK is the Jellybean release or later. + */ + public static boolean isJellybeanOrLater() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; + } + + public static boolean isJellybeanMR1OrLater() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1; + } + + public static boolean isJellybeanMR2OrLater() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2; + } + + public static boolean isKitKatOrLater() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; + } + + public static boolean isLollipopOrLater() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; + } + + /** + * Concats a list of strings... + * @param list + * @param delimiter + * @return + */ + public static String listStringConcat(List list, String delimiter) { + StringBuilder builder = new StringBuilder(); + boolean first = true; + for (String item : list) { + if (TextUtils.isEmpty(item)) continue; + if (!first) builder.append(delimiter); + builder.append(item); + first = false; + } + return builder.toString(); + } + + + /** + * Calls {@link Context#startActivity(Intent)} with the given implicit {@link Intent} + * after making sure there is an Activity to handle it. + *

This may happen if e.g. the web browser has been disabled through restricted + * profiles. + * + * @return Whether there was an Activity to handle the given {@link Intent}. + */ + public static boolean tryStartActivity(Context context, Intent intent) { + if (intent.resolveActivity(context.getPackageManager()) != null) { + context.startActivity(intent); + return true; + } + return false; + } + + public static final String IMDB_APP_PERSON_SEARCH_URI = "imdb:///find?q=%s&s=nm"; + public static final String IMDB_PERSON_SEARCH_URL = "http://m.imdb.com/find?q=%s&s=nm"; + + public static final String IMDB_APP_MOVIE_URI = "imdb:///title/%s/"; + public static final String IMDB_MOVIE_URL = "http://m.imdb.com/title/%s/"; + + /** + * Open the IMDb app or web page for the given person name. + */ + public static void openImdbForPerson(Context context, String name) { + if (context == null || TextUtils.isEmpty(name)) { + return; + } + + Intent intent = new Intent(Intent.ACTION_VIEW, + Uri.parse(String.format(IMDB_APP_PERSON_SEARCH_URI, name))); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); + // try launching IMDb app + if (!Utils.tryStartActivity(context, intent)) { + // on failure, try launching the web page + intent = new Intent(Intent.ACTION_VIEW, Uri.parse(String.format(IMDB_PERSON_SEARCH_URL, name))); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); + context.startActivity(intent); + } + } + + /** + * Open the IMDb app or web page for the given person name. + */ + public static void openImdbForMovie(Context context, String imdbNumber) { + if (context == null || TextUtils.isEmpty(imdbNumber)) { + return; + } + + Intent intent = new Intent(Intent.ACTION_VIEW, + Uri.parse(String.format(IMDB_APP_MOVIE_URI, imdbNumber))); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); + // try launching IMDb app + if (!Utils.tryStartActivity(context, intent)) { + // on failure, try launching the web page + intent = new Intent(Intent.ACTION_VIEW, Uri.parse(String.format(IMDB_MOVIE_URL, imdbNumber))); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); + context.startActivity(intent); + } + } +} diff --git a/app/src/main/res/anim/activity_in.xml b/app/src/main/res/anim/activity_in.xml new file mode 100644 index 0000000..5ff440b --- /dev/null +++ b/app/src/main/res/anim/activity_in.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/activity_out.xml b/app/src/main/res/anim/activity_out.xml new file mode 100644 index 0000000..82a28ce --- /dev/null +++ b/app/src/main/res/anim/activity_out.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/button_in.xml b/app/src/main/res/anim/button_in.xml new file mode 100644 index 0000000..0a5e9eb --- /dev/null +++ b/app/src/main/res/anim/button_in.xml @@ -0,0 +1,33 @@ + + + + + + + + + diff --git a/app/src/main/res/anim/button_out.xml b/app/src/main/res/anim/button_out.xml new file mode 100644 index 0000000..1ac0757 --- /dev/null +++ b/app/src/main/res/anim/button_out.xml @@ -0,0 +1,33 @@ + + + + + + + + + diff --git a/app/src/main/res/anim/fragment_details_enter.xml b/app/src/main/res/anim/fragment_details_enter.xml new file mode 100644 index 0000000..5797275 --- /dev/null +++ b/app/src/main/res/anim/fragment_details_enter.xml @@ -0,0 +1,30 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/fragment_list_popenter.xml b/app/src/main/res/anim/fragment_list_popenter.xml new file mode 100644 index 0000000..63d02e8 --- /dev/null +++ b/app/src/main/res/anim/fragment_list_popenter.xml @@ -0,0 +1,25 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-sw600dp-xxhdpi/remote_back_black.png b/app/src/main/res/drawable-sw600dp-xxhdpi/remote_back_black.png new file mode 100644 index 0000000000000000000000000000000000000000..7df8e7e5ee2973fa050d7472814f1c1375f5ba60 GIT binary patch literal 4242 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 UqL?0Ul(+-Z<*b`ytphUUzgX{g2LJ#7 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-sw600dp-xxhdpi/remote_back_white.png b/app/src/main/res/drawable-sw600dp-xxhdpi/remote_back_white.png new file mode 100644 index 0000000000000000000000000000000000000000..d2ad60bda4d89e5fb112dde4f2569b8ed11fa4fe GIT binary patch literal 3673 zcmdT{`Bzg(7JdN%*|f2>A&rm`9-x9Ojj{@itVRepwvT`aqzOv1BPffAr0oHM0=6#= zgk8rL+OQ-LBWoaS2QpzX?I4nbB_a`&1nsbdB_b2%xA_lds?I%i>brI8+4wJ zgy-R^OeLss6^-%HnAZ71$IS5Syy87d-5lEc)cH)|;#4jz@lrTP_OZ)jCRMEk_vxy^ zUrwK@XYckwA1VC%Df4fA)t}|BtETPluP*$Xig}EdN=#fe8_oVpRa5@I?2g_+!N27l z{sEb+`Kv7}&-&J`!_U;*w`RKREly@o8EXOsjpt4s@5`XZG}CLdv*_|rNhqZB=>MO92;LgHk%i3r;HS+gj>N|0G-0QfsZn5eVh;&LS%{a^6J z#ky$6m;;1I`>Ba7n6^7~ZPxLANhA8He0{PM{mN+w2=MB$#$>5O2(Pg zG>#L>Q56Wl^vo4)PZ;5+s-rdR5v*Tkuc1Ym3`J%xCv(kWLz9kp7&^0`c5oJg>RKqH z`IA?3M7j2hx?gjy-AD+BnjK7mfQ7;MmLXEpVryOG!WGvoRkYmJ5(sdKV49SPUpoYb zJ3Phy2-wOp6PG(d$-IC!gZ?0K9zcUOSzXv_= z>3~7ZPPi`I>}>CZ^VpC=RYEH#{<#5($Zv;xggOY@dzqNSQ9))|RY(=wKV+5mb4T1F zOaI0AJcGWxo8VK3bxk6PZ>g~*KY)H!hk&n+MWhXQ*m7sRO15vnO0!brEm^Ame!BRV zuamNV9ilAs)W<%RfBHm%R{J2vceNhf9b3!4Q{oCJ!gEk$BPG77K5O_TVEaxh7J~0w zk3eq4z%>O2w;Luz_>uWYxG7Xzmr=f9T(6Fko5}^CtNdU)I#obuHAc7bO!6i8MWbRz zP%t&DO61E2ZiK-xE=hMiqMvz;cgZolyCd{CA`wC4-z{Dh8sAxVzbC%0xn&hks;&?} zD0xKn*1;itr3)aH98=V8E||nCvSF#i+^pzn^%&EuNpv_07_yD3f~#Lx4=aYy?jG#L zrQEV#kfV0g)2W_tw{PZj02?TaW&5_C;D!yPRAUs~Pm8rr)~1cP5QwYH%?k|HtfCVf zFP;f84rr@GYJAaZona%EZ-52n8xwZMypQBqf}ohMhQHlk22xQnr#%5n^Z1hY?IdYp z8RD{IlqIRUO5Cr;j!J_~efewdXzS$h(+AtrhOJpky9I@d&`VkpN?t()yW6!jwo}+n zZlkh!(G#34Yw0$>7TqF6-iw)_qQ-UYK3k*Rg)J}b7BEQEyDR=%6_>xnW#vongLS7< zMd?W|J8)!?4P(f?h9vGb&6jk7eRa|EAZlS<8jf}#`v7(mow~wPzqiQUV+0Kv`hRve z_4zN67Be+gLNvwa>FTUsUxbZWE3WcXo-vMauS^85e4p&l*@Lf|m=i zhl_vhs7=6Dh-S?2eUCU8)W>hM0i7>f>ciiym8nf{-n;}e=a9J^W6QM3w(goTUETj72Fib4Or)Ln#f^|u6h z&Udp*H6T^+yth&=-#Aqplv-4Qz@6C|YFsdc7_G6DBbQOQjf;ihHPNEU7S3_CiJQJncX{Ta&Tm?&iTY~B zmsYo2Ne35`0ak)ffGh4A8m#YnwrL&*Mi#{?Q z%sXfo^DWw&?>ULD+&s*EWjCF<)PPQ3X$NpwdC4z{Zk6KKht?S1!j|S&*TgY!(}{ob}I64ZS5UV;BWc!hZFx_7z_UCUM>yD)`k{{DpetpM*S+dMA{o0+g| zwaK14(uNZESa+sY|ctBssb!RGmCRU{Yfe0zVkc7o%%0urr&_q(U6` z7RFHxmQ=lor(h=;uRs^!8q#E2h%9bx!_qCuWupVB9x|<$9mA2M3``)BkjNM}p>L5skY?PVY7CMYQ=QkGXM zIw7aS6t{iyo#j6@y`{NuPdZy!fmz>Z@y(3voO14!;B#P0b!K5_OplUWD|SiWAoMb= zxmERIr-H(9hnTAGd@pV2lW*?byG}2KR@c#1ZSpqtqw_dB&19>(UbS zJn|McqWrp=HcP6J&{tws<6z-Tn-7t2kb7h*xU*oe`G%+8{v@ev->@k>SuS3>GRZg` z0jw1E^DG2Hy@KN9=AswwwmHEtYIs9>%fwu%(;Zj$o-6#2d1Vf2E-dh z`|Xrl@>8UB;vb5}&+3H@XPSk!{sFBb$hAbKDR*yV3GtY34=k+^KS?+H#Br1A*oNsT zR^>hUu&$)rSl9^eImWe6@5xnGgv%fflU=$bCBI>qaqaHtQRV#Hqg-cnOFVX8 zt8_o>8*PEj)sv`KoIc&_`3VPQXO;%RkIHic7wD%nnW)=(=}owDPnfb_D;AHgxfVG` zl!JoKtqXs`U?gf8Tl1e4@$kXX&m1N%QJzbV=9Sp_6^^gLlfvG*1lYTXLQ!%T`7wDB z3vq(S{su9Uq2|XF3(7(XrK^>a6k9nbXvrM=Q+wI^X%%H*3Nv~#?iIpdbuhkliX>h} zmpI^CSU~;ipyVE zbBAC@1~ya_TWNlI7eclENW@z3STnRbM(Jv%IGn)H$8-SOb3I?Z&o6F-*Xu3~JU_#G zJ=jPcfv)93ZXuA8Ro$R#sSuNi?~cE5VmE8(+S^rpyrH?^r|54v=~X1Dq)FBGfGm!) z*>BvKbdePO$bK)&Q#F0i09(2paCb52nlcJMR`-y~dgo{8MFA~Q|=sj1;5Ftgx zaN)dV9~dL=*xVG}FTbS1>M)Vd)yghZi8qZA3ZnhagYp9n47$daRDcDpVF5y|2EgE% zCdBBSwO8ggsX!IS{jf3Np6xXPYxN>|cK zNB7N@117r-Y!6@~yP>f{H}~t|qsN1s@Ji^|^jSz_ZGBR|=IAaRvbkbyeYm}G!MZ_< z@c2a<;x*4Kw)z-Qc7Z+p-U>l`X(B%h=Tsls0X~}-iXN-d#3t#t=_;&qcldLlhLb5^ p#UL7u&O=uJfB%C2U5|?vwt-~#>Qx#kMRmtZv`?USizn{ZzX8OPIDP;C literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-sw600dp-xxhdpi/remote_codec_black.png b/app/src/main/res/drawable-sw600dp-xxhdpi/remote_codec_black.png new file mode 100644 index 0000000000000000000000000000000000000000..f765cb912853dbf31365b3d0027db9d682f839f2 GIT binary patch literal 5153 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 ezo%naZEE;VTw6cbhQGY+{W(YYnHq;DCCpJR>y literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-sw600dp-xxhdpi/remote_codec_white.png b/app/src/main/res/drawable-sw600dp-xxhdpi/remote_codec_white.png new file mode 100644 index 0000000000000000000000000000000000000000..924d987ea3bdd6b4a7b55014130b28cafe368639 GIT binary patch literal 4061 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 bJIdW<%-n$f$rg{s2$-h(GI!PPKzRQHjPCH8 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-sw600dp-xxhdpi/remote_info_black.png b/app/src/main/res/drawable-sw600dp-xxhdpi/remote_info_black.png new file mode 100644 index 0000000000000000000000000000000000000000..2afd3566187a7a0653d649b5b2ad11a8477f72bc GIT binary patch literal 3109 zcmeHJ`&&}!7FN8LqL$^2w9=DJQz=f06;Ykkn#R~-+DIf zXy_sMN|%*JMn>?%KL#E%GBRc_4@+~%^6yaO_t0cc2tE{Ow6uKR;XS+tSyp8H7)^q9 zNS253f$u5-$Y^oya7d7a%-YV{b^G%>_lZVER!7)k9(;;oVN~a_dCqrT~=-j z3~%Y^^CGs|=ARkAOj5Ra4mQGs{ z-m6SZORtwnF0Jb9Fq|OEf-R0{?^IkkS&9W;Xh`5LZ&&0M6CCXPE?iB+?U_Mi7!@#v znJdgJofMmJ-LUqOBAyG&19aVymyWN=bjIZR;bz0LRC|5)cSmu61@U--KiUK)dfBse zIZ#AdMTs%=CJGip&`vAK@j{!(-c*MndF;V~?|V`N{zM08V>5t>S!fM0f!M5JF<6lg zQ{5oz1E4xJ%HJA#x&=f~I&=CxVUWkmSkNT;>uQM7Hk@}j{r!;(7dSuZ7^8!8*HCLW z;2Vx{`?0p=GXJsZ#k?DY!sf))#lWNM&ucVqSqmNX9@c8b-JH~5SD#%W*4_up|=G) zMR|*ShFV=(QS!`%WJ4_29k-w!=ch}@07_j)T}O&|O38z(yH#Zp^I#4HGt9lE0jOrm z(9&QjF-+G4_qg9;&$9BEIiSD)%9c5>QMfQ!!2EVFy>l(A7DS8oeBx*d9M-VP)C<49 zvVnEhGMbkyR<76d4p3;0pH~*!-2(3^BJ^)j{HF`y0`OvAiR7xLJeFV!r8~s= zss}ik(50ejI)g7uFkmsM#tBNl-|4`|%*X4e$+o`h24fF$QTcBE=%Nc7amRx`w4nI5 zx;g4cL|-Y^-5f%i^X;ON{Bb-tO?6dM+-c{%s%UM(YKu2O+MkPcWnVb+NyZ^#AjG_41b1yVk;EoX!k=#IQ(^859CwZ#1qoX5t&A+hU z4hXK(I$fbkHcqGsB;KgJBp*?!`TdcO{`p>tC(2FB3CtEI(j-A(F{4j3?}cmbRs7eQ zEG+jQZt|PFS%+YtnI`Y^7EeQ^31wAKLMiw&|HRb#AiTFoOVfN=k-65#4moqTj}iCC z>s^tqgKv04t*n=OLe4Yp#xQF}YIV}DT?4*i{2J&(U~G1x2;^YavEUGYb5>i6VgGdF zzE&|ngS4A3sbQu?Uj<&-bNnOeG^cG9@m&*7gIzoaSjV`iJyDd7r8w#7s;1+6%-I&A z%h|{EU5TBuDGwF;0gx9`AeZ{)66g^HgUAa`(7H=|4qd2?6}*zDRbQP&lQf#+kV!%r z29v|De-NdrW@obF&6PXAEY;UtINoMY93)43kXO%>5#7;Z7phshKS9mdXYXx?3?$xa zf;+C_6eAqurjW|(I3rHt&XMIKyO%x`+M_Le{Y|n=VB2oOn{7QmOi^$J$1%r7hE*vL z3ri=lE9Y=9hdApwwgZk`>4hd#M6y`2qbIYTk@!hX0)p%3)ohFHzdRS62Gg;Uu&;&+ zMp)TBf35s?GW^<%9Cy-3mHCj#idZ^^0g2lB<*-*I4Xe;67jR_4OMZ1FqZ3hACRPA~ z_Q!v4^YMT9kN!drd{8T#Up+O++f%*c?<={Tqt#MyNfiU(_Vj zVtRtI1xfTzh~-MwocJ*GDA@xQ!i73V@=XTc9w%oHsg=8`EMX$)K_Xh0Ll)j{SR^0d zdNJ~O9hBmNPY8sUTV}k4rBjL%CrGG$UV=f#)7AH=SNv4b|D1lIc}?AFGk|Is=7JG? z3kHJ0Dql>6+Jon+m{Z(3pLFgb&rn3|*}<1(M18)i>8mX{+0-NHsvD)MHxu*ikZ(N* zZ%V7BlK>`;^29)>3>(?`Cc|U;_3PVjcVgdJL8pS^j!K^RBdu3Es+q0ab6GQ=G^Ggv zug6SWqM+5!_jluqmHG!7@5G^m-PnaYhL79bNOhz0&g@G?I9NTkZ)hJXfxGy~piPV$ z{&MSQp{Tv3N0tJWb;^8JMpjc9yMxuyI@jOF8%FK{s%l}-${PTyx7s1ncByKrGGC0R zRs}deg~QD4V`C^uG#w*8?~fGEiM!5Q&0yvUu4y;FTas=PX)^?WGc;D6)drM8><2~$ zk*o5fbyA?;KMWAl68T6|Q|lR;M@WH8yQm>ql}Jv5tWaH>5@l=YR7wiai?+DU;-}+M zfx7~@T3Q|g3|qlTYmAMHLPk(MQ%lD|dCP(z+k$E9JAz51M{Z7avqKJvH-TZHfH)Y8 zi`X};BDvPfVn%lLBn=dA<2~n92S>9N`V9TM(sGz_(Uz))&~)8gipyK`N`3lA3Z!8SSaXKB#?tA? zM~hURLZ=7IcI5E`;C-CD3@R&|Yp7kZ(advx^jSVB|7o#bnvJ56O4HGOvmcDr!kIhy zXQ=9edxr4${9bEFzmK>fG=lFXc>woE7c`Aly-U2B8?28B>Dpodb%p>P1Rl5CG~@s3gY>kAV}dNfE52E25zTs#};?Poo~YaCUn zs=qW=O+!x1(9x9EoI_|*pyIWLj);slH??-qv{QXTTfKG?;_4Tlj1D_*z_UGWyYWT_ zGctS6S26ZtR^D8JUsil!_g?E+d#!ix zopIFP$8y=aWdHzJ`hH6}4gjcfvwX1_L7M$tzDCsI(8E3yV8Psfu6>w|AWNgZJr#pk zD9wWM`d0xL0hh%3`gt$u!&%|%iN_tE%_H0Ld?^P`aNmuq&lksX?B%+c6q?JI)ahZRq z=_s9{e!B@R|GGg7;4#eMfsLU+Nra4oK)MR-4_aYpL6u+7 zB?tnR13EM^_RAnFzt#%WG;7egDh#sr97;x9T#)sS;pDFx4@1DoZf=GR;B31M@gFwG zk*7UC%Owp`4K5qSga-`s8kN{cT9FMnC?1k=m@$VNn#Ke zm#51D{P#Ii>dl|OUsqpq}IO4!g(1aMH| z=*R2##)_0_RhLDJm>g0~HP^3;`}^$~^-<67WPpuz#VBGv86pj&4PlBDb-?jzZ7-Npv( zN+jh1<$nbyZWfn`qkB&s7Uh0|0El&(B6;HHKB3WkylRg4q=H}g;yqXWXzq{dvnxW8 z>N4BV!S51sv-5VE}=297%P$OmNjpp~Sno>$V;?_cD>?m|d&8Disxz^im z44Kf(ki_+&_t|U87#!bxPKfm z1TGS;1xj9mUS?JnG`*)HOx!{l%f#4PVaoUFo+lgBRH5(zz1uLM}-2F zPXl&8w7jt|y(iNKAjY}u8d>cM)Yw%NW-6CR*+Ls&H@>1XZfY;sGV&V@VRoyimq;F= zCqROT&OSJXp4#p^td|;yzwVmRvtES$w&oWiF+xkLD?R=Msk0vdVQ* zC7A!!BTN+KSu^))&DQm^UYwN9#a5uJtzML&l1h?EUC)CJ6_E@JUNXm&r$?24!o*GZ zo6i;A$eJqE5@7KfuKU6ZiNep3`5064Ci3BkK$XWG2N~-2kNY{c3uvHgUubZq^0wec zh7u1a_MBoHV~FwupNL@Hb8o1ffCukOZ&vF~fu5bysIDEM*bJrJf~8LYc|x7(EEP(X zG_n2cxlwUYyjwEgkg1YDS`6$PSf@*lhIAmA$2VlC&O2LyQmIe-$@mn`+bmAmBYzhj z@Dm=qNo#7WFy;PGGK0$om_=Os$7$TQWJj6-BCzU|I{_Qp9EEg62Lap_sbf@IOZ(ZC z6G|!Rhz0-Dfo@uDSUb12#v@iZy1Q;})`_+7)T8Shkhl8w=w@d|w!Uyr_cz>pF}0}x z2NX06pwF4E>*6=%{&c;KFm{oauI3jNC#TUB# zJNgn=8aaPC(CHO@&H(?YOVFko&+G|_R_Hee?AQQ2Xc(H-G~!*Kox^u%^FPOa6CU{f z3~f`s70{U)trO%WNuLj*+aH4ogU#&3wKQAg(JHkVHo`OE*M`ni8Oe6MvV)S`tSn`} zbjg1ZqkGZ)8upIR=wqB_^njt>ed{XTXk5oqdBT!0SJ-by-O|F!Auc^ot&Rm$o3{q! z;j_WW(fS)(AgvS4q5kvd)4vr(sIrtPl@%42)Io!8I0ZI6g!{?vdM(E$J%E4(s)#Wc zIBKVU!QSd$*bJ(pRJ3z7(0FYG(!>`~hF@ul-Icwx+K6^8J-p)}Fp|+`(oFf)N1ru( zbnn_7A^Id+{xk_clZPswAsRmuuFM|b)C)Kw`)7*AHu?qz5aGt0DZcDR+Sy;?pCA4C z5T~)Z*0KT1PdhZ0LrRT_G)!rV;Ja|4Q-^-;xE{GsYgU#IN+zrA1}x-Wq__(PrM*ZQ zhD`h^V|O&^_L<6GVb{H3Kpc^23&+H-4`l~KRS>`oYHlwv$@o$9-Ot`Af+B&Re-l;Q;I_;e{%Mk(ynOHbeZTMhe!K5w zM`&=MInA0zB9Y7`3&cxEBw$T}ME1_rE^EyD=CJ~FmQ(Jl;TvzN#Al0=0aPvta?rWwzqp6Z} zcgWX9D&#F$f6WdXS>i8>I73@rE`O0oQ`~gOvaaTyI?|JQtcu$LY!6vori%-WZCC8&)SGr5S!8wwoRwhrXak{r(r&)jg z?kQXsiZ4~sA#!Q&Iev)Xf({-TzQ;Gs|M5w8M4d+*-pmSR^MwaEbf(T704cj0q}^EKqk~yMh_R+SiL^jye@Dj2KiOkY88@1tIjpk zlaCekyV^)OTkT~$?b*2D`nf7gh@6v>jCbM7=RIzyY-t1{p?x>HAEufQq@C(XWuj8V z!cK<1S^Nb|-QB!bH^6`CNu$u$ZeEA4ac}vO0+EG1wcVxdf2&}K9M!r@XBaNGwScMh z>ihVdd|hndHVks^3(-g53XTa(ofQ$JUyGX(!!r@V`gq*Oh8UiAE2*=d?M(=ubS&AR zZ!+d^z0Gw&*k}RIR3_43BDT(7g9)iJCVE&UT1lao#qF93k)Il2?=f)f`0#(&JKq!G zK(}3M&0y-5#1MT7U7vx8n3-y>H1rDL;@>GFY-5D4z7JcD507DokLv%Q$S8Z_lwVIQ zVl?;>DJS+ly7-Ss34C*<1RG&*q3dBd04xW?!hn-^VUTh>cnq8HJ%Pw5`vWPvO8G7| zTo;r_4Y%5U(5lkobC>y=vLvq7P6aX013e2*HMx@DfWp@*5(BmF-Auu-#_sijB!mq< zQ}iV>Nfc#(fCF)*v%EVH5>(PSz;`4l`glp^jy{=n(JGS+H6G z>M-<+2T`&;<5hCQCNt*hLY|axUUsgQuF%!eZ8QMX%*cP6qK);vXzN?kmms E2iYG>J^%m! literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-sw600dp-xxhdpi/remote_left_white.png b/app/src/main/res/drawable-sw600dp-xxhdpi/remote_left_white.png new file mode 100644 index 0000000000000000000000000000000000000000..5fdac7a54d2e0303248ff86e373b55126bd33e53 GIT binary patch literal 2969 zcmd5;X;>3k7Okp;5KMxJs1yjw60loQ3Z+#R1q6v1$BZK#76-9}ao>W70o)1^gds8* z+octhaYLgv;@T==At4Z9#&K*qAd3)@Ehqs2H;{Q1qSJgh|2z5WOX|IM&pG$p`(9Pr z5|My2%6b$8K^)=2fTa)wU!^W41B{f3uqoh&5xGbZ01Z;#ocp!_wyi#W7n#df|mMDHKpoz`LS3k&K1YzDqe?F7MJ_Hbsem^ zXo2AC8$B;7-rRi_FV3Fh2A${TT~{P5soAkaJUhzgu;Nl&v z+p3$6J3@Uj%@w}SAG&{G?iq-k%qG#UWg(Ti?;LU~oVQ5nq(NK*_cZdIAtSg<}IyUb*-$&SeV<_ZqcGFA# zpFBC|mA2^BZ^Je6ogQk-YzD&EmZs_DKNod}g1Pl86S=?kaWF;RViv zvZr`^ffDZbikN+KkX2r4#V0-=LuZvR=m%s;eYscxFLg;0TEgv`YSYzcEBNQB#lj)7gjZQB#oKG zlu@()|I(ZWg*x%#E7^1dnmr?3SX*X`F8ccjx&QEx;x5xbeTFA#0w+=RVa;rEGH;#; z+%slpSOx0Cy54LqyOTUZ-Bs*0dCVWQh0(0fI4rO{gUX$W8Z72wh6>r?h6*)s3Y32P zfwv}fnSqvCbu+^+0;fswKe_>RFnoq;HUtTzN?>vez+F@Vb|ZiVj@WxScD4wG;3s_JSnm6(61_s83n(%z`OTj(?{XSt=3GGqM3W5sgnPA4u>w=b%*vM zbM)!wPVXi16&^!?51iizOy8Z&1XcK3RWbk6qHb|6+|ln6O}7aW*NM6h^S8uUZ+!Rg zg-0^sR-L-bSuH(O@Dvh#!!#tYCe;dI&}FxHGwEz5 zy2)`h6aD!^SR?b;41W@w74VrDB^zd;LsOw-1E)6e$xC~%3e-5Tf{Otu2aeg}4chny zX6qxaKCl7ukwo}WS*M6jg$ba+$6Wo{!ca=H{@8HV(_mx&#mAfT zIe;;g4Lj-_lokEQM^seRy;TIQ*XOTstCq)~~Se*q6){z^a%#7 z%@+9!IG`_1fU|k`=xtvt=3xTJP+;&>q6%C-@V1kQPi*$2cQ~2T*++Q!)l?Y5C{L>b z4w=Xn1CwhD4_hOFB%uQS%Ltf?KNdeYK}yH=WbY^H676xj$Th;kqfJ6kTVgYB!)(w} z8@1-_BRvNyxfsH5&j59&vkkog9&A)^v<+;j;z2l6w11Dk$;(W#NQ744@GhHq(V-DQO)uvO!eH62(*>$|n=%I@RooDv^)f5@P^ z%>gH?c=x1M`77A(H1)C9Ae#X3@;rs5>4$OJixwsgnY??!V%H9?1}i+LdOAq#`wYIioFF8(XY~vE)?n3iN)~gIPU?ETJHF^*V9gMa4&PufF^7K>1*E hGUs>sdI%9k11!ZX( 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-sw600dp-xxhdpi/remote_menu_white.png b/app/src/main/res/drawable-sw600dp-xxhdpi/remote_menu_white.png new file mode 100644 index 0000000000000000000000000000000000000000..f5019ff1e6fe589d7760b8e6c9008171cfc58d21 GIT binary patch literal 2906 zcmds3`(IMo7C-2ilh4R8F$*32Oy)F+TKInSKpitROC?Rn@aCPu!Xyk)gsEF#kxpad zBegQUIf4pgVJV1>N4-6isTmSYm@ecvpCe}EB@JKq&}V+R|G@oW&ffd8zk98<&)R#P z?^>Tzx-T)--e$cG008!JU*q-z077IQpRR%>|4Q`x5}sC(;$v~Zin-mCHCMor)p=j1 zQ{f#W=7HF?xt0zKtxv}#L|bd@*4Uw~0|Fxp0l;Q54!1LfJ~pi;v>yHYPWHb)Oz(~V zv0GQBcFkO!CkNUyWWSH@5BO zz$g0aJgVDWH$x!|qPJR0-Y}m~~XTF*b+7NpBkBzX|red2g|KdB3z5M~4-Jc!l^l`BqgsVzX zxcCDDp9$MG2KF3eHggevx9KO4{y>Zi25Fgf*RU3bZf2;{((W);ERtbe;3kr=9X*uu--&NyDV_B*4)P84NX@FXOBvlNLG`0^t|v-NI60lmw1s`E-U z>~>}dHJ8z0=RtCF7DES(E(Zp}4#+?|ORg#=x|MOv2woMVTYX_ao|1MhvxQ;g`C|l; zk=b>Jg9Nbb4R;A|Y*bz$XpMNPo-@B|n~eu)(uX_O<(_ukEsVW>TXy0B0=q@W-pejCIe{}cDE7AHbO8Xr5tZ~D&aWu3| z*`^$!90Z9Wt-+ZWI+Rn3Z>&Tt?LBoY-@V0m>^rWki9s(nFq*ms^p^VH=n0Hl47~A9 zeI;KQLQ>R=8wa+t-?_z+61}ZD^`Y#?XB4RWK>yEj6}PaS&)m3e7O?gv1`UP${@g3j|9yub z2!G=<3}DE}kiI~Qk1Y1}h4pCCndwphBr6OVcyLoY^@wrIb;aFau|_y_Nd+3OWhp_9w%%n!g=Jhsovgg*v`yA)b3mCtP~wTP(sfe2!9rzi7|S?D|Lvk@K|aB7ZI;+? zo8{gwaE{SuPw_^0Ma~#D6D3TI$!#-?O?*@iP8H+ybvmq+`_4tMC1-pJtF;oX=u{eyqUT)9sw|&K4rB5;TUfoeFGL71g7VMD%^Q`0#4XzFMd%9?S zo|}k(mxJP;O68@K{dfC9U*lULuI>RuC| zsXvt5nuuTN(sD~(dJ(*l*Oreu@h*x&8Qy+U|GE_FcN#1yGM;xfyLHHuZeq4~ol5(R z->B@q&7&sK9)bp8EBa7A*p9@oPg(0Fl`}K^s^R-@f>Lk%HnAv7!=DIS?t=7pV7ks* zN-i#0b}l9wTYW})3LigU5eRpu=WExg7V#i}%S}SNX?`vyai$_0P0c=&8c|=!r5L@F zaw5ld2RN);QqHAtItg=QZjyt5P$h$*-Gkhv(L_ZBqBp?n{R8Oe%YpmF^Izs+N`B4-L=ZP3gCOe* z#;h;Fgt^R}+$y(br^e{_8vDX{)TXZE18Mua3uQGx)<$H}9Y$Xwg@P{7(g{Y2EpKig zt>&XbU^=f(TkSwOzWe}sLgmeFNprIhz2T~pH{J;N{iu@SaAPdj(W;m38OSSQsMNcq z016LY5DPa*t}z63gdbcg*dMzG$#WHqdQDyhZSoC3_ND4c*p>hXll{!hlmCS`>$9HjS%>Lq{6LIakvd;b&a&?!S literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-sw600dp-xxhdpi/remote_right_black.png b/app/src/main/res/drawable-sw600dp-xxhdpi/remote_right_black.png new file mode 100644 index 0000000000000000000000000000000000000000..a7ee91c30ae6a8a4752a917e6801b351a6f20376 GIT binary patch literal 2557 zcmd5;X;2eq7|wWp$!f11qBetEw4d*0`LpPhHJ zYuQqN6QgNH6bi*eyu^1og#uNP7lRIv=B0cGaHGcs`1?|Z$?uV$D=>gePFfN!11H+a z3tBY0EDeB@z7q#6p4165VV2`J=U!TYu-#%`pU|{BPw#b|=#o*IUbM%;>nsLr<83Sa z_GsqZdpu~bIfxc!tJH~g1?I(tg;DFwIX66{iyPxwJqouxfc*mxLxiZLi~1fSadGBF{a^aM-kD_m`iGn;U&W2Gu`NlF;ng2}Zk2$Fj7?9!8d*jsI{4ib9@rF8D2QX99HaO={I_R6HF zCniUU15(3s?dSe@-n|tWCo7!f<#e`0bF=Nsg`KJ5LxCx|M-6F;@H;M9O-cu~RQyb) z#3OXDLV#I%C*LX7mOlt-g3}EBu6h&Zx@dt2N05*vCcn?VhH2?s zS|5XPB)B7gy(N`SQ>@QHeDEVh90`+ud^Lkbg8`hwYn*gnQEEPv?m~m9^i(JShU$y7 zJYN?_5zdw@Yhm&kteZ~c-gy|xLVS24yh>f0-CUx!8yptU;OHGB1R`5UN1iek;afb7 z9%)4z8v`%gZolFM!l^mPRKT`anavoE|4#!Q%8Gbj+MflHd5TiEOx1!8XMKuU9tQuz6P^z8}IstCCN^6+07vJg~Q%1c46adat{Ox;<$xkz+@7IUUAE)B_X*zzX zU=!!`)%GJ~<7g*Qjzl*D$RDU5UGPXBK*tnG#{0mTwk9l4gx{aa3f1@Tk8$*9ih2xU z8*8)OhGSGYh#8+*^>q%?aR$(rWpN}lnDhxU06zKQ2fZkRj1R5AD(lsg_xTYz;FIolYYwJ|6Ou#mS1u9+98kvVoNbWGwnwnQws1PUfnO#TlFR=o@aO-Rss6wnUiKg(>$FL8KyHMr%^gkb!>L#!dHDhv=6}6KdD# zPLb*@ViI&Vl{fOKkU@m}0P&F9V`WICpG$L9Pq*A;oryrQ=W?_iQ})aT6*P2k=gxY4 zlgwe9r35}UvF8FG18FXDG^Q`AGRJ1uhd76fk#M2;D*uM6GV?r=}tW^E%<^4 z|HUET zXkgD<63xvzZgJ3SfeBz~)VwK)Z#SRud=Pdu33iJR;w8tVE8yGJa98(BjC|jLcWd5R z@w3BMy5)sLU9FudgY|@nM;o}F?R~80zs^qwZ&^x!__O$ziLQr(`yO z*`+m8%?4-HpJ~j^H)Zl&uA5p8#i}DyUert}Mi*e(m=JnNsi8maz_aT(93PBYw=CSe n-aRVUxpx}%6Z}s?N|I?!^Xn_MUcVVvlRw3am-?PwB+2|2tju7d literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-sw600dp-xxhdpi/remote_right_white.png b/app/src/main/res/drawable-sw600dp-xxhdpi/remote_right_white.png new file mode 100644 index 0000000000000000000000000000000000000000..a0320f7c0628c59382e89083de1515022acc6af3 GIT binary patch literal 2658 zcmd5;4ObIa8lK6=B!v+{kPI4ZBAhB#OVC!MuzW+{w4z8O(rOq;tLve)M1c?i8ASOq z0!P_uYZOXtU86`uD{54RN=aO_?7EwUcF7hH3I!UBz=A|{?+l=(r$3;{nR7Gmyzlco z@5jB9;*BwqISvaPAPAZh9TmO_f?yqWaS>oK#)y`I1xbEe5)M71zEwBR90itFQlk>~ zfQUBgf{$OJ}CTJ-t|&VNy76cgWfxcn_}8;%YSc{lUNU(F5YPVMkCScgBHUlZis zC`>8*d*k|$e-#X*ep<1vXz`}4X}ZqNhK+5zN-lgHtZ`UT#0@QU-w@}2b!}9&Q?;AJ z`~_jvEL*y9ZAIwt=##6%eI1oH)y=AnD>dK#rOVuR?Lzh@Hc;>f{E&CA+p6@n>Erh- zxAu&--up7qKQ$COzhdh#y-gR?k)%w_NQTY}W%1W-n=3nNEVl-FpsW2GWM_)oE@qjh zF1xZis~H*XeIp+!bDN?Wx|U4O(EwS$rSRlZNbk`8&8gNg%TrAp8|T6Le~e5X;*~%{ zi*aSvzE70n4`O&?-(!OT#wG3(pH>2l$uSRZRA!<4y=u@EEj z^S7mAB|5CqcD>ZdA=;(ehgMwE%a z4oH*+@#P&OdNu=zhR>~Jp$y~;dd<4yiM@|=F&3Ka!9d{rEd@jfDfm=~x4VL1pMDpE zTNo+8xm%Wpk-FGh$w`AKoKJ*+I|1(8;w<7~O#p3Sw+rX!>PJO#keIWGwH(CHEZklv zb$liwYRn&D0>pqH%zC7zlwtA5&xycrlz9PflWX<(wAu+O+Rx!*A7B z_=}=GnCU_!$%jEv)dURSFVzQh<3}WX1qyrN0;)Eb)@1iW|4HUzFcY*ak|aROTtrmM zR^Ov*bJ2$~u++4jUVcE8xGs@G2nLPgVpQW82;g|EzpZ>~H2xiXPrlfD; zONo((^t|Y*+M2O43J>-w_VL-?^?EpdY**V^&2I^@1|U z#6K5rz=Ji%&JFlkLrKEBHQr?MYxXZ?0Pr;F)CX$olwb3o42|$6A99s zEVZI7R+nNSiAxiS(y2_f)lrg1z&^X;O-cjSpl}13b>GwO-L1At=m+Sdx@C(L(2}Hb z9?a~xyvc41ep6GQ#uh=`+@jCJvuh67oL2yNhI5q3RLP4Tz zI3GZFX#%YA6*p>!%NWcvcztuI5GvqNotpE_%OW?6KF0o{U=c52xjc`krifK#>OVxTgN;RjA? z7NUY-FHq=Uk}C9Z!1dP3SqDZW;35brE)`6a4w-d^em1r2t=Cs7{swuohl0#g6|M+= zmM|v>WV7#8B_3c5+Wu6PTVp#>@b4ZNmdCd3c#=^2>UV<$ayA}o&Jq2yt-v>J{C12R zu`P1wp~kxRiazH)2k(RR9p?J)oiEJ1-xqsj^j5g&P3HKMd?@9ycgB%g*SUIIohuXn zSm@txNipY)^tTq9*G`UupPdK(fd6tRN%;DwbGa!EXLM`!jZyc}5i#KxLh=3o2f`Gd AVgLXD literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-sw600dp-xxhdpi/remote_select_black.png b/app/src/main/res/drawable-sw600dp-xxhdpi/remote_select_black.png new file mode 100644 index 0000000000000000000000000000000000000000..e6920ffd6e291097d703b871472321367d435136 GIT binary patch literal 2013 zcmeAS@N?(olHy`uVBq!ia0y~yVB7%09Be?56MhC-K#DEN+ueoXKL{?^yL>WGgtNdS zvKXl2ItVj5Y0Rzw3bL1Y`ns~;;}PJI=L(%u+zJ%Q@^oFrUwdHEEvDJePU)P44kw(!XbRGIvtxq|`}MJy~+* z`%H?NR=`*qtsbee)9~m9K3{_!*FEp4|GX`7p(Wh&pQhsN8O{fy{+)eYJ87fl0!??% zdul&tJ(+IV_HjBVTd8K;g*Anh#|1Y$JM+j|KjZF}J+C<;f}g1|ytiTsTU;#MP%g{3 z`l2?Jp2&G%cV7deLIMLEaY!&g5EB~%3^g<`LJ+1ZpjzCjfXXq9gPR3pU>XNA3uq`@ zA*L#lErl3&prFs^fLw%Xq*|op#r-4U%VOU}SbUzyLu41`IGX%Xrm?If2Qy z8yXzh)kWtOU%FwYkZ|E(W$W`f=G%BVBuWf`CR9S?H>+HpcYemS;%`6C0o630R(U*c zo~_>ba*)GzdQK|0Wl4;gXI+?HQP-{}ai$fRdS*TGog{zrtbEa&X##V08uu5ZHvN;G zkzgS2`ynUHbDHNh&%DVw@*j;Cs~lHl`ponHZ(wivv+ew9FYZs|yurMg%l=IE9%G(U z@^|APrH2F1?bSAHvd`Io3-J^6jVCL|de8%`sy!*P3K=*G@Hun@$W%A$Z`)Tu^ z-$9ECW~Mwk_rXYJR)T@Jn#7guD)UdZ|J2Lz5{Xl{EKQdKx%?+zPgwouUvr-19Y4QU z`~2QQpvPYvUeqq|N3x~Wy1C)ty0w?y{$9L1Vmc&OGLC21e8w zX>e4}P@Va$=Gc}wo6ncCF*eJ^s7ZeR@}{tIAIM?nd?)RBt;q5C)16N1!=J594kqaN z7_7Lgl0WIsiM)Rk9&c%RaAt00i?;BW_Xk*-e`YkLZ}!^cv&pY$cFOOL`I4%ZdV(|3 zf4uL{+GD)G*1+*UDBaHp{gD{=cy40AK4vT_i-m^)j=+fzNBYB~3Yb=5sT5NcR^#9{ z0vV)PO003!mfN63-+v#5d!;OE+^$3UOTk6o)r_erxojD?c7ck?VlmHmXBd2UX}`B% zwlq1opl<5_g?oLAQus2ofO)*eytQF#&g3WAlQyfYKf`!6TK#AEp0@IYrgT$pBYz{` yqB$ukLKPpkdFFZVnD%k5_#mu$fKK|b=r8{U`BxXhnRKs$TWGgtNdS zvKXl2ItVj5Y0Rzw3bL1Y`ns~;;}PJIvzCo?`Ue!*@9E+gQgQ3;U0XlpNQt(K-~Fz* zC9#&=ifdH9!`B(X#F>y;3RvfUP+nd0c9)&IQuC_S{$;kY z_POdn;|C!)aJ@@V#_xJ*xM$v^Gm~RfX3pS}*}Gju-_y)<+R`aoCvx1-Y|WnaWFNn{ zLEdbY@}*PK7Yl5-_IRtuCaVRjx0c!VVOhc7D&iBiyrkZ=9JKVf$agG4%T* z*0Wac??2{*!wWe z`X1-?vEGVx&B}(-Wen;?3A#r&Ze^M-z$jqA06{D~3=qV~%m70U2N)m-Qx#AxZdE|# z7{137nqTfC%pS;bve&%_;HFA#x z3>Y7uh`)T7b3M>(3!D+vJWFlrx8kx#*=&r>jepl{ncpoOZ_FXV^9E?w&YT=Gg@g^w zvjyKhU$*h4gQ#7>CL#G}(O2KU`OdBM-1A?AvAkSF?Tqz64_R0F8F_!&)t)!8GSps%Voyf|<5j5#;|UVeV!*#edHU2XyG-W}{) z{WhKXCU@C;-}kjgCg;do&rkpT;qX+`&u^Jmdv^dsv7zCkAWly}6CAdPpS9VqX!euv zNt<)_cm7$86mR>!?V7aiwN>znca|q(C!J-uJxhJ3KhWX(Y&J}LbVDnF>DW2BNiph` zvIVoBdxm+6UF&J!oYAmx{pJ}r4=Dby=J2n!SS|T@mf(iwBK{t+yKgmFUT&{WKdX>n zu%)zZXZ@F%zHPN0Nj>0j)pg%7KQ(zXBV)6yO<#e1fA3kgRRxPT3h@_)7<+z-OSHG2 zv}f*zSwG(#c%{h4C~VO8$Mi0c4>Y@&QxHcsS&XYkv%2=j#C@TE&(-Z$D4O@A-}Ljt#;ff=xqu#8{9I+T z%KGXX-zq092c}!M1AM1dU7}2UH@)J&I_JIN-Y3=fy=+c?+xWS18^3)_r15>b4ZB)25}4RHB;*TmdIDF` z0gp*yGbff30b5>&mk6X6Fhm;nrecpIqRuTffh^fzR-3_1sIzz1P70U zihlSI{Ml6*fLxJ=h6Sy7N?A>z+t-Yq@dRlSG?2eP;^Le_jX zw)W71SHJ6t_4-5TuGFt29T%?dt1`8o>`pXJqS2yh4Q@pw)htN3(OyQAhw!g5L-%EL z@wuQ=&d+~7!Fj~Bo%=Gb_Add8-ufk{O*Bq~VV-%b(lk2w&9iRhvF(u@iB5f(bs|ks z@>bQ;bZkc~N5Z+jLbbDcJbL&173LB;E!tyCqD(ke99h?%uxhI>*JMPWrjJQFB!{oN zp740m)|YsopV{K2AHuT!HPtEaY)lTF&SQGnFYIUvN!_eZMB87jmJ?1#{eA2%b9eNJ zaWSU$dY#NuzqFGo=@`DT3frlAmfMx+tr=?$HTDr`obBb^+_q;gA14oHdCk`5Sy58fI@o25j)7QN~?lS1O_PeIC^mJ?vTX+&+MWSp@_t78Mq_VDg3sMg=BftjZ zXHi^zGuj2>{s{rD{TL4HB=i1U^a6ZP%0S`Nl1-2SSbM(j#8-!!x%&-D#4RYtnn*eq$t~J@#_CTbiqF1)cRI8*Z8+jcA znDy(%5*F$oj5k1CNxW~ZdH7)2`{C`AZ??Vf=FBjg;}h`rHgk|LvnA`oYI932hPds4 z-ndHS;AH!HxCCDTUcby`W+i`w(Bs3Py8#6+*a@1`SxeQeaE4);R{U=!EMO#K2KJX2 zTxGZb|5*g}DwK#c53Z^`B|wcx3jiz}LGi00AlTKb6+dsO zpAxxf1y!y9z=I%Dg~V)v>9eg00j_g}+#wZm(E)PJ9~b$D!Ai#h9@!J_)pSlPuG7Hg z8!+VJ-(XUE5Iwj2HIM9DD)Ep|5PEL4Ku6<{ zS_f*xQ~6c}gg6TWdnP#NuKb~glqzF^?5UaC%U!>cW1RNAr)W1qRGb_(nLLr4{?Z;A$!L&1|+1K(V)!dSfe;7%v%*(TazU zG9xIoG7M4RD44^YQDoT-o7R2BBPWSqL1z%V$nuE_Oj|FYThB5;Fhap2-wS}jH#sUq zF%KP(OA$pp1P-o6mOm*$`o=)Ibve|k)bPk^SXY@3fC~gbpg#SW8f5(rMEf8&U`p{W z1auhC{oX}Nt;+-cMk}@A3*aD0g$x1i*9K=DN=bPS4}1}V4F%DykEXkIP9vDeDUa~y zL0gT1&2qBlja1o`Z-LCbe}=REMX}}C)a+WTpAz9{L?6L)TJ2mE@$qr@M>__V?uu!; zhYQ>ETbziz7tPEeQEDe(#0&2FV~)hMkx;CJ+jiQQRG&DB?X+)tom2hWTCl>tmJm_J zt@~oX1K@+)w%BBv`6IS5#VY7rNxzKWBur{dp+uzLPx#}YcW)E7D$$A9SC;2UxY6e! zX*d~Ag72Od{ReUoAjt`MY010Trq-#I9oLEpi4QjvTYUPQrgMmTRrVppG+5y^>nVwn zdFv;O39U2p6ZA`~jkythm#Uh2Uy4#2M&*rN&uf@QZ!)p(irv<>kx3EP+9Yp|3Y}(+ zhF5F5Tkj{xo@jP>#ZCOUwQi^a-4!}5tYsy=ITiiC|7(ymx`C+KceuK7Wdk&e1_=Wj I{Utg71;SxuI{*Lx literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-sw600dp-xxhdpi/remote_up_white.png b/app/src/main/res/drawable-sw600dp-xxhdpi/remote_up_white.png new file mode 100644 index 0000000000000000000000000000000000000000..2e97410bf0b690e61a522198f735717b877c0312 GIT binary patch literal 2751 zcmd^Bi&v7_9{oTBpN;uSNoOpPnq(9ob$r46#yU}e_Y>FuoKSrS4Oi~ z*XG}$66-1}FwEQR;qPdYe;K-V&_4LbB26b%3UNom*wqBung$YbZQ0|~c(vUcbD76j z+b_dU{F0Hw%Oz2?>5*kdGYned!9Ngzqr{vx9WyZ!Ks(gSHzG%L-j5s9uWwMb=mZtK zrz@RM&o(ebly`Lmk?3Rj;!a_9A9;Sb1(^#NHPKWVIsz2tJ~!N-hP>yBJMOrn&{!T{ z#V3SN-f09$@*+w>$3g<4*@Vg!SB-rwF+p&A)$@e2cNmUH#tKQlSoZ>Lboi3ETW81l zaFuH^*{}aWSS`#;IiI8rsmMF(P^`{v+a%NU82nFDpYD_c$#hJH45{5W1s305lFrzQzWj>V&H5)U+X zL9V9?^_1YjC+#y4!uOzs-WEY&=yAiD@9}Bth;+JLyH$Kt=ZpG?+Eb?X7z_Urm332W zQwqlzRaBPesTgygE%-`Z@ZjNk(TPWM1FGi)b<*qh&ke~A1)63ARWzfWrVB-GpKfg} z*9;TX4#lG{3-DIt{c9LeqQ4~Zr*^v!tljC=8&_?Hub zlceoGY&6zYe#iIrW$1())=6!=+lMhPd1;M{DywEyeu9dclLqfn?+}>4=>)4Xjob^Y ztUBAh!K?Il0%(*_?MF`lOw!Z^K6ISn=sB?VPC9HfUz>)%U`eD~kw0z%h|3^)iN+e2 z-G9e-Yc3plF__U5JCp{nse|DDJ&gIKca9F(LHV^2uX#XBFn9w%la`>X0d#gRfHvyc z;C7vp^5flKZe-XPYaaNtt zLpK5WvqLw0&90?#xZh^+i;AGRius?Hro;ZPSHg$_YY_2nFVy}Gn%-E;9KEtftJwt+ z&mh9dA4H^^K}lW;p6lk-d%ZgkE9Z&2L9jSDeQA40wJ>--jAak^_@fB8JMTuMZ_!%w z;AIem0@bifP}O}Oo?BJRTu$CKJJrT#*$(aFq}|-3rSAi&9}br1P^BggEhj;$V|zK9 zWv>E(hgV<%s&N2@wxmigo{VzRLxA!+rljyuEUbA0k&Y3>)I=ngg8PLJWU~dcAjJ?I zgBPe=pUgc29eh8NA(UA5y)}s^XIk`5fv9}lRAO|hDDRB6t2x3U?K+qmql66`)IRSB7mKE11%cWogv z4>I`>Ay4I8i1o<249^wW!-j;GV?ZP5YsoI87@`rmKDZfm@8j5p zl+T&A@3Z)+BkbYR<3|mg7tgB4kAe(Wle9tt0j_J`8$x^dHfBsw=` zj**THHmZC1TK^rT;1%KAO7)k;qmgByvSZ~-JECulG3vMW_A_$1Fq7}z{wcClwa=391hA)q#hihY*O7npw^qvcYAC^zhy+q&dk@W$@w=^$`Q{1 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/drawer_image.png b/app/src/main/res/drawable-xhdpi/drawer_image.png new file mode 100644 index 0000000000000000000000000000000000000000..bacec5e757305cdf77ab173914d8d9952b549f1a GIT binary patch literal 437872 zcmXt<2Q-`i+s3W7HZ?i}fd=X>7>>Ewic%c6s!%@6$oCX@~dC>XfFAb+6Rb1z(+B z|2$8fIM>cQk8+H)(4-8qw*L7`CM@sqXodZg-@AB!8ILW);!N|(9667Enby6?owIQM z%e1SDs52>ykruin{A}QE_{G6Q|KM)p)leJ3@uKkx)y%;AN`F5j>L?`qaB9^?&NaDE z)SQQhclW{WekaXoW#NV$+ee{6rIw)Nz_f9yL8?rq{#s}3^}!)2Q~jHaFx5YSt*WbA z8+jV=2e7cgM_=!)$5!rSmr@l|kiBGJE0%XRo6jkQ81V3HE4F1a@|2;suyd4F;<2VLj_);&z==PZvUup zg9@I_q(b3)^d;4Tdl|E$1Qb;hc}MT$(M#zZ5Fjt-9P-wf_x`C4{aa5{xWxzx&Kw*1#JkTH6>E==5yhN00TkQ62BWzAx zyE>XXcPzKMjp>VPcx(p$-g%-(bgS~v6yX?!$qr#IoVC!U_Ktssx#?k1hcUjFyUM0a zazmii6OpTbdHXA+d0C!Xwf53Q&Re$g>rRq-8V`bbOF^E*8T3 z1Wrtb^?I?<#kYD^+sGV|BXB>1xXtx{sdOGU`Rov15@^Ysl!qVLsA+rp=-Q?;eTi!ibn zh=G~X!udXD1z1xbhuhv8cP`h_ABb#o{|Nzgk*;zXjhb4?+j?*Ns#V$N{$${em@{X& zrYXJ&jn1SUZ67-k+Nh;@;yYKXs@#1;w}3fE4yN+hMiTR0n>flaS8bc;TAuG~Hv&!C z5K(uS*bG+Qhj~`p*39_qQb-O-`OoexT!i6N!i*7C;m5ntO&=7u8`dC&J7HxtZnabU z({q=pju)raPq)(jxu`;c_X!NV$Y~J;-r)Qno&nl-%~lM@lV?-OmW=Pg69iY^tWNei zz0jh9V;`CLW(}PU_CMo_2i5mWP&zA4;F<8(7l<)xta%` z5ET>l4<3;7F@j5X#uV{BbfXGpuUQAfa^xuZZGP=tl$AK)gvqEqAzm2if7Pji6i7_9 z?-7ev;t_o@w3dTWlPN~mJEb<7NTeZ?HWsvg^XAXY9J=(H7iRDDU?}{sll}aV_fqA} zsx`%qV#5tIxOhW3=BUUkt~0%tmH-*FW2%tu5_sNHuCRRsVWZh^(H4uAVO!<06}#tM zRm%@I--i_T$c0n7xoc38@l&ANgKGtyOwI3G(KH>ZPw?1O4svwi_$Zi6Fe|iLX>_mt zQfTv03@3i?kJA>Sy+7mzL908AJ&ovw4Y9cQdNhB_O6&|UDv{fVxr45w$_gYOwO%dQ zWm<4Z&-Y2MFYS zW2gdAzi;`)U7#eE80)<7u9ZDpbNr1ubok!#gpF(H1u^ZrI#>U0KjeH